@k-l-lambda/lilylet 0.1.30

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.
@@ -0,0 +1,12 @@
1
+
2
+ export * from "./types";
3
+ export * from "./parser";
4
+ export * from "./serializer";
5
+
6
+ import * as meiEncoder from "./meiEncoder";
7
+ import * as musicXmlDecoder from "./musicXmlDecoder";
8
+
9
+ export {
10
+ meiEncoder,
11
+ musicXmlDecoder,
12
+ };
@@ -0,0 +1,593 @@
1
+
2
+ %{
3
+ // Helper functions
4
+ const fraction = (numerator, denominator) => ({ numerator, denominator });
5
+
6
+ const pitch = (phonet, accidental, octave) => ({
7
+ phonet,
8
+ accidental: accidental || undefined,
9
+ octave: octave || 0,
10
+ });
11
+
12
+ const duration = (division, dots) => ({
13
+ division,
14
+ dots: dots || 0,
15
+ });
16
+
17
+ const noteEvent = (pitches, dur, marks, options = {}) => {
18
+ // Check if this is a pitched rest (e.g., g'\rest)
19
+ const pitchedRestMark = marks && marks.find(m => m && m.pitchedRest);
20
+ if (pitchedRestMark) {
21
+ const pitch = Array.isArray(pitches) ? pitches[0] : pitches;
22
+ return {
23
+ type: 'rest',
24
+ duration: dur,
25
+ pitch: pitch,
26
+ };
27
+ }
28
+ return {
29
+ type: 'note',
30
+ pitches: Array.isArray(pitches) ? pitches : [pitches],
31
+ duration: dur,
32
+ marks: marks && marks.length ? marks : undefined,
33
+ ...options,
34
+ };
35
+ };
36
+
37
+ const restEvent = (dur, options = {}) => ({
38
+ type: 'rest',
39
+ duration: dur,
40
+ ...options,
41
+ });
42
+
43
+ const contextChange = (changes) => ({
44
+ type: 'context',
45
+ ...changes,
46
+ });
47
+
48
+ const keySignature = (phonet, accidental, mode) => ({
49
+ pitch: phonet,
50
+ accidental: accidental || undefined,
51
+ mode,
52
+ });
53
+
54
+ const voice = (staff, events) => ({
55
+ staff: staff || 1,
56
+ events,
57
+ });
58
+
59
+ const part = (voices, name) => ({
60
+ name: name || undefined,
61
+ voices,
62
+ });
63
+
64
+ const measure = (parts, key, timeSig, partial) => ({
65
+ key: key || undefined,
66
+ timeSig: timeSig || undefined,
67
+ parts,
68
+ partial: partial || undefined,
69
+ });
70
+
71
+ const tupletEvent = (ratio, events) => ({
72
+ type: 'tuplet',
73
+ ratio,
74
+ events,
75
+ });
76
+
77
+ const tremoloEvent = (pitchA, pitchB, count, division) => ({
78
+ type: 'tremolo',
79
+ pitchA,
80
+ pitchB,
81
+ count,
82
+ division,
83
+ });
84
+
85
+ // Articulation/mark helpers - all marks have markType for discrimination
86
+ const articulation = (type, placement) => ({ markType: 'articulation', type, placement });
87
+ const ornament = (type) => ({ markType: 'ornament', type });
88
+ const dynamic = (type) => ({ markType: 'dynamic', type });
89
+ const hairpin = (type) => ({ markType: 'hairpin', type });
90
+ const pedal = (type) => ({ markType: 'pedal', type });
91
+ const tie = (start) => ({ markType: 'tie', start });
92
+ const slur = (start) => ({ markType: 'slur', start });
93
+ const beam = (start) => ({ markType: 'beam', start });
94
+ const fingering = (finger, placement) => ({ markType: 'fingering', finger, placement });
95
+ const navigation = (type) => ({ markType: 'navigation', type });
96
+
97
+ const barlineEvent = (style) => ({ type: 'barline', style });
98
+ const harmonyEvent = (text) => ({ type: 'harmony', text });
99
+ const markupEvent = (content, placement) => ({ type: 'markup', content, placement: placement || undefined });
100
+ const markupMark = (content) => ({ markType: 'markup', content });
101
+
102
+ // Parse PITCH token (e.g., "c", "cs", "bf", "css", "bff") into phonet and accidental
103
+ const parsePitch = (text, octave) => {
104
+ const phonet = text[0].toLowerCase();
105
+ const accStr = text.slice(1).toLowerCase();
106
+ let accidental = undefined;
107
+ if (accStr === 's') accidental = 'sharp';
108
+ else if (accStr === 'f') accidental = 'flat';
109
+ else if (accStr === 'ss') accidental = 'doubleSharp';
110
+ else if (accStr === 'ff') accidental = 'doubleFlat';
111
+ return pitch(phonet, accidental, octave || 0);
112
+ };
113
+
114
+ // Parse PITCH token for key signature (no octave)
115
+ const parsePitchName = (text) => {
116
+ const phonet = text[0].toLowerCase();
117
+ const accStr = text.slice(1).toLowerCase();
118
+ let accidental = undefined;
119
+ if (accStr === 's') accidental = 'sharp';
120
+ else if (accStr === 'f') accidental = 'flat';
121
+ else if (accStr === 'ss') accidental = 'doubleSharp';
122
+ else if (accStr === 'ff') accidental = 'doubleFlat';
123
+ return { phonet, accidental };
124
+ };
125
+
126
+ // Global state for parsing
127
+ let currentStaff = 1;
128
+ let currentKey = null;
129
+ let currentTimeSig = null;
130
+ let currentDuration = { division: 4, dots: 0 }; // default quarter note
131
+
132
+ // Reset parser state - call before each parse
133
+ const resetParserState = () => {
134
+ currentStaff = 1;
135
+ currentKey = null;
136
+ currentTimeSig = null;
137
+ currentDuration = { division: 4, dots: 0 };
138
+ };
139
+
140
+ // Export reset function
141
+ parser.resetState = resetParserState;
142
+ %}
143
+
144
+
145
+ %lex
146
+
147
+ %option flex unicode case-insensitive
148
+
149
+
150
+ %%
151
+
152
+ [ \t]+ {}
153
+ (\r?\n)+ return 'NEWLINE'
154
+ \%.* {}
155
+
156
+ \[title return 'HEADER_TITLE'
157
+ \[subtitle return 'HEADER_SUBTITLE'
158
+ \[composer return 'HEADER_COMPOSER'
159
+ \[arranger return 'HEADER_ARRANGER'
160
+ \[lyricist return 'HEADER_LYRICIST'
161
+ \[opus return 'HEADER_OPUS'
162
+ \[instrument return 'HEADER_INSTRUMENT'
163
+ \[genre return 'HEADER_GENRE'
164
+ \] return ']'
165
+
166
+ \"[^"]*\" return 'STRING'
167
+
168
+ "\\clef" return 'CMD_CLEF'
169
+ "\\key" return 'CMD_KEY'
170
+ "\\time" return 'CMD_TIME'
171
+ "\\tempo" return 'CMD_TEMPO'
172
+ "\\staff" return 'CMD_STAFF'
173
+ "\\grace" return 'CMD_GRACE'
174
+ "\\times" return 'CMD_TIMES'
175
+ "\\repeat" return 'CMD_REPEAT'
176
+ "\\ottava" return 'CMD_OTTAVA'
177
+ "\\stemUp" return 'CMD_STEMUP'
178
+ "\\stemDown" return 'CMD_STEMDOWN'
179
+ "\\stemNeutral" return 'CMD_STEMNEUTRAL'
180
+
181
+ "\\major" return 'MODE_MAJOR'
182
+ "\\minor" return 'MODE_MINOR'
183
+
184
+ "\\sustainOn" return 'CMD_SUSTAINON'
185
+ "\\sustainOff" return 'CMD_SUSTAINOFF'
186
+
187
+ "\\bar" return 'CMD_BAR'
188
+ "\\coda" return 'CMD_CODA'
189
+ "\\segno" return 'CMD_SEGNO'
190
+ "\\chords" return 'CMD_CHORDS'
191
+ "\\markup" return 'CMD_MARKUP'
192
+
193
+ "\\<" return 'CMD_CRESC_BEGIN'
194
+ "\\>" return 'CMD_DIM_BEGIN'
195
+ "\\!" return 'CMD_DYNAMICS_END'
196
+
197
+ "\\staccato" return 'ART_STACCATO'
198
+ "\\staccatissimo" return 'ART_STACCATISSIMO'
199
+ "\\tenuto" return 'ART_TENUTO'
200
+ "\\marcato" return 'ART_MARCATO'
201
+ "\\accent" return 'ART_ACCENT'
202
+ "\\portato" return 'ART_PORTATO'
203
+
204
+ "\\trill" return 'ORN_TRILL'
205
+ "\\turn" return 'ORN_TURN'
206
+ "\\mordent" return 'ORN_MORDENT'
207
+ "\\prall" return 'ORN_PRALL'
208
+ "\\fermata" return 'ORN_FERMATA'
209
+ "\\shortfermata" return 'ORN_SHORTFERMATA'
210
+ "\\arpeggio" return 'ORN_ARPEGGIO'
211
+
212
+ "\\ppp" return 'DYN_PPP'
213
+ "\\pp" return 'DYN_PP'
214
+ "\\mp" return 'DYN_MP'
215
+ "\\mf" return 'DYN_MF'
216
+ "\\fff" return 'DYN_FFF'
217
+ "\\ff" return 'DYN_FF'
218
+ "\\sfz" return 'DYN_SFZ'
219
+ "\\rfz" return 'DYN_RFZ'
220
+ "\\sf" return 'DYN_SF'
221
+ "\\p" return 'DYN_P'
222
+ "\\f" return 'DYN_F'
223
+
224
+ "\\rest" return 'CMD_REST'
225
+
226
+ "\\\\\\" return 'PART_SEP'
227
+ "\\\\" return 'VOICE_SEP'
228
+
229
+ "tremolo" return 'TREMOLO'
230
+
231
+ [a-g](ss|ff|s|f)? return 'PITCH'
232
+
233
+ "'" return 'OCT_UP'
234
+ "," return 'OCT_DOWN'
235
+
236
+ [0-9]+ return 'NUMBER'
237
+
238
+ "/" return '/'
239
+ "#" return '#'
240
+ "{" return '{'
241
+ "}" return '}'
242
+ "<" return '<'
243
+ ">" return '>'
244
+ "|" return '|'
245
+ "[" return '['
246
+ "]" return ']'
247
+ "(" return '('
248
+ ")" return ')'
249
+ "~" return '~'
250
+ "." return '.'
251
+ "-" return '-'
252
+ "_" return '_'
253
+ "^" return '^'
254
+ "!" return '!'
255
+ ":" return ':'
256
+ "=" return '='
257
+
258
+ [rR] return 'REST_CHAR'
259
+ [sS] return 'SPACE_CHAR'
260
+
261
+ <<EOF>> return 'EOF'
262
+
263
+ . {}
264
+
265
+
266
+ /lex
267
+
268
+ %start document
269
+
270
+ %%
271
+
272
+ document
273
+ : content EOF { return { metadata: $1.metadata, measures: $1.measures }; }
274
+ ;
275
+
276
+ content
277
+ : headers measures -> ({ metadata: $1, measures: $2 })
278
+ | headers newlines measures -> ({ metadata: $1, measures: $3 })
279
+ | newlines measures -> ({ metadata: undefined, measures: $2 })
280
+ | measures -> ({ metadata: undefined, measures: $1 })
281
+ ;
282
+
283
+ newlines
284
+ : NEWLINE
285
+ | newlines NEWLINE
286
+ ;
287
+
288
+ headers
289
+ : header -> $1
290
+ | headers header -> ({ ...$1, ...$2 })
291
+ | headers NEWLINE -> $1
292
+ | headers NEWLINE header -> ({ ...$1, ...$3 })
293
+ ;
294
+
295
+ header
296
+ : HEADER_TITLE STRING ']' -> ({ title: $2.slice(1, -1) })
297
+ | HEADER_SUBTITLE STRING ']' -> ({ subtitle: $2.slice(1, -1) })
298
+ | HEADER_COMPOSER STRING ']' -> ({ composer: $2.slice(1, -1) })
299
+ | HEADER_ARRANGER STRING ']' -> ({ arranger: $2.slice(1, -1) })
300
+ | HEADER_LYRICIST STRING ']' -> ({ lyricist: $2.slice(1, -1) })
301
+ | HEADER_OPUS STRING ']' -> ({ opus: $2.slice(1, -1) })
302
+ | HEADER_INSTRUMENT STRING ']' -> ({ instrument: $2.slice(1, -1) })
303
+ | HEADER_GENRE STRING ']' -> ({ genre: $2.slice(1, -1) })
304
+ ;
305
+
306
+ measures
307
+ : measure_content { $$ = [$1]; }
308
+ | measures '|' measure_content { $$ = $1.concat([$3]); }
309
+ | measures '|' { $$ = $1; }
310
+ ;
311
+
312
+ measure_content
313
+ : parts -> measure($1, currentKey, currentTimeSig)
314
+ ;
315
+
316
+ parts
317
+ : part_voices { $$ = [part($1)]; }
318
+ | parts PART_SEP part_start part_voices { $$ = $1.concat([part($4)]); }
319
+ ;
320
+
321
+ part_start
322
+ : /* empty */ %{ currentStaff = 1; %}
323
+ ;
324
+
325
+ part_voices
326
+ : voice_events { $$ = [voice(currentStaff, $1)]; }
327
+ | part_voices VOICE_SEP voice_events { $$ = $1.concat([voice(currentStaff, $3)]); }
328
+ ;
329
+
330
+ voice_events
331
+ : /* empty */ { $$ = []; }
332
+ | voice_events event { $$ = $1.concat(Array.isArray($2) ? $2 : [$2]); }
333
+ ;
334
+
335
+ event
336
+ : note_event
337
+ | rest_event
338
+ | context_event
339
+ | grace_event
340
+ | tuplet_event
341
+ | tremolo_event
342
+ | pitch_reset_event
343
+ | barline_event
344
+ | harmony_event
345
+ | markup_event
346
+ ;
347
+
348
+ barline_event
349
+ : CMD_BAR STRING -> barlineEvent($2.slice(1, -1))
350
+ ;
351
+
352
+ harmony_event
353
+ : CMD_CHORDS STRING -> harmonyEvent($2.slice(1, -1))
354
+ ;
355
+
356
+ markup_event
357
+ : CMD_MARKUP STRING -> markupEvent($2.slice(1, -1))
358
+ ;
359
+
360
+ pitch_reset_event
361
+ : NEWLINE -> ({ type: 'pitchReset' })
362
+ ;
363
+
364
+ note_event
365
+ : chord duration post_events %{ currentDuration = $2; $$ = noteEvent($1, $2, $3); %}
366
+ | pitch duration post_events %{ currentDuration = $2; $$ = noteEvent($1, $2, $3); %}
367
+ | chord post_events -> noteEvent($1, currentDuration, $2)
368
+ | pitch post_events -> noteEvent($1, currentDuration, $2)
369
+ ;
370
+
371
+ chord
372
+ : '<' pitches '>' -> $2
373
+ ;
374
+
375
+ pitches
376
+ : pitch { $$ = [$1]; }
377
+ | pitches pitch { $$ = $1.concat([$2]); }
378
+ ;
379
+
380
+ pitch
381
+ : PITCH octave -> parsePitch($1, $2)
382
+ | PITCH -> parsePitch($1, 0)
383
+ ;
384
+
385
+ octave
386
+ : OCT_UP -> 1
387
+ | OCT_DOWN -> -1
388
+ | octave OCT_UP -> $1 + 1
389
+ | octave OCT_DOWN -> $1 - 1
390
+ ;
391
+
392
+ duration
393
+ : NUMBER dots -> duration(Number($1), $2)
394
+ ;
395
+
396
+ dots
397
+ : /* empty */ { $$ = 0; }
398
+ | dots '.' { $$ = $1 + 1; }
399
+ ;
400
+
401
+ rest_event
402
+ : REST_CHAR duration post_events %{ currentDuration = $2; $$ = restEvent($2, { fullMeasure: $1 === 'R' }); %}
403
+ | SPACE_CHAR duration post_events %{ currentDuration = $2; $$ = restEvent($2, { invisible: true }); %}
404
+ | REST_CHAR post_events -> restEvent(currentDuration, { fullMeasure: $1 === 'R' })
405
+ | SPACE_CHAR post_events -> restEvent(currentDuration, { invisible: true })
406
+ ;
407
+
408
+ context_event
409
+ : clef_cmd -> contextChange({ clef: $1 })
410
+ | key_cmd -> contextChange({ key: $1 })
411
+ | time_cmd -> contextChange({ time: $1 })
412
+ | tempo_cmd -> contextChange({ tempo: $1 })
413
+ | staff_cmd -> contextChange({ staff: $1 })
414
+ | ottava_cmd -> contextChange({ ottava: $1 })
415
+ | stem_cmd -> contextChange({ stemDirection: $1 })
416
+ ;
417
+
418
+ clef_cmd
419
+ : CMD_CLEF STRING -> $2.slice(1, -1)
420
+ ;
421
+
422
+ key_cmd
423
+ : CMD_KEY pitch_name mode %{ currentKey = keySignature($2.phonet, $2.accidental, $3); $$ = currentKey; %}
424
+ ;
425
+
426
+ pitch_name
427
+ : PITCH -> parsePitchName($1)
428
+ ;
429
+
430
+ mode
431
+ : MODE_MAJOR -> 'major'
432
+ | MODE_MINOR -> 'minor'
433
+ ;
434
+
435
+ time_cmd
436
+ : CMD_TIME NUMBER '/' NUMBER %{ currentTimeSig = fraction(Number($2), Number($4)); $$ = currentTimeSig; %}
437
+ ;
438
+
439
+ tempo_cmd
440
+ : CMD_TEMPO STRING duration '=' NUMBER -> ({ text: $2.slice(1, -1), beat: $3, bpm: Number($5) })
441
+ | CMD_TEMPO STRING -> ({ text: $2.slice(1, -1) })
442
+ | CMD_TEMPO duration '=' NUMBER -> ({ beat: $2, bpm: Number($4) })
443
+ ;
444
+
445
+ staff_cmd
446
+ : CMD_STAFF STRING %{ currentStaff = Number($2.slice(1, -1)); $$ = currentStaff; %}
447
+ ;
448
+
449
+ ottava_cmd
450
+ : CMD_OTTAVA '#' NUMBER -> Number($3)
451
+ | CMD_OTTAVA '#' '-' NUMBER -> -Number($4)
452
+ | CMD_OTTAVA -> 0
453
+ ;
454
+
455
+ stem_cmd
456
+ : CMD_STEMUP -> 'up'
457
+ | CMD_STEMDOWN -> 'down'
458
+ | CMD_STEMNEUTRAL -> 'auto'
459
+ ;
460
+
461
+ grace_event
462
+ : CMD_GRACE '{' voice_events '}' -> ($3.filter(e => e.type === 'note' || e.type === 'rest').map(e => ({ ...e, grace: true })))
463
+ | CMD_GRACE note_event -> ({ ...$2, grace: true })
464
+ | CMD_GRACE rest_event -> ({ ...$2, grace: true })
465
+ ;
466
+
467
+ tuplet_event
468
+ : CMD_TIMES NUMBER '/' NUMBER '{' voice_events '}' -> tupletEvent(fraction(Number($2), Number($4)), $6.filter(e => e.type === 'note' || e.type === 'rest'))
469
+ ;
470
+
471
+ tremolo_event
472
+ : CMD_REPEAT TREMOLO NUMBER '{' pitch duration pitch duration '}' %{ currentDuration = $6; $$ = tremoloEvent([$5], [$7], Number($3), $6.division); %}
473
+ | CMD_REPEAT TREMOLO NUMBER '{' pitch duration pitch '}' %{ currentDuration = $6; $$ = tremoloEvent([$5], [$7], Number($3), $6.division); %}
474
+ | CMD_REPEAT TREMOLO NUMBER '{' pitch pitch '}' -> tremoloEvent([$5], [$6], Number($3), currentDuration.division)
475
+ ;
476
+
477
+ post_events
478
+ : /* empty */ { $$ = []; }
479
+ | post_events post_event { $$ = $1.concat([$2]); }
480
+ ;
481
+
482
+ post_event
483
+ : articulation_mark
484
+ | ornament_mark
485
+ | dynamic_mark
486
+ | hairpin_mark
487
+ | pedal_mark
488
+ | tie_mark
489
+ | slur_mark
490
+ | beam_mark
491
+ | tremolo_mark
492
+ | direction_mark
493
+ | rest_mark
494
+ | fingering_mark
495
+ | navigation_mark
496
+ | markup_mark
497
+ ;
498
+
499
+ rest_mark
500
+ : CMD_REST -> ({ pitchedRest: true })
501
+ ;
502
+
503
+ articulation_mark
504
+ : ART_STACCATO -> articulation('staccato')
505
+ | ART_STACCATISSIMO -> articulation('staccatissimo')
506
+ | ART_TENUTO -> articulation('tenuto')
507
+ | ART_MARCATO -> articulation('marcato')
508
+ | ART_ACCENT -> articulation('accent')
509
+ | ART_PORTATO -> articulation('portato')
510
+ | '-' '.' -> articulation('staccato')
511
+ | '-' '_' -> articulation('portato')
512
+ | '-' '^' -> articulation('marcato')
513
+ | '-' '>' -> articulation('accent')
514
+ | '-' '!' -> articulation('staccatissimo')
515
+ | '-' '-' -> articulation('tenuto')
516
+ | '>' -> articulation('accent')
517
+ | '.' -> articulation('staccato')
518
+ | '-' -> articulation('tenuto')
519
+ | '!' -> articulation('staccatissimo')
520
+ | '^' -> articulation('marcato')
521
+ | '_' -> articulation('portato')
522
+ ;
523
+
524
+ ornament_mark
525
+ : ORN_TRILL -> ornament('trill')
526
+ | ORN_TURN -> ornament('turn')
527
+ | ORN_MORDENT -> ornament('mordent')
528
+ | ORN_PRALL -> ornament('prall')
529
+ | ORN_FERMATA -> ornament('fermata')
530
+ | ORN_SHORTFERMATA -> ornament('shortFermata')
531
+ | ORN_ARPEGGIO -> ornament('arpeggio')
532
+ ;
533
+
534
+ dynamic_mark
535
+ : DYN_PPP -> dynamic('ppp')
536
+ | DYN_PP -> dynamic('pp')
537
+ | DYN_P -> dynamic('p')
538
+ | DYN_MP -> dynamic('mp')
539
+ | DYN_MF -> dynamic('mf')
540
+ | DYN_F -> dynamic('f')
541
+ | DYN_FF -> dynamic('ff')
542
+ | DYN_FFF -> dynamic('fff')
543
+ | DYN_SFZ -> dynamic('sfz')
544
+ | DYN_RFZ -> dynamic('rfz')
545
+ | DYN_SF -> dynamic('sfz')
546
+ ;
547
+
548
+ hairpin_mark
549
+ : CMD_CRESC_BEGIN -> hairpin('crescendoStart')
550
+ | CMD_DIM_BEGIN -> hairpin('diminuendoStart')
551
+ | CMD_DYNAMICS_END -> hairpin('crescendoEnd')
552
+ ;
553
+
554
+ pedal_mark
555
+ : CMD_SUSTAINON -> pedal('sustainOn')
556
+ | CMD_SUSTAINOFF -> pedal('sustainOff')
557
+ ;
558
+
559
+ tie_mark
560
+ : '~' -> tie(true)
561
+ ;
562
+
563
+ slur_mark
564
+ : '(' -> slur(true)
565
+ | ')' -> slur(false)
566
+ ;
567
+
568
+ beam_mark
569
+ : '[' -> beam(true)
570
+ | ']' -> beam(false)
571
+ ;
572
+
573
+ tremolo_mark
574
+ : ':' NUMBER -> ({ tremolo: Number($2) })
575
+ ;
576
+
577
+ direction_mark
578
+ : '^' post_event -> ({ ...$2, placement: 'above' })
579
+ | '_' post_event -> ({ ...$2, placement: 'below' })
580
+ ;
581
+
582
+ fingering_mark
583
+ : '-' NUMBER %{ const n = Number($2); if (n >= 1 && n <= 5) $$ = fingering(n); else $$ = null; %}
584
+ ;
585
+
586
+ navigation_mark
587
+ : CMD_CODA -> navigation('coda')
588
+ | CMD_SEGNO -> navigation('segno')
589
+ ;
590
+
591
+ markup_mark
592
+ : CMD_MARKUP STRING -> markupMark($2.slice(1, -1))
593
+ ;