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.