@k-l-lambda/lilylet 0.1.53 → 0.1.55

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;
@@ -921,9 +941,13 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
921
941
  case 'context': {
922
942
  const ctx = event;
923
943
  // Check for clef changes - emit <clef> element only if different from current
944
+ // and only when on the home staff (don't emit cross-staff clefs into this layer)
924
945
  if (ctx.clef && ctx.clef !== currentClef) {
925
- const clefInfo = CLEF_SHAPES[ctx.clef] || CLEF_SHAPES.treble;
926
- xml += `${currentIndent}<clef xml:id="${generateId('clef')}" shape="${clefInfo.shape}" line="${clefInfo.line}" />\n`;
946
+ const layerStaff = voice.staff || 1;
947
+ if (currentStaff === layerStaff) {
948
+ const clefInfo = CLEF_SHAPES[ctx.clef] || CLEF_SHAPES.treble;
949
+ xml += `${currentIndent}<clef xml:id="${generateId('clef')}" shape="${clefInfo.shape}" line="${clefInfo.line}" />\n`;
950
+ }
927
951
  currentClef = ctx.clef;
928
952
  }
929
953
  // Check for ottava changes
@@ -1002,11 +1026,25 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
1002
1026
  break;
1003
1027
  }
1004
1028
  // Close beam element if beam ends
1005
- if (beamEnd && beamElementOpen) {
1006
- xml += `${baseIndent}</beam>\n`;
1007
- beamElementOpen = false;
1029
+ if (beamEnd) {
1030
+ if (isGraceNote && graceBeamOpen) {
1031
+ // Close grace beam
1032
+ const graceBaseIndent = beamElementOpen ? baseIndent + ' ' : baseIndent;
1033
+ xml += `${graceBaseIndent}</beam>\n`;
1034
+ graceBeamOpen = false;
1035
+ }
1036
+ else if (!isGraceNote && beamElementOpen) {
1037
+ // Close main beam
1038
+ xml += `${baseIndent}</beam>\n`;
1039
+ beamElementOpen = false;
1040
+ }
1008
1041
  }
1009
1042
  }
1043
+ // Close any unclosed grace beam
1044
+ if (graceBeamOpen) {
1045
+ const graceBaseIndent = beamElementOpen ? baseIndent + ' ' : baseIndent;
1046
+ xml += `${graceBaseIndent}</beam>\n`;
1047
+ }
1010
1048
  // Close any unclosed beam
