@k-l-lambda/lilylet 0.1.70 → 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 (116) hide show
  1. package/lib/gmInstruments.d.ts +1 -0
  2. package/lib/gmInstruments.js +1 -0
  3. package/lib/highlight.d.ts +1 -0
  4. package/lib/highlight.js +1 -0
  5. package/lib/lilylet/abcDecoder.js +16 -7
  6. package/lib/lilylet/gmInstruments.d.ts +1 -0
  7. package/lib/lilylet/gmInstruments.js +295 -0
  8. package/lib/lilylet/highlight.d.ts +29 -0
  9. package/lib/lilylet/highlight.js +145 -0
  10. package/lib/lilylet/meiEncoder.js +126 -14
  11. package/lib/lilylet/staffLayout.d.ts +5 -0
  12. package/lib/lilylet/staffLayout.js +62 -0
  13. package/package.json +8 -2
  14. package/source/lilylet/abcDecoder.ts +14 -7
  15. package/source/lilylet/gmInstruments.ts +305 -0
  16. package/source/lilylet/highlight.ts +192 -0
  17. package/source/lilylet/meiEncoder.ts +135 -11
  18. package/source/lilylet/staffLayout.ts +76 -0
  19. package/lib/source/abc/abc.d.ts +0 -102
  20. package/lib/source/abc/abc.js +0 -25
  21. package/lib/source/abc/parser.d.ts +0 -3
  22. package/lib/source/abc/parser.js +0 -6
  23. package/lib/source/lilylet/abcDecoder.d.ts +0 -25
  24. package/lib/source/lilylet/abcDecoder.js +0 -1035
  25. package/lib/source/lilylet/index.d.ts +0 -10
  26. package/lib/source/lilylet/index.js +0 -10
  27. package/lib/source/lilylet/lilypondDecoder.d.ts +0 -29
  28. package/lib/source/lilylet/lilypondDecoder.js +0 -1223
  29. package/lib/source/lilylet/lilypondEncoder.d.ts +0 -34
  30. package/lib/source/lilylet/lilypondEncoder.js +0 -893
  31. package/lib/source/lilylet/meiEncoder.d.ts +0 -8
  32. package/lib/source/lilylet/meiEncoder.js +0 -1985
  33. package/lib/source/lilylet/musicXmlDecoder.d.ts +0 -20
  34. package/lib/source/lilylet/musicXmlDecoder.js +0 -1195
  35. package/lib/source/lilylet/musicXmlEncoder.d.ts +0 -15
  36. package/lib/source/lilylet/musicXmlEncoder.js +0 -701
  37. package/lib/source/lilylet/musicXmlTypes.d.ts +0 -199
  38. package/lib/source/lilylet/musicXmlTypes.js +0 -7
  39. package/lib/source/lilylet/musicXmlUtils.d.ts +0 -92
  40. package/lib/source/lilylet/musicXmlUtils.js +0 -469
  41. package/lib/source/lilylet/parser.d.ts +0 -14
  42. package/lib/source/lilylet/parser.js +0 -161
  43. package/lib/source/lilylet/serializer.d.ts +0 -11
  44. package/lib/source/lilylet/serializer.js +0 -791
  45. package/lib/source/lilylet/types.d.ts +0 -253
  46. package/lib/source/lilylet/types.js +0 -100
  47. package/lib/tests/abc-abcjs-parse.d.ts +0 -8
  48. package/lib/tests/abc-abcjs-parse.js +0 -90
  49. package/lib/tests/abc-abcjs-svg.d.ts +0 -1
  50. package/lib/tests/abc-abcjs-svg.js +0 -143
  51. package/lib/tests/abc-decoder.d.ts +0 -1
  52. package/lib/tests/abc-decoder.js +0 -67
  53. package/lib/tests/abc-mei-compare.d.ts +0 -1
  54. package/lib/tests/abc-mei-compare.js +0 -525
  55. package/lib/tests/auto-beam.d.ts +0 -9
  56. package/lib/tests/auto-beam.js +0 -151
  57. package/lib/tests/computeMeiHashes.d.ts +0 -1
  58. package/lib/tests/computeMeiHashes.js +0 -87
  59. package/lib/tests/encoder-mutation.d.ts +0 -9
  60. package/lib/tests/encoder-mutation.js +0 -110
  61. package/lib/tests/gpt-review-issues.d.ts +0 -5
  62. package/lib/tests/gpt-review-issues.js +0 -255
  63. package/lib/tests/json-to-lyl.d.ts +0 -1
  64. package/lib/tests/json-to-lyl.js +0 -18
  65. package/lib/tests/lilypond-roundtrip.d.ts +0 -7
  66. package/lib/tests/lilypond-roundtrip.js +0 -558
  67. package/lib/tests/lilypondDecoder.d.ts +0 -6
  68. package/lib/tests/lilypondDecoder.js +0 -95
  69. package/lib/tests/ly-to-lyl.d.ts +0 -1
  70. package/lib/tests/ly-to-lyl.js +0 -12
  71. package/lib/tests/mei.d.ts +0 -1
  72. package/lib/tests/mei.js +0 -278
  73. package/lib/tests/musicxml-decoder.d.ts +0 -4
  74. package/lib/tests/musicxml-decoder.js +0 -61
  75. package/lib/tests/musicxml-detail.d.ts +0 -4
  76. package/lib/tests/musicxml-detail.js +0 -85
  77. package/lib/tests/musicxml-fprod.d.ts +0 -9
  78. package/lib/tests/musicxml-fprod.js +0 -153
  79. package/lib/tests/musicxml-roundtrip.d.ts +0 -7
  80. package/lib/tests/musicxml-roundtrip.js +0 -296
  81. package/lib/tests/musicxml-to-mei.d.ts +0 -6
  82. package/lib/tests/musicxml-to-mei.js +0 -115
  83. package/lib/tests/parser.d.ts +0 -1
  84. package/lib/tests/parser.js +0 -17
  85. package/lib/tests/render-k283.d.ts +0 -1
  86. package/lib/tests/render-k283.js +0 -33
  87. package/lib/tests/render-lyl.d.ts +0 -1
  88. package/lib/tests/render-lyl.js +0 -35
  89. package/lib/tests/unit/afterGraceInsideTuplet.test.d.ts +0 -23
  90. package/lib/tests/unit/afterGraceInsideTuplet.test.js +0 -186
  91. package/lib/tests/unit/changeStaffBeforeTuplet.test.d.ts +0 -21
  92. package/lib/tests/unit/changeStaffBeforeTuplet.test.js +0 -356
  93. package/lib/tests/unit/crossStaffDecoder.test.d.ts +0 -15
  94. package/lib/tests/unit/crossStaffDecoder.test.js +0 -147
  95. package/lib/tests/unit/crossStaffEdgeCases.test.d.ts +0 -1
  96. package/lib/tests/unit/crossStaffEdgeCases.test.js +0 -209
  97. package/lib/tests/unit/crossStaffMultiMeasure.test.d.ts +0 -15
  98. package/lib/tests/unit/crossStaffMultiMeasure.test.js +0 -231
  99. package/lib/tests/unit/fullMeasureRestDecoder.test.d.ts +0 -11
  100. package/lib/tests/unit/fullMeasureRestDecoder.test.js +0 -154
  101. package/lib/tests/unit/gptReviewIssues.test.d.ts +0 -8
  102. package/lib/tests/unit/gptReviewIssues.test.js +0 -240
  103. package/lib/tests/unit/parallelMusicDecoder.test.d.ts +0 -13
  104. package/lib/tests/unit/parallelMusicDecoder.test.js +0 -261
  105. package/lib/tests/unit/partialWarning.test.d.ts +0 -4
  106. package/lib/tests/unit/partialWarning.test.js +0 -65
  107. package/lib/tests/unit/serializerRoundTrip.test.d.ts +0 -8
  108. package/lib/tests/unit/serializerRoundTrip.test.js +0 -263
  109. package/lib/tests/unit/staffInsideTuplet.test.d.ts +0 -25
  110. package/lib/tests/unit/staffInsideTuplet.test.js +0 -133
  111. package/lib/tests/unit/timesFirstNoteEscape.test.d.ts +0 -16
  112. package/lib/tests/unit/timesFirstNoteEscape.test.js +0 -152
  113. package/lib/tests/unit/tupletWithBaseDuration.test.d.ts +0 -17
  114. package/lib/tests/unit/tupletWithBaseDuration.test.js +0 -139
  115. package/lib/tests/unit/voiceStaffParsing.test.d.ts +0 -13
  116. 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);