@k-l-lambda/lilylet 0.1.54 → 0.1.56
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.
- package/lib/lilylet/grammar.jison.js +140 -134
- package/lib/lilylet/meiEncoder.js +42 -13
- package/lib/lilylet/types.d.ts +1 -0
- package/package.json +1 -1
- package/source/lilylet/grammar.jison.js +140 -134
- package/source/lilylet/lilylet.jison +3 -2
- package/source/lilylet/meiEncoder.ts +38 -13
- package/source/lilylet/types.ts +1 -0
|
@@ -396,7 +396,9 @@ pitches
|
|
|
396
396
|
;
|
|
397
397
|
|
|
398
398
|
pitch
|
|
399
|
-
: PITCH octave
|
|
399
|
+
: PITCH octave '!' -> { ...parsePitch($1, $2), courtesy: true }
|
|
400
|
+
| PITCH '!' -> { ...parsePitch($1, 0), courtesy: true }
|
|
401
|
+
| PITCH octave -> parsePitch($1, $2)
|
|
400
402
|
| PITCH -> parsePitch($1, 0)
|
|
401
403
|
;
|
|
402
404
|
|
|
@@ -564,7 +566,6 @@ articulation_mark
|
|
|
564
566
|
| '>' -> articulation('accent')
|
|
565
567
|
| '.' -> articulation('staccato')
|
|
566
568
|
| '-' -> articulation('tenuto')
|
|
567
|
-
| '!' -> articulation('staccatissimo')
|
|
568
569
|
| '^' -> articulation('marcato')
|
|
569
570
|
| '_' -> articulation('portato')
|
|
570
571
|
;
|
|
@@ -198,7 +198,10 @@ const encodePitch = (pitch: Pitch, keyFifths: number = 0, ottavaShift: number =
|
|
|
198
198
|
|
|
199
199
|
if (pitch.accidental) {
|
|
200
200
|
const noteAccid = ACCIDENTALS[pitch.accidental];
|
|
201
|
-
if (
|
|
201
|
+
if (pitch.courtesy) {
|
|
202
|
+
// Courtesy accidental (!) - always display
|
|
203
|
+
accid = noteAccid;
|
|
204
|
+
} else if (prevMeasureAccid !== undefined && noteAccid !== prevMeasureAccid) {
|
|
202
205
|
// Previous note in this measure had a different accidental - must re-assert
|
|
203
206
|
accid = noteAccid;
|
|
204
207
|
} else if (noteAccid !== keyAccid) {
|
|
@@ -211,13 +214,21 @@ const encodePitch = (pitch: Pitch, keyFifths: number = 0, ottavaShift: number =
|
|
|
211
214
|
if (measureAccidentals) measureAccidentals.set(pitchKey, noteAccid);
|
|
212
215
|
} else if (keyAccid) {
|
|
213
216
|
// Note has no accidental but key implies one - output natural
|
|
214
|
-
if (
|
|
217
|
+
if (pitch.courtesy) {
|
|
218
|
+
// Courtesy accidental (!) - always display
|
|
219
|
+
accid = 'n';
|
|
220
|
+
} else if (prevMeasureAccid === 'n') {
|
|
215
221
|
// Already cancelled earlier in this measure - no need to show again
|
|
216
222
|
} else {
|
|
217
223
|
accid = 'n';
|
|
218
224
|
}
|
|
219
225
|
accidGes = 'n';
|
|
220
226
|
if (measureAccidentals) measureAccidentals.set(pitchKey, 'n');
|
|
227
|
+
} else if (pitch.courtesy && prevMeasureAccid && prevMeasureAccid !== 'n') {
|
|
228
|
+
// Courtesy accidental after an in-measure accidental - force natural display
|
|
229
|
+
accid = 'n';
|
|
230
|
+
accidGes = 'n';
|
|
231
|
+
if (measureAccidentals) measureAccidentals.set(pitchKey, 'n');
|
|
221
232
|
} else if (measureAccidentals) {
|
|
222
233
|
// No explicit accidental, no key accidental - check if earlier note in measure had one
|
|
223
234
|
if (prevMeasureAccid && prevMeasureAccid !== 'n') {
|
|
@@ -603,11 +614,14 @@ const noteEventToMEI = (
|
|
|
603
614
|
|
|
604
615
|
|
|
605
616
|
// Convert RestEvent to MEI
|
|
606
|
-
const restEventToMEI = (event: RestEvent, indent: string, keyFifths: number = 0, ottavaShift: number = 0, measureAccidentals?: Map<string, string
|
|
617
|
+
const restEventToMEI = (event: RestEvent, indent: string, keyFifths: number = 0, ottavaShift: number = 0, measureAccidentals?: Map<string, string>, crossStaff?: number): string => {
|
|
607
618
|
const dur = DURATIONS[event.duration.division] || "4";
|
|
608
619
|
let attrs = `xml:id="${generateId('rest')}" dur="${dur}"`;
|
|
609
620
|
if (event.duration.dots > 0) attrs += ` dots="${event.duration.dots}"`;
|
|
610
621
|
|
|
622
|
+
// Cross-staff attribute
|
|
623
|
+
if (crossStaff) attrs += ` staff="${crossStaff}"`;
|
|
624
|
+
|
|
611
625
|
// Pitched rest (positioned at specific pitch)
|
|
612
626
|
if (event.pitch) {
|
|
613
627
|
const pitch = encodePitch(event.pitch, keyFifths, ottavaShift);
|
|
@@ -1189,9 +1203,12 @@ const encodeLayer = (voice: Voice, layerN: number, indent: string, initialTiePit
|
|
|
1189
1203
|
}
|
|
1190
1204
|
break;
|
|
1191
1205
|
}
|
|
1192
|
-
case 'rest':
|
|
1193
|
-
|
|
1206
|
+
case 'rest': {
|
|
1207
|
+
// For cross-staff notation: pass staff number if different from voice's home staff
|
|
1208
|
+
const restCrossStaff = currentStaff !== (voice.staff || 1) ? currentStaff : undefined;
|
|
1209
|
+
xml += restEventToMEI(event as RestEvent, currentIndent, keyFifths, currentOttavaShift, measureAccidentals, restCrossStaff);
|
|
1194
1210
|
break;
|
|
1211
|
+
}
|
|
1195
1212
|
case 'tuplet': {
|
|
1196
1213
|
// Tuplet can be nested inside beam in MEI: <beam><tuplet>...</tuplet></beam>
|
|
1197
1214
|
// Pass beamElementOpen to tuplet so it knows not to create its own beam
|
|
@@ -1236,9 +1253,13 @@ const encodeLayer = (voice: Voice, layerN: number, indent: string, initialTiePit
|
|
|
1236
1253
|
case 'context': {
|
|
1237
1254
|
const ctx = event as ContextChange;
|
|
1238
1255
|
// Check for clef changes - emit <clef> element only if different from current
|
|
1256
|
+
// and only when on the home staff (don't emit cross-staff clefs into this layer)
|
|
1239
1257
|
if (ctx.clef && ctx.clef !== currentClef) {
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1258
|
+
const layerStaff = voice.staff || 1;
|
|
1259
|
+
if (currentStaff === layerStaff) {
|
|
1260
|
+
const clefInfo = CLEF_SHAPES[ctx.clef] || CLEF_SHAPES.treble;
|
|
1261
|
+
xml += `${currentIndent}<clef xml:id="${generateId('clef')}" shape="${clefInfo.shape}" line="${clefInfo.line}" />\n`;
|
|
1262
|
+
}
|
|
1242
1263
|
currentClef = ctx.clef;
|
|
1243
1264
|
}
|
|
1244
1265
|
// Check for ottava changes
|
|
@@ -1832,16 +1853,20 @@ const analyzePartStructure = (doc: LilyletDoc): PartInfo[] => {
|
|
|
1832
1853
|
for (let pi = 0; pi < measure.parts.length; pi++) {
|
|
1833
1854
|
const part = measure.parts[pi];
|
|
1834
1855
|
for (const voice of part.voices) {
|
|
1835
|
-
|
|
1836
|
-
partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff,
|
|
1856
|
+
let currentStaff = voice.staff || 1;
|
|
1857
|
+
partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff, currentStaff);
|
|
1837
1858
|
|
|
1838
|
-
//
|
|
1859
|
+
// Scan context changes for staff switches and clefs
|
|
1839
1860
|
for (const event of voice.events) {
|
|
1840
1861
|
if (event.type === 'context') {
|
|
1841
1862
|
const ctx = event as ContextChange;
|
|
1842
|
-
if (ctx.
|
|
1843
|
-
|
|
1844
|
-
partInfos[pi].
|
|
1863
|
+
if (ctx.staff !== undefined) {
|
|
1864
|
+
currentStaff = ctx.staff;
|
|
1865
|
+
partInfos[pi].maxStaff = Math.max(partInfos[pi].maxStaff, currentStaff);
|
|
1866
|
+
}
|
|
1867
|
+
if (ctx.clef && !partInfos[pi].clefs[currentStaff]) {
|
|
1868
|
+
// Only set if not already set - take the FIRST clef per staff
|
|
1869
|
+
partInfos[pi].clefs[currentStaff] = ctx.clef;
|
|
1845
1870
|
}
|
|
1846
1871
|
}
|
|
1847
1872
|
}
|
package/source/lilylet/types.ts
CHANGED
|
@@ -110,6 +110,7 @@ export interface Pitch {
|
|
|
110
110
|
phonet: Phonet;
|
|
111
111
|
accidental?: Accidental;
|
|
112
112
|
octave: number; // 0 = middle C octave, positive = higher, negative = lower
|
|
113
|
+
courtesy?: boolean; // force display of accidental (! in LilyPond)
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
export interface Duration {
|