@barmark/render 0.10.0 → 0.12.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 +45 -1
- package/README.md +12 -1
- package/dist/element.js +14 -0
- package/dist/html.d.ts +2 -0
- package/dist/html.js +88 -21
- package/dist/model.d.ts +3 -1
- package/dist/model.js +157 -18
- package/dist/speech.js +3 -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,48 @@ package uses [semantic versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.12.0] - 2026-11-03
|
|
10
|
+
|
|
11
|
+
Needs `barmark` 0.14.1.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **Breaking:** the element's one-part attribute is now `show-part`
|
|
16
|
+
(property `showPart`), not `part`. HTML keeps `part` on every element
|
|
17
|
+
for `::part()` styling, so a page styling `<barmark-chart part="…">`
|
|
18
|
+
got a one-part view by accident, and `el.part` could not return the
|
|
19
|
+
label. `renderChart`'s option stays `part`.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- A system shows and speaks what its rows share once, on its first drawn
|
|
24
|
+
row: the bar number, "repeat start", the ending's number and ×N. Every
|
|
25
|
+
row still draws its repeat bar lines. A system whose first part's line
|
|
26
|
+
has an error numbers the next row.
|
|
27
|
+
- Tacet rows carry `bm-tacet` in a part's own view too, and a tacet bar
|
|
28
|
+
cut in a `->` replay is marked and spoken as cut.
|
|
29
|
+
- The "no part labelled X" warning is about the whole chart: its line is
|
|
30
|
+
0, and the diagnostics list shows it without a line number.
|
|
31
|
+
|
|
32
|
+
## [0.11.0] - 2026-11-03
|
|
33
|
+
|
|
34
|
+
Needs `barmark` 0.14.0.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **Several parts** (barmark 0.14.0, grammar 7.4): a system draws as
|
|
39
|
+
stacked rows, one per declared part in score order, sharing the
|
|
40
|
+
section's columns, with the part's label in the gutter and one bar
|
|
41
|
+
number. A part tacet there gets an empty row marked "tacet"; a tutti
|
|
42
|
+
line stays one row. The header lists the parts. The model gains
|
|
43
|
+
`parts`, a `system` item (its `rows`, each a row or an error) and
|
|
44
|
+
`tacet` on a bar. A tutti bar the parts play differently (`%` or a
|
|
45
|
+
leading `/` after a system) is drawn as written, `%` included, with
|
|
46
|
+
`simile` on its cell, rather than as one part's chords.
|
|
47
|
+
- **One part's view**: the `part` option and attribute draw only the
|
|
48
|
+
part with that label, as a single-part chart, with "tacet" where it
|
|
49
|
+
rests. An undeclared label draws the whole chart with a warning.
|
|
50
|
+
|
|
9
51
|
## [0.10.0] - 2026-11-03
|
|
10
52
|
|
|
11
53
|
### Added
|
|
@@ -275,7 +317,9 @@ that needs no script or as the `<barmark-chart>` web component. Requires
|
|
|
275
317
|
zoom without sideways scrolling, the element renders and re-renders, and
|
|
276
318
|
a screen reader hears "bar 9: D". Run them with `bun run test:browser`.
|
|
277
319
|
|
|
278
|
-
[Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.
|
|
320
|
+
[Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.12.0...HEAD
|
|
321
|
+
[0.12.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.11.0...render-v0.12.0
|
|
322
|
+
[0.11.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.10.0...render-v0.11.0
|
|
279
323
|
[0.10.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.9.1...render-v0.10.0
|
|
280
324
|
[0.9.1]: https://github.com/kevinpeckham/barmark/compare/render-v0.9.0...render-v0.9.1
|
|
281
325
|
[0.9.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.8.0...render-v0.9.0
|
package/README.md
CHANGED
|
@@ -48,7 +48,9 @@ The element renders from its `text` property, its `src` URL, or a child
|
|
|
48
48
|
whenever one changes; it fetches a `src` once, and renders once however
|
|
49
49
|
many properties and attributes change together. A chart rendered on the
|
|
50
50
|
server by `renderChart` keeps its music until it has a source of its own,
|
|
51
|
-
and still follows `theme` and `
|
|
51
|
+
and still follows `theme`, `bar-numbers` and `wrap`; a change to
|
|
52
|
+
`numerals`, `rehearsal-letters` or `show-part` needs the chart's text, so
|
|
53
|
+
it waits for a source.
|
|
52
54
|
|
|
53
55
|
For a live editor, set `text`:
|
|
54
56
|
|
|
@@ -77,6 +79,7 @@ the element loads, with `<style data-barmark-font>` holding
|
|
|
77
79
|
| `rehearsalLetters` | `rehearsal-letters` (`rehearsalLetters`) | `false` | boxed A, B, C… on each section as played |
|
|
78
80
|
| `numerals` | `numerals` (`numerals`) | none | `"nashville"` (6m⁷) or `"roman"` (vi⁷): each chord as its degree in its bar's key |
|
|
79
81
|
| `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 |
|
|
82
|
+
| `part` | `show-part` (`showPart`) | none | draw only the part with this label, as one player reads it: tutti bars, its own bars, and "tacet" where it rests. The attribute isn't `part`, which HTML keeps for `::part()` styling |
|
|
80
83
|
| `diagnostics` | | `false` | list the chart's errors and warnings below it |
|
|
81
84
|
| `fontUrl` | | none | emit Edwin's `@font-face` rule before the chart |
|
|
82
85
|
| (the `text` argument) | (`text`) | `null` | the chart to draw; wins over `src` and the script child |
|
|
@@ -185,6 +188,14 @@ section shows the numbers it was written with. A line with an error shows as wri
|
|
|
185
188
|
with its message. `layoutChart(text)` returns all of this as data, if
|
|
186
189
|
you want to draw it yourself.
|
|
187
190
|
|
|
191
|
+
A chart of several parts (barmark's grammar 7.4) draws each system as
|
|
192
|
+
stacked rows, one per part in score order, with the part's label in the
|
|
193
|
+
gutter and one bar number on the top row. A part tacet in a system gets
|
|
194
|
+
an empty row that says "tacet" in its first bar, so every system keeps its
|
|
195
|
+
shape; a tutti line is one row. The header lists the parts' names, and
|
|
196
|
+
each part row is announced by name to a screen reader. On a screen too
|
|
197
|
+
narrow for a section, its rows wrap one by one, as any row does.
|
|
198
|
+
|
|
188
199
|
## License
|
|
189
200
|
|
|
190
201
|
MIT. Edwin is © MuseScore Limited, under the SIL Open Font License 1.1.
|
package/dist/element.js
CHANGED
|
@@ -10,12 +10,15 @@ 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
|
+
// Not `part`: that global attribute names an element for ::part() styling.
|
|
14
|
+
const part = el.getAttribute("show-part");
|
|
13
15
|
return {
|
|
14
16
|
theme: THEMES.find((t) => t === theme) ?? "auto",
|
|
15
17
|
barNumbers: el.getAttribute("bar-numbers") !== "off",
|
|
16
18
|
rehearsalLetters: el.hasAttribute("rehearsal-letters"),
|
|
17
19
|
wrap: el.getAttribute("wrap") !== "off",
|
|
18
20
|
...(numerals ? { numerals } : {}),
|
|
21
|
+
...(part ? { part } : {}),
|
|
19
22
|
};
|
|
20
23
|
}
|
|
21
24
|
/** What a chart draws: its `text` while that is a string, else `src`, else its script child. */
|
|
@@ -56,6 +59,7 @@ function defineElement() {
|
|
|
56
59
|
"rehearsal-letters",
|
|
57
60
|
"numerals",
|
|
58
61
|
"wrap",
|
|
62
|
+
"show-part",
|
|
59
63
|
];
|
|
60
64
|
#observer = new MutationObserver(() => this.#schedule());
|
|
61
65
|
#renders = 0;
|
|
@@ -136,6 +140,16 @@ function defineElement() {
|
|
|
136
140
|
else
|
|
137
141
|
this.setAttribute("numerals", v);
|
|
138
142
|
}
|
|
143
|
+
/** The label of the part to draw on its own, or null for the whole chart. */
|
|
144
|
+
get showPart() {
|
|
145
|
+
return this.getAttribute("show-part");
|
|
146
|
+
}
|
|
147
|
+
set showPart(v) {
|
|
148
|
+
if (v === null)
|
|
149
|
+
this.removeAttribute("show-part");
|
|
150
|
+
else
|
|
151
|
+
this.setAttribute("show-part", v);
|
|
152
|
+
}
|
|
139
153
|
/** Every change in one task (an upgrade sets each attribute in turn) renders once. */
|
|
140
154
|
#schedule() {
|
|
141
155
|
if (this.#scheduled)
|
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
|
@@ -7,7 +7,7 @@ import { printDirection, printPasses, } from "barmark";
|
|
|
7
7
|
import { glyphs, NUMERALS, splitPitch, typesetChord, typesetNumeral, } from "./chord.js";
|
|
8
8
|
import { escapeHtml } from "./escape.js";
|
|
9
9
|
import { fontFaceCss } from "./font.js";
|
|
10
|
-
import { layoutChart } from "./model.js";
|
|
10
|
+
import { isTacet as isTacetCell, layoutChart } from "./model.js";
|
|
11
11
|
import { speakBar } from "./speech.js";
|
|
12
12
|
import { FIT_STEPS, MIN_BAR, stylesheet } from "./styles.js";
|
|
13
13
|
export function renderChart(text, options = {}) {
|
|
@@ -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 ? ` show-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,65 @@ 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
|
+
// The first drawn row carries what the system shares: its number, ×N, the ending's number.
|
|
175
|
+
const lead = item.rows.findIndex((p) => p.row.kind === "row");
|
|
176
|
+
const rows = item.rows.map((p, i) => p.row.kind === "error"
|
|
177
|
+
? errorHtml(p.row.source, p.row.messages)
|
|
178
|
+
: rowHtml(p.row, columns, settings, { label: p.label, name: p.name, first: i === lead }));
|
|
179
|
+
return `<div class="bm-system" role="group" aria-label="${names}">${rows.join("")}</div>`;
|
|
180
|
+
}
|
|
181
|
+
/** The left column: the bar number, and in a system the part's label. */
|
|
182
|
+
function gutterHtml(row, settings, part) {
|
|
183
|
+
if (!settings.gutter)
|
|
184
|
+
return "";
|
|
185
|
+
const number = settings.barNumbers && (!part || part.first) ? (row.barNumber ?? "") : "";
|
|
186
|
+
const label = part
|
|
187
|
+
? `<span class="bm-part-label" part="part-label">${escapeHtml(part.label)}</span>`
|
|
163
188
|
: "";
|
|
189
|
+
return `<span class="bm-bar-number" part="bar-number" aria-hidden="true">${label}${number}</span>`;
|
|
190
|
+
}
|
|
191
|
+
/** A row's cells; a tacet row says "tacet" in its first bar. */
|
|
192
|
+
function rowCells(row, numerals, ending) {
|
|
164
193
|
let n = row.barNumber;
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
const html =
|
|
194
|
+
const firstTacet = row.cells.findIndex(isTacetCell);
|
|
195
|
+
return row.cells.map((cell, i) => {
|
|
196
|
+
const marks = repeatMarks(row, i) + (i === 0 ? ending : "");
|
|
197
|
+
const html = isTacetCell(cell)
|
|
198
|
+
? tacetHtml(cell, n, marks, i === firstTacet)
|
|
199
|
+
: cellHtml(cell, n, marks, numerals);
|
|
169
200
|
if (n !== null && cell.kind === "slots")
|
|
170
201
|
n++;
|
|
171
202
|
return html;
|
|
172
203
|
});
|
|
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 (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) {
|
|
219
|
+
const opens = leads(part) && (row.opens || typeof row.repeat === "number" || row.repeat === "open");
|
|
220
|
+
return sr(part ? escapeHtml(part.name) : "") + sr(opens ? "repeat start" : "") + sr(ending);
|
|
221
|
+
}
|
|
222
|
+
/** Whether a row says what its system shares: a row on its own, or a system's first drawn row. */
|
|
223
|
+
const leads = (part) => !part || part.first;
|
|
224
|
+
function rowHtml(row, columns, settings, part) {
|
|
225
|
+
const label = row.ending && leads(part) ? endingLabel(row.ending) : null;
|
|
226
|
+
const cells = rowCells(row, settings.numerals, label ? ` data-ending="${label.shown}"` : "");
|
|
173
227
|
const empty = `<span class="bm-empty" aria-hidden="true"></span>`.repeat(Math.max(0, columns - row.cells.length));
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
return `<div class="bm-row${label ? " bm-ending" : ""}">${opens ? `<span class="bm-sr">repeat start</span>` : ""}${ending}${number}${cells.join("")}${empty}<span class="bm-repeat">${rowRepeatHtml(row)}</span></div>`;
|
|
228
|
+
const said = rowOpening(row, part, label?.spoken ?? "");
|
|
229
|
+
return `<div class="${rowClasses(row, part)}">${said}${gutterHtml(row, settings, part)}${cells.join("")}${empty}<span class="bm-repeat">${leads(part) ? rowRepeatHtml(row) : ""}</span></div>`;
|
|
177
230
|
}
|
|
178
231
|
/** An ending's passes as printed, "1.", "1.–3.", "1., 3.", and spoken, "ending for passes 1 to 3". */
|
|
179
232
|
function endingLabel(passes) {
|
|
@@ -202,13 +255,25 @@ function cellHtml(cell, barNumber, marks, numerals) {
|
|
|
202
255
|
const spoken = `<span class="bm-sr">${escapeHtml(speakBar(barNumber, cell, view !== null))}</span>`;
|
|
203
256
|
if (cell.kind !== "slots")
|
|
204
257
|
return betweenBarsHtml(cell, spoken, marks);
|
|
258
|
+
const change = cell.tempoChange ? ` data-tempo-change="${cell.tempoChange}"` : "";
|
|
259
|
+
const [body, end] = barBody(cell, view);
|
|
260
|
+
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>`;
|
|
261
|
+
}
|
|
262
|
+
/** What a bar shows: its meter, its slots (or `%`), and "cut"; then the words after its last slot. */
|
|
263
|
+
function barBody(cell, view) {
|
|
205
264
|
const meter = cell.meter
|
|
206
265
|
? `<span class="bm-meter"><span>${cell.meter.n}</span><span>${cell.meter.d}</span></span>`
|
|
207
266
|
: "";
|
|
208
267
|
const cut = cell.cut ? `<span class="bm-cut">cut</span>` : "";
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
return
|
|
268
|
+
const [written, end] = slotsWithWords(cell, view);
|
|
269
|
+
const slots = cell.simile ? `<span class="bm-simile">%</span>` : written.join("");
|
|
270
|
+
return [`${meter}${slots}${cut}`, end];
|
|
271
|
+
}
|
|
272
|
+
/** A bar where the part rests; the first of a row says so, in its body. */
|
|
273
|
+
function tacetHtml(cell, n, marks, first) {
|
|
274
|
+
const cut = cell.kind === "slots" && cell.cut;
|
|
275
|
+
const spoken = `<span class="bm-sr">bar ${n ?? ""}: tacet${cut ? ", cut on this pass" : ""}</span>`;
|
|
276
|
+
return `<div class="bm-bar bm-tacet-bar" part="bar"${marks}${cut ? " data-cut" : ""}>${spoken}<span class="bm-bar-body" aria-hidden="true">${first ? "tacet" : ""}</span></div>`;
|
|
212
277
|
}
|
|
213
278
|
/** A bar's slots, each under any words before it, and the words after the last slot. */
|
|
214
279
|
function slotsWithWords(cell, view) {
|
|
@@ -319,5 +384,7 @@ function notesHtml(record) {
|
|
|
319
384
|
function diagnosticsHtml(model) {
|
|
320
385
|
if (!model.diagnostics.length)
|
|
321
386
|
return "";
|
|
322
|
-
|
|
387
|
+
// Line 0: about the whole chart, not a line of it.
|
|
388
|
+
const at = (line) => (line > 0 ? `line ${line}: ` : "");
|
|
389
|
+
return `<ul class="bm-diagnostics">${model.diagnostics.map((d) => `<li class="bm-${d.severity}">${at(d.line)}${escapeHtml(d.message)}</li>`).join("")}</ul>`;
|
|
323
390
|
}
|
package/dist/model.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
import type { ChartModel, LayoutOptions } from "./types.js";
|
|
1
|
+
import type { CellModel, ChartModel, LayoutOptions } from "./types.js";
|
|
2
2
|
export declare function layoutChart(text: string, options?: LayoutOptions): ChartModel;
|
|
3
|
+
/** A bar where the part rests, in a system or a part's own view (barmark's grammar 7.4). */
|
|
4
|
+
export declare const isTacet: (c: CellModel) => boolean;
|
package/dist/model.js
CHANGED
|
@@ -4,9 +4,46 @@
|
|
|
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 0: the warning is about the chart, not one of its lines.
|
|
38
|
+
line: 0,
|
|
39
|
+
severity: "warning",
|
|
40
|
+
message: `no part labelled ${part}`,
|
|
41
|
+
token: part,
|
|
42
|
+
};
|
|
43
|
+
return { ...whole, diagnostics: [missing, ...whole.diagnostics] };
|
|
44
|
+
}
|
|
8
45
|
export function layoutChart(text, options = {}) {
|
|
9
|
-
const parse =
|
|
46
|
+
const parse = parseFor(text, options.part);
|
|
10
47
|
const { header, body } = splitHeader(parse.lines);
|
|
11
48
|
const groups = groupLines(body);
|
|
12
49
|
const ctx = {
|
|
@@ -14,17 +51,25 @@ export function layoutChart(text, options = {}) {
|
|
|
14
51
|
source: text.split(/\r?\n/),
|
|
15
52
|
errors: errorsByLine(parse),
|
|
16
53
|
written: new Map(groups.flatMap((g) => (g.heading ? [[g.heading.line, g]] : []))),
|
|
54
|
+
bars: barsByLine(parse),
|
|
17
55
|
};
|
|
18
56
|
const blocks = groups.map((g) => layoutGroup(g, ctx));
|
|
19
57
|
if (options.rehearsalLetters)
|
|
20
58
|
letterBlocks(blocks);
|
|
21
|
-
return {
|
|
59
|
+
return {
|
|
60
|
+
title: parse.title,
|
|
61
|
+
part: parse.part,
|
|
62
|
+
parts: parse.parts,
|
|
63
|
+
header,
|
|
64
|
+
blocks,
|
|
65
|
+
diagnostics: parse.diagnostics,
|
|
66
|
+
};
|
|
22
67
|
}
|
|
23
68
|
/** Rehearsal letters mark position: every section occurrence, and leading bars, in play order. */
|
|
24
69
|
function letterBlocks(blocks) {
|
|
25
70
|
let n = 0;
|
|
26
71
|
for (const b of blocks) {
|
|
27
|
-
if (b.form === null && !b.items.some((i) => i.kind === "row"))
|
|
72
|
+
if (b.form === null && !b.items.some((i) => i.kind === "row" || i.kind === "system"))
|
|
28
73
|
continue;
|
|
29
74
|
b.rehearsal = letters(n++);
|
|
30
75
|
}
|
|
@@ -114,34 +159,61 @@ function substituteText(items, own) {
|
|
|
114
159
|
return next ? [next] : [];
|
|
115
160
|
});
|
|
116
161
|
// 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"));
|
|
162
|
+
const after = Math.max(out.findLastIndex((i) => i.kind === "row" || i.kind === "system"), out.findLastIndex((i) => i.kind === "text"));
|
|
118
163
|
out.splice(after + 1, 0, ...queue);
|
|
119
164
|
return out;
|
|
120
165
|
}
|
|
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;
|
|
166
|
+
/** The row's last bar, marked cut. */
|
|
167
|
+
function cutRow(row) {
|
|
127
168
|
const c = row.cells.findLastIndex((cell) => cell.kind === "slots");
|
|
128
169
|
const cells = row.cells.map((cell, i) => i === c && cell.kind === "slots" ? { ...cell, cut: true } : cell);
|
|
129
|
-
return
|
|
170
|
+
return { ...row, cells };
|
|
171
|
+
}
|
|
172
|
+
const hasBars = (i) => (i.kind === "row" && lastSlots(i.cells) !== null) || i.kind === "system";
|
|
173
|
+
/** The parser cuts the last bar it would play; a fermata is not a bar. A system's every part loses it. */
|
|
174
|
+
function markCut(items) {
|
|
175
|
+
const r = items.findLastIndex(hasBars);
|
|
176
|
+
const item = items[r];
|
|
177
|
+
if (!item || (item.kind !== "row" && item.kind !== "system"))
|
|
178
|
+
return items;
|
|
179
|
+
const cut = item.kind === "row"
|
|
180
|
+
? cutRow(item)
|
|
181
|
+
: {
|
|
182
|
+
...item,
|
|
183
|
+
rows: item.rows.map((p) => (p.row.kind === "row" ? { ...p, row: cutRow(p.row) } : p)),
|
|
184
|
+
};
|
|
185
|
+
return items.map((it, i) => (i === r ? cut : it));
|
|
130
186
|
}
|
|
187
|
+
/** How many cells an item's widest row has. */
|
|
188
|
+
const widthOf = (i) => i.kind === "row"
|
|
189
|
+
? i.cells.length
|
|
190
|
+
: i.kind === "system"
|
|
191
|
+
? Math.max(0, ...i.rows.map((p) => (p.row.kind === "row" ? p.row.cells.length : 0)))
|
|
192
|
+
: 0;
|
|
131
193
|
function blockOf(h, occurrence, items) {
|
|
132
194
|
return {
|
|
133
195
|
heading: h?.name ?? null,
|
|
134
196
|
repeat: h?.repeat ?? null,
|
|
135
197
|
form: occurrence?.form ?? null,
|
|
136
198
|
rehearsal: null,
|
|
137
|
-
columns: Math.max(0, ...items.map(
|
|
199
|
+
columns: Math.max(0, ...items.map(widthOf)),
|
|
138
200
|
items,
|
|
139
201
|
};
|
|
140
202
|
}
|
|
141
203
|
function layoutLines(lines, occurrence, ctx) {
|
|
142
204
|
const items = [];
|
|
143
205
|
let previous = null;
|
|
144
|
-
for (const l of lines) {
|
|
206
|
+
for (const [i, l] of lines.entries()) {
|
|
207
|
+
const system = systemOf(l);
|
|
208
|
+
if (system !== null) {
|
|
209
|
+
// A system is laid out once, at its first line.
|
|
210
|
+
if (systemOf(lines[i - 1]) === system)
|
|
211
|
+
continue;
|
|
212
|
+
const item = layoutSystem(systemLines(lines, i), occurrence, previous, ctx);
|
|
213
|
+
previous = lastOfSystem(item) ?? previous;
|
|
214
|
+
items.push(item);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
145
217
|
if (ctx.errors.has(l.line)) {
|
|
146
218
|
items.push(errorItem(l.line, ctx));
|
|
147
219
|
continue;
|
|
@@ -155,10 +227,66 @@ function layoutLines(lines, occurrence, ctx) {
|
|
|
155
227
|
}
|
|
156
228
|
return items;
|
|
157
229
|
}
|
|
230
|
+
const systemOf = (l) => (l?.kind === "bars" || l?.kind === "invalid") && l.system !== undefined ? l.system : null;
|
|
231
|
+
/** The lines of the system that starts at `i`. */
|
|
232
|
+
function systemLines(lines, i) {
|
|
233
|
+
const system = systemOf(lines[i]);
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const l of lines.slice(i)) {
|
|
236
|
+
if (systemOf(l) !== system)
|
|
237
|
+
break;
|
|
238
|
+
out.push(l);
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
const lastOfSystem = (item) => {
|
|
243
|
+
const first = item.rows.find((p) => p.row.kind === "row" && !p.row.cells.some(isTacet))?.row;
|
|
244
|
+
return first?.kind === "row" ? lastSlots(first.cells) : null;
|
|
245
|
+
};
|
|
246
|
+
/** A bar where the part rests, in a system or a part's own view (barmark's grammar 7.4). */
|
|
247
|
+
export const isTacet = (c) => c.kind === "slots" && c.tacet === true;
|
|
248
|
+
/** A row per declared part, in score order: its own line, its error, or tacet bars. */
|
|
249
|
+
function layoutSystem(lines, occurrence, previous, ctx) {
|
|
250
|
+
const ref = lines.find((l) => l.kind === "bars" && !ctx.errors.has(l.line));
|
|
251
|
+
const reference = ref
|
|
252
|
+
? layoutRow(ref, playedOn(ref.line, occurrence, ctx.bars), previous, ctx)
|
|
253
|
+
: null;
|
|
254
|
+
const rows = ctx.parse.parts.map((p) => {
|
|
255
|
+
const own = lines.find((l) => l.part === p.label);
|
|
256
|
+
const row = own && ctx.errors.has(own.line)
|
|
257
|
+
? errorItem(own.line, ctx)
|
|
258
|
+
: own?.kind === "bars"
|
|
259
|
+
? layoutRow(own, playedOn(own.line, occurrence, ctx.bars), previous, ctx)
|
|
260
|
+
: tacetRow(reference, lines[0]?.line ?? 0);
|
|
261
|
+
return { label: p.label ?? "", name: p.name, row };
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
kind: "system",
|
|
265
|
+
line: lines[0]?.line ?? 0,
|
|
266
|
+
barNumber: reference?.barNumber ?? null,
|
|
267
|
+
rows,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const NO_ROW = {
|
|
271
|
+
kind: "row",
|
|
272
|
+
line: 0,
|
|
273
|
+
barNumber: null,
|
|
274
|
+
opens: false,
|
|
275
|
+
closes: false,
|
|
276
|
+
repeat: null,
|
|
277
|
+
ending: null,
|
|
278
|
+
cells: [],
|
|
279
|
+
};
|
|
280
|
+
/** The system's bars, with nothing played in them (barmark's grammar 7.4). */
|
|
281
|
+
function tacetRow(reference, line) {
|
|
282
|
+
const base = reference ?? NO_ROW;
|
|
283
|
+
const cells = base.cells.map((c) => c.kind === "slots" ? { kind: "slots", meter: c.meter, slots: [], cut: false, tacet: true } : c);
|
|
284
|
+
return { ...base, line, cells };
|
|
285
|
+
}
|
|
158
286
|
function layoutLine(l, occurrence, previous, ctx) {
|
|
159
287
|
switch (l.kind) {
|
|
160
288
|
case "bars":
|
|
161
|
-
return layoutRow(l, playedOn(l.line, occurrence, ctx.
|
|
289
|
+
return layoutRow(l, playedOn(l.line, occurrence, ctx.bars), previous, ctx);
|
|
162
290
|
case "text":
|
|
163
291
|
return textItem(l.text);
|
|
164
292
|
case "directive":
|
|
@@ -193,7 +321,7 @@ function playedOn(line, occurrence, bars) {
|
|
|
193
321
|
function layoutRow(l, played, previous, ctx) {
|
|
194
322
|
const cells = [];
|
|
195
323
|
// A cut bar is not played: what it would play is what the line's bar played when written.
|
|
196
|
-
const written = ctx.
|
|
324
|
+
const written = ctx.bars.filter((b) => b.line === l.line);
|
|
197
325
|
let bar = 0;
|
|
198
326
|
let prev = previous;
|
|
199
327
|
for (const cell of l.cells) {
|
|
@@ -222,8 +350,16 @@ function layoutCell(cell, played, previous) {
|
|
|
222
350
|
return { kind: "fermata" };
|
|
223
351
|
if (cell.kind === "direction" || cell.kind === "rehearsal")
|
|
224
352
|
return cell;
|
|
353
|
+
// Parts that play a tutti bar differently each read their own: draw it as written.
|
|
354
|
+
if (played?.perPart)
|
|
355
|
+
return withPlayed(asWritten(cell, previous), played);
|
|
225
356
|
return withPlayed(layoutBar(cell, played, previous), played);
|
|
226
357
|
}
|
|
358
|
+
/** A tutti bar as written: its own slots, holds as holds, or `%` itself. */
|
|
359
|
+
function asWritten(cell, previous) {
|
|
360
|
+
const bar = layoutBar(cell, undefined, previous);
|
|
361
|
+
return cell.kind === "simile" ? { ...bar, slots: [], simile: true } : bar;
|
|
362
|
+
}
|
|
227
363
|
/** A bar or `%`, with the slots it plays. */
|
|
228
364
|
function layoutBar(cell, played, previous) {
|
|
229
365
|
const own = cell.kind === "slots" ? cell : null;
|
|
@@ -235,8 +371,11 @@ function layoutBar(cell, played, previous) {
|
|
|
235
371
|
...wordsOf(cell),
|
|
236
372
|
};
|
|
237
373
|
}
|
|
238
|
-
/** The words written on a bar,
|
|
239
|
-
const wordsOf = (cell) =>
|
|
374
|
+
/** The words written on a bar, and whether the part rests there, as fields to spread. */
|
|
375
|
+
const wordsOf = (cell) => ({
|
|
376
|
+
...(cell.words && { words: cell.words }),
|
|
377
|
+
...(cell.kind === "slots" && cell.tacet && { tacet: cell.tacet }),
|
|
378
|
+
});
|
|
240
379
|
/** What the parser says about the bar as played: inside a rit. or accel., and its key. */
|
|
241
380
|
function withPlayed(cell, played) {
|
|
242
381
|
const change = played?.tempoChange?.kind;
|
package/dist/speech.js
CHANGED
|
@@ -148,6 +148,9 @@ 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.simile)
|
|
153
|
+
return `${bar}: each part repeats its bar before`;
|
|
151
154
|
const meter = cell.meter ? `, in ${cell.meter.n}/${cell.meter.d}` : "";
|
|
152
155
|
const cut = cell.cut ? ", cut on this pass" : "";
|
|
153
156
|
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.12.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.1"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|