@barmark/render 0.11.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 CHANGED
@@ -6,6 +6,29 @@ 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
+
9
32
  ## [0.11.0] - 2026-11-03
10
33
 
11
34
  Needs `barmark` 0.14.0.
@@ -294,7 +317,8 @@ that needs no script or as the `<barmark-chart>` web component. Requires
294
317
  zoom without sideways scrolling, the element renders and re-renders, and
295
318
  a screen reader hears "bar 9: D". Run them with `bun run test:browser`.
296
319
 
297
- [Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.11.0...HEAD
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
298
322
  [0.11.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.10.0...render-v0.11.0
299
323
  [0.10.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.9.1...render-v0.10.0
300
324
  [0.9.1]: https://github.com/kevinpeckham/barmark/compare/render-v0.9.0...render-v0.9.1
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 `bar-numbers`.
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,7 +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 |
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 |
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 |
81
83
  | `diagnostics` | | `false` | list the chart's errors and warnings below it |
82
84
  | `fontUrl` | | none | emit Edwin's `@font-face` rule before the chart |
83
85
  | (the `text` argument) | (`text`) | `null` | the chart to draw; wins over `src` and the script child |
package/dist/element.js CHANGED
@@ -10,7 +10,8 @@ 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
+ // Not `part`: that global attribute names an element for ::part() styling.
14
+ const part = el.getAttribute("show-part");
14
15
  return {
15
16
  theme: THEMES.find((t) => t === theme) ?? "auto",
16
17
  barNumbers: el.getAttribute("bar-numbers") !== "off",
@@ -58,7 +59,7 @@ function defineElement() {
58
59
  "rehearsal-letters",
59
60
  "numerals",
60
61
  "wrap",
61
- "part",
62
+ "show-part",
62
63
  ];
63
64
  #observer = new MutationObserver(() => this.#schedule());
64
65
  #renders = 0;
@@ -139,6 +140,16 @@ function defineElement() {
139
140
  else
140
141
  this.setAttribute("numerals", v);
141
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
+ }
142
153
  /** Every change in one task (an upgrade sets each attribute in turn) renders once. */
143
154
  #schedule() {
144
155
  if (this.#scheduled)
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,7 +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
+ options.part ? ` show-part="${escapeHtml(options.part)}"` : "",
26
26
  ].join("");
27
27
  }
28
28
  export const THEMES = ["auto", "light", "dark"];
@@ -171,9 +171,11 @@ function errorHtml(source, messages) {
171
171
  /** A system: its parts' rows stacked, sharing the section's columns (barmark's grammar 7.4). */
172
172
  function systemHtml(item, columns, settings) {
173
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");
174
176
  const rows = item.rows.map((p, i) => p.row.kind === "error"
175
177
  ? errorHtml(p.row.source, p.row.messages)
176
- : rowHtml(p.row, columns, settings, { label: p.label, name: p.name, first: i === 0 }));
178
+ : rowHtml(p.row, columns, settings, { label: p.label, name: p.name, first: i === lead }));
177
179
  return `<div class="bm-system" role="group" aria-label="${names}">${rows.join("")}</div>`;
178
180
  }
179
181
  /** The left column: the bar number, and in a system the part's label. */
@@ -186,16 +188,14 @@ function gutterHtml(row, settings, part) {
186
188
  : "";
187
189
  return `<span class="bm-bar-number" part="bar-number" aria-hidden="true">${label}${number}</span>`;
188
190
  }
189
- const isTacetCell = (c) => c.kind === "slots" && c.tacet === true;
190
191
  /** A row's cells; a tacet row says "tacet" in its first bar. */
191
192
  function rowCells(row, numerals, ending) {
192
193
  let n = row.barNumber;
193
194
  const firstTacet = row.cells.findIndex(isTacetCell);
194
195
  return row.cells.map((cell, i) => {
195
196
  const marks = repeatMarks(row, i) + (i === 0 ? ending : "");
196
- const spoken = `<span class="bm-sr">bar ${n ?? ""}: tacet</span>`;
197
197
  const html = isTacetCell(cell)
198
- ? tacetHtml(spoken, marks, i === firstTacet ? "tacet" : "")
198
+ ? tacetHtml(cell, n, marks, i === firstTacet)
199
199
  : cellHtml(cell, n, marks, numerals);
200
200
  if (n !== null && cell.kind === "slots")
201
201
  n++;
@@ -208,7 +208,7 @@ function rowClasses(row, part) {
208
208
  const classes = ["bm-row"];
209
209
  if (part)
210
210
  classes.push("bm-part-row");
211
- if (part && row.cells.some(isTacetCell))
211
+ if (row.cells.some(isTacetCell))
212
212
  classes.push("bm-tacet");
213
213
  if (row.ending)
214
214
  classes.push("bm-ending");
@@ -216,15 +216,17 @@ function rowClasses(row, part) {
216
216
  }
217
217
  /** What a screen reader hears before a row's bars: its part, a repeat start, its ending. */
218
218
  function rowOpening(row, part, ending) {
219
- const opens = row.opens || typeof row.repeat === "number" || row.repeat === "open";
219
+ const opens = leads(part) && (row.opens || typeof row.repeat === "number" || row.repeat === "open");
220
220
  return sr(part ? escapeHtml(part.name) : "") + sr(opens ? "repeat start" : "") + sr(ending);
221
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;
222
224
  function rowHtml(row, columns, settings, part) {
223
- const label = row.ending ? endingLabel(row.ending) : null;
225
+ const label = row.ending && leads(part) ? endingLabel(row.ending) : null;
224
226
  const cells = rowCells(row, settings.numerals, label ? ` data-ending="${label.shown}"` : "");
225
227
  const empty = `<span class="bm-empty" aria-hidden="true"></span>`.repeat(Math.max(0, columns - row.cells.length));
226
228
  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>`;
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>`;
228
230
  }
229
231
  /** An ending's passes as printed, "1.", "1.–3.", "1., 3.", and spoken, "ending for passes 1 to 3". */
230
232
  function endingLabel(passes) {
@@ -268,8 +270,10 @@ function barBody(cell, view) {
268
270
  return [`${meter}${slots}${cut}`, end];
269
271
  }
270
272
  /** 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>`;
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>`;
273
277
  }
274
278
  /** A bar's slots, each under any words before it, and the words after the last slot. */
275
279
  function slotsWithWords(cell, view) {
@@ -380,5 +384,7 @@ function notesHtml(record) {
380
384
  function diagnosticsHtml(model) {
381
385
  if (!model.diagnostics.length)
382
386
  return "";
383
- return `<ul class="bm-diagnostics">${model.diagnostics.map((d) => `<li class="bm-${d.severity}">line ${d.line}: ${escapeHtml(d.message)}</li>`).join("")}</ul>`;
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>`;
384
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
@@ -34,7 +34,8 @@ function parseFor(text, part) {
34
34
  return one ?? parseChart(text);
35
35
  const whole = parseChart(text);
36
36
  const missing = {
37
- line: 1,
37
+ // Line 0: the warning is about the chart, not one of its lines.
38
+ line: 0,
38
39
  severity: "warning",
39
40
  message: `no part labelled ${part}`,
40
41
  token: part,
@@ -242,7 +243,8 @@ const lastOfSystem = (item) => {
242
243
  const first = item.rows.find((p) => p.row.kind === "row" && !p.row.cells.some(isTacet))?.row;
243
244
  return first?.kind === "row" ? lastSlots(first.cells) : null;
244
245
  };
245
- const isTacet = (c) => c.kind === "slots" && c.tacet === true;
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;
246
248
  /** A row per declared part, in score order: its own line, its error, or tacet bars. */
247
249
  function layoutSystem(lines, occurrence, previous, ctx) {
248
250
  const ref = lines.find((l) => l.kind === "bars" && !ctx.errors.has(l.line));
package/dist/speech.js CHANGED
@@ -149,8 +149,6 @@ export function speakBar(barNumber, cell, numerals = false) {
149
149
  if (cell.kind === "rehearsal")
150
150
  return `rehearsal ${cell.label}`;
151
151
  const bar = `bar${barNumber === null ? "" : ` ${barNumber}`}`;
152
- if (cell.tacet)
153
- return `${bar}: tacet`;
154
152
  if (cell.simile)
155
153
  return `${bar}: each part repeats its bar before`;
156
154
  const meter = cell.meter ? `, in ${cell.meter.n}/${cell.meter.d}` : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barmark/render",
3
- "version": "0.11.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.14.0"
50
+ "barmark": "^0.14.1"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "rm -rf dist && tsc -p tsconfig.build.json",