@k-l-lambda/lilylet 0.1.53 → 0.1.54

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.
@@ -329,7 +329,7 @@ case 92:
329
329
  this.$ = 'auto';
330
330
  break;
331
331
  case 93:
332
- this.$ = ($$[$0-1].filter(e => e.type === 'note' || e.type === 'rest').map(e => ({ ...e, grace: true })));
332
+ this.$ = ($$[$0-1].map(e => (e.type === 'note' || e.type === 'rest') ? { ...e, grace: true } : e));
333
333
  break;
334
334
  case 94: case 95:
335
335
  this.$ = ({ ...$$[$0], grace: true });
@@ -754,15 +754,35 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
754
754
  }
755
755
  return true;
756
756
  };
757
+ let graceBeamOpen = false; // Whether a grace note <beam> is open (independent of main beam)
757
758
  for (const event of voice.events) {
758
759
  // Check for beam start/end in this event (including inside tuplets)
759
760
  const { beamStart, beamEnd } = getEventBeamMarks(event);
760
- // Open beam element if beam starts
761
- if (beamStart && !beamElementOpen) {
762
- xml += `${baseIndent}<beam xml:id="${generateId('beam')}">\n`;
763
- beamElementOpen = true;
761
+ // Grace notes have independent beam groups - don't interfere with main beam
762
+ const isGraceNote = event.type === 'note' && event.grace;
763
+ if (isGraceNote) {
764
+ // Grace beam: open/close independently, nested inside parent beam if open
765
+ const graceBaseIndent = beamElementOpen ? baseIndent + ' ' : baseIndent;
766
+ if (beamStart && !graceBeamOpen) {
767
+ xml += `${graceBaseIndent}<beam xml:id="${generateId('beam')}">\n`;
768
+ graceBeamOpen = true;
769
+ }
770
+ }
771
+ else {
772
+ // Close any open grace beam before processing a non-grace event
773
+ if (graceBeamOpen) {
774
+ const graceBaseIndent = beamElementOpen ? baseIndent + ' ' : baseIndent;
775
+ xml += `${graceBaseIndent}</beam>\n`;
776
+ graceBeamOpen = false;
777
+ }
778
+ // Open main beam element if beam starts
779
+ if (beamStart && !beamElementOpen) {
780
+ xml += `${baseIndent}<beam xml:id="${generateId('beam')}">\n`;
781
+ beamElementOpen = true;
782
+ }
764
783
  }
765
- const currentIndent = beamElementOpen ? baseIndent + ' ' : baseIndent;
784
+ const graceIndentBase = beamElementOpen ? baseIndent + ' ' : baseIndent;
785
+ const currentIndent = graceBeamOpen ? graceIndentBase + ' ' : (beamElementOpen ? baseIndent + ' ' : baseIndent);
766
786
  switch (event.type) {
767
787
  case 'note': {
768
788
  const noteEvent = event;
@@ -1002,11 +1022,25 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
1002
1022
  break;
1003
1023
  }
1004
1024
  // Close beam element if beam ends
1005
- if (beamEnd && beamElementOpen) {
1006
- xml += `${baseIndent}</beam>\n`;
1007
- beamElementOpen = false;
1025
+ if (beamEnd) {
1026
+ if (isGraceNote && graceBeamOpen) {
1027
+ // Close grace beam
1028
+ const graceBaseIndent = beamElementOpen ? baseIndent + ' ' : baseIndent;
1029
+ xml += `${graceBaseIndent}</beam>\n`;
1030
+ graceBeamOpen = false;
1031
+ }
1032
+ else if (!isGraceNote && beamElementOpen) {
1033
+ // Close main beam
1034
+ xml += `${baseIndent}</beam>\n`;
1035
+ beamElementOpen = false;
1036
+ }
1008
1037
  }
1009
1038
  }
1039
+ // Close any unclosed grace beam
1040
+ if (graceBeamOpen) {
1041
+ const graceBaseIndent = beamElementOpen ? baseIndent + ' ' : baseIndent;
1042
+ xml += `${graceBaseIndent}</beam>\n`;
1043
+ }
1010
1044
  // Close any unclosed beam
1011
1045
  if (beamElementOpen) {
1012
1046
  xml += `${baseIndent}</beam>\n`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k-l-lambda/lilylet",
3
- "version": "0.1.53",
3
+ "version": "0.1.54",
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",
@@ -329,7 +329,7 @@ case 92:
329
329
  this.$ = 'auto';
330
330
  break;
331
331
  case 93:
332
- this.$ = ($$[$0-1].filter(e => e.type === 'note' || e.type === 'rest').map(e => ({ ...e, grace: true })));
332
+ this.$ = ($$[$0-1].map(e => (e.type === 'note' || e.type === 'rest') ? { ...e, grace: true } : e));
333
333
  break;
334
334
  case 94: case 95:
335
335
  this.$ = ({ ...$$[$0], grace: true });
@@ -501,7 +501,7 @@ stem_cmd
501
501
  ;
502
502
 
503
503
  grace_event
504
- : CMD_GRACE '{' voice_events '}' -> ($3.filter(e => e.type === 'note' || e.type === 'rest').map(e => ({ ...e, grace: true })))
504
+ : CMD_GRACE '{' voice_events '}' -> ($3.map(e => (e.type === 'note' || e.type === 'rest') ? { ...e, grace: true } : e))
505
505
  | CMD_GRACE note_event -> ({ ...$2, grace: true })
506
506
  | CMD_GRACE rest_event -> ({ ...$2, grace: true })
507
507
  ;
@@ -1031,17 +1031,39 @@ const encodeLayer = (voice: Voice, layerN: number, indent: string, initialTiePit
1031
1031
  return true;
1032
1032
  };
1033
1033
 
1034
+ let graceBeamOpen = false; // Whether a grace note <beam> is open (independent of main beam)
1035
+
1034
1036
  for (const event of voice.events) {
1035
1037
  // Check for beam start/end in this event (including inside tuplets)
1036
1038
  const { beamStart, beamEnd } = getEventBeamMarks(event);
1037
1039
 
1038
- // Open beam element if beam starts
1039
- if (beamStart && !beamElementOpen) {
1040
- xml += `${baseIndent}<beam xml:id="${generateId('beam')}">\n`;
1041
- beamElementOpen = true;
1040
+ // Grace notes have independent beam groups - don't interfere with main beam
1041
+ const isGraceNote = event.type === 'note' && (event as NoteEvent).grace;
1042
+
1043
+ if (isGraceNote) {
1044
+ // Grace beam: open/close independently, nested inside parent beam if open
1045
+ const graceBaseIndent = beamElementOpen ? baseIndent + ' ' : baseIndent;
1046
+ if (beamStart && !graceBeamOpen) {
1047
+ xml += `${graceBaseIndent}<beam xml:id="${generateId('beam')}">\n`;
1048
+ graceBeamOpen = true;
1049
+ }
1050
+ } else {
1051
+ // Close any open grace beam before processing a non-grace event
1052
+ if (graceBeamOpen) {
1053
+ const graceBaseIndent = beamElementOpen ? baseIndent + ' ' : baseIndent;
1054
+ xml += `${graceBaseIndent}</beam>\n`;
1055
+ graceBeamOpen = false;
1056
+ }
1057
+
1058
+ // Open main beam element if beam starts
1059
+ if (beamStart && !beamElementOpen) {
1060
+ xml += `${baseIndent}<beam xml:id="${generateId('beam')}">\n`;
1061
+ beamElementOpen = true;
1062
+ }
1042
1063
  }
1043
1064
 
1044
- const currentIndent = beamElementOpen ? baseIndent + ' ' : baseIndent;
1065
+ const graceIndentBase = beamElementOpen ? baseIndent + ' ' : baseIndent;
1066
+ const currentIndent = graceBeamOpen ? graceIndentBase + ' ' : (beamElementOpen ? baseIndent + ' ' : baseIndent);
1045
1067
 
1046
1068
  switch (event.type) {
1047
1069
  case 'note': {
@@ -1292,12 +1314,26 @@ const encodeLayer = (voice: Voice, layerN: number, indent: string, initialTiePit
1292
1314
  }
1293
1315
 
1294
1316
  // Close beam element if beam ends
1295
- if (beamEnd && beamElementOpen) {
1296
- xml += `${baseIndent}</beam>\n`;
1297
- beamElementOpen = false;
1317
+ if (beamEnd) {
1318
+ if (isGraceNote && graceBeamOpen) {
1319
+ // Close grace beam
1320
+ const graceBaseIndent = beamElementOpen ? baseIndent + ' ' : baseIndent;
1321
+ xml += `${graceBaseIndent}</beam>\n`;
1322
+ graceBeamOpen = false;
1323
+ } else if (!isGraceNote && beamElementOpen) {
1324
+ // Close main beam
1325
+ xml += `${baseIndent}</beam>\n`;
1326
+ beamElementOpen = false;
1327
+ }
1298
1328
  }
1299
1329
  }
1300
1330
 
1331
+ // Close any unclosed grace beam
1332
+ if (graceBeamOpen) {
1333
+ const graceBaseIndent = beamElementOpen ? baseIndent + ' ' : baseIndent;
1334
+ xml += `${graceBaseIndent}</beam>\n`;
1335
+ }
1336
+
1301
1337
  // Close any unclosed beam
1302
1338
  if (beamElementOpen) {
1303
1339
  xml += `${baseIndent}</beam>\n`;