@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].
|
|
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
|
-
//
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
|
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
|
|
926
|
-
|
|
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
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
1436
|
-
partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff,
|
|
1437
|
-
//
|
|
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.
|
|
1442
|
-
|
|
1443
|
-
partInfos[pi].
|
|
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.
|
|
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].
|
|
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.
|
|
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
|
-
//
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
|
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
|
|
1219
|
-
|
|
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
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
-
|
|
1800
|
-
partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff,
|
|
1839
|
+
let currentStaff = voice.staff || 1;
|
|
1840
|
+
partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff, currentStaff);
|
|
1801
1841
|
|
|
1802
|
-
//
|
|
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.
|
|
1807
|
-
|
|
1808
|
-
partInfos[pi].
|
|
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
|
}
|