@k-l-lambda/lilylet 0.1.57 → 0.1.59
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.
|
@@ -451,7 +451,8 @@ const parseLilyDocument = (lilyDocument) => {
|
|
|
451
451
|
// Update key/time from context on music events
|
|
452
452
|
if (term instanceof lilyParser.MusicEvent ||
|
|
453
453
|
term instanceof lilyParser.LilyTerms.StemDirection ||
|
|
454
|
-
term instanceof lilyParser.LilyTerms.OctaveShift
|
|
454
|
+
term instanceof lilyParser.LilyTerms.OctaveShift ||
|
|
455
|
+
term instanceof lilyParser.LilyTerms.Change) {
|
|
455
456
|
if (context.key && measure.key === null) {
|
|
456
457
|
measure.key = context.key.key;
|
|
457
458
|
}
|
|
@@ -585,6 +586,7 @@ const parseLilyDocument = (lilyDocument) => {
|
|
|
585
586
|
const restEvent = {
|
|
586
587
|
type: 'rest',
|
|
587
588
|
duration: convertDuration(term.durationValue),
|
|
589
|
+
fullMeasure: (term.name === 'R') || undefined,
|
|
588
590
|
invisible: term.isSpacer || undefined,
|
|
589
591
|
};
|
|
590
592
|
voice.events.push(restEvent);
|
|
@@ -625,10 +627,21 @@ const parseLilyDocument = (lilyDocument) => {
|
|
|
625
627
|
lastOttava = term.value;
|
|
626
628
|
}
|
|
627
629
|
}
|
|
628
|
-
// Handle staff change
|
|
630
|
+
// Handle staff change (\change Staff = "N")
|
|
631
|
+
// args[0] is Assignment { key: "Staff", value: { exp: '"N"' } }
|
|
629
632
|
else if (term instanceof lilyParser.LilyTerms.Change) {
|
|
630
|
-
|
|
631
|
-
|
|
633
|
+
const assignment = term.args?.[0];
|
|
634
|
+
if (assignment?.key === 'Staff') {
|
|
635
|
+
// exp is like '"2"' — strip surrounding quotes then parse strictly
|
|
636
|
+
const exp = assignment?.value?.exp ?? '';
|
|
637
|
+
const raw = exp.replace(/^"|"$/g, '');
|
|
638
|
+
// Only accept pure integer staff names (e.g. "1", "2")
|
|
639
|
+
// Reject compound names like "1_2", "RH", "LH"
|
|
640
|
+
const staffNum = /^\d+$/.test(raw) ? parseInt(raw, 10) : NaN;
|
|
641
|
+
if (!isNaN(staffNum)) {
|
|
642
|
+
voice.events.push({ type: 'context', staff: staffNum });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
632
645
|
}
|
|
633
646
|
// Handle tempo
|
|
634
647
|
else if (term instanceof lilyParser.LilyTerms.Tempo) {
|
|
@@ -439,10 +439,13 @@ const serializeEvent = (event, env, prevDuration) => {
|
|
|
439
439
|
};
|
|
440
440
|
// Find first clef in voice events
|
|
441
441
|
const findVoiceClef = (voice) => {
|
|
442
|
+
let activeStaff = voice.staff;
|
|
442
443
|
for (const event of voice.events) {
|
|
443
444
|
if (event.type === 'context') {
|
|
444
445
|
const ctx = event;
|
|
445
|
-
if (ctx.
|
|
446
|
+
if (ctx.staff)
|
|
447
|
+
activeStaff = ctx.staff;
|
|
448
|
+
if (ctx.clef && activeStaff === voice.staff) {
|
|
446
449
|
return ctx.clef;
|
|
447
450
|
}
|
|
448
451
|
}
|
|
@@ -500,10 +503,21 @@ const serializeVoice = (voice, currentStaff, isGrandStaff = false, measureContex
|
|
|
500
503
|
for (const event of voice.events) {
|
|
501
504
|
if (event.type === 'context') {
|
|
502
505
|
const ctx = event;
|
|
503
|
-
//
|
|
504
|
-
if (ctx.staff && ctx.staff !==
|
|
506
|
+
// Cross-staff context: update activeStaff and emit \staff directive
|
|
507
|
+
if (ctx.staff && ctx.staff !== activeStaff) {
|
|
508
|
+
activeStaff = ctx.staff;
|
|
509
|
+
parts.push('\\staff "' + activeStaff + '"');
|
|
510
|
+
// Emit target staff clef if the event carries one or allStaffClefs knows it
|
|
511
|
+
const ctxClef = ctx.clef || allStaffClefs?.[activeStaff];
|
|
512
|
+
if (ctxClef && emittedClefs?.[activeStaff] !== ctxClef) {
|
|
513
|
+
parts.push('\\clef "' + CLEF_MAP[ctxClef] + '"');
|
|
514
|
+
if (emittedClefs)
|
|
515
|
+
emittedClefs[activeStaff] = ctxClef;
|
|
516
|
+
}
|
|
505
517
|
continue;
|
|
506
518
|
}
|
|
519
|
+
if (ctx.staff)
|
|
520
|
+
continue; // same staff, no-op for staff field; clef handled below
|
|
507
521
|
// Skip clef-only context events if clef already established for this staff
|
|
508
522
|
if (clefOutputted && ctx.clef && !ctx.key && !ctx.time && !ctx.ottava && !ctx.stemDirection && !ctx.tempo) {
|
|
509
523
|
continue;
|
|
@@ -511,8 +525,8 @@ const serializeVoice = (voice, currentStaff, isGrandStaff = false, measureContex
|
|
|
511
525
|
}
|
|
512
526
|
if (event.type === 'note') {
|
|
513
527
|
const noteEvt = event;
|
|
514
|
-
// Cross-staff
|
|
515
|
-
const effectiveStaff = noteEvt.staff ||
|
|
528
|
+
// Cross-staff via explicit note.staff (lilylet native cross-staff)
|
|
529
|
+
const effectiveStaff = noteEvt.staff || activeStaff;
|
|
516
530
|
if (effectiveStaff !== activeStaff) {
|
|
517
531
|
activeStaff = effectiveStaff;
|
|
518
532
|
parts.push('\\staff "' + activeStaff + '"');
|
|
@@ -679,12 +693,16 @@ export const serializeLilyletDoc = (doc) => {
|
|
|
679
693
|
// Collect clefs from this measure's voices
|
|
680
694
|
for (const part of measure.parts) {
|
|
681
695
|
for (const voice of part.voices) {
|
|
696
|
+
let clefActiveStaff = voice.staff;
|
|
682
697
|
for (const event of voice.events) {
|
|
683
|
-
if (event.type === 'context'
|
|
698
|
+
if (event.type === 'context') {
|
|
684
699
|
const ctx = event;
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
700
|
+
if (ctx.staff) {
|
|
701
|
+
clefActiveStaff = ctx.staff;
|
|
702
|
+
}
|
|
703
|
+
if (ctx.clef) {
|
|
704
|
+
staffClefs[clefActiveStaff] = ctx.clef;
|
|
705
|
+
}
|
|
688
706
|
}
|
|
689
707
|
}
|
|
690
708
|
}
|
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.59",
|
|
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",
|
|
@@ -556,7 +556,8 @@ const parseLilyDocument = (lilyDocument: lilyParser.LilyDocument): ParsedMeasure
|
|
|
556
556
|
// Update key/time from context on music events
|
|
557
557
|
if (term instanceof lilyParser.MusicEvent ||
|
|
558
558
|
term instanceof lilyParser.LilyTerms.StemDirection ||
|
|
559
|
-
term instanceof lilyParser.LilyTerms.OctaveShift
|
|
559
|
+
term instanceof lilyParser.LilyTerms.OctaveShift ||
|
|
560
|
+
term instanceof lilyParser.LilyTerms.Change) {
|
|
560
561
|
|
|
561
562
|
if (context.key && measure.key === null) {
|
|
562
563
|
measure.key = context.key.key;
|
|
@@ -708,6 +709,7 @@ const parseLilyDocument = (lilyDocument: lilyParser.LilyDocument): ParsedMeasure
|
|
|
708
709
|
const restEvent: RestEvent = {
|
|
709
710
|
type: 'rest',
|
|
710
711
|
duration: convertDuration(term.durationValue),
|
|
712
|
+
fullMeasure: (term.name === 'R') || undefined,
|
|
711
713
|
invisible: term.isSpacer || undefined,
|
|
712
714
|
};
|
|
713
715
|
|
|
@@ -749,10 +751,21 @@ const parseLilyDocument = (lilyDocument: lilyParser.LilyDocument): ParsedMeasure
|
|
|
749
751
|
lastOttava = term.value;
|
|
750
752
|
}
|
|
751
753
|
}
|
|
752
|
-
// Handle staff change
|
|
754
|
+
// Handle staff change (\change Staff = "N")
|
|
755
|
+
// args[0] is Assignment { key: "Staff", value: { exp: '"N"' } }
|
|
753
756
|
else if (term instanceof lilyParser.LilyTerms.Change) {
|
|
754
|
-
|
|
755
|
-
|
|
757
|
+
const assignment = (term as any).args?.[0];
|
|
758
|
+
if (assignment?.key === 'Staff') {
|
|
759
|
+
// exp is like '"2"' — strip surrounding quotes then parse strictly
|
|
760
|
+
const exp: string = assignment?.value?.exp ?? '';
|
|
761
|
+
const raw = exp.replace(/^"|"$/g, '');
|
|
762
|
+
// Only accept pure integer staff names (e.g. "1", "2")
|
|
763
|
+
// Reject compound names like "1_2", "RH", "LH"
|
|
764
|
+
const staffNum = /^\d+$/.test(raw) ? parseInt(raw, 10) : NaN;
|
|
765
|
+
if (!isNaN(staffNum)) {
|
|
766
|
+
voice.events.push({ type: 'context', staff: staffNum } as any);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
756
769
|
}
|
|
757
770
|
// Handle tempo
|
|
758
771
|
else if (term instanceof lilyParser.LilyTerms.Tempo) {
|
|
@@ -574,10 +574,12 @@ interface MeasureContext {
|
|
|
574
574
|
|
|
575
575
|
// Find first clef in voice events
|
|
576
576
|
const findVoiceClef = (voice: Voice): Clef | undefined => {
|
|
577
|
+
let activeStaff = voice.staff;
|
|
577
578
|
for (const event of voice.events) {
|
|
578
579
|
if (event.type === 'context') {
|
|
579
580
|
const ctx = event as ContextChange;
|
|
580
|
-
if (ctx.
|
|
581
|
+
if (ctx.staff) activeStaff = ctx.staff;
|
|
582
|
+
if (ctx.clef && activeStaff === voice.staff) {
|
|
581
583
|
return ctx.clef;
|
|
582
584
|
}
|
|
583
585
|
}
|
|
@@ -648,10 +650,19 @@ const serializeVoice = (
|
|
|
648
650
|
for (const event of voice.events) {
|
|
649
651
|
if (event.type === 'context') {
|
|
650
652
|
const ctx = event as ContextChange;
|
|
651
|
-
//
|
|
652
|
-
if (ctx.staff && ctx.staff !==
|
|
653
|
+
// Cross-staff context: update activeStaff and emit \staff directive
|
|
654
|
+
if (ctx.staff && ctx.staff !== activeStaff) {
|
|
655
|
+
activeStaff = ctx.staff;
|
|
656
|
+
parts.push('\\staff "' + activeStaff + '"');
|
|
657
|
+
// Emit target staff clef if the event carries one or allStaffClefs knows it
|
|
658
|
+
const ctxClef = ctx.clef || allStaffClefs?.[activeStaff];
|
|
659
|
+
if (ctxClef && emittedClefs?.[activeStaff] !== ctxClef) {
|
|
660
|
+
parts.push('\\clef "' + CLEF_MAP[ctxClef] + '"');
|
|
661
|
+
if (emittedClefs) emittedClefs[activeStaff] = ctxClef;
|
|
662
|
+
}
|
|
653
663
|
continue;
|
|
654
664
|
}
|
|
665
|
+
if (ctx.staff) continue; // same staff, no-op for staff field; clef handled below
|
|
655
666
|
// Skip clef-only context events if clef already established for this staff
|
|
656
667
|
if (clefOutputted && ctx.clef && !ctx.key && !ctx.time && !ctx.ottava && !ctx.stemDirection && !ctx.tempo) {
|
|
657
668
|
continue;
|
|
@@ -661,8 +672,8 @@ const serializeVoice = (
|
|
|
661
672
|
if (event.type === 'note') {
|
|
662
673
|
const noteEvt = event as NoteEvent;
|
|
663
674
|
|
|
664
|
-
// Cross-staff
|
|
665
|
-
const effectiveStaff = noteEvt.staff ||
|
|
675
|
+
// Cross-staff via explicit note.staff (lilylet native cross-staff)
|
|
676
|
+
const effectiveStaff = noteEvt.staff || activeStaff;
|
|
666
677
|
if (effectiveStaff !== activeStaff) {
|
|
667
678
|
activeStaff = effectiveStaff;
|
|
668
679
|
parts.push('\\staff "' + activeStaff + '"');
|
|
@@ -872,12 +883,16 @@ export const serializeLilyletDoc = (doc: LilyletDoc): string => {
|
|
|
872
883
|
// Collect clefs from this measure's voices
|
|
873
884
|
for (const part of measure.parts) {
|
|
874
885
|
for (const voice of part.voices) {
|
|
886
|
+
let clefActiveStaff = voice.staff;
|
|
875
887
|
for (const event of voice.events) {
|
|
876
|
-
if (event.type === 'context'
|
|
888
|
+
if (event.type === 'context') {
|
|
877
889
|
const ctx = event as ContextChange;
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
890
|
+
if (ctx.staff) {
|
|
891
|
+
clefActiveStaff = ctx.staff;
|
|
892
|
+
}
|
|
893
|
+
if (ctx.clef) {
|
|
894
|
+
staffClefs[clefActiveStaff] = ctx.clef;
|
|
895
|
+
}
|
|
881
896
|
}
|
|
882
897
|
}
|
|
883
898
|
}
|