@k-l-lambda/lilylet 0.1.71 → 0.1.72

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.
Files changed (104) hide show
  1. package/lib/highlight.d.ts +1 -0
  2. package/lib/highlight.js +1 -0
  3. package/lib/lilylet/highlight.d.ts +29 -0
  4. package/lib/lilylet/highlight.js +145 -0
  5. package/package.json +8 -2
  6. package/source/lilylet/highlight.ts +192 -0
  7. package/lib/source/abc/abc.d.ts +0 -102
  8. package/lib/source/abc/abc.js +0 -25
  9. package/lib/source/abc/parser.d.ts +0 -3
  10. package/lib/source/abc/parser.js +0 -6
  11. package/lib/source/lilylet/abcDecoder.d.ts +0 -25
  12. package/lib/source/lilylet/abcDecoder.js +0 -1035
  13. package/lib/source/lilylet/index.d.ts +0 -10
  14. package/lib/source/lilylet/index.js +0 -10
  15. package/lib/source/lilylet/lilypondDecoder.d.ts +0 -29
  16. package/lib/source/lilylet/lilypondDecoder.js +0 -1223
  17. package/lib/source/lilylet/lilypondEncoder.d.ts +0 -34
  18. package/lib/source/lilylet/lilypondEncoder.js +0 -893
  19. package/lib/source/lilylet/meiEncoder.d.ts +0 -8
  20. package/lib/source/lilylet/meiEncoder.js +0 -1985
  21. package/lib/source/lilylet/musicXmlDecoder.d.ts +0 -20
  22. package/lib/source/lilylet/musicXmlDecoder.js +0 -1195
  23. package/lib/source/lilylet/musicXmlEncoder.d.ts +0 -15
  24. package/lib/source/lilylet/musicXmlEncoder.js +0 -701
  25. package/lib/source/lilylet/musicXmlTypes.d.ts +0 -199
  26. package/lib/source/lilylet/musicXmlTypes.js +0 -7
  27. package/lib/source/lilylet/musicXmlUtils.d.ts +0 -92
  28. package/lib/source/lilylet/musicXmlUtils.js +0 -469
  29. package/lib/source/lilylet/parser.d.ts +0 -14
  30. package/lib/source/lilylet/parser.js +0 -161
  31. package/lib/source/lilylet/serializer.d.ts +0 -11
  32. package/lib/source/lilylet/serializer.js +0 -791
  33. package/lib/source/lilylet/types.d.ts +0 -253
  34. package/lib/source/lilylet/types.js +0 -100
  35. package/lib/tests/abc-abcjs-parse.d.ts +0 -8
  36. package/lib/tests/abc-abcjs-parse.js +0 -90
  37. package/lib/tests/abc-abcjs-svg.d.ts +0 -1
  38. package/lib/tests/abc-abcjs-svg.js +0 -143
  39. package/lib/tests/abc-decoder.d.ts +0 -1
  40. package/lib/tests/abc-decoder.js +0 -67
  41. package/lib/tests/abc-mei-compare.d.ts +0 -1
  42. package/lib/tests/abc-mei-compare.js +0 -525
  43. package/lib/tests/auto-beam.d.ts +0 -9
  44. package/lib/tests/auto-beam.js +0 -151
  45. package/lib/tests/computeMeiHashes.d.ts +0 -1
  46. package/lib/tests/computeMeiHashes.js +0 -87
  47. package/lib/tests/encoder-mutation.d.ts +0 -9
  48. package/lib/tests/encoder-mutation.js +0 -110
  49. package/lib/tests/gpt-review-issues.d.ts +0 -5
  50. package/lib/tests/gpt-review-issues.js +0 -255
  51. package/lib/tests/json-to-lyl.d.ts +0 -1
  52. package/lib/tests/json-to-lyl.js +0 -18
  53. package/lib/tests/lilypond-roundtrip.d.ts +0 -7
  54. package/lib/tests/lilypond-roundtrip.js +0 -558
  55. package/lib/tests/lilypondDecoder.d.ts +0 -6
  56. package/lib/tests/lilypondDecoder.js +0 -95
  57. package/lib/tests/ly-to-lyl.d.ts +0 -1
  58. package/lib/tests/ly-to-lyl.js +0 -12
  59. package/lib/tests/mei.d.ts +0 -1
  60. package/lib/tests/mei.js +0 -278
  61. package/lib/tests/musicxml-decoder.d.ts +0 -4
  62. package/lib/tests/musicxml-decoder.js +0 -61
  63. package/lib/tests/musicxml-detail.d.ts +0 -4
  64. package/lib/tests/musicxml-detail.js +0 -85
  65. package/lib/tests/musicxml-fprod.d.ts +0 -9
  66. package/lib/tests/musicxml-fprod.js +0 -153
  67. package/lib/tests/musicxml-roundtrip.d.ts +0 -7
  68. package/lib/tests/musicxml-roundtrip.js +0 -296
  69. package/lib/tests/musicxml-to-mei.d.ts +0 -6
  70. package/lib/tests/musicxml-to-mei.js +0 -115
  71. package/lib/tests/parser.d.ts +0 -1
  72. package/lib/tests/parser.js +0 -17
  73. package/lib/tests/render-k283.d.ts +0 -1
  74. package/lib/tests/render-k283.js +0 -33
  75. package/lib/tests/render-lyl.d.ts +0 -1
  76. package/lib/tests/render-lyl.js +0 -35
  77. package/lib/tests/unit/afterGraceInsideTuplet.test.d.ts +0 -23
  78. package/lib/tests/unit/afterGraceInsideTuplet.test.js +0 -186
  79. package/lib/tests/unit/changeStaffBeforeTuplet.test.d.ts +0 -21
  80. package/lib/tests/unit/changeStaffBeforeTuplet.test.js +0 -356
  81. package/lib/tests/unit/crossStaffDecoder.test.d.ts +0 -15
  82. package/lib/tests/unit/crossStaffDecoder.test.js +0 -147
  83. package/lib/tests/unit/crossStaffEdgeCases.test.d.ts +0 -1
  84. package/lib/tests/unit/crossStaffEdgeCases.test.js +0 -209
  85. package/lib/tests/unit/crossStaffMultiMeasure.test.d.ts +0 -15
  86. package/lib/tests/unit/crossStaffMultiMeasure.test.js +0 -231
  87. package/lib/tests/unit/fullMeasureRestDecoder.test.d.ts +0 -11
  88. package/lib/tests/unit/fullMeasureRestDecoder.test.js +0 -154
  89. package/lib/tests/unit/gptReviewIssues.test.d.ts +0 -8
  90. package/lib/tests/unit/gptReviewIssues.test.js +0 -240
  91. package/lib/tests/unit/parallelMusicDecoder.test.d.ts +0 -13
  92. package/lib/tests/unit/parallelMusicDecoder.test.js +0 -261
  93. package/lib/tests/unit/partialWarning.test.d.ts +0 -4
  94. package/lib/tests/unit/partialWarning.test.js +0 -65
  95. package/lib/tests/unit/serializerRoundTrip.test.d.ts +0 -8
  96. package/lib/tests/unit/serializerRoundTrip.test.js +0 -263
  97. package/lib/tests/unit/staffInsideTuplet.test.d.ts +0 -25
  98. package/lib/tests/unit/staffInsideTuplet.test.js +0 -133
  99. package/lib/tests/unit/timesFirstNoteEscape.test.d.ts +0 -16
  100. package/lib/tests/unit/timesFirstNoteEscape.test.js +0 -152
  101. package/lib/tests/unit/tupletWithBaseDuration.test.d.ts +0 -17
  102. package/lib/tests/unit/tupletWithBaseDuration.test.js +0 -139
  103. package/lib/tests/unit/voiceStaffParsing.test.d.ts +0 -13
  104. package/lib/tests/unit/voiceStaffParsing.test.js +0 -118
