@k-l-lambda/lilylet 0.1.60 → 0.1.63

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.
@@ -301,6 +301,10 @@ const serializeContextChange = (event) => {
301
301
  if (event.time) {
302
302
  parts.push('\\time ' + event.time.numerator + '/' + event.time.denominator);
303
303
  }
304
+ // Partial (pickup measure duration check)
305
+ if (event.partial) {
306
+ parts.push('\\partial ' + event.partial.division + '.'.repeat(event.partial.dots || 0));
307
+ }
304
308
  // Ottava
305
309
  if (event.ottava !== undefined) {
306
310
  if (event.ottava === 0) {
@@ -343,8 +347,11 @@ const serializeTempo = (tempo) => {
343
347
  const serializeTupletEvent = (event, env) => {
344
348
  const parts = [];
345
349
  let currentEnv = env;
346
- // \times numerator/denominator { ... }
347
- parts.push('\\times ' + event.ratio.numerator + '/' + event.ratio.denominator + ' {');
350
+ // \tuplet denominator/numerator { ... } for tuplet type, \times numerator/denominator for times type
351
+ const keyword = event.type === 'times'
352
+ ? '\\times ' + event.ratio.numerator + '/' + event.ratio.denominator
353
+ : '\\tuplet ' + event.ratio.denominator + '/' + event.ratio.numerator;
354
+ parts.push(keyword + ' {');
348
355
  let prevDuration;
349
356
  for (const e of event.events) {
350
357
  if (e.type === 'note') {
@@ -359,6 +366,20 @@ const serializeTupletEvent = (event, env) => {
359
366
  currentEnv = newEnv;
360
367
  prevDuration = e.duration;
361
368
  }
369
+ else if (e.type === 'context') {
370
+ const ctx = e;
371
+ if (ctx.staff != null) {
372
+ parts.push(' \\staff "' + ctx.staff + '"');
373
+ }
374
+ else if (ctx.stemDirection != null) {
375
+ if (ctx.stemDirection === StemDirection.up)
376
+ parts.push(' \\stemUp');
377
+ else if (ctx.stemDirection === StemDirection.down)
378
+ parts.push(' \\stemDown');
379
+ else if (ctx.stemDirection === StemDirection.auto)
380
+ parts.push(' \\stemNeutral');
381
+ }
382
+ }
362
383
  }
363
384
  parts.push(' }');
364
385
  return { str: parts.join(''), newEnv: currentEnv };
@@ -428,6 +449,7 @@ const serializeEvent = (event, env, prevDuration) => {
428
449
  case 'context':
429
450
  return { str: serializeContextChange(event), newEnv: env };
430
451
  case 'tuplet':
452
+ case 'times':
431
453
  return serializeTupletEvent(event, env);
432
454
  case 'tremolo':
433
455
  return serializeTremoloEvent(event, env);
@@ -468,7 +490,7 @@ const serializeVoice = (voice, currentStaff, isGrandStaff = false, measureContex
468
490
  // before any music collapse to the last one (earlier ones are no-ops).
469
491
  // leadStaffScanEnd is the index of the first event that ends this scan —
470
492
  // context{staff} events before this index are skipped in the main loop.
471
- const MUSICAL_TYPES = new Set(['note', 'rest', 'tuplet', 'tremolo']);
493
+ const MUSICAL_TYPES = new Set(['note', 'rest', 'tuplet', 'times', 'tremolo']);
472
494
  let effectiveInitialStaff = voice.staff;
473
495
  let leadStaffScanEnd = 0;
474
496
  for (let i = 0; i < voice.events.length; i++) {
@@ -607,6 +629,13 @@ const serializeVoice = (voice, currentStaff, isGrandStaff = false, measureContex
607
629
  else if (event.type === 'rest') {
608
630
  prevDuration = event.duration;
609
631
  }
632
+ else if (event.type === 'tuplet' || event.type === 'times') {
633
+ // After a tuplet/times block the LilyPond parser's "current duration" is the
634
+ // last note duration inside the tuplet, not the duration before the tuplet.
635
+ // Reset prevDuration so the first note after the block always emits its
636
+ // duration explicitly, avoiding wrong inheritance from inside the tuplet.
637
+ prevDuration = undefined;
638
+ }
610
639
  else if (event.type === 'context' && event.clef && emittedClefs) {
611
640
  const ctx = event;
612
641
  emittedClefs[ctx.staff || activeStaff] = ctx.clef;
@@ -667,7 +696,7 @@ const serializeMeasure = (measure, _isFirst, currentStaff, isGrandStaff = false,
667
696
  }
668
697
  staff = newStaff;
669
698
  }
670
- parts.push(partStrs.join(' \\\\\\\\\n'));
699
+ parts.push(partStrs.join(' \\\\\\\n'));
671
700
  }
672
701
  return { str: parts.join(' '), newStaff: staff };
673
702
  };
@@ -181,6 +181,7 @@ export interface ContextChange {
181
181
  type: 'context';
182
182
  key?: KeySignature;
183
183
  time?: Fraction;
184
+ partial?: Duration;
184
185
  clef?: Clef;
185
186
  ottava?: number;
186
187
  stemDirection?: StemDirection;
@@ -197,7 +198,12 @@ export interface TremoloEvent {
197
198
  export interface TupletEvent {
198
199
  type: 'tuplet';
199
200
  ratio: Fraction;
200
- events: (NoteEvent | RestEvent)[];
201
+ events: (NoteEvent | RestEvent | ContextChange)[];
202
+ }
203
+ export interface TimesEvent {
204
+ type: 'times';
205
+ ratio: Fraction;
206
+ events: (NoteEvent | RestEvent | ContextChange)[];
201
207
  }
202
208
  export interface PitchResetEvent {
203
209
  type: 'pitchReset';
@@ -215,7 +221,7 @@ export interface MarkupEvent {
215
221
  content: string;
216
222
  placement?: Placement;
217
223
  }
218
- export type Event = NoteEvent | RestEvent | ContextChange | TremoloEvent | TupletEvent | PitchResetEvent | BarlineEvent | HarmonyEvent | MarkupEvent;
224
+ export type Event = NoteEvent | RestEvent | ContextChange | TremoloEvent | TupletEvent | TimesEvent | PitchResetEvent | BarlineEvent | HarmonyEvent | MarkupEvent;
219
225
  export interface Voice {
220
226
  staff: number;
221
227
  events: Event[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k-l-lambda/lilylet",
3
- "version": "0.1.60",
3
+ "version": "0.1.63",
4
4
  "description": "Lilylet is a lilyopnd-like sheet music language designed for Markdown rendering and symbolic music representation in AIGC applications.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -27,12 +27,18 @@
27
27
  "build": "tsc -p tsconfig.build.json && node tools/convertGrammarToESM.cjs && node tools/fixEsmExtensions.cjs",
28
28
  "build:grammar": "npx tsx ./tools/buildJisonParser.ts && node tools/convertGrammarToESM.cjs",
29
29
  "prepublishOnly": "npm run build:grammar && npm run build",
30
- "test": "npx tsx ./tests/parser.ts",
31
- "test:mei": "npx tsx ./tests/mei.ts",
32
- "test:unit": "npx tsx ./tests/unit/encodePitch.test.ts",
33
- "test:decoder": "npx tsx ./tests/lilypondDecoder.ts",
34
- "test:abc": "npx tsx ./tests/abc-decoder.ts",
35
- "ts": "npx tsx"
30
+ "test": "tsx ./tests/parser.ts",
31
+ "test:mei": "tsx ./tests/mei.ts",
32
+ "test:mei-hashes": "tsx ./tests/computeMeiHashes.ts",
33
+ "test:unit": "tsx ./tests/unit/encodePitch.test.ts",
34
+ "test:partial": "tsx ./tests/unit/partialWarning.test.ts",
35
+ "test:decoder": "tsx ./tests/lilypondDecoder.ts",
36
+ "test:abc": "tsx ./tests/abc-decoder.ts",
37
+ "test:roundtrip": "tsx ./tests/lilypond-roundtrip.ts",
38
+ "test:abc-svg": "tsx ./tests/abc-abcjs-svg.ts",
39
+ "build:tests": "tsc -p tsconfig.tests.json; cp source/lilylet/grammar.jison.js lib-tests/source/lilylet/ && cp source/abc/grammar.jison.js lib-tests/source/abc/ && node tools/fixEsmExtensions.cjs lib-tests && ln -sfn ../../tests/assets lib-tests/tests/assets",
40
+ "test:roundtrip:compiled": "node lib-tests/tests/lilypond-roundtrip.js",
41
+ "ts": "tsx"
36
42
  },
37
43
  "repository": {
38
44
  "type": "git",
@@ -47,13 +53,15 @@
47
53
  "devDependencies": {
48
54
  "@types/node": "^20.11.20",
49
55
  "@types/yargs": "^17.0.32",
56
+ "abcjs": "^6.6.2",
50
57
  "formidable": "^3.5.4",
51
58
  "jison": "^0.4.18",
59
+ "jsdom": "^29.0.2",
52
60
  "sha1": "^1.1.1",
53
61
  "ts-node": "^10.9.2",
62
+ "tsx": "^4.21.0",
54
63
  "typescript": "^5.3.3",
55
64
  "verovio": "^5.7.0",
56
- "xmldom": "^0.6.0",
57
65
  "yargs": "^17.7.2"
58
66
  },
59
67
  "dependencies": {
@@ -0,0 +1,97 @@
1
+ # ABC Grammar TODO
2
+
3
+ Issues found by comparing `abc.jison` against the abcjs parser implementation.
4
+
5
+ ---
6
+
7
+ ## High Priority
8
+
9
+ ### 1. Church modes in key signatures
10
+
11
+ `key_mode` only distinguishes `major` / `minor`. The `NAME` fallback does a naive
12
+ `startsWith("ma")` check, which mis-classifies all church modes:
13
+
14
+ - Dorian (`Dor`, `dorian`)
15
+ - Phrygian (`Phr`, `phrygian`)
16
+ - Lydian (`Lyd`, `lydian`)
17
+ - Mixolydian (`Mix`, `mixolydian`)
18
+ - Aeolian (`Aeo`, `aeolian`)
19
+ - Locrian (`Loc`, `locrian`)
20
+ - Scottish bagpipe: `HP`, `Hp`
21
+
22
+ These should be recognized as distinct modes rather than silently falling back to
23
+ major or minor. The `abcDecoder.ts` key-mapping logic will also need updating to
24
+ handle these modes.
25
+
26
+ ### 2. Bare-number Q: tempo (`Q:120`)
27
+
28
+ `numeric_tempo` only matches `frac '=' number` (e.g. `Q:1/4=120`). Two common
29
+ forms are not parsed:
30
+
31
+ - `Q:120` — plain BPM number (unit inferred from the current meter/L: value)
32
+ - `Q:"Allegro"` — text-only tempo marking
33
+ - `Q:"Allegro" 1/4=120` — combined text + numeric form
34
+
35
+ `Q:120` is the most frequently seen form in real-world ABC files.
36
+
37
+ ---
38
+
39
+ ## Medium Priority
40
+
41
+ ### 3. Missing rest type: `y` (spacer)
42
+
43
+ The `rest_phonet` rule covers `z`, `Z`, `x` but not `y` (an invisible spacer rest
44
+ used for spacing/layout). Files containing `y` currently cause a parse error.
45
+
46
+ ### 4. Volta ending bracket (`endEnding`)
47
+
48
+ The `bar` rule appends the ending number directly to the bar token string
49
+ (`'|' + N → "|1"`). There is no representation of the *closing* bracket of a
50
+ first-ending, so `[1 ... [2` style repeat structures cannot be round-tripped.
51
+ abcjs tracks `startEnding` / `endEnding` flags on bar elements; consider a
52
+ similar approach.
53
+
54
+ ---
55
+
56
+ ## Low Priority
57
+
58
+ ### 5. `~` maps to `mordent` instead of `irishroll`
59
+
60
+ `abc.jison` line 507: `'~' → articulation("mordent")`.
61
+ The standard ABC specification defines `~` as an *Irish roll* ornament, not a
62
+ mordent. abcjs uses the name `irishroll`. The mordent is correctly represented by
63
+ `M`. This is a semantic mismatch that may affect downstream rendering/export.
64
+
65
+ ### 6. Microtonal accidentals (`^/`, `_/`)
66
+
67
+ The `accidentals` rule does not cover quarter-tone accidentals:
68
+
69
+ - `^/` → quarter sharp
70
+ - `_/` → quarter flat
71
+
72
+ These are recognised by abcjs. Files using microtonal notation will silently drop
73
+ the accidental.
74
+
75
+ ### 7. Short trill decoration `t`
76
+
77
+ The single-letter `t` (half/short trill, `trillh` in abcjs) is not handled. The
78
+ current `P`/`PP` lexer patterns only match uppercase ornament letters
79
+ (`HJLMOPRSTuv`), so lowercase `t` falls through as an unknown token.
80
+
81
+ ### 8. Overlay voices (`&`)
82
+
83
+ The `&` operator (alternative voice within a single bar, same staff) is not
84
+ supported. abcjs resolves overlays into separate voices via `resolveOverlays()`.
85
+ This is a moderately common pattern in two-voice piano or lute transcriptions.
86
+
87
+ ---
88
+
89
+ ## Out of Scope (noted for awareness)
90
+
91
+ - **Lyrics** (`w:` / `W:` fields): not handled at the ABC grammar level; notes
92
+ have no `lyric` property. Would require grammar additions and decoder support.
93
+ - **Extended clef types**: only `treble`, `bass`, `tenor` are recognised. abcjs
94
+ also handles `alto`, `baritone`, `mezzo`, `soprano`, `tab`, `perc`, etc.
95
+ - **Decoration name aliases**: abcjs normalises `tr`→`trill`, `emphasis`→`accent`,
96
+ `marcato`→`umarcato`, `<`/`>`→`accent`. The lilylet grammar passes `NAME`
97
+ through as-is; the decoder would need to handle the aliases explicitly.
@@ -82,18 +82,24 @@
82
82
  const {patches} = body;
83
83
  const measures = [];
84
84
  let measure = null;
85
- let lastVoice = 1;
85
+ const seenVoices = new Set();
86
+
86
87
  patches.forEach(patch => {
87
88
  const voice = patch.control.V || 1;
88
- if (voice <= lastVoice) {
89
+ if (seenVoices.has(voice)) {
89
90
  if (measure)
90
91
  measures.push(measure);
91
92
  measure = {voices: []};
93
+ seenVoices.clear();
92
94
  }
95
+ if (!measure)
96
+ measure = {voices: []};
97
+ seenVoices.add(voice);
93
98
  measure.voices.push(patch);
94
99
  });
95
100
 
96
- measures.push(measure);
101
+ if (measure)
102
+ measures.push(measure);
97
103
 
98
104
  measures.forEach((measure, index) => measure.index = index + 1);
99
105
 
@@ -151,6 +157,21 @@
151
157
 
152
158
 
153
159
  const octaveShift = shift => ({octaveShift: shift});
160
+
161
+
162
+ const parseMode = (name) => {
163
+ const n = name.toLowerCase();
164
+ if (n.startsWith("ma")) return "major";
165
+ if (n === "m" || n.startsWith("mi")) return "minor";
166
+ if (n.startsWith("dor")) return "dorian";
167
+ if (n.startsWith("phr")) return "phrygian";
168
+ if (n.startsWith("lyd")) return "lydian";
169
+ if (n.startsWith("mix")) return "mixolydian";
170
+ if (n.startsWith("aeo")) return "aeolian";
171
+ if (n.startsWith("loc")) return "locrian";
172
+ if (n === "hp") return "highland";
173
+ return n;
174
+ };
154
175
  %}
155
176
 
156
177
 
@@ -160,24 +181,27 @@
160
181
 
161
182
  %x string
162
183
  %x comment
184
+ %x spec_comment_name
163
185
  %x spec_comment
186
+ %x spec_comment_skip
164
187
  %x title_string
165
188
  %x key_signature
189
+ %x voice_header
166
190
  %x exclamation_exp
167
191
 
168
-
169
192
  H \b[A-Z](?=\:[^|])
170
- A \b[A-G](?=[\W\d\sA-Ga-g_zHJLMOPRSTuv]*\b)
193
+ A \b[A-G](?=[\W\d\sA-Ga-g_yzHJLMOPRSTuv]*\b)
171
194
  Am \b[A-G](?=[m][a][j]|[m][i][n]\b)
172
- a \b[a-g](?=[\W\d\sA-Ga-g_zHJLMOPRSTuv]*\b)
195
+ a \b[a-g](?=[\W\d\sA-Ga-g_yzHJLMOPRSTuv]*\b)
173
196
  z \b[z]
174
197
  Z \b[Z]
175
198
  x \b[x](?=[\W\d\s])
199
+ y \b[y]
176
200
  N [0-9]
177
201
  P \b[HJLMOPRSTuv](?=[A-Ga-g][A-Ga-g0-9]*\b)
178
202
  PP \b[HJLMOPRSTuv](?=[xz!\[^_=\s"])
179
203
 
180
- SPECIAL [:!^_,'/<>={}()\[\]|.\-+~]
204
+ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~&]
181
205
 
182
206
 
183
207
  %%
@@ -193,10 +217,30 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~]
193
217
  <title_string>[^\n]+ return 'STR_CONTENT'
194
218
 
195
219
  ^[K][:][\s]* { this.pushState('key_signature'); return 'K:'; }
220
+ ^[V][:][ \t]* { this.pushState('voice_header'); return 'V:'; }
221
+ <voice_header>\" { this.pushState('string'); return 'STR_START'; }
222
+ <voice_header>[a-zA-Z][a-zA-Z0-9,]* return 'NAME';
223
+ <voice_header>[0-9]+ return 'N';
224
+ <voice_header>[=] return '=';
225
+ <voice_header>[+\-] return yytext;
226
+ <voice_header>[ \t]+ {}
227
+ <voice_header>\n { this.popState(); }
228
+ <voice_header>\] { this.popState(); return ']'; }
196
229
  <key_signature>"treble" return 'TREBLE';
197
230
  <key_signature>"bass" return 'BASS';
198
231
  <key_signature>"tenor" return 'TENOR';
232
+ <key_signature>"none" return 'NAME';
233
+ <key_signature>"Dor" return 'NAME';
234
+ <key_signature>"Phr" return 'NAME';
235
+ <key_signature>"Lyd" return 'NAME';
236
+ <key_signature>"Mix" return 'NAME';
237
+ <key_signature>"Aeo" return 'NAME';
238
+ <key_signature>"Loc" return 'NAME';
239
+ <key_signature>"HP" return 'NAME';
240
+ <key_signature>"Hp" return 'NAME';
241
+ <key_signature>[a-z]+[ \t]*=[^\n\]]* {}
199
242
  <key_signature>[A-G] return 'A';
243
+ <key_signature>[A-Z][a-z]+ return 'NAME';
200
244
  <key_signature>[b] return 'FLAT';
201
245
  <key_signature>[#] return 'SHARP';
202
246
  <key_signature>[m][a-z]* return 'NAME';
@@ -208,14 +252,20 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~]
208
252
  <key_signature>\] { this.popState(); return ']'; }
209
253
 
210
254
  ^[%] { this.pushState('comment'); }
211
- <comment>[%] { this.pushState('spec_comment'); }
255
+ <comment>[%] { this.pushState('spec_comment_name'); }
212
256
  <comment>[^\n]+ { return 'COMMENT'; }
213
- <spec_comment>\n { this.popState(); this.popState(); }
214
257
  <comment>\n { this.popState(); }
215
- <spec_comment>\s {}
216
- <spec_comment>"score" return 'SCORE'
217
- <spec_comment>[\w]+ return 'NN'
258
+ <spec_comment_name>[ \t]+ {}
259
+ <spec_comment_name>"score" { this.popState(); this.pushState('spec_comment'); return 'SCORE'; }
260
+ <spec_comment_name>"staves" { this.popState(); this.pushState('spec_comment'); return 'SCORE'; }
261
+ <spec_comment_name>[\w]+ { this.popState(); this.pushState('spec_comment_skip'); }
262
+ <spec_comment_name>\n { this.popState(); this.popState(); }
263
+ <spec_comment>[ \t]+ {}
218
264
  <spec_comment>[(){}\[\]|] return yytext
265
+ <spec_comment>[\w]+ return 'NN'
266
+ <spec_comment>\n { this.popState(); this.popState(); return 'LAYOUT_END'; }
267
+ <spec_comment_skip>[^\n]+ {}
268
+ <spec_comment_skip>\n { this.popState(); this.popState(); }
219
269
 
220
270
  [!] { this.pushState('exclamation_exp'); return '!'; }
221
271
  <exclamation_exp>[!] { this.popState(); return '!'; }
@@ -231,6 +281,7 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~]
231
281
  <exclamation_exp>{N} return 'N'
232
282
  <exclamation_exp>[a-zA-Z][\w-]* return 'NAME'
233
283
 
284
+ \\\n {}
234
285
  \s+ {}
235
286
 
236
287
  {SPECIAL} return yytext
@@ -250,6 +301,7 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~]
250
301
  "staff" return 'STAFF'
251
302
  "maj" return 'MAJ'
252
303
  "min" return 'MIN'
304
+ {y} return 'y'
253
305
  [a-zA-Z][\w-]* return 'NAME'
254
306
 
255
307
  <<EOF>> return 'EOF'
@@ -284,6 +336,7 @@ head_lines
284
336
  | head_lines head_line -> [...$1, $2]
285
337
  | head_lines comment -> [...$1, $2]
286
338
  | head_lines staff_layout_statement -> [...$1, $2]
339
+ | head_lines 'LAYOUT_END' -> $1
287
340
  | head_lines ']' -> $1
288
341
  | head_lines '}' -> $1
289
342
  | head_lines ')' -> $1
@@ -294,7 +347,7 @@ comment
294
347
  ;
295
348
 
296
349
  staff_layout_statement
297
- : 'SCORE' staff_layout -> $2
350
+ : 'SCORE' staff_layout 'LAYOUT_END' -> $2
298
351
  ;
299
352
 
300
353
  staff_layout
@@ -320,6 +373,7 @@ head_line
320
373
  : 'T:' string_content -> header('T', $2)
321
374
  | 'C:' string_content -> header('C', $2)
322
375
  | 'K:' key_signature -> header('K', $2)
376
+ | 'V:' header_value -> header('V', $2)
323
377
  | H ':' header_value -> header($1, $3)
324
378
  ;
325
379
 
@@ -328,6 +382,7 @@ header_value
328
382
  | number
329
383
  | frac
330
384
  | numeric_tempo
385
+ | string frac '=' number -> ({text: $1, note: $2, bpm: $4})
331
386
  | upper_phonet
332
387
  | voice_exp
333
388
  | staff_shift
@@ -342,6 +397,11 @@ staff_shift
342
397
  ;
343
398
 
344
399
  key_signature
400
+ : key_root -> $1
401
+ | NAME -> key(null, $1)
402
+ ;
403
+
404
+ key_root
345
405
  : A -> key($1, null)
346
406
  | A sharp_or_flat -> key($1 + $2, null)
347
407
  | A key_mode -> key($1, $2)
@@ -362,7 +422,7 @@ sharp_or_flat
362
422
  key_mode
363
423
  : MAJ -> 'major'
364
424
  | MIN -> 'minor'
365
- | NAME -> $1.startsWith("ma") ? "major" : "minor"
425
+ | NAME -> parseMode($1)
366
426
  ;
367
427
 
368
428
  plus_minus_number
@@ -402,6 +462,10 @@ voice_exp
402
462
  | number NAME assigns -> voice($1, $2, $3)
403
463
  | NAME -> voice(1, $1)
404
464
  | NAME assigns -> voice(1, $1, $2)
465
+ | upper_phonet number -> voice(1, $1 + String($2))
466
+ | upper_phonet number assigns -> voice(1, $1 + String($2), $3)
467
+ | upper_phonet number NAME -> voice(1, $1 + String($2))
468
+ | upper_phonet number NAME assigns -> voice(1, $1 + String($2), $4)
405
469
  ;
406
470
 
407
471
  assigns
@@ -418,6 +482,7 @@ assign_value
418
482
  | number
419
483
  | plus_minus_number
420
484
  | NAME
485
+ | upper_phonet
421
486
  ;
422
487
 
423
488
  upper_phonet
@@ -437,6 +502,9 @@ patches
437
502
  | patches tailless_patch -> [...$1, $2]
438
503
  | patches ']' -> $1
439
504
  | patches '}' -> $1
505
+ | patches head_line -> $1
506
+ | patches 'LAYOUT_END' -> $1
507
+ | patches '&' patch -> $1
440
508
  ;
441
509
 
442
510
  patch
@@ -459,10 +527,11 @@ bar
459
527
  | ':' '|' ']' -> ':|]'
460
528
  | '|' N -> '|' + $2
461
529
  | ':' '|' N -> ':|' + $2
530
+ | '&' -> '&'
462
531
  ;
463
532
 
464
533
  music
465
- : %empty
534
+ : %empty -> []
466
535
  | music expressive_mark -> $1 ? [...$1, $2] : [$2]
467
536
  | music text -> $1 ? [...$1, $2] : [$2]
468
537
  | music event -> $1 ? [...$1, $2] : [$2]
@@ -474,11 +543,13 @@ music
474
543
  | music NAME -> $1
475
544
  | music '^' NAME -> $1
476
545
  | music '^' -> $1
546
+ | music '[' N -> $1
477
547
  ;
478
548
 
479
549
  control
480
550
  : '[' H ':' header_value ']' -> ({control: header($2, $4)})
481
551
  | '[' 'K:' header_value ']' -> ({control: header("K", $3)})
552
+ | '[' 'V:' header_value ']' -> ({control: header("V", $3)})
482
553
  | '[' NAME ':' header_value ']' -> ({control: header($2, $4)})
483
554
  ;
484
555
 
@@ -510,6 +581,7 @@ articulation
510
581
  articulation_content
511
582
  : scope_articulation -> articulation($1)
512
583
  | scope_articulation parenthese -> articulation($1, $2)
584
+ | scope_articulation '=' assign_value -> articulation($1 + '=' + String($3))
513
585
  | DYNAMIC -> articulation($1)
514
586
  | a -> articulation($1)
515
587
  | "^" -> articulation($1)
@@ -613,6 +685,8 @@ accidentals
613
685
  | '^' '^' -> 2
614
686
  | '_' '_' -> -2
615
687
  | '=' '=' -> 0
688
+ | '^' '/' -> 0.5
689
+ | '_' '/' -> -0.5
616
690
  ;
617
691
 
618
692
  pitch
@@ -637,6 +711,7 @@ rest_phonet
637
711
  : z
638
712
  | Z
639
713
  | x
714
+ | y
640
715
  ;
641
716
 
642
717
  event