@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.
- package/lib/gmInstruments.d.ts +1 -0
- package/lib/gmInstruments.js +1 -0
- package/lib/highlight.d.ts +1 -0
- package/lib/highlight.js +1 -0
- package/lib/lilylet/abcDecoder.js +16 -7
- package/lib/lilylet/gmInstruments.d.ts +1 -0
- package/lib/lilylet/gmInstruments.js +295 -0
- package/lib/lilylet/highlight.d.ts +29 -0
- package/lib/lilylet/highlight.js +145 -0
- package/lib/lilylet/meiEncoder.js +126 -14
- package/lib/lilylet/staffLayout.d.ts +5 -0
- package/lib/lilylet/staffLayout.js +62 -0
- package/package.json +8 -2
- package/source/lilylet/abcDecoder.ts +14 -7
- package/source/lilylet/gmInstruments.ts +305 -0
- package/source/lilylet/highlight.ts +192 -0
- package/source/lilylet/meiEncoder.ts +135 -11
- package/source/lilylet/staffLayout.ts +76 -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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./lilylet/gmInstruments.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./lilylet/gmInstruments.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./lilylet/highlight.js";
|
package/lib/highlight.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./lilylet/highlight.js";
|
|
@@ -498,25 +498,34 @@ const abcLayoutToStaves = (layout) => {
|
|
|
498
498
|
// arc or unbounded: a staff only if every descendant is too (no nested groups)
|
|
499
499
|
return (node.items || []).every(isStaffLeaf);
|
|
500
500
|
};
|
|
501
|
-
|
|
501
|
+
// A square group maps to lilylet Bracket `<>` at the TOP level, but to lilylet Square
|
|
502
|
+
// `[]` when nested inside another group — e.g. ABC `[[1 2] 3 | 4]` → `<[1,2]3-4>`.
|
|
503
|
+
// A curly group always maps to Brace `{}`. `nested` is false for a top-level entry.
|
|
504
|
+
const emit = (node, nested) => {
|
|
502
505
|
if (isStaffLeaf(node))
|
|
503
506
|
return firstVoice(node) || "";
|
|
504
507
|
const group = node;
|
|
505
|
-
const open = group.bound === "curly" ? "{" :
|
|
506
|
-
const close = group.bound === "curly" ? "}" : ">";
|
|
508
|
+
const open = group.bound === "curly" ? "{" : (group.bound === "square" && nested) ? "[" : "<";
|
|
509
|
+
const close = group.bound === "curly" ? "}" : (group.bound === "square" && nested) ? "]" : ">";
|
|
507
510
|
const items = group.items || [];
|
|
508
511
|
let inner = "";
|
|
509
512
|
items.forEach((item, i) => {
|
|
510
|
-
inner += emit(item);
|
|
513
|
+
inner += emit(item, true);
|
|
511
514
|
if (i < items.length - 1) {
|
|
512
|
-
|
|
513
|
-
|
|
515
|
+
// A Blank separator (',') is only needed between two bare staff leaves; a
|
|
516
|
+
// grouped neighbour's bracket already delimits the slot, so suppress it there
|
|
517
|
+
// (giving `[1,2]3` not `[1,2],3`). A Solid join ('-', barThru) is always kept.
|
|
518
|
+
const next = items[i + 1];
|
|
519
|
+
if (item.barThruAfter)
|
|
520
|
+
inner += "-";
|
|
521
|
+
else if (isStaffLeaf(item) && isStaffLeaf(next))
|
|
522
|
+
inner += ",";
|
|
514
523
|
}
|
|
515
524
|
});
|
|
516
525
|
return `${open}${inner}${close}`;
|
|
517
526
|
};
|
|
518
527
|
const tops = layout.map((top, i) => {
|
|
519
|
-
let s = emit(top);
|
|
528
|
+
let s = emit(top, false);
|
|
520
529
|
// A bare top-level staff leaf (e.g. the `9` in `[ … ] 9 [ … ]`) still occupies a slot;
|
|
521
530
|
// emit() already yields its id with no wrapper, which is the desired output.
|
|
522
531
|
return { s, barThru: !!top.barThruAfter, isLast: i === layout.length - 1 };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const gmProgramOf: (name: string | undefined | null) => number | undefined;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// General MIDI program lookup: instrument name → GM program number (0–127).
|
|
2
|
+
//
|
|
3
|
+
// Verovio's MIDI export honors ONLY the numeric `@midi.instrnum` on an MEI
|
|
4
|
+
// <instrDef> (the GM-name attribute `@midi.instrname` is parsed but never used
|
|
5
|
+
// for MIDI). lilylet already carries human instrument names (from ABC voice
|
|
6
|
+
// names, MusicXML part names, etc.) in metadata.instruments; this table maps
|
|
7
|
+
// those names to GM programs so the MEI encoder can emit <instrDef midi.instrnum>
|
|
8
|
+
// and multi-instrument scores get distinct timbres instead of all-piano.
|
|
9
|
+
//
|
|
10
|
+
// The name set is seeded from the notagen dataset (Piano, Violins, Viola,
|
|
11
|
+
// Violoncellos, Oboe, Horn, Flute, Clarinet, Bassoon, Violin, Trombone,
|
|
12
|
+
// Timpani, Voice, Bass, Trumpet, Harp, Contrabasses, Vocal, Organ, …) plus
|
|
13
|
+
// common GM aliases, then matched through a normalizer that handles plurals
|
|
14
|
+
// ("Violins" → violin) and trailing part numbers ("Violin I", "Horn 2").
|
|
15
|
+
// Normalized name → GM program (0-based). Keys are lowercase, singular,
|
|
16
|
+
// whitespace-collapsed. Plural/number variants are resolved by the normalizer.
|
|
17
|
+
const GM_PROGRAMS = {
|
|
18
|
+
// Piano (0–7)
|
|
19
|
+
"piano": 0,
|
|
20
|
+
"acoustic grand piano": 0,
|
|
21
|
+
"grand piano": 0,
|
|
22
|
+
"bright acoustic piano": 1,
|
|
23
|
+
"electric piano": 4,
|
|
24
|
+
"harpsichord": 6,
|
|
25
|
+
"clavichord": 7,
|
|
26
|
+
"clavi": 7,
|
|
27
|
+
// Chromatic percussion (8–15)
|
|
28
|
+
"celesta": 8,
|
|
29
|
+
"glockenspiel": 9,
|
|
30
|
+
"music box": 10,
|
|
31
|
+
"vibraphone": 11,
|
|
32
|
+
"marimba": 12,
|
|
33
|
+
"xylophone": 13,
|
|
34
|
+
"tubular bells": 14,
|
|
35
|
+
"dulcimer": 15,
|
|
36
|
+
// Organ (16–23)
|
|
37
|
+
"organ": 19,
|
|
38
|
+
"hammond organ": 16,
|
|
39
|
+
"percussive organ": 17,
|
|
40
|
+
"rock organ": 18,
|
|
41
|
+
"church organ": 19,
|
|
42
|
+
"pipe organ": 19,
|
|
43
|
+
"reed organ": 20,
|
|
44
|
+
"accordion": 21,
|
|
45
|
+
"harmonica": 22,
|
|
46
|
+
// Guitar (24–31)
|
|
47
|
+
"guitar": 24,
|
|
48
|
+
"acoustic guitar": 24,
|
|
49
|
+
"nylon guitar": 24,
|
|
50
|
+
"steel guitar": 25,
|
|
51
|
+
"electric guitar": 27,
|
|
52
|
+
"guitarre": 24, // fr./de. guitar
|
|
53
|
+
"gitarre": 24, // de.
|
|
54
|
+
"chitarra": 24, // it.
|
|
55
|
+
// Bass (32–39) — orchestral "Bass" means double bass (Contrabass, 43); the
|
|
56
|
+
// electric/acoustic bass-guitar programs live here but are not the default.
|
|
57
|
+
"acoustic bass": 32,
|
|
58
|
+
"electric bass": 33,
|
|
59
|
+
"fretless bass": 35,
|
|
60
|
+
"basso": 43, // it. bass → double bass
|
|
61
|
+
"basse": 43, // fr.
|
|
62
|
+
"bassi": 43, // it. pl.
|
|
63
|
+
"bas": 43, // de./nl. abbrev
|
|
64
|
+
// Strings (40–47)
|
|
65
|
+
"violin": 40,
|
|
66
|
+
"viola": 41,
|
|
67
|
+
"cello": 42,
|
|
68
|
+
"violoncello": 42,
|
|
69
|
+
"contrabass": 43,
|
|
70
|
+
"double bass": 43,
|
|
71
|
+
"bass": 43,
|
|
72
|
+
"tremolo strings": 44,
|
|
73
|
+
"pizzicato strings": 45,
|
|
74
|
+
"harp": 46,
|
|
75
|
+
"orchestral harp": 46,
|
|
76
|
+
"timpani": 47,
|
|
77
|
+
// Ensemble (48–55)
|
|
78
|
+
"strings": 48,
|
|
79
|
+
"string ensemble": 48,
|
|
80
|
+
"string orchestra": 48,
|
|
81
|
+
"synth strings": 50,
|
|
82
|
+
"voice": 52,
|
|
83
|
+
"vocal": 52,
|
|
84
|
+
"voices": 52,
|
|
85
|
+
"choir": 52,
|
|
86
|
+
"choir aahs": 52,
|
|
87
|
+
"soprano": 52,
|
|
88
|
+
"alto": 52,
|
|
89
|
+
"tenor": 52,
|
|
90
|
+
"bass voice": 52,
|
|
91
|
+
"orchestra hit": 55,
|
|
92
|
+
// Brass (56–63)
|
|
93
|
+
"trumpet": 56,
|
|
94
|
+
"trombone": 57,
|
|
95
|
+
"tuba": 58,
|
|
96
|
+
"muted trumpet": 59,
|
|
97
|
+
"horn": 60,
|
|
98
|
+
"french horn": 60,
|
|
99
|
+
"brass": 61,
|
|
100
|
+
"brass section": 61,
|
|
101
|
+
// Reed (64–71)
|
|
102
|
+
"soprano sax": 64,
|
|
103
|
+
"alto sax": 65,
|
|
104
|
+
"tenor sax": 66,
|
|
105
|
+
"baritone sax": 67,
|
|
106
|
+
"saxophone": 66,
|
|
107
|
+
"sax": 66,
|
|
108
|
+
"oboe": 68,
|
|
109
|
+
"english horn": 69,
|
|
110
|
+
"cor anglais": 69,
|
|
111
|
+
"bassoon": 70,
|
|
112
|
+
"clarinet": 71,
|
|
113
|
+
// Pipe (72–79)
|
|
114
|
+
"piccolo": 72,
|
|
115
|
+
"flute": 73,
|
|
116
|
+
"recorder": 74,
|
|
117
|
+
"pan flute": 75,
|
|
118
|
+
// --- Foreign-language names, abbreviations and common spelling variants,
|
|
119
|
+
// harvested from the notagen corpus. Mapped to the nearest GM program.
|
|
120
|
+
// Keyboard
|
|
121
|
+
"pianoforte": 0,
|
|
122
|
+
"fortepiano": 0,
|
|
123
|
+
"klavier": 0,
|
|
124
|
+
"keyboard": 0,
|
|
125
|
+
"cembalo": 6, // it. harpsichord
|
|
126
|
+
"clavicembalo": 6,
|
|
127
|
+
"harpichord": 6, // misspelling
|
|
128
|
+
"organo": 19, // it. organ
|
|
129
|
+
"orgel": 19, // de. organ
|
|
130
|
+
// Strings (it./de./fr./variants)
|
|
131
|
+
"violino": 40,
|
|
132
|
+
"violini": 40,
|
|
133
|
+
"violine": 40, // de.
|
|
134
|
+
"violinen": 40,
|
|
135
|
+
"violon": 40, // fr.
|
|
136
|
+
"violons": 40,
|
|
137
|
+
"violn": 40, // abbrev/OCR variant
|
|
138
|
+
"violno": 40, // OCR variant
|
|
139
|
+
"viole": 41, // it. violas (also fr. "viole")
|
|
140
|
+
"bratsche": 41, // de. viola
|
|
141
|
+
"celli": 42,
|
|
142
|
+
"violoncelli": 42,
|
|
143
|
+
"violoncelle": 42, // fr.
|
|
144
|
+
"violoncelles": 42,
|
|
145
|
+
"violonchelo": 42, // es.
|
|
146
|
+
"soloncello": 42, // OCR variant of violoncello
|
|
147
|
+
"gambe": 42, // fr. viola da gamba
|
|
148
|
+
"gamba": 42, // viola da gamba ≈ cello
|
|
149
|
+
"viola da gamba": 42,
|
|
150
|
+
"contrabasso": 43, // it.
|
|
151
|
+
"contrabassi": 43,
|
|
152
|
+
"contrabbasso": 43, // it.
|
|
153
|
+
"contra-basso": 43,
|
|
154
|
+
"contrabajo": 43, // es.
|
|
155
|
+
"kontrabass": 43, // de.
|
|
156
|
+
"kontrabasse": 43, // de. pl.
|
|
157
|
+
"kontrabasso": 43,
|
|
158
|
+
"contrebasse": 43, // fr.
|
|
159
|
+
"violone": 43, // large bass viol ≈ contrabass
|
|
160
|
+
"arpa": 46, // it./es. harp
|
|
161
|
+
"harfe": 46, // de. harp
|
|
162
|
+
"pauken": 47, // de. timpani
|
|
163
|
+
// Voice (it./de./fr.)
|
|
164
|
+
"canto": 52, // it.
|
|
165
|
+
"coro": 52, // it. choir
|
|
166
|
+
"chorus": 52,
|
|
167
|
+
"chorale": 52,
|
|
168
|
+
"sopran": 52, // de.
|
|
169
|
+
"contralto": 52, // it. alto
|
|
170
|
+
"tenore": 52, // it.
|
|
171
|
+
"tenori": 52,
|
|
172
|
+
"gesang": 52, // de. voice
|
|
173
|
+
"singstimme": 52, // de. voice
|
|
174
|
+
"voce": 52, // it.
|
|
175
|
+
"voix": 52, // fr.
|
|
176
|
+
"chanto": 52, // OCR variant of canto
|
|
177
|
+
"women": 52, // women's voices
|
|
178
|
+
"contra-fagotto": 70, // hyphenated contrabassoon ≈ bassoon
|
|
179
|
+
// Brass
|
|
180
|
+
"tromboni": 57, // it. trombones
|
|
181
|
+
"posaune": 57, // de. trombone
|
|
182
|
+
"posaunen": 57,
|
|
183
|
+
"trombe": 56, // it. trumpets
|
|
184
|
+
"tromba": 56, // it. trumpet
|
|
185
|
+
"trompete": 56, // de. trumpet
|
|
186
|
+
"trompeten": 56,
|
|
187
|
+
"trompette": 56, // fr. trumpet
|
|
188
|
+
"cornetto": 56, // historical cornett ≈ trumpet
|
|
189
|
+
"cornettino": 56,
|
|
190
|
+
"corno": 60, // it. horn
|
|
191
|
+
"corni": 60, // it. horns
|
|
192
|
+
// Reed (it./de./fr.)
|
|
193
|
+
"oboi": 68, // it. oboes
|
|
194
|
+
"oboen": 68, // de.
|
|
195
|
+
"hautbois": 68, // fr. oboe
|
|
196
|
+
"corno inglese": 69, // it. english horn
|
|
197
|
+
"inglese": 69, // "corno inglese" trailing word fallback also covers it
|
|
198
|
+
"ingles": 69, // es. variant
|
|
199
|
+
"fagotto": 70, // it. bassoon
|
|
200
|
+
"fagotti": 70,
|
|
201
|
+
"fagott": 70, // de.
|
|
202
|
+
"fagotte": 70, // de. pl.
|
|
203
|
+
"fagot": 70, // es.
|
|
204
|
+
"basson": 70, // fr. bassoon
|
|
205
|
+
"bassons": 70,
|
|
206
|
+
"contrafagotto": 70, // it. contrabassoon ≈ bassoon timbre
|
|
207
|
+
"contrabassoon": 70,
|
|
208
|
+
"klarinette": 71, // de. clarinet
|
|
209
|
+
"clarinetto": 71, // it.
|
|
210
|
+
"clarinetti": 71,
|
|
211
|
+
"clarinette": 71, // fr.
|
|
212
|
+
// Pipe (it./de.)
|
|
213
|
+
"flauto": 73, // it. flute
|
|
214
|
+
"flauti": 73, // it. flutes
|
|
215
|
+
"flote": 73, // de. Flöte (diacritics stripped by the normalizer)
|
|
216
|
+
"floten": 73, // de. Flöten
|
|
217
|
+
"traverso": 73, // baroque transverse flute
|
|
218
|
+
"flauto traverso": 73,
|
|
219
|
+
};
|
|
220
|
+
// Normalize an instrument name for lookup: lowercase, turn literal "\n" escapes
|
|
221
|
+
// and real newlines into spaces, strip diacritics (Flöte→flote, Hautböis→...),
|
|
222
|
+
// drop a trailing part designator (roman numeral or arabic number — "Violin I",
|
|
223
|
+
// "Horn 2", "Oboe II"), collapse whitespace.
|
|
224
|
+
const normalizeInstrumentName = (raw) => {
|
|
225
|
+
let s = raw.toLowerCase().trim();
|
|
226
|
+
s = s.replace(/\\n/g, " "); // literal backslash-n escape → space
|
|
227
|
+
s = s.normalize("NFD").replace(/[̀-ͯ]/g, ""); // strip diacritics
|
|
228
|
+
s = s.replace(/\s+/g, " ").trim();
|
|
229
|
+
s = s.replace(/\s+(?:[ivx]+|\d+)\.?$/i, "").trim();
|
|
230
|
+
return s;
|
|
231
|
+
};
|
|
232
|
+
// Choral single-letter voice-part abbreviations → "Voice" (GM 52). Matched ONLY
|
|
233
|
+
// against the whole name, never per-word: a bare "S"/"A"/"T"/"B" staff label in a
|
|
234
|
+
// chorale means Soprano/Alto/Tenor/Bass, but the same letters appear as key
|
|
235
|
+
// designators in "Clarinet in B", "Horn in F", "Trumpet in C" — so these must not
|
|
236
|
+
// enter GM_PROGRAMS where the word-scan would misread them.
|
|
237
|
+
const SATB_VOICE = {
|
|
238
|
+
"s": 52,
|
|
239
|
+
"a": 52,
|
|
240
|
+
"t": 52,
|
|
241
|
+
"b": 52,
|
|
242
|
+
};
|
|
243
|
+
// Look up a single normalized name: exact match, else de-pluralized
|
|
244
|
+
// ("violins"→violin, "violoncellos"→violoncello, "contrabasses"→contrabass),
|
|
245
|
+
// else with a trailing attached part-number stripped ("violin1"→violin,
|
|
246
|
+
// "violino2"→violino).
|
|
247
|
+
const lookupNormalized = (norm) => {
|
|
248
|
+
if (norm in GM_PROGRAMS)
|
|
249
|
+
return GM_PROGRAMS[norm];
|
|
250
|
+
// Try "-es" before "-s".
|
|
251
|
+
if (norm.endsWith("es")) {
|
|
252
|
+
const sing = norm.slice(0, -2);
|
|
253
|
+
if (sing in GM_PROGRAMS)
|
|
254
|
+
return GM_PROGRAMS[sing];
|
|
255
|
+
}
|
|
256
|
+
if (norm.endsWith("s")) {
|
|
257
|
+
const sing = norm.slice(0, -1);
|
|
258
|
+
if (sing in GM_PROGRAMS)
|
|
259
|
+
return GM_PROGRAMS[sing];
|
|
260
|
+
}
|
|
261
|
+
// Attached trailing digits ("violin1", "violino2"): strip and retry.
|
|
262
|
+
const deNum = norm.replace(/\d+$/, "");
|
|
263
|
+
if (deNum !== norm && deNum in GM_PROGRAMS)
|
|
264
|
+
return GM_PROGRAMS[deNum];
|
|
265
|
+
return undefined;
|
|
266
|
+
};
|
|
267
|
+
// Resolve an instrument name to a GM program number (0–127), or undefined if no
|
|
268
|
+
// confident match (caller then omits <instrDef>, leaving Verovio's default).
|
|
269
|
+
//
|
|
270
|
+
// Match priority: the full normalized string first (including the SATB
|
|
271
|
+
// single-letter voice abbreviations), then individual words from the last toward
|
|
272
|
+
// the first. Multi-word names ("Singstimme Voice", "First Violins", "Solo Flute")
|
|
273
|
+
// usually put the instrument at the end, so the trailing word is tried before
|
|
274
|
+
// earlier qualifier words. Each word attempt runs through the de-plural path
|
|
275
|
+
// (lookupNormalized); SATB letters are intentionally NOT part of the word scan.
|
|
276
|
+
export const gmProgramOf = (name) => {
|
|
277
|
+
if (!name)
|
|
278
|
+
return undefined;
|
|
279
|
+
const norm = normalizeInstrumentName(name);
|
|
280
|
+
const direct = lookupNormalized(norm);
|
|
281
|
+
if (direct !== undefined)
|
|
282
|
+
return direct;
|
|
283
|
+
// Whole-name-only: a lone S/A/T/B is a chorale voice part.
|
|
284
|
+
if (norm in SATB_VOICE)
|
|
285
|
+
return SATB_VOICE[norm];
|
|
286
|
+
const words = norm.split(" ");
|
|
287
|
+
if (words.length > 1) {
|
|
288
|
+
for (let i = words.length - 1; i >= 0; i--) {
|
|
289
|
+
const hit = lookupNormalized(words[i]);
|
|
290
|
+
if (hit !== undefined)
|
|
291
|
+
return hit;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Generic highlight scopes Lilylet tokens can carry. */
|
|
2
|
+
export type HighlightScope = "articulation" | "bar" | "brace" | "chordBracket" | "comment" | "dynamic" | "grace" | "hairpin" | "header" | "keyword" | "markup" | "mode" | "navigation" | "number" | "octave" | "operator" | "ornament" | "paren" | "pedal" | "pitch" | "punctuation" | "rest" | "separator" | "squareBracket" | "stem" | "string" | "tie" | "tuplet";
|
|
3
|
+
export interface HighlightRule {
|
|
4
|
+
/** Sticky, case-insensitive regex anchored at the scan position. */
|
|
5
|
+
re: RegExp;
|
|
6
|
+
scope: HighlightScope;
|
|
7
|
+
}
|
|
8
|
+
export interface HighlightToken {
|
|
9
|
+
scope: HighlightScope;
|
|
10
|
+
/** Start offset within the line (inclusive). */
|
|
11
|
+
start: number;
|
|
12
|
+
/** End offset within the line (exclusive). */
|
|
13
|
+
end: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Ordered highlight rules. Order mirrors the grammar's lexer; the tokenizer
|
|
17
|
+
* applies LONGEST-match (flex semantics), using order only as a tie-breaker.
|
|
18
|
+
*/
|
|
19
|
+
export declare const HIGHLIGHT_RULES: HighlightRule[];
|
|
20
|
+
/**
|
|
21
|
+
* Match a single token at `pos` in `line` using longest-match. Returns the
|
|
22
|
+
* winning token, or null if no rule matches (caller should advance one char).
|
|
23
|
+
*/
|
|
24
|
+
export declare const matchAt: (line: string, pos: number) => HighlightToken | null;
|
|
25
|
+
/**
|
|
26
|
+
* Tokenize one line into a list of scoped spans. Characters that match no rule
|
|
27
|
+
* are skipped (no token emitted), mirroring the lexer's catch-all.
|
|
28
|
+
*/
|
|
29
|
+
export declare const tokenizeLine: (line: string) => HighlightToken[];
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// AUTO-GENERATED by tools/buildHighlight.ts from source/lilylet/lilylet.jison.
|
|
2
|
+
// Do NOT edit by hand. Run `npm run build:highlight` to regenerate.
|
|
3
|
+
//
|
|
4
|
+
// Framework-agnostic syntax-highlighting definition for Lilylet, derived from
|
|
5
|
+
// the grammar's lexer so it never drifts from the language. No editor
|
|
6
|
+
// dependency: it exposes generic SCOPE names and a longest-match tokenizer.
|
|
7
|
+
// Downstream editors (CodeMirror, Monaco, Prism, ...) map SCOPE -> their theme.
|
|
8
|
+
/**
|
|
9
|
+
* Ordered highlight rules. Order mirrors the grammar's lexer; the tokenizer
|
|
10
|
+
* applies LONGEST-match (flex semantics), using order only as a tie-breaker.
|
|
11
|
+
*/
|
|
12
|
+
export const HIGHLIGHT_RULES = [
|
|
13
|
+
{ re: /\%.*/iy, scope: "comment" },
|
|
14
|
+
{ re: /\[title/iy, scope: "header" },
|
|
15
|
+
{ re: /\[subtitle/iy, scope: "header" },
|
|
16
|
+
{ re: /\[composer/iy, scope: "header" },
|
|
17
|
+
{ re: /\[arranger/iy, scope: "header" },
|
|
18
|
+
{ re: /\[lyricist/iy, scope: "header" },
|
|
19
|
+
{ re: /\[opus/iy, scope: "header" },
|
|
20
|
+
{ re: /\[instrument\-[A-Za-z0-9_]+(?:\-[A-Za-z0-9_]+)*/iy, scope: "header" },
|
|
21
|
+
{ re: /\[instrument/iy, scope: "header" },
|
|
22
|
+
{ re: /\[genre/iy, scope: "header" },
|
|
23
|
+
{ re: /\[staves/iy, scope: "header" },
|
|
24
|
+
{ re: /\[auto\-beam/iy, scope: "header" },
|
|
25
|
+
{ re: /\]/iy, scope: "squareBracket" },
|
|
26
|
+
{ re: /"[^"]*"/iy, scope: "string" },
|
|
27
|
+
{ re: /\\clef/iy, scope: "keyword" },
|
|
28
|
+
{ re: /\\key/iy, scope: "keyword" },
|
|
29
|
+
{ re: /\\time/iy, scope: "keyword" },
|
|
30
|
+
{ re: /\\partial/iy, scope: "keyword" },
|
|
31
|
+
{ re: /\\numericTimeSignature/iy, scope: "keyword" },
|
|
32
|
+
{ re: /\\defaultTimeSignature/iy, scope: "keyword" },
|
|
33
|
+
{ re: /\\tempo/iy, scope: "keyword" },
|
|
34
|
+
{ re: /\\staff/iy, scope: "keyword" },
|
|
35
|
+
{ re: /\\grace/iy, scope: "grace" },
|
|
36
|
+
{ re: /\\times/iy, scope: "tuplet" },
|
|
37
|
+
{ re: /\\tuplet/iy, scope: "tuplet" },
|
|
38
|
+
{ re: /\\repeat/iy, scope: "keyword" },
|
|
39
|
+
{ re: /\\ottava/iy, scope: "keyword" },
|
|
40
|
+
{ re: /\\stemUp/iy, scope: "stem" },
|
|
41
|
+
{ re: /\\stemDown/iy, scope: "stem" },
|
|
42
|
+
{ re: /\\stemNeutral/iy, scope: "stem" },
|
|
43
|
+
{ re: /\\major/iy, scope: "mode" },
|
|
44
|
+
{ re: /\\minor/iy, scope: "mode" },
|
|
45
|
+
{ re: /\\sustainOn/iy, scope: "pedal" },
|
|
46
|
+
{ re: /\\sustainOff/iy, scope: "pedal" },
|
|
47
|
+
{ re: /\\bar/iy, scope: "keyword" },
|
|
48
|
+
{ re: /\\coda/iy, scope: "navigation" },
|
|
49
|
+
{ re: /\\segno/iy, scope: "navigation" },
|
|
50
|
+
{ re: /\\chords/iy, scope: "keyword" },
|
|
51
|
+
{ re: /\\markup/iy, scope: "markup" },
|
|
52
|
+
{ re: /\\</iy, scope: "hairpin" },
|
|
53
|
+
{ re: /\\>/iy, scope: "hairpin" },
|
|
54
|
+
{ re: /\\!/iy, scope: "hairpin" },
|
|
55
|
+
{ re: /\\staccato/iy, scope: "articulation" },
|
|
56
|
+
{ re: /\\staccatissimo/iy, scope: "articulation" },
|
|
57
|
+
{ re: /\\tenuto/iy, scope: "articulation" },
|
|
58
|
+
{ re: /\\marcato/iy, scope: "articulation" },
|
|
59
|
+
{ re: /\\accent/iy, scope: "articulation" },
|
|
60
|
+
{ re: /\\portato/iy, scope: "articulation" },
|
|
61
|
+
{ re: /\\trill/iy, scope: "ornament" },
|
|
62
|
+
{ re: /\\turn/iy, scope: "ornament" },
|
|
63
|
+
{ re: /\\mordent/iy, scope: "ornament" },
|
|
64
|
+
{ re: /\\prall/iy, scope: "ornament" },
|
|
65
|
+
{ re: /\\fermata/iy, scope: "ornament" },
|
|
66
|
+
{ re: /\\shortfermata/iy, scope: "ornament" },
|
|
67
|
+
{ re: /\\arpeggio/iy, scope: "ornament" },
|
|
68
|
+
{ re: /\\ppp/iy, scope: "dynamic" },
|
|
69
|
+
{ re: /\\pp/iy, scope: "dynamic" },
|
|
70
|
+
{ re: /\\mp/iy, scope: "dynamic" },
|
|
71
|
+
{ re: /\\mf/iy, scope: "dynamic" },
|
|
72
|
+
{ re: /\\fff/iy, scope: "dynamic" },
|
|
73
|
+
{ re: /\\ff/iy, scope: "dynamic" },
|
|
74
|
+
{ re: /\\sfz/iy, scope: "dynamic" },
|
|
75
|
+
{ re: /\\rfz/iy, scope: "dynamic" },
|
|
76
|
+
{ re: /\\sf/iy, scope: "dynamic" },
|
|
77
|
+
{ re: /\\fp/iy, scope: "dynamic" },
|
|
78
|
+
{ re: /\\p/iy, scope: "dynamic" },
|
|
79
|
+
{ re: /\\f/iy, scope: "dynamic" },
|
|
80
|
+
{ re: /\\rest/iy, scope: "rest" },
|
|
81
|
+
{ re: /\\\\\\/iy, scope: "separator" },
|
|
82
|
+
{ re: /\\\\/iy, scope: "separator" },
|
|
83
|
+
{ re: /tremolo/iy, scope: "keyword" },
|
|
84
|
+
{ re: /[a-g](ss|ff|s|f)?/iy, scope: "pitch" },
|
|
85
|
+
{ re: /'/iy, scope: "octave" },
|
|
86
|
+
{ re: /,/iy, scope: "octave" },
|
|
87
|
+
{ re: /[0-9]+/iy, scope: "number" },
|
|
88
|
+
{ re: /\//iy, scope: "operator" },
|
|
89
|
+
{ re: /#/iy, scope: "punctuation" },
|
|
90
|
+
{ re: /\{/iy, scope: "brace" },
|
|
91
|
+
{ re: /\}/iy, scope: "brace" },
|
|
92
|
+
{ re: /</iy, scope: "chordBracket" },
|
|
93
|
+
{ re: />/iy, scope: "chordBracket" },
|
|
94
|
+
{ re: /\|/iy, scope: "bar" },
|
|
95
|
+
{ re: /\[/iy, scope: "squareBracket" },
|
|
96
|
+
{ re: /\]/iy, scope: "squareBracket" },
|
|
97
|
+
{ re: /\(/iy, scope: "paren" },
|
|
98
|
+
{ re: /\)/iy, scope: "paren" },
|
|
99
|
+
{ re: /~/iy, scope: "tie" },
|
|
100
|
+
{ re: /\./iy, scope: "punctuation" },
|
|
101
|
+
{ re: /-/iy, scope: "punctuation" },
|
|
102
|
+
{ re: /[_]/iy, scope: "punctuation" },
|
|
103
|
+
{ re: /\^/iy, scope: "punctuation" },
|
|
104
|
+
{ re: /!/iy, scope: "punctuation" },
|
|
105
|
+
{ re: /:/iy, scope: "operator" },
|
|
106
|
+
{ re: /=/iy, scope: "operator" },
|
|
107
|
+
{ re: /[rR]/iy, scope: "rest" },
|
|
108
|
+
{ re: /[sS]/iy, scope: "rest" },
|
|
109
|
+
];
|
|
110
|
+
/**
|
|
111
|
+
* Match a single token at `pos` in `line` using longest-match. Returns the
|
|
112
|
+
* winning token, or null if no rule matches (caller should advance one char).
|
|
113
|
+
*/
|
|
114
|
+
export const matchAt = (line, pos) => {
|
|
115
|
+
let best = null;
|
|
116
|
+
for (const rule of HIGHLIGHT_RULES) {
|
|
117
|
+
rule.re.lastIndex = pos;
|
|
118
|
+
const m = rule.re.exec(line);
|
|
119
|
+
if (m && m.index === pos && m[0].length > 0) {
|
|
120
|
+
const end = pos + m[0].length;
|
|
121
|
+
if (!best || end > best.end)
|
|
122
|
+
best = { scope: rule.scope, start: pos, end };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return best;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Tokenize one line into a list of scoped spans. Characters that match no rule
|
|
129
|
+
* are skipped (no token emitted), mirroring the lexer's catch-all.
|
|
130
|
+
*/
|
|
131
|
+
export const tokenizeLine = (line) => {
|
|
132
|
+
const tokens = [];
|
|
133
|
+
let pos = 0;
|
|
134
|
+
while (pos < line.length) {
|
|
135
|
+
const tok = matchAt(line, pos);
|
|
136
|
+
if (tok) {
|
|
137
|
+
tokens.push(tok);
|
|
138
|
+
pos = tok.end;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
pos++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return tokens;
|
|
145
|
+
};
|