@@ -1,17 +0,0 @@
1
- /**
2
- * Unit test: \tuplet N/M D { notes } with base-duration argument must
3
- * preserve the tuplet wrapper in the decoded doc.
4
- *
5
- * Bug: The LilyPond decoder accessed args[1].body to find the tuplet body,
6
- * but \tuplet 3/2 4 { ... } has args = ["3/2", "4", {body}].
7
- * args[1] is "4" (the base duration), not the music block, so body was
8
- * empty and all notes were silently decoded as plain notes outside any tuplet.
9
- *
10
- * Real-world case: chopin-28-14.ly — every measure is wrapped in
11
- * \tuplet 3/2 4 { | notes }
12
- * producing 12 eighth notes in the time of 8, but the decoded lyl had
13
- * no \times wrapper at all.
14
- *
15
- * Usage: npx tsx tests/unit/tupletWithBaseDuration.test.ts
16
- */
17
- export {};
@@ -1,139 +0,0 @@
1
- /**
2
- * Unit test: \tuplet N/M D { notes } with base-duration argument must
3
- * preserve the tuplet wrapper in the decoded doc.
4
- *
5
- * Bug: The LilyPond decoder accessed args[1].body to find the tuplet body,
6
- * but \tuplet 3/2 4 { ... } has args = ["3/2", "4", {body}].
7
- * args[1] is "4" (the base duration), not the music block, so body was
8
- * empty and all notes were silently decoded as plain notes outside any tuplet.
9
- *
10
- * Real-world case: chopin-28-14.ly — every measure is wrapped in
11
- * \tuplet 3/2 4 { | notes }
12
- * producing 12 eighth notes in the time of 8, but the decoded lyl had
13
- * no \times wrapper at all.
14
- *
15
- * Usage: npx tsx tests/unit/tupletWithBaseDuration.test.ts
16
- */
17
- import { decode } from "../../source/lilylet/lilypondDecoder.js";
18
- import { serializeLilyletDoc } from "../../source/lilylet/serializer.js";
19
- let passed = 0;
20
- let failed = 0;
21
- function assert(condition, message) {
22
- if (condition) {
23
- console.log(` ✓ ${message}`);
24
- passed++;
25
- }
26
- else {
27
- console.error(` ✗ FAIL: ${message}`);
28
- failed++;
29
- }
30
- }
31
- const LY_BOILERPLATE = `
32
- \\version "2.22.0"
33
- \\language "english"
34
- \\header { tagline = ##f }
35
- \\layout { \\context { \\Score autoBeaming = ##f } }
36
- `;
37
- // Warm-up
38
- {
39
- const w = console.warn, a = console.assert;
40
- console.warn = () => { };
41
- console.assert = () => { };
42
- try {
43
- await decode('{ c }');
44
- }
45
- catch { }
46
- console.warn = w;
47
- console.assert = a;
48
- }
49
- // ─── Test 1: \tuplet 3/2 4 { notes } preserves tuplet ─────────────────────
50
- // Chopin 28-14 pattern: \tuplet 3/2 4 wraps 12 eighth notes per measure.
51
- // Without base-duration: \tuplet 3/2 { ef8 bf gf ... } = 12 notes
52
- // With base-duration: \tuplet 3/2 4 { | ef8 bf gf ... } = same semantics
53
- console.log('\nTest 1: \\tuplet 3/2 4 { notes } — tuplet wrapper preserved');
54
- await (async () => {
55
- const LY = LY_BOILERPLATE + `
56
- \\score {
57
- \\new Staff {
58
- \\new Voice {
59
- \\relative ef, {
60
- \\time 4/4
61
- \\tuplet 3/2 4 {
62
- ef8 [ bf' gf ] cf [ ef, cf' ] d, [ cf' f, ] bf [ d, bf']
63
- }
64
- }
65
- }
66
- }
67
- \\layout {}
68
- }
69
- `;
70
- const doc = await decode(LY);
71
- assert(doc.measures.length >= 1, `decoded 1 measure (got ${doc.measures.length})`);
72
- if (!doc.measures[0])
73
- return;
74
- const voice = doc.measures[0].parts[0]?.voices[0];
75
- assert(!!voice, 'voice exists');
76
- if (!voice)
77
- return;
78
- // The voice should have a tuplet event, NOT 12 bare note events
79
- const tuplets = voice.events.filter(e => e.type === 'tuplet' || e.type === 'times');
80
- const bareNotes = voice.events.filter(e => e.type === 'note');
81
- assert(tuplets.length >= 1, `voice contains at least 1 tuplet/times event (got ${tuplets.length}) — not bare notes`);
82
- assert(bareNotes.length === 0, `no bare notes at top level (got ${bareNotes.length}) — all should be inside tuplet`);
83
- if (tuplets.length >= 1) {
84
- const t = tuplets[0];
85
- const innerNotes = t.events.filter(e => e.type === 'note');
86
- assert(innerNotes.length === 12, `tuplet contains 12 inner notes (got ${innerNotes.length})`);
87
- assert(t.ratio.numerator === 2 && t.ratio.denominator === 3, `ratio is 2/3 (lilylet: play 12 eighth notes in time of 8) — got ${t.ratio.numerator}/${t.ratio.denominator}`);
88
- }
89
- })();
90
- // ─── Test 2: \tuplet 3/2 (no base duration) — should still work ────────────
91
- console.log('\nTest 2: \\tuplet 3/2 { notes } without base duration (baseline)');
92
- await (async () => {
93
- const LY = LY_BOILERPLATE + `
94
- \\score {
95
- \\new Staff {
96
- \\new Voice {
97
- \\relative ef, {
98
- \\time 4/4
99
- \\tuplet 3/2 {
100
- ef8 [ bf' gf ] cf [ ef, cf' ] d, [ cf' f, ] bf [ d, bf']
101
- }
102
- }
103
- }
104
- }
105
- \\layout {}
106
- }
107
- `;
108
- const doc = await decode(LY);
109
- const voice = doc.measures[0]?.parts[0]?.voices[0];
110
- const tuplets = voice?.events.filter(e => e.type === 'tuplet' || e.type === 'times') ?? [];
111
- assert(tuplets.length >= 1, `\\tuplet 3/2 (no base dur) also produces tuplet event (got ${tuplets.length})`);
112
- })();
113
- // ─── Test 3: serialized lyl contains \tuplet or \times wrapper ─────────────
114
- console.log('\nTest 3: serialized lyl contains tuplet notation');
115
- await (async () => {
116
- const LY = LY_BOILERPLATE + `
117
- \\score {
118
- \\new Staff {
119
- \\new Voice {
120
- \\relative ef, {
121
- \\time 4/4
122
- \\tuplet 3/2 4 {
123
- ef8 [ bf' gf ] cf [ ef, cf' ] d, [ cf' f, ] bf [ d, bf']
124
- }
125
- }
126
- }
127
- }
128
- \\layout {}
129
- }
130
- `;
131
- const doc = await decode(LY);
132
- const lyl = serializeLilyletDoc(doc);
133
- console.log(` lyl: ${lyl.trim().split('\n').find(l => l.includes('time') || l.includes('tuplet') || l.includes('times')) ?? '(not found)'}`);
134
- assert(lyl.includes('\\tuplet') || lyl.includes('\\times'), `serialized lyl contains \\tuplet or \\times notation (got no tuplet wrapper)`);
135
- })();
136
- // ─── summary ─────────────────────────────────────────────────────────────────
137
- console.log(`\n${'═'.repeat(50)}`);
138
- console.log(`Total: ${passed + failed} Passed: ${passed} Failed: ${failed}`);
139
- process.exit(failed > 0 ? 1 : 0);
@@ -1,13 +0,0 @@
1
- /**
2
- * Unit tests for voice.staff parsing edge cases from GPT review of the
3
- * leadingStaff fix (commit c0763a4, 47fcf61).
4
- *
5
- * Cases:
6
- * A. non-staff context before \staff "N" in same voice line
7
- * B. second voice after \\ with no explicit \staff (fallback)
8
- * C. trailing | + newline → spurious empty measure filtered
9
- * D. measure with only \bar → filtered as empty
10
- *
11
- * Usage: npx tsx tests/unit/voiceStaffParsing.test.ts
12
- */
13
- export {};
@@ -1,118 +0,0 @@
1
- /**
2
- * Unit tests for voice.staff parsing edge cases from GPT review of the
3
- * leadingStaff fix (commit c0763a4, 47fcf61).
4
- *
5
- * Cases:
6
- * A. non-staff context before \staff "N" in same voice line
7
- * B. second voice after \\ with no explicit \staff (fallback)
8
- * C. trailing | + newline → spurious empty measure filtered
9
- * D. measure with only \bar → filtered as empty
10
- *
11
- * Usage: npx tsx tests/unit/voiceStaffParsing.test.ts
12
- */
13
- import { parseCode } from '../../source/lilylet/parser.js';
14
- let passed = 0;
15
- let failed = 0;
16
- function assert(condition, message) {
17
- if (condition) {
18
- console.log(` ✓ ${message}`);
19
- passed++;
20
- }
21
- else {
22
- console.error(` ✗ FAIL: ${message}`);
23
- failed++;
24
- }
25
- }
26
- function voiceStaff(lyl, measureIdx = 0, voiceIdx = 0) {
27
- const doc = parseCode(lyl);
28
- return doc.measures[measureIdx]?.parts[0]?.voices[voiceIdx]?.staff ?? -1;
29
- }
30
- function measureCount(lyl) {
31
- return parseCode(lyl).measures.length;
32
- }
33
- // ─── Case A1: \clef before \staff "2" ────────────────────────────────────────
34
- console.log('\nCase A1: \\clef before \\staff "2" — staff must be 2');
35
- {
36
- const lyl = `\\clef "bass" \\staff "2" c4 d e f |`;
37
- assert(voiceStaff(lyl) === 2, `\\clef "bass" \\staff "2" c4 → voice.staff=2 (got ${voiceStaff(lyl)})`);
38
- }
39
- // ─── Case A2: \key before \staff "2" ─────────────────────────────────────────
40
- console.log('\nCase A2: \\key before \\staff "2" — staff must be 2');
41
- {
42
- const lyl = `\\key g \\major \\staff "2" c4 d e f |`;
43
- assert(voiceStaff(lyl) === 2, `\\key g \\major \\staff "2" → voice.staff=2 (got ${voiceStaff(lyl)})`);
44
- }
45
- // ─── Case A3: \time \clef \key before \staff "2" \times ──────────────────────
46
- console.log('\nCase A3: multiple context events before \\staff "2" \\times');
47
- {
48
- const lyl = `\\time 4/4 \\clef "treble" \\key c \\major \\staff "2" \\times 2/3 { c8 d e } r2. |`;
49
- const doc = parseCode(lyl);
50
- const voice = doc.measures[0]?.parts[0]?.voices[0];
51
- assert(!!voice, 'voice exists');
52
- if (voice) {
53
- assert(voice.staff === 2, `context chain before \\staff "2" → voice.staff=2 (got ${voice.staff})`);
54
- // Verify the event stream: leading context events (non-staff) then staff=2 context before tuplet
55
- const staffCtxIdx = voice.events.findIndex((e) => e.type === 'context' && e.staff === 2);
56
- const tupletIdx = voice.events.findIndex((e) => e.type === 'tuplet' || e.type === 'times');
57
- assert(staffCtxIdx >= 0, `context{staff:2} event exists in events`);
58
- assert(tupletIdx >= 0, `tuplet event exists in events`);
59
- assert(staffCtxIdx < tupletIdx, `context{staff:2} (idx=${staffCtxIdx}) appears before tuplet (idx=${tupletIdx})`);
60
- }
61
- }
62
- // ─── Case A4: \staff "1" first, cross-staff switch — staff must be 1 ─────────
63
- console.log('\nCase A4: \\staff "1" then \\staff "2" — leading staff is 1');
64
- {
65
- const lyl = `\\staff "1" c4 d \\staff "2" e f |`;
66
- assert(voiceStaff(lyl) === 1, `\\staff "1" c4 d \\staff "2" e f → voice.staff=1 (got ${voiceStaff(lyl)})`);
67
- }
68
- // ─── Case B1: second voice — stale currentStaff=2 must NOT leak ──────────────
69
- // Voice 0 ends after switching to \staff "2" (currentStaff=2 is stale).
70
- // Voice 1 has no explicit \staff — should default to staff=1 (part_start reset),
71
- // NOT inherit the stale currentStaff=2 from voice 0.
72
- console.log('\nCase B1: second voice (no \\staff) must NOT inherit stale currentStaff=2');
73
- {
74
- const lyl = `\\staff "1" c4 \\staff "2" d e f \\\\ g4 a b c |`;
75
- const doc = parseCode(lyl);
76
- const voices = doc.measures[0]?.parts[0]?.voices ?? [];
77
- assert(voices.length === 2, `two voices parsed (got ${voices.length})`);
78
- assert(voices[0].staff === 1, `voice 0 leading \\staff "1" (got ${voices[0]?.staff})`);
79
- // part_start resets currentStaff=1 for each part_voices attempt;
80
- // if stale currentStaff=2 leaked, voice 1 would wrongly get staff=2
81
- assert(voices[1].staff === 1, `voice 1 no \\staff → defaults to 1, not stale 2 (got ${voices[1]?.staff})`);
82
- }
83
- // ─── Case B2: \\ then \staff "2" ─────────────────────────────────────────────
84
- console.log('\nCase B2: second voice starts with \\staff "2"');
85
- {
86
- const lyl = `\\staff "1" c4 d e f \\\\ \\staff "2" c4 d e f |`;
87
- const doc = parseCode(lyl);
88
- const voices = doc.measures[0]?.parts[0]?.voices ?? [];
89
- assert(voices.length === 2, `two voices parsed (got ${voices.length})`);
90
- assert(voices[1].staff === 2, `voice 1 \\staff "2" → staff=2 (got ${voices[1]?.staff})`);
91
- }
92
- // ─── Case C: trailing newline after last | — no spurious measure ─────────────
93
- console.log('\nCase C: trailing newline after last | — no spurious empty measure');
94
- {
95
- // lyl with explicit trailing newline
96
- const lyl = `\\staff "1" c4 d e f |\n`;
97
- const count = measureCount(lyl);
98
- assert(count === 1, `exactly 1 measure (got ${count})`);
99
- }
100
- // ─── Case D: measure with only \bar — filtered as empty ──────────────────────
101
- console.log('\nCase D: measure with only \\bar "|." — filtered as empty');
102
- {
103
- const lyl = `\\staff "1" c4 d e f | \\bar "|." |`;
104
- const count = measureCount(lyl);
105
- // The real measure (c4 d e f) is kept; the barline-only measure is filtered
106
- assert(count === 1, `barline-only measure filtered, 1 real measure remains (got ${count})`);
107
- }
108
- // ─── Case E: measure with notes then trailing empty measure ──────────────────
109
- console.log('\nCase E: two real measures, no spurious third');
110
- {
111
- const lyl = `\\staff "1" c4 d e f | g4 a b c |\n`;
112
- const count = measureCount(lyl);
113
- assert(count === 2, `exactly 2 measures (got ${count})`);
114
- }
115
- // ─── Summary ─────────────────────────────────────────────────────────────────
116
- console.log(`\n${'═'.repeat(50)}`);
117
- console.log(`Total: ${passed + failed} Passed: ${passed} Failed: ${failed}`);
118
- process.exit(failed > 0 ? 1 : 0);