@k-l-lambda/lilylet 0.1.68 → 0.1.70
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/abc/abc.d.ts +1 -0
- package/lib/abc/grammar.jison.js +158 -135
- package/lib/lilylet/abcDecoder.js +219 -4
- package/lib/lilylet/grammar.jison.js +239 -207
- package/lib/lilylet/index.d.ts +1 -0
- package/lib/lilylet/index.js +1 -0
- package/lib/lilylet/meiEncoder.js +140 -35
- package/lib/lilylet/musicXmlDecoder.js +9 -0
- package/lib/lilylet/musicXmlEncoder.js +114 -0
- package/lib/lilylet/serializer.js +36 -6
- package/lib/lilylet/staffLayout.d.ts +57 -0
- package/lib/lilylet/staffLayout.js +226 -0
- package/lib/lilylet/types.d.ts +8 -0
- package/lib/staffLayout.d.ts +1 -0
- package/lib/staffLayout.js +1 -0
- package/package.json +1 -1
- package/source/abc/abc.jison +38 -13
- package/source/abc/abc.ts +1 -0
- package/source/abc/grammar.jison.js +158 -135
- package/source/lilylet/abcDecoder.ts +228 -4
- package/source/lilylet/grammar.jison.js +239 -207
- package/source/lilylet/index.ts +1 -0
- package/source/lilylet/lilylet.jison +28 -2
- package/source/lilylet/meiEncoder.ts +166 -34
- package/source/lilylet/musicXmlDecoder.ts +9 -0
- package/source/lilylet/musicXmlEncoder.ts +134 -0
- package/source/lilylet/serializer.ts +40 -6
- package/source/lilylet/staffLayout.ts +281 -0
- package/source/lilylet/types.ts +10 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staff-layout parser, ported from FindLab starry (app/staffLayout/).
|
|
3
|
+
*
|
|
4
|
+
* The layout string uses STAFF as the leaf unit (distinct from ABC's %%score,
|
|
5
|
+
* which is voice-leaf). Brackets group staves: `{}` = Brace (grand staff),
|
|
6
|
+
* `<>` = Bracket, `[]` = Square; conjunctions between consecutive staves:
|
|
7
|
+
* `,` = Blank, `-` = Solid, `.` = Dashed. Staff ids are [a-zA-Z_0-9]+; a slot
|
|
8
|
+
* with no id is an anonymous staff (auto-named "1","2",…).
|
|
9
|
+
*
|
|
10
|
+
* Example: "<[v1-v2].va> {pl-pr} <b>" → 6 staves v1,v2,va,pl,pr,b grouped as
|
|
11
|
+
* a Bracket over { a Square [v1-v2] dashed-joined to va }, a Brace {pl-pr},
|
|
12
|
+
* and a Bracket <b>.
|
|
13
|
+
*/
|
|
14
|
+
export var StaffGroupType;
|
|
15
|
+
(function (StaffGroupType) {
|
|
16
|
+
StaffGroupType[StaffGroupType["Default"] = 0] = "Default";
|
|
17
|
+
StaffGroupType[StaffGroupType["Brace"] = 1] = "Brace";
|
|
18
|
+
StaffGroupType[StaffGroupType["Bracket"] = 2] = "Bracket";
|
|
19
|
+
StaffGroupType[StaffGroupType["Square"] = 3] = "Square";
|
|
20
|
+
})(StaffGroupType || (StaffGroupType = {}));
|
|
21
|
+
export var StaffConjunctionType;
|
|
22
|
+
(function (StaffConjunctionType) {
|
|
23
|
+
StaffConjunctionType[StaffConjunctionType["Blank"] = 0] = "Blank";
|
|
24
|
+
StaffConjunctionType[StaffConjunctionType["Dashed"] = 1] = "Dashed";
|
|
25
|
+
StaffConjunctionType[StaffConjunctionType["Solid"] = 2] = "Solid";
|
|
26
|
+
})(StaffConjunctionType || (StaffConjunctionType = {}));
|
|
27
|
+
const singleGroup = (id) => ({ type: StaffGroupType.Default, staff: id });
|
|
28
|
+
const BOUNDS_TO_GROUPTYPE = {
|
|
29
|
+
"{": StaffGroupType.Brace,
|
|
30
|
+
"}": StaffGroupType.Brace,
|
|
31
|
+
"<": StaffGroupType.Bracket,
|
|
32
|
+
">": StaffGroupType.Bracket,
|
|
33
|
+
"[": StaffGroupType.Square,
|
|
34
|
+
"]": StaffGroupType.Square,
|
|
35
|
+
};
|
|
36
|
+
const OPEN_BOUNDS = "{<[";
|
|
37
|
+
const CLOSE_BOUNDS = "}>]";
|
|
38
|
+
const CONJUNCTIONS_MAP = {
|
|
39
|
+
",": StaffConjunctionType.Blank,
|
|
40
|
+
"-": StaffConjunctionType.Solid,
|
|
41
|
+
".": StaffConjunctionType.Dashed,
|
|
42
|
+
};
|
|
43
|
+
// MEI staffGrp @symbol by StaffGroupType (Default → none). MEI's allowed values
|
|
44
|
+
// are brace | bracket | bracketsq | line | none — note the square variant is
|
|
45
|
+
// "bracketsq", NOT "square" (the latter is MusicXML's <group-symbol> value).
|
|
46
|
+
const GROUP_SYMBOLS_MEI = [null, "brace", "bracket", "bracketsq"];
|
|
47
|
+
const randomB64 = () => {
|
|
48
|
+
const code = Buffer.from(Math.random().toString().slice(2)).toString("base64").replace(/=/g, "");
|
|
49
|
+
return code.split("").reverse().slice(0, 6).join("");
|
|
50
|
+
};
|
|
51
|
+
const makeUniqueName = (set, index, prefix) => {
|
|
52
|
+
let name = prefix;
|
|
53
|
+
if (!name)
|
|
54
|
+
name = index.toString();
|
|
55
|
+
else if (set.has(name))
|
|
56
|
+
name += "_" + index.toString();
|
|
57
|
+
while (set.has(name))
|
|
58
|
+
name = (prefix ? prefix + "_" : "") + randomB64();
|
|
59
|
+
return name;
|
|
60
|
+
};
|
|
61
|
+
// Tokenize a layout string into RawItem[] (one per staff slot). Lexer: whitespace
|
|
62
|
+
// skipped, single chars [-,.{}<>[]] are bounds/conjunctions, [a-zA-Z_0-9]+ is an id.
|
|
63
|
+
// An item accumulates leading open-bounds (leftBounds), an optional id, trailing
|
|
64
|
+
// close-bounds (rightBounds); a conjunction terminates the item and starts the next.
|
|
65
|
+
const tokenize = (code) => {
|
|
66
|
+
const tokens = code.match(/[A-Za-z0-9_]+|[-,.{}<>\[\]]/g) || [];
|
|
67
|
+
const items = [];
|
|
68
|
+
let cur = { id: null, leftBounds: [], rightBounds: [], conjunction: null };
|
|
69
|
+
let seenId = false; // id slot filled for the current item
|
|
70
|
+
let seenRight = false; // started collecting right bounds / closing
|
|
71
|
+
const pushItem = () => {
|
|
72
|
+
items.push(cur);
|
|
73
|
+
cur = { id: null, leftBounds: [], rightBounds: [], conjunction: null };
|
|
74
|
+
seenId = false;
|
|
75
|
+
seenRight = false;
|
|
76
|
+
};
|
|
77
|
+
for (const tok of tokens) {
|
|
78
|
+
if (tok in CONJUNCTIONS_MAP) {
|
|
79
|
+
cur.conjunction = tok;
|
|
80
|
+
pushItem();
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (OPEN_BOUNDS.includes(tok)) {
|
|
84
|
+
// An open bound after the id/closing starts a fresh item's left bounds.
|
|
85
|
+
if (seenId || seenRight)
|
|
86
|
+
pushItem();
|
|
87
|
+
cur.leftBounds.push(tok);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (CLOSE_BOUNDS.includes(tok)) {
|
|
91
|
+
cur.rightBounds.push(tok);
|
|
92
|
+
seenRight = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// id token
|
|
96
|
+
if (seenId || seenRight)
|
|
97
|
+
pushItem();
|
|
98
|
+
cur.id = tok;
|
|
99
|
+
seenId = true;
|
|
100
|
+
}
|
|
101
|
+
// Flush the final item if it carries any content.
|
|
102
|
+
if (cur.id !== null || cur.leftBounds.length || cur.rightBounds.length)
|
|
103
|
+
pushItem();
|
|
104
|
+
return items;
|
|
105
|
+
};
|
|
106
|
+
const makeGroupsFromRaw = (parent, seq) => {
|
|
107
|
+
let remains = seq;
|
|
108
|
+
while (remains.length) {
|
|
109
|
+
const word = remains.shift();
|
|
110
|
+
const bound = BOUNDS_TO_GROUPTYPE[word];
|
|
111
|
+
if (bound !== undefined) {
|
|
112
|
+
if (CLOSE_BOUNDS.includes(word) && bound === parent.type)
|
|
113
|
+
break;
|
|
114
|
+
if (OPEN_BOUNDS.includes(word)) {
|
|
115
|
+
const group = { type: bound, level: Number.isFinite(parent.level) ? parent.level + 1 : 0 };
|
|
116
|
+
remains = makeGroupsFromRaw(group, remains);
|
|
117
|
+
parent.subs = parent.subs || [];
|
|
118
|
+
parent.subs.push(group);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
parent.subs = parent.subs || [];
|
|
123
|
+
parent.subs.push(singleGroup(word));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
while (parent.type === StaffGroupType.Default && parent.subs && parent.subs.length === 1) {
|
|
127
|
+
const sub = parent.subs[0];
|
|
128
|
+
parent.type = sub.type;
|
|
129
|
+
parent.subs = sub.subs;
|
|
130
|
+
parent.staff = sub.staff;
|
|
131
|
+
parent.level = sub.level;
|
|
132
|
+
}
|
|
133
|
+
while (parent.subs && parent.subs.length === 1 && parent.subs[0].type === StaffGroupType.Default) {
|
|
134
|
+
const sub = parent.subs[0];
|
|
135
|
+
parent.subs = sub.subs;
|
|
136
|
+
parent.staff = sub.staff;
|
|
137
|
+
}
|
|
138
|
+
parent.grand = parent.type === StaffGroupType.Brace && !!parent.subs && parent.subs.every(sub => !!sub.staff);
|
|
139
|
+
return remains;
|
|
140
|
+
};
|
|
141
|
+
const groupHead = (group) => {
|
|
142
|
+
if (group.staff)
|
|
143
|
+
return group.staff;
|
|
144
|
+
else if (group.subs)
|
|
145
|
+
return groupHead(group.subs[0]);
|
|
146
|
+
};
|
|
147
|
+
const groupTail = (group) => {
|
|
148
|
+
if (group.staff)
|
|
149
|
+
return group.staff;
|
|
150
|
+
else if (group.subs)
|
|
151
|
+
return groupTail(group.subs[group.subs.length - 1]);
|
|
152
|
+
};
|
|
153
|
+
export const groupKey = (group) => {
|
|
154
|
+
if (group.staff)
|
|
155
|
+
return group.staff;
|
|
156
|
+
else if (group.subs)
|
|
157
|
+
return `${groupHead(group)}-${groupTail(group)}`;
|
|
158
|
+
};
|
|
159
|
+
const groupDict = (group, dict) => {
|
|
160
|
+
const key = groupKey(group);
|
|
161
|
+
if (key !== undefined)
|
|
162
|
+
dict[key] = group;
|
|
163
|
+
if (group.subs)
|
|
164
|
+
group.subs.forEach(sub => groupDict(sub, dict));
|
|
165
|
+
};
|
|
166
|
+
export class StaffLayout {
|
|
167
|
+
staffIds;
|
|
168
|
+
conjunctions;
|
|
169
|
+
group;
|
|
170
|
+
groups;
|
|
171
|
+
constructor(raw) {
|
|
172
|
+
// make unique ids (anonymous slots get "1","2",… ; named collisions disambiguated)
|
|
173
|
+
const ids = new Set();
|
|
174
|
+
raw.forEach((item, i) => {
|
|
175
|
+
item.id = makeUniqueName(ids, i + 1, item.id || undefined);
|
|
176
|
+
ids.add(item.id);
|
|
177
|
+
});
|
|
178
|
+
this.staffIds = raw.map(item => item.id);
|
|
179
|
+
this.conjunctions = raw.slice(0, raw.length - 1).map(item => item.conjunction ? CONJUNCTIONS_MAP[item.conjunction] : StaffConjunctionType.Blank);
|
|
180
|
+
// make groups
|
|
181
|
+
const seq = [].concat(...raw.map(item => [...item.leftBounds, item.id, ...item.rightBounds]));
|
|
182
|
+
this.group = { type: StaffGroupType.Default };
|
|
183
|
+
makeGroupsFromRaw(this.group, seq);
|
|
184
|
+
const dict = {};
|
|
185
|
+
groupDict(this.group, dict);
|
|
186
|
+
this.groups = Object.entries(dict).map(([key, group]) => {
|
|
187
|
+
let ids = key.split("-");
|
|
188
|
+
if (ids.length === 1)
|
|
189
|
+
ids = [ids[0], ids[0]];
|
|
190
|
+
const range = ids.map(id => this.staffIds.indexOf(id));
|
|
191
|
+
const cons = this.conjunctions.slice(range[0], range[1]);
|
|
192
|
+
const bar = cons.length ? Math.min(...cons) : 0;
|
|
193
|
+
group.key = key;
|
|
194
|
+
group.bar = bar;
|
|
195
|
+
return { group, range, key };
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
get stavesCount() {
|
|
199
|
+
return this.staffIds.length;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export const parseStaffLayout = (code) => new StaffLayout(tokenize(code));
|
|
203
|
+
// ── MEI staffGrp encoding (ported from FindLab staffLayout/encoding.js encodeMEI) ──
|
|
204
|
+
// Recursively emit nested <staffGrp> with symbol (brace/bracket/square) and bar.thru,
|
|
205
|
+
// with <staffDef n="..."> leaves keyed by staff index. nameDict maps a group key to a
|
|
206
|
+
// label. Returns the inner XML (no <scoreDef> wrapper); the caller positions it.
|
|
207
|
+
const bool = (x) => (x ? "true" : "false");
|
|
208
|
+
const stateMEIGroup = (statements, group, nameDict, ids, indent, tab) => {
|
|
209
|
+
const pad = tab.repeat(indent);
|
|
210
|
+
const name = group.key !== undefined ? nameDict[group.key] : undefined;
|
|
211
|
+
if (group.subs) {
|
|
212
|
+
const symbol = GROUP_SYMBOLS_MEI[group.type] ? ` symbol="${GROUP_SYMBOLS_MEI[group.type]}"` : "";
|
|
213
|
+
statements.push(`${pad}<staffGrp bar.thru="${bool((group.bar ?? 0) > 1)}"${symbol}>`);
|
|
214
|
+
if (name)
|
|
215
|
+
statements.push(`${pad}${tab}<label>${name}</label>`);
|
|
216
|
+
group.subs.forEach(sub => stateMEIGroup(statements, sub, nameDict, ids, indent + 1, tab));
|
|
217
|
+
statements.push(`${pad}</staffGrp>`);
|
|
218
|
+
}
|
|
219
|
+
if (group.staff)
|
|
220
|
+
statements.push(`${pad}<staffDef n="${ids.indexOf(group.staff) + 1}">`);
|
|
221
|
+
};
|
|
222
|
+
export const encodeStaffLayoutMEI = (layout, nameDict = {}, indent = 0, tab = "\t") => {
|
|
223
|
+
const statements = [];
|
|
224
|
+
stateMEIGroup(statements, layout.group, nameDict, layout.staffIds, indent, tab);
|
|
225
|
+
return statements.join("\n");
|
|
226
|
+
};
|
package/lib/lilylet/types.d.ts
CHANGED
|
@@ -239,8 +239,16 @@ export interface Metadata {
|
|
|
239
239
|
opus?: string;
|
|
240
240
|
instrument?: string;
|
|
241
241
|
genre?: string;
|
|
242
|
+
staves?: string;
|
|
243
|
+
instruments?: {
|
|
244
|
+
[key: string]: InstrumentName;
|
|
245
|
+
};
|
|
242
246
|
autoBeam?: 'auto' | 'on' | 'off';
|
|
243
247
|
}
|
|
248
|
+
export interface InstrumentName {
|
|
249
|
+
name: string;
|
|
250
|
+
shortName?: string;
|
|
251
|
+
}
|
|
244
252
|
export interface Part {
|
|
245
253
|
name?: string;
|
|
246
254
|
voices: Voice[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./lilylet/staffLayout.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./lilylet/staffLayout.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k-l-lambda/lilylet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.70",
|
|
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",
|
package/source/abc/abc.jison
CHANGED
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
|
|
38
38
|
const patch = (terms, bar) => {
|
|
39
39
|
const control = {};
|
|
40
|
-
terms.forEach(term => {
|
|
40
|
+
(terms || []).forEach(term => {
|
|
41
41
|
if (term.control)
|
|
42
42
|
control[term.control.name] = term.control.value;
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
return {
|
|
46
46
|
control,
|
|
47
|
-
terms,
|
|
47
|
+
terms: terms || [],
|
|
48
48
|
bar,
|
|
49
49
|
};
|
|
50
50
|
};
|
|
@@ -195,10 +195,10 @@ Am \b[A-G](?=[m][a][j]|[m][i][n]\b)
|
|
|
195
195
|
a \b[a-g](?=[\W\d\sA-Ga-g_yzHJLMOPRSTuv]*\b)
|
|
196
196
|
z \b[z]
|
|
197
197
|
Z \b[Z]
|
|
198
|
-
x \b[x](?=[\W\d\s])
|
|
198
|
+
x \b[x](?=[\W\d\s]|[_^=]*[A-Ga-g])
|
|
199
199
|
y \b[y]
|
|
200
200
|
N [0-9]
|
|
201
|
-
P \b[HJLMOPRSTuv](?=[A-Ga-g][A-Ga-g0-
|
|
201
|
+
P \b[HJLMOPRSTuv](?=[_^=]*[A-Ga-g][A-Ga-g0-9_^=,']*)
|
|
202
202
|
PP \b[HJLMOPRSTuv](?=[xz!\[^_=\s"])
|
|
203
203
|
|
|
204
204
|
SPECIAL [:!^_,'/<>={}()\[\]|.\-+~&]
|
|
@@ -211,8 +211,8 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~&]
|
|
|
211
211
|
<string>\\\" return 'STR_CONTENT'
|
|
212
212
|
<string>[^"]+ return 'STR_CONTENT'
|
|
213
213
|
|
|
214
|
-
^[T][:][\s]* { this.pushState('title_string'); return 'T:'; }
|
|
215
|
-
^[C][:][\s]* { this.pushState('title_string'); return 'C:'; }
|
|
214
|
+
^[T][:][\s]* { var pre = this.matched.slice(0, this.matched.length - yytext.length); if (pre === '' || pre.slice(-1) === '\n') { this.pushState('title_string'); return 'T:'; } this.unput(yytext.slice(1)); yytext = 'T'; return 'T'; }
|
|
215
|
+
^[C][:][\s]* { var pre = this.matched.slice(0, this.matched.length - yytext.length); if (pre === '' || pre.slice(-1) === '\n') { this.pushState('title_string'); return 'C:'; } this.unput(yytext.slice(1)); yytext = 'C'; return 'A'; }
|
|
216
216
|
<title_string>\n { this.popState(); }
|
|
217
217
|
<title_string>[^\n]+ return 'STR_CONTENT'
|
|
218
218
|
|
|
@@ -226,10 +226,10 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~&]
|
|
|
226
226
|
<voice_header>[ \t]+ {}
|
|
227
227
|
<voice_header>\n { this.popState(); }
|
|
228
228
|
<voice_header>\] { this.popState(); return ']'; }
|
|
229
|
-
<key_signature>"treble"
|
|
230
|
-
<key_signature>"bass"
|
|
231
|
-
<key_signature>"tenor"
|
|
232
|
-
<key_signature>"alto"
|
|
229
|
+
<key_signature>"treble"[0-9]* return 'TREBLE';
|
|
230
|
+
<key_signature>"bass"[0-9]* return 'BASS';
|
|
231
|
+
<key_signature>"tenor"[0-9]* return 'TENOR';
|
|
232
|
+
<key_signature>"alto"[0-9]* return 'ALTO';
|
|
233
233
|
<key_signature>"none" return 'NAME';
|
|
234
234
|
<key_signature>"Dor" return 'NAME';
|
|
235
235
|
<key_signature>"Phr" return 'NAME';
|
|
@@ -262,9 +262,10 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~&]
|
|
|
262
262
|
<spec_comment_name>[\w]+ { this.popState(); this.pushState('spec_comment_skip'); }
|
|
263
263
|
<spec_comment_name>\n { this.popState(); this.popState(); }
|
|
264
264
|
<spec_comment>[ \t]+ {}
|
|
265
|
-
<spec_comment>[([{]
|
|
265
|
+
<spec_comment>[([{] { this._scoreDepth = (this._scoreDepth || 0) + 1; return yytext; }
|
|
266
266
|
<spec_comment>[)\]}] { this._scoreDepth = (this._scoreDepth || 0) - 1; return yytext; }
|
|
267
267
|
<spec_comment>[|] return yytext
|
|
268
|
+
<spec_comment>[A-Z][:] { this.popState(); this.popState(); this.unput(yytext); return 'LAYOUT_END'; }
|
|
268
269
|
<spec_comment>[\w]+ return 'NN'
|
|
269
270
|
<spec_comment>\n { if (this._scoreDepth > 0) { /* layout continues on next line */ } else { this.popState(); this.popState(); return 'LAYOUT_END'; } }
|
|
270
271
|
<spec_comment_skip>[^\n]+ {}
|
|
@@ -360,7 +361,7 @@ staff_layout
|
|
|
360
361
|
staff_layout_items
|
|
361
362
|
: staff_layout_item -> [$1]
|
|
362
363
|
| staff_layout_items staff_layout_item -> [...$1, $2]
|
|
363
|
-
| staff_layout_items '|'
|
|
364
|
+
| staff_layout_items '|' { if ($1.length) $1[$1.length - 1].barThruAfter = true; $$ = $1; }
|
|
364
365
|
;
|
|
365
366
|
|
|
366
367
|
staff_layout_item
|
|
@@ -467,6 +468,7 @@ numeric_tempo
|
|
|
467
468
|
|
|
468
469
|
voice_exp
|
|
469
470
|
: number -> voice($1)
|
|
471
|
+
| number assigns -> voice($1, null, $2)
|
|
470
472
|
| number NAME -> voice($1, $2)
|
|
471
473
|
| number NAME assigns -> voice($1, $2, $3)
|
|
472
474
|
| number NAME plus_minus_number -> voice($1, $2 + ($3 < 0 ? '-' : '+') + Math.abs($3))
|
|
@@ -539,10 +541,18 @@ bar
|
|
|
539
541
|
| '|' ']' ':' -> '|]'
|
|
540
542
|
| ':' '|' ']' -> ':|]'
|
|
541
543
|
| '|' N -> '|' + $2
|
|
544
|
+
| '|' N volta_rest -> '|' + $2 + $3
|
|
542
545
|
| ':' '|' N -> ':|' + $2
|
|
546
|
+
| ':' '|' N volta_rest -> ':|' + $2 + $3
|
|
543
547
|
| '&' -> '&'
|
|
544
548
|
;
|
|
545
549
|
|
|
550
|
+
// Additional volta endings after the first number, comma-separated: "1,2", "1,2,3".
|
|
551
|
+
volta_rest
|
|
552
|
+
: ',' N -> ',' + $2
|
|
553
|
+
| volta_rest ',' N -> $1 + ',' + $3
|
|
554
|
+
;
|
|
555
|
+
|
|
546
556
|
music
|
|
547
557
|
: %empty -> []
|
|
548
558
|
| music expressive_mark -> $1 ? [...$1, $2] : [$2]
|
|
@@ -555,9 +565,17 @@ music
|
|
|
555
565
|
| music N -> $1
|
|
556
566
|
| music NAME -> $1
|
|
557
567
|
| music '^' NAME -> $1
|
|
568
|
+
| music '^' articulation_letter -> $1
|
|
558
569
|
| music '[' N -> $1
|
|
559
570
|
;
|
|
560
571
|
|
|
572
|
+
// Articulation-class letters (P macro: HJLMOPRSTuv). After a stray '^' inside a
|
|
573
|
+
// text annotation, a word like "Menuetto"/"Largo" starts with one of these and the
|
|
574
|
+
// lexer emits it as the articulation token, not NAME — swallow it like '^' NAME.
|
|
575
|
+
articulation_letter
|
|
576
|
+
: 'P' | 'T' | 'H' | 'J' | 'L' | 'M' | 'R' | 'O' | 'S' | 'u' | 'v'
|
|
577
|
+
;
|
|
578
|
+
|
|
561
579
|
control
|
|
562
580
|
: '[' H ':' header_value ']' -> ({control: header($2, $4)})
|
|
563
581
|
| '[' 'K:' header_value ']' -> ({control: header("K", $3)})
|
|
@@ -756,7 +774,14 @@ duration
|
|
|
756
774
|
: number '/' number -> frac(Number($1), Number($3))
|
|
757
775
|
| '/' number -> frac(1, Number($2))
|
|
758
776
|
| number -> frac(Number($1))
|
|
759
|
-
|
|
|
777
|
+
| slashes -> frac(1, Math.pow(2, $1))
|
|
778
|
+
| number slashes -> frac(Number($1), Math.pow(2, $2))
|
|
779
|
+
;
|
|
780
|
+
|
|
781
|
+
// One or more '/' halve the unit length each: '/'=1/2, '//'=1/4, '///'=1/8.
|
|
782
|
+
slashes
|
|
783
|
+
: '/' -> 1
|
|
784
|
+
| slashes '/' -> $1 + 1
|
|
760
785
|
;
|
|
761
786
|
|
|
762
787
|
broken_rhythm
|
package/source/abc/abc.ts
CHANGED