diff --git a/README.md b/README.md
index b1ea0f92..ffec5edc 100644
--- a/README.md
+++ b/README.md
@@ -521,6 +521,9 @@ See https://www.chordpro.org/chordpro/directives-key/
Chordfont directive. See https://www.chordpro.org/chordpro/directives-props_title_legacy/
TITLESIZE : string
Chordfont directive. See https://www.chordpro.org/chordpro/directives-props_title_legacy/
+TITLECOLOUR : string
+Chorus directive. Support repeating an earlier defined section.
+See https://www.chordpro.org/chordpro/directives-env_chorus/
defaultCss ⇒ string
Generates basic CSS, scoped within the provided selector, to use with output generated by [HtmlTableFormatter](#HtmlTableFormatter)
defaultCss ⇒ string
@@ -783,6 +786,7 @@ If not, it returns [INDETERMINATE](#INDETERMINATE)
* [.bodyLines](#Song+bodyLines) ⇒ [Array.<Line>
](#Line)
* [.bodyParagraphs](#Song+bodyParagraphs) ⇒ [Array.<Paragraph>
](#Paragraph)
* [.paragraphs](#Song+paragraphs) : [Array.<Paragraph>
](#Paragraph)
+ * [.expandedBodyParagraphs](#Song+expandedBodyParagraphs) : [Array.<Paragraph>
](#Paragraph)
* ~~[.metaData](#Song+metaData) ⇒~~
* [.clone()](#Song+clone) ⇒ [Song
](#Song)
* [.setKey(key)](#Song+setKey) ⇒ [Song
](#Song)
@@ -826,6 +830,12 @@ if you want to skip the "header lines": the lines that only contain me
### song.paragraphs : [Array.<Paragraph>
](#Paragraph)
The [Paragraph](#Paragraph) items of which the song consists
+**Kind**: instance property of [Song
](#Song)
+
+
+### song.expandedBodyParagraphs : [Array.<Paragraph>
](#Paragraph)
+The body paragraphs of the song, with any {chorus}
tag expanded into the targetted chorus
+
**Kind**: instance property of [Song
](#Song)
@@ -1116,6 +1126,7 @@ https://chordpro.org/chordpro/directives-env_bridge/, https://chordpro.org/chord
| [configuration.metadata] | object
| {}
| |
| [configuration.metadata.separator] | string
| "\", \""
| The separator to be used when rendering a metadata value that has multiple values. See: https://bit.ly/2SC9c2u
|
| [configuration.key] | [Key
](#Key) \| string
|
| The key to use for rendering. The chord sheet will be transposed from the song's original key (as indicated by the {key}
directive) to the specified key. Note that transposing will only work if the original song key is set.
|
+| [configuration.expandChorusDirective] | boolean
| false
| Whether or not to expand {chorus}
directives by rendering the last defined chorus inline after the directive.
|
@@ -1891,6 +1902,13 @@ See https://www.chordpro.org/chordpro/directives-key/
## TITLESIZE : string
Chordfont directive. See https://www.chordpro.org/chordpro/directives-props_title_legacy/
+**Kind**: global variable
+
+
+## TITLECOLOUR : string
+Chorus directive. Support repeating an earlier defined section.
+See https://www.chordpro.org/chordpro/directives-env_chorus/
+
**Kind**: global variable
diff --git a/package.json b/package.json
index 5ad12f7c..d7fc3190 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,7 @@
"build": "yarn build:code-generate && yarn build:sources && yarn build:browserify",
"jest": "jest",
"test": "yarn lint && yarn jest",
- "test:debug": "open -a \"Brave Browser\" chrome://inspect && node --nolazy --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --colors --verbose",
+ "jest:debug": "open -a \"Brave Browser\" chrome://inspect && node --nolazy --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --colors --verbose",
"eslint": "node_modules/.bin/eslint --ext .ts .",
"lint": "yarn build:code-generate && yarn eslint",
"lint:fix": "yarn build:code-generate && node_modules/.bin/eslint --fix --ext .ts .",
diff --git a/src/chord_sheet/line.ts b/src/chord_sheet/line.ts
index feab8c0a..00f4e9c5 100644
--- a/src/chord_sheet/line.ts
+++ b/src/chord_sheet/line.ts
@@ -5,7 +5,7 @@ import { CHORUS, NONE, VERSE } from '../constants';
import Item from './item';
import Font from './font';
-type MapItemFunc = (_item: Item) => Item;
+type MapItemFunc = (_item: Item) => Item | null;
export type LineType = 'verse' | 'chorus' | 'none';
@@ -32,6 +32,8 @@ class Line {
transposeKey: string | null = null;
+ lineNumber: number | null = null;
+
/**
* The text font that applies to this line. Is derived from the directives:
* `textfont`, `textsize` and `textcolour`
@@ -61,6 +63,10 @@ class Line {
return this.items.length === 0;
}
+ isNotEmpty(): boolean {
+ return !this.isEmpty();
+ }
+
/**
* Adds an item ({@link ChordLyricsPair} or {@link Tag}) to the line
* @param {ChordLyricsPair|Tag} item The item to be added
@@ -101,7 +107,7 @@ class Line {
const clonedItem = item.clone();
return func ? func(clonedItem) : clonedItem;
})
- .filter((item) => item);
+ .filter((item) => item !== null) as Item[];
clonedLine.type = this.type;
return clonedLine;
diff --git a/src/chord_sheet/song.ts b/src/chord_sheet/song.ts
index 935d0527..5342bf93 100644
--- a/src/chord_sheet/song.ts
+++ b/src/chord_sheet/song.ts
@@ -15,16 +15,17 @@ import {
} from '../constants';
import Tag, {
+ CAPO,
END_OF_CHORUS,
END_OF_TAB,
END_OF_VERSE,
+ KEY,
+ NEW_KEY,
START_OF_CHORUS,
START_OF_TAB,
START_OF_VERSE,
TRANSPOSE,
- NEW_KEY,
- CAPO,
- KEY,
+ CHORUS as CHORUS_TAG,
} from './tag';
interface MapItemsCallback {
@@ -146,18 +147,83 @@ class Song extends MetadataAccessors {
this.setCurrentProperties(this.sectionType);
this.currentLine.transposeKey = this.transposeKey ?? this.currentKey;
this.currentLine.key = this.currentKey || this.metadata.getSingle(KEY);
+ this.currentLine.lineNumber = this.lines.length - 1;
return this.currentLine;
}
+ private expandLine(line: Line): Line[] {
+ const expandedLines = line.items.flatMap((item: Item) => {
+ if (item instanceof Tag && item.name === CHORUS_TAG) {
+ return this.getLastChorusBefore(line.lineNumber);
+ }
+
+ return [];
+ });
+
+ return [line, ...expandedLines];
+ }
+
+ private getLastChorusBefore(lineNumber: number | null): Line[] {
+ const lines: Line[] = [];
+
+ if (!lineNumber) {
+ return lines;
+ }
+
+ for (let i = lineNumber - 1; i >= 0; i -= 1) {
+ const line = this.lines[i];
+
+ if (line.type === CHORUS) {
+ const filteredLine = this.filterChorusStartEndDirectives(line);
+
+ if (!(line.isNotEmpty() && filteredLine.isEmpty())) {
+ lines.unshift(line);
+ }
+ } else if (lines.length > 0) {
+ break;
+ }
+ }
+
+ return lines;
+ }
+
+ private filterChorusStartEndDirectives(line: Line) {
+ return line.mapItems((item: Item) => {
+ if (item instanceof Tag) {
+ if (item.name === START_OF_CHORUS || item.name === END_OF_CHORUS) {
+ return null;
+ }
+ }
+
+ return item;
+ });
+ }
+
/**
* The {@link Paragraph} items of which the song consists
* @member {Paragraph[]}
*/
get paragraphs(): Paragraph[] {
+ return this.linesToParagraphs(this.lines);
+ }
+
+ /**
+ * The body paragraphs of the song, with any `{chorus}` tag expanded into the targetted chorus
+ * @type {Paragraph[]}
+ */
+ get expandedBodyParagraphs(): Paragraph[] {
+ return this.selectRenderableItems(
+ this.linesToParagraphs(
+ this.lines.flatMap((line: Line) => this.expandLine(line)),
+ ),
+ ) as Paragraph[];
+ }
+
+ linesToParagraphs(lines: Line[]) {
let currentParagraph = new Paragraph();
const paragraphs = [currentParagraph];
- this.lines.forEach((line) => {
+ lines.forEach((line) => {
if (line.isEmpty()) {
currentParagraph = new Paragraph();
paragraphs.push(currentParagraph);
diff --git a/src/chord_sheet/tag.ts b/src/chord_sheet/tag.ts
index baa55a9d..8425dd28 100644
--- a/src/chord_sheet/tag.ts
+++ b/src/chord_sheet/tag.ts
@@ -205,6 +205,13 @@ export const TITLESIZE = 'titlesize';
*/
export const TITLECOLOUR = 'titlecolour';
+/**
+ * Chorus directive. Support repeating an earlier defined section.
+ * See https://www.chordpro.org/chordpro/directives-env_chorus/
+ * @type {string}
+ */
+export const CHORUS = 'chorus';
+
const TITLE_SHORT = 't';
const SUBTITLE_SHORT = 'st';
const COMMENT_SHORT = 'c';
@@ -261,6 +268,7 @@ const DIRECTIVES_WITH_RENDERABLE_LABEL = [
START_OF_CHORUS,
START_OF_BRIDGE,
START_OF_TAB,
+ CHORUS,
];
const ALIASES: Record = {
diff --git a/src/formatter/configuration/configuration.ts b/src/formatter/configuration/configuration.ts
index d933d1e9..4931dc21 100644
--- a/src/formatter/configuration/configuration.ts
+++ b/src/formatter/configuration/configuration.ts
@@ -9,12 +9,14 @@ export type ConfigurationProperties = Record & {
separator: string,
},
key?: Key | string | null,
+ expandChorusDirective?: boolean,
}
export const defaultConfiguration: ConfigurationProperties = {
evaluate: false,
metadata: { separator: ',' },
key: null,
+ expandChorusDirective: false,
};
class Configuration {
@@ -26,13 +28,12 @@ class Configuration {
configuration: Record;
- constructor(configuration: ConfigurationProperties = defaultConfiguration) {
- if ('evaluate' in configuration) {
- this.evaluate = !!configuration.evaluate;
- } else {
- this.evaluate = !!defaultConfiguration.evaluate;
- }
+ expandChorusDirective: boolean;
+ constructor(configuration: ConfigurationProperties = defaultConfiguration) {
+ const mergedConfig: ConfigurationProperties = { ...defaultConfiguration, ...configuration };
+ this.evaluate = !!mergedConfig.evaluate;
+ this.expandChorusDirective = !!mergedConfig.expandChorusDirective;
this.metadata = new MetadataConfiguration(configuration.metadata);
this.key = configuration.key ? Key.wrap(configuration.key) : null;
this.configuration = configuration;
diff --git a/src/formatter/formatter.ts b/src/formatter/formatter.ts
index 9bf25870..02168748 100644
--- a/src/formatter/formatter.ts
+++ b/src/formatter/formatter.ts
@@ -18,6 +18,8 @@ class Formatter {
* from the song's original key (as indicated by the `{key}` directive) to the specified key.
* Note that transposing will only work
* if the original song key is set.
+ * @param {boolean} [configuration.expandChorusDirective=false] Whether or not to expand `{chorus}` directives
+ * by rendering the last defined chorus inline after the directive.
*/
constructor(configuration: ConfigurationProperties | null = null) {
this.configuration = new Configuration(configuration || {});
diff --git a/src/formatter/html_formatter.ts b/src/formatter/html_formatter.ts
index a5a07dbb..9530d07a 100644
--- a/src/formatter/html_formatter.ts
+++ b/src/formatter/html_formatter.ts
@@ -2,11 +2,13 @@ import Formatter from './formatter';
import Configuration from './configuration/configuration';
import Song from '../chord_sheet/song';
import { breakingChange, scopeCss } from '../utilities';
+import Paragraph from '../chord_sheet/paragraph';
export type HtmlTemplateArgs = {
configuration: Configuration;
song: Song;
renderBlankLines?: boolean;
+ bodyParagraphs: Paragraph[],
};
export type Template = (_args: HtmlTemplateArgs) => string;
@@ -22,7 +24,15 @@ abstract class HtmlFormatter extends Formatter {
* @returns {string} The HTML string
*/
format(song: Song): string {
- return this.template({ song, configuration: this.configuration });
+ const { bodyParagraphs, expandedBodyParagraphs } = song;
+
+ return this.template(
+ {
+ song,
+ configuration: this.configuration,
+ bodyParagraphs: this.configuration.expandChorusDirective ? expandedBodyParagraphs : bodyParagraphs,
+ },
+ );
}
/**
diff --git a/src/formatter/templates/html_div_formatter.ts b/src/formatter/templates/html_div_formatter.ts
index 0e5a8b22..1990e64f 100644
--- a/src/formatter/templates/html_div_formatter.ts
+++ b/src/formatter/templates/html_div_formatter.ts
@@ -26,9 +26,9 @@ export default (
song: {
title,
subtitle,
- bodyParagraphs,
metadata,
},
+ bodyParagraphs,
}: HtmlTemplateArgs,
): string => stripHTML(`
${ when(title, () => `${ title }
`) }
diff --git a/src/formatter/templates/html_table_formatter.ts b/src/formatter/templates/html_table_formatter.ts
index 5a5b0176..073ef8ac 100644
--- a/src/formatter/templates/html_table_formatter.ts
+++ b/src/formatter/templates/html_table_formatter.ts
@@ -28,10 +28,10 @@ export default (
song: {
title,
subtitle,
- bodyParagraphs,
bodyLines,
metadata,
},
+ bodyParagraphs,
}: HtmlTemplateArgs,
): string => stripHTML(`
${ when(title, () => `${ title}
`) }
diff --git a/src/formatter/text_formatter.ts b/src/formatter/text_formatter.ts
index b7fdad68..fe46f80a 100644
--- a/src/formatter/text_formatter.ts
+++ b/src/formatter/text_formatter.ts
@@ -42,9 +42,10 @@ class TextFormatter extends Formatter {
}
formatParagraphs(): string {
- const { bodyParagraphs, metadata } = this.song;
+ const { bodyParagraphs, expandedBodyParagraphs, metadata } = this.song;
+ const { expandChorusDirective } = this.configuration;
- return bodyParagraphs
+ return (expandChorusDirective ? expandedBodyParagraphs : bodyParagraphs)
.map((paragraph: Paragraph) => this.formatParagraph(paragraph, metadata))
.join('\n\n');
}
diff --git a/test/integration/chord_pro_to_chord_sheet.test.ts b/test/integration/chord_pro_to_chord_sheet.test.ts
index 2a1a18a1..1bde4718 100644
--- a/test/integration/chord_pro_to_chord_sheet.test.ts
+++ b/test/integration/chord_pro_to_chord_sheet.test.ts
@@ -73,7 +73,7 @@ Let it [Am]be
[G]wisdom, let it
{end_of_bridge}`.substring(1);
- const expectedHTML = `
+ const expectedText = `
Verse 1:
Am
Let it be
@@ -89,8 +89,102 @@ wisdom, let it`.substring(1);
const song = new ChordProParser().parse(chordSheet);
const formatted = new TextFormatter().format(song);
- console.warn(formatted);
+ expect(formatted).toEqual(expectedText);
+ });
+
+ it('can expand chorus directives when expandChorusDirective=true', () => {
+ const chordSheet = `
+{start_of_chorus: Chorus 1:}
+[C]Whisper words of
+{end_of_chorus}
+
+{start_of_verse: Verse 1:}
+Let it [Am]be
+{end_of_verse}
+
+{chorus: Repeat chorus 1:}
+
+{start_of_chorus: Chorus 2:}
+[G]wisdom, let it
+{end_of_chorus}
+
+{chorus: Repeat chorus 2:}
+
+{chorus: Repeat chorus 2 again:}`.substring(1);
+
+ const expectedText = `
+Chorus 1:
+C
+Whisper words of
+
+Verse 1:
+ Am
+Let it be
+
+Repeat chorus 1:
+C
+Whisper words of
+
+Chorus 2:
+G
+wisdom, let it
+
+Repeat chorus 2:
+G
+wisdom, let it
+
+Repeat chorus 2 again:
+G
+wisdom, let it`.substring(1);
+
+ const song = new ChordProParser().parse(chordSheet);
+ const formatted = new TextFormatter({ expandChorusDirective: true }).format(song);
+
+ expect(formatted).toEqual(expectedText);
+ });
+
+ it('does not expand chorus directives when expandChorusDirective=false', () => {
+ const chordSheet = `
+{start_of_chorus: Chorus 1:}
+[C]Whisper words of
+{end_of_chorus}
+
+{start_of_verse: Verse 1:}
+Let it [Am]be
+{end_of_verse}
+
+{chorus: Repeat chorus 1:}
+
+{start_of_chorus: Chorus 2:}
+[G]wisdom, let it
+{end_of_chorus}
+
+{chorus: Repeat chorus 2:}
+
+{chorus: Repeat chorus 2 again:}`.substring(1);
+
+ const expectedText = `
+Chorus 1:
+C
+Whisper words of
+
+Verse 1:
+ Am
+Let it be
+
+Repeat chorus 1:
+
+Chorus 2:
+G
+wisdom, let it
+
+Repeat chorus 2:
+
+Repeat chorus 2 again:`.substring(1);
+
+ const song = new ChordProParser().parse(chordSheet);
+ const formatted = new TextFormatter({ expandChorusDirective: false }).format(song);
- expect(formatted).toEqual(expectedHTML);
+ expect(formatted).toEqual(expectedText);
});
});
diff --git a/test/integration/chord_pro_to_html_div.test.ts b/test/integration/chord_pro_to_html_div.test.ts
index a81f043b..8db0c13a 100644
--- a/test/integration/chord_pro_to_html_div.test.ts
+++ b/test/integration/chord_pro_to_html_div.test.ts
@@ -220,4 +220,216 @@ Let it [Am]Be
expect(formatted).toEqual(expectedHTML);
});
+
+ it('can expand {chorus} directives when expandChorusDirective=true', () => {
+ const chordSheet = `
+{start_of_chorus: Chorus 1:}
+[C]Whisper words of
+{end_of_chorus}
+
+{start_of_verse: Verse 1:}
+Let it [Am]be
+{end_of_verse}
+
+{chorus: Repeat chorus 1:}
+
+{start_of_chorus: Chorus 2:}
+[G]wisdom, let it
+{end_of_chorus}
+
+{chorus: Repeat chorus 2:}
+
+{chorus: Repeat chorus 2 again:}`.substring(1);
+
+ const expectedText = stripHTML(`
+
+
+
+
+
+
Repeat chorus 1:
+
+
+
+
+
+
+
Repeat chorus 2:
+
+
+
+
+
+
Repeat chorus 2 again:
+
+
+
+
+ `);
+
+ const song = new ChordProParser().parse(chordSheet);
+ const formatted = new HtmlDivFormatter({ expandChorusDirective: true }).format(song);
+
+ expect(formatted).toEqual(expectedText);
+ });
+
+ it('does not expand {chorus} directives when expandChorusDirective=false', () => {
+ const chordSheet = `
+{start_of_chorus: Chorus 1:}
+[C]Whisper words of
+{end_of_chorus}
+
+{start_of_verse: Verse 1:}
+Let it [Am]be
+{end_of_verse}
+
+{chorus: Repeat chorus 1:}
+
+{start_of_chorus: Chorus 2:}
+[G]wisdom, let it
+{end_of_chorus}
+
+{chorus: Repeat chorus 2:}
+
+{chorus: Repeat chorus 2 again:}`.substring(1);
+
+ const expectedText = stripHTML(`
+
+
+
+
+
+
+
+
+
Repeat chorus 2 again:
+
+
+
+ `);
+
+ const song = new ChordProParser().parse(chordSheet);
+ const formatted = new HtmlDivFormatter({ expandChorusDirective: false }).format(song);
+
+ expect(formatted).toEqual(expectedText);
+ });
});
diff --git a/test/integration/chord_pro_to_html_table.test.ts b/test/integration/chord_pro_to_html_table.test.ts
index ac1f9b4e..67ee5618 100644
--- a/test/integration/chord_pro_to_html_table.test.ts
+++ b/test/integration/chord_pro_to_html_table.test.ts
@@ -223,4 +223,240 @@ Let it [Am]Be
expect(formatted).toEqual(expectedHTML);
});
+
+ it('can expand {chorus} directives when expandChorusDirective=true', () => {
+ const chordSheet = `
+{start_of_chorus: Chorus 1:}
+[C]Whisper words of
+{end_of_chorus}
+
+{start_of_verse: Verse 1:}
+Let it [Am]be
+{end_of_verse}
+
+{chorus: Repeat chorus 1:}
+
+{start_of_chorus: Chorus 2:}
+[G]wisdom, let it
+{end_of_chorus}
+
+{chorus: Repeat chorus 2:}
+
+{chorus: Repeat chorus 2 again:}`.substring(1);
+
+ const expectedText = stripHTML(`
+
+
+
+
+
+ C |
+ |
+
+
+ Whisper |
+ words of |
+
+
+
+
+
+
+
+ |
+ Am |
+
+
+ Let it |
+ be |
+
+
+
+
+
+
+
+ C |
+ |
+
+
+ Whisper |
+ words of |
+
+
+
+
+
+
+
+ G |
+ |
+
+
+ wisdom, |
+ let it |
+
+
+
+
+
+
+
+ G |
+ |
+
+
+ wisdom, |
+ let it |
+
+
+
+
+
+
+ Repeat chorus 2 again: |
+
+
+
+
+ G |
+ |
+
+
+ wisdom, |
+ let it |
+
+
+
+
+ `);
+
+ const song = new ChordProParser().parse(chordSheet);
+ const formatted = new HtmlTableFormatter({ expandChorusDirective: true }).format(song);
+
+ expect(formatted).toEqual(expectedText);
+ });
+
+ it('does not expand {chorus} directives when expandChorusDirective=false', () => {
+ const chordSheet = `
+{start_of_chorus: Chorus 1:}
+[C]Whisper words of
+{end_of_chorus}
+
+{start_of_verse: Verse 1:}
+Let it [Am]be
+{end_of_verse}
+
+{chorus: Repeat chorus 1:}
+
+{start_of_chorus: Chorus 2:}
+[G]wisdom, let it
+{end_of_chorus}
+
+{chorus: Repeat chorus 2:}
+
+{chorus: Repeat chorus 2 again:}`.substring(1);
+
+ const expectedText = stripHTML(`
+
+
+
+
+
+ C |
+ |
+
+
+ Whisper |
+ words of |
+
+
+
+
+
+
+
+ |
+ Am |
+
+
+ Let it |
+ be |
+
+
+
+
+
+
+
+
+ G |
+ |
+
+
+ wisdom, |
+ let it |
+
+
+
+
+
+
+
+ Repeat chorus 2 again: |
+
+
+
+
+ `);
+
+ const song = new ChordProParser().parse(chordSheet);
+ const formatted = new HtmlTableFormatter({ expandChorusDirective: false }).format(song);
+
+ expect(formatted).toEqual(expectedText);
+ });
});