@dsebastien/music-notation 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +107 -2
- package/dist/THIRD_PARTY_LICENSES.md +57 -0
- package/dist/blocks/block-source.d.ts +12 -0
- package/dist/blocks/note-set-field.d.ts +8 -0
- package/dist/blocks/renderers.d.ts +16 -0
- package/dist/chord/guitar.d.ts +24 -0
- package/dist/chords/chart.d.ts +6 -0
- package/dist/chords/chordpro.d.ts +39 -0
- package/dist/chunks/index-b71brbpf.js +1028 -0
- package/dist/chunks/index-b71brbpf.js.map +23 -0
- package/dist/chunks/index-y8a78sfx.js +50 -0
- package/dist/chunks/index-y8a78sfx.js.map +10 -0
- package/dist/fretboard/fretboard.d.ts +10 -0
- package/dist/fretboard/tuning.d.ts +5 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +281 -0
- package/dist/index.js.map +10 -0
- package/dist/keyboard/keyboard.d.ts +14 -0
- package/dist/music-notation.css +190 -0
- package/dist/music-notation.publish.min.js +192 -0
- package/dist/publish/auto.d.ts +15 -0
- package/dist/publish/index.d.ts +5 -0
- package/dist/publish/index.js +81 -0
- package/dist/publish/index.js.map +10 -0
- package/dist/render/dom.d.ts +12 -0
- package/dist/render/render-block.d.ts +14 -0
- package/dist/styles.d.ts +4 -0
- package/dist/theory/chord-symbol.d.ts +10 -0
- package/dist/theory/note-set.d.ts +32 -0
- package/dist/theory/pitch.d.ts +27 -0
- package/dist/theory/transpose.d.ts +9 -0
- package/dist/voicings/index.d.ts +6 -0
- package/dist/voicings/index.js +78 -0
- package/dist/voicings/index.js.map +10 -0
- package/package.json +131 -5
|
@@ -0,0 +1,1028 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chromaOf2,
|
|
3
|
+
midiOf2,
|
|
4
|
+
nameOf2,
|
|
5
|
+
splitChordSymbol,
|
|
6
|
+
isFlat2
|
|
7
|
+
} from "./index-y8a78sfx.js";
|
|
8
|
+
|
|
9
|
+
// src/render/dom.ts
|
|
10
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
11
|
+
var apply = (el, attrs) => {
|
|
12
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
13
|
+
if (value !== undefined)
|
|
14
|
+
el.setAttribute(name, String(value));
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function svg(doc, tag, attrs = {}, children = []) {
|
|
18
|
+
const el = doc.createElementNS(SVG_NS, tag);
|
|
19
|
+
apply(el, attrs);
|
|
20
|
+
for (const child of children)
|
|
21
|
+
if (child)
|
|
22
|
+
el.appendChild(child);
|
|
23
|
+
return el;
|
|
24
|
+
}
|
|
25
|
+
function svgText(doc, text, attrs = {}) {
|
|
26
|
+
const el = svg(doc, "text", attrs);
|
|
27
|
+
el.textContent = text;
|
|
28
|
+
return el;
|
|
29
|
+
}
|
|
30
|
+
function html(doc, tag, cls, text) {
|
|
31
|
+
const el = doc.createElement(tag);
|
|
32
|
+
if (cls)
|
|
33
|
+
el.className = cls;
|
|
34
|
+
if (text !== undefined)
|
|
35
|
+
el.textContent = text;
|
|
36
|
+
return el;
|
|
37
|
+
}
|
|
38
|
+
function svgRoot(doc, width, height, cls, label) {
|
|
39
|
+
return svg(doc, "svg", {
|
|
40
|
+
xmlns: SVG_NS,
|
|
41
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
42
|
+
width,
|
|
43
|
+
class: `mn-svg ${cls}`,
|
|
44
|
+
role: "img",
|
|
45
|
+
"aria-label": label
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/chord/guitar.ts
|
|
50
|
+
var STANDARD_TUNING2 = [40, 45, 50, 55, 59, 64];
|
|
51
|
+
function parseFrets2(text) {
|
|
52
|
+
const trimmed = text.trim();
|
|
53
|
+
const tokens = /[\s,-]/.test(trimmed) ? trimmed.split(/[\s,-]+/) : [...trimmed];
|
|
54
|
+
const frets = tokens.filter(Boolean).map((t) => /^[xX]$/.test(t) ? null : /^\d+$/.test(t) ? Number(t) : NaN);
|
|
55
|
+
return frets.length >= 2 && frets.every((f) => f === null || Number.isInteger(f) && f <= 36) ? frets : null;
|
|
56
|
+
}
|
|
57
|
+
function parseFingers2(text) {
|
|
58
|
+
const trimmed = text.trim();
|
|
59
|
+
const tokens = /[\s,]/.test(trimmed) || trimmed.includes("-", 1) ? trimmed.split(/[\s,-]+/) : [...trimmed];
|
|
60
|
+
return tokens.filter(Boolean).map((t) => /^[0xX]$/.test(t) ? null : t.toUpperCase());
|
|
61
|
+
}
|
|
62
|
+
function baseFretOf2(frets) {
|
|
63
|
+
const fretted = frets.filter((f) => f !== null && f > 0);
|
|
64
|
+
if (!fretted.length || Math.max(...fretted) <= 4)
|
|
65
|
+
return 1;
|
|
66
|
+
return Math.min(...fretted);
|
|
67
|
+
}
|
|
68
|
+
function barresOf(voicing) {
|
|
69
|
+
const byKey = new Map;
|
|
70
|
+
voicing.frets.forEach((fret, string) => {
|
|
71
|
+
const finger = voicing.fingers[string];
|
|
72
|
+
if (fret === null || fret === 0 || !finger)
|
|
73
|
+
return;
|
|
74
|
+
const key = `${finger}@${fret}`;
|
|
75
|
+
byKey.set(key, [...byKey.get(key) ?? [], string]);
|
|
76
|
+
});
|
|
77
|
+
const barres = [];
|
|
78
|
+
for (const [key, strings] of byKey) {
|
|
79
|
+
if (strings.length < 2)
|
|
80
|
+
continue;
|
|
81
|
+
const [finger, fret] = key.split("@");
|
|
82
|
+
barres.push({
|
|
83
|
+
fret: Number(fret),
|
|
84
|
+
from: Math.min(...strings),
|
|
85
|
+
to: Math.max(...strings),
|
|
86
|
+
finger
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return barres;
|
|
90
|
+
}
|
|
91
|
+
var SPACING = 18;
|
|
92
|
+
var ROW = 20;
|
|
93
|
+
var LEFT = 30;
|
|
94
|
+
var TITLE = 22;
|
|
95
|
+
var MARKERS = 16;
|
|
96
|
+
function renderGuitarDiagram2(doc, voicing, options = {}) {
|
|
97
|
+
const strings = voicing.frets.length;
|
|
98
|
+
const base = voicing.baseFret ?? baseFretOf2(voicing.frets);
|
|
99
|
+
const fretted = voicing.frets.filter((f) => f !== null && f > 0);
|
|
100
|
+
const rows = Math.max(4, fretted.length ? Math.max(...fretted) - base + 1 : 0);
|
|
101
|
+
const top = (options.title ? TITLE : 4) + MARKERS;
|
|
102
|
+
const width = LEFT + (strings - 1) * SPACING + 14;
|
|
103
|
+
const height = top + rows * ROW + 10;
|
|
104
|
+
const x = (string) => LEFT + string * SPACING;
|
|
105
|
+
const y = (row) => top + row * ROW;
|
|
106
|
+
const label = options.title ? `${options.title} chord diagram` : "Chord diagram";
|
|
107
|
+
const root = svgRoot(doc, width, height, "mn-guitar-chord", label);
|
|
108
|
+
if (options.title) {
|
|
109
|
+
root.appendChild(svgText(doc, options.title, {
|
|
110
|
+
class: "mn-title",
|
|
111
|
+
x: width / 2,
|
|
112
|
+
y: 15,
|
|
113
|
+
"text-anchor": "middle"
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
for (let row = 0;row <= rows; row++) {
|
|
117
|
+
root.appendChild(svg(doc, "line", {
|
|
118
|
+
class: row === 0 && base === 1 ? "mn-nut" : "mn-fret",
|
|
119
|
+
x1: x(0),
|
|
120
|
+
x2: x(strings - 1),
|
|
121
|
+
y1: y(row),
|
|
122
|
+
y2: y(row)
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
for (let s = 0;s < strings; s++) {
|
|
126
|
+
root.appendChild(svg(doc, "line", { class: "mn-string", x1: x(s), x2: x(s), y1: y(0), y2: y(rows) }));
|
|
127
|
+
}
|
|
128
|
+
if (base > 1) {
|
|
129
|
+
root.appendChild(svgText(doc, `${base}fr`, {
|
|
130
|
+
class: "mn-base-fret",
|
|
131
|
+
x: LEFT - 10,
|
|
132
|
+
y: y(0.5) + 4,
|
|
133
|
+
"text-anchor": "end"
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
for (const barre of barresOf(voicing)) {
|
|
137
|
+
const cy = y(barre.fret - base + 0.5);
|
|
138
|
+
root.appendChild(svg(doc, "rect", {
|
|
139
|
+
class: "mn-barre",
|
|
140
|
+
x: x(barre.from) - 7,
|
|
141
|
+
y: cy - 7,
|
|
142
|
+
width: x(barre.to) - x(barre.from) + 14,
|
|
143
|
+
height: 14,
|
|
144
|
+
rx: 7
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
const tuning = options.tuning ?? (strings === 6 ? STANDARD_TUNING2 : undefined);
|
|
148
|
+
voicing.frets.forEach((fret, s) => {
|
|
149
|
+
const cx = x(s);
|
|
150
|
+
if (fret === null) {
|
|
151
|
+
root.appendChild(svgText(doc, "×", {
|
|
152
|
+
class: "mn-muted",
|
|
153
|
+
x: cx,
|
|
154
|
+
y: top - 4,
|
|
155
|
+
"text-anchor": "middle"
|
|
156
|
+
}));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const open = tuning?.[s];
|
|
160
|
+
const isRoot = options.root !== undefined && options.root !== null && open !== undefined && (open + fret) % 12 === options.root;
|
|
161
|
+
if (fret === 0) {
|
|
162
|
+
root.appendChild(svg(doc, "circle", {
|
|
163
|
+
class: `mn-open${isRoot ? " mn-root" : ""}`,
|
|
164
|
+
cx,
|
|
165
|
+
cy: top - 8,
|
|
166
|
+
r: 4
|
|
167
|
+
}));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const cy = y(fret - base + 0.5);
|
|
171
|
+
root.appendChild(svg(doc, "circle", { class: `mn-dot${isRoot ? " mn-root" : ""}`, cx, cy, r: 7 }));
|
|
172
|
+
const finger = voicing.fingers[s];
|
|
173
|
+
if (finger) {
|
|
174
|
+
root.appendChild(svgText(doc, finger, {
|
|
175
|
+
class: "mn-dot-text",
|
|
176
|
+
x: cx,
|
|
177
|
+
y: cy + 4,
|
|
178
|
+
"text-anchor": "middle"
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
return root;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/chords/chordpro.ts
|
|
186
|
+
var ALIASES = {
|
|
187
|
+
t: "title",
|
|
188
|
+
st: "subtitle",
|
|
189
|
+
c: "comment",
|
|
190
|
+
ci: "comment",
|
|
191
|
+
comment_italic: "comment",
|
|
192
|
+
cb: "comment",
|
|
193
|
+
comment_box: "comment",
|
|
194
|
+
highlight: "comment",
|
|
195
|
+
soc: "start_of_chorus",
|
|
196
|
+
eoc: "end_of_chorus",
|
|
197
|
+
sov: "start_of_verse",
|
|
198
|
+
eov: "end_of_verse",
|
|
199
|
+
sob: "start_of_bridge",
|
|
200
|
+
eob: "end_of_bridge",
|
|
201
|
+
sot: "start_of_tab",
|
|
202
|
+
eot: "end_of_tab",
|
|
203
|
+
sog: "start_of_grid",
|
|
204
|
+
eog: "end_of_grid"
|
|
205
|
+
};
|
|
206
|
+
var SECTION_KINDS = new Set(["verse", "chorus", "bridge", "tab", "grid"]);
|
|
207
|
+
function parseLyrics2(line) {
|
|
208
|
+
const segments = [];
|
|
209
|
+
const re = /\[([^\]]*)\]/g;
|
|
210
|
+
let last = 0;
|
|
211
|
+
let chord = null;
|
|
212
|
+
let m;
|
|
213
|
+
while (m = re.exec(line)) {
|
|
214
|
+
const text = line.slice(last, m.index);
|
|
215
|
+
if (text || chord !== null)
|
|
216
|
+
segments.push({ chord, text });
|
|
217
|
+
chord = m[1].trim();
|
|
218
|
+
last = m.index + m[0].length;
|
|
219
|
+
}
|
|
220
|
+
const rest = line.slice(last);
|
|
221
|
+
if (rest || chord !== null)
|
|
222
|
+
segments.push({ chord, text: rest });
|
|
223
|
+
return segments;
|
|
224
|
+
}
|
|
225
|
+
function parseDirective(line) {
|
|
226
|
+
const trimmed = line.trim();
|
|
227
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
|
|
228
|
+
return null;
|
|
229
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
230
|
+
const m = /^[\w-]+/.exec(inner);
|
|
231
|
+
if (!m)
|
|
232
|
+
return null;
|
|
233
|
+
const rest = inner.slice(m[0].length);
|
|
234
|
+
if (rest && !/^[\s:]/.test(rest))
|
|
235
|
+
return null;
|
|
236
|
+
return { name: m[0].toLowerCase(), value: rest.replace(/^\s*:?/, "").trim() };
|
|
237
|
+
}
|
|
238
|
+
function parseChordPro2(source) {
|
|
239
|
+
const song = {
|
|
240
|
+
title: null,
|
|
241
|
+
subtitle: null,
|
|
242
|
+
artist: null,
|
|
243
|
+
key: null,
|
|
244
|
+
capo: null,
|
|
245
|
+
tempo: null,
|
|
246
|
+
time: null,
|
|
247
|
+
transpose: 0,
|
|
248
|
+
sections: []
|
|
249
|
+
};
|
|
250
|
+
let section = { kind: "none", label: null, lines: [] };
|
|
251
|
+
const open = (kind, label) => {
|
|
252
|
+
if (section.lines.length || section.kind !== "none")
|
|
253
|
+
song.sections.push(section);
|
|
254
|
+
section = { kind, label, lines: [] };
|
|
255
|
+
};
|
|
256
|
+
for (const raw of source.split(/\r?\n/)) {
|
|
257
|
+
const line = raw.replace(/\s+$/, "");
|
|
258
|
+
if (/^\s*#/.test(line))
|
|
259
|
+
continue;
|
|
260
|
+
const directive = parseDirective(line);
|
|
261
|
+
if (directive) {
|
|
262
|
+
const { name, value } = directive;
|
|
263
|
+
const key = ALIASES[name] ?? name;
|
|
264
|
+
const start = /^start_of_(\w+)$/.exec(key);
|
|
265
|
+
const end = /^end_of_(\w+)$/.exec(key);
|
|
266
|
+
if (start) {
|
|
267
|
+
const kind = SECTION_KINDS.has(start[1]) ? start[1] : "verse";
|
|
268
|
+
open(kind, value.replace(/^label\s*=\s*"?|"$/g, "") || null);
|
|
269
|
+
} else if (end) {
|
|
270
|
+
open("none", null);
|
|
271
|
+
} else if (key === "comment") {
|
|
272
|
+
section.lines.push({ kind: "comment", text: value });
|
|
273
|
+
} else if (key === "title" || key === "subtitle" || key === "artist" || key === "key" || key === "tempo" || key === "time") {
|
|
274
|
+
song[key] = value || null;
|
|
275
|
+
} else if (key === "capo") {
|
|
276
|
+
const capo = Number(value);
|
|
277
|
+
song.capo = Number.isInteger(capo) && capo > 0 ? capo : null;
|
|
278
|
+
} else if (key === "transpose") {
|
|
279
|
+
const semitones = Number(value);
|
|
280
|
+
if (Number.isInteger(semitones))
|
|
281
|
+
song.transpose = semitones;
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (!line.trim())
|
|
286
|
+
section.lines.push({ kind: "empty" });
|
|
287
|
+
else if (section.kind === "tab" || section.kind === "grid")
|
|
288
|
+
section.lines.push({ kind: "text", text: line });
|
|
289
|
+
else
|
|
290
|
+
section.lines.push({ kind: "lyrics", segments: parseLyrics2(line) });
|
|
291
|
+
}
|
|
292
|
+
open("none", null);
|
|
293
|
+
for (const s of song.sections) {
|
|
294
|
+
while (s.lines[0]?.kind === "empty")
|
|
295
|
+
s.lines.shift();
|
|
296
|
+
while (s.lines[s.lines.length - 1]?.kind === "empty")
|
|
297
|
+
s.lines.pop();
|
|
298
|
+
}
|
|
299
|
+
song.sections = song.sections.filter((s) => s.lines.length || s.label);
|
|
300
|
+
return song;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/theory/transpose.ts
|
|
304
|
+
var FLAT_KEYS = new Set([5, 10, 3, 8, 1]);
|
|
305
|
+
var FLAT_MINOR_KEYS = new Set([2, 7, 0, 5, 10, 3]);
|
|
306
|
+
function keyPrefersFlats2(key) {
|
|
307
|
+
const m = /^([A-G][#♯b♭]?)(m|min|minor)?\b/.exec(key.trim());
|
|
308
|
+
if (!m)
|
|
309
|
+
return false;
|
|
310
|
+
const chroma = chromaOf2(m[1]);
|
|
311
|
+
if (chroma === null)
|
|
312
|
+
return false;
|
|
313
|
+
if (isFlat2(m[1]))
|
|
314
|
+
return true;
|
|
315
|
+
return m[2] ? FLAT_MINOR_KEYS.has(chroma) : FLAT_KEYS.has(chroma);
|
|
316
|
+
}
|
|
317
|
+
var shift = (note, semitones, preferFlats) => {
|
|
318
|
+
const chroma = chromaOf2(note);
|
|
319
|
+
return chroma === null ? note : nameOf2(chroma + semitones, preferFlats);
|
|
320
|
+
};
|
|
321
|
+
function transposeChord2(symbol, semitones, preferFlats) {
|
|
322
|
+
const trimmed = symbol.trim();
|
|
323
|
+
const parts = splitChordSymbol(trimmed);
|
|
324
|
+
const steps = Math.round(semitones) || 0;
|
|
325
|
+
if (!parts || steps % 12 === 0)
|
|
326
|
+
return trimmed;
|
|
327
|
+
const flats = preferFlats ?? isFlat2(parts.root);
|
|
328
|
+
const bass = parts.bass ? `/${shift(parts.bass, steps, flats)}` : "";
|
|
329
|
+
return `${shift(parts.root, steps, flats)}${parts.suffix}${bass}`;
|
|
330
|
+
}
|
|
331
|
+
function transposeKey2(key, semitones) {
|
|
332
|
+
const m = /^([A-G][#♯b♭]?)(.*)$/.exec(key.trim());
|
|
333
|
+
const chroma = m ? chromaOf2(m[1]) : null;
|
|
334
|
+
const steps = Math.round(semitones) || 0;
|
|
335
|
+
if (!m || chroma === null || steps % 12 === 0)
|
|
336
|
+
return key.trim();
|
|
337
|
+
const target = ((chroma + steps) % 12 + 12) % 12;
|
|
338
|
+
const minor = /^\s*(m|min|minor)\b/.test(m[2]);
|
|
339
|
+
const flats = minor ? FLAT_MINOR_KEYS.has(target) : FLAT_KEYS.has(target);
|
|
340
|
+
return `${nameOf2(target, flats)}${m[2]}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/chords/chart.ts
|
|
344
|
+
var SECTION_LABELS = {
|
|
345
|
+
chorus: "Chorus",
|
|
346
|
+
bridge: "Bridge",
|
|
347
|
+
tab: "Tab",
|
|
348
|
+
grid: "Grid"
|
|
349
|
+
};
|
|
350
|
+
function renderChart2(doc, song, options = {}) {
|
|
351
|
+
const extra = options.transpose ?? 0;
|
|
352
|
+
const semitones = song.transpose + (Number.isFinite(extra) ? Math.round(extra) : 0);
|
|
353
|
+
const key = song.key ? transposeKey2(song.key, semitones) : null;
|
|
354
|
+
const flats = key ? keyPrefersFlats2(key) : undefined;
|
|
355
|
+
const chord = (symbol) => transposeChord2(symbol, semitones, flats);
|
|
356
|
+
const root = html(doc, "div", "mn-chart");
|
|
357
|
+
if (song.title || song.subtitle || song.artist) {
|
|
358
|
+
const header = html(doc, "div", "mn-chart-header");
|
|
359
|
+
if (song.title)
|
|
360
|
+
header.appendChild(html(doc, "div", "mn-chart-title", song.title));
|
|
361
|
+
const by = [song.subtitle, song.artist].filter(Boolean).join(" · ");
|
|
362
|
+
if (by)
|
|
363
|
+
header.appendChild(html(doc, "div", "mn-chart-subtitle", by));
|
|
364
|
+
root.appendChild(header);
|
|
365
|
+
}
|
|
366
|
+
const meta = [
|
|
367
|
+
key ? `Key ${key}` : null,
|
|
368
|
+
song.capo ? `Capo ${song.capo}` : null,
|
|
369
|
+
song.tempo ? `${song.tempo} BPM` : null,
|
|
370
|
+
song.time ? song.time : null,
|
|
371
|
+
semitones % 12 ? `Transposed ${semitones > 0 ? "+" : ""}${semitones}` : null
|
|
372
|
+
].filter((m) => m !== null);
|
|
373
|
+
if (meta.length)
|
|
374
|
+
root.appendChild(html(doc, "div", "mn-chart-meta", meta.join(" · ")));
|
|
375
|
+
for (const section of song.sections) {
|
|
376
|
+
const el = html(doc, "div", `mn-section mn-section-${section.kind}`);
|
|
377
|
+
const label = section.label ?? SECTION_LABELS[section.kind];
|
|
378
|
+
if (label)
|
|
379
|
+
el.appendChild(html(doc, "div", "mn-section-label", label));
|
|
380
|
+
for (const line of section.lines) {
|
|
381
|
+
if (line.kind === "empty")
|
|
382
|
+
el.appendChild(html(doc, "div", "mn-line mn-line-empty"));
|
|
383
|
+
else if (line.kind === "comment")
|
|
384
|
+
el.appendChild(html(doc, "div", "mn-line mn-comment", line.text));
|
|
385
|
+
else if (line.kind === "text")
|
|
386
|
+
el.appendChild(html(doc, "pre", "mn-line mn-pre", line.text));
|
|
387
|
+
else {
|
|
388
|
+
const row = html(doc, "div", "mn-line");
|
|
389
|
+
const chordsOnly = line.segments.every((s) => !s.text.trim());
|
|
390
|
+
if (chordsOnly)
|
|
391
|
+
row.classList.add("mn-chords-only");
|
|
392
|
+
for (const segment of line.segments) {
|
|
393
|
+
const seg = html(doc, "span", "mn-seg");
|
|
394
|
+
seg.appendChild(html(doc, "span", "mn-chord", segment.chord === null ? "" : chord(segment.chord)));
|
|
395
|
+
seg.appendChild(html(doc, "span", "mn-lyric", segment.text || (chordsOnly ? "" : " ")));
|
|
396
|
+
row.appendChild(seg);
|
|
397
|
+
}
|
|
398
|
+
el.appendChild(row);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
root.appendChild(el);
|
|
402
|
+
}
|
|
403
|
+
return root;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/theory/note-set.ts
|
|
407
|
+
import { Chord, Scale } from "tonal";
|
|
408
|
+
var memberFor = (set, chroma) => set.members.find((m) => m.chroma === chroma);
|
|
409
|
+
var memberOf2 = (set, midi) => set.pitches && !set.pitches.includes(midi) ? undefined : memberFor(set, (midi % 12 + 12) % 12);
|
|
410
|
+
function intervalLabel2(interval) {
|
|
411
|
+
const m = /^(\d+)([PMmdA]+)$/.exec(interval);
|
|
412
|
+
if (!m)
|
|
413
|
+
return interval;
|
|
414
|
+
const [, number, quality] = m;
|
|
415
|
+
if (number === "1" && quality === "P")
|
|
416
|
+
return "R";
|
|
417
|
+
const perfect = ["1", "4", "5", "8", "11", "12"].includes(String((Number(number) - 1) % 7 + 1));
|
|
418
|
+
if (quality === "P" || quality === "M")
|
|
419
|
+
return number;
|
|
420
|
+
if (quality === "m")
|
|
421
|
+
return `b${number}`;
|
|
422
|
+
if (quality.startsWith("A"))
|
|
423
|
+
return `${"#".repeat(quality.length)}${number}`;
|
|
424
|
+
return `${"b".repeat(quality.length + (perfect ? 0 : 1))}${number}`;
|
|
425
|
+
}
|
|
426
|
+
function labelOf2(member, label, finger) {
|
|
427
|
+
switch (label) {
|
|
428
|
+
case "none":
|
|
429
|
+
return "";
|
|
430
|
+
case "degrees":
|
|
431
|
+
return member.degree === null ? member.name : String(member.degree);
|
|
432
|
+
case "intervals":
|
|
433
|
+
return member.interval === null ? member.name : intervalLabel2(member.interval);
|
|
434
|
+
case "fingers":
|
|
435
|
+
return finger ?? "";
|
|
436
|
+
default:
|
|
437
|
+
return member.name;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
var fromNames = (title, names, intervals, chord = false) => {
|
|
441
|
+
const members = [];
|
|
442
|
+
names.forEach((name, i) => {
|
|
443
|
+
const chroma = chromaOf2(name);
|
|
444
|
+
if (chroma === null || members.some((m) => m.chroma === chroma))
|
|
445
|
+
return;
|
|
446
|
+
members.push({
|
|
447
|
+
chroma,
|
|
448
|
+
name,
|
|
449
|
+
degree: intervals ? chord ? Number(/^\d+/.exec(intervals[i] ?? "")?.[0] ?? i + 1) : i + 1 : null,
|
|
450
|
+
interval: intervals?.[i] ?? null
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
if (!members.length)
|
|
454
|
+
return null;
|
|
455
|
+
return {
|
|
456
|
+
title,
|
|
457
|
+
members,
|
|
458
|
+
root: intervals ? members[0].chroma : null,
|
|
459
|
+
pitches: null,
|
|
460
|
+
preferFlats: names.some(isFlat2)
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
function chordNoteSet2(symbol) {
|
|
464
|
+
const parts = splitChordSymbol(symbol);
|
|
465
|
+
if (!parts)
|
|
466
|
+
return null;
|
|
467
|
+
const chord = Chord.get(parts.root + parts.suffix);
|
|
468
|
+
if (chord.empty)
|
|
469
|
+
return null;
|
|
470
|
+
const bass = parts.bass;
|
|
471
|
+
const set = fromNames(symbol.trim(), chord.notes, chord.intervals, true);
|
|
472
|
+
const bassChroma = bass ? chromaOf2(bass) : null;
|
|
473
|
+
if (set && bass && bassChroma !== null && !memberFor(set, bassChroma)) {
|
|
474
|
+
set.members.push({ chroma: bassChroma, name: bass, degree: null, interval: null });
|
|
475
|
+
}
|
|
476
|
+
return set;
|
|
477
|
+
}
|
|
478
|
+
function scaleNoteSet2(name) {
|
|
479
|
+
const scale = Scale.get(name.trim());
|
|
480
|
+
if (scale.empty || !scale.tonic)
|
|
481
|
+
return null;
|
|
482
|
+
return fromNames(name.trim(), scale.notes, scale.intervals);
|
|
483
|
+
}
|
|
484
|
+
function listNoteSet2(text) {
|
|
485
|
+
const tokens = text.split(/[\s,]+/).filter(Boolean);
|
|
486
|
+
if (!tokens.length)
|
|
487
|
+
return null;
|
|
488
|
+
const withOctaves = tokens.every((t) => midiOf2(t) !== null);
|
|
489
|
+
const names = withOctaves ? tokens.map((t) => t.replace(/-?\d$/, "")) : tokens;
|
|
490
|
+
if (!withOctaves && names.some((n) => chromaOf2(n) === null))
|
|
491
|
+
return null;
|
|
492
|
+
const set = fromNames(tokens.join(" "), names, null);
|
|
493
|
+
if (set && withOctaves)
|
|
494
|
+
set.pitches = tokens.map((t) => midiOf2(t));
|
|
495
|
+
return set;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/fretboard/fretboard.ts
|
|
499
|
+
var STRING_GAP = 20;
|
|
500
|
+
var FRET_W = 44;
|
|
501
|
+
var OPEN_W = 30;
|
|
502
|
+
var LEFT2 = 24;
|
|
503
|
+
var TITLE2 = 22;
|
|
504
|
+
var BOTTOM = 22;
|
|
505
|
+
var SINGLE_INLAYS = new Set([3, 5, 7, 9, 15, 17, 19, 21]);
|
|
506
|
+
function renderFretboard2(doc, set, options = {}) {
|
|
507
|
+
const tuning = options.tuning ?? STANDARD_TUNING2;
|
|
508
|
+
const [from, to] = options.frets ?? [0, 12];
|
|
509
|
+
const first = Math.max(from, 1);
|
|
510
|
+
const strings = tuning.length;
|
|
511
|
+
const label = options.label ?? "notes";
|
|
512
|
+
const top = (options.title ? TITLE2 : 0) + 12;
|
|
513
|
+
const openW = from === 0 ? OPEN_W : 0;
|
|
514
|
+
const nutX = LEFT2 + openW;
|
|
515
|
+
const width = nutX + (to - first + 1) * FRET_W + 8;
|
|
516
|
+
const height = top + (strings - 1) * STRING_GAP + BOTTOM + 8;
|
|
517
|
+
const y = (row) => top + row * STRING_GAP;
|
|
518
|
+
const wireX = (fret) => nutX + (fret - first + 1) * FRET_W;
|
|
519
|
+
const centerX = (fret) => fret === 0 ? LEFT2 + OPEN_W / 2 : wireX(fret) - FRET_W / 2;
|
|
520
|
+
const root = svgRoot(doc, width, height, "mn-fretboard", options.title ?? `Fretboard: ${set.title}`);
|
|
521
|
+
if (options.title) {
|
|
522
|
+
root.appendChild(svgText(doc, options.title, {
|
|
523
|
+
class: "mn-title",
|
|
524
|
+
x: width / 2,
|
|
525
|
+
y: 15,
|
|
526
|
+
"text-anchor": "middle"
|
|
527
|
+
}));
|
|
528
|
+
}
|
|
529
|
+
const middle = y((strings - 1) / 2);
|
|
530
|
+
for (let fret = first;fret <= to; fret++) {
|
|
531
|
+
const cx = wireX(fret) - FRET_W / 2;
|
|
532
|
+
if (SINGLE_INLAYS.has(fret))
|
|
533
|
+
root.appendChild(svg(doc, "circle", { class: "mn-inlay", cx, cy: middle, r: 4 }));
|
|
534
|
+
if (fret % 12 === 0) {
|
|
535
|
+
root.appendChild(svg(doc, "circle", { class: "mn-inlay", cx, cy: y(strings > 3 ? 1.5 : 0.5), r: 4 }));
|
|
536
|
+
root.appendChild(svg(doc, "circle", {
|
|
537
|
+
class: "mn-inlay",
|
|
538
|
+
cx,
|
|
539
|
+
cy: y(strings > 3 ? strings - 2.5 : strings - 1.5),
|
|
540
|
+
r: 4
|
|
541
|
+
}));
|
|
542
|
+
}
|
|
543
|
+
root.appendChild(svgText(doc, String(fret), {
|
|
544
|
+
class: "mn-fret-number",
|
|
545
|
+
x: cx,
|
|
546
|
+
y: y(strings - 1) + BOTTOM,
|
|
547
|
+
"text-anchor": "middle"
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
root.appendChild(svg(doc, "line", {
|
|
551
|
+
class: first === 1 ? "mn-nut" : "mn-fret",
|
|
552
|
+
x1: nutX,
|
|
553
|
+
x2: nutX,
|
|
554
|
+
y1: y(0),
|
|
555
|
+
y2: y(strings - 1)
|
|
556
|
+
}));
|
|
557
|
+
for (let fret = first;fret <= to; fret++) {
|
|
558
|
+
root.appendChild(svg(doc, "line", {
|
|
559
|
+
class: "mn-fret",
|
|
560
|
+
x1: wireX(fret),
|
|
561
|
+
x2: wireX(fret),
|
|
562
|
+
y1: y(0),
|
|
563
|
+
y2: y(strings - 1)
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
for (let row = 0;row < strings; row++) {
|
|
567
|
+
const open = tuning[strings - 1 - row];
|
|
568
|
+
root.appendChild(svg(doc, "line", {
|
|
569
|
+
class: "mn-string",
|
|
570
|
+
x1: nutX - openW,
|
|
571
|
+
x2: wireX(to),
|
|
572
|
+
y1: y(row),
|
|
573
|
+
y2: y(row)
|
|
574
|
+
}));
|
|
575
|
+
root.appendChild(svgText(doc, nameOf2(open, set.preferFlats), {
|
|
576
|
+
class: "mn-string-name",
|
|
577
|
+
x: LEFT2 - 8,
|
|
578
|
+
y: y(row) + 4,
|
|
579
|
+
"text-anchor": "end"
|
|
580
|
+
}));
|
|
581
|
+
for (let fret = from;fret <= to; fret++) {
|
|
582
|
+
const member = memberOf2(set, open + fret);
|
|
583
|
+
if (!member)
|
|
584
|
+
continue;
|
|
585
|
+
const cx = centerX(fret);
|
|
586
|
+
const isRoot = set.root === member.chroma;
|
|
587
|
+
root.appendChild(svg(doc, "circle", {
|
|
588
|
+
class: `mn-dot${isRoot ? " mn-root" : ""}`,
|
|
589
|
+
cx,
|
|
590
|
+
cy: y(row),
|
|
591
|
+
r: 8
|
|
592
|
+
}));
|
|
593
|
+
const text = labelOf2(member, label);
|
|
594
|
+
if (text) {
|
|
595
|
+
root.appendChild(svgText(doc, text, {
|
|
596
|
+
class: "mn-dot-text",
|
|
597
|
+
x: cx,
|
|
598
|
+
y: y(row) + 4,
|
|
599
|
+
"text-anchor": "middle"
|
|
600
|
+
}));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return root;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/fretboard/tuning.ts
|
|
608
|
+
var NAMED = new Map(Object.entries({
|
|
609
|
+
standard: "E2 A2 D3 G3 B3 E4",
|
|
610
|
+
"drop d": "D2 A2 D3 G3 B3 E4",
|
|
611
|
+
dadgad: "D2 A2 D3 G3 A3 D4",
|
|
612
|
+
"open g": "D2 G2 D3 G3 B3 D4",
|
|
613
|
+
"open d": "D2 A2 D3 F#3 A3 D4",
|
|
614
|
+
"open e": "E2 B2 E3 G#3 B3 E4",
|
|
615
|
+
"half step down": "Eb2 Ab2 Db3 Gb3 Bb3 Eb4",
|
|
616
|
+
bass: "E1 A1 D2 G2",
|
|
617
|
+
ukulele: "G4 C4 E4 A4"
|
|
618
|
+
}));
|
|
619
|
+
function parseTuning2(text) {
|
|
620
|
+
const trimmed = text.trim();
|
|
621
|
+
const named = NAMED.get(trimmed.toLowerCase().replace(/\s*\(.*\)$/, ""));
|
|
622
|
+
if (named)
|
|
623
|
+
return parseTuning2(named);
|
|
624
|
+
const spaced = trimmed.split(/[\s,]+/).filter(Boolean);
|
|
625
|
+
if (spaced.length > 1 && spaced.every((t) => midiOf2(t) !== null)) {
|
|
626
|
+
return spaced.map((t) => midiOf2(t));
|
|
627
|
+
}
|
|
628
|
+
const names = spaced.length > 1 ? spaced : trimmed.match(/[A-G][#♯b♭]?|[a-g][#♯♭]?/g) ?? [];
|
|
629
|
+
if (names.length < 2 || names.join("") !== spaced.join(""))
|
|
630
|
+
return null;
|
|
631
|
+
const midis = [];
|
|
632
|
+
for (const name of names) {
|
|
633
|
+
const chroma = chromaOf2(name);
|
|
634
|
+
if (chroma === null)
|
|
635
|
+
return null;
|
|
636
|
+
const previous = midis[midis.length - 1];
|
|
637
|
+
if (previous === undefined) {
|
|
638
|
+
const candidates = [28, 40, 52].map((c) => c - c % 12 + chroma);
|
|
639
|
+
midis.push(candidates.reduce((a, b) => Math.abs(b - 40) < Math.abs(a - 40) ? b : a));
|
|
640
|
+
} else {
|
|
641
|
+
let midi = previous - previous % 12 + chroma;
|
|
642
|
+
while (midi <= previous)
|
|
643
|
+
midi += 12;
|
|
644
|
+
midis.push(midi);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return midis;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/keyboard/keyboard.ts
|
|
651
|
+
var BLACK = new Set([1, 3, 6, 8, 10]);
|
|
652
|
+
var WHITE_W = 22;
|
|
653
|
+
var WHITE_H = 104;
|
|
654
|
+
var BLACK_W = 14;
|
|
655
|
+
var BLACK_H = 64;
|
|
656
|
+
var TITLE3 = 22;
|
|
657
|
+
var isBlack = (midi) => BLACK.has((midi % 12 + 12) % 12);
|
|
658
|
+
function parseKeyRange2(text) {
|
|
659
|
+
const parts = text.split(/\s*-\s*(?=[A-Ga-g])/);
|
|
660
|
+
if (parts.length !== 2)
|
|
661
|
+
return null;
|
|
662
|
+
const [from, to] = parts.map((n) => midiOf2(n));
|
|
663
|
+
return from !== undefined && from !== null && to !== undefined && to !== null && from < to ? [from, to] : null;
|
|
664
|
+
}
|
|
665
|
+
function keyRangeOf2(set) {
|
|
666
|
+
if (!set.pitches?.length)
|
|
667
|
+
return [60, 83];
|
|
668
|
+
const low = Math.min(...set.pitches);
|
|
669
|
+
const high = Math.max(...set.pitches);
|
|
670
|
+
return [low - (low % 12 + 12) % 12, high + 11 - (high % 12 + 12) % 12];
|
|
671
|
+
}
|
|
672
|
+
function renderKeyboard2(doc, set, options = {}) {
|
|
673
|
+
let [from, to] = options.range ?? keyRangeOf2(set);
|
|
674
|
+
if (isBlack(from))
|
|
675
|
+
from--;
|
|
676
|
+
if (isBlack(to))
|
|
677
|
+
to++;
|
|
678
|
+
const whites = [];
|
|
679
|
+
for (let midi = from;midi <= to; midi++)
|
|
680
|
+
if (!isBlack(midi))
|
|
681
|
+
whites.push(midi);
|
|
682
|
+
const top = options.title ? TITLE3 : 0;
|
|
683
|
+
const width = whites.length * WHITE_W + 2;
|
|
684
|
+
const height = top + WHITE_H + 2;
|
|
685
|
+
const label = options.label ?? "notes";
|
|
686
|
+
const root = svgRoot(doc, width, height, "mn-keyboard", options.title ?? `Keyboard: ${set.title}`);
|
|
687
|
+
if (options.title) {
|
|
688
|
+
root.appendChild(svgText(doc, options.title, {
|
|
689
|
+
class: "mn-title",
|
|
690
|
+
x: width / 2,
|
|
691
|
+
y: 15,
|
|
692
|
+
"text-anchor": "middle"
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
695
|
+
const keyClass = (midi, black) => {
|
|
696
|
+
const member = memberOf2(set, midi);
|
|
697
|
+
const classes = [black ? "mn-key-black" : "mn-key-white"];
|
|
698
|
+
if (member)
|
|
699
|
+
classes.push("mn-on");
|
|
700
|
+
if (member && set.root === member.chroma)
|
|
701
|
+
classes.push("mn-root");
|
|
702
|
+
return { member, cls: classes.join(" ") };
|
|
703
|
+
};
|
|
704
|
+
const caption = (midi, x, y, black) => {
|
|
705
|
+
const member = memberOf2(set, midi);
|
|
706
|
+
if (!member)
|
|
707
|
+
return;
|
|
708
|
+
const text = labelOf2(member, label, options.fingers?.get(midi));
|
|
709
|
+
if (!text)
|
|
710
|
+
return;
|
|
711
|
+
root.appendChild(svgText(doc, text, {
|
|
712
|
+
class: black ? "mn-key-label mn-key-label-black" : "mn-key-label",
|
|
713
|
+
x,
|
|
714
|
+
y,
|
|
715
|
+
"text-anchor": "middle"
|
|
716
|
+
}));
|
|
717
|
+
};
|
|
718
|
+
whites.forEach((midi, i) => {
|
|
719
|
+
const x = 1 + i * WHITE_W;
|
|
720
|
+
const { cls } = keyClass(midi, false);
|
|
721
|
+
root.appendChild(svg(doc, "rect", { class: cls, x, y: top + 1, width: WHITE_W, height: WHITE_H, rx: 2 }));
|
|
722
|
+
caption(midi, x + WHITE_W / 2, top + WHITE_H - 8, false);
|
|
723
|
+
});
|
|
724
|
+
whites.forEach((midi, i) => {
|
|
725
|
+
const black = midi + 1;
|
|
726
|
+
if (black > to || !isBlack(black))
|
|
727
|
+
return;
|
|
728
|
+
const x = 1 + (i + 1) * WHITE_W - BLACK_W / 2;
|
|
729
|
+
const { cls } = keyClass(black, true);
|
|
730
|
+
root.appendChild(svg(doc, "rect", { class: cls, x, y: top + 1, width: BLACK_W, height: BLACK_H, rx: 2 }));
|
|
731
|
+
caption(black, x + BLACK_W / 2, top + BLACK_H - 8, true);
|
|
732
|
+
});
|
|
733
|
+
return root;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/theory/chord-symbol.ts
|
|
737
|
+
var QUALITY_SUFFIXES = [
|
|
738
|
+
[/^(m7b5|m7-5|ø7?)$/, "Minor 7 flat 5"],
|
|
739
|
+
[/^(dim7|°7)$/, "Diminished 7"],
|
|
740
|
+
[/^(maj9|M9|Δ9)$/, "Major 9"],
|
|
741
|
+
[/^(maj7|M7|Δ7?)$/, "Major 7"],
|
|
742
|
+
[/^(7sus4)$/, "7sus4"],
|
|
743
|
+
[/^(m9|min9|-9)$/, "Minor 9"],
|
|
744
|
+
[/^(m7|min7|-7)$/, "Minor 7"],
|
|
745
|
+
[/^(m6|min6|-6)$/, "Minor 6"],
|
|
746
|
+
[/^(m|min|-)$/, "Minor"],
|
|
747
|
+
[/^(dim|°)$/, "Diminished"],
|
|
748
|
+
[/^(aug|\+)$/, "Augmented"],
|
|
749
|
+
[/^(sus2)$/, "Sus2"],
|
|
750
|
+
[/^(sus4|sus)$/, "Sus4"],
|
|
751
|
+
[/^(add9|add2)$/, "Add9"],
|
|
752
|
+
[/^5$/, "5 (Power)"],
|
|
753
|
+
[/^(6\/9|69)$/, "6/9"],
|
|
754
|
+
[/^(mM7|mMaj7|mmaj7|m\(maj7\)|-\(maj7\))$/, "Minor major 7"],
|
|
755
|
+
[/^(13|11|9|7|6)$/, "$1"],
|
|
756
|
+
[/^(maj|M)?$/, "Major"]
|
|
757
|
+
];
|
|
758
|
+
var spell = (note) => note[0] + (note[1] === "#" ? " Sharp" : note[1] === "b" ? " Flat" : "");
|
|
759
|
+
var plain = (quality) => quality.replace(/\s*\([^)]*\)/g, "").trim();
|
|
760
|
+
function parseChord2(symbol) {
|
|
761
|
+
const parts = splitChordSymbol(symbol);
|
|
762
|
+
if (!parts)
|
|
763
|
+
return null;
|
|
764
|
+
const trimmed = symbol.trim();
|
|
765
|
+
const { root, suffix, bass } = parts;
|
|
766
|
+
let quality = null;
|
|
767
|
+
for (const [re, label] of QUALITY_SUFFIXES) {
|
|
768
|
+
const hit = re.exec(suffix);
|
|
769
|
+
if (hit) {
|
|
770
|
+
quality = label.replace("$1", hit[1] ?? "");
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (!quality)
|
|
775
|
+
return null;
|
|
776
|
+
return {
|
|
777
|
+
symbol: trimmed,
|
|
778
|
+
root,
|
|
779
|
+
quality,
|
|
780
|
+
bass,
|
|
781
|
+
name: `${spell(root)} ${plain(quality)}${bass ? ` over ${spell(bass)}` : ""}`
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/blocks/block-source.ts
|
|
786
|
+
var FIELD = /^([A-Za-z][\w-]*)\s*:\s*(.*)$/;
|
|
787
|
+
function parseBlockSource(source) {
|
|
788
|
+
const fields = new Map;
|
|
789
|
+
let head = null;
|
|
790
|
+
source.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).forEach((line, i) => {
|
|
791
|
+
const m = FIELD.exec(line);
|
|
792
|
+
if (m)
|
|
793
|
+
fields.set(m[1].toLowerCase(), m[2].trim());
|
|
794
|
+
else if (i === 0)
|
|
795
|
+
head = line;
|
|
796
|
+
else
|
|
797
|
+
throw new Error(`Expected "key: value", got "${line}"`);
|
|
798
|
+
});
|
|
799
|
+
return { head, fields };
|
|
800
|
+
}
|
|
801
|
+
function parseRange(text) {
|
|
802
|
+
const m = /^(\d+)\s*(?:-\s*(\d+))?$/.exec(text.trim());
|
|
803
|
+
if (!m)
|
|
804
|
+
return null;
|
|
805
|
+
const from = Number(m[1]);
|
|
806
|
+
const to = m[2] === undefined ? from : Number(m[2]);
|
|
807
|
+
return from <= to ? [from, to] : null;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/blocks/note-set-field.ts
|
|
811
|
+
var LABELS = new Set(["notes", "degrees", "intervals", "none"]);
|
|
812
|
+
function noteSetOf(block) {
|
|
813
|
+
const scale = block.fields.get("scale");
|
|
814
|
+
const chord = block.fields.get("chord");
|
|
815
|
+
const notes = block.fields.get("notes");
|
|
816
|
+
if (scale !== undefined)
|
|
817
|
+
return orFail(scaleNoteSet2(scale), `Unknown scale "${scale}"`);
|
|
818
|
+
if (chord !== undefined)
|
|
819
|
+
return orFail(chordNoteSet2(chord), `Unknown chord "${chord}"`);
|
|
820
|
+
if (notes !== undefined)
|
|
821
|
+
return orFail(listNoteSet2(notes), `Invalid notes "${notes}"`);
|
|
822
|
+
if (block.head) {
|
|
823
|
+
const exact = listNoteSet2(block.head);
|
|
824
|
+
const set = scaleNoteSet2(block.head) ?? (exact?.pitches ? exact : null) ?? chordNoteSet2(block.head) ?? exact;
|
|
825
|
+
return orFail(set, `"${block.head}" is not a scale, chord or list of notes`);
|
|
826
|
+
}
|
|
827
|
+
throw new Error('Add "scale:", "chord:" or "notes:"');
|
|
828
|
+
}
|
|
829
|
+
function labelField(block, fallback, fingers = false) {
|
|
830
|
+
const value = block.fields.get("labels")?.toLowerCase();
|
|
831
|
+
if (value === undefined)
|
|
832
|
+
return fallback;
|
|
833
|
+
if (!LABELS.has(value) && !(fingers && value === "fingers")) {
|
|
834
|
+
const choices = fingers ? "notes, degrees, intervals, fingers or none" : "notes, degrees, intervals or none";
|
|
835
|
+
throw new Error(`Unknown labels "${value}": use ${choices}`);
|
|
836
|
+
}
|
|
837
|
+
return value;
|
|
838
|
+
}
|
|
839
|
+
function orFail(value, message) {
|
|
840
|
+
if (value === null)
|
|
841
|
+
throw new Error(message);
|
|
842
|
+
return value;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/blocks/renderers.ts
|
|
846
|
+
var field = (block, ...names) => {
|
|
847
|
+
for (const name of names) {
|
|
848
|
+
const value = block.fields.get(name);
|
|
849
|
+
if (value !== undefined && value !== "")
|
|
850
|
+
return value;
|
|
851
|
+
}
|
|
852
|
+
return;
|
|
853
|
+
};
|
|
854
|
+
var rootOf = (symbol) => {
|
|
855
|
+
const parts = splitChordSymbol(symbol);
|
|
856
|
+
return parts && parseChord2(symbol) ? chromaOf2(parts.root) : null;
|
|
857
|
+
};
|
|
858
|
+
var positiveInt = (text, what) => {
|
|
859
|
+
const n = Number(text);
|
|
860
|
+
if (!Number.isInteger(n) || n < 1)
|
|
861
|
+
throw new Error(`${what} must be a whole number from 1, got "${text}"`);
|
|
862
|
+
return n;
|
|
863
|
+
};
|
|
864
|
+
function shorthand(head) {
|
|
865
|
+
const tokens = head?.split(/\s+/).filter(Boolean) ?? [];
|
|
866
|
+
const symbol = tokens[0] && !parseFrets2(tokens[0]) ? tokens.shift() : undefined;
|
|
867
|
+
return { symbol, frets: tokens[0], fingers: tokens[1] };
|
|
868
|
+
}
|
|
869
|
+
function pianoPitches(symbol) {
|
|
870
|
+
const set = orFail(chordNoteSet2(symbol), `Unknown chord "${symbol}"`);
|
|
871
|
+
const bassName = splitChordSymbol(symbol)?.bass;
|
|
872
|
+
const bass = bassName ? chromaOf2(bassName) : null;
|
|
873
|
+
const pitches = [];
|
|
874
|
+
let previous = 59;
|
|
875
|
+
for (const member of set.members) {
|
|
876
|
+
if (bass !== null && member.chroma === bass && member.degree === null)
|
|
877
|
+
continue;
|
|
878
|
+
let midi = 60 + member.chroma;
|
|
879
|
+
while (midi <= previous)
|
|
880
|
+
midi += 12;
|
|
881
|
+
while (midi - 12 > previous)
|
|
882
|
+
midi -= 12;
|
|
883
|
+
pitches.push(midi);
|
|
884
|
+
previous = midi;
|
|
885
|
+
}
|
|
886
|
+
if (bass !== null)
|
|
887
|
+
pitches.unshift(48 + bass);
|
|
888
|
+
return { ...set, pitches };
|
|
889
|
+
}
|
|
890
|
+
var renderChord = (doc, source, options) => {
|
|
891
|
+
const block = parseBlockSource(source);
|
|
892
|
+
const short = shorthand(block.head);
|
|
893
|
+
const symbol = field(block, "symbol", "chord") ?? short.symbol;
|
|
894
|
+
const title = field(block, "title", "name") ?? symbol;
|
|
895
|
+
const instrument = field(block, "instrument")?.toLowerCase();
|
|
896
|
+
const pitchesText = field(block, "pitches", "notes");
|
|
897
|
+
if (instrument === "piano" || instrument === "keyboard" || pitchesText && !instrument) {
|
|
898
|
+
let set;
|
|
899
|
+
if (pitchesText) {
|
|
900
|
+
set = orFail(listNoteSet2(pitchesText), `Invalid pitches "${pitchesText}"`);
|
|
901
|
+
if (!set.pitches) {
|
|
902
|
+
let previous = 59;
|
|
903
|
+
set.pitches = set.members.map((m) => {
|
|
904
|
+
let midi = 60 + m.chroma;
|
|
905
|
+
while (midi <= previous)
|
|
906
|
+
midi += 12;
|
|
907
|
+
previous = midi;
|
|
908
|
+
return midi;
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
const chord = symbol ? chordNoteSet2(symbol) : null;
|
|
912
|
+
if (chord)
|
|
913
|
+
set.root = chord.root;
|
|
914
|
+
} else if (symbol) {
|
|
915
|
+
set = pianoPitches(symbol);
|
|
916
|
+
} else {
|
|
917
|
+
throw new Error('Add a chord symbol or "pitches:"');
|
|
918
|
+
}
|
|
919
|
+
const fingering = field(block, "fingering", "fingers");
|
|
920
|
+
const fingers = new Map;
|
|
921
|
+
if (fingering) {
|
|
922
|
+
const sorted = [...set.pitches].sort((a, b) => a - b);
|
|
923
|
+
fingering.split(/[\s,-]+/).filter(Boolean).forEach((finger, i) => {
|
|
924
|
+
if (sorted[i] !== undefined)
|
|
925
|
+
fingers.set(sorted[i], finger);
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
const range = field(block, "range");
|
|
929
|
+
return renderKeyboard2(doc, set, {
|
|
930
|
+
title,
|
|
931
|
+
label: labelField(block, fingers.size ? "fingers" : "notes", fingers.size > 0),
|
|
932
|
+
fingers,
|
|
933
|
+
range: range ? orFail(parseKeyRange2(range), `Invalid range "${range}": use e.g. C3-B4`) : keyRangeOf2(set)
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
if (instrument && instrument !== "guitar" && !field(block, "tuning")) {
|
|
937
|
+
throw new Error(`Unknown instrument "${instrument}": use guitar or piano, or give a tuning`);
|
|
938
|
+
}
|
|
939
|
+
const fretsText = field(block, "frets") ?? short.frets;
|
|
940
|
+
let voicing;
|
|
941
|
+
if (fretsText) {
|
|
942
|
+
const frets = orFail(parseFrets2(fretsText), `Invalid frets "${fretsText}": use e.g. x02220`);
|
|
943
|
+
const fingersText = field(block, "fingers") ?? short.fingers;
|
|
944
|
+
const fingers = fingersText ? parseFingers2(fingersText) : [];
|
|
945
|
+
if (fingersText && fingers.length !== frets.length) {
|
|
946
|
+
throw new Error(`"${fingersText}" has ${fingers.length} fingers for ${frets.length} strings`);
|
|
947
|
+
}
|
|
948
|
+
voicing = { frets, fingers: frets.map((_, i) => fingers[i] ?? null) };
|
|
949
|
+
} else if (symbol && options.voicings) {
|
|
950
|
+
const n = field(block, "voicing");
|
|
951
|
+
const index = n ? positiveInt(n, "voicing") - 1 : 0;
|
|
952
|
+
voicing = orFail(options.voicings(symbol, index), `No voicing ${index + 1} found for "${symbol}": add "frets:"`);
|
|
953
|
+
} else {
|
|
954
|
+
throw new Error(symbol ? `Add "frets:" for ${symbol} (e.g. frets: x02220)` : 'Add a chord symbol and "frets:"');
|
|
955
|
+
}
|
|
956
|
+
const position = field(block, "position", "base");
|
|
957
|
+
if (position) {
|
|
958
|
+
const base = positiveInt(position, "position");
|
|
959
|
+
const fretted = voicing.frets.filter((f) => f !== null && f > 0);
|
|
960
|
+
if (fretted.length && base > Math.min(...fretted)) {
|
|
961
|
+
throw new Error(`position ${base} is above the lowest fretted note (fret ${Math.min(...fretted)})`);
|
|
962
|
+
}
|
|
963
|
+
voicing.baseFret = base;
|
|
964
|
+
} else
|
|
965
|
+
voicing.baseFret ??= baseFretOf2(voicing.frets);
|
|
966
|
+
const tuningText = field(block, "tuning");
|
|
967
|
+
const tuning = tuningText ? orFail(parseTuning2(tuningText), `Unknown tuning "${tuningText}"`) : undefined;
|
|
968
|
+
if (tuning && tuning.length !== voicing.frets.length) {
|
|
969
|
+
throw new Error(`The tuning has ${tuning.length} strings, the frets ${voicing.frets.length}`);
|
|
970
|
+
}
|
|
971
|
+
const root = symbol ? chordNoteSet2(symbol)?.root ?? rootOf(symbol) : null;
|
|
972
|
+
return renderGuitarDiagram2(doc, voicing, { title, root, tuning });
|
|
973
|
+
};
|
|
974
|
+
var renderKeyboardBlock = (doc, source) => {
|
|
975
|
+
const block = parseBlockSource(source);
|
|
976
|
+
const set = noteSetOf(block);
|
|
977
|
+
const range = field(block, "range");
|
|
978
|
+
return renderKeyboard2(doc, set, {
|
|
979
|
+
title: field(block, "title"),
|
|
980
|
+
label: labelField(block, "notes"),
|
|
981
|
+
range: range ? orFail(parseKeyRange2(range), `Invalid range "${range}": use e.g. C3-B4`) : undefined
|
|
982
|
+
});
|
|
983
|
+
};
|
|
984
|
+
var renderFretboardBlock = (doc, source) => {
|
|
985
|
+
const block = parseBlockSource(source);
|
|
986
|
+
const set = noteSetOf(block);
|
|
987
|
+
const fretsText = field(block, "frets");
|
|
988
|
+
const frets = fretsText ? orFail(parseRange(fretsText), `Invalid frets "${fretsText}": use e.g. 5-8`) : undefined;
|
|
989
|
+
if (frets && frets[1] > 36)
|
|
990
|
+
throw new Error("Frets go up to 36");
|
|
991
|
+
const tuningText = field(block, "tuning");
|
|
992
|
+
return renderFretboard2(doc, set, {
|
|
993
|
+
title: field(block, "title"),
|
|
994
|
+
label: labelField(block, "notes"),
|
|
995
|
+
frets,
|
|
996
|
+
tuning: tuningText ? orFail(parseTuning2(tuningText), `Unknown tuning "${tuningText}"`) : undefined
|
|
997
|
+
});
|
|
998
|
+
};
|
|
999
|
+
var renderChordsBlock = (doc, source, options) => renderChart2(doc, parseChordPro2(source), { transpose: options.transpose });
|
|
1000
|
+
var RENDERERS = {
|
|
1001
|
+
chord: renderChord,
|
|
1002
|
+
chords: renderChordsBlock,
|
|
1003
|
+
keyboard: renderKeyboardBlock,
|
|
1004
|
+
fretboard: renderFretboardBlock
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
// src/render/render-block.ts
|
|
1008
|
+
var LANGUAGES2 = Object.keys(RENDERERS);
|
|
1009
|
+
var isNotationLanguage2 = (language) => Object.prototype.hasOwnProperty.call(RENDERERS, language);
|
|
1010
|
+
function renderBlock2(language, source, host, options = {}) {
|
|
1011
|
+
const doc = host.ownerDocument;
|
|
1012
|
+
const el = html(doc, "div", `mn-block mn-block-${language}`);
|
|
1013
|
+
let error = null;
|
|
1014
|
+
try {
|
|
1015
|
+
el.appendChild(RENDERERS[language](doc, source, options));
|
|
1016
|
+
} catch (e) {
|
|
1017
|
+
error = e instanceof Error ? e.message : String(e);
|
|
1018
|
+
el.classList.add("mn-error");
|
|
1019
|
+
el.appendChild(html(doc, "div", "mn-error-message", `${language}: ${error}`));
|
|
1020
|
+
}
|
|
1021
|
+
host.appendChild(el);
|
|
1022
|
+
return { ok: error === null, el, error, destroy: () => el.remove() };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export { STANDARD_TUNING2, parseFrets2, parseFingers2, baseFretOf2, renderGuitarDiagram2, parseLyrics2, parseChordPro2, keyPrefersFlats2, transposeChord2, transposeKey2, renderChart2, memberOf2, intervalLabel2, labelOf2, chordNoteSet2, scaleNoteSet2, listNoteSet2, renderFretboard2, parseTuning2, parseKeyRange2, keyRangeOf2, renderKeyboard2, parseChord2, LANGUAGES2, isNotationLanguage2, renderBlock2 };
|
|
1026
|
+
|
|
1027
|
+
//# debugId=627AFEC8F03B83D064756E2164756E21
|
|
1028
|
+
//# sourceMappingURL=index-b71brbpf.js.map
|