Skip to content

Commit 84b33d8

Browse files
Parse conditional directives (#1500)
1 parent 5c50a3f commit 84b33d8

12 files changed

+139
-27
lines changed

script/build_chord_pro_section_grammar.ts

+18-12
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,32 @@ function sectionTags(sectionName: string, shortTag: boolean, type: 'start' | 'en
2424
}
2525

2626
export default function buildChordProSectionGrammar(_: BuildOptions, _data: string): string {
27-
const sectionsGrammars = sections.map(([name, shortTags]) => `
28-
${capitalize(name)}Section
29-
= startTag:${capitalize(name)}StartTag
27+
const sectionsGrammars = sections.map(([name, shortTags]) => {
28+
const sectionName = capitalize(name);
29+
const startTag = sectionTags(name, shortTags, 'start');
30+
const endTag = sectionTags(name, shortTags, 'end');
31+
32+
return `
33+
${sectionName}Section
34+
= startTag:${sectionName}StartTag
3035
NewLine
31-
content:$(!${capitalize(name)}EndTag SectionCharacter)*
32-
endTag:${capitalize(name)}EndTag
36+
content:$(!${sectionName}EndTag SectionCharacter)*
37+
endTag:${sectionName}EndTag
3338
{
3439
return helpers.buildSection(startTag, endTag, content);
3540
}
3641
37-
${capitalize(name)}StartTag
38-
= "{" _ tagName:(${sectionTags(name, shortTags, 'start')}) _ tagColonWithValue: TagColonWithValue? _ "}" {
39-
return helpers.buildTag(tagName, tagColonWithValue, location());
42+
${sectionName}StartTag
43+
= "{" _ tagName:(${startTag}) selector:TagSelector? _ tagColonWithValue:TagColonWithValue? _ "}" {
44+
return helpers.buildTag(tagName, tagColonWithValue, selector, location());
4045
}
4146
42-
${capitalize(name)}EndTag
43-
= "{" _ tagName:(${sectionTags(name, shortTags, 'end')}) _ "}" {
44-
return helpers.buildTag(tagName, null, location());
47+
${sectionName}EndTag
48+
= "{" _ tagName:(${endTag}) _ "}" {
49+
return helpers.buildTag(tagName, null, null, location());
4550
}
46-
`);
51+
`;
52+
});
4753

4854
return `
4955
Section

src/chord_sheet/line.ts

+2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class Line {
5454

5555
lineNumber: number | null = null;
5656

57+
selector: string | null = null;
58+
5759
/**
5860
* The text font that applies to this line. Is derived from the directives:
5961
* `textfont`, `textsize` and `textcolour`

src/chord_sheet/paragraph.ts

+18-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ import Literal from './chord_pro/literal';
44
import Tag from './tag';
55
import Item from './item';
66

7+
function getCommonValue(values: string[], fallback: string | null): string | null {
8+
const uniqueValues = [...new Set(values)];
9+
10+
if (uniqueValues.length === 1) {
11+
return uniqueValues[0];
12+
}
13+
14+
return fallback;
15+
}
16+
717
/**
818
* Represents a paragraph of lines in a chord sheet
919
*/
@@ -87,13 +97,16 @@ class Paragraph {
8797
*/
8898
get type(): string {
8999
const types = this.lines.map((line) => line.type);
90-
const uniqueTypes = [...new Set(types)];
100+
return getCommonValue(types, INDETERMINATE) as string;
101+
}
91102

92-
if (uniqueTypes.length === 1) {
93-
return uniqueTypes[0];
94-
}
103+
get selector(): string | null {
104+
const selectors =
105+
this.lines
106+
.map((line) => line.selector)
107+
.filter((selector) => selector !== null);
95108

96-
return INDETERMINATE;
109+
return getCommonValue(selectors, null);
97110
}
98111

99112
/**

src/chord_sheet/tag.ts

+2
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,8 @@ class Tag extends AstComponent {
411411

412412
chordDefinition?: ChordDefinition;
413413

414+
selector: string | null = null;
415+
414416
/**
415417
* The tag attributes. For example, section related tags can have a label:
416418
* `{start_of_verse: label="Verse 1"}`

src/chord_sheet_serializer.ts

+2
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,10 @@ class ChordSheetSerializer {
218218
location: { offset = null, line = null, column = null } = {},
219219
chordDefinition,
220220
attributes,
221+
selector,
221222
} = astComponent;
222223
const tag = new Tag(name, value, { line, column, offset }, attributes);
224+
tag.selector = selector || null;
223225

224226
if (chordDefinition) {
225227
tag.chordDefinition = new ChordDefinition(

src/parser/chord_pro/grammar.pegjs

+13-4
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ WordChar
157157
}
158158

159159
ChordDefinition
160-
= "{" _ name:("chord" / "define") _ ":" _ value:ChordDefinitionValue _ "}" {
160+
= "{" _ name:("chord" / "define") selector:TagSelector? _ ":" _ value:ChordDefinitionValue _ "}" {
161161
const { text, ...chordDefinition } = value;
162162

163163
return {
@@ -166,14 +166,23 @@ ChordDefinition
166166
value: text,
167167
chordDefinition,
168168
location: location().start,
169+
selector,
169170
};
170171
}
171172

172173
Tag
173-
= "{" _ tagName:$(TagName) _ tagColonWithValue:TagColonWithValue? "}" {
174-
return helpers.buildTag(tagName, tagColonWithValue, location());
174+
= "{" _ tagName:$(TagName) selector:TagSelector? _ tagColonWithValue:TagColonWithValue? "}" {
175+
return helpers.buildTag(tagName, tagColonWithValue, selector, location());
175176
}
176177

178+
TagSelector
179+
= "-" value:TagSelectorValue {
180+
return value;
181+
}
182+
183+
TagSelectorValue
184+
= $([a-zA-Z0-9-_]+)
185+
177186
TagColonWithValue
178187
= ":" tagValue:TagValue {
179188
return tagValue;
@@ -209,7 +218,7 @@ TagAttribute
209218
}
210219

211220
TagName
212-
= [a-zA-Z-_]+
221+
= [a-zA-Z_]+
213222

214223
TagSimpleValue
215224
= _ chars:TagValueChar* {

src/parser/chord_pro/helpers.ts

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export function buildSection(startTag: SerializedTag, endTag: SerializedTag, con
3232
export function buildTag(
3333
name: string,
3434
value: Partial<{ value: string | null, attributes: Record<string, string>}> | null,
35+
selector: string | null,
3536
location: FileRange,
3637
): SerializedTag {
3738
return {
@@ -40,6 +41,7 @@ export function buildTag(
4041
location: location.start,
4142
value: value?.value || '',
4243
attributes: value?.attributes || {},
44+
selector,
4345
};
4446
}
4547

src/serialized_types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export type SerializedTag = SerializedTraceInfo & {
3939
value: string,
4040
chordDefinition?: SerializedChordDefinition,
4141
attributes?: Record<string, string>,
42+
selector?: string | null,
4243
};
4344

4445
export interface SerializedComment {

src/song_builder.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class SongBuilder {
3030

3131
sectionType: string = NONE;
3232

33+
selector: string | null = null;
34+
3335
song: Song;
3436

3537
transposeKey: string | null = null;
@@ -61,17 +63,18 @@ class SongBuilder {
6163
this.lines.push(this.currentLine);
6264
}
6365

64-
this.setCurrentProperties(this.sectionType);
66+
this.setCurrentProperties(this.sectionType, this.selector);
6567
this.currentLine.transposeKey = this.transposeKey ?? this.currentKey;
6668
this.currentLine.key = this.currentKey || this.metadata.getSingle(KEY);
6769
this.currentLine.lineNumber = this.lines.length - 1;
6870
return this.currentLine;
6971
}
7072

71-
setCurrentProperties(sectionType: string): void {
73+
setCurrentProperties(sectionType: string, selector: string | null = null): void {
7274
if (!this.currentLine) throw new Error('Expected this.currentLine to be present');
7375

7476
this.currentLine.type = sectionType as LineType;
77+
this.currentLine.selector = selector;
7578
this.currentLine.textFont = this.fontStack.textFont.clone();
7679
this.currentLine.chordFont = this.fontStack.chordFont.clone();
7780
}
@@ -151,12 +154,14 @@ class SongBuilder {
151154
startSection(sectionType: string, tag: Tag): void {
152155
this.checkCurrentSectionType(NONE, tag);
153156
this.sectionType = sectionType;
154-
this.setCurrentProperties(sectionType);
157+
this.selector = tag.selector;
158+
this.setCurrentProperties(sectionType, tag.selector);
155159
}
156160

157161
endSection(sectionType: string, tag: Tag): void {
158162
this.checkCurrentSectionType(sectionType, tag);
159163
this.sectionType = NONE;
164+
this.selector = null;
160165
}
161166

162167
checkCurrentSectionType(sectionType: string, tag: Tag): void {

test/jest.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ declare global {
1717

1818
toBeComment(_contents: string): jest.CustomMatcherResult;
1919

20-
toBeTag(_name: string, _value?: string): jest.CustomMatcherResult;
20+
toBeTag(_name: string, _value?: string, _selector?: string): jest.CustomMatcherResult;
2121

2222
toBeSoftLineBreak(): jest.CustomMatcherResult;
2323
}

test/matchers.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ function toBeChordLyricsPair(received, chords, lyrics, annotation = '') {
119119
return toBeClassInstanceWithProperties(received, ChordLyricsPair, { chords, lyrics, annotation });
120120
}
121121

122-
function toBeTag(received, name, value = '') {
123-
return toBeClassInstanceWithProperties(received, Tag, { name, value });
122+
function toBeTag(received, name, value = '', selector = null) {
123+
return toBeClassInstanceWithProperties(received, Tag, { name, value, selector });
124124
}
125125

126126
function toBeComment(received, content) {

test/parser/chord_pro_parser.test.ts

+70
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,15 @@ This part is [G]key
337337
expect(song.title).toEqual('my {title}');
338338
});
339339

340+
it('allows conditional directives', () => {
341+
const chordSheet = '{title-guitar: Guitar song}';
342+
const song = new ChordProParser().parse(chordSheet);
343+
344+
const tag = song.lines[0].items[0] as Tag;
345+
346+
expect(tag).toBeTag('title', 'Guitar song', 'guitar');
347+
});
348+
340349
it('parses annotation', () => {
341350
const chordSheet = '[*Full band!]Let it be';
342351
const song = new ChordProParser().parse(chordSheet);
@@ -670,6 +679,32 @@ Let it [Am]be
670679
expect(lines[2].items[0]).toBeLiteral('LY line 2');
671680
});
672681

682+
it('parses conditional sections', () => {
683+
const chordSheet = heredoc`
684+
{start_of_ly-guitar: Intro}
685+
LY line 1
686+
LY line 2
687+
{end_of_ly}
688+
`;
689+
690+
const parser = new ChordProParser();
691+
const song = parser.parse(chordSheet);
692+
const { paragraphs } = song;
693+
const paragraph = paragraphs[0];
694+
const { lines } = paragraph;
695+
696+
expect(paragraphs).toHaveLength(1);
697+
expect(paragraph.type).toEqual(LILYPOND);
698+
expect(paragraph.selector).toEqual('guitar');
699+
expect(lines).toHaveLength(3);
700+
701+
expect(lines[0].items[0]).toBeTag('start_of_ly', 'Intro', 'guitar');
702+
expect(lines[1].items[0]).toBeLiteral('LY line 1');
703+
expect(lines[2].items[0]).toBeLiteral('LY line 2');
704+
705+
expect(lines.every((line) => line.selector === 'guitar')).toBe(true);
706+
});
707+
673708
it('parses soft line breaks when enabled', () => {
674709
const chordSheet = heredoc`
675710
[Am]Let it be,\\ let it [C/G]be
@@ -748,6 +783,23 @@ Let it [Am]be
748783
fingers: [],
749784
});
750785
});
786+
787+
it('parses conditional chord definitions', () => {
788+
const chordSheet = '{define-guitar: Am base-fret 1 frets 0 2 2 1 0 0}';
789+
const parser = new ChordProParser();
790+
const song = parser.parse(chordSheet);
791+
const tag = song.lines[0].items[0];
792+
const { chordDefinition } = (tag as Tag);
793+
794+
expect(tag).toBeTag('define', 'Am base-fret 1 frets 0 2 2 1 0 0', 'guitar');
795+
796+
expect(chordDefinition).toEqual({
797+
name: 'Am',
798+
baseFret: 1,
799+
frets: [0, 2, 2, 1, 0, 0],
800+
fingers: [],
801+
});
802+
});
751803
});
752804

753805
describe('{chord} chord definitions', () => {
@@ -786,5 +838,23 @@ Let it [Am]be
786838
fingers: [],
787839
});
788840
});
841+
842+
it('parses conditional chord definitions', () => {
843+
const chordSheet = '{chord-ukulele: D7 base-fret 3 frets x 3 2 3 1 x }';
844+
845+
const parser = new ChordProParser();
846+
const song = parser.parse(chordSheet);
847+
const tag = song.lines[0].items[0];
848+
const { chordDefinition } = (tag as Tag);
849+
850+
expect(tag).toBeTag('chord', 'D7 base-fret 3 frets x 3 2 3 1 x', 'ukulele');
851+
852+
expect(chordDefinition).toEqual({
853+
name: 'D7',
854+
baseFret: 3,
855+
frets: ['x', 3, 2, 3, 1, 'x'],
856+
fingers: [],
857+
});
858+
});
789859
});
790860
});

0 commit comments

Comments
 (0)