Skip to content

Commit

Permalink
Adapt main rendering function, update unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
no-chris committed Jan 25, 2023
1 parent c638399 commit fa2ab3f
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 50 deletions.
1 change: 1 addition & 0 deletions packages/chord-mark/src/parser/songLinesFactory.js
Expand Up @@ -63,6 +63,7 @@ const defaultTimeSignature = '4/4';
* @typedef {SongLine} SongKeyDeclarationLine
* @type {Object}
* @property {KeyDeclaration} model
* @property {String} [symbol] - rendering property
*/

export default function songLinesFactory() {
Expand Down
97 changes: 62 additions & 35 deletions packages/chord-mark/src/renderer/components/renderSong.js
Expand Up @@ -4,19 +4,22 @@ import simpleChordSpacer from '../spacers/chord/simple';
import alignedChordSpacer from '../spacers/chord/aligned';
import chordLyricsSpacer from '../spacers/chord/chordLyrics';

import { forEachChordInSong } from '../../parser/helper/songs';

import renderChordLineModel from './renderChordLine';
import renderEmptyLine from './renderEmptyLine';
import renderKeyDeclaration from './renderKeyDeclaration';
import renderLine from './renderLine';
import renderSectionLabelLine from './renderSectionLabel';
import renderLyricLine from './renderLyricLine';
import renderSectionLabelLine from './renderSectionLabel';
import renderTimeSignature from './renderTimeSignature';

import songTpl from './tpl/song.js';
import getChordSymbol from '../helpers/getChordSymbol';
import getMainAccidental from '../helpers/getMainAccidental';
import renderAllSectionsLabels from '../helpers/renderAllSectionLabels';
import {
getKeyAccidental,
guessKey,
transposeKey,
} from '../helpers/keyHelpers';

import { chordRendererFactory } from 'chord-symbol';

Expand All @@ -27,26 +30,26 @@ import { defaultTimeSignature } from '../../parser/syntax';

/**
* @param {Song} parsedSong
* @param {('auto'|'flat'|'sharp')} accidentalsType
* @param {Boolean} alignBars
* @param {Boolean} alignChordsWithLyrics
* @param {Boolean} autoRepeatChords
* @param {('all'|'lyrics'|'chords'|'chordsFirstLyricLine')} chartType
* @param {Function|Boolean} chordSymbolRenderer - must be an instance of a ChordSymbol renderer, returned by chordRendererFactory()
* @param {Function|Boolean} customRenderer
* @param {Boolean} expandSectionCopy
* @param {Boolean} expandSectionMultiply
* @param {Boolean} harmonizeAccidentals
* @param {Boolean|('none'|'max'|'core')} simplifyChords
* @param {('never'|'uneven'|'always')} printChordsDuration
* @param {('never'|'grids'|'always')} printBarSeparators - mainly useful when converting a ChordMark file to a format that
* @param {Object} options
* @param {('auto'|'flat'|'sharp')} options.accidentalsType
* @param {Boolean} options.alignBars
* @param {Boolean} options.alignChordsWithLyrics
* @param {Boolean} options.autoRepeatChords
* @param {('all'|'lyrics'|'chords'|'chordsFirstLyricLine')} options.chartType
* @param {Function|Boolean} options.chordSymbolRenderer - must be an instance of a ChordSymbol renderer, returned by chordRendererFactory()
* @param {Function|Boolean} options.customRenderer
* @param {Boolean} options.expandSectionCopy
* @param {Boolean} options.expandSectionMultiply
* @param {Boolean|('none'|'max'|'core')} options.simplifyChords
* @param {('never'|'uneven'|'always')} options.printChordsDuration
* @param {('never'|'grids'|'always')} options.printBarSeparators - mainly useful when converting a ChordMark file to a format that
* 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
* @param {Boolean} options.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
* @param {Boolean} options.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
* @param {Number} options.transposeValue
* @param {Boolean} options.useShortNamings
* @returns {String} rendered HTML
*/
// eslint-disable-next-line max-lines-per-function
Expand All @@ -62,7 +65,6 @@ export default function renderSong(
customRenderer = false,
expandSectionCopy = true,
expandSectionMultiply = false,
harmonizeAccidentals = true,
printChordsDuration = 'uneven',
printBarSeparators = 'always',
printSubBeatDelimiters: shouldPrintSubBeatDelimiters = true,
Expand All @@ -78,7 +80,20 @@ export default function renderSong(
let contextTimeSignature = defaultTimeSignature.string;
let previousBarTimeSignature;

allLines = renderChords()
const detectedKey = guessKey(allChords);
let currentKey;

if (detectedKey) {
currentKey = transposeKey(
detectedKey,
transposeValue,
accidentalsType === 'auto'
);
}
let renderChord = getChordSymbolRenderer();

allLines = allLines
.map(renderChords)
.map(addPrintChordsDurationsFlag)
.map(addPrintBarTimeSignatureFlag)
.filter(shouldRenderLine)
Expand Down Expand Up @@ -108,29 +123,42 @@ export default function renderSong(
return songTpl({ song: allRenderedLines.join('') });
}

function renderChords() {
const renderChord = getChordSymbolRenderer();
function renderChords(line) {
if (line.type === lineTypes.KEY_DECLARATION) {
currentKey = transposeKey(
line.model,
transposeValue,
accidentalsType === 'auto'
);

return forEachChordInSong(allLines, (chord) => {
chord.symbol = getChordSymbol(chord.model, renderChord);
});
renderChord = getChordSymbolRenderer();
line.symbol = renderChord(line.model.chordModel);
} else if (line.type === lineTypes.CHORD) {
line.model.allBars.forEach((bar) => {
bar.allChords.forEach((chord) => {
chord.symbol = getChordSymbol(chord.model, renderChord);
});
});
}
return line;
}

function getChordSymbolRenderer() {
if (typeof chordSymbolRenderer === 'function') {
return chordSymbolRenderer;
}
const accidental =
const accidentals =
accidentalsType === 'auto'
? getMainAccidental(allChords)
? currentKey
? getKeyAccidental(currentKey)
: 'sharp'
: accidentalsType;

return chordRendererFactory({
simplify: simplifyChords,
useShortNamings,
transposeValue,
harmonizeAccidentals,
useFlats: accidental === 'flat',
accidentals,
});
}

Expand Down Expand Up @@ -275,16 +303,15 @@ export default function renderSong(
rendered = renderEmptyLine();
} else if (line.type === lineTypes.SECTION_LABEL) {
shouldOpenSection = true;

shouldClosePriorSection = lineIsInASection;

lineIsInASection = true;

sectionWrapperClasses = getSectionWrapperClasses(line);

rendered = renderSectionLabelLine(line);
} else if (line.type === lineTypes.TIME_SIGNATURE) {
rendered = renderTimeSignature(line);
} else if (line.type === lineTypes.KEY_DECLARATION) {
rendered = renderKeyDeclaration(line);
} else {
rendered = renderLyricLine(line, {
alignChordsWithLyrics,
Expand Down
24 changes: 15 additions & 9 deletions packages/chord-mark/tests/integration/parser/parseSong.spec.js
Expand Up @@ -240,15 +240,21 @@ Let it _be _ _ _`;
},
],
allChords: [
{ model: parseChord('C'), occurrences: 3 },
{ model: parseChord('G'), occurrences: 2 },
{ model: parseChord('Am'), occurrences: 2 },
{ model: parseChord('Am/G'), occurrences: 1 },
{ model: parseChord('FM7'), occurrences: 1 },
{ model: parseChord('F6'), occurrences: 1 },
{ model: parseChord('F'), occurrences: 1 },
{ model: parseChord('C/E'), occurrences: 1 },
{ model: parseChord('Dm7'), occurrences: 1 },
{
model: parseChord('C'),
occurrences: 3,
duration: 6,
isFirst: true,
isLast: true,
},
{ model: parseChord('G'), occurrences: 2, duration: 4 },
{ model: parseChord('Am'), occurrences: 2, duration: 1.5 },
{ model: parseChord('Am/G'), occurrences: 1, duration: 0.5 },
{ model: parseChord('FM7'), occurrences: 1, duration: 1 },
{ model: parseChord('F6'), occurrences: 1, duration: 1 },
{ model: parseChord('F'), occurrences: 1, duration: 1 },
{ model: parseChord('C/E'), occurrences: 1, duration: 0.5 },
{ model: parseChord('Dm7'), occurrences: 1, duration: 0.5 },
],
};

Expand Down
Expand Up @@ -62,7 +62,7 @@ describe.each([
'transposed (song1-output-transposed)',
'song1-input.txt',
'song1-output-transposed.txt',
{ transposeValue: -4, accidentalsType: 'flat' },
{ transposeValue: -4, accidentals: 'flat' },
],
[
'no bar separators (song1-output-no-bar-sep)',
Expand Down
17 changes: 12 additions & 5 deletions packages/chord-mark/tests/unit/parser/songLinesFactory.spec.js
Expand Up @@ -3,6 +3,8 @@ jest.mock('../../../src/parser/parseLyricLine');

import _ from 'lodash';

import { chordParserFactory } from 'chord-symbol';

import songLinesFactory from '../../../src/parser/songLinesFactory';

import {
Expand Down Expand Up @@ -1237,19 +1239,24 @@ verseContent2

describe('Key declaration', () => {
test('Correctly parse key declaration', () => {
const input = ['key C#min', 'key Ab', 'key Bbmaj'];
const parseChord = chordParserFactory();
const input = ['key C#mi', 'key Ab', 'key Bbmaj'];

const expected = [
{
type: 'keyDeclaration',
string: 'key C#min',
model: { key: 'C#mi' },
string: 'key C#mi',
model: { string: 'C#mi', chordModel: parseChord('C#mi') },
},
{
type: 'keyDeclaration',
string: 'key Ab',
model: { string: 'Ab', chordModel: parseChord('Ab') },
},
{ type: 'keyDeclaration', string: 'key Ab', model: { key: 'Ab' } },
{
type: 'keyDeclaration',
string: 'key Bbmaj',
model: { key: 'Bb' },
model: { string: 'Bb', chordModel: parseChord('Bbmaj') },
},
];

Expand Down
125 changes: 125 additions & 0 deletions packages/chord-mark/tests/unit/renderer/components/renderSong.spec.js
Expand Up @@ -857,3 +857,128 @@ verseLine1`;
expect(element.childNodes[1].nodeName).toBe('P');
});
});

describe('Keys, accidental & transpose', () => {
describe.each([
[
'accidentals = auto (default): use accidental relevant for detected key (flat)',
`F F A#`,
'|F |% |Bb |',
],
[
'accidentals = auto (default): use accidental relevant for detected key, with transpose (flat)',
`F F A#`,
'|Bb |% |Eb |',
{ transposeValue: -7 },
],
[
'accidentals = auto (default): use accidental relevant for detected key (sharp)',
`G G Gb`,
'|G |% |F# |',
],
[
'accidentals = auto (default): use accidental relevant for detected key, with transpose (sharp)',
`G G Gb`,
'|B |% |A# |',
{ transposeValue: +4 },
],
[
'accidentals = force flat',
`G G F#`,
'|G |% |Gb |',
{ accidentalsType: 'flat' },
],
[
'accidentals = force sharp',
`F F Bb`,
'|F |% |A# |',
{ accidentalsType: 'sharp' },
],
[
'Key transpose: use # if transposeValue > 0',
`C`,
'|C# |',
{ transposeValue: +1 },
],
[
'Key transpose: use b if transposeValue < 0',
`C`,
'|Db |',
{ transposeValue: -11 },
],
[
'Key transpose: avoid theoretical keys (C+3 => Eb)',
`C`,
'|Eb |',
{ transposeValue: +3 },
],
[
'Key transpose: avoid theoretical keys (C+8 => Ab)',
`C`,
'|Ab |',
{ transposeValue: +8 },
],
[
'Key transpose: avoid theoretical keys (Cm-11 => C#m)',
`Cm`,
'|C#m |',
{ transposeValue: -11 },
],
[
'Key transpose: force sharp on theoretical key (C+8 => G#)',
`C`,
'|G# |',
{ transposeValue: +8, accidentalsType: 'sharp' },
],
[
'Key transpose: force flat theoretical key (Cm-11 => Dbm)',
`Cm`,
'|Dbm |',
{ transposeValue: -11, accidentalsType: 'flat' },
],
[
'Explicit key: auto accidentals',
`key Dm\n` + 'Dm A# C Dm\n' + 'key C#m\n' + 'Dbm7 Ab7',
'key: Dm\n' +
'|Dm |Bb |C |Dm |\n' +
'key: C#m\n' +
'|C#m7 |G#7 |',
{ accidentalsType: 'auto' },
],
[
'Explicit key: force sharp',
`key Dm\n` + 'Dm A# C Dm\n' + 'key C#m\n' + 'Dbm7 Ab7',
'key: Dm\n' +
'|Dm |A# |C |Dm |\n' +
'key: C#m\n' +
'|C#m7 |G#7 |',
{ accidentalsType: 'sharp' },
],
[
'Explicit key: force flat',
`key Dm\n` + 'Dm A# C Dm\n' + 'key C#m\n' + 'C#m7 G#7',
'key: Dm\n' +
'|Dm |Bb |C |Dm |\n' +
'key: Dbm\n' +
'|Dbm7 |Ab7 |',
{ accidentalsType: 'flat' },
],
[
'Explicit key + transpose: auto accidentals',
`key Dm\n` + 'Dm A# C Dm\n' + 'key C#m\n' + 'C#m7 G#7 D',
'key: C#m\n' +
'|C#m |A |B |C#m |\n' +
'key: Cm\n' +
'|Cm7 |G7 |Db |',
{ accidentalsType: 'auto', transposeValue: -1 },
],
])('%s', (title, song, expected, options = {}) => {
test('renders with correct accidental', () => {
const rendered = renderSongText(song, {
alignBars: false,
...options,
});
expect(toText(rendered)).toBe(expected);
});
});
});

0 comments on commit fa2ab3f

Please sign in to comment.