Skip to content

Commit

Permalink
Merge pull request #592 from no-chris/time-signature-in-chord-line
Browse files Browse the repository at this point in the history
Time signature in chord line
  • Loading branch information
no-chris committed Jan 7, 2023
2 parents 21b796b + 1ca55a6 commit 1ebf546
Show file tree
Hide file tree
Showing 28 changed files with 871 additions and 216 deletions.
1 change: 1 addition & 0 deletions packages/chord-mark-converters/README.md
Expand Up @@ -106,6 +106,7 @@ const parsed = parseSong(input);
const ultimateGuitar = renderSong(parsed, {
printBarSeparators: 'grids',
printChordsDuration: 'never',
printInlineTimeSignatures: false,
printSubBeatDelimiters: false,
customRenderer: chordMark2UltimateGuitar(),
chordSymbolRenderer: chordRendererFactory({
Expand Down
@@ -0,0 +1,17 @@
import _isString from 'lodash/isString';

export default class InvalidBarRepeatException extends Error {
constructor({ string } = {}) {
if (!string || !_isString(string)) {
throw new TypeError(
'InvalidBarRepeatException cannot be created without chord string, received: ' +
string
);
}

super();

this.name = 'InvalidBarRepeatException';
this.string = string;
}
}
17 changes: 13 additions & 4 deletions packages/chord-mark/src/parser/matchers/isChordLine.js
@@ -1,10 +1,17 @@
import _escapeRegExp from 'lodash/escapeRegExp';
import clearSpaces from '../helper/clearSpaces';

import syntax from '../syntax';
import isChord from './isChord';
import isTimeSignatureString from './isTimeSignatureString';

const chordBeatCountSymbols = new RegExp(syntax.chordBeatCount + '*$', 'g');
const barRepeatSymbols = new RegExp('^' + syntax.barRepeat + '+$');
const chordBeatCountSymbols = new RegExp(
_escapeRegExp(syntax.chordBeatCount) + '*$',
'g'
);
const barRepeatSymbols = new RegExp(
'^' + _escapeRegExp(syntax.barRepeat) + '+$'
);

/**
* Check if the given line only contains chords and allowed characters.
Expand All @@ -16,13 +23,15 @@ const barRepeatSymbols = new RegExp('^' + syntax.barRepeat + '+$');
export default function isChordLine(line = '') {
return clearSpaces(getParseableChordLine(line))
.split(' ')
.every((potentialChordToken, index) => {
.every((potentialChordToken, index, allTokens) => {
const clean = cleanToken(potentialChordToken);

return (
isChord(clean) ||
(potentialChordToken.match(barRepeatSymbols) && index > 0) ||
clean === syntax.noChord
clean === syntax.noChord ||
(isTimeSignatureString(potentialChordToken) &&
allTokens.length > 1)
);
});
}
Expand Down
160 changes: 97 additions & 63 deletions packages/chord-mark/src/parser/parseChordLine.js
@@ -1,21 +1,28 @@
/* eslint-disable max-lines-per-function */
import _isEqual from 'lodash/isEqual';
import _escapeRegExp from 'lodash/escapeRegExp';
import _cloneDeep from 'lodash/cloneDeep';

import syntax from './syntax';
import syntax, { defaultTimeSignature } from './syntax';
import clearSpaces from './helper/clearSpaces';

import isTimeSignatureString from './matchers/isTimeSignatureString';
import parseChord from './parseChord';
import parseTimeSignature from './parseTimeSignature';

import InvalidBeatCountException from './exceptions/InvalidBeatCountException';
import InvalidChordRepetitionException from './exceptions/InvalidChordRepetitionException';
import InvalidSubBeatGroupException from './exceptions/InvalidSubBeatGroupException';
import InvalidBarRepeatException from './exceptions/InvalidBarRepeatException';
import { getParseableChordLine, cleanToken } from './matchers/isChordLine';

const chordBeatCountSymbols = new RegExp(syntax.chordBeatCount, 'g');
const barRepeatSymbols = new RegExp('^' + syntax.barRepeat + '+$');
const defaultTimeSignature = parseTimeSignature('4/4');
const chordBeatCountSymbols = new RegExp(
_escapeRegExp(syntax.chordBeatCount),
'g'
);
const barRepeatSymbols = new RegExp(
'^' + _escapeRegExp(syntax.barRepeat) + '+$'
);

/**
* @typedef {Object} ChordLine
Expand All @@ -29,8 +36,11 @@ const defaultTimeSignature = parseTimeSignature('4/4');
* @type {Object}
* @property {TimeSignature} timeSignature
* @property {ChordLineChord[]} allChords
* @property {Boolean} isRepeated
* @property {Boolean} hasUnevenChordsDurations
* @property {Boolean} isRepeated - the bar has been created with the bar repeat symbol
* @property {Boolean} hasUnevenChordsDurations - the chords in the bar do not have the same duration
* @property {Boolean} lineHadTimeSignatureChange - there has been an inline time signature change.
* This value will be `true` for all the bars after the time signature change occurred,
* even if the TS is changed back again to the context one.
*/

/**
Expand All @@ -55,7 +65,7 @@ export default function parseChordLine(
chordLine,
{ timeSignature = defaultTimeSignature } = {}
) {
const { beatCount } = timeSignature;
let { beatCount } = timeSignature;

const allBars = [];
const emptyBar = { allChords: [] };
Expand All @@ -68,70 +78,22 @@ export default function parseChordLine(
let previousBar;
let isInSubBeatGroup = false;
let subBeatGroupIndex = 0;
let lineHadTimeSignatureChange = false;

checkSubBeatConsistency(chordLine);

const allTokens = clearSpaces(getParseableChordLine(chordLine)).split(' ');

allTokens.forEach((token, tokenIndex) => {
if (token.match(barRepeatSymbols)) {
if (previousBar) {
const repeatedBar = _cloneDeep(previousBar);
repeatedBar.isRepeated = true;

for (let i = 0; i < token.length; i++) {
allBars.push(_cloneDeep(repeatedBar));
}
} else {
throw new Error(
'A chord line cannot start with the barRepeat symbol' //todo: convert to own exception
);
}
repeatPreviousBars(token);
} else if (isTimeSignatureString(token)) {
changeTimeSignature(token);
} else {
if (token.startsWith(syntax.subBeatOpener)) {
isInSubBeatGroup = true;
}
if (isInSubBeatGroup) {
checkSubBeatGroupToken(chordLine, token);
updateSubBeatGroupsChordCount(token);
}

cleanedToken = cleanToken(token);
chord = {
string: token,
duration: getChordDuration(token, beatCount, isInSubBeatGroup),
model: isNoChordSymbol(cleanedToken)
? syntax.noChord
: parseChord(cleanedToken),
beat: currentBeatCount + 1,
isInSubBeatGroup,
};
currentBeatCount += chord.duration;

checkInvalidChordRepetition(bar, chord);

bar.allChords.push(chord);

if (token.endsWith(syntax.subBeatCloser)) {
checkSubBeatGroupChordCount(token);
isInSubBeatGroup = false;
subBeatGroupIndex++;
currentBeatCount += 1;
}
parseChordToken(token);

if (shouldChangeBar(currentBeatCount, beatCount)) {
bar.timeSignature = timeSignature;
bar.hasUnevenChordsDurations = hasUnevenChordsDurations(bar);
const barClone = _cloneDeep(bar);

bar.isRepeated = _isEqual(bar, previousBar);

allBars.push(_cloneDeep(bar));

previousBar = barClone;

bar = _cloneDeep(emptyBar);
currentBeatCount = 0;
changeBar();
} else {
checkInvalidBeatCount(
chord,
Expand All @@ -142,12 +104,69 @@ export default function parseChordLine(
}
}
});

setSubBeatInfo(allBars, subBeatGroupsChordCount);

return {
allBars,
};

function repeatPreviousBars(token) {
if (
currentBeatCount === 0 &&
previousBar &&
_isEqual(timeSignature, previousBar.timeSignature)
) {
const repeatedBar = _cloneDeep(previousBar);
repeatedBar.isRepeated = true;

for (let i = 0; i < token.length; i++) {
allBars.push(_cloneDeep(repeatedBar));
}
} else {
throw new InvalidBarRepeatException({ string: chordLine });
}
}

function changeTimeSignature(token) {
timeSignature = parseTimeSignature(token);
beatCount = timeSignature.beatCount;
lineHadTimeSignatureChange = true;
}

function parseChordToken(token) {
if (token.startsWith(syntax.subBeatOpener)) {
isInSubBeatGroup = true;
}
if (isInSubBeatGroup) {
checkSubBeatGroupToken(chordLine, token);
updateSubBeatGroupsChordCount(token);
}

cleanedToken = cleanToken(token);
chord = {
string: token,
duration: getChordDuration(token, beatCount, isInSubBeatGroup),
model: isNoChordSymbol(cleanedToken)
? syntax.noChord
: parseChord(cleanedToken),
beat: currentBeatCount + 1,
isInSubBeatGroup,
};
currentBeatCount += chord.duration;

checkInvalidChordRepetition(bar, chord);

bar.allChords.push(chord);

if (token.endsWith(syntax.subBeatCloser)) {
checkSubBeatGroupChordCount(token);
isInSubBeatGroup = false;
subBeatGroupIndex++;
currentBeatCount += 1;
}
}

function updateSubBeatGroupsChordCount() {
if (subBeatGroupsChordCount[subBeatGroupIndex]) {
subBeatGroupsChordCount[subBeatGroupIndex]++;
Expand All @@ -167,6 +186,22 @@ export default function parseChordLine(
position: 0, // duh
});
}

function changeBar() {
bar.timeSignature = timeSignature;
bar.lineHadTimeSignatureChange = lineHadTimeSignatureChange;
bar.hasUnevenChordsDurations = hasUnevenChordsDurations(bar);
const barClone = _cloneDeep(bar);

bar.isRepeated = _isEqual(bar, previousBar);

allBars.push(_cloneDeep(bar));

previousBar = barClone;

bar = _cloneDeep(emptyBar);
currentBeatCount = 0;
}
}

function checkSubBeatGroupToken(chordLine, token) {
Expand All @@ -180,8 +215,7 @@ function checkSubBeatGroupToken(chordLine, token) {
}

function hasBeatCount(token) {
const regex = new RegExp(syntax.chordBeatCount, 'g');
return (token.match(regex) || []).length > 0;
return token.indexOf(syntax.chordBeatCount) > -1;
}

function isNoChordSymbol(token) {
Expand Down
6 changes: 5 additions & 1 deletion packages/chord-mark/src/parser/syntax.js
@@ -1,6 +1,8 @@
import parseTimeSignature from './parseTimeSignature';

export default {
barRepeat: '%',
chordBeatCount: '\\.',
chordBeatCount: '.',
chordLineRepeat: '%',
chordPositionMarker: '_',
noChord: 'NC',
Expand All @@ -9,3 +11,5 @@ export default {
subBeatOpener: '{',
subBeatCloser: '}',
};

export const defaultTimeSignature = parseTimeSignature('4/4');
21 changes: 18 additions & 3 deletions packages/chord-mark/src/renderer/components/renderBarContent.js
@@ -1,6 +1,9 @@
import _isFinite from 'lodash/isFinite';

import symbols from '../symbols';

import renderChordSymbol from './renderChordSymbol';
import renderTimeSignature from './renderTimeSignature';
import barContentTpl from './tpl/barContent.js';

const space = ' ';
Expand All @@ -13,18 +16,30 @@ const defaultSpacesAfter = 2;
* @param {Boolean} isLastBar
* @param {Boolean} shouldPrintBarSeparators
* @param {Boolean} shouldPrintSubBeatDelimiters
* @param {Boolean} shouldPrintTimeSignature
* @returns {String} rendered html
*/
export default function renderBarContent(
bar,
isLastBar = false,
shouldPrintBarSeparators = true,
shouldPrintSubBeatDelimiters = true
{
shouldPrintBarSeparators = true,
shouldPrintSubBeatDelimiters = true,
shouldPrintTimeSignature = false,
} = {}
) {
let spacesWithin = 0;
let spacesAfter = 0;

const barContent = bar.allChords.reduce((rendering, chord, i) => {
let barContent = '';

if (shouldPrintTimeSignature) {
barContent +=
renderTimeSignature(bar.timeSignature) +
' '.repeat(symbols.spacesAfterTimeSignature);
}

barContent += bar.allChords.reduce((rendering, chord, i) => {
spacesWithin = _isFinite(chord.spacesWithin)
? chord.spacesWithin
: defaultSpacesWithin;
Expand Down

0 comments on commit 1ebf546

Please sign in to comment.