Skip to content

Commit

Permalink
Implement disable chop first word for chords over words (#1534)
Browse files Browse the repository at this point in the history
  • Loading branch information
martijnversluis authored Jan 3, 2025
1 parent f5c2dc0 commit 8d14aa2
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 33 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"build": "yarn unibuild",
"build:release": "yarn unibuild --force --release",
"ci": "yarn install && yarn unibuild ci",
"debug:chordpro": "yarn build && tsx script/debug_parser.ts chord_pro --skip-chord-grammar",
"debug:chordpro": "yarn build && tsx script/debug_parser.ts chord_pro",
"debug:chords-over-words": "yarn build && tsx script/debug_parser.ts chords_over_words --include-chord-grammar",
"eslint": "node_modules/.bin/eslint",
"lint": "yarn unibuild lint",
"prepare": "yarn install && yarn build",
Expand Down
7 changes: 4 additions & 3 deletions script/debug_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import esbuild from 'esbuild';

const parserName = process.argv[2];
const args = process.argv.slice(3);
const skipChordGrammar = args.includes('--skip-chord-grammar');
const includeChordGrammar = args.includes('--include-chord-grammar');
const includeSectionsGrammar = args.includes('--include-sections-grammar');

const parserFolder = `./src/parser/${parserName}`;
const grammarFile = `${parserFolder}/grammar.pegjs`;
Expand All @@ -19,10 +20,10 @@ const sectionsGrammarFile = './src/parser/chord_pro/sections_grammar.pegjs';
const chordDefinitionGrammarFile = './src/parser/chord_definition/grammar.pegjs';

const parserGrammar = fs.readFileSync(grammarFile, 'utf8');
const chordGrammar = skipChordGrammar ? '' : fs.readFileSync(chordGrammarFile);
const chordGrammar = includeChordGrammar ? fs.readFileSync(chordGrammarFile) : '';
const chordSuffixGrammar = fs.readFileSync(chordSuffixGrammarFile);
const whitespaceGrammar = fs.readFileSync(whitespaceGrammarFile);
const sectionsGrammar = fs.readFileSync(sectionsGrammarFile);
const sectionsGrammar = includeSectionsGrammar ? fs.readFileSync(sectionsGrammarFile) : '';
const chordDefinitionGrammar = fs.readFileSync(chordDefinitionGrammarFile);

const result = esbuild.buildSync({
Expand Down
28 changes: 8 additions & 20 deletions src/parser/chords_over_words/grammar.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ChordSheet

ChordSheetContents
= newLine:NewLine? lines:ChordSheetLineWithNewLine* trailingLine:ChordSheetLine? {
return helpers.composeChordSheetContents(newLine, lines, trailingLine);
return helpers.composeChordSheetContents(newLine, lines, trailingLine, options.chopFirstWord !== false);
}

ChordSheetLineWithNewLine
Expand Down Expand Up @@ -39,7 +39,7 @@ ChordsLine
}

RhythmSymbolWithSpacing
= _ symbol:RhythmSymbol _ {
= _S_ symbol:RhythmSymbol _S_ {
return symbol;
}

Expand All @@ -64,12 +64,12 @@ NonEmptyLyrics
= $(WordChar+)

ChordWithSpacing
= _ chord:Chord _ {
= _S_ chord:Chord _S_ {
return chord;
}

DirectionLine
= line:$(_ keyword:Keyword _ WordChar* _) {
= line:$(_S_ keyword:Keyword _S_ WordChar* _S_) {
return {
type: "line",
items: [
Expand Down Expand Up @@ -103,7 +103,7 @@ Metadata
}

InlineMetadata
= key:$(MetadataKey) _ Colon _ value:$(MetadataValue) {
= key:$(MetadataKey) _S_ Colon _S_ value:$(MetadataValue) {
return {
type: "line",
items: [
Expand All @@ -121,12 +121,12 @@ MetadataPair
= MetadataPairWithBrackets / MetadataPairWithoutBrackets

MetadataPairWithBrackets
= "{" _ pair:MetadataPairWithoutBrackets _ "}" {
= "{" _S_ pair:MetadataPairWithoutBrackets _S_ "}" {
return pair;
}

MetadataPairWithoutBrackets
= key:$(MetadataKey) _ Colon _ value:$(MetadataValue) {
= key:$(MetadataKey) _S_ Colon _S_ value:$(MetadataValue) {
return [key, value];
}

Expand All @@ -142,17 +142,5 @@ MetadataValue
MetadataSeparator
= "---" NewLine

_ "whitespace"
_S_ "whitespace"
= [ \t]*

NewLine
= CarriageReturn / LineFeed / CarriageReturnLineFeed

CarriageReturnLineFeed
= CarriageReturn LineFeed

LineFeed
= "\n"

CarriageReturn
= "\r"
28 changes: 19 additions & 9 deletions src/parser/chords_over_words/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { chopFirstWord } from '../parser_helpers';
import { chopFirstWord as chopFirstWordFunc } from '../parser_helpers';

import {
SerializedChord,
Expand Down Expand Up @@ -66,13 +66,14 @@ function chordProperties(chord: Chord): ChordProperties {
function constructChordLyricsPairs(
chords: Chord[],
lyrics: string,
chopFirstWord: boolean,
): (SerializedChordLyricsPair | SerializedSoftLineBreak)[] {
return chords.map((chord, i) => {
const nextChord = chords[i + 1];
const start = chord.column - 1;
const end = nextChord ? nextChord.column - 1 : lyrics.length;
const pairLyrics = lyrics.substring(start, end);
const [firstWord, rest] = chopFirstWord(pairLyrics);
const [firstWord, rest] = chopFirstWord ? chopFirstWordFunc(pairLyrics) : [pairLyrics, null];
const chordData = (chord.type === 'chord') ? { chord: chordProperties(chord) } : { chords: chord.value };

if (rest) {
Expand All @@ -86,11 +87,15 @@ function constructChordLyricsPairs(
}).flat();
}

function pairChordsWithLyrics(chordsLine: ChordsLine, lyricsLine: LyricsLine): SerializedLine {
function pairChordsWithLyrics(
chordsLine: ChordsLine,
lyricsLine: LyricsLine,
chopFirstWord: boolean,
): SerializedLine {
const { content: lyrics } = lyricsLine;

const chords = chordsLine.items as Chord[];
const chordLyricsPairs = constructChordLyricsPairs(chords, lyrics);
const chordLyricsPairs = constructChordLyricsPairs(chords, lyrics, chopFirstWord);
const firstChord = chords[0];

if (firstChord && firstChord.column > 1) {
Expand Down Expand Up @@ -151,29 +156,33 @@ function lyricsToLine(lyricsLine: LyricsLine): SerializedLine {
return { type: 'line', items: [] };
}

function buildLine(chordSheetLine: ChordSheetLine, nextLine: ChordSheetLine): [SerializedLine, boolean] {
function buildLine(
chordSheetLine: ChordSheetLine,
nextLine: ChordSheetLine,
chopFirstWord: boolean,
): [SerializedLine, boolean] {
const { type } = chordSheetLine;

if (type === 'lyricsLine') {
return [lyricsToLine(chordSheetLine), false];
} if (type === 'chordsLine') {
if (nextLine && nextLine.type === 'lyricsLine' && nextLine.content && nextLine.content.length > 0) {
return [pairChordsWithLyrics(chordSheetLine, nextLine), true];
return [pairChordsWithLyrics(chordSheetLine, nextLine, chopFirstWord), true];
}
return [chordsToLine(chordSheetLine), false];
}
return [chordSheetLine, false];
}

function arrangeChordSheetLines(chordSheetLines: ChordSheetLine[]): SerializedLine[] {
function arrangeChordSheetLines(chordSheetLines: ChordSheetLine[], chopFirstWord: boolean): SerializedLine[] {
const arrangedLines: SerializedLine[] = [];
let lineIndex = 0;
const lastLineIndex = chordSheetLines.length - 1;

while (lineIndex <= lastLineIndex) {
const chordSheetLine = chordSheetLines[lineIndex];
const nextLine = chordSheetLines[lineIndex + 1];
const [arrangedLine, skipNextLine] = buildLine(chordSheetLine, nextLine);
const [arrangedLine, skipNextLine] = buildLine(chordSheetLine, nextLine, chopFirstWord);
arrangedLines.push(arrangedLine);
lineIndex += (skipNextLine ? 2 : 1);
}
Expand All @@ -185,7 +194,8 @@ export function composeChordSheetContents(
newLine: NewLine | null,
lines: ChordSheetLine[],
trailingLine: ChordSheetLine | null,
chopFirstWord: boolean,
) {
const allLines = combineChordSheetLines(newLine, lines, trailingLine);
return arrangeChordSheetLines(allLines);
return arrangeChordSheetLines(allLines, chopFirstWord);
}
3 changes: 3 additions & 0 deletions src/parser/chords_over_words_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import NullTracer from './null_tracer';

export type ChordsOverWordsParserOptions = ParseOptions & {
softLineBreaks?: boolean;
chopFirstWord?: boolean;
};

/**
Expand Down Expand Up @@ -65,6 +66,8 @@ class ChordsOverWordsParser {
* @param {ChordsOverWordsParserOptions} options Parser options.
* @param {ChordsOverWordsParserOptions.softLineBreaks} options.softLineBreaks=false If true, a backslash
* followed by a space is treated as a soft line break
* @param {ChordsOverWordsParserOptions.chopFirstWord} options.chopFirstWord=false If true, only the first lyric
* word is paired with the chord, the rest of the lyric is put in a separate chord lyric pair
* @see https://peggyjs.org/documentation.html#using-the-parser
* @returns {Song} The parsed song
*/
Expand Down
17 changes: 17 additions & 0 deletions test/parser/chords_over_words_parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,4 +594,21 @@ describe('ChordsOverWordsParser', () => {
expect(lineItems[8]).toBeChordLyricsPair('C', 'be');
});
});

describe('with options chopFirstWord=false', () => {
it('does not chop the first word', () => {
const chordOverWords = heredoc`
Am C/G
Let it be, let it be`;

const parser = new ChordsOverWordsParser();
const song = parser.parse(chordOverWords, { chopFirstWord: false });
const { lines } = song;

const linePairs = lines[0].items;
expect(linePairs[0]).toBeChordLyricsPair('', 'Let it ');
expect(linePairs[1]).toBeChordLyricsPair('Am', 'be, let it ');
expect(linePairs[2]).toBeChordLyricsPair('C/G', 'be');
});
});
});
1 change: 1 addition & 0 deletions unibuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ unibuild((u: Builder) => {
'src/parser/chords_over_words/grammar.pegjs',
'src/parser/chord/base_grammar.pegjs',
chordSuffixGrammar,
'src/parser/whitespace_grammar.pegjs',
],
outfile: 'src/parser/chords_over_words/peg_parser.ts',
build: ({ release }: BuildOptions, ...grammars: string[]) => {
Expand Down

0 comments on commit 8d14aa2

Please sign in to comment.