@k-l-lambda/lilylet 0.1.68 → 0.1.69
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 +155 -135
- package/lib/lilylet/abcDecoder.js +3 -3
- package/lib/lilylet/serializer.js +23 -6
- package/package.json +1 -1
- package/source/abc/abc.jison +37 -12
- package/source/abc/grammar.jison.js +155 -135
- package/source/lilylet/abcDecoder.ts +4 -3
- package/source/lilylet/serializer.ts +27 -6
|
@@ -681,7 +681,7 @@ const processBarPatch = (patch, unitLength, slurDepth, pitchCtx) => {
|
|
|
681
681
|
}
|
|
682
682
|
// Grace notes
|
|
683
683
|
if (term.grace) {
|
|
684
|
-
const graceEvents = convertGraceEvents(term, unitLength);
|
|
684
|
+
const graceEvents = convertGraceEvents(term, unitLength, pitchCtx);
|
|
685
685
|
events.push(...graceEvents);
|
|
686
686
|
i++;
|
|
687
687
|
continue;
|
|
@@ -852,7 +852,7 @@ const convertEventTerm = (eventTerm, unitLength, pendingMarks, pendingContextCha
|
|
|
852
852
|
/**
|
|
853
853
|
* Convert grace notes to NoteEvents with grace flag
|
|
854
854
|
*/
|
|
855
|
-
const convertGraceEvents = (graceTerm, unitLength) => {
|
|
855
|
+
const convertGraceEvents = (graceTerm, unitLength, pitchCtx) => {
|
|
856
856
|
const events = [];
|
|
857
857
|
if (!graceTerm.events)
|
|
858
858
|
return events;
|
|
@@ -862,7 +862,7 @@ const convertGraceEvents = (graceTerm, unitLength) => {
|
|
|
862
862
|
const chord = eventData.chord;
|
|
863
863
|
if (!chord || !chord.pitches)
|
|
864
864
|
continue;
|
|
865
|
-
const pitches = chord.pitches.filter((p) => p.phonet !== "z" && p.phonet !== "Z" && p.phonet !== "x").map(convertPitch);
|
|
865
|
+
const pitches = chord.pitches.filter((p) => p.phonet !== "z" && p.phonet !== "Z" && p.phonet !== "x").map((p) => convertPitch(p, pitchCtx));
|
|
866
866
|
if (pitches.length === 0)
|
|
867
867
|
continue;
|
|
868
868
|
const duration = convertDuration(eventData.duration, unitLength);
|
|
@@ -199,11 +199,12 @@ const serializeMarks = (marks) => {
|
|
|
199
199
|
return parts.join('');
|
|
200
200
|
};
|
|
201
201
|
// Serialize a note event with pitch environment tracking
|
|
202
|
-
const serializeNoteEvent = (event, env, prevDuration) => {
|
|
202
|
+
const serializeNoteEvent = (event, env, prevDuration, suppressGracePrefix = false) => {
|
|
203
203
|
const parts = [];
|
|
204
204
|
let currentEnv = env;
|
|
205
|
-
// Grace note prefix
|
|
206
|
-
|
|
205
|
+
// Grace note prefix. When the caller groups consecutive grace notes into a single
|
|
206
|
+
// scoped \grace { ... }, it suppresses the per-note prefix and emits the wrapper.
|
|
207
|
+
if (event.grace && !suppressGracePrefix) {
|
|
207
208
|
parts.push('\\grace ');
|
|
208
209
|
}
|
|
209
210
|
// Single note or chord
|
|
@@ -440,10 +441,10 @@ const serializeBarlineEvent = (event) => {
|
|
|
440
441
|
return '';
|
|
441
442
|
};
|
|
442
443
|
// Serialize a single event with pitch environment tracking
|
|
443
|
-
const serializeEvent = (event, env, prevDuration) => {
|
|
444
|
+
const serializeEvent = (event, env, prevDuration, suppressGracePrefix = false) => {
|
|
444
445
|
switch (event.type) {
|
|
445
446
|
case 'note':
|
|
446
|
-
return serializeNoteEvent(event, env, prevDuration);
|
|
447
|
+
return serializeNoteEvent(event, env, prevDuration, suppressGracePrefix);
|
|
447
448
|
case 'rest':
|
|
448
449
|
return serializeRestEvent(event, env, prevDuration);
|
|
449
450
|
case 'context':
|
|
@@ -576,6 +577,7 @@ const serializeVoice = (voice, currentStaff, isGrandStaff = false, measureContex
|
|
|
576
577
|
}
|
|
577
578
|
let activeStaff = effectiveInitialStaff;
|
|
578
579
|
let activeStemDir;
|
|
580
|
+
let graceGroupOpen = false; // whether a scoped \grace { ... } is currently open
|
|
579
581
|
for (let eventIdx = 0; eventIdx < voice.events.length; eventIdx++) {
|
|
580
582
|
const event = voice.events[eventIdx];
|
|
581
583
|
// Skip leading context-staff events already absorbed into effectiveInitialStaff
|
|
@@ -639,7 +641,18 @@ const serializeVoice = (voice, currentStaff, isGrandStaff = false, measureContex
|
|
|
639
641
|
activeStemDir = stemDir;
|
|
640
642
|
}
|
|
641
643
|
}
|
|
642
|
-
const
|
|
644
|
+
const isGraceNote = event.type === 'note' && !!event.grace;
|
|
645
|
+
// Group consecutive grace notes into one scoped \grace { ... } instead of
|
|
646
|
+
// emitting a separate \grace prefix per note.
|
|
647
|
+
if (isGraceNote && !graceGroupOpen) {
|
|
648
|
+
parts.push('\\grace {');
|
|
649
|
+
graceGroupOpen = true;
|
|
650
|
+
}
|
|
651
|
+
else if (!isGraceNote && graceGroupOpen) {
|
|
652
|
+
parts.push('}');
|
|
653
|
+
graceGroupOpen = false;
|
|
654
|
+
}
|
|
655
|
+
const { str: eventStr, newEnv } = serializeEvent(event, pitchEnv, prevDuration, graceGroupOpen);
|
|
643
656
|
pitchEnv = newEnv;
|
|
644
657
|
if (eventStr) {
|
|
645
658
|
parts.push(eventStr);
|
|
@@ -663,6 +676,10 @@ const serializeVoice = (voice, currentStaff, isGrandStaff = false, measureContex
|
|
|
663
676
|
emittedClefs[ctx.staff || activeStaff] = ctx.clef;
|
|
664
677
|
}
|
|
665
678
|
}
|
|
679
|
+
// Close a grace group left open at the end of the voice (unusual but possible).
|
|
680
|
+
if (graceGroupOpen) {
|
|
681
|
+
parts.push('}');
|
|
682
|
+
}
|
|
666
683
|
return { str: parts.join(' '), newStaff: voice.staff };
|
|
667
684
|
};
|
|
668
685
|
// Serialize a part, tracking staff state across voices
|
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.69",
|
|
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",
|
package/source/abc/abc.jison
CHANGED
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
|
|
38
38
|
const patch = (terms, bar) => {
|
|
39
39
|
const control = {};
|
|
40
|
-
terms.forEach(term => {
|
|
40
|
+
(terms || []).forEach(term => {
|
|
41
41
|
if (term.control)
|
|
42
42
|
control[term.control.name] = term.control.value;
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
return {
|
|
46
46
|
control,
|
|
47
|
-
terms,
|
|
47
|
+
terms: terms || [],
|
|
48
48
|
bar,
|
|
49
49
|
};
|
|
50
50
|
};
|
|
@@ -195,10 +195,10 @@ Am \b[A-G](?=[m][a][j]|[m][i][n]\b)
|
|
|
195
195
|
a \b[a-g](?=[\W\d\sA-Ga-g_yzHJLMOPRSTuv]*\b)
|
|
196
196
|
z \b[z]
|
|
197
197
|
Z \b[Z]
|
|
198
|
-
x \b[x](?=[\W\d\s])
|
|
198
|
+
x \b[x](?=[\W\d\s]|[_^=]*[A-Ga-g])
|
|
199
199
|
y \b[y]
|
|
200
200
|
N [0-9]
|
|
201
|
-
P \b[HJLMOPRSTuv](?=[A-Ga-g][A-Ga-g0-
|
|
201
|
+
P \b[HJLMOPRSTuv](?=[_^=]*[A-Ga-g][A-Ga-g0-9_^=,']*)
|
|
202
202
|
PP \b[HJLMOPRSTuv](?=[xz!\[^_=\s"])
|
|
203
203
|
|
|
204
204
|
SPECIAL [:!^_,'/<>={}()\[\]|.\-+~&]
|
|
@@ -211,8 +211,8 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~&]
|
|
|
211
211
|
<string>\\\" return 'STR_CONTENT'
|
|
212
212
|
<string>[^"]+ return 'STR_CONTENT'
|
|
213
213
|
|
|
214
|
-
^[T][:][\s]* { this.pushState('title_string'); return 'T:'; }
|
|
215
|
-
^[C][:][\s]* { this.pushState('title_string'); return 'C:'; }
|
|
214
|
+
^[T][:][\s]* { var pre = this.matched.slice(0, this.matched.length - yytext.length); if (pre === '' || pre.slice(-1) === '\n') { this.pushState('title_string'); return 'T:'; } this.unput(yytext.slice(1)); yytext = 'T'; return 'T'; }
|
|
215
|
+
^[C][:][\s]* { var pre = this.matched.slice(0, this.matched.length - yytext.length); if (pre === '' || pre.slice(-1) === '\n') { this.pushState('title_string'); return 'C:'; } this.unput(yytext.slice(1)); yytext = 'C'; return 'A'; }
|
|
216
216
|
<title_string>\n { this.popState(); }
|
|
217
217
|
<title_string>[^\n]+ return 'STR_CONTENT'
|
|
218
218
|
|
|
@@ -226,10 +226,10 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~&]
|
|
|
226
226
|
<voice_header>[ \t]+ {}
|
|
227
227
|
<voice_header>\n { this.popState(); }
|
|
228
228
|
<voice_header>\] { this.popState(); return ']'; }
|
|
229
|
-
<key_signature>"treble"
|
|
230
|
-
<key_signature>"bass"
|
|
231
|
-
<key_signature>"tenor"
|
|
232
|
-
<key_signature>"alto"
|
|
229
|
+
<key_signature>"treble"[0-9]* return 'TREBLE';
|
|
230
|
+
<key_signature>"bass"[0-9]* return 'BASS';
|
|
231
|
+
<key_signature>"tenor"[0-9]* return 'TENOR';
|
|
232
|
+
<key_signature>"alto"[0-9]* return 'ALTO';
|
|
233
233
|
<key_signature>"none" return 'NAME';
|
|
234
234
|
<key_signature>"Dor" return 'NAME';
|
|
235
235
|
<key_signature>"Phr" return 'NAME';
|
|
@@ -262,9 +262,10 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~&]
|
|
|
262
262
|
<spec_comment_name>[\w]+ { this.popState(); this.pushState('spec_comment_skip'); }
|
|
263
263
|
<spec_comment_name>\n { this.popState(); this.popState(); }
|
|
264
264
|
<spec_comment>[ \t]+ {}
|
|
265
|
-
<spec_comment>[([{]
|
|
265
|
+
<spec_comment>[([{] { this._scoreDepth = (this._scoreDepth || 0) + 1; return yytext; }
|
|
266
266
|
<spec_comment>[)\]}] { this._scoreDepth = (this._scoreDepth || 0) - 1; return yytext; }
|
|
267
267
|
<spec_comment>[|] return yytext
|
|
268
|
+
<spec_comment>[A-Z][:] { this.popState(); this.popState(); this.unput(yytext); return 'LAYOUT_END'; }
|
|
268
269
|
<spec_comment>[\w]+ return 'NN'
|
|
269
270
|
<spec_comment>\n { if (this._scoreDepth > 0) { /* layout continues on next line */ } else { this.popState(); this.popState(); return 'LAYOUT_END'; } }
|
|
270
271
|
<spec_comment_skip>[^\n]+ {}
|
|
@@ -467,6 +468,7 @@ numeric_tempo
|
|
|
467
468
|
|
|
468
469
|
voice_exp
|
|
469
470
|
: number -> voice($1)
|
|
471
|
+
| number assigns -> voice($1, null, $2)
|
|
470
472
|
| number NAME -> voice($1, $2)
|
|
471
473
|
| number NAME assigns -> voice($1, $2, $3)
|
|
472
474
|
| number NAME plus_minus_number -> voice($1, $2 + ($3 < 0 ? '-' : '+') + Math.abs($3))
|
|
@@ -539,10 +541,18 @@ bar
|
|
|
539
541
|
| '|' ']' ':' -> '|]'
|
|
540
542
|
| ':' '|' ']' -> ':|]'
|
|
541
543
|
| '|' N -> '|' + $2
|
|
544
|
+
| '|' N volta_rest -> '|' + $2 + $3
|
|
542
545
|
| ':' '|' N -> ':|' + $2
|
|
546
|
+
| ':' '|' N volta_rest -> ':|' + $2 + $3
|
|
543
547
|
| '&' -> '&'
|
|
544
548
|
;
|
|
545
549
|
|
|
550
|
+
// Additional volta endings after the first number, comma-separated: "1,2", "1,2,3".
|
|
551
|
+
volta_rest
|
|
552
|
+
: ',' N -> ',' + $2
|
|
553
|
+
| volta_rest ',' N -> $1 + ',' + $3
|
|
554
|
+
;
|
|
555
|
+
|
|
546
556
|
music
|
|
547
557
|
: %empty -> []
|
|
548
558
|
| music expressive_mark -> $1 ? [...$1, $2] : [$2]
|
|
@@ -555,9 +565,17 @@ music
|
|
|
555
565
|
| music N -> $1
|
|
556
566
|
| music NAME -> $1
|
|
557
567
|
| music '^' NAME -> $1
|
|
568
|
+
| music '^' articulation_letter -> $1
|
|
558
569
|
| music '[' N -> $1
|
|
559
570
|
;
|
|
560
571
|
|
|
572
|
+
// Articulation-class letters (P macro: HJLMOPRSTuv). After a stray '^' inside a
|
|
573
|
+
// text annotation, a word like "Menuetto"/"Largo" starts with one of these and the
|
|
574
|
+
// lexer emits it as the articulation token, not NAME — swallow it like '^' NAME.
|
|
575
|
+
articulation_letter
|
|
576
|
+
: 'P' | 'T' | 'H' | 'J' | 'L' | 'M' | 'R' | 'O' | 'S' | 'u' | 'v'
|
|
577
|
+
;
|
|
578
|
+
|
|
561
579
|
control
|
|
562
580
|
: '[' H ':' header_value ']' -> ({control: header($2, $4)})
|
|
563
581
|
| '[' 'K:' header_value ']' -> ({control: header("K", $3)})
|
|
@@ -756,7 +774,14 @@ duration
|
|
|
756
774
|
: number '/' number -> frac(Number($1), Number($3))
|
|
757
775
|
| '/' number -> frac(1, Number($2))
|
|
758
776
|
| number -> frac(Number($1))
|
|
759
|
-
|
|
|
777
|
+
| slashes -> frac(1, Math.pow(2, $1))
|
|
778
|
+
| number slashes -> frac(Number($1), Math.pow(2, $2))
|
|
779
|
+
;
|
|
780
|
+
|
|
781
|
+
// One or more '/' halve the unit length each: '/'=1/2, '//'=1/4, '///'=1/8.
|
|
782
|
+
slashes
|
|
783
|
+
: '/' -> 1
|
|
784
|
+
| slashes '/' -> $1 + 1
|
|
760
785
|
;
|
|
761
786
|
|
|
762
787
|
broken_rhythm
|