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