@k-l-lambda/lilylet 0.1.71 → 0.1.73
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/highlight.d.ts +1 -0
- package/lib/highlight.js +1 -0
- package/lib/lilylet/highlight.d.ts +29 -0
- package/lib/lilylet/highlight.js +145 -0
- package/package.json +8 -2
- package/source/lilylet/highlight.ts +192 -0
- package/lib/source/abc/abc.d.ts +0 -102
- package/lib/source/abc/abc.js +0 -25
- package/lib/source/abc/parser.d.ts +0 -3
- package/lib/source/abc/parser.js +0 -6
- package/lib/source/lilylet/abcDecoder.d.ts +0 -25
- package/lib/source/lilylet/abcDecoder.js +0 -1035
- package/lib/source/lilylet/index.d.ts +0 -10
- package/lib/source/lilylet/index.js +0 -10
- package/lib/source/lilylet/lilypondDecoder.d.ts +0 -29
- package/lib/source/lilylet/lilypondDecoder.js +0 -1223
- package/lib/source/lilylet/lilypondEncoder.d.ts +0 -34
- package/lib/source/lilylet/lilypondEncoder.js +0 -893
- package/lib/source/lilylet/meiEncoder.d.ts +0 -8
- package/lib/source/lilylet/meiEncoder.js +0 -1985
- package/lib/source/lilylet/musicXmlDecoder.d.ts +0 -20
- package/lib/source/lilylet/musicXmlDecoder.js +0 -1195
- package/lib/source/lilylet/musicXmlEncoder.d.ts +0 -15
- package/lib/source/lilylet/musicXmlEncoder.js +0 -701
- package/lib/source/lilylet/musicXmlTypes.d.ts +0 -199
- package/lib/source/lilylet/musicXmlTypes.js +0 -7
- package/lib/source/lilylet/musicXmlUtils.d.ts +0 -92
- package/lib/source/lilylet/musicXmlUtils.js +0 -469
- package/lib/source/lilylet/parser.d.ts +0 -14
- package/lib/source/lilylet/parser.js +0 -161
- package/lib/source/lilylet/serializer.d.ts +0 -11
- package/lib/source/lilylet/serializer.js +0 -791
- package/lib/source/lilylet/types.d.ts +0 -253
- package/lib/source/lilylet/types.js +0 -100
- package/lib/tests/abc-abcjs-parse.d.ts +0 -8
- package/lib/tests/abc-abcjs-parse.js +0 -90
- package/lib/tests/abc-abcjs-svg.d.ts +0 -1
- package/lib/tests/abc-abcjs-svg.js +0 -143
- package/lib/tests/abc-decoder.d.ts +0 -1
- package/lib/tests/abc-decoder.js +0 -67
- package/lib/tests/abc-mei-compare.d.ts +0 -1
- package/lib/tests/abc-mei-compare.js +0 -525
- package/lib/tests/auto-beam.d.ts +0 -9
- package/lib/tests/auto-beam.js +0 -151
- package/lib/tests/computeMeiHashes.d.ts +0 -1
- package/lib/tests/computeMeiHashes.js +0 -87
- package/lib/tests/encoder-mutation.d.ts +0 -9
- package/lib/tests/encoder-mutation.js +0 -110
- package/lib/tests/gpt-review-issues.d.ts +0 -5
- package/lib/tests/gpt-review-issues.js +0 -255
- package/lib/tests/json-to-lyl.d.ts +0 -1
- package/lib/tests/json-to-lyl.js +0 -18
- package/lib/tests/lilypond-roundtrip.d.ts +0 -7
- package/lib/tests/lilypond-roundtrip.js +0 -558
- package/lib/tests/lilypondDecoder.d.ts +0 -6
- package/lib/tests/lilypondDecoder.js +0 -95
- package/lib/tests/ly-to-lyl.d.ts +0 -1
- package/lib/tests/ly-to-lyl.js +0 -12
- package/lib/tests/mei.d.ts +0 -1
- package/lib/tests/mei.js +0 -278
- package/lib/tests/musicxml-decoder.d.ts +0 -4
- package/lib/tests/musicxml-decoder.js +0 -61
- package/lib/tests/musicxml-detail.d.ts +0 -4
- package/lib/tests/musicxml-detail.js +0 -85
- package/lib/tests/musicxml-fprod.d.ts +0 -9
- package/lib/tests/musicxml-fprod.js +0 -153
- package/lib/tests/musicxml-roundtrip.d.ts +0 -7
- package/lib/tests/musicxml-roundtrip.js +0 -296
- package/lib/tests/musicxml-to-mei.d.ts +0 -6
- package/lib/tests/musicxml-to-mei.js +0 -115
- package/lib/tests/parser.d.ts +0 -1
- package/lib/tests/parser.js +0 -17
- package/lib/tests/render-k283.d.ts +0 -1
- package/lib/tests/render-k283.js +0 -33
- package/lib/tests/render-lyl.d.ts +0 -1
- package/lib/tests/render-lyl.js +0 -35
- package/lib/tests/unit/afterGraceInsideTuplet.test.d.ts +0 -23
- package/lib/tests/unit/afterGraceInsideTuplet.test.js +0 -186
- package/lib/tests/unit/changeStaffBeforeTuplet.test.d.ts +0 -21
- package/lib/tests/unit/changeStaffBeforeTuplet.test.js +0 -356
- package/lib/tests/unit/crossStaffDecoder.test.d.ts +0 -15
- package/lib/tests/unit/crossStaffDecoder.test.js +0 -147
- package/lib/tests/unit/crossStaffEdgeCases.test.d.ts +0 -1
- package/lib/tests/unit/crossStaffEdgeCases.test.js +0 -209
- package/lib/tests/unit/crossStaffMultiMeasure.test.d.ts +0 -15
- package/lib/tests/unit/crossStaffMultiMeasure.test.js +0 -231
- package/lib/tests/unit/fullMeasureRestDecoder.test.d.ts +0 -11
- package/lib/tests/unit/fullMeasureRestDecoder.test.js +0 -154
- package/lib/tests/unit/gptReviewIssues.test.d.ts +0 -8
- package/lib/tests/unit/gptReviewIssues.test.js +0 -240
- package/lib/tests/unit/parallelMusicDecoder.test.d.ts +0 -13
- package/lib/tests/unit/parallelMusicDecoder.test.js +0 -261
- package/lib/tests/unit/partialWarning.test.d.ts +0 -4
- package/lib/tests/unit/partialWarning.test.js +0 -65
- package/lib/tests/unit/serializerRoundTrip.test.d.ts +0 -8
- package/lib/tests/unit/serializerRoundTrip.test.js +0 -263
- package/lib/tests/unit/staffInsideTuplet.test.d.ts +0 -25
- package/lib/tests/unit/staffInsideTuplet.test.js +0 -133
- package/lib/tests/unit/timesFirstNoteEscape.test.d.ts +0 -16
- package/lib/tests/unit/timesFirstNoteEscape.test.js +0 -152
- package/lib/tests/unit/tupletWithBaseDuration.test.d.ts +0 -17
- package/lib/tests/unit/tupletWithBaseDuration.test.js +0 -139
- package/lib/tests/unit/voiceStaffParsing.test.d.ts +0 -13
- 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);
|