diff --git a/README.md b/README.md index 5cf5335f..9aa5a3ee 100644 --- a/README.md +++ b/README.md @@ -319,10 +319,10 @@ use those to change the generated output. ### Chord diagrams -| Directive | Support | -|:--------- |:------------------------:| -| define | :heavy_multiplication_x: | -| chord | :heavy_multiplication_x: | +| Directive | Support | +|:--------- |:------------------:| +| define | :heavy_check_mark: | +| chord | :heavy_check_mark: | ### Fonts, sizes and colours diff --git a/doc/README.hbs b/doc/README.hbs index b85d8569..45bcd1b3 100644 --- a/doc/README.hbs +++ b/doc/README.hbs @@ -319,10 +319,10 @@ use those to change the generated output. ### Chord diagrams -| Directive | Support | -|:--------- |:------------------------:| -| define | :heavy_multiplication_x: | -| chord | :heavy_multiplication_x: | +| Directive | Support | +|:--------- |:------------------:| +| define | :heavy_check_mark: | +| chord | :heavy_check_mark: | ### Fonts, sizes and colours diff --git a/src/chord_sheet/chord_pro/chord_definition.ts b/src/chord_sheet/chord_pro/chord_definition.ts new file mode 100644 index 00000000..36c262f7 --- /dev/null +++ b/src/chord_sheet/chord_pro/chord_definition.ts @@ -0,0 +1,20 @@ +import { Fret } from '../../constants'; + +class ChordDefinition { + name: string; + + baseFret: number; + + frets: Fret[]; + + fingers: number[]; + + constructor(name: string, baseFret: number, frets: Fret[], fingers?: number[]) { + this.name = name; + this.baseFret = baseFret; + this.frets = frets; + this.fingers = fingers || []; + } +} + +export default ChordDefinition; diff --git a/src/chord_sheet/tag.ts b/src/chord_sheet/tag.ts index dddbe285..5918db1e 100644 --- a/src/chord_sheet/tag.ts +++ b/src/chord_sheet/tag.ts @@ -4,6 +4,7 @@ */ import AstComponent from './ast_component'; import TraceInfo from './trace_info'; +import ChordDefinition from './chord_pro/chord_definition'; export const ALBUM = 'album'; @@ -406,6 +407,8 @@ class Tag extends AstComponent { _value = ''; + chordDefinition?: ChordDefinition; + constructor(name: string, value: string | null = null, traceInfo: TraceInfo | null = null) { super(traceInfo); this.parseNameValue(name, value); diff --git a/src/chord_sheet_serializer.ts b/src/chord_sheet_serializer.ts index 82364afe..13a95c72 100644 --- a/src/chord_sheet_serializer.ts +++ b/src/chord_sheet_serializer.ts @@ -11,6 +11,7 @@ import Item from './chord_sheet/item'; import Evaluatable from './chord_sheet/chord_pro/evaluatable'; import { + SerializedChordDefinition, SerializedChordLyricsPair, SerializedComment, SerializedComponent, @@ -21,6 +22,7 @@ import { } from './serialized_types'; import SoftLineBreak from './chord_sheet/soft_line_break'; import { warn } from './utilities'; +import ChordDefinition from './chord_sheet/chord_pro/chord_definition'; const CHORD_LYRICS_PAIR = 'chordLyricsPair'; const CHORD_SHEET = 'chordSheet'; @@ -83,12 +85,27 @@ class ChordSheetSerializer { throw new Error(`Don't know how to serialize ${item.constructor.name}`); } - serializeTag(tag: Tag): SerializedTag { + serializeChordDefinition(chordDefinition: ChordDefinition): SerializedChordDefinition { return { + name: chordDefinition.name, + baseFret: chordDefinition.baseFret, + frets: chordDefinition.frets, + fingers: chordDefinition.fingers, + }; + } + + serializeTag(tag: Tag): SerializedTag { + const serializedTag: SerializedTag = { type: TAG, name: tag.originalName, value: tag.value, }; + + if (tag.chordDefinition) { + serializedTag.chordDefinition = this.serializeChordDefinition(tag.chordDefinition); + } + + return serializedTag; } serializeChordLyricsPair(chordLyricsPair: ChordLyricsPair) { @@ -194,8 +211,20 @@ class ChordSheetSerializer { name, value, location: { offset = null, line = null, column = null } = {}, + chordDefinition, } = astComponent; - return new Tag(name, value, { line, column, offset }); + const tag = new Tag(name, value, { line, column, offset }); + + if (chordDefinition) { + tag.chordDefinition = new ChordDefinition( + chordDefinition.name, + chordDefinition.baseFret, + chordDefinition.frets, + chordDefinition.fingers, + ); + } + + return tag; } parseComment(astComponent: SerializedComment): Comment { diff --git a/src/constants.ts b/src/constants.ts index 82903cfe..43af68d3 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -92,3 +92,9 @@ export const MINOR = 'm'; export const MAJOR = 'M'; export type Mode = 'M' | 'm'; + +type FretNumber = number; +type OpenFret = '0'; +type NonSoundingString = '-1' | 'N' | 'x'; + +export type Fret = FretNumber | OpenFret | NonSoundingString; diff --git a/src/parser/chord_pro/grammar.pegjs b/src/parser/chord_pro/grammar.pegjs index fffa446b..60f5f7c3 100644 --- a/src/parser/chord_pro/grammar.pegjs +++ b/src/parser/chord_pro/grammar.pegjs @@ -97,7 +97,8 @@ Line } Token - = Tag + = ChordDefinition + / Tag / AnnotationLyricsPair / chordLyricsPair:ChordLyricsPair / MetaTernary @@ -220,6 +221,62 @@ WordChar return sequence; } +ChordDefinition + = "{" _ name:("chord" / "define") _ ":" _ value:ChordDefinitionValue _ "}" { + const { text, ...chordDefinition } = value; + + return { + type: 'tag', + name, + value: text, + chordDefinition, + location: location().start, + }; + } + +ChordDefinitionValue + = name:$([A-Za-z0-9]+) _ "base-fret" __ baseFret:FretNumber __ "frets" frets:FretWithLeadingSpace+ fingers:ChordFingersDefinition? { + return { name, baseFret, frets, fingers, text: text() }; + } + +ChordFingersDefinition + = __ "fingers" fingers:FingerNumberWithLeadingSpace+ { + return fingers; + } + +FingerNumberWithLeadingSpace + = __ finger:FingerNumber { + return finger; + } + +FingerNumber + = number:[1-9] { + return parseInt(number, 10); + } + +FretWithLeadingSpace + = __ fret:Fret { + return fret; + } + +Fret + = _ fret:(FretNumber / OpenFret / NonSoundingString) { + return fret; + } + +FretNumber + = number:[1-9] { + return parseInt(number, 10); + } + +OpenFret + = "0" { + return 0; + } + +NonSoundingString + = "-1" / "N" / "x" + Tag = "{" _ tagName:$(TagName) _ tagColonWithValue: TagColonWithValue? _ "}" { return { @@ -252,8 +309,14 @@ TagValueChar return sequence; } -_ "whitespace" - = [ \t\n\r]* +__ "whitespace" + = WhitespaceCharacter+ + +_ "optional whitespace" + = WhitespaceCharacter* + +WhitespaceCharacter + = [ \t\n\r] Space "space" = [ \t]+ diff --git a/src/serialized_types.ts b/src/serialized_types.ts index 7fc43439..cb9509a2 100644 --- a/src/serialized_types.ts +++ b/src/serialized_types.ts @@ -1,4 +1,4 @@ -import { ChordType, Modifier } from './constants'; +import { ChordType, Fret, Modifier } from './constants'; export interface SerializedTraceInfo { location?: { @@ -26,10 +26,18 @@ export interface SerializedChordLyricsPair { annotation?: string | null, } +export interface SerializedChordDefinition { + name: string, + baseFret: number, + frets: Fret[], + fingers?: number[], +} + export type SerializedTag = SerializedTraceInfo & { type: 'tag', name: string, value: string, + chordDefinition?: SerializedChordDefinition, }; export interface SerializedComment { diff --git a/test/parser/chord_pro_parser.test.ts b/test/parser/chord_pro_parser.test.ts index da3ccc3c..7d24d0a5 100644 --- a/test/parser/chord_pro_parser.test.ts +++ b/test/parser/chord_pro_parser.test.ts @@ -5,6 +5,7 @@ import { LILYPOND, NONE, TAB, + Tag, Ternary, VERSE, } from '../../src'; @@ -652,4 +653,80 @@ Let it [Am]be expect(items[1]).toBeChordLyricsPair('', 'it be, let it '); expect(items[2]).toBeChordLyricsPair('C/G', 'be'); }); + + describe('{define} chord definitions', () => { + it('parses chord definitions with finger numbers', () => { + const chordSheet = '{define: D7 base-fret 3 frets x 3 2 3 1 x fingers 1 2 3 4 5 6 }'; + + const parser = new ChordProParser(); + const song = parser.parse(chordSheet); + const tag = song.lines[0].items[0]; + const { chordDefinition } = (tag as Tag); + + expect(tag).toBeTag('define', 'D7 base-fret 3 frets x 3 2 3 1 x fingers 1 2 3 4 5 6'); + + expect(chordDefinition).toEqual({ + name: 'D7', + baseFret: 3, + frets: ['x', 3, 2, 3, 1, 'x'], + fingers: [1, 2, 3, 4, 5, 6], + }); + }); + + it('parses chord definitions without finger numbers', () => { + const chordSheet = '{define: D7 base-fret 3 frets x 3 2 3 1 x }'; + + const parser = new ChordProParser(); + const song = parser.parse(chordSheet); + const tag = song.lines[0].items[0]; + const { chordDefinition } = (tag as Tag); + + expect(tag).toBeTag('define', 'D7 base-fret 3 frets x 3 2 3 1 x'); + + expect(chordDefinition).toEqual({ + name: 'D7', + baseFret: 3, + frets: ['x', 3, 2, 3, 1, 'x'], + fingers: [], + }); + }); + }); + + describe('{chord} chord definitions', () => { + it('parses chord definitions with finger numbers', () => { + const chordSheet = '{chord: D7 base-fret 3 frets x 3 2 3 1 x fingers 1 2 3 4 5 6 }'; + + const parser = new ChordProParser(); + const song = parser.parse(chordSheet); + const tag = song.lines[0].items[0]; + const { chordDefinition } = (tag as Tag); + + expect(tag).toBeTag('chord', 'D7 base-fret 3 frets x 3 2 3 1 x fingers 1 2 3 4 5 6'); + + expect(chordDefinition).toEqual({ + name: 'D7', + baseFret: 3, + frets: ['x', 3, 2, 3, 1, 'x'], + fingers: [1, 2, 3, 4, 5, 6], + }); + }); + + it('parses chord definitions without finger numbers', () => { + const chordSheet = '{chord: D7 base-fret 3 frets x 3 2 3 1 x }'; + + const parser = new ChordProParser(); + const song = parser.parse(chordSheet); + const tag = song.lines[0].items[0]; + const { chordDefinition } = (tag as Tag); + + expect(tag).toBeTag('chord', 'D7 base-fret 3 frets x 3 2 3 1 x'); + + expect(chordDefinition).toEqual({ + name: 'D7', + baseFret: 3, + frets: ['x', 3, 2, 3, 1, 'x'], + fingers: [], + }); + }); + }); });