From 4ee15439eb965069815ebccb211a41df0a9c280b Mon Sep 17 00:00:00 2001 From: Martijn Versluis Date: Fri, 3 Jan 2025 21:30:24 +0100 Subject: [PATCH] Make decapo configurable (#1533) --- src/formatter/chords_over_words_formatter.ts | 1 + src/formatter/configuration/configuration.ts | 4 + src/formatter/templates/html_div_formatter.ts | 1 + .../templates/html_table_formatter.ts | 1 + src/formatter/text_formatter.ts | 1 + src/helpers.ts | 16 +- test/formatter/html_div_formatter.test.ts | 54 +++++- test/formatter/html_table_formatter.test.ts | 50 +++++- test/formatter/text_formatter.test.ts | 21 ++- test/helpers.test.ts | 13 +- test/helpers/render_chord.test.ts | 158 +++++++++++------- 11 files changed, 250 insertions(+), 70 deletions(-) diff --git a/src/formatter/chords_over_words_formatter.ts b/src/formatter/chords_over_words_formatter.ts index 0f69170b..a3f3aeb7 100644 --- a/src/formatter/chords_over_words_formatter.ts +++ b/src/formatter/chords_over_words_formatter.ts @@ -119,6 +119,7 @@ class ChordsOverWordsFormatter extends Formatter { { renderKey: this.configuration.key, normalizeChords: this.configuration.normalizeChords, + decapo: this.configuration.decapo, }, ); } diff --git a/src/formatter/configuration/configuration.ts b/src/formatter/configuration/configuration.ts index f730baf7..7dd163ed 100644 --- a/src/formatter/configuration/configuration.ts +++ b/src/formatter/configuration/configuration.ts @@ -22,6 +22,7 @@ export type ConfigurationProperties = Record & { delegates: Partial>; instrument?: InstrumentConfigurationProperties; user?: UserConfigurationProperties; + decapo?: boolean; }; export const defaultConfiguration: ConfigurationProperties = { @@ -58,6 +59,8 @@ class Configuration { user?: UserConfiguration; + decapo?: boolean; + get metadataSeparator(): string { return this.metadata.separator ?? ''; } @@ -73,6 +76,7 @@ class Configuration { this.delegates = { ...defaultConfiguration.delegates, ...configuration.delegates }; this.instrument = configuration.instrument ? new InstrumentConfiguration(configuration.instrument) : undefined; this.user = configuration.user ? new UserConfiguration(configuration.user) : undefined; + this.decapo = !!configuration.decapo; } } diff --git a/src/formatter/templates/html_div_formatter.ts b/src/formatter/templates/html_div_formatter.ts index 1e4ee82b..96245995 100644 --- a/src/formatter/templates/html_div_formatter.ts +++ b/src/formatter/templates/html_div_formatter.ts @@ -68,6 +68,7 @@ export default ( renderKey: key, useUnicodeModifier: configuration.useUnicodeModifiers, normalizeChords: configuration.normalizeChords, + decapo: configuration.decapo, }, ) } diff --git a/src/formatter/templates/html_table_formatter.ts b/src/formatter/templates/html_table_formatter.ts index ac6c5d95..38719d04 100644 --- a/src/formatter/templates/html_table_formatter.ts +++ b/src/formatter/templates/html_table_formatter.ts @@ -77,6 +77,7 @@ export default ( renderKey: key, useUnicodeModifier: configuration.useUnicodeModifiers, normalizeChords: configuration.normalizeChords, + decapo: configuration.decapo, }, ) } diff --git a/src/formatter/text_formatter.ts b/src/formatter/text_formatter.ts index d09d550f..337ccb8e 100644 --- a/src/formatter/text_formatter.ts +++ b/src/formatter/text_formatter.ts @@ -121,6 +121,7 @@ class TextFormatter extends Formatter { renderKey: this.configuration.key, useUnicodeModifier: this.configuration.useUnicodeModifiers, normalizeChords: this.configuration.normalizeChords, + decapo: this.configuration.decapo, }, ); return chords; diff --git a/src/helpers.ts b/src/helpers.ts index 16606187..e9329293 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -58,8 +58,21 @@ interface RenderChordOptions { renderKey?: Key | null; useUnicodeModifier?: boolean; normalizeChords?: boolean; + decapo?: boolean; } +/** + * Renders a chord in the context of a line and song and taking into account some options + * @param chordString The chord to render + * @param line The line the chord is in + * @param song The song the line is in + * @param renderKey The key to render the chord in. If not provided, the line key will be used, + * or the song key if the line key is not provided. + * @param useUnicodeModifier Whether to use unicode modifiers ('\u266f'/'\u266d') or plain text ('#'/'b'). + * Default `false`. + * @param normalizeChords Whether to normalize the chord to the key (default `true`) + * @param decapo Whether to transpose all chords to eliminate the capo (default `false`) + */ export function renderChord( chordString: string, line: Line, @@ -68,12 +81,13 @@ export function renderChord( renderKey = null, useUnicodeModifier = false, normalizeChords = true, + decapo = false, }: RenderChordOptions = {}, ): string { const chord = Chord.parse(chordString); const songKey = song.key; const capoString = song.metadata.getSingle(CAPO); - const capo = capoString ? parseInt(capoString, 10) : null; + const capo = (decapo && capoString) ? parseInt(capoString, 10) : null; const chordStyle = song.metadata.getSingle(CHORD_STYLE) as ChordType; if (!chord) { diff --git a/test/formatter/html_div_formatter.test.ts b/test/formatter/html_div_formatter.test.ts index 165fa4a4..15779059 100644 --- a/test/formatter/html_div_formatter.test.ts +++ b/test/formatter/html_div_formatter.test.ts @@ -565,7 +565,7 @@ describe('HtmlDivFormatter', () => { expect(typeof cssObject).toEqual('object'); }); - it('applies the correct normalization when a capo is active', () => { + it('applies the correct normalization when a capo is active and decapo is on', () => { const songWithCapo = new ChordSheetSerializer().deserialize({ type: 'chordSheet', lines: [ @@ -614,6 +614,58 @@ describe('HtmlDivFormatter', () => { `); + expect(new HtmlDivFormatter({ decapo: true }).format(songWithCapo)).toEqual(expectedChordSheet); + }); + + it('does not apply normalization for capo when decapo is off', () => { + const songWithCapo = new ChordSheetSerializer().deserialize({ + type: 'chordSheet', + lines: [ + { + type: 'line', + items: [{ type: 'tag', name: 'key', value: 'F' }], + }, + { + type: 'line', + items: [{ type: 'tag', name: 'capo', value: '1' }], + }, + { + type: 'line', + items: [ + { type: 'chordLyricsPair', chords: '', lyrics: 'My ' }, + { type: 'chordLyricsPair', chords: 'Dm7', lyrics: 'heart has always ' }, + { type: 'chordLyricsPair', chords: 'C/E', lyrics: 'longed for something ' }, + { type: 'chordLyricsPair', chords: 'F', lyrics: 'more' }, + ], + }, + ], + }); + + const expectedChordSheet = stripHTML(` +
+
+
+
+
+
My
+
+
+
Dm7
+
heart has always
+
+
+
C/E
+
longed for something
+
+
+
F
+
more
+
+
+
+
+ `); + expect(new HtmlDivFormatter().format(songWithCapo)).toEqual(expectedChordSheet); }); diff --git a/test/formatter/html_table_formatter.test.ts b/test/formatter/html_table_formatter.test.ts index b3d1c2b6..c236708a 100644 --- a/test/formatter/html_table_formatter.test.ts +++ b/test/formatter/html_table_formatter.test.ts @@ -615,7 +615,7 @@ describe('HtmlTableFormatter', () => { expect(typeof cssObject).toEqual('object'); }); - it('applies the correct normalization when a capo is active', () => { + it('applies the correct normalization when a capo is active and decapo is on', () => { const songWithCapo = new ChordSheetSerializer().deserialize({ type: 'chordSheet', lines: [ @@ -660,6 +660,54 @@ describe('HtmlTableFormatter', () => { `); + expect(new HtmlTableFormatter({ decapo: true }).format(songWithCapo)).toEqual(expectedChordSheet); + }); + + it('does not apply normalization for capo when decapo is off', () => { + const songWithCapo = new ChordSheetSerializer().deserialize({ + type: 'chordSheet', + lines: [ + { + type: 'line', + items: [{ type: 'tag', name: 'key', value: 'F' }], + }, + { + type: 'line', + items: [{ type: 'tag', name: 'capo', value: '1' }], + }, + { + type: 'line', + items: [ + { type: 'chordLyricsPair', chords: '', lyrics: 'My ' }, + { type: 'chordLyricsPair', chords: 'Dm7', lyrics: 'heart has always ' }, + { type: 'chordLyricsPair', chords: 'C/E', lyrics: 'longed for something ' }, + { type: 'chordLyricsPair', chords: 'F', lyrics: 'more' }, + ], + }, + ], + }); + + const expectedChordSheet = stripHTML(` +
+
+ + + + + + + + + + + + + +
Dm7C/EF
My heart has always longed for something more
+
+
+ `); + expect(new HtmlTableFormatter().format(songWithCapo)).toEqual(expectedChordSheet); }); diff --git a/test/formatter/text_formatter.test.ts b/test/formatter/text_formatter.test.ts index ddecc796..752e61d8 100644 --- a/test/formatter/text_formatter.test.ts +++ b/test/formatter/text_formatter.test.ts @@ -124,7 +124,7 @@ Let it be, let it be, let it be, let it be`; expect(formatter.format(songWithIntro)).toEqual(expectedChordSheet); }); - it('applies the correct normalization when a capo is active', () => { + it('applies the correct normalization when a capo is active and decapo is on', () => { const songWithCapo = createSongFromAst([ [tag('key', 'F')], [tag('capo', '1')], @@ -140,6 +140,25 @@ Let it be, let it be, let it be, let it be`; C#m7 B/D# E My heart has always longed for something more`; + expect(new TextFormatter({ decapo: true }).format(songWithCapo)).toEqual(expectedChordSheet); + }); + + it('does not apply normalization for capo when decapo is off', () => { + const songWithCapo = createSongFromAst([ + [tag('key', 'F')], + [tag('capo', '1')], + [ + chordLyricsPair('', 'My '), + chordLyricsPair('Dm7', 'heart has always '), + chordLyricsPair('C/E', 'longed for something '), + chordLyricsPair('F', 'more'), + ], + ]); + + const expectedChordSheet = heredoc` + Dm7 C/E F + My heart has always longed for something more`; + expect(new TextFormatter().format(songWithCapo)).toEqual(expectedChordSheet); }); diff --git a/test/helpers.test.ts b/test/helpers.test.ts index 8254f7c1..38d62c42 100644 --- a/test/helpers.test.ts +++ b/test/helpers.test.ts @@ -6,13 +6,22 @@ import Metadata from '../src/chord_sheet/metadata'; import Configuration from '../src/formatter/configuration/configuration'; describe('renderChord', () => { - it('correctly normalizes when a capo is set', () => { + it('correctly normalizes when a capo is set and decapo is enabled', () => { const line = createLine(); const song = new Song(); song.setMetadata('key', 'F'); song.setMetadata('capo', '1'); - expect(renderChord('Dm7', line, song)).toEqual('C#m7'); + expect(renderChord('Dm7', line, song, { decapo: true })).toEqual('C#m7'); + }); + + it('does not normalize for capo when decapo is disabled', () => { + const line = createLine(); + const song = new Song(); + song.setMetadata('key', 'F'); + song.setMetadata('capo', '1'); + + expect(renderChord('Dm7', line, song, { decapo: false })).toEqual('Dm7'); }); it('can render in a different key', () => { diff --git a/test/helpers/render_chord.test.ts b/test/helpers/render_chord.test.ts index e0ee71d1..dbf0fcf5 100644 --- a/test/helpers/render_chord.test.ts +++ b/test/helpers/render_chord.test.ts @@ -7,38 +7,53 @@ import { Key } from '../../src'; describe('renderChord helper', () => { describe('chord transposition symbol', () => { eachTestCase(` - # | songKey | capo | lineKey | lineTransposeKey | renderKey | outcome | - -- | ------- | ---- | ------- | ---------------- | --------- | ------- | - 1 | | | | | | "Em7" | - 2 | | | | | "F" | "Em7" | - 3 | | | | "A" | | "Em7" | - 4 | | | | "A" | "F" | "Em7" | - 5 | | | "Bb" | | "F" | "Em7" | - 6 | | | "Bb" | "A" | "F" | "Em7" | - 7 | | 3 | | | | "Dbm7" | - 8 | | 3 | | | "F" | "Dbm7" | - 9 | | 3 | | "A" | | "Dbm7" | - 10 | | 3 | | "A" | "F" | "Dbm7" | - 11 | | 3 | "Bb" | | | "Dbm7" | - 12 | | 3 | "Bb" | | "F" | "Dbm7" | - 13 | | 3 | "Bb" | "A" | | "Dbm7" | - 14 | | 3 | "Bb" | "A" | "F" | "Dbm7" | - 15 | "G" | | | | | "Em7" | - 16 | "G" | | | | "F" | "Dm7" | - 17 | "G" | | | "A" | "F" | "Em7" | - 18 | "G" | | "Bb" | | | "Em7" | - 19 | "G" | | "Bb" | | "F" | "Dm7" | - 20 | "G" | | "Bb" | "A" | | "Gbm7" | - 21 | "G" | | "Bb" | "A" | "F" | "Em7" | - 22 | "G" | 3 | | | | "C#m7" | - 23 | "G" | 3 | | | "F" | "Bm7" | - 24 | "G" | 3 | | "A" | | "Ebm7" | - 25 | "G" | 3 | | "A" | "F" | "C#m7" | - 26 | "G" | 3 | "Bb" | | "F" | "Bm7" | - 27 | "G" | 3 | "Bb" | "A" | | "Ebm7" | - 28 | "G" | 3 | "Bb" | "A" | "F" | "C#m7" | + # | songKey | capo | lineKey | lineTransposeKey | renderKey | decapo | outcome | + -- | ------- | ---- | ------- | ---------------- | --------- | ------ | ------- | + 1 | | | | | | | "Em7" | + 2 | | | | | "F" | | "Em7" | + 3 | | | | "A" | | | "Em7" | + 4 | | | | "A" | "F" | | "Em7" | + 5 | | | "Bb" | | "F" | | "Em7" | + 6 | | | "Bb" | "A" | "F" | | "Em7" | + 7 | | 3 | | | | true | "Dbm7" | + 8 | | 3 | | | "F" | true | "Dbm7" | + 9 | | 3 | | "A" | | true | "Dbm7" | + 10 | | 3 | | "A" | "F" | true | "Dbm7" | + 11 | | 3 | "Bb" | | | true | "Dbm7" | + 12 | | 3 | "Bb" | | "F" | true | "Dbm7" | + 13 | | 3 | "Bb" | "A" | | true | "Dbm7" | + 14 | | 3 | "Bb" | "A" | "F" | true | "Dbm7" | + 15 | "G" | | | | | | "Em7" | + 16 | "G" | | | | "F" | | "Dm7" | + 17 | "G" | | | "A" | "F" | | "Em7" | + 18 | "G" | | "Bb" | | | | "Em7" | + 19 | "G" | | "Bb" | | "F" | | "Dm7" | + 20 | "G" | | "Bb" | "A" | | | "Gbm7" | + 21 | "G" | | "Bb" | "A" | "F" | | "Em7" | + 22 | "G" | 3 | | | | true | "C#m7" | + 23 | "G" | 3 | | | "F" | true | "Bm7" | + 24 | "G" | 3 | | "A" | | true | "Ebm7" | + 25 | "G" | 3 | | "A" | "F" | true | "C#m7" | + 26 | "G" | 3 | "Bb" | | "F" | true | "Bm7" | + 27 | "G" | 3 | "Bb" | "A" | | true | "Ebm7" | + 28 | "G" | 3 | "Bb" | "A" | "F" | true | "C#m7" | + 29 | | 3 | | | | false | "Em7" | + 30 | | 3 | | | "F" | false | "Em7" | + 31 | | 3 | | "A" | | false | "Em7" | + 32 | | 3 | | "A" | "F" | false | "Em7" | + 33 | | 3 | "Bb" | | | false | "Em7" | + 34 | | 3 | "Bb" | | "F" | false | "Em7" | + 35 | | 3 | "Bb" | "A" | | false | "Em7" | + 36 | | 3 | "Bb" | "A" | "F" | false | "Em7" | + 37 | "G" | 3 | | | | false | "Em7" | + 38 | "G" | 3 | | | "F" | false | "Dm7" | + 39 | "G" | 3 | | "A" | | false | "F#m7" | + 40 | "G" | 3 | | "A" | "F" | false | "Em7" | + 41 | "G" | 3 | "Bb" | | "F" | false | "Dm7" | + 42 | "G" | 3 | "Bb" | "A" | | false | "Gbm7" | + 43 | "G" | 3 | "Bb" | "A" | "F" | false | "Em7" | `, ({ - songKey, capo, lineKey, lineTransposeKey, renderKey, outcome, + songKey, capo, lineKey, lineTransposeKey, renderKey, decapo, outcome, }) => { const song = new Song(); song.metadata.add('key', songKey); @@ -48,45 +63,60 @@ describe('renderChord helper', () => { line.key = lineKey; line.transposeKey = lineTransposeKey; - const renderedChord = renderChord('Em7', line, song, { renderKey: Key.wrap(renderKey) }); + const renderedChord = renderChord('Em7', line, song, { renderKey: Key.wrap(renderKey), decapo }); expect(renderedChord).toEqual(outcome); }); }); describe('chord transposition solfege', () => { eachTestCase(` - # | songKey | capo | lineKey | lineTransposeKey | renderKey | outcome | - -- | ------- | ---- | ------- | ---------------- | --------- | ------- | - 1 | | | | | | "Mim7" | - 2 | | | | | "Fa" | "Mim7" | - 3 | | | | "La" | | "Mim7" | - 4 | | | | "La" | "Fa" | "Mim7" | - 5 | | | "Sib" | | "Fa" | "Mim7" | - 6 | | | "Sib" | "La" | "Fa" | "Mim7" | - 7 | | 3 | | | | "Rebm7" | - 8 | | 3 | | | "Fa" | "Rebm7" | - 9 | | 3 | | "La" | | "Rebm7" | - 10 | | 3 | | "La" | "Fa" | "Rebm7" | - 11 | | 3 | "Sib" | | | "Rebm7" | - 12 | | 3 | "Sib" | | "Fa" | "Rebm7" | - 13 | | 3 | "Sib" | "La" | | "Rebm7" | - 14 | | 3 | "Sib" | "La" | "Fa" | "Rebm7" | - 15 | "Sol" | | | | | "Mim7" | - 16 | "Sol" | | | | "Fa" | "Rem7" | - 17 | "Sol" | | | "La" | "Fa" | "Mim7" | - 18 | "Sol" | | "Sib" | | | "Mim7" | - 19 | "Sol" | | "Sib" | | "Fa" | "Rem7" | - 20 | "Sol" | | "Sib" | "La" | | "Solbm7" | - 21 | "Sol" | | "Sib" | "La" | "Fa" | "Mim7" | - 22 | "Sol" | 3 | | | | "Do#m7" | - 23 | "Sol" | 3 | | | "Fa" | "Sim7" | - 24 | "Sol" | 3 | | "La" | | "Mibm7" | - 25 | "Sol" | 3 | | "La" | "Fa" | "Do#m7" | - 26 | "Sol" | 3 | "Sib" | | "Fa" | "Sim7" | - 27 | "Sol" | 3 | "Sib" | "La" | | "Mibm7" | - 28 | "Sol" | 3 | "Sib" | "La" | "Fa" | "Do#m7" | + # | songKey | capo | lineKey | lineTransposeKey | renderKey | decapo | outcome | + -- | ------- | ---- | ------- | ---------------- | --------- | ------ | ------- | + 1 | | | | | | | "Mim7" | + 2 | | | | | "Fa" | | "Mim7" | + 3 | | | | "La" | | | "Mim7" | + 4 | | | | "La" | "Fa" | | "Mim7" | + 5 | | | "Sib" | | "Fa" | | "Mim7" | + 6 | | | "Sib" | "La" | "Fa" | | "Mim7" | + 7 | | 3 | | | | true | "Rebm7" | + 8 | | 3 | | | "Fa" | true | "Rebm7" | + 9 | | 3 | | "La" | | true | "Rebm7" | + 10 | | 3 | | "La" | "Fa" | true | "Rebm7" | + 11 | | 3 | "Sib" | | | true | "Rebm7" | + 12 | | 3 | "Sib" | | "Fa" | true | "Rebm7" | + 13 | | 3 | "Sib" | "La" | | true | "Rebm7" | + 14 | | 3 | "Sib" | "La" | "Fa" | true | "Rebm7" | + 15 | "Sol" | | | | | | "Mim7" | + 16 | "Sol" | | | | "Fa" | | "Rem7" | + 17 | "Sol" | | | "La" | "Fa" | | "Mim7" | + 18 | "Sol" | | "Sib" | | | | "Mim7" | + 19 | "Sol" | | "Sib" | | "Fa" | | "Rem7" | + 20 | "Sol" | | "Sib" | "La" | | | "Solbm7" | + 21 | "Sol" | | "Sib" | "La" | "Fa" | | "Mim7" | + 22 | "Sol" | 3 | | | | true | "Do#m7" | + 23 | "Sol" | 3 | | | "Fa" | true | "Sim7" | + 24 | "Sol" | 3 | | "La" | | true | "Mibm7" | + 25 | "Sol" | 3 | | "La" | "Fa" | true | "Do#m7" | + 26 | "Sol" | 3 | "Sib" | | "Fa" | true | "Sim7" | + 27 | "Sol" | 3 | "Sib" | "La" | | true | "Mibm7" | + 28 | "Sol" | 3 | "Sib" | "La" | "Fa" | true | "Do#m7" | + 29 | | 3 | | | | false | "Mim7" | + 30 | | 3 | | | "Fa" | false | "Mim7" | + 31 | | 3 | | "La" | | false | "Mim7" | + 32 | | 3 | | "La" | "Fa" | false | "Mim7" | + 33 | | 3 | "Sib" | | | false | "Mim7" | + 34 | | 3 | "Sib" | | "Fa" | false | "Mim7" | + 35 | | 3 | "Sib" | "La" | | false | "Mim7" | + 36 | | 3 | "Sib" | "La" | "Fa" | false | "Mim7" | + 37 | "Sol" | 3 | | | | false | "Mim7" | + 38 | "Sol" | 3 | | | "Fa" | false | "Rem7" | + 39 | "Sol" | 3 | | "La" | | false | "Fa#m7" | + 40 | "Sol" | 3 | | "La" | "Fa" | false | "Mim7" | + 41 | "Sol" | 3 | "Sib" | | "Fa" | false | "Rem7" | + 42 | "Sol" | 3 | "Sib" | "La" | | false | "Solbm7" | + 43 | "Sol" | 3 | "Sib" | "La" | "Fa" | false | "Mim7" | `, ({ - songKey, capo, lineKey, lineTransposeKey, renderKey, outcome, + songKey, capo, lineKey, lineTransposeKey, renderKey, decapo, outcome, }) => { const song = new Song(); song.metadata.add('key', songKey); @@ -96,7 +126,7 @@ describe('renderChord helper', () => { line.key = lineKey; line.transposeKey = lineTransposeKey; - const renderedChord = renderChord('Mim7', line, song, { renderKey: Key.wrap(renderKey) }); + const renderedChord = renderChord('Mim7', line, song, { renderKey: Key.wrap(renderKey), decapo }); expect(renderedChord).toEqual(outcome); }); });