1011
1049
  if (beamElementOpen) {
1012
1050
  xml += `${baseIndent}</beam>\n`;
@@ -1432,15 +1470,19 @@ const analyzePartStructure = (doc) => {
1432
1470
  for (let pi = 0; pi < measure.parts.length; pi++) {
1433
1471
  const part = measure.parts[pi];
1434
1472
  for (const voice of part.voices) {
1435
- const localStaff = voice.staff || 1;
1436
- partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff, localStaff);
1437
- // Get FIRST clef from context changes (for initial staffDef)
1473
+ let currentStaff = voice.staff || 1;
1474
+ partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff, currentStaff);
1475
+ // Scan context changes for staff switches and clefs
1438
1476
  for (const event of voice.events) {
1439
1477
  if (event.type === 'context') {
1440
1478
  const ctx = event;
1441
- if (ctx.clef && !partInfos[pi].clefs[localStaff]) {
1442
- // Only set if not already set - take the FIRST clef
1443
- partInfos[pi].clefs[localStaff] = ctx.clef;
1479
+ if (ctx.staff !== undefined) {
1480
+ currentStaff = ctx.staff;
1481
+ partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff, currentStaff);
1482
+ }
1483
+ if (ctx.clef && !partInfos[pi].clefs[currentStaff]) {
1484
+ // Only set if not already set - take the FIRST clef per staff
1485
+ partInfos[pi].clefs[currentStaff] = ctx.clef;
1444
1486
  }
1445
1487
  }
1446
1488
  }
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.55",
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': {
@@ -1214,9 +1236,13 @@ const encodeLayer = (voice: Voice, layerN: number, indent: string, initialTiePit
1214
1236
  case 'context': {
1215
1237
  const ctx = event as ContextChange;
1216
1238
  // Check for clef changes - emit <clef> element only if different from current
1239
+ // and only when on the home staff (don't emit cross-staff clefs into this layer)
1217
1240
  if (ctx.clef && ctx.clef !== currentClef) {
1218
- const clefInfo = CLEF_SHAPES[ctx.clef] || CLEF_SHAPES.treble;
1219
- xml += `${currentIndent}<clef xml:id="${generateId('clef')}" shape="${clefInfo.shape}" line="${clefInfo.line}" />\n`;
1241
+ const layerStaff = voice.staff || 1;
1242
+ if (currentStaff === layerStaff) {
1243
+ const clefInfo = CLEF_SHAPES[ctx.clef] || CLEF_SHAPES.treble;
1244
+ xml += `${currentIndent}<clef xml:id="${generateId('clef')}" shape="${clefInfo.shape}" line="${clefInfo.line}" />\n`;
1245
+ }
1220
1246
  currentClef = ctx.clef;
1221
1247
  }
1222
1248
  // Check for ottava changes
@@ -1292,12 +1318,26 @@ const encodeLayer = (voice: Voice, layerN: number, indent: string, initialTiePit
1292
1318
  }
1293
1319
 
1294
1320
  // Close beam element if beam ends
1295
- if (beamEnd && beamElementOpen) {
1296
- xml += `${baseIndent}</beam>\n`;
1297
- beamElementOpen = false;
1321
+ if (beamEnd) {
1322
+ if (isGraceNote && graceBeamOpen) {
1323
+ // Close grace beam
1324
+ const graceBaseIndent = beamElementOpen ? baseIndent + ' ' : baseIndent;
1325
+ xml += `${graceBaseIndent}</beam>\n`;
1326
+ graceBeamOpen = false;
1327
+ } else if (!isGraceNote && beamElementOpen) {
1328
+ // Close main beam
1329
+ xml += `${baseIndent}</beam>\n`;
1330
+ beamElementOpen = false;
1331
+ }
1298
1332
  }
1299
1333
  }
1300
1334
 
1335
+ // Close any unclosed grace beam
1336
+ if (graceBeamOpen) {
1337
+ const graceBaseIndent = beamElementOpen ? baseIndent + ' ' : baseIndent;
1338
+ xml += `${graceBaseIndent}</beam>\n`;
1339
+ }
1340
+
1301
1341
  // Close any unclosed beam
1302
1342
  if (beamElementOpen) {
1303
1343
  xml += `${baseIndent}</beam>\n`;
@@ -1796,16 +1836,20 @@ const analyzePartStructure = (doc: LilyletDoc): PartInfo[] => {
1796
1836
  for (let pi = 0; pi < measure.parts.length; pi++) {
1797
1837
  const part = measure.parts[pi];
1798
1838
  for (const voice of part.voices) {
1799
- const localStaff = voice.staff || 1;
1800
- partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff, localStaff);
1839
+ let currentStaff = voice.staff || 1;
1840
+ partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff, currentStaff);
1801
1841
 
1802
- // Get FIRST clef from context changes (for initial staffDef)
1842
+ // Scan context changes for staff switches and clefs
1803
1843
  for (const event of voice.events) {
1804
1844
  if (event.type === 'context') {
1805
1845
  const ctx = event as ContextChange;
1806
- if (ctx.clef && !partInfos[pi].clefs[localStaff]) {
1807
- // Only set if not already set - take the FIRST clef
1808
- partInfos[pi].clefs[localStaff] = ctx.clef;
1846
+ if (ctx.staff !== undefined) {
1847
+ currentStaff = ctx.staff;
1848
+ partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff, currentStaff);
1849
+ }
1850
+ if (ctx.clef && !partInfos[pi].clefs[currentStaff]) {
1851
+ // Only set if not already set - take the FIRST clef per staff
1852
+ partInfos[pi].clefs[currentStaff] = ctx.clef;
1809
1853
  }
1810
1854
  }
1811
1855
  }