@k-l-lambda/lilylet 0.1.62 → 0.1.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/abc/grammar.jison.js +300 -187
- package/lib/lilylet/abcDecoder.js +40 -12
- package/lib/lilylet/lilypondEncoder.js +3 -0
- package/lib/lilylet/meiEncoder.js +29 -8
- package/package.json +5 -2
- package/source/abc/abc.jison +90 -15
- package/source/abc/grammar.jison.js +300 -187
- package/source/lilylet/abcDecoder.ts +42 -14
- package/source/lilylet/lilypondEncoder.ts +2 -0
- package/source/lilylet/meiEncoder.ts +30 -8
|
@@ -695,13 +695,13 @@ const convertEventTerm = (eventTerm, unitLength, pendingMarks, pendingContextCha
|
|
|
695
695
|
return undefined;
|
|
696
696
|
const firstPitch = chord.pitches[0];
|
|
697
697
|
// Check if rest
|
|
698
|
-
if (firstPitch.phonet === "z" || firstPitch.phonet === "Z" || firstPitch.phonet === "x") {
|
|
698
|
+
if (firstPitch.phonet === "z" || firstPitch.phonet === "Z" || firstPitch.phonet === "x" || firstPitch.phonet === "y") {
|
|
699
699
|
const duration = convertDuration(eventData.duration, unitLength);
|
|
700
700
|
const rest = {
|
|
701
701
|
type: "rest",
|
|
702
702
|
duration,
|
|
703
703
|
};
|
|
704
|
-
if (firstPitch.phonet === "x") {
|
|
704
|
+
if (firstPitch.phonet === "x" || firstPitch.phonet === "y") {
|
|
705
705
|
rest.invisible = true;
|
|
706
706
|
}
|
|
707
707
|
if (firstPitch.phonet === "Z") {
|
|
@@ -712,7 +712,7 @@ const convertEventTerm = (eventTerm, unitLength, pendingMarks, pendingContextCha
|
|
|
712
712
|
return rest;
|
|
713
713
|
}
|
|
714
714
|
// Note or chord
|
|
715
|
-
const pitches = chord.pitches.filter(p => p.phonet !== "z" && p.phonet !== "Z" && p.phonet !== "x").map(convertPitch);
|
|
715
|
+
const pitches = chord.pitches.filter(p => p.phonet !== "z" && p.phonet !== "Z" && p.phonet !== "x" && p.phonet !== "y").map(convertPitch);
|
|
716
716
|
if (pitches.length === 0)
|
|
717
717
|
return undefined;
|
|
718
718
|
const duration = convertDuration(eventData.duration, unitLength);
|
|
@@ -781,6 +781,14 @@ const decodeTune = (tune) => {
|
|
|
781
781
|
let tempo;
|
|
782
782
|
const voiceConfigs = new Map();
|
|
783
783
|
const voiceClefs = new Map();
|
|
784
|
+
// Pre-scan for unit length (needed for bare Q: tempo)
|
|
785
|
+
for (const h of headers) {
|
|
786
|
+
const hdr = h;
|
|
787
|
+
if (hdr.name === "L" && hdr.value?.numerator && hdr.value?.denominator) {
|
|
788
|
+
unitLength = hdr.value;
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
784
792
|
for (const h of headers) {
|
|
785
793
|
if (h.comment)
|
|
786
794
|
continue;
|
|
@@ -822,25 +830,45 @@ const decodeTune = (tune) => {
|
|
|
822
830
|
tempo = { beat: beatDuration, bpm: header.value.bpm };
|
|
823
831
|
}
|
|
824
832
|
else if (typeof header.value === "number") {
|
|
825
|
-
|
|
833
|
+
const beat = convertDuration({ numerator: 1, denominator: 1 }, unitLength);
|
|
834
|
+
tempo = { beat, bpm: header.value };
|
|
826
835
|
}
|
|
827
836
|
break;
|
|
828
837
|
case "V": {
|
|
829
838
|
const voiceValue = header.value;
|
|
830
839
|
if (voiceValue) {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
840
|
+
let voiceId;
|
|
841
|
+
let clefStr;
|
|
842
|
+
if (typeof voiceValue === "number") {
|
|
843
|
+
voiceId = voiceValue;
|
|
844
|
+
}
|
|
845
|
+
else if (typeof voiceValue === "string") {
|
|
846
|
+
voiceId = voiceValue;
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
const rawClef = (voiceValue.clef || "").replace(/,+$/, "").trim();
|
|
850
|
+
const isKnownClef = !!convertClef(rawClef);
|
|
851
|
+
if (isKnownClef) {
|
|
852
|
+
// V:1 treble → voiceId=number, clef=treble
|
|
853
|
+
voiceId = voiceValue.name || 1;
|
|
854
|
+
clefStr = rawClef;
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
// V:S clef=treble → voiceId=voiceName, clef from properties
|
|
858
|
+
voiceId = rawClef || voiceValue.name || 1;
|
|
859
|
+
const propClef = (voiceValue.properties?.clef || "").replace(/,+$/, "").trim();
|
|
860
|
+
clefStr = propClef || undefined;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
voiceConfigs.set(voiceId, {
|
|
864
|
+
name: typeof voiceId === "number" ? voiceId : 1,
|
|
837
865
|
clef: clefStr,
|
|
838
|
-
properties: voiceValue
|
|
866
|
+
properties: voiceValue?.properties,
|
|
839
867
|
});
|
|
840
868
|
if (clefStr) {
|
|
841
869
|
const clef = convertClef(clefStr);
|
|
842
870
|
if (clef)
|
|
843
|
-
voiceClefs.set(
|
|
871
|
+
voiceClefs.set(voiceId, clef);
|
|
844
872
|
}
|
|
845
873
|
}
|
|
846
874
|
break;
|
|
@@ -469,6 +469,9 @@ const encodeTupletEvent = (event, env, lastDuration) => {
|
|
|
469
469
|
newEnv = ne;
|
|
470
470
|
newDuration = nd;
|
|
471
471
|
}
|
|
472
|
+
else if (subEvent.type === 'context') {
|
|
473
|
+
result += encodeContextChange(subEvent) + ' ';
|
|
474
|
+
}
|
|
472
475
|
}
|
|
473
476
|
result += '}';
|
|
474
477
|
return { str: result, newEnv, newDuration };
|
|
@@ -538,7 +538,7 @@ const tupletHasInternalBeams = (event) => {
|
|
|
538
538
|
return starts > 0 && starts === ends;
|
|
539
539
|
};
|
|
540
540
|
// Convert TupletEvent to MEI
|
|
541
|
-
const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff, ottavaShift = 0, inParentBeam = false, measureAccidentals) => {
|
|
541
|
+
const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff, ottavaShift = 0, inParentBeam = false, measureAccidentals, currentClef) => {
|
|
542
542
|
// LilyPond \times 2/3 means "multiply duration by 2/3"
|
|
543
543
|
// So 3 notes × 2/3 = 2 beats worth (3 in time of 2)
|
|
544
544
|
// MEI: num = number of notes written, numbase = normal equivalent
|
|
@@ -561,6 +561,8 @@ const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff
|
|
|
561
561
|
// Handle internal beam groups: if notes have manual beam marks, respect them
|
|
562
562
|
const hasInternalBeams = !inParentBeam && tupletHasInternalBeams(event);
|
|
563
563
|
let beamOpen = false;
|
|
564
|
+
let activeClef = currentClef;
|
|
565
|
+
let endingClef;
|
|
564
566
|
for (const e of event.events) {
|
|
565
567
|
if (e.type === 'note') {
|
|
566
568
|
const noteEvent = e;
|
|
@@ -607,13 +609,27 @@ const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff
|
|
|
607
609
|
const restIndent = beamOpen ? baseIndent + ' ' : baseIndent;
|
|
608
610
|
xml += restEventToMEI(e, restIndent, keyFifths, ottavaShift, measureAccidentals);
|
|
609
611
|
}
|
|
612
|
+
else if (e.type === 'context') {
|
|
613
|
+
const ctx = e;
|
|
614
|
+
if (ctx.clef && ctx.clef !== activeClef) {
|
|
615
|
+
const layerStaffNum = layerStaff || 1;
|
|
616
|
+
const effectiveStaffNum = effectiveStaff ?? layerStaffNum;
|
|
617
|
+
if (effectiveStaffNum === layerStaffNum) {
|
|
618
|
+
const clefIndent = beamOpen ? baseIndent + ' ' : baseIndent;
|
|
619
|
+
const clefInfo = CLEF_SHAPES[ctx.clef] || CLEF_SHAPES.treble;
|
|
620
|
+
xml += `${clefIndent}<clef xml:id="${generateId('clef')}" shape="${clefInfo.shape}" line="${clefInfo.line}" />\n`;
|
|
621
|
+
}
|
|
622
|
+
activeClef = ctx.clef;
|
|
623
|
+
endingClef = ctx.clef;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
610
626
|
}
|
|
611
627
|
// Close any unclosed beam
|
|
612
628
|
if (beamOpen) {
|
|
613
629
|
xml += `${baseIndent}</beam>\n`;
|
|
614
630
|
}
|
|
615
631
|
xml += `${indent}</tuplet>\n`;
|
|
616
|
-
return { xml, firstNoteId, slurStarts, slurEnds, dynamics, fermatas, trills, mordents, turns, arpeggios };
|
|
632
|
+
return { xml, firstNoteId, slurStarts, slurEnds, dynamics, fermatas, trills, mordents, turns, arpeggios, endingClef };
|
|
617
633
|
};
|
|
618
634
|
// Convert TremoloEvent to MEI (fingered tremolo - alternating between two notes)
|
|
619
635
|
const tremoloEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0, measureAccidentals) => {
|
|
@@ -688,7 +704,7 @@ const getEventBeamMarks = (event) => {
|
|
|
688
704
|
const markOptions = extractMarkOptions(event.marks);
|
|
689
705
|
return { beamStart: markOptions.beamStart, beamEnd: markOptions.beamEnd };
|
|
690
706
|
}
|
|
691
|
-
if (event.type === 'tuplet') {
|
|
707
|
+
if (event.type === 'tuplet' || event.type === 'times') {
|
|
692
708
|
const tuplet = event;
|
|
693
709
|
// If the tuplet has internal beam groups, don't report beam marks to the parent
|
|
694
710
|
// so the parent won't wrap the tuplet in an external <beam>
|
|
@@ -923,11 +939,16 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
|
|
|
923
939
|
xml += restEventToMEI(event, currentIndent, keyFifths, currentOttavaShift, measureAccidentals, restCrossStaff);
|
|
924
940
|
break;
|
|
925
941
|
}
|
|
926
|
-
case 'tuplet':
|
|
942
|
+
case 'tuplet':
|
|
943
|
+
case 'times': {
|
|
927
944
|
// Tuplet can be nested inside beam in MEI: <beam><tuplet>...</tuplet></beam>
|
|
928
945
|
// Pass beamElementOpen to tuplet so it knows not to create its own beam
|
|
929
|
-
const tupletResult = tupletEventToMEI(event, currentIndent, voice.staff, keyFifths, currentStaff, currentOttavaShift, beamElementOpen, measureAccidentals);
|
|
946
|
+
const tupletResult = tupletEventToMEI(event, currentIndent, voice.staff, keyFifths, currentStaff, currentOttavaShift, beamElementOpen, measureAccidentals, currentClef);
|
|
930
947
|
xml += tupletResult.xml;
|
|
948
|
+
// Propagate clef change from inside the tuplet to the parent tracker
|
|
949
|
+
if (tupletResult.endingClef) {
|
|
950
|
+
currentClef = tupletResult.endingClef;
|
|
951
|
+
}
|
|
931
952
|
// Flush any pending markups onto the first note of the tuplet
|
|
932
953
|
if (tupletResult.firstNoteId) {
|
|
933
954
|
flushPendingMarkups(tupletResult.firstNoteId);
|
|
@@ -1566,7 +1587,7 @@ const docHasBeamMarks = (doc) => {
|
|
|
1566
1587
|
}
|
|
1567
1588
|
}
|
|
1568
1589
|
}
|
|
1569
|
-
else if (event.type === 'tuplet') {
|
|
1590
|
+
else if (event.type === 'tuplet' || event.type === 'times') {
|
|
1570
1591
|
const tuplet = event;
|
|
1571
1592
|
for (const e of tuplet.events) {
|
|
1572
1593
|
if (e.type === 'note') {
|
|
@@ -1729,7 +1750,7 @@ const applyAutoBeamToVoice = (events, beamGroups) => {
|
|
|
1729
1750
|
flushRun();
|
|
1730
1751
|
position += dur;
|
|
1731
1752
|
}
|
|
1732
|
-
else if (event.type === 'tuplet') {
|
|
1753
|
+
else if (event.type === 'tuplet' || event.type === 'times') {
|
|
1733
1754
|
const tuplet = event;
|
|
1734
1755
|
const ratio = tuplet.ratio; // LilyPond ratio: num/den
|
|
1735
1756
|
// Check if all inner notes are beamable (division >= 8)
|
|
@@ -1907,7 +1928,7 @@ const encode = (doc, options = {}) => {
|
|
|
1907
1928
|
for (const event of voice.events) {
|
|
1908
1929
|
// Check for actual musical content (not just context changes or pitch resets)
|
|
1909
1930
|
if (event.type === 'note' || event.type === 'rest' ||
|
|
1910
|
-
event.type === 'tuplet' || event.type === 'tremolo') {
|
|
1931
|
+
event.type === 'tuplet' || event.type === 'times' || event.type === 'tremolo') {
|
|
1911
1932
|
return true;
|
|
1912
1933
|
}
|
|
1913
1934
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k-l-lambda/lilylet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
4
4
|
"description": "Lilylet is a lilyopnd-like sheet music language designed for Markdown rendering and symbolic music representation in AIGC applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -29,11 +29,13 @@
|
|
|
29
29
|
"prepublishOnly": "npm run build:grammar && npm run build",
|
|
30
30
|
"test": "tsx ./tests/parser.ts",
|
|
31
31
|
"test:mei": "tsx ./tests/mei.ts",
|
|
32
|
+
"test:mei-hashes": "tsx ./tests/computeMeiHashes.ts",
|
|
32
33
|
"test:unit": "tsx ./tests/unit/encodePitch.test.ts",
|
|
33
34
|
"test:partial": "tsx ./tests/unit/partialWarning.test.ts",
|
|
34
35
|
"test:decoder": "tsx ./tests/lilypondDecoder.ts",
|
|
35
36
|
"test:abc": "tsx ./tests/abc-decoder.ts",
|
|
36
37
|
"test:roundtrip": "tsx ./tests/lilypond-roundtrip.ts",
|
|
38
|
+
"test:abc-svg": "tsx ./tests/abc-abcjs-svg.ts",
|
|
37
39
|
"build:tests": "tsc -p tsconfig.tests.json; cp source/lilylet/grammar.jison.js lib-tests/source/lilylet/ && cp source/abc/grammar.jison.js lib-tests/source/abc/ && node tools/fixEsmExtensions.cjs lib-tests && ln -sfn ../../tests/assets lib-tests/tests/assets",
|
|
38
40
|
"test:roundtrip:compiled": "node lib-tests/tests/lilypond-roundtrip.js",
|
|
39
41
|
"ts": "tsx"
|
|
@@ -51,14 +53,15 @@
|
|
|
51
53
|
"devDependencies": {
|
|
52
54
|
"@types/node": "^20.11.20",
|
|
53
55
|
"@types/yargs": "^17.0.32",
|
|
56
|
+
"abcjs": "^6.6.2",
|
|
54
57
|
"formidable": "^3.5.4",
|
|
55
58
|
"jison": "^0.4.18",
|
|
59
|
+
"jsdom": "^29.0.2",
|
|
56
60
|
"sha1": "^1.1.1",
|
|
57
61
|
"ts-node": "^10.9.2",
|
|
58
62
|
"tsx": "^4.21.0",
|
|
59
63
|
"typescript": "^5.3.3",
|
|
60
64
|
"verovio": "^5.7.0",
|
|
61
|
-
"xmldom": "^0.6.0",
|
|
62
65
|
"yargs": "^17.7.2"
|
|
63
66
|
},
|
|
64
67
|
"dependencies": {
|
package/source/abc/abc.jison
CHANGED
|
@@ -82,18 +82,24 @@
|
|
|
82
82
|
const {patches} = body;
|
|
83
83
|
const measures = [];
|
|
84
84
|
let measure = null;
|
|
85
|
-
|
|
85
|
+
const seenVoices = new Set();
|
|
86
|
+
|
|
86
87
|
patches.forEach(patch => {
|
|
87
88
|
const voice = patch.control.V || 1;
|
|
88
|
-
if (voice
|
|
89
|
+
if (seenVoices.has(voice)) {
|
|
89
90
|
if (measure)
|
|
90
91
|
measures.push(measure);
|
|
91
92
|
measure = {voices: []};
|
|
93
|
+
seenVoices.clear();
|
|
92
94
|
}
|
|
95
|
+
if (!measure)
|
|
96
|
+
measure = {voices: []};
|
|
97
|
+
seenVoices.add(voice);
|
|
93
98
|
measure.voices.push(patch);
|
|
94
99
|
});
|
|
95
100
|
|
|
96
|
-
|
|
101
|
+
if (measure)
|
|
102
|
+
measures.push(measure);
|
|
97
103
|
|
|
98
104
|
measures.forEach((measure, index) => measure.index = index + 1);
|
|
99
105
|
|
|
@@ -151,6 +157,21 @@
|
|
|
151
157
|
|
|
152
158
|
|
|
153
159
|
const octaveShift = shift => ({octaveShift: shift});
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
const parseMode = (name) => {
|
|
163
|
+
const n = name.toLowerCase();
|
|
164
|
+
if (n.startsWith("ma")) return "major";
|
|
165
|
+
if (n === "m" || n.startsWith("mi")) return "minor";
|
|
166
|
+
if (n.startsWith("dor")) return "dorian";
|
|
167
|
+
if (n.startsWith("phr")) return "phrygian";
|
|
168
|
+
if (n.startsWith("lyd")) return "lydian";
|
|
169
|
+
if (n.startsWith("mix")) return "mixolydian";
|
|
170
|
+
if (n.startsWith("aeo")) return "aeolian";
|
|
171
|
+
if (n.startsWith("loc")) return "locrian";
|
|
172
|
+
if (n === "hp") return "highland";
|
|
173
|
+
return n;
|
|
174
|
+
};
|
|
154
175
|
%}
|
|
155
176
|
|
|
156
177
|
|
|
@@ -160,24 +181,27 @@
|
|
|
160
181
|
|
|
161
182
|
%x string
|
|
162
183
|
%x comment
|
|
184
|
+
%x spec_comment_name
|
|
163
185
|
%x spec_comment
|
|
186
|
+
%x spec_comment_skip
|
|
164
187
|
%x title_string
|
|
165
188
|
%x key_signature
|
|
189
|
+
%x voice_header
|
|
166
190
|
%x exclamation_exp
|
|
167
191
|
|
|
168
|
-
|
|
169
192
|
H \b[A-Z](?=\:[^|])
|
|
170
|
-
A \b[A-G](?=[\W\d\sA-Ga-
|
|
193
|
+
A \b[A-G](?=[\W\d\sA-Ga-g_yzHJLMOPRSTuv]*\b)
|
|
171
194
|
Am \b[A-G](?=[m][a][j]|[m][i][n]\b)
|
|
172
|
-
a \b[a-g](?=[\W\d\sA-Ga-
|
|
195
|
+
a \b[a-g](?=[\W\d\sA-Ga-g_yzHJLMOPRSTuv]*\b)
|
|
173
196
|
z \b[z]
|
|
174
197
|
Z \b[Z]
|
|
175
198
|
x \b[x](?=[\W\d\s])
|
|
199
|
+
y \b[y]
|
|
176
200
|
N [0-9]
|
|
177
201
|
P \b[HJLMOPRSTuv](?=[A-Ga-g][A-Ga-g0-9]*\b)
|
|
178
202
|
PP \b[HJLMOPRSTuv](?=[xz!\[^_=\s"])
|
|
179
203
|
|
|
180
|
-
SPECIAL [:!^_,'/<>={}()\[\]
|
|
204
|
+
SPECIAL [:!^_,'/<>={}()\[\]|.\-+~&]
|
|
181
205
|
|
|
182
206
|
|
|
183
207
|
%%
|
|
@@ -193,10 +217,30 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~]
|
|
|
193
217
|
<title_string>[^\n]+ return 'STR_CONTENT'
|
|
194
218
|
|
|
195
219
|
^[K][:][\s]* { this.pushState('key_signature'); return 'K:'; }
|
|
220
|
+
^[V][:][ \t]* { this.pushState('voice_header'); return 'V:'; }
|
|
221
|
+
<voice_header>\" { this.pushState('string'); return 'STR_START'; }
|
|
222
|
+
<voice_header>[a-zA-Z][a-zA-Z0-9,]* return 'NAME';
|
|
223
|
+
<voice_header>[0-9]+ return 'N';
|
|
224
|
+
<voice_header>[=] return '=';
|
|
225
|
+
<voice_header>[+\-] return yytext;
|
|
226
|
+
<voice_header>[ \t]+ {}
|
|
227
|
+
<voice_header>\n { this.popState(); }
|
|
228
|
+
<voice_header>\] { this.popState(); return ']'; }
|
|
196
229
|
<key_signature>"treble" return 'TREBLE';
|
|
197
230
|
<key_signature>"bass" return 'BASS';
|
|
198
231
|
<key_signature>"tenor" return 'TENOR';
|
|
232
|
+
<key_signature>"none" return 'NAME';
|
|
233
|
+
<key_signature>"Dor" return 'NAME';
|
|
234
|
+
<key_signature>"Phr" return 'NAME';
|
|
235
|
+
<key_signature>"Lyd" return 'NAME';
|
|
236
|
+
<key_signature>"Mix" return 'NAME';
|
|
237
|
+
<key_signature>"Aeo" return 'NAME';
|
|
238
|
+
<key_signature>"Loc" return 'NAME';
|
|
239
|
+
<key_signature>"HP" return 'NAME';
|
|
240
|
+
<key_signature>"Hp" return 'NAME';
|
|
241
|
+
<key_signature>[a-z]+[ \t]*=[^\n\]]* {}
|
|
199
242
|
<key_signature>[A-G] return 'A';
|
|
243
|
+
<key_signature>[A-Z][a-z]+ return 'NAME';
|
|
200
244
|
<key_signature>[b] return 'FLAT';
|
|
201
245
|
<key_signature>[#] return 'SHARP';
|
|
202
246
|
<key_signature>[m][a-z]* return 'NAME';
|
|
@@ -208,14 +252,20 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~]
|
|
|
208
252
|
<key_signature>\] { this.popState(); return ']'; }
|
|
209
253
|
|
|
210
254
|
^[%] { this.pushState('comment'); }
|
|
211
|
-
<comment>[%] { this.pushState('
|
|
255
|
+
<comment>[%] { this.pushState('spec_comment_name'); }
|
|
212
256
|
<comment>[^\n]+ { return 'COMMENT'; }
|
|
213
|
-
<spec_comment>\n { this.popState(); this.popState(); }
|
|
214
257
|
<comment>\n { this.popState(); }
|
|
215
|
-
<
|
|
216
|
-
<
|
|
217
|
-
<spec_comment
|
|
258
|
+
<spec_comment_name>[ \t]+ {}
|
|
259
|
+
<spec_comment_name>"score" { this.popState(); this.pushState('spec_comment'); return 'SCORE'; }
|
|
260
|
+
<spec_comment_name>"staves" { this.popState(); this.pushState('spec_comment'); return 'SCORE'; }
|
|
261
|
+
<spec_comment_name>[\w]+ { this.popState(); this.pushState('spec_comment_skip'); }
|
|
262
|
+
<spec_comment_name>\n { this.popState(); this.popState(); }
|
|
263
|
+
<spec_comment>[ \t]+ {}
|
|
218
264
|
<spec_comment>[(){}\[\]|] return yytext
|
|
265
|
+
<spec_comment>[\w]+ return 'NN'
|
|
266
|
+
<spec_comment>\n { this.popState(); this.popState(); return 'LAYOUT_END'; }
|
|
267
|
+
<spec_comment_skip>[^\n]+ {}
|
|
268
|
+
<spec_comment_skip>\n { this.popState(); this.popState(); }
|
|
219
269
|
|
|
220
270
|
[!] { this.pushState('exclamation_exp'); return '!'; }
|
|
221
271
|
<exclamation_exp>[!] { this.popState(); return '!'; }
|
|
@@ -231,6 +281,7 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~]
|
|
|
231
281
|
<exclamation_exp>{N} return 'N'
|
|
232
282
|
<exclamation_exp>[a-zA-Z][\w-]* return 'NAME'
|
|
233
283
|
|
|
284
|
+
\\\n {}
|
|
234
285
|
\s+ {}
|
|
235
286
|
|
|
236
287
|
{SPECIAL} return yytext
|
|
@@ -250,6 +301,7 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~]
|
|
|
250
301
|
"staff" return 'STAFF'
|
|
251
302
|
"maj" return 'MAJ'
|
|
252
303
|
"min" return 'MIN'
|
|
304
|
+
{y} return 'y'
|
|
253
305
|
[a-zA-Z][\w-]* return 'NAME'
|
|
254
306
|
|
|
255
307
|
<<EOF>> return 'EOF'
|
|
@@ -284,6 +336,7 @@ head_lines
|
|
|
284
336
|
| head_lines head_line -> [...$1, $2]
|
|
285
337
|
| head_lines comment -> [...$1, $2]
|
|
286
338
|
| head_lines staff_layout_statement -> [...$1, $2]
|
|
339
|
+
| head_lines 'LAYOUT_END' -> $1
|
|
287
340
|
| head_lines ']' -> $1
|
|
288
341
|
| head_lines '}' -> $1
|
|
289
342
|
| head_lines ')' -> $1
|
|
@@ -294,7 +347,7 @@ comment
|
|
|
294
347
|
;
|
|
295
348
|
|
|
296
349
|
staff_layout_statement
|
|
297
|
-
: 'SCORE' staff_layout
|
|
350
|
+
: 'SCORE' staff_layout 'LAYOUT_END' -> $2
|
|
298
351
|
;
|
|
299
352
|
|
|
300
353
|
staff_layout
|
|
@@ -320,6 +373,7 @@ head_line
|
|
|
320
373
|
: 'T:' string_content -> header('T', $2)
|
|
321
374
|
| 'C:' string_content -> header('C', $2)
|
|
322
375
|
| 'K:' key_signature -> header('K', $2)
|
|
376
|
+
| 'V:' header_value -> header('V', $2)
|
|
323
377
|
| H ':' header_value -> header($1, $3)
|
|
324
378
|
;
|
|
325
379
|
|
|
@@ -328,6 +382,7 @@ header_value
|
|
|
328
382
|
| number
|
|
329
383
|
| frac
|
|
330
384
|
| numeric_tempo
|
|
385
|
+
| string frac '=' number -> ({text: $1, note: $2, bpm: $4})
|
|
331
386
|
| upper_phonet
|
|
332
387
|
| voice_exp
|
|
333
388
|
| staff_shift
|
|
@@ -342,6 +397,11 @@ staff_shift
|
|
|
342
397
|
;
|
|
343
398
|
|
|
344
399
|
key_signature
|
|
400
|
+
: key_root -> $1
|
|
401
|
+
| NAME -> key(null, $1)
|
|
402
|
+
;
|
|
403
|
+
|
|
404
|
+
key_root
|
|
345
405
|
: A -> key($1, null)
|
|
346
406
|
| A sharp_or_flat -> key($1 + $2, null)
|
|
347
407
|
| A key_mode -> key($1, $2)
|
|
@@ -362,7 +422,7 @@ sharp_or_flat
|
|
|
362
422
|
key_mode
|
|
363
423
|
: MAJ -> 'major'
|
|
364
424
|
| MIN -> 'minor'
|
|
365
|
-
| NAME -> $1
|
|
425
|
+
| NAME -> parseMode($1)
|
|
366
426
|
;
|
|
367
427
|
|
|
368
428
|
plus_minus_number
|
|
@@ -402,6 +462,10 @@ voice_exp
|
|
|
402
462
|
| number NAME assigns -> voice($1, $2, $3)
|
|
403
463
|
| NAME -> voice(1, $1)
|
|
404
464
|
| NAME assigns -> voice(1, $1, $2)
|
|
465
|
+
| upper_phonet number -> voice(1, $1 + String($2))
|
|
466
|
+
| upper_phonet number assigns -> voice(1, $1 + String($2), $3)
|
|
467
|
+
| upper_phonet number NAME -> voice(1, $1 + String($2))
|
|
468
|
+
| upper_phonet number NAME assigns -> voice(1, $1 + String($2), $4)
|
|
405
469
|
;
|
|
406
470
|
|
|
407
471
|
assigns
|
|
@@ -418,6 +482,7 @@ assign_value
|
|
|
418
482
|
| number
|
|
419
483
|
| plus_minus_number
|
|
420
484
|
| NAME
|
|
485
|
+
| upper_phonet
|
|
421
486
|
;
|
|
422
487
|
|
|
423
488
|
upper_phonet
|
|
@@ -437,6 +502,9 @@ patches
|
|
|
437
502
|
| patches tailless_patch -> [...$1, $2]
|
|
438
503
|
| patches ']' -> $1
|
|
439
504
|
| patches '}' -> $1
|
|
505
|
+
| patches head_line -> $1
|
|
506
|
+
| patches 'LAYOUT_END' -> $1
|
|
507
|
+
| patches '&' patch -> $1
|
|
440
508
|
;
|
|
441
509
|
|
|
442
510
|
patch
|
|
@@ -459,10 +527,11 @@ bar
|
|
|
459
527
|
| ':' '|' ']' -> ':|]'
|
|
460
528
|
| '|' N -> '|' + $2
|
|
461
529
|
| ':' '|' N -> ':|' + $2
|
|
530
|
+
| '&' -> '&'
|
|
462
531
|
;
|
|
463
532
|
|
|
464
533
|
music
|
|
465
|
-
: %empty
|
|
534
|
+
: %empty -> []
|
|
466
535
|
| music expressive_mark -> $1 ? [...$1, $2] : [$2]
|
|
467
536
|
| music text -> $1 ? [...$1, $2] : [$2]
|
|
468
537
|
| music event -> $1 ? [...$1, $2] : [$2]
|
|
@@ -474,11 +543,13 @@ music
|
|
|
474
543
|
| music NAME -> $1
|
|
475
544
|
| music '^' NAME -> $1
|
|
476
545
|
| music '^' -> $1
|
|
546
|
+
| music '[' N -> $1
|
|
477
547
|
;
|
|
478
548
|
|
|
479
549
|
control
|
|
480
550
|
: '[' H ':' header_value ']' -> ({control: header($2, $4)})
|
|
481
551
|
| '[' 'K:' header_value ']' -> ({control: header("K", $3)})
|
|
552
|
+
| '[' 'V:' header_value ']' -> ({control: header("V", $3)})
|
|
482
553
|
| '[' NAME ':' header_value ']' -> ({control: header($2, $4)})
|
|
483
554
|
;
|
|
484
555
|
|
|
@@ -510,6 +581,7 @@ articulation
|
|
|
510
581
|
articulation_content
|
|
511
582
|
: scope_articulation -> articulation($1)
|
|
512
583
|
| scope_articulation parenthese -> articulation($1, $2)
|
|
584
|
+
| scope_articulation '=' assign_value -> articulation($1 + '=' + String($3))
|
|
513
585
|
| DYNAMIC -> articulation($1)
|
|
514
586
|
| a -> articulation($1)
|
|
515
587
|
| "^" -> articulation($1)
|
|
@@ -613,6 +685,8 @@ accidentals
|
|
|
613
685
|
| '^' '^' -> 2
|
|
614
686
|
| '_' '_' -> -2
|
|
615
687
|
| '=' '=' -> 0
|
|
688
|
+
| '^' '/' -> 0.5
|
|
689
|
+
| '_' '/' -> -0.5
|
|
616
690
|
;
|
|
617
691
|
|
|
618
692
|
pitch
|
|
@@ -637,6 +711,7 @@ rest_phonet
|
|
|
637
711
|
: z
|
|
638
712
|
| Z
|
|
639
713
|
| x
|
|
714
|
+
| y
|
|
640
715
|
;
|
|
641
716
|
|
|
642
717
|
event
|