@k-l-lambda/lilylet 0.1.58 → 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
  }
@@ -626,10 +627,21 @@ const parseLilyDocument = (lilyDocument) => {
626
627
  lastOttava = term.value;
627
628
  }
628
629
  }
629
- // Handle staff change
630
+ // Handle staff change (\change Staff = "N")
631
+ // args[0] is Assignment { key: "Staff", value: { exp: '"N"' } }
630
632
  else if (term instanceof lilyParser.LilyTerms.Change) {
631
- // Ignore \change Staff commands - staff is fixed per track
632
- // (Cross-staff notation is not supported in this decoder)
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
+ }
633
645
  }
634
646
  // Handle tempo
635
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.clef) {
446
+ if (ctx.staff)
447
+ activeStaff = ctx.staff;
448
+ if (ctx.clef && activeStaff === voice.staff) {
446
449
  return ctx.clef;
447
450
  }
448
451
  }
@@ -504,10 +507,17 @@ const serializeVoice = (voice, currentStaff, isGrandStaff = false, measureContex
504
507
  if (ctx.staff && ctx.staff !== activeStaff) {
505
508
  activeStaff = ctx.staff;
506
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
+ }
507
517
  continue;
508
518
  }
509
519
  if (ctx.staff)
510
- continue; // same staff, no-op
520
+ continue; // same staff, no-op for staff field; clef handled below
511
521
  // Skip clef-only context events if clef already established for this staff
512
522
  if (clefOutputted && ctx.clef && !ctx.key && !ctx.time && !ctx.ottava && !ctx.stemDirection && !ctx.tempo) {
513
523
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k-l-lambda/lilylet",
3
- "version": "0.1.58",
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;
@@ -750,10 +751,21 @@ const parseLilyDocument = (lilyDocument: lilyParser.LilyDocument): ParsedMeasure
750
751
  lastOttava = term.value;
751
752
  }
752
753
  }
753
- // Handle staff change
754
+ // Handle staff change (\change Staff = "N")
755
+ // args[0] is Assignment { key: "Staff", value: { exp: '"N"' } }
754
756
  else if (term instanceof lilyParser.LilyTerms.Change) {
755
- // Ignore \change Staff commands - staff is fixed per track
756
- // (Cross-staff notation is not supported in this decoder)
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
+ }
757
769
  }
758
770
  // Handle tempo
759
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.clef) {
581
+ if (ctx.staff) activeStaff = ctx.staff;
582
+ if (ctx.clef && activeStaff === voice.staff) {
581
583
  return ctx.clef;
582
584
  }
583
585
  }
@@ -652,9 +654,15 @@ const serializeVoice = (
652
654
  if (ctx.staff && ctx.staff !== activeStaff) {
653
655
  activeStaff = ctx.staff;
654
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
+ }
655
663
  continue;
656
664
  }
657
- if (ctx.staff) continue; // same staff, no-op
665
+ if (ctx.staff) continue; // same staff, no-op for staff field; clef handled below
658
666
  // Skip clef-only context events if clef already established for this staff
659
667
  if (clefOutputted && ctx.clef && !ctx.key && !ctx.time && !ctx.ottava && !ctx.stemDirection && !ctx.tempo) {
660
668
  continue;