@k-l-lambda/lilylet 0.1.51 → 0.1.53
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 +67 -62
- package/lib/lilylet/meiEncoder.js +124 -32
- package/lib/lilylet/serializer.js +1 -0
- package/lib/lilylet/types.d.ts +2 -1
- package/lib/lilylet/types.js +1 -0
- package/package.json +1 -1
- package/source/lilylet/grammar.jison.js +67 -62
- package/source/lilylet/lilylet.jison +2 -0
- package/source/lilylet/meiEncoder.ts +125 -28
- package/source/lilylet/serializer.ts +1 -0
- package/source/lilylet/types.ts +1 -0
|
@@ -85,6 +85,7 @@ const DYNAMIC_MAP = {
|
|
|
85
85
|
fff: "fff",
|
|
86
86
|
sfz: "sfz",
|
|
87
87
|
rfz: "rfz",
|
|
88
|
+
fp: "fp",
|
|
88
89
|
};
|
|
89
90
|
// ID generation state - uses session prefix to prevent collisions in concurrent encoding
|
|
90
91
|
let idCounter = 0;
|
|
@@ -118,10 +119,12 @@ const getKeyAccidentals = (fifths) => {
|
|
|
118
119
|
}
|
|
119
120
|
return result;
|
|
120
121
|
};
|
|
121
|
-
// Convert Pitch to MEI attributes, checking against key signature
|
|
122
|
+
// Convert Pitch to MEI attributes, checking against key signature and in-measure accidentals
|
|
122
123
|
// ottavaShift: current ottava level (1 = 8va up, -1 = 8vb down, 2 = 15ma up, etc.)
|
|
123
124
|
// The written pitch should be adjusted by subtracting the ottava shift
|
|
124
|
-
|
|
125
|
+
// measureAccidentals: tracks accidentals used earlier in the same measure (keyed by "pname-oct")
|
|
126
|
+
// - mutated to record new accidentals; used to add cancellation naturals
|
|
127
|
+
const encodePitch = (pitch, keyFifths = 0, ottavaShift = 0, measureAccidentals) => {
|
|
125
128
|
// Lilylet octave: 0 = middle C octave (C4), positive = higher, negative = lower
|
|
126
129
|
// When ottava is active, the source pitch is the sounding pitch, but we need to output the written pitch
|
|
127
130
|
// For 8va up (ottavaShift=1), written pitch is one octave lower than sounding
|
|
@@ -132,19 +135,44 @@ const encodePitch = (pitch, keyFifths = 0, ottavaShift = 0) => {
|
|
|
132
135
|
// Determine accid (written/displayed) and accid.ges (gestural/sounding)
|
|
133
136
|
let accid;
|
|
134
137
|
let accidGes;
|
|
138
|
+
const pitchKey = `${pitch.phonet}-${oct}`;
|
|
139
|
+
// Check what was previously established for this pitch in this measure
|
|
140
|
+
const prevMeasureAccid = measureAccidentals?.get(pitchKey);
|
|
135
141
|
if (pitch.accidental) {
|
|
136
142
|
const noteAccid = ACCIDENTALS[pitch.accidental];
|
|
137
|
-
if (noteAccid !==
|
|
143
|
+
if (prevMeasureAccid !== undefined && noteAccid !== prevMeasureAccid) {
|
|
144
|
+
// Previous note in this measure had a different accidental - must re-assert
|
|
145
|
+
accid = noteAccid;
|
|
146
|
+
}
|
|
147
|
+
else if (noteAccid !== keyAccid) {
|
|
138
148
|
// Accidental differs from key signature - display it
|
|
139
149
|
accid = noteAccid;
|
|
140
150
|
}
|
|
141
151
|
// Always set gestural accidental for MIDI generation
|
|
142
152
|
accidGes = noteAccid;
|
|
153
|
+
// Record this accidental for in-measure tracking
|
|
154
|
+
if (measureAccidentals)
|
|
155
|
+
measureAccidentals.set(pitchKey, noteAccid);
|
|
143
156
|
}
|
|
144
157
|
else if (keyAccid) {
|
|
145
158
|
// Note has no accidental but key implies one - output natural
|
|
146
|
-
|
|
159
|
+
if (prevMeasureAccid === 'n') {
|
|
160
|
+
// Already cancelled earlier in this measure - no need to show again
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
accid = 'n';
|
|
164
|
+
}
|
|
147
165
|
accidGes = 'n';
|
|
166
|
+
if (measureAccidentals)
|
|
167
|
+
measureAccidentals.set(pitchKey, 'n');
|
|
168
|
+
}
|
|
169
|
+
else if (measureAccidentals) {
|
|
170
|
+
// No explicit accidental, no key accidental - check if earlier note in measure had one
|
|
171
|
+
if (prevMeasureAccid && prevMeasureAccid !== 'n') {
|
|
172
|
+
// Previous note had an accidental - add cancellation natural
|
|
173
|
+
accid = 'n';
|
|
174
|
+
measureAccidentals.set(pitchKey, 'n');
|
|
175
|
+
}
|
|
148
176
|
}
|
|
149
177
|
return { pname: pitch.phonet, oct, accid, accidGes };
|
|
150
178
|
};
|
|
@@ -338,7 +366,7 @@ const extractMarkOptions = (marks) => {
|
|
|
338
366
|
return result;
|
|
339
367
|
};
|
|
340
368
|
// Convert NoteEvent to MEI
|
|
341
|
-
const noteEventToMEI = (event, indent, layerStaff, tieEnd, contextStemDir, keyFifths = 0, ottavaShift = 0) => {
|
|
369
|
+
const noteEventToMEI = (event, indent, layerStaff, tieEnd, contextStemDir, keyFifths = 0, ottavaShift = 0, measureAccidentals) => {
|
|
342
370
|
const dur = DURATIONS[event.duration.division] || "4";
|
|
343
371
|
const dots = event.duration.dots || 0;
|
|
344
372
|
const markOptions = extractMarkOptions(event.marks);
|
|
@@ -372,7 +400,7 @@ const noteEventToMEI = (event, indent, layerStaff, tieEnd, contextStemDir, keyFi
|
|
|
372
400
|
};
|
|
373
401
|
// Single note
|
|
374
402
|
if (event.pitches.length === 1) {
|
|
375
|
-
const pitch = encodePitch(event.pitches[0], keyFifths, ottavaShift);
|
|
403
|
+
const pitch = encodePitch(event.pitches[0], keyFifths, ottavaShift, measureAccidentals);
|
|
376
404
|
const noteId = generateId('note');
|
|
377
405
|
return {
|
|
378
406
|
xml: buildNoteElement(pitch, dur, dots, indent, false, noteOptions, noteId),
|
|
@@ -415,7 +443,7 @@ const noteEventToMEI = (event, indent, layerStaff, tieEnd, contextStemDir, keyFi
|
|
|
415
443
|
}
|
|
416
444
|
let result = `${indent}<chord ${chordAttrs}>\n`;
|
|
417
445
|
for (const p of event.pitches) {
|
|
418
|
-
const pitch = encodePitch(p, keyFifths, ottavaShift);
|
|
446
|
+
const pitch = encodePitch(p, keyFifths, ottavaShift, measureAccidentals);
|
|
419
447
|
result += buildNoteElement(pitch, dur, dots, indent + ' ', true);
|
|
420
448
|
}
|
|
421
449
|
// Artics for chord - group by placement
|
|
@@ -455,7 +483,7 @@ const noteEventToMEI = (event, indent, layerStaff, tieEnd, contextStemDir, keyFi
|
|
|
455
483
|
};
|
|
456
484
|
};
|
|
457
485
|
// Convert RestEvent to MEI
|
|
458
|
-
const restEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0) => {
|
|
486
|
+
const restEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0, measureAccidentals) => {
|
|
459
487
|
const dur = DURATIONS[event.duration.division] || "4";
|
|
460
488
|
let attrs = `xml:id="${generateId('rest')}" dur="${dur}"`;
|
|
461
489
|
if (event.duration.dots > 0)
|
|
@@ -475,8 +503,24 @@ const restEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0) => {
|
|
|
475
503
|
}
|
|
476
504
|
return `${indent}<rest ${attrs} />\n`;
|
|
477
505
|
};
|
|
506
|
+
// Check if a tuplet has balanced internal beam groups (both [ and ] inside the same tuplet)
|
|
507
|
+
// Returns false for cross-tuplet beams where [ is in one tuplet and ] in another
|
|
508
|
+
const tupletHasInternalBeams = (event) => {
|
|
509
|
+
let starts = 0;
|
|
510
|
+
let ends = 0;
|
|
511
|
+
for (const e of event.events) {
|
|
512
|
+
if (e.type === 'note') {
|
|
513
|
+
const markOptions = extractMarkOptions(e.marks);
|
|
514
|
+
if (markOptions.beamStart)
|
|
515
|
+
starts++;
|
|
516
|
+
if (markOptions.beamEnd)
|
|
517
|
+
ends++;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return starts > 0 && starts === ends;
|
|
521
|
+
};
|
|
478
522
|
// Convert TupletEvent to MEI
|
|
479
|
-
const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff, ottavaShift = 0, inParentBeam = false) => {
|
|
523
|
+
const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff, ottavaShift = 0, inParentBeam = false, measureAccidentals) => {
|
|
480
524
|
// LilyPond \times 2/3 means "multiply duration by 2/3"
|
|
481
525
|
// So 3 notes × 2/3 = 2 beats worth (3 in time of 2)
|
|
482
526
|
// MEI: num = number of notes written, numbase = normal equivalent
|
|
@@ -487,6 +531,7 @@ const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff
|
|
|
487
531
|
// Effective staff for cross-staff notation
|
|
488
532
|
const effectiveStaff = currentStaff ?? layerStaff;
|
|
489
533
|
// Collect control event info from notes inside tuplet
|
|
534
|
+
let firstNoteId = null;
|
|
490
535
|
const slurStarts = [];
|
|
491
536
|
const slurEnds = [];
|
|
492
537
|
const dynamics = [];
|
|
@@ -495,18 +540,27 @@ const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff
|
|
|
495
540
|
const mordents = [];
|
|
496
541
|
const turns = [];
|
|
497
542
|
const arpeggios = [];
|
|
498
|
-
//
|
|
499
|
-
|
|
500
|
-
|
|
543
|
+
// Handle internal beam groups: if notes have manual beam marks, respect them
|
|
544
|
+
const hasInternalBeams = !inParentBeam && tupletHasInternalBeams(event);
|
|
545
|
+
let beamOpen = false;
|
|
501
546
|
for (const e of event.events) {
|
|
502
547
|
if (e.type === 'note') {
|
|
503
|
-
// For cross-staff notation: set note's staff if different from layerStaff
|
|
504
548
|
const noteEvent = e;
|
|
549
|
+
const markOptions = extractMarkOptions(noteEvent.marks);
|
|
550
|
+
// Open beam if this note starts a beam group
|
|
551
|
+
if (hasInternalBeams && markOptions.beamStart && !beamOpen) {
|
|
552
|
+
xml += `${baseIndent}<beam xml:id="${generateId('beam')}">\n`;
|
|
553
|
+
beamOpen = true;
|
|
554
|
+
}
|
|
555
|
+
const noteIndent = beamOpen ? baseIndent + ' ' : baseIndent;
|
|
556
|
+
// For cross-staff notation: set note's staff if different from layerStaff
|
|
505
557
|
const effectiveNoteEvent = effectiveStaff && layerStaff && effectiveStaff !== layerStaff
|
|
506
558
|
? { ...noteEvent, staff: effectiveStaff }
|
|
507
559
|
: noteEvent;
|
|
508
|
-
const result = noteEventToMEI(effectiveNoteEvent,
|
|
560
|
+
const result = noteEventToMEI(effectiveNoteEvent, noteIndent, layerStaff, false, undefined, keyFifths, ottavaShift, measureAccidentals);
|
|
509
561
|
xml += result.xml;
|
|
562
|
+
if (!firstNoteId)
|
|
563
|
+
firstNoteId = result.elementId;
|
|
510
564
|
// Collect slur info
|
|
511
565
|
if (result.slurStart)
|
|
512
566
|
slurStarts.push(result.elementId);
|
|
@@ -525,16 +579,26 @@ const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff
|
|
|
525
579
|
turns.push({ startid: result.elementId });
|
|
526
580
|
if (result.arpeggio)
|
|
527
581
|
arpeggios.push({ plist: result.elementId });
|
|
582
|
+
// Close beam if this note ends a beam group
|
|
583
|
+
if (hasInternalBeams && markOptions.beamEnd && beamOpen) {
|
|
584
|
+
xml += `${baseIndent}</beam>\n`;
|
|
585
|
+
beamOpen = false;
|
|
586
|
+
}
|
|
528
587
|
}
|
|
529
588
|
else if (e.type === 'rest') {
|
|
530
|
-
|
|
589
|
+
const restIndent = beamOpen ? baseIndent + ' ' : baseIndent;
|
|
590
|
+
xml += restEventToMEI(e, restIndent, keyFifths, ottavaShift, measureAccidentals);
|
|
531
591
|
}
|
|
532
592
|
}
|
|
593
|
+
// Close any unclosed beam
|
|
594
|
+
if (beamOpen) {
|
|
595
|
+
xml += `${baseIndent}</beam>\n`;
|
|
596
|
+
}
|
|
533
597
|
xml += `${indent}</tuplet>\n`;
|
|
534
|
-
return { xml, slurStarts, slurEnds, dynamics, fermatas, trills, mordents, turns, arpeggios };
|
|
598
|
+
return { xml, firstNoteId, slurStarts, slurEnds, dynamics, fermatas, trills, mordents, turns, arpeggios };
|
|
535
599
|
};
|
|
536
600
|
// Convert TremoloEvent to MEI (fingered tremolo - alternating between two notes)
|
|
537
|
-
const tremoloEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0) => {
|
|
601
|
+
const tremoloEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0, measureAccidentals) => {
|
|
538
602
|
const ftremId = generateId('fTrem');
|
|
539
603
|
// For \repeat tremolo 4 { c16 d16 }:
|
|
540
604
|
// - count = 4 (repetitions)
|
|
@@ -553,7 +617,7 @@ const tremoloEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0) => {
|
|
|
553
617
|
let result = `${indent}<fTrem xml:id="${ftremId}" beams="${beams}">\n`;
|
|
554
618
|
// First note (or chord)
|
|
555
619
|
if (event.pitchA.length === 1) {
|
|
556
|
-
const pitch = encodePitch(event.pitchA[0], keyFifths, ottavaShift);
|
|
620
|
+
const pitch = encodePitch(event.pitchA[0], keyFifths, ottavaShift, measureAccidentals);
|
|
557
621
|
let attrs = `xml:id="${generateId('note')}" pname="${pitch.pname}" oct="${pitch.oct}" dur="${noteDur}"`;
|
|
558
622
|
if (pitch.accid)
|
|
559
623
|
attrs += ` accid="${pitch.accid}"`;
|
|
@@ -564,7 +628,7 @@ const tremoloEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0) => {
|
|
|
564
628
|
else if (event.pitchA.length > 1) {
|
|
565
629
|
result += `${indent} <chord xml:id="${generateId('chord')}" dur="${noteDur}">\n`;
|
|
566
630
|
for (const p of event.pitchA) {
|
|
567
|
-
const pitch = encodePitch(p, keyFifths, ottavaShift);
|
|
631
|
+
const pitch = encodePitch(p, keyFifths, ottavaShift, measureAccidentals);
|
|
568
632
|
let attrs = `xml:id="${generateId('note')}" pname="${pitch.pname}" oct="${pitch.oct}"`;
|
|
569
633
|
if (pitch.accid)
|
|
570
634
|
attrs += ` accid="${pitch.accid}"`;
|
|
@@ -576,7 +640,7 @@ const tremoloEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0) => {
|
|
|
576
640
|
}
|
|
577
641
|
// Second note (or chord)
|
|
578
642
|
if (event.pitchB.length === 1) {
|
|
579
|
-
const pitch = encodePitch(event.pitchB[0], keyFifths, ottavaShift);
|
|
643
|
+
const pitch = encodePitch(event.pitchB[0], keyFifths, ottavaShift, measureAccidentals);
|
|
580
644
|
let attrs = `xml:id="${generateId('note')}" pname="${pitch.pname}" oct="${pitch.oct}" dur="${noteDur}"`;
|
|
581
645
|
if (pitch.accid)
|
|
582
646
|
attrs += ` accid="${pitch.accid}"`;
|
|
@@ -587,7 +651,7 @@ const tremoloEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0) => {
|
|
|
587
651
|
else if (event.pitchB.length > 1) {
|
|
588
652
|
result += `${indent} <chord xml:id="${generateId('chord')}" dur="${noteDur}">\n`;
|
|
589
653
|
for (const p of event.pitchB) {
|
|
590
|
-
const pitch = encodePitch(p, keyFifths, ottavaShift);
|
|
654
|
+
const pitch = encodePitch(p, keyFifths, ottavaShift, measureAccidentals);
|
|
591
655
|
let attrs = `xml:id="${generateId('note')}" pname="${pitch.pname}" oct="${pitch.oct}"`;
|
|
592
656
|
if (pitch.accid)
|
|
593
657
|
attrs += ` accid="${pitch.accid}"`;
|
|
@@ -608,6 +672,11 @@ const getEventBeamMarks = (event) => {
|
|
|
608
672
|
}
|
|
609
673
|
if (event.type === 'tuplet') {
|
|
610
674
|
const tuplet = event;
|
|
675
|
+
// If the tuplet has internal beam groups, don't report beam marks to the parent
|
|
676
|
+
// so the parent won't wrap the tuplet in an external <beam>
|
|
677
|
+
if (tupletHasInternalBeams(tuplet)) {
|
|
678
|
+
return { beamStart: false, beamEnd: false };
|
|
679
|
+
}
|
|
611
680
|
let beamStart = false;
|
|
612
681
|
let beamEnd = false;
|
|
613
682
|
for (const e of tuplet.events) {
|
|
@@ -659,12 +728,22 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
|
|
|
659
728
|
const harmonies = [];
|
|
660
729
|
const barlines = [];
|
|
661
730
|
const markups = [];
|
|
731
|
+
const pendingMarkups = [];
|
|
662
732
|
// Track current stem direction from context changes
|
|
663
733
|
let currentStemDirection = undefined;
|
|
664
734
|
// Track current staff for cross-staff notation
|
|
665
735
|
let currentStaff = voice.staff || 1;
|
|
736
|
+
// Track in-measure accidentals for cancellation naturals
|
|
737
|
+
const measureAccidentals = new Map();
|
|
666
738
|
// Track pending tie pitches (for tie="t" on next note) - initialized from previous measure
|
|
667
739
|
let pendingTiePitches = [...initialTiePitches];
|
|
740
|
+
// Helper to flush pending markups onto a note ID
|
|
741
|
+
const flushPendingMarkups = (noteId) => {
|
|
742
|
+
for (const mkup of pendingMarkups) {
|
|
743
|
+
markups.push({ startid: noteId, content: mkup.content, placement: mkup.placement });
|
|
744
|
+
}
|
|
745
|
+
pendingMarkups.length = 0;
|
|
746
|
+
};
|
|
668
747
|
// Helper to check if pitches match for tie continuation
|
|
669
748
|
const pitchesMatch = (p1, p2) => {
|
|
670
749
|
if (p1.length !== p2.length)
|
|
@@ -697,9 +776,11 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
|
|
|
697
776
|
const effectiveNoteEvent = currentStaff !== voice.staff
|
|
698
777
|
? { ...noteEvent, staff: currentStaff }
|
|
699
778
|
: noteEvent;
|
|
700
|
-
const result = noteEventToMEI(effectiveNoteEvent, currentIndent, voice.staff, tieEnd, currentStemDirection, keyFifths, currentOttavaShift);
|
|
779
|
+
const result = noteEventToMEI(effectiveNoteEvent, currentIndent, voice.staff, tieEnd, currentStemDirection, keyFifths, currentOttavaShift, measureAccidentals);
|
|
701
780
|
xml += result.xml;
|
|
702
781
|
lastNoteId = result.elementId;
|
|
782
|
+
// Flush any pending markups onto this note
|
|
783
|
+
flushPendingMarkups(result.elementId);
|
|
703
784
|
// If there's a pending ottava, start the span on this note
|
|
704
785
|
if (pendingOttava !== null && pendingOttava !== 0) {
|
|
705
786
|
const dis = Math.abs(pendingOttava) === 2 ? 15 : 8;
|
|
@@ -799,13 +880,18 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
|
|
|
799
880
|
break;
|
|
800
881
|
}
|
|
801
882
|
case 'rest':
|
|
802
|
-
xml += restEventToMEI(event, currentIndent, keyFifths, currentOttavaShift);
|
|
883
|
+
xml += restEventToMEI(event, currentIndent, keyFifths, currentOttavaShift, measureAccidentals);
|
|
803
884
|
break;
|
|
804
885
|
case 'tuplet': {
|
|
805
886
|
// Tuplet can be nested inside beam in MEI: <beam><tuplet>...</tuplet></beam>
|
|
806
887
|
// Pass beamElementOpen to tuplet so it knows not to create its own beam
|
|
807
|
-
const tupletResult = tupletEventToMEI(event, currentIndent, voice.staff, keyFifths, currentStaff, currentOttavaShift, beamElementOpen);
|
|
888
|
+
const tupletResult = tupletEventToMEI(event, currentIndent, voice.staff, keyFifths, currentStaff, currentOttavaShift, beamElementOpen, measureAccidentals);
|
|
808
889
|
xml += tupletResult.xml;
|
|
890
|
+
// Flush any pending markups onto the first note of the tuplet
|
|
891
|
+
if (tupletResult.firstNoteId) {
|
|
892
|
+
flushPendingMarkups(tupletResult.firstNoteId);
|
|
893
|
+
lastNoteId = tupletResult.firstNoteId;
|
|
894
|
+
}
|
|
809
895
|
// Process slur ends first (to close any pending slurs from before this tuplet)
|
|
810
896
|
for (const endId of tupletResult.slurEnds) {
|
|
811
897
|
if (currentSlur) {
|
|
@@ -830,7 +916,7 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
|
|
|
830
916
|
break;
|
|
831
917
|
}
|
|
832
918
|
case 'tremolo':
|
|
833
|
-
xml += tremoloEventToMEI(event, currentIndent, keyFifths, currentOttavaShift);
|
|
919
|
+
xml += tremoloEventToMEI(event, currentIndent, keyFifths, currentOttavaShift, measureAccidentals);
|
|
834
920
|
break;
|
|
835
921
|
case 'context': {
|
|
836
922
|
const ctx = event;
|
|
@@ -898,14 +984,20 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
|
|
|
898
984
|
}
|
|
899
985
|
break;
|
|
900
986
|
case 'markup':
|
|
901
|
-
|
|
902
|
-
|
|
987
|
+
{
|
|
988
|
+
// Markup needs a note ID to attach to
|
|
903
989
|
const mkupEvent = event;
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
990
|
+
if (lastNoteId) {
|
|
991
|
+
markups.push({
|
|
992
|
+
startid: lastNoteId,
|
|
993
|
+
content: mkupEvent.content,
|
|
994
|
+
placement: mkupEvent.placement,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
// No note yet - save as pending, will attach to next note
|
|
999
|
+
pendingMarkups.push({ content: mkupEvent.content, placement: mkupEvent.placement });
|
|
1000
|
+
}
|
|
909
1001
|
}
|
|
910
1002
|
break;
|
|
911
1003
|
}
|
package/lib/lilylet/types.d.ts
CHANGED
package/lib/lilylet/types.js
CHANGED
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.53",
|
|
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",
|