Skip to content

Commit

Permalink
Start grammar for OnSong parser
Browse files Browse the repository at this point in the history
  • Loading branch information
bkeepers committed Feb 17, 2022
1 parent b2fa44b commit f96211a
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 6 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ lib
node_modules
src/formatter/templates/*.js
src/parser/chord_pro_peg_parser.js
src/parser/on_song_grammar.js
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ node_modules
lib
src/formatter/templates/*.js
src/parser/chord_pro_peg_parser.js
src/parser/on_song_grammar.js
.tool-versions
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"eslint-plugin-import": "^2.21.2",
"jest": "^27.0.1",
"jsdoc-to-markdown": "^7.1.0",
"pegjs": "^0.10.0",
"peggy": "^1.2.0",
"print": "^1.2.0"
},
"scripts": {
Expand All @@ -35,7 +35,7 @@
"build:template": "handlebars \"src/formatter/templates/$TEMPLATE.handlebars\" -f \"src/formatter/templates/$TEMPLATE.js\" --known each --known if --known with --known paragraphClasses --known isChordLyricsPair --known isTag --known isComment --known shouldRenderLine --known hasChordContents --known lineHasContents --known lineClasses --known toUpperCase --known paragraphClasses --commonjs handlebars",
"build:templates": "TEMPLATE=html_div_formatter yarn build:template && TEMPLATE=html_table_formatter yarn build:template",
"build:sources": "rm -rf lib && babel src --out-dir lib",
"build:pegjs": "pegjs -o src/parser/chord_pro_peg_parser.js src/parser/chord_pro_grammar.pegjs",
"build:pegjs": "peggy -o src/parser/chord_pro_peg_parser.js src/parser/chord_pro_grammar.pegjs && peggy src/parser/on_song_grammar.pegjs",
"build": "yarn build:templates && yarn build:pegjs && yarn build:sources",
"readme": "jsdoc2md -f src/**/*.js -f src/*.js --template doc/README.hbs > README.md",
"prepublishOnly": "yarn install && yarn test && yarn build",
Expand Down
110 changes: 110 additions & 0 deletions src/parser/on_song_grammar.pegjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// https://www.onsongapp.com/docs/features/formats/onsong/

ChordSheet
= metadata:Metadata __ sections:Section* {
return { type: "chordsheet", metadata, sections }
}

// https://www.onsongapp.com/docs/features/formats/onsong/metadata/
Metadata
// First line is implicitly title
= first:(!Metatag value:MetaValue EOL {
return { type: "metatag", name: "title", value }
})?
// Second line is implicitly artist
second:(!Metatag value:MetaValue EOL {
return { type: "metatag", name: "artist", value }
})?
rest:(@Metatag __)*
{
return [ first, second, ...rest ].filter(Boolean)
}

Metatag "metatag"
= name:MetaName _ ":" _ value:MetaValue EOL {
return { type: "metatag", name: name.toLowerCase().trim(), value }
}

MetaName
= $[^\r\n:]+

MetaValue
= $[^\r\n]+

// https://www.onsongapp.com/docs/features/formats/onsong/section/
Section
// Section with explcit name and maybe a body
= name:SectionName __ items:SectionBody* { return { type: 'section', name, items } }
// Section without explicit name
/ items:SectionBody+ { return { type: 'section', name: null, items } }

SectionName
= name:MetaName _ ":" EOL {
return name.trim()
}

SectionBody
= @Stanza __

Stanza
= lines:Line+ {
return { type: 'stanza', lines }
}

Line
= parts:(ChordLyricsPair / JustChords / JustLyrics)+ EOL {
return {type: 'line', parts }
}

JustChords
= chords:Chord {
return { type: "ChordLyricsPair", chords, lyrics: "" }
}

JustLyrics
= lyrics:Lyrics {
return { type: "ChordLyricsPair", lyrics, chords: "" }
}

ChordLyricsPair
= chords:Chord lyrics:Lyrics {
return { type: "ChordLyricsPair", chords: chords || '', lyrics }
}

Chord "chord"
= !Escape "[" chords:$(ChordChar*) "]" {
return chords;
}

ChordChar
= [^\]]

Lyrics "lyrics"
= $(Char+)

// MusicalInstruction // e.g. (Repeat 8x)
// = "(" [^)]+ ")"

Char
= [^\|\[\]\\#\r\n]

Escape
= "\\"

Space
= "\t" / " "

NewLine "new line"
= "\r\n" / "\r" / "\n"

EOL "end of line" // Strict linebreak or end of file
= _ (NewLine / EOF)

EOF
= !.

_ // Insignificant space
= Space*

__ // Insignificant whitespace
= (Space / NewLine)*
207 changes: 207 additions & 0 deletions test/parser/on_song_grammar.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import * as peggy from 'peggy';
import '../matchers';
import { readFileSync } from 'fs';

describe('OnSongGrammar', () => {
const examples = {
Metadata: {
// First and second lines are assumed to be title / artist
'Song Title\nArtist Name': [
{ type: 'metatag', name: 'title', value: 'Song Title' },
{ type: 'metatag', name: 'artist', value: 'Artist Name' },
],
'Song Title\nArtist:Artist Name': [
{ type: 'metatag', name: 'title', value: 'Song Title' },
{ type: 'metatag', name: 'artist', value: 'Artist Name' },
],
'Song Title\nTime:3/4': [
{ type: 'metatag', name: 'title', value: 'Song Title' },
{ type: 'metatag', name: 'time', value: '3/4' },
],
'title: Song Title\nArtist: Artist Name': [
{ type: 'metatag', name: 'title', value: 'Song Title' },
{ type: 'metatag', name: 'artist', value: 'Artist Name' },
],
// "before the first blank line or until no more metatags are encountered"
'A: 1\n\nB:2\n\n': [
{ type: 'metatag', name: 'a', value: '1' },
{ type: 'metatag', name: 'b', value: '2' },
],
// TODO:
// "{title: ChordPro}": [
// { type: 'metatag', name: 'title', value: 'ChordPro' },
// ]
// "Notes:" : { // Known metatag without a value
// { type: 'metatag', name: 'Notes', value: '' },
// }
'Unknown Tag:': Error, // Unknown metatag without a value
},

// Inline Tags - https://www.onsongapp.com/docs/features/formats/onsong/metadata/?#inline-tags

SectionName: {
'Chorus:': 'Chorus',
'Verse 1:\n': 'Verse 1',
'Intro :': 'Intro',
'Intro: ': 'Intro',
},

Section: {
'Chorus:\nThis is a stanza\n\nThis is another stanza': {
type: 'section',
name: 'Chorus',
items: [
{
type: 'stanza',
lines: [
{ type: 'line', parts: [{ type: 'ChordLyricsPair', chords: '', lyrics: 'This is a stanza' }] }
],
},
{
type: 'stanza',
lines: [
{ type: 'line', parts: [{ type: 'ChordLyricsPair', chords: '', lyrics: 'This is another stanza' }] },
],
},
],
},

'Intro:\n\n': {
type: 'section',
name: 'Intro',
items: [],
},

'Intro:\n\n\n[G]': {
type: 'section',
name: 'Intro',
items: [
{
type: 'stanza',
lines: [{ type: 'line', parts: [{ type: 'ChordLyricsPair', chords: 'G', lyrics: '' }] }],
},
],
},

'Chord and lyrics': {
type: 'section',
name: null,
items: [
{
type: 'stanza',
lines: [
{ type: 'line', parts: [{ type: 'ChordLyricsPair', chords: '', lyrics: 'Chord and lyrics' }] },
],
},
],
},
},

Line: {
'This [D]is a s[G]ong,': {
type: 'line',
parts: [
{ type: 'ChordLyricsPair', chords: '', lyrics: 'This ' },
{ type: 'ChordLyricsPair', chords: 'D', lyrics: 'is a s' },
{ type: 'ChordLyricsPair', chords: 'G', lyrics: 'ong,' },
],
},
'Ends with a chord [D]': {
type: 'line',
parts: [
{ type: 'ChordLyricsPair', chords: '', lyrics: 'Ends with a chord ' },
{ type: 'ChordLyricsPair', chords: 'D', lyrics: '' },
],
},
'[D]Starts with a chord': {
type: 'line',
parts: [
{ type: 'ChordLyricsPair', chords: 'D', lyrics: 'Starts with a chord' },
],
},
'Just lyrics': {
type: 'line',
parts: [
{ type: 'ChordLyricsPair', chords: '', lyrics: 'Just lyrics' },
],
},

'[G]': {
type: 'line',
parts: [{ type: 'ChordLyricsPair', chords: 'G', lyrics: '' }],
},
},

Chord: {
'[G]': 'G',
'[D/F#]': 'D/F#',
'[Bsus2]': 'Bsus2',
'\\[notachord]': Error,
},

ChordSheet: {
'Title\n\nChord and lyrics': {
type: 'chordsheet',
metadata: [{ type: 'metatag', name: 'title', value: 'Title' }],
sections: [
{
type: 'section',
items: [
{
lines: [
{ parts: [{ chords: '', lyrics: 'Chord and lyrics', type: 'ChordLyricsPair' }], type: 'line' },
],
type: 'stanza',
},
],
name: null,
},
],
},
'Title\n\nIntro:\n': {
type: 'chordsheet',
metadata: [{ type: 'metatag', name: 'title', value: 'Title' }],
sections: [
{
type: 'section',
name: 'Intro',
items: [],
},
],
},
'Tempo: 73\nUnknown(s): Value:with@various:characters1-5\n\nChorus:': {
type: 'chordsheet',
metadata: [
{ name: 'tempo', type: 'metatag', value: '73' },
{ name: 'unknown(s)', type: 'metatag', value: 'Value:with@various:characters1-5' },
],
sections: [
{ items: [], name: 'Chorus', type: 'section' },
],
},
},
};

const grammar = readFileSync('src/parser/on_song_grammar.pegjs', { encoding: 'utf-8' });
const { parse } = peggy.generate(grammar, {
// Allow starting with these in tests
allowedStartRules: Object.keys(examples),
});

Object.entries(examples).forEach(([startRule, ruleExamples]) => {
describe(startRule, () => {
Object.entries(ruleExamples).forEach(([input, expected]) => {
test(JSON.stringify(input), () => {
try {
const actual = parse(input, { startRule });
expect(actual).toEqual(expected);
} catch (e) {
if (expected !== Error) {
throw e;
}
}
});
});
});
});
});
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3931,10 +3931,10 @@ path-parse@^1.0.6:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==

pegjs@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/pegjs/-/pegjs-0.10.0.tgz#cf8bafae6eddff4b5a7efb185269eaaf4610ddbd"
integrity sha1-z4uvrm7d/0tafvsYUmnqr0YQ3b0=
peggy@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/peggy/-/peggy-1.2.0.tgz#657ba45900cbef1dc9f52356704bdbb193c2021c"
integrity sha512-PQ+NKpAobImfMprYQtc4Egmyi29bidRGEX0kKjCU5uuW09s0Cthwqhfy7mLkwcB4VcgacE5L/ZjruD/kOPCUUw==

picocolors@^1.0.0:
version "1.0.0"
Expand Down

0 comments on commit f96211a

Please sign in to comment.