diff --git a/packages/chord-mark-converters/README.md b/packages/chord-mark-converters/README.md index 8a7fce31..325775a5 100644 --- a/packages/chord-mark-converters/README.md +++ b/packages/chord-mark-converters/README.md @@ -106,6 +106,7 @@ const parsed = parseSong(input); const ultimateGuitar = renderSong(parsed, { printBarSeparators: 'grids', printChordsDuration: 'never', + printInlineTimeSignatures: false, printSubBeatDelimiters: false, customRenderer: chordMark2UltimateGuitar(), chordSymbolRenderer: chordRendererFactory({ diff --git a/packages/chord-mark/src/parser/exceptions/InvalidBarRepeatException.js b/packages/chord-mark/src/parser/exceptions/InvalidBarRepeatException.js new file mode 100644 index 00000000..862ebb26 --- /dev/null +++ b/packages/chord-mark/src/parser/exceptions/InvalidBarRepeatException.js @@ -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; + } +} diff --git a/packages/chord-mark/src/parser/matchers/isChordLine.js b/packages/chord-mark/src/parser/matchers/isChordLine.js index 6e1920e6..b98ea3b6 100644 --- a/packages/chord-mark/src/parser/matchers/isChordLine.js +++ b/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. @@ -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) ); }); } diff --git a/packages/chord-mark/src/parser/parseChordLine.js b/packages/chord-mark/src/parser/parseChordLine.js index ce7e20f4..5972346f 100644 --- a/packages/chord-mark/src/parser/parseChordLine.js +++ b/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 @@ -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. */ /** @@ -55,7 +65,7 @@ export default function parseChordLine( chordLine, { timeSignature = defaultTimeSignature } = {} ) { - const { beatCount } = timeSignature; + let { beatCount } = timeSignature; const allBars = []; const emptyBar = { allChords: [] }; @@ -68,6 +78,7 @@ export default function parseChordLine( let previousBar; let isInSubBeatGroup = false; let subBeatGroupIndex = 0; + let lineHadTimeSignatureChange = false; checkSubBeatConsistency(chordLine); @@ -75,63 +86,14 @@ export default function parseChordLine( 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, @@ -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]++; @@ -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) { @@ -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) { diff --git a/packages/chord-mark/src/parser/syntax.js b/packages/chord-mark/src/parser/syntax.js index bf05a28c..4fcaf9fb 100644 --- a/packages/chord-mark/src/parser/syntax.js +++ b/packages/chord-mark/src/parser/syntax.js @@ -1,6 +1,8 @@ +import parseTimeSignature from './parseTimeSignature'; + export default { barRepeat: '%', - chordBeatCount: '\\.', + chordBeatCount: '.', chordLineRepeat: '%', chordPositionMarker: '_', noChord: 'NC', @@ -9,3 +11,5 @@ export default { subBeatOpener: '{', subBeatCloser: '}', }; + +export const defaultTimeSignature = parseTimeSignature('4/4'); diff --git a/packages/chord-mark/src/renderer/components/renderBarContent.js b/packages/chord-mark/src/renderer/components/renderBarContent.js index 80785a79..25e9b88a 100644 --- a/packages/chord-mark/src/renderer/components/renderBarContent.js +++ b/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 = ' '; @@ -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; diff --git a/packages/chord-mark/src/renderer/components/renderChordLine.js b/packages/chord-mark/src/renderer/components/renderChordLine.js index e50cb45e..080d27c4 100644 --- a/packages/chord-mark/src/renderer/components/renderChordLine.js +++ b/packages/chord-mark/src/renderer/components/renderChordLine.js @@ -1,28 +1,44 @@ +import _cloneDeep from 'lodash/cloneDeep'; +import _isEqual from 'lodash/isEqual'; + import chordLineTpl from './tpl/chordLine.js'; import renderBarContent from './renderBarContent'; import barSeparatorTpl from './tpl/barSeparator.js'; + +import { defaultTimeSignature } from '../../parser/syntax'; import symbols from '../symbols'; /** * @param {ChordLine} chordLineModel + * @param {TimeSignature} contextTimeSignature * @param {Boolean} shouldPrintBarSeparators * @param {Boolean} shouldPrintSubBeatDelimiters + * @param {Boolean} shouldPrintInlineTimeSignatures * @returns {String} rendered html */ export default function renderChordLine( chordLineModel, - shouldPrintBarSeparators, - shouldPrintSubBeatDelimiters = true + contextTimeSignature = defaultTimeSignature, + { + shouldPrintBarSeparators = true, + shouldPrintSubBeatDelimiters = true, + shouldPrintInlineTimeSignatures = true, + } = {} ) { + let previousTimeSignature = _cloneDeep(contextTimeSignature); const allBarsRendered = chordLineModel.allBars.map((bar, i) => { const isLastBar = !chordLineModel.allBars[i + 1]; - return renderBarContent( - bar, - isLastBar, + const shouldPrintTimeSignature = + shouldPrintInlineTimeSignatures && + !_isEqual(bar.timeSignature, previousTimeSignature); + const barContent = renderBarContent(bar, isLastBar, { shouldPrintBarSeparators, - shouldPrintSubBeatDelimiters - ); + shouldPrintSubBeatDelimiters, + shouldPrintTimeSignature, + }); + previousTimeSignature = _cloneDeep(bar.timeSignature); + return barContent; }); const barSeparator = shouldPrintBarSeparators diff --git a/packages/chord-mark/src/renderer/components/renderSong.js b/packages/chord-mark/src/renderer/components/renderSong.js index c601ea34..d7a5f651 100644 --- a/packages/chord-mark/src/renderer/components/renderSong.js +++ b/packages/chord-mark/src/renderer/components/renderSong.js @@ -1,3 +1,5 @@ +import _cloneDeep from 'lodash/cloneDeep'; + import getMaxBeatsWidth from '../spacers/chord/getMaxBeatsWidth'; import simpleChordSpacer from '../spacers/chord/simple'; @@ -23,6 +25,8 @@ import { chordRendererFactory } from 'chord-symbol'; import lineTypes from '../../parser/lineTypes'; import replaceRepeatedBars from '../replaceRepeatedBars'; +import { defaultTimeSignature } from '../../parser/syntax'; + /** * @param {Song} parsedSong * @param {('auto'|'flat'|'sharp')} accidentalsType @@ -41,6 +45,8 @@ import replaceRepeatedBars from '../replaceRepeatedBars'; * do not allow bar separators to be printed (e.g. Ultimate Guitar) * @param {Boolean} printSubBeatDelimiters - mainly useful when converting a ChordMark file to a format that * do not allow sub-beat groups to be printed (e.g. Ultimate Guitar) + * @param {Boolean} printInlineTimeSignatures - mainly useful when converting a ChordMark file to a format that + * do not allow inline time signatures to be printed (e.g. Ultimate Guitar) * @param {Number} transposeValue * @param {Boolean} useShortNamings * @returns {String} rendered HTML @@ -61,7 +67,8 @@ export default function renderSong( harmonizeAccidentals = true, printChordsDuration = 'uneven', printBarSeparators = 'always', - printSubBeatDelimiters = true, + printSubBeatDelimiters: shouldPrintSubBeatDelimiters = true, + printInlineTimeSignatures: shouldPrintInlineTimeSignatures = true, simplifyChords = 'none', transposeValue = 0, useShortNamings = true, @@ -80,8 +87,8 @@ export default function renderSong( const maxBeatsWidth = getMaxBeatsWidth( allLines, - shouldAlignChords, - printSubBeatDelimiters + shouldAlignChordsWithLyrics, + shouldPrintSubBeatDelimiters ); allLines = renderAllSectionsLabels(allLines, { @@ -189,22 +196,22 @@ export default function renderSong( function spaceChordLine(line, lineIndex) { if (line.type === lineTypes.CHORD) { let spaced = - alignBars && !shouldAlignChords(line) + alignBars && !shouldAlignChordsWithLyrics(line) ? alignedChordSpacer( line.model, maxBeatsWidth, shouldPrintBarSeparators(line.model), - printSubBeatDelimiters + shouldPrintSubBeatDelimiters ) : simpleChordSpacer(line.model); const nextLine = allLines[lineIndex + 1]; - if (shouldAlignChords(line)) { + if (shouldAlignChordsWithLyrics(line)) { const { chordLine, lyricsLine } = chordLyricsSpacer( spaced, nextLine.model, shouldPrintBarSeparators(line.model), - printSubBeatDelimiters + shouldPrintSubBeatDelimiters ); allLines[lineIndex + 1].model = lyricsLine; spaced = chordLine; @@ -214,21 +221,26 @@ export default function renderSong( } function renderAllLines() { + let timeSignature = defaultTimeSignature; + return allLines .map((line) => { let rendered; if (line.type === lineTypes.CHORD) { - rendered = renderChordLineModel( - line.model, - shouldPrintBarSeparators(line.model), - printSubBeatDelimiters - ); + rendered = renderChordLineModel(line.model, timeSignature, { + shouldPrintBarSeparators: shouldPrintBarSeparators( + line.model + ), + shouldPrintSubBeatDelimiters, + shouldPrintInlineTimeSignatures, + }); } else if (line.type === lineTypes.EMPTY_LINE) { rendered = renderEmptyLine(); } else if (line.type === lineTypes.SECTION_LABEL) { rendered = renderSectionLabelLine(line); } else if (line.type === lineTypes.TIME_SIGNATURE) { + timeSignature = _cloneDeep(line.model); rendered = renderTimeSignature(line); } else { rendered = renderLyricLine(line, { @@ -246,7 +258,7 @@ export default function renderSong( .filter(Boolean); } - function shouldAlignChords(line) { + function shouldAlignChordsWithLyrics(line) { return ( chartType === 'all' && alignChordsWithLyrics && diff --git a/packages/chord-mark/src/renderer/spacers/chord/aligned.js b/packages/chord-mark/src/renderer/spacers/chord/aligned.js index a5050134..9b0b3786 100644 --- a/packages/chord-mark/src/renderer/spacers/chord/aligned.js +++ b/packages/chord-mark/src/renderer/spacers/chord/aligned.js @@ -1,6 +1,7 @@ import _cloneDeep from 'lodash/cloneDeep'; import symbols from '../../symbols'; import { getBeatString } from './getBeatString'; +import { spaceBar } from './simple'; /** * @param {ChordLine} chordLineInput @@ -18,32 +19,36 @@ export default function space( const chordLine = _cloneDeep(chordLineInput); chordLine.allBars.forEach((bar, barIndex) => { - bar.allChords.forEach((chord) => { - const beatString = getBeatString( - bar, - chord.beat, - shouldPrintSubBeatDelimiters - ); + if (bar.lineHadTimeSignatureChange) { + spaceBar(bar); + } else { + bar.allChords.forEach((chord) => { + const beatString = getBeatString( + bar, + chord.beat, + shouldPrintSubBeatDelimiters + ); - if (chord.isInSubBeatGroup && !chord.isLastOfSubBeat) { - chord.spacesWithin = 0; - chord.spacesAfter = symbols.spacesAfterSubBeatDefault; - } else { - chord.spacesWithin = - maxBeatsWidth[barIndex][chord.beat] - beatString.length; - chord.spacesAfter = 0; - } + if (chord.isInSubBeatGroup && !chord.isLastOfSubBeat) { + chord.spacesWithin = 0; + chord.spacesAfter = symbols.spacesAfterSubBeatDefault; + } else { + chord.spacesWithin = + maxBeatsWidth[barIndex][chord.beat] - beatString.length; + chord.spacesAfter = 0; + } - if (shouldFillEmptyBeats(bar, chord)) { - chord.spacesAfter = - symbols.spacesAfterDefault + - getEmptyBeatsWidth(bar, chord, maxBeatsWidth[barIndex]); - } + if (shouldFillEmptyBeats(bar, chord)) { + chord.spacesAfter = + symbols.spacesAfterDefault + + getEmptyBeatsWidth(bar, chord, maxBeatsWidth[barIndex]); + } - if (shouldSpaceLastBeat(bar, chord, shouldPrintBarSeparators)) { - chord.spacesAfter = symbols.spacesAfterDefault; - } - }); + if (shouldSpaceLastBeat(bar, chord, shouldPrintBarSeparators)) { + chord.spacesAfter = symbols.spacesAfterDefault; + } + }); + } }); return chordLine; diff --git a/packages/chord-mark/src/renderer/spacers/chord/chordLyrics.js b/packages/chord-mark/src/renderer/spacers/chord/chordLyrics.js index d5af2539..fce4641c 100644 --- a/packages/chord-mark/src/renderer/spacers/chord/chordLyrics.js +++ b/packages/chord-mark/src/renderer/spacers/chord/chordLyrics.js @@ -40,10 +40,10 @@ export default function space( let chordToken; let lyricToken; - let currentBarIndex = 0; + let currentBarIndex = -1; chordLine.allBars.forEach((bar, barIndex) => { - bar.allChords.forEach((chord, chordIndex) => { + bar.allChords.forEach((chord) => { lyricToken = tokenizedLyrics.shift(); if (lyricToken) { @@ -53,9 +53,7 @@ export default function space( shouldPrintSubBeatDelimiters ); - if (isFirstChord(barIndex, chordIndex)) { - chordToken = barSeparatorToken + chordToken; - } else if (isNewBar(currentBarIndex, barIndex)) { + if (isNewBar(currentBarIndex, barIndex)) { chordToken = barSeparatorToken + chordToken; currentBarIndex = barIndex; } @@ -107,9 +105,6 @@ const hasNoPositionMarkers = (lyricsLine) => const shouldOffsetChordLine = (lyricsLine) => lyricsLine.chordPositions[0] > 0; -const isFirstChord = (barIndex, chordIndex) => - barIndex === 0 && chordIndex === 0; - const isNewBar = (currentBarIndex, barIndex) => currentBarIndex !== barIndex; // source: https://github.com/es-shims/String.prototype.trimEnd/blob/main/implementation.js diff --git a/packages/chord-mark/src/renderer/spacers/chord/getMaxBeatsWidth.js b/packages/chord-mark/src/renderer/spacers/chord/getMaxBeatsWidth.js index aecc4970..01957324 100644 --- a/packages/chord-mark/src/renderer/spacers/chord/getMaxBeatsWidth.js +++ b/packages/chord-mark/src/renderer/spacers/chord/getMaxBeatsWidth.js @@ -4,47 +4,49 @@ import lineTypes from '../../../parser/lineTypes'; /** * @param {SongLine[]} allLines - * @param {Function} shouldAlignChords + * @param {Function} shouldAlignChordsWithLyrics * @param {Boolean} shouldPrintSubBeatDelimiters * @returns {Array} */ export default function getMaxBeatsWidth( allLines, - shouldAlignChords, + shouldAlignChordsWithLyrics, shouldPrintSubBeatDelimiters = true ) { const maxBeatsWidth = []; allLines .filter((line) => line.type === lineTypes.CHORD) - .filter((line) => !shouldAlignChords(line)) + .filter((line) => !shouldAlignChordsWithLyrics(line)) .forEach((line) => { - line.model.allBars.forEach((bar, barIndex) => { - if (!maxBeatsWidth[barIndex]) { - maxBeatsWidth[barIndex] = {}; + line.model.allBars + .filter((bar) => !bar.lineHadTimeSignatureChange) + .forEach((bar, barIndex) => { + if (!maxBeatsWidth[barIndex]) { + maxBeatsWidth[barIndex] = {}; - for (let i = 1; i <= bar.timeSignature.beatCount; i++) { - maxBeatsWidth[barIndex][i] = 0; + for (let i = 1; i <= bar.timeSignature.beatCount; i++) { + maxBeatsWidth[barIndex][i] = 0; + } } - } - bar.allChords - .filter( - (chord) => - !chord.isInSubBeatGroup || chord.isLastOfSubBeat - ) - .forEach((chord) => { - const beatString = getBeatString( - bar, - chord.beat, - shouldPrintSubBeatDelimiters - ); - maxBeatsWidth[barIndex][chord.beat] = Math.max( - maxBeatsWidth[barIndex][chord.beat], - beatString.length - ); - }); - }); + bar.allChords + .filter( + (chord) => + !chord.isInSubBeatGroup || chord.isLastOfSubBeat + ) + .forEach((chord) => { + const beatString = getBeatString( + bar, + chord.beat, + shouldPrintSubBeatDelimiters + ); + maxBeatsWidth[barIndex][chord.beat] = Math.max( + maxBeatsWidth[barIndex][chord.beat], + beatString.length + ); + }); + }); }); return maxBeatsWidth; diff --git a/packages/chord-mark/src/renderer/spacers/chord/simple.js b/packages/chord-mark/src/renderer/spacers/chord/simple.js index 02dcfd94..ba56e525 100644 --- a/packages/chord-mark/src/renderer/spacers/chord/simple.js +++ b/packages/chord-mark/src/renderer/spacers/chord/simple.js @@ -9,14 +9,18 @@ export default function space(chordLineInput) { const chordLine = _cloneDeep(chordLineInput); chordLine.allBars.forEach((bar) => { - bar.allChords.forEach((chord) => { - chord.spacesAfter = - chord.isInSubBeatGroup && !chord.isLastOfSubBeat - ? symbols.spacesAfterSubBeatDefault - : symbols.spacesAfterDefault; - chord.spacesWithin = 0; - }); + spaceBar(bar); }); return chordLine; } + +export function spaceBar(bar) { + bar.allChords.forEach((chord) => { + chord.spacesAfter = + chord.isInSubBeatGroup && !chord.isLastOfSubBeat + ? symbols.spacesAfterSubBeatDefault + : symbols.spacesAfterDefault; + chord.spacesWithin = 0; + }); +} diff --git a/packages/chord-mark/src/renderer/symbols.js b/packages/chord-mark/src/renderer/symbols.js index 5ac4b6df..029a2185 100644 --- a/packages/chord-mark/src/renderer/symbols.js +++ b/packages/chord-mark/src/renderer/symbols.js @@ -8,6 +8,7 @@ export default { noChordSymbol: 'NC', spacesAfterDefault: 2, spacesAfterSubBeatDefault: 1, + spacesAfterTimeSignature: 1, subBeatGroupOpener: '{', subBeatGroupCloser: '}', }; diff --git a/packages/chord-mark/tests/integration/parser/parseSong.spec.js b/packages/chord-mark/tests/integration/parser/parseSong.spec.js index 612b7325..bc7d7213 100644 --- a/packages/chord-mark/tests/integration/parser/parseSong.spec.js +++ b/packages/chord-mark/tests/integration/parser/parseSong.spec.js @@ -52,6 +52,7 @@ Let it _be _ _ _`; timeSignature: _cloneDeep(ts4_4), isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], hasPositionedChords: false, @@ -120,6 +121,7 @@ Let it _be _ _ _`; timeSignature: _cloneDeep(ts4_4), isRepeated: false, hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: false, }, ], hasPositionedChords: true, @@ -161,6 +163,7 @@ Let it _be _ _ _`; timeSignature: _cloneDeep(ts4_4), isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], hasPositionedChords: false, @@ -221,6 +224,7 @@ Let it _be _ _ _`; timeSignature: _cloneDeep(ts4_4), isRepeated: false, hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: false, }, ], hasPositionedChords: true, diff --git a/packages/chord-mark/tests/integration/renderer/components/renderSong/data/song4-input.txt b/packages/chord-mark/tests/integration/renderer/components/renderSong/data/song4-input.txt new file mode 100644 index 00000000..e0674471 --- /dev/null +++ b/packages/chord-mark/tests/integration/renderer/components/renderSong/data/song4-input.txt @@ -0,0 +1,19 @@ +4/4 + +#i +A.. Dsus2.. A +Good _morning, good _morning, good _morning, ah! + +#v +5/4 A... G.. G... A.. +_Nothing to do to _save his life, _call his wife _in, +5/4 A... G.. 3/4 G 4/4 A +_Nothing to say, but, "_What a day! _How's your boy _been?" +5/4 D 4/4 E.. E7.. +_Nothing to do, it's up to _you, +3/4 A G +I've got _nothing to say, but _it's okay, + +#c +A.. Dsus2.. A +_Good morning, good _morning, good _morning, ah! diff --git a/packages/chord-mark/tests/integration/renderer/components/renderSong/data/song4-output-inline-time-signatures.txt b/packages/chord-mark/tests/integration/renderer/components/renderSong/data/song4-output-inline-time-signatures.txt new file mode 100644 index 00000000..535b62f8 --- /dev/null +++ b/packages/chord-mark/tests/integration/renderer/components/renderSong/data/song4-output-inline-time-signatures.txt @@ -0,0 +1,19 @@ +4/4 + +Intro + |A Dsus2 |A | +Good morning, good morning, good morning, ah! + +Verse +|5/4 A... G.. |G... A.. | +Nothing to do to save his life, call his wife in, +|5/4 A... G.. |3/4 G |4/4 A | +Nothing to say, but, "What a day! How's your boy been?" +|5/4 D |4/4 E E7 | +Nothing to do, it's up to you, + |3/4 A |G | +I've got nothing to say, but it's okay, + +Chorus +|A Dsus2 |A | +Good morning, good morning, good morning, ah! diff --git a/packages/chord-mark/tests/integration/renderer/components/renderSong/data/song4-output-no-inline-time-signatures.txt b/packages/chord-mark/tests/integration/renderer/components/renderSong/data/song4-output-no-inline-time-signatures.txt new file mode 100644 index 00000000..8d559966 --- /dev/null +++ b/packages/chord-mark/tests/integration/renderer/components/renderSong/data/song4-output-no-inline-time-signatures.txt @@ -0,0 +1,19 @@ +4/4 + +Intro + |A Dsus2 |A | +Good morning, good morning, good morning, ah! + +Verse +|A... G.. |G... A.. | +Nothing to do to save his life, call his wife in, +|A... G.. |G |A | +Nothing to say, but, "What a day! How's your boy been?" +|D |E E7 | +Nothing to do, it's up to you, + |A |G | +I've got nothing to say, but it's okay, + +Chorus +|A Dsus2 |A | +Good morning, good morning, good morning, ah! diff --git a/packages/chord-mark/tests/integration/renderer/components/renderSong/renderSong.spec.js b/packages/chord-mark/tests/integration/renderer/components/renderSong/renderSong.spec.js index 5897818f..6dc1b3b9 100644 --- a/packages/chord-mark/tests/integration/renderer/components/renderSong/renderSong.spec.js +++ b/packages/chord-mark/tests/integration/renderer/components/renderSong/renderSong.spec.js @@ -103,6 +103,18 @@ describe.each([ 'song3-output-no-sub-beats.txt', { printSubBeatDelimiters: false }, ], + [ + 'sub-beat delimiters', + 'song4-input.txt', + 'song4-output-inline-time-signatures.txt', + { printInlineTimeSignatures: true }, + ], + [ + 'sub-beat delimiters', + 'song4-input.txt', + 'song4-output-no-inline-time-signatures.txt', + { printInlineTimeSignatures: false }, + ], ])('Render components: %s', (title, inputFile, outputFile, options) => { test('produces expected rendering', () => { const input = removeLastLine( diff --git a/packages/chord-mark/tests/unit/parser/exceptions/InvalidBarRepeatException.spec.js b/packages/chord-mark/tests/unit/parser/exceptions/InvalidBarRepeatException.spec.js new file mode 100644 index 00000000..e9034ae6 --- /dev/null +++ b/packages/chord-mark/tests/unit/parser/exceptions/InvalidBarRepeatException.spec.js @@ -0,0 +1,48 @@ +import InvalidBarRepeatException from '../../../../src/parser/exceptions/InvalidBarRepeatException'; + +describe('InvalidBarRepeatException', () => { + test('Module', () => { + expect(InvalidBarRepeatException).toBeInstanceOf(Function); + }); +}); + +describe('Behavior', () => { + test('Correctly fills exception properties', () => { + const error = new InvalidBarRepeatException({ + string: 'Cm7... %', + }); + expect(error).toBeInstanceOf(InvalidBarRepeatException); + expect(error.string).toEqual('Cm7... %'); + }); + + test('Throw if given no parameter', () => { + const throwingFn = () => { + throw new InvalidBarRepeatException(); + }; + expect(throwingFn).toThrow(TypeError); + expect(throwingFn).toThrow( + 'InvalidBarRepeatException cannot be created without chord string, received: undefined' + ); + }); +}); + +describe.each([ + [ + 'no string', + 'string', + 'InvalidBarRepeatException cannot be created without chord string, received: undefined', + ], +])('Throw TypeError on %s', (title, propertyToRemove, message) => { + test('Test details', () => { + const errorParameters = { + string: 'Cm7... %', + }; + delete errorParameters[propertyToRemove]; + + const throwingFn = () => { + throw new InvalidBarRepeatException(errorParameters); + }; + expect(throwingFn).toThrow(TypeError); + expect(throwingFn).toThrow(message); + }); +}); diff --git a/packages/chord-mark/tests/unit/parser/matchers/isChordLine.spec.js b/packages/chord-mark/tests/unit/parser/matchers/isChordLine.spec.js index 53c9670f..d29f77c0 100644 --- a/packages/chord-mark/tests/unit/parser/matchers/isChordLine.spec.js +++ b/packages/chord-mark/tests/unit/parser/matchers/isChordLine.spec.js @@ -37,6 +37,17 @@ describe.each([ ['F {C A} {B', true], ['F {C A(add b9)} B}', true], + // time signature in chord line + ['2/4 A', true], + ['A 2/4', true], + ['A B 2/4 C', true], + ['A B C 2/4', true], + ['A B C 2/4 3/4', true], + ['A B C 2/4 3/4 B', true], + ['A B C 2/4 C 3/4 B', true], + ['A % 2/4', true], + ['A 2/4 %', true], // duh! This is rejected at parsing time + [undefined, false], ['', false], ['AB ', false], @@ -52,6 +63,8 @@ describe.each([ ['%..', false], ['A B %.', false], ['5/4\n', false], + ['4/4', false], + ['3/4', false], ['A B{', false], ['A }B', false], ['A { B', false], diff --git a/packages/chord-mark/tests/unit/parser/parseChordLine.spec.js b/packages/chord-mark/tests/unit/parser/parseChordLine.spec.js index 89c7daf3..99efc9c2 100644 --- a/packages/chord-mark/tests/unit/parser/parseChordLine.spec.js +++ b/packages/chord-mark/tests/unit/parser/parseChordLine.spec.js @@ -9,6 +9,7 @@ import parseTimeSignature from '../../../src/parser/parseTimeSignature'; import InvalidBeatCountException from '../../../src/parser/exceptions/InvalidBeatCountException'; import InvalidChordRepetitionException from '../../../src/parser/exceptions/InvalidChordRepetitionException'; import InvalidSubBeatGroupException from '../../../src/parser/exceptions/InvalidSubBeatGroupException'; +import InvalidBarRepeatException from '../../../src/parser/exceptions/InvalidBarRepeatException'; import { forEachChordInChordLine } from '../../../src/parser/helper/songs'; @@ -18,6 +19,7 @@ describe('parseChordLine', () => { }); }); +const ts2_4 = parseTimeSignature('2/4'); const ts3_4 = parseTimeSignature('3/4'); const ts4_4 = parseTimeSignature('4/4'); const ts5_4 = parseTimeSignature('5/4'); @@ -46,6 +48,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -77,6 +80,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -115,6 +119,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: false, }, ], }, @@ -160,6 +165,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -184,6 +190,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -198,6 +205,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -222,6 +230,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -243,6 +252,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -274,6 +284,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -295,6 +306,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: false, }, ], }, @@ -319,6 +331,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -340,6 +353,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -354,6 +368,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -378,6 +393,7 @@ describe.each([ timeSignature: ts3_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -409,6 +425,7 @@ describe.each([ timeSignature: ts3_4, isRepeated: false, hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: false, }, ], }, @@ -440,6 +457,7 @@ describe.each([ timeSignature: ts3_4, isRepeated: false, hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: false, }, ], }, @@ -478,6 +496,7 @@ describe.each([ timeSignature: ts3_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -502,6 +521,7 @@ describe.each([ timeSignature: ts3_8, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -516,6 +536,7 @@ describe.each([ timeSignature: ts3_8, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -530,6 +551,7 @@ describe.each([ timeSignature: ts3_8, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -554,6 +576,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -578,6 +601,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -602,6 +626,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -633,6 +658,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -657,6 +683,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -671,6 +698,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -692,6 +720,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -706,6 +735,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -730,6 +760,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -744,6 +775,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: true, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -758,6 +790,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -772,6 +805,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: true, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -810,6 +844,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -838,6 +873,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: true, hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: false, }, ], }, @@ -862,6 +898,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, { allChords: [ @@ -876,6 +913,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, }, ], }, @@ -917,6 +955,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: false, }, ], }, @@ -967,6 +1006,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: false, }, ], }, @@ -1026,6 +1066,7 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: false, }, ], }, @@ -1094,6 +1135,228 @@ describe.each([ timeSignature: ts4_4, isRepeated: false, hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: false, + }, + ], + }, + ], + [ + 'Inline time signature / 1 change', + 'C 3/4 D.. E.', + ts4_4, + { + allBars: [ + { + allChords: [ + { + string: 'C', + model: { symbol: 'C' }, + duration: 4, + beat: 1, + isInSubBeatGroup: false, + }, + ], + timeSignature: ts4_4, + isRepeated: false, + hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, + }, + { + allChords: [ + { + string: 'D..', + model: { symbol: 'D' }, + duration: 2, + beat: 1, + isInSubBeatGroup: false, + }, + { + string: 'E.', + model: { symbol: 'E' }, + duration: 1, + beat: 3, + isInSubBeatGroup: false, + }, + ], + timeSignature: ts3_4, + isRepeated: false, + hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: true, + }, + ], + }, + ], + [ + 'Inline time signature / 2 changes', + 'C 3/4 D.. E. 5/4 F... C..', + ts4_4, + { + allBars: [ + { + allChords: [ + { + string: 'C', + model: { symbol: 'C' }, + duration: 4, + beat: 1, + isInSubBeatGroup: false, + }, + ], + timeSignature: ts4_4, + isRepeated: false, + hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, + }, + { + allChords: [ + { + string: 'D..', + model: { symbol: 'D' }, + duration: 2, + beat: 1, + isInSubBeatGroup: false, + }, + { + string: 'E.', + model: { symbol: 'E' }, + duration: 1, + beat: 3, + isInSubBeatGroup: false, + }, + ], + timeSignature: ts3_4, + isRepeated: false, + hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: true, + }, + { + allChords: [ + { + string: 'F...', + model: { symbol: 'F' }, + duration: 3, + beat: 1, + isInSubBeatGroup: false, + }, + { + string: 'C..', + model: { symbol: 'C' }, + duration: 2, + beat: 4, + isInSubBeatGroup: false, + }, + ], + timeSignature: ts5_4, + isRepeated: false, + hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: true, + }, + ], + }, + ], + [ + 'Inline time signature / 3 consecutive changes', // useless, but can be parsed + 'C 3/4 D.. E. 2/4 4/4 5/4 F... C..', + ts4_4, + { + allBars: [ + { + allChords: [ + { + string: 'C', + model: { symbol: 'C' }, + duration: 4, + beat: 1, + isInSubBeatGroup: false, + }, + ], + timeSignature: ts4_4, + isRepeated: false, + hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: false, + }, + { + allChords: [ + { + string: 'D..', + model: { symbol: 'D' }, + duration: 2, + beat: 1, + isInSubBeatGroup: false, + }, + { + string: 'E.', + model: { symbol: 'E' }, + duration: 1, + beat: 3, + isInSubBeatGroup: false, + }, + ], + timeSignature: ts3_4, + isRepeated: false, + hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: true, + }, + { + allChords: [ + { + string: 'F...', + model: { symbol: 'F' }, + duration: 3, + beat: 1, + isInSubBeatGroup: false, + }, + { + string: 'C..', + model: { symbol: 'C' }, + duration: 2, + beat: 4, + isInSubBeatGroup: false, + }, + ], + timeSignature: ts5_4, + isRepeated: false, + hasUnevenChordsDurations: true, + lineHadTimeSignatureChange: true, + }, + ], + }, + ], + [ + 'Inline time signature / 2 changes, 1 at the start', + '2/4 G 4/4 G°', + ts4_4, + { + allBars: [ + { + allChords: [ + { + string: 'G', + model: { symbol: 'G' }, + duration: 2, + beat: 1, + isInSubBeatGroup: false, + }, + ], + timeSignature: ts2_4, + isRepeated: false, + hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: true, + }, + { + allChords: [ + { + string: 'G°', + model: { symbol: 'G°' }, + duration: 4, + beat: 1, + isInSubBeatGroup: false, + }, + ], + timeSignature: ts4_4, + isRepeated: false, + hasUnevenChordsDurations: false, + lineHadTimeSignatureChange: true, }, ], }, @@ -1232,21 +1495,26 @@ describe.each([ }); }); -describe.each([['% A'], ['%% A'], [' % A'], [' % A']])( - 'Throw if line starts with repeatBar symbol: %s', - (input) => { - const throwingFn = () => { - parseChordLine(input); - }; +describe.each([ + ['no bar to repeat (1)', '% A'], + ['no bar to repeat (2)', '%% A'], + ['no bar to repeat (3)', ' % A'], + ['no bar to repeat (4)', ' % A'], + ['no bar to repeat (5)', '3/4 % C'], + ['previous bar incomplete (1)', 'A... % A'], + ['previous bar incomplete (2)', 'A.. B. % A'], + ['previous bar incomplete (3)', 'A B.. C. %'], + ['previous bar incomplete (4)', 'A B. %'], + ['repeat bar with a different time signature', 'C 3/4 %'], +])('Invalid bar repeat usage: %s => %s', (title, input) => { + const throwingFn = () => { + parseChordLine(input); + }; - test('Throw Error', () => { - expect(throwingFn).toThrow(Error); - expect(throwingFn).toThrow( - 'A chord line cannot start with the barRepeat symbol' - ); - }); - } -); + test('Throw InvalidBarRepeatException', () => { + expect(throwingFn).toThrow(InvalidBarRepeatException); + }); +}); describe.each([ ['A... {B7. D7.}'], diff --git a/packages/chord-mark/tests/unit/renderer/components/renderBarContent.spec.js b/packages/chord-mark/tests/unit/renderer/components/renderBarContent.spec.js index 7644f7a1..5f2cb49b 100644 --- a/packages/chord-mark/tests/unit/renderer/components/renderBarContent.spec.js +++ b/packages/chord-mark/tests/unit/renderer/components/renderBarContent.spec.js @@ -169,11 +169,9 @@ describe.each([ chord.spacesAfter = spacesAfter; }); - const rendered = renderBarContent( - parsed.allBars[0], - isLastBar, - shouldPrintBarSeparators - ); + const rendered = renderBarContent(parsed.allBars[0], isLastBar, { + shouldPrintBarSeparators, + }); expect(stripTags(rendered)).toEqual(output); }); @@ -204,12 +202,29 @@ describe.each([ ); const spaced = simpleSpacer(parsed); - const rendered = renderBarContent( - spaced.allBars[0], - undefined, - true, - shouldPrintSubBeatDelimiters + const rendered = renderBarContent(spaced.allBars[0], undefined, { + shouldPrintSubBeatDelimiters, + }); + + expect(stripTags(rendered)).toEqual(output); + }); +}); + +describe.each([ + ['by default, do not print time signature', 'A', 'A ', undefined], + ['shouldPrintTimeSignature = false', 'A', 'A ', false], + ['shouldPrintTimeSignature = true', 'A', '4/4 A ', true], +])('%s: %s', (title, input, output, shouldPrintTimeSignature) => { + test('Print time signature: ' + output, () => { + const parsed = forEachChordInChordLine( + parseChordLine(input), + (chord) => (chord.symbol = getChordSymbol(chord.model)) ); + const spaced = simpleSpacer(parsed); + + const rendered = renderBarContent(spaced.allBars[0], undefined, { + shouldPrintTimeSignature, + }); expect(stripTags(rendered)).toEqual(output); }); diff --git a/packages/chord-mark/tests/unit/renderer/components/renderChordLine.spec.js b/packages/chord-mark/tests/unit/renderer/components/renderChordLine.spec.js index 5766fcbe..b4469d30 100644 --- a/packages/chord-mark/tests/unit/renderer/components/renderChordLine.spec.js +++ b/packages/chord-mark/tests/unit/renderer/components/renderChordLine.spec.js @@ -1,13 +1,17 @@ -jest.mock('../../../../src/renderer/components/renderBarContent'); +import { forEachChordInChordLine } from '../../../../src/parser/helper/songs'; import renderChordLine from '../../../../src/renderer/components/renderChordLine'; -import renderBarContent from '../../../../src/renderer/components/renderBarContent'; - import parseChordLine from '../../../../src/parser/parseChordLine'; -import getChordSymbol from '../../../../src/renderer/helpers/getChordSymbol'; import stripTags from '../../../../src/core/dom/stripTags'; import htmlToElement from '../../../../src/core/dom/htmlToElement'; +import getChordSymbol from '../../../../src/renderer/helpers/getChordSymbol'; + +function renderChordSymbols(chordLine) { + return forEachChordInChordLine(chordLine, (chord) => { + chord.symbol = getChordSymbol(chord.model); + }); +} describe('chordLine renderer', () => { test('Module', () => { @@ -15,8 +19,6 @@ describe('chordLine renderer', () => { }); test('Should return valid html', () => { - renderBarContent.mockReturnValue('C'); - const rendered = renderChordLine(parseChordLine('C'), true); const element = htmlToElement(rendered); @@ -27,59 +29,87 @@ describe('chordLine renderer', () => { }); describe.each([ - ['A B C', '|A|B|C|'], - ['A.. B.. C.. D..', '|A B|C D|'], + ['A B C', '|A |B |C |'], + ['A.. B.. C.. D..', '|A B |C D |'], ])('Render chordLine "%s" as "%s"', (input, output) => { test('expected rendering', () => { - renderBarContent.mockImplementation((bar) => - bar.allChords.map((chord) => getChordSymbol(chord.model)).join(' ') - ); - const chordLine = parseChordLine(input); - - const rendered = renderChordLine(chordLine, true); + const rendered = renderChordLine(renderChordSymbols(chordLine)); expect(stripTags(rendered)).toEqual(output); }); }); describe.each([ - ['A B C', undefined, '|A|B|C|'], - ['A B C', 0, '|A|B|C|'], - ['A B C', 1, ' |A|B|C|'], - ['A B C', 2, ' |A|B|C|'], - ['A B C', 3, ' |A|B|C|'], - ['A B C', 10, ' |A|B|C|'], + ['A B C', undefined, '|A |B |C |'], + ['A B C', 0, '|A |B |C |'], + ['A B C', 1, ' |A |B |C |'], + ['A B C', 2, ' |A |B |C |'], + ['A B C', 3, ' |A |B |C |'], + ['A B C', 10, ' |A |B |C |'], ])('respect offset parameter', (input, offset, output) => { test('offset chordline by ' + (offset || '0'), () => { - renderBarContent.mockImplementation((bar) => - bar.allChords.map((chord) => getChordSymbol(chord.model)).join(' ') - ); - const chordLine = parseChordLine(input); chordLine.offset = offset; - const rendered = renderChordLine(chordLine, true); + const rendered = renderChordLine(renderChordSymbols(chordLine)); expect(stripTags(rendered)).toEqual(output); }); }); describe.each([ - ['A B C', '|A |B |C |', true], - ['A B C', 'A B C ', false], + ['A B C', '|A |B |C |', true], + ['A B C', 'A B C', false], ])('%s => %s', (input, output, shouldPrintBarSeparators) => { test('respect shouldPrintBarSeparators', () => { - renderBarContent.mockImplementation( - (bar) => - bar.allChords - .map((chord) => getChordSymbol(chord.model)) - .join(' ') + ' ' + const chordLine = parseChordLine(input); + + const rendered = renderChordLine( + renderChordSymbols(chordLine), + undefined, + { shouldPrintBarSeparators } + ); + + expect(stripTags(rendered)).toEqual(output); + }); +}); + +describe.each([ + ['A.. B. {C D}', '|A B {C D} |', undefined], + ['A.. B. {C D}', '|A B {C D} |', true], + ['A.. B. {C D}', '|A B C D |', false], +])('%s => %s', (input, output, shouldPrintSubBeatDelimiters) => { + test('respect shouldPrintSubBeatDelimiters', () => { + const chordLine = parseChordLine(input); + + const rendered = renderChordLine( + renderChordSymbols(chordLine), + undefined, + { shouldPrintSubBeatDelimiters } ); + expect(stripTags(rendered)).toEqual(output); + }); +}); + +describe.each([ + ['4/4 D', '|D |', true], + ['3/4 D', '|3/4 D |', true], + ['C 3/4 D', '|C |3/4 D |', true], + ['C 3/4 D E', '|C |3/4 D |E |', true], + ['C 3/4 D 2/4 E', '|C |3/4 D |2/4 E |', true], + ['C 3/4 D 2/4 E', '|C |3/4 D |2/4 E |', undefined], + ['C 3/4 D 2/4 E', '|C |D |E |', false], +])('%s => %s', (input, output, shouldPrintInlineTimeSignatures) => { + test('Print time signature after an inline change', () => { const chordLine = parseChordLine(input); - const rendered = renderChordLine(chordLine, shouldPrintBarSeparators); + const rendered = renderChordLine( + renderChordSymbols(chordLine), + undefined, + { shouldPrintInlineTimeSignatures } + ); expect(stripTags(rendered)).toEqual(output); }); diff --git a/packages/chord-mark/tests/unit/renderer/components/renderSong.spec.js b/packages/chord-mark/tests/unit/renderer/components/renderSong.spec.js index dc565c37..3a264350 100644 --- a/packages/chord-mark/tests/unit/renderer/components/renderSong.spec.js +++ b/packages/chord-mark/tests/unit/renderer/components/renderSong.spec.js @@ -494,6 +494,42 @@ No woman no cry }); }); +describe('printInlineTimeSignatures', () => { + const input = `2/4 G 4/4 G° +_ It was an ear_ly morning yesterday. +C/G G +_ I was up before the da_wn.`; + + const outputWithTimeSignatures = `|2/4 G |4/4 G° | + It was an early morning yesterday. +|C/G |G | + I was up before the dawn.`; + + test('true by default', () => { + const rendered = renderSongText(input); + expect(toText(rendered)).toBe(outputWithTimeSignatures); + }); + + test('explicit true', () => { + const rendered = renderSongText(input, { + printInlineTimeSignatures: true, + }); + expect(toText(rendered)).toBe(outputWithTimeSignatures); + }); + + test('explicit false', () => { + const expected = `|G |G° | + It was an early morning yesterday. +|C/G |G | + I was up before the dawn.`; + + const rendered = renderSongText(input, { + printInlineTimeSignatures: false, + }); + expect(toText(rendered)).toBe(expected); + }); +}); + describe('printChordsDuration', () => { const input = `4/4 A7 diff --git a/packages/chord-mark/tests/unit/renderer/spacers/chord/aligned.spec.js b/packages/chord-mark/tests/unit/renderer/spacers/chord/aligned.spec.js index c5705753..fb27b897 100644 --- a/packages/chord-mark/tests/unit/renderer/spacers/chord/aligned.spec.js +++ b/packages/chord-mark/tests/unit/renderer/spacers/chord/aligned.spec.js @@ -167,6 +167,27 @@ describe.each([ false, false, ], + + [ + 'uses simple spacer after an inline time signature change', + 'A.. E7.. 3/4 C.. D. 2/4 D. E.', + [ + { 1: 20, 2: 20, 3: 20, 4: 20 }, + { 1: 20, 2: 20, 3: 20, 4: 20 }, + { 1: 20, 2: 20, 3: 20, 4: 20 }, + ], + [17, 16, 0, 0, 0, 0], + [ + defaultSpacesAfter + 20 + defaultSpacesAfter, + defaultSpacesAfter + 20, + defaultSpacesAfter, + defaultSpacesAfter, + defaultSpacesAfter, + defaultSpacesAfter, + ], + true, + true, + ], ])( 'Aligned spacer: %s', ( diff --git a/packages/chord-mark/tests/unit/renderer/spacers/chord/chordLyrics.spec.js b/packages/chord-mark/tests/unit/renderer/spacers/chord/chordLyrics.spec.js index 28a5cb21..1b407b97 100644 --- a/packages/chord-mark/tests/unit/renderer/spacers/chord/chordLyrics.spec.js +++ b/packages/chord-mark/tests/unit/renderer/spacers/chord/chordLyrics.spec.js @@ -162,10 +162,9 @@ describe.each([ // assertions - const renderedChords = renderChordLine( - chordLine, - shouldPrintBarSeparators - ); + const renderedChords = renderChordLine(chordLine, undefined, { + shouldPrintBarSeparators, + }); const renderedLyrics = renderLyricLine( { model: lyricsLine }, { alignChordsWithLyrics: true } diff --git a/packages/chord-mark/tests/unit/renderer/spacers/chord/getMaxBeatsWidth.spec.js b/packages/chord-mark/tests/unit/renderer/spacers/chord/getMaxBeatsWidth.spec.js index abe681f6..4324a6af 100644 --- a/packages/chord-mark/tests/unit/renderer/spacers/chord/getMaxBeatsWidth.spec.js +++ b/packages/chord-mark/tests/unit/renderer/spacers/chord/getMaxBeatsWidth.spec.js @@ -142,6 +142,38 @@ describe.each([ }, ], ], + + [ + 'do not consider bars after an inline time signature change - simple', + ['C 3/4 E'], + [ + { + 1: 'C'.length, + 2: 0, + 3: 0, + 4: 0, + }, + ], + ], + + [ + 'do not consider bars after an inline time signature change - complex', + ['C B7 3/4 E 5/4 F 4/4 B'], + [ + { + 1: 'C'.length, + 2: 0, + 3: 0, + 4: 0, + }, + { + 1: 'B7'.length, + 2: 0, + 3: 0, + 4: 0, + }, + ], + ], ])('getMaxBeatsWidth(): %s', (title, input, output) => { test('Correctly computes the maximum width for each beat', () => { const parsedSong = parseSong(input); diff --git a/packages/documentation/docs/reference/chords.mdx b/packages/documentation/docs/reference/chords.mdx index 969a3301..d57f79f7 100644 --- a/packages/documentation/docs/reference/chords.mdx +++ b/packages/documentation/docs/reference/chords.mdx @@ -68,6 +68,11 @@ You can mix multiple time signatures per song. The most commonly used time signatures are recognized by . +It is possible to declare a time signature change inside a chord line. +The new time signature will last until a new one is declared on the same line, or until the end of the line. + + + ## Repeating bars You can use the `%` symbol to repeat the last declared bar on the current line.