@barmark/render 0.10.0 → 0.11.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/CHANGELOG.md +21 -1
- package/README.md +9 -0
- package/dist/element.js +3 -0
- package/dist/html.d.ts +2 -0
- package/dist/html.js +80 -19
- package/dist/model.js +155 -18
- package/dist/speech.js +5 -0
- package/dist/styles.js +9 -2
- package/dist/types.d.ts +29 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,25 @@ package uses [semantic versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.11.0] - 2026-11-03
|
|
10
|
+
|
|
11
|
+
Needs `barmark` 0.14.0.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Several parts** (barmark 0.14.0, grammar 7.4): a system draws as
|
|
16
|
+
stacked rows, one per declared part in score order, sharing the
|
|
17
|
+
section's columns, with the part's label in the gutter and one bar
|
|
18
|
+
number. A part tacet there gets an empty row marked "tacet"; a tutti
|
|
19
|
+
line stays one row. The header lists the parts. The model gains
|
|
20
|
+
`parts`, a `system` item (its `rows`, each a row or an error) and
|
|
21
|
+
`tacet` on a bar. A tutti bar the parts play differently (`%` or a
|
|
22
|
+
leading `/` after a system) is drawn as written, `%` included, with
|
|
23
|
+
`simile` on its cell, rather than as one part's chords.
|
|
24
|
+
- **One part's view**: the `part` option and attribute draw only the
|
|
25
|
+
part with that label, as a single-part chart, with "tacet" where it
|
|
26
|
+
rests. An undeclared label draws the whole chart with a warning.
|
|
27
|
+
|
|
9
28
|
## [0.10.0] - 2026-11-03
|
|
10
29
|
|
|
11
30
|
### Added
|
|
@@ -275,7 +294,8 @@ that needs no script or as the `<barmark-chart>` web component. Requires
|
|
|
275
294
|
zoom without sideways scrolling, the element renders and re-renders, and
|
|
276
295
|
a screen reader hears "bar 9: D". Run them with `bun run test:browser`.
|
|
277
296
|
|
|
278
|
-
[Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.
|
|
297
|
+
[Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.11.0...HEAD
|
|
298
|
+
[0.11.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.10.0...render-v0.11.0
|
|
279
299
|
[0.10.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.9.1...render-v0.10.0
|
|
280
300
|
[0.9.1]: https://github.com/kevinpeckham/barmark/compare/render-v0.9.0...render-v0.9.1
|
|
281
301
|
[0.9.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.8.0...render-v0.9.0
|
package/README.md
CHANGED
|
@@ -77,6 +77,7 @@ the element loads, with `<style data-barmark-font>` holding
|
|
|
77
77
|
| `rehearsalLetters` | `rehearsal-letters` (`rehearsalLetters`) | `false` | boxed A, B, C… on each section as played |
|
|
78
78
|
| `numerals` | `numerals` (`numerals`) | none | `"nashville"` (6m⁷) or `"roman"` (vi⁷): each chord as its degree in its bar's key |
|
|
79
79
|
| `wrap` | `wrap="off"` (`wrap`) | `true` | wrap a section's rows when it is wider than the chart's box; off, rows keep their columns and the chart overflows, for its page to scroll |
|
|
80
|
+
| `part` | `part` (`part`) | none | draw only the part with this label, as one player reads it: tutti bars, its own bars, and "tacet" where it rests |
|
|
80
81
|
| `diagnostics` | | `false` | list the chart's errors and warnings below it |
|
|
81
82
|
| `fontUrl` | | none | emit Edwin's `@font-face` rule before the chart |
|
|
82
83
|
| (the `text` argument) | (`text`) | `null` | the chart to draw; wins over `src` and the script child |
|
|
@@ -185,6 +186,14 @@ section shows the numbers it was written with. A line with an error shows as wri
|
|
|
185
186
|
with its message. `layoutChart(text)` returns all of this as data, if
|
|
186
187
|
you want to draw it yourself.
|
|
187
188
|
|
|
189
|
+
A chart of several parts (barmark's grammar 7.4) draws each system as
|
|
190
|
+
stacked rows, one per part in score order, with the part's label in the
|
|
191
|
+
gutter and one bar number on the top row. A part tacet in a system gets
|
|
192
|
+
an empty row that says "tacet" in its first bar, so every system keeps its
|
|
193
|
+
shape; a tutti line is one row. The header lists the parts' names, and
|
|
194
|
+
each part row is announced by name to a screen reader. On a screen too
|
|
195
|
+
narrow for a section, its rows wrap one by one, as any row does.
|
|
196
|
+
|
|
188
197
|
## License
|
|
189
198
|
|
|
190
199
|
MIT. Edwin is © MuseScore Limited, under the SIL Open Font License 1.1.
|
package/dist/element.js
CHANGED
|
@@ -10,12 +10,14 @@ import { loadErrorContent, shadowContent, THEMES } from "./html.js";
|
|
|
10
10
|
export function optionsFromAttributes(el) {
|
|
11
11
|
const theme = el.getAttribute("theme");
|
|
12
12
|
const numerals = NUMERALS.find((n) => n === el.getAttribute("numerals"));
|
|
13
|
+
const part = el.getAttribute("part");
|
|
13
14
|
return {
|
|
14
15
|
theme: THEMES.find((t) => t === theme) ?? "auto",
|
|
15
16
|
barNumbers: el.getAttribute("bar-numbers") !== "off",
|
|
16
17
|
rehearsalLetters: el.hasAttribute("rehearsal-letters"),
|
|
17
18
|
wrap: el.getAttribute("wrap") !== "off",
|
|
18
19
|
...(numerals ? { numerals } : {}),
|
|
20
|
+
...(part ? { part } : {}),
|
|
19
21
|
};
|
|
20
22
|
}
|
|
21
23
|
/** What a chart draws: its `text` while that is a string, else `src`, else its script child. */
|
|
@@ -56,6 +58,7 @@ function defineElement() {
|
|
|
56
58
|
"rehearsal-letters",
|
|
57
59
|
"numerals",
|
|
58
60
|
"wrap",
|
|
61
|
+
"part",
|
|
59
62
|
];
|
|
60
63
|
#observer = new MutationObserver(() => this.#schedule());
|
|
61
64
|
#renders = 0;
|
package/dist/html.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export type RenderOptions = {
|
|
|
14
14
|
* With `false` rows keep their columns, and the page scrolls the chart instead.
|
|
15
15
|
*/
|
|
16
16
|
wrap?: boolean;
|
|
17
|
+
/** Draw only the part with this label (barmark's grammar 7.4): one player's view. */
|
|
18
|
+
part?: string;
|
|
17
19
|
};
|
|
18
20
|
export declare function renderChart(text: string, options?: RenderOptions): string;
|
|
19
21
|
export declare const THEMES: readonly ["auto", "light", "dark"];
|
package/dist/html.js
CHANGED
|
@@ -22,6 +22,7 @@ function elementAttributes(options) {
|
|
|
22
22
|
options.rehearsalLetters ? " rehearsal-letters" : "",
|
|
23
23
|
numeralsOf(options) ? ` numerals="${numeralsOf(options)}"` : "",
|
|
24
24
|
options.wrap === false ? ' wrap="off"' : "",
|
|
25
|
+
options.part ? ` part="${escapeHtml(options.part)}"` : "",
|
|
25
26
|
].join("");
|
|
26
27
|
}
|
|
27
28
|
export const THEMES = ["auto", "light", "dark"];
|
|
@@ -37,20 +38,28 @@ export function renderChartUnwrapped(text, options = {}) {
|
|
|
37
38
|
return `${fontStyle(options)}${chartHtml(text, options)}`;
|
|
38
39
|
}
|
|
39
40
|
function chartHtml(text, options) {
|
|
40
|
-
const model = layoutChart(text, {
|
|
41
|
+
const model = layoutChart(text, {
|
|
42
|
+
rehearsalLetters: options.rehearsalLetters ?? false,
|
|
43
|
+
...(options.part && { part: options.part }),
|
|
44
|
+
});
|
|
45
|
+
const barNumbers = options.barNumbers ?? true;
|
|
46
|
+
const parts = model.parts.length > 1;
|
|
41
47
|
const settings = {
|
|
42
|
-
barNumbers
|
|
48
|
+
barNumbers,
|
|
49
|
+
gutter: barNumbers || parts,
|
|
43
50
|
numerals: numeralsOf(options),
|
|
44
51
|
};
|
|
45
|
-
|
|
52
|
+
// Every section's left column is as wide as the longest part label, so tutti rows line up with systems.
|
|
53
|
+
const widest = Math.max(0, ...model.parts.map((p) => p.label?.length ?? 0));
|
|
54
|
+
const gutter = parts ? ` data-parts style="--bm-label-chars:${widest}"` : "";
|
|
55
|
+
return `<div class="bm-chart" data-theme="${themeOf(options)}"${gutter}${settings.barNumbers ? "" : ' data-bar-numbers="off"'}${options.wrap === false ? ' data-wrap="off"' : ""}>${headerHtml(model)}${model.blocks.map((b) => blockHtml(b, settings)).join("")}${options.diagnostics ? diagnosticsHtml(model) : ""}</div>`;
|
|
46
56
|
}
|
|
47
57
|
function headerHtml(model) {
|
|
48
58
|
const title = model.title
|
|
49
59
|
? `<h2 class="bm-title" part="title">${escapeHtml(model.title)}</h2>`
|
|
50
60
|
: "";
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
.join(" · ");
|
|
61
|
+
const names = model.parts.length > 1 ? model.parts.map((p) => p.name).join(", ") : model.part?.name;
|
|
62
|
+
const meta = [names ? escapeHtml(names) : "", metaText(model.header)].filter(Boolean).join(" · ");
|
|
54
63
|
if (!title && !meta)
|
|
55
64
|
return "";
|
|
56
65
|
return `<header class="bm-header">${title}${meta ? `<p class="bm-meta" part="meta">${meta}</p>` : ""}</header>`;
|
|
@@ -144,6 +153,8 @@ function itemHtml(item, columns, settings) {
|
|
|
144
153
|
switch (item.kind) {
|
|
145
154
|
case "row":
|
|
146
155
|
return rowHtml(item, columns, settings);
|
|
156
|
+
case "system":
|
|
157
|
+
return systemHtml(item, columns, settings);
|
|
147
158
|
case "text":
|
|
148
159
|
return `<p class="bm-text${item.note ? " bm-note" : ""}" part="text">${escapeHtml(item.text)}</p>`;
|
|
149
160
|
case "directive":
|
|
@@ -157,23 +168,63 @@ function errorHtml(source, messages) {
|
|
|
157
168
|
const message = escapeHtml(messages.join("; "));
|
|
158
169
|
return `<div class="bm-error" title="${message}"><code>${escapeHtml(source)}</code><span class="bm-sr">Error: ${message}</span></div>`;
|
|
159
170
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
/** A system: its parts' rows stacked, sharing the section's columns (barmark's grammar 7.4). */
|
|
172
|
+
function systemHtml(item, columns, settings) {
|
|
173
|
+
const names = escapeHtml(item.rows.map((p) => p.name).join(", "));
|
|
174
|
+
const rows = item.rows.map((p, i) => p.row.kind === "error"
|
|
175
|
+
? errorHtml(p.row.source, p.row.messages)
|
|
176
|
+
: rowHtml(p.row, columns, settings, { label: p.label, name: p.name, first: i === 0 }));
|
|
177
|
+
return `<div class="bm-system" role="group" aria-label="${names}">${rows.join("")}</div>`;
|
|
178
|
+
}
|
|
179
|
+
/** The left column: the bar number, and in a system the part's label. */
|
|
180
|
+
function gutterHtml(row, settings, part) {
|
|
181
|
+
if (!settings.gutter)
|
|
182
|
+
return "";
|
|
183
|
+
const number = settings.barNumbers && (!part || part.first) ? (row.barNumber ?? "") : "";
|
|
184
|
+
const label = part
|
|
185
|
+
? `<span class="bm-part-label" part="part-label">${escapeHtml(part.label)}</span>`
|
|
163
186
|
: "";
|
|
187
|
+
return `<span class="bm-bar-number" part="bar-number" aria-hidden="true">${label}${number}</span>`;
|
|
188
|
+
}
|
|
189
|
+
const isTacetCell = (c) => c.kind === "slots" && c.tacet === true;
|
|
190
|
+
/** A row's cells; a tacet row says "tacet" in its first bar. */
|
|
191
|
+
function rowCells(row, numerals, ending) {
|
|
164
192
|
let n = row.barNumber;
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
const
|
|
193
|
+
const firstTacet = row.cells.findIndex(isTacetCell);
|
|
194
|
+
return row.cells.map((cell, i) => {
|
|
195
|
+
const marks = repeatMarks(row, i) + (i === 0 ? ending : "");
|
|
196
|
+
const spoken = `<span class="bm-sr">bar ${n ?? ""}: tacet</span>`;
|
|
197
|
+
const html = isTacetCell(cell)
|
|
198
|
+
? tacetHtml(spoken, marks, i === firstTacet ? "tacet" : "")
|
|
199
|
+
: cellHtml(cell, n, marks, numerals);
|
|
169
200
|
if (n !== null && cell.kind === "slots")
|
|
170
201
|
n++;
|
|
171
202
|
return html;
|
|
172
203
|
});
|
|
173
|
-
|
|
204
|
+
}
|
|
205
|
+
const sr = (text) => (text ? `<span class="bm-sr">${text}</span>` : "");
|
|
206
|
+
/** The row's class list. */
|
|
207
|
+
function rowClasses(row, part) {
|
|
208
|
+
const classes = ["bm-row"];
|
|
209
|
+
if (part)
|
|
210
|
+
classes.push("bm-part-row");
|
|
211
|
+
if (part && row.cells.some(isTacetCell))
|
|
212
|
+
classes.push("bm-tacet");
|
|
213
|
+
if (row.ending)
|
|
214
|
+
classes.push("bm-ending");
|
|
215
|
+
return classes.join(" ");
|
|
216
|
+
}
|
|
217
|
+
/** What a screen reader hears before a row's bars: its part, a repeat start, its ending. */
|
|
218
|
+
function rowOpening(row, part, ending) {
|
|
174
219
|
const opens = row.opens || typeof row.repeat === "number" || row.repeat === "open";
|
|
175
|
-
|
|
176
|
-
|
|
220
|
+
return sr(part ? escapeHtml(part.name) : "") + sr(opens ? "repeat start" : "") + sr(ending);
|
|
221
|
+
}
|
|
222
|
+
function rowHtml(row, columns, settings, part) {
|
|
223
|
+
const label = row.ending ? endingLabel(row.ending) : null;
|
|
224
|
+
const cells = rowCells(row, settings.numerals, label ? ` data-ending="${label.shown}"` : "");
|
|
225
|
+
const empty = `<span class="bm-empty" aria-hidden="true"></span>`.repeat(Math.max(0, columns - row.cells.length));
|
|
226
|
+
const said = rowOpening(row, part, label?.spoken ?? "");
|
|
227
|
+
return `<div class="${rowClasses(row, part)}">${said}${gutterHtml(row, settings, part)}${cells.join("")}${empty}<span class="bm-repeat">${rowRepeatHtml(row)}</span></div>`;
|
|
177
228
|
}
|
|
178
229
|
/** An ending's passes as printed, "1.", "1.–3.", "1., 3.", and spoken, "ending for passes 1 to 3". */
|
|
179
230
|
function endingLabel(passes) {
|
|
@@ -202,13 +253,23 @@ function cellHtml(cell, barNumber, marks, numerals) {
|
|
|
202
253
|
const spoken = `<span class="bm-sr">${escapeHtml(speakBar(barNumber, cell, view !== null))}</span>`;
|
|
203
254
|
if (cell.kind !== "slots")
|
|
204
255
|
return betweenBarsHtml(cell, spoken, marks);
|
|
256
|
+
const change = cell.tempoChange ? ` data-tempo-change="${cell.tempoChange}"` : "";
|
|
257
|
+
const [body, end] = barBody(cell, view);
|
|
258
|
+
return `<div class="bm-bar" part="bar"${cell.cut ? " data-cut" : ""}${change}${marks}>${spoken}<span class="bm-bar-body" aria-hidden="true">${body}</span>${end}</div>`;
|
|
259
|
+
}
|
|
260
|
+
/** What a bar shows: its meter, its slots (or `%`), and "cut"; then the words after its last slot. */
|
|
261
|
+
function barBody(cell, view) {
|
|
205
262
|
const meter = cell.meter
|
|
206
263
|
? `<span class="bm-meter"><span>${cell.meter.n}</span><span>${cell.meter.d}</span></span>`
|
|
207
264
|
: "";
|
|
208
265
|
const cut = cell.cut ? `<span class="bm-cut">cut</span>` : "";
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
return
|
|
266
|
+
const [written, end] = slotsWithWords(cell, view);
|
|
267
|
+
const slots = cell.simile ? `<span class="bm-simile">%</span>` : written.join("");
|
|
268
|
+
return [`${meter}${slots}${cut}`, end];
|
|
269
|
+
}
|
|
270
|
+
/** A bar where the part rests; the first of a row says so, in its body. */
|
|
271
|
+
function tacetHtml(spoken, marks, text = "") {
|
|
272
|
+
return `<div class="bm-bar bm-tacet-bar" part="bar"${marks}>${spoken}<span class="bm-bar-body" aria-hidden="true">${text}</span></div>`;
|
|
212
273
|
}
|
|
213
274
|
/** A bar's slots, each under any words before it, and the words after the last slot. */
|
|
214
275
|
function slotsWithWords(cell, view) {
|
package/dist/model.js
CHANGED
|
@@ -4,9 +4,45 @@
|
|
|
4
4
|
* draws. Built from the parser's document lines; the played bars supply
|
|
5
5
|
* bar numbers and the slots `%` copies.
|
|
6
6
|
*/
|
|
7
|
-
import { parseChart, } from "barmark";
|
|
7
|
+
import { extractPart, parseChart, } from "barmark";
|
|
8
|
+
/** Whether a tutti bar's parts play different slots, as `%` or `/` on a tutti line can. */
|
|
9
|
+
const playedApart = (b, line) => {
|
|
10
|
+
const own = (b.parts ?? []).filter((p) => p?.line === line).map((p) => JSON.stringify(p?.slots));
|
|
11
|
+
return new Set(own).size > 1;
|
|
12
|
+
};
|
|
13
|
+
/** Each part's bars on its own lines; a tutti bar once (barmark's grammar 7.4). */
|
|
14
|
+
function barsByLine(parse) {
|
|
15
|
+
if (parse.parts.length < 2)
|
|
16
|
+
return parse.bars;
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
return parse.bars.flatMap((b) => (b.parts ?? []).flatMap((p) => {
|
|
19
|
+
const key = `${b.index}:${p?.line}`;
|
|
20
|
+
if (!p || seen.has(key))
|
|
21
|
+
return [];
|
|
22
|
+
seen.add(key);
|
|
23
|
+
const { words: _w, parts: _p, tacet: _t, ...shared } = b;
|
|
24
|
+
const apart = playedApart(b, p.line) ? { perPart: true } : {};
|
|
25
|
+
return [
|
|
26
|
+
{ ...shared, ...apart, line: p.line, slots: p.slots, ...(p.words && { words: p.words }) },
|
|
27
|
+
];
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
/** The whole chart, or one part of it; an undeclared part draws the whole chart, with a warning. */
|
|
31
|
+
function parseFor(text, part) {
|
|
32
|
+
const one = part ? extractPart(text, part) : null;
|
|
33
|
+
if (one || !part)
|
|
34
|
+
return one ?? parseChart(text);
|
|
35
|
+
const whole = parseChart(text);
|
|
36
|
+
const missing = {
|
|
37
|
+
line: 1,
|
|
38
|
+
severity: "warning",
|
|
39
|
+
message: `no part labelled ${part}`,
|
|
40
|
+
token: part,
|
|
41
|
+
};
|
|
42
|
+
return { ...whole, diagnostics: [missing, ...whole.diagnostics] };
|
|
43
|
+
}
|
|
8
44
|
export function layoutChart(text, options = {}) {
|
|
9
|
-
const parse =
|
|
45
|
+
const parse = parseFor(text, options.part);
|
|
10
46
|
const { header, body } = splitHeader(parse.lines);
|
|
11
47
|
const groups = groupLines(body);
|
|
12
48
|
const ctx = {
|
|
@@ -14,17 +50,25 @@ export function layoutChart(text, options = {}) {
|
|
|
14
50
|
source: text.split(/\r?\n/),
|
|
15
51
|
errors: errorsByLine(parse),
|
|
16
52
|
written: new Map(groups.flatMap((g) => (g.heading ? [[g.heading.line, g]] : []))),
|
|
53
|
+
bars: barsByLine(parse),
|
|
17
54
|
};
|
|
18
55
|
const blocks = groups.map((g) => layoutGroup(g, ctx));
|
|
19
56
|
if (options.rehearsalLetters)
|
|
20
57
|
letterBlocks(blocks);
|
|
21
|
-
return {
|
|
58
|
+
return {
|
|
59
|
+
title: parse.title,
|
|
60
|
+
part: parse.part,
|
|
61
|
+
parts: parse.parts,
|
|
62
|
+
header,
|
|
63
|
+
blocks,
|
|
64
|
+
diagnostics: parse.diagnostics,
|
|
65
|
+
};
|
|
22
66
|
}
|
|
23
67
|
/** Rehearsal letters mark position: every section occurrence, and leading bars, in play order. */
|
|
24
68
|
function letterBlocks(blocks) {
|
|
25
69
|
let n = 0;
|
|
26
70
|
for (const b of blocks) {
|
|
27
|
-
if (b.form === null && !b.items.some((i) => i.kind === "row"))
|
|
71
|
+
if (b.form === null && !b.items.some((i) => i.kind === "row" || i.kind === "system"))
|
|
28
72
|
continue;
|
|
29
73
|
b.rehearsal = letters(n++);
|
|
30
74
|
}
|
|
@@ -114,34 +158,61 @@ function substituteText(items, own) {
|
|
|
114
158
|
return next ? [next] : [];
|
|
115
159
|
});
|
|
116
160
|
// Extra lines continue after the last row and any text already following it.
|
|
117
|
-
const after = Math.max(out.findLastIndex((i) => i.kind === "row"), out.findLastIndex((i) => i.kind === "text"));
|
|
161
|
+
const after = Math.max(out.findLastIndex((i) => i.kind === "row" || i.kind === "system"), out.findLastIndex((i) => i.kind === "text"));
|
|
118
162
|
out.splice(after + 1, 0, ...queue);
|
|
119
163
|
return out;
|
|
120
164
|
}
|
|
121
|
-
/** The
|
|
122
|
-
function
|
|
123
|
-
const r = items.findLastIndex((i) => i.kind === "row" && lastSlots(i.cells) !== null);
|
|
124
|
-
const row = items[r];
|
|
125
|
-
if (row?.kind !== "row")
|
|
126
|
-
return items;
|
|
165
|
+
/** The row's last bar, marked cut. */
|
|
166
|
+
function cutRow(row) {
|
|
127
167
|
const c = row.cells.findLastIndex((cell) => cell.kind === "slots");
|
|
128
168
|
const cells = row.cells.map((cell, i) => i === c && cell.kind === "slots" ? { ...cell, cut: true } : cell);
|
|
129
|
-
return
|
|
169
|
+
return { ...row, cells };
|
|
170
|
+
}
|
|
171
|
+
const hasBars = (i) => (i.kind === "row" && lastSlots(i.cells) !== null) || i.kind === "system";
|
|
172
|
+
/** The parser cuts the last bar it would play; a fermata is not a bar. A system's every part loses it. */
|
|
173
|
+
function markCut(items) {
|
|
174
|
+
const r = items.findLastIndex(hasBars);
|
|
175
|
+
const item = items[r];
|
|
176
|
+
if (!item || (item.kind !== "row" && item.kind !== "system"))
|
|
177
|
+
return items;
|
|
178
|
+
const cut = item.kind === "row"
|
|
179
|
+
? cutRow(item)
|
|
180
|
+
: {
|
|
181
|
+
...item,
|
|
182
|
+
rows: item.rows.map((p) => (p.row.kind === "row" ? { ...p, row: cutRow(p.row) } : p)),
|
|
183
|
+
};
|
|
184
|
+
return items.map((it, i) => (i === r ? cut : it));
|
|
130
185
|
}
|
|
186
|
+
/** How many cells an item's widest row has. */
|
|
187
|
+
const widthOf = (i) => i.kind === "row"
|
|
188
|
+
? i.cells.length
|
|
189
|
+
: i.kind === "system"
|
|
190
|
+
? Math.max(0, ...i.rows.map((p) => (p.row.kind === "row" ? p.row.cells.length : 0)))
|
|
191
|
+
: 0;
|
|
131
192
|
function blockOf(h, occurrence, items) {
|
|
132
193
|
return {
|
|
133
194
|
heading: h?.name ?? null,
|
|
134
195
|
repeat: h?.repeat ?? null,
|
|
135
196
|
form: occurrence?.form ?? null,
|
|
136
197
|
rehearsal: null,
|
|
137
|
-
columns: Math.max(0, ...items.map(
|
|
198
|
+
columns: Math.max(0, ...items.map(widthOf)),
|
|
138
199
|
items,
|
|
139
200
|
};
|
|
140
201
|
}
|
|
141
202
|
function layoutLines(lines, occurrence, ctx) {
|
|
142
203
|
const items = [];
|
|
143
204
|
let previous = null;
|
|
144
|
-
for (const l of lines) {
|
|
205
|
+
for (const [i, l] of lines.entries()) {
|
|
206
|
+
const system = systemOf(l);
|
|
207
|
+
if (system !== null) {
|
|
208
|
+
// A system is laid out once, at its first line.
|
|
209
|
+
if (systemOf(lines[i - 1]) === system)
|
|
210
|
+
continue;
|
|
211
|
+
const item = layoutSystem(systemLines(lines, i), occurrence, previous, ctx);
|
|
212
|
+
previous = lastOfSystem(item) ?? previous;
|
|
213
|
+
items.push(item);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
145
216
|
if (ctx.errors.has(l.line)) {
|
|
146
217
|
items.push(errorItem(l.line, ctx));
|
|
147
218
|
continue;
|
|
@@ -155,10 +226,65 @@ function layoutLines(lines, occurrence, ctx) {
|
|
|
155
226
|
}
|
|
156
227
|
return items;
|
|
157
228
|
}
|
|
229
|
+
const systemOf = (l) => (l?.kind === "bars" || l?.kind === "invalid") && l.system !== undefined ? l.system : null;
|
|
230
|
+
/** The lines of the system that starts at `i`. */
|
|
231
|
+
function systemLines(lines, i) {
|
|
232
|
+
const system = systemOf(lines[i]);
|
|
233
|
+
const out = [];
|
|
234
|
+
for (const l of lines.slice(i)) {
|
|
235
|
+
if (systemOf(l) !== system)
|
|
236
|
+
break;
|
|
237
|
+
out.push(l);
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
const lastOfSystem = (item) => {
|
|
242
|
+
const first = item.rows.find((p) => p.row.kind === "row" && !p.row.cells.some(isTacet))?.row;
|
|
243
|
+
return first?.kind === "row" ? lastSlots(first.cells) : null;
|
|
244
|
+
};
|
|
245
|
+
const isTacet = (c) => c.kind === "slots" && c.tacet === true;
|
|
246
|
+
/** A row per declared part, in score order: its own line, its error, or tacet bars. */
|
|
247
|
+
function layoutSystem(lines, occurrence, previous, ctx) {
|
|
248
|
+
const ref = lines.find((l) => l.kind === "bars" && !ctx.errors.has(l.line));
|
|
249
|
+
const reference = ref
|
|
250
|
+
? layoutRow(ref, playedOn(ref.line, occurrence, ctx.bars), previous, ctx)
|
|
251
|
+
: null;
|
|
252
|
+
const rows = ctx.parse.parts.map((p) => {
|
|
253
|
+
const own = lines.find((l) => l.part === p.label);
|
|
254
|
+
const row = own && ctx.errors.has(own.line)
|
|
255
|
+
? errorItem(own.line, ctx)
|
|
256
|
+
: own?.kind === "bars"
|
|
257
|
+
? layoutRow(own, playedOn(own.line, occurrence, ctx.bars), previous, ctx)
|
|
258
|
+
: tacetRow(reference, lines[0]?.line ?? 0);
|
|
259
|
+
return { label: p.label ?? "", name: p.name, row };
|
|
260
|
+
});
|
|
261
|
+
return {
|
|
262
|
+
kind: "system",
|
|
263
|
+
line: lines[0]?.line ?? 0,
|
|
264
|
+
barNumber: reference?.barNumber ?? null,
|
|
265
|
+
rows,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const NO_ROW = {
|
|
269
|
+
kind: "row",
|
|
270
|
+
line: 0,
|
|
271
|
+
barNumber: null,
|
|
272
|
+
opens: false,
|
|
273
|
+
closes: false,
|
|
274
|
+
repeat: null,
|
|
275
|
+
ending: null,
|
|
276
|
+
cells: [],
|
|
277
|
+
};
|
|
278
|
+
/** The system's bars, with nothing played in them (barmark's grammar 7.4). */
|
|
279
|
+
function tacetRow(reference, line) {
|
|
280
|
+
const base = reference ?? NO_ROW;
|
|
281
|
+
const cells = base.cells.map((c) => c.kind === "slots" ? { kind: "slots", meter: c.meter, slots: [], cut: false, tacet: true } : c);
|
|
282
|
+
return { ...base, line, cells };
|
|
283
|
+
}
|
|
158
284
|
function layoutLine(l, occurrence, previous, ctx) {
|
|
159
285
|
switch (l.kind) {
|
|
160
286
|
case "bars":
|
|
161
|
-
return layoutRow(l, playedOn(l.line, occurrence, ctx.
|
|
287
|
+
return layoutRow(l, playedOn(l.line, occurrence, ctx.bars), previous, ctx);
|
|
162
288
|
case "text":
|
|
163
289
|
return textItem(l.text);
|
|
164
290
|
case "directive":
|
|
@@ -193,7 +319,7 @@ function playedOn(line, occurrence, bars) {
|
|
|
193
319
|
function layoutRow(l, played, previous, ctx) {
|
|
194
320
|
const cells = [];
|
|
195
321
|
// A cut bar is not played: what it would play is what the line's bar played when written.
|
|
196
|
-
const written = ctx.
|
|
322
|
+
const written = ctx.bars.filter((b) => b.line === l.line);
|
|
197
323
|
let bar = 0;
|
|
198
324
|
let prev = previous;
|
|
199
325
|
for (const cell of l.cells) {
|
|
@@ -222,8 +348,16 @@ function layoutCell(cell, played, previous) {
|
|
|
222
348
|
return { kind: "fermata" };
|
|
223
349
|
if (cell.kind === "direction" || cell.kind === "rehearsal")
|
|
224
350
|
return cell;
|
|
351
|
+
// Parts that play a tutti bar differently each read their own: draw it as written.
|
|
352
|
+
if (played?.perPart)
|
|
353
|
+
return withPlayed(asWritten(cell, previous), played);
|
|
225
354
|
return withPlayed(layoutBar(cell, played, previous), played);
|
|
226
355
|
}
|
|
356
|
+
/** A tutti bar as written: its own slots, holds as holds, or `%` itself. */
|
|
357
|
+
function asWritten(cell, previous) {
|
|
358
|
+
const bar = layoutBar(cell, undefined, previous);
|
|
359
|
+
return cell.kind === "simile" ? { ...bar, slots: [], simile: true } : bar;
|
|
360
|
+
}
|
|
227
361
|
/** A bar or `%`, with the slots it plays. */
|
|
228
362
|
function layoutBar(cell, played, previous) {
|
|
229
363
|
const own = cell.kind === "slots" ? cell : null;
|
|
@@ -235,8 +369,11 @@ function layoutBar(cell, played, previous) {
|
|
|
235
369
|
...wordsOf(cell),
|
|
236
370
|
};
|
|
237
371
|
}
|
|
238
|
-
/** The words written on a bar,
|
|
239
|
-
const wordsOf = (cell) =>
|
|
372
|
+
/** The words written on a bar, and whether the part rests there, as fields to spread. */
|
|
373
|
+
const wordsOf = (cell) => ({
|
|
374
|
+
...(cell.words && { words: cell.words }),
|
|
375
|
+
...(cell.kind === "slots" && cell.tacet && { tacet: cell.tacet }),
|
|
376
|
+
});
|
|
240
377
|
/** What the parser says about the bar as played: inside a rit. or accel., and its key. */
|
|
241
378
|
function withPlayed(cell, played) {
|
|
242
379
|
const change = played?.tempoChange?.kind;
|
package/dist/speech.js
CHANGED
|
@@ -148,6 +148,11 @@ export function speakBar(barNumber, cell, numerals = false) {
|
|
|
148
148
|
return speakDirection(cell.direction);
|
|
149
149
|
if (cell.kind === "rehearsal")
|
|
150
150
|
return `rehearsal ${cell.label}`;
|
|
151
|
+
const bar = `bar${barNumber === null ? "" : ` ${barNumber}`}`;
|
|
152
|
+
if (cell.tacet)
|
|
153
|
+
return `${bar}: tacet`;
|
|
154
|
+
if (cell.simile)
|
|
155
|
+
return `${bar}: each part repeats its bar before`;
|
|
151
156
|
const meter = cell.meter ? `, in ${cell.meter.n}/${cell.meter.d}` : "";
|
|
152
157
|
const cut = cell.cut ? ", cut on this pass" : "";
|
|
153
158
|
const words = (at) => (cell.words ?? []).filter((w) => w.at === at).map((w) => `“${w.text}”`);
|
package/dist/styles.js
CHANGED
|
@@ -85,7 +85,14 @@ const css = `
|
|
|
85
85
|
.bm-grid{display:grid;grid-template-columns:${em(GUTTER)} repeat(var(--bm-cols),auto) auto;width:max-content;max-width:100%}
|
|
86
86
|
.bm-chart[data-bar-numbers="off"] .bm-grid{grid-template-columns:repeat(var(--bm-cols),auto) auto}
|
|
87
87
|
.bm-chart[data-wrap="off"] .bm-grid{max-width:none}
|
|
88
|
-
.bm-row{display:contents}
|
|
88
|
+
.bm-row,.bm-system{display:contents}
|
|
89
|
+
.bm-chart[data-parts] .bm-grid{grid-template-columns:calc(${em(GUTTER + 0.9)} + var(--bm-label-chars) * .62em) repeat(var(--bm-cols),auto) auto}
|
|
90
|
+
.bm-chart[data-parts] .bm-bar-number{width:auto;justify-content:flex-end;gap:.35em;padding-right:.45em}
|
|
91
|
+
.bm-part-row>.bm-bar-number{border-right:.15em solid var(--bm-rule)}
|
|
92
|
+
.bm-part-label{margin-right:auto}
|
|
93
|
+
.bm-part-label{font-family:${LABEL};font-weight:600;color:var(--bm-ink)}
|
|
94
|
+
.bm-tacet-bar .bm-bar-body{font-family:${LABEL};font-style:italic;color:var(--bm-muted)}
|
|
95
|
+
:is(.bm-row,.bm-system,.bm-error)+.bm-system>.bm-row:first-child>*,.bm-system+.bm-row>*{margin-top:${em(ROW_GAP * 2)}}
|
|
89
96
|
.bm-row+.bm-row>*{margin-top:${em(ROW_GAP)}}
|
|
90
97
|
.bm-row+.bm-row>.bm-bar-number{margin-top:${at(SIZES.barNumber, ROW_GAP)}}
|
|
91
98
|
.bm-bar-number{display:flex;flex:none;align-items:baseline;width:${at(SIZES.barNumber, GUTTER)};padding-top:${at(SIZES.barNumber, 0.65)};font:${em(SIZES.barNumber)}/1 ${SANS};color:var(--bm-muted)}
|
|
@@ -102,7 +109,7 @@ const css = `
|
|
|
102
109
|
.bm-chord{white-space:nowrap}
|
|
103
110
|
.bm-chord sup{font-size:${em(SIZES.superscript)};vertical-align:.55em;line-height:0}
|
|
104
111
|
.bm-chord sub{font-size:${em(SIZES.octave)};line-height:0}
|
|
105
|
-
.bm-hold,.bm-unknown,.bm-brace{color:var(--bm-muted)}
|
|
112
|
+
.bm-hold,.bm-unknown,.bm-brace,.bm-simile{color:var(--bm-muted)}
|
|
106
113
|
.bm-nc{font-size:${em(SIZES.noChord)};letter-spacing:.06em}
|
|
107
114
|
.bm-silence{display:inline-block;width:.9em;height:.32em;background:var(--bm-ink);vertical-align:.25em}
|
|
108
115
|
.bm-ring,.bm-notes{font-size:${em(SIZES.noteNames)}}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { ChartDiagnostic, Direction, Directives, Key, Meter, Part, Repeat, Slot, Words } from "barmark";
|
|
2
|
+
/** `part`: draw only the part with this label (barmark's grammar 7.4), as a single-part chart. */
|
|
2
3
|
export type LayoutOptions = {
|
|
3
4
|
rehearsalLetters?: boolean;
|
|
5
|
+
part?: string;
|
|
4
6
|
};
|
|
5
7
|
/** What to draw, with every musical decision already made. */
|
|
6
8
|
export type ChartModel = {
|
|
7
9
|
title: string | null;
|
|
8
|
-
/** What `part:` names; null when the chart names none. */
|
|
10
|
+
/** What `part:` names in a single-part chart; null when the chart names none or several. */
|
|
9
11
|
part: Part | null;
|
|
12
|
+
/** Every declared part, in score order (barmark's grammar 7.4). */
|
|
13
|
+
parts: Part[];
|
|
10
14
|
/** Directives before the first heading, bar line or text line. */
|
|
11
15
|
header: Directives;
|
|
12
16
|
blocks: SectionBlock[];
|
|
@@ -24,6 +28,9 @@ export type SectionBlock = {
|
|
|
24
28
|
columns: number;
|
|
25
29
|
items: BlockItem[];
|
|
26
30
|
};
|
|
31
|
+
export type RowItem = Extract<BlockItem, {
|
|
32
|
+
kind: "row";
|
|
33
|
+
}>;
|
|
27
34
|
export type BlockItem = {
|
|
28
35
|
kind: "row";
|
|
29
36
|
line: number;
|
|
@@ -36,6 +43,21 @@ export type BlockItem = {
|
|
|
36
43
|
ending: number[] | null;
|
|
37
44
|
cells: CellModel[];
|
|
38
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* A system (barmark's grammar 7.4): a row per declared part, in score
|
|
48
|
+
* order, sharing bars. A part tacet there has a row of `tacet` bars; a
|
|
49
|
+
* part whose line has an error has an error item.
|
|
50
|
+
*/
|
|
51
|
+
| {
|
|
52
|
+
kind: "system";
|
|
53
|
+
line: number;
|
|
54
|
+
barNumber: number | null;
|
|
55
|
+
rows: Array<{
|
|
56
|
+
label: string;
|
|
57
|
+
name: string;
|
|
58
|
+
row: RowItem | ErrorItem;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
39
61
|
/** `note` marks a `###` line, whose text is given without the marks. */
|
|
40
62
|
| {
|
|
41
63
|
kind: "text";
|
|
@@ -44,7 +66,8 @@ export type BlockItem = {
|
|
|
44
66
|
} | {
|
|
45
67
|
kind: "directive";
|
|
46
68
|
set: Directives;
|
|
47
|
-
} |
|
|
69
|
+
} | ErrorItem;
|
|
70
|
+
export type ErrorItem = {
|
|
48
71
|
kind: "error";
|
|
49
72
|
line: number;
|
|
50
73
|
source: string;
|
|
@@ -61,6 +84,10 @@ export type CellModel = {
|
|
|
61
84
|
key?: Key;
|
|
62
85
|
/** Words written on the bar (barmark's grammar 2.6), each before slot `at`. */
|
|
63
86
|
words?: Words[];
|
|
87
|
+
/** A bar where the part rests, in a system (barmark's grammar 7.4). */
|
|
88
|
+
tacet?: true;
|
|
89
|
+
/** `%` on a tutti line whose parts each copy a different bar: drawn as `%`, its slots empty (barmark's grammar 7.4). */
|
|
90
|
+
simile?: true;
|
|
64
91
|
} | {
|
|
65
92
|
kind: "fermata";
|
|
66
93
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barmark/render",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Render barmark chord charts as a clean, accessible bar grid: HTML with Declarative Shadow DOM, or the <barmark-chart> web component.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"url": "https://github.com/kevinpeckham/barmark/issues"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"barmark": "^0.
|
|
50
|
+
"barmark": "^0.14.0"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|