@barmark/render 0.7.2 → 0.8.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,32 @@ package uses [semantic versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.0] - 2026-11-03
10
+
11
+ Requires `barmark` 0.11.0.
12
+
13
+ ### Changed
14
+
15
+ - **Depends on `barmark` `^0.11.0`** (was `^0.10.0`).
16
+
17
+ ### Added
18
+
19
+ - **Endings:** an ending row has a bracket over its bars and a `1.`, `2.`
20
+ or `1.–3.` label; screen readers hear "ending for pass 1".
21
+ - **Directions:** Segno and Coda as their signs, To Coda with the coda
22
+ sign, Fine and the jumps as words. Each is spoken ("dal segno al
23
+ coda"). Bar numbers skip them, since they are not bars.
24
+
25
+ ## [0.7.3] - 2026-11-03
26
+
27
+ Requires `barmark` 0.10.0. No change to the renderer's code: a page that
28
+ installs `barmark` 0.10.0 alongside it gets one copy instead of two.
29
+
30
+ ### Changed
31
+
32
+ - **Depends on `barmark` `^0.10.0`** (was `^0.9.0`, which in 0.x stops
33
+ short of 0.10.0).
34
+
9
35
  ## [0.7.2] - 2026-11-02
10
36
 
11
37
  Requires `barmark` 0.9.0, and shows its swing amount.
@@ -213,7 +239,9 @@ that needs no script or as the `<barmark-chart>` web component. Requires
213
239
  zoom without sideways scrolling, the element renders and re-renders, and
214
240
  a screen reader hears "bar 9: D". Run them with `bun run test:browser`.
215
241
 
216
- [Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.7.2...HEAD
242
+ [Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.8.0...HEAD
243
+ [0.8.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.7.3...render-v0.8.0
244
+ [0.7.3]: https://github.com/kevinpeckham/barmark/compare/render-v0.7.2...render-v0.7.3
217
245
  [0.7.2]: https://github.com/kevinpeckham/barmark/compare/render-v0.7.1...render-v0.7.2
218
246
  [0.7.1]: https://github.com/kevinpeckham/barmark/compare/render-v0.7.0...render-v0.7.1
219
247
  [0.7.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.6.0...render-v0.7.0
package/dist/html.js CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * The chart model as HTML: a <barmark-chart> holding a declarative shadow
3
+ * root, or the bare markup for pages that bring the stylesheet themselves.
4
+ * Every string from the chart is escaped; every bar carries its spoken name.
5
+ */
6
+ import { printDirection, printPasses, } from "barmark";
1
7
  import { glyphs, NUMERALS, splitPitch, typesetChord, typesetNumeral, } from "./chord.js";
2
8
  import { escapeHtml } from "./escape.js";
3
9
  import { fontFaceCss } from "./font.js";
@@ -91,9 +97,11 @@ function fitWidth(block) {
91
97
  return FIT_STEPS.find((step) => step >= total) ?? FIT_STEPS.at(-1) ?? 0;
92
98
  }
93
99
  /** Em per bar: padding, plus each slot's text and the gap after it. */
94
- const cellWidth = (cell) => cell.kind === "fermata"
95
- ? MIN_BAR
96
- : Math.max(MIN_BAR, 1.5 + (cell.meter ? 0.8 : 0) + cell.slots.reduce((w, s) => w + slotWidth(s), 0));
100
+ const cellWidth = (cell) => cell.kind === "direction"
101
+ ? 1 + 0.55 * printDirection(cell.direction).length
102
+ : cell.kind === "fermata"
103
+ ? MIN_BAR
104
+ : Math.max(MIN_BAR, 1.5 + (cell.meter ? 0.8 : 0) + cell.slots.reduce((w, s) => w + slotWidth(s), 0));
97
105
  /** Roughly: 0.65em a baseline character, 0.4em a superscript one, 0.6em between slots. */
98
106
  function slotWidth(slot) {
99
107
  if (slot.kind === "group")
@@ -145,15 +153,28 @@ function rowHtml(row, columns, settings) {
145
153
  ? `<span class="bm-bar-number" part="bar-number" aria-hidden="true">${row.barNumber ?? ""}</span>`
146
154
  : "";
147
155
  let n = row.barNumber;
156
+ const label = row.ending ? endingLabel(row.ending) : null;
148
157
  const cells = row.cells.map((cell, i) => {
149
- const html = cellHtml(cell, n, repeatMarks(row, i), settings.numerals);
158
+ const ending = i === 0 && label ? ` data-ending="${label.shown}"` : "";
159
+ const html = cellHtml(cell, n, repeatMarks(row, i) + ending, settings.numerals);
150
160
  if (n !== null && cell.kind === "slots")
151
161
  n++;
152
162
  return html;
153
163
  });
154
164
  const empty = `<span class="bm-empty" aria-hidden="true"></span>`.repeat(Math.max(0, columns - row.cells.length));
155
165
  const opens = row.opens || typeof row.repeat === "number" || row.repeat === "open";
156
- return `<div class="bm-row">${opens ? `<span class="bm-sr">repeat start</span>` : ""}${number}${cells.join("")}${empty}<span class="bm-repeat">${rowRepeatHtml(row)}</span></div>`;
166
+ const ending = label ? `<span class="bm-sr">${label.spoken}</span>` : "";
167
+ 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>`;
168
+ }
169
+ /** An ending's passes as printed, "1.", "1.–3.", "1., 3.", and spoken, "ending for passes 1 to 3". */
170
+ function endingLabel(passes) {
171
+ const parts = printPasses(passes)
172
+ .split(",")
173
+ .map((part) => part.split("-"));
174
+ const shown = parts.map((p) => p.map((n) => `${n}.`).join("–")).join(", ");
175
+ const said = parts.map((p) => p.join(" to "));
176
+ const list = said.length > 1 ? `${said.slice(0, -1).join(", ")} and ${said.at(-1)}` : said[0];
177
+ return { shown, spoken: `ending for pass${passes.length > 1 ? "es" : ""} ${list}` };
157
178
  }
158
179
  /** Repeat bar lines: ‖: on a row's first bar, :‖ on its last. */
159
180
  function repeatMarks(row, i) {
@@ -170,8 +191,8 @@ function rowRepeatHtml(row) {
170
191
  function cellHtml(cell, barNumber, marks, numerals) {
171
192
  const view = numerals && cell.kind === "slots" && cell.key ? { style: numerals, key: cell.key } : null;
172
193
  const spoken = `<span class="bm-sr">${escapeHtml(speakBar(barNumber, cell, view !== null))}</span>`;
173
- if (cell.kind === "fermata")
174
- return `<div class="bm-bar bm-fermata" part="bar"${marks}>${spoken}<span aria-hidden="true">${FERMATA}</span></div>`;
194
+ if (cell.kind !== "slots")
195
+ return betweenBarsHtml(cell, spoken, marks);
175
196
  const meter = cell.meter
176
197
  ? `<span class="bm-meter"><span>${cell.meter.n}</span><span>${cell.meter.d}</span></span>`
177
198
  : "";
@@ -179,6 +200,23 @@ function cellHtml(cell, barNumber, marks, numerals) {
179
200
  const change = cell.tempoChange ? ` data-tempo-change="${cell.tempoChange}"` : "";
180
201
  return `<div class="bm-bar" part="bar"${cell.cut ? " data-cut" : ""}${change}${marks}>${spoken}<span class="bm-bar-body" aria-hidden="true">${meter}${cell.slots.map((s) => slotHtml(s, view)).join("")}${cut}</span></div>`;
181
202
  }
203
+ /** A fermata or a direction: a place between bars, drawn as its sign. */
204
+ function betweenBarsHtml(cell, spoken, marks) {
205
+ if (cell.kind === "fermata")
206
+ return `<div class="bm-bar bm-fermata" part="bar"${marks}>${spoken}<span aria-hidden="true">${FERMATA}</span></div>`;
207
+ return `<div class="bm-bar bm-direction" part="direction" data-direction="${cell.direction.kind}"${marks}>${spoken}<span aria-hidden="true">${directionGlyph(cell.direction)}</span></div>`;
208
+ }
209
+ /** Segno and coda as the score's signs; To Coda and the jumps as their words. */
210
+ function directionGlyph(d) {
211
+ if (d.kind === "segno")
212
+ return SEGNO;
213
+ if (d.kind === "coda")
214
+ return CODA;
215
+ const words = `<span class="bm-direction-words">${escapeHtml(printDirection(d))}</span>`;
216
+ return d.kind === "to coda" ? `${words} ${CODA}` : words;
217
+ }
218
+ const SEGNO = `<svg class="bm-sign" viewBox="0 0 20 28" width="0.9em" height="1.25em"><path d="M14.5 6.5c-1-3-6.5-3.5-7.5-.5s3 4.5 4.5 5.5 5 3.5 3 7-7.5 2-8-1" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M4 24 16 3" stroke="currentColor" stroke-width="1.6"/><circle cx="4.5" cy="13" r="1.6" fill="currentColor"/><circle cx="15.5" cy="15" r="1.6" fill="currentColor"/></svg>`;
219
+ const CODA = `<svg class="bm-sign" viewBox="0 0 24 24" width="1em" height="1em"><circle cx="12" cy="12" r="6.5" fill="none" stroke="currentColor" stroke-width="2"/><path d="M12 1v22M1 12h22" stroke="currentColor" stroke-width="1.6"/></svg>`;
182
220
  const FERMATA = `<svg class="bm-fermata-glyph" viewBox="0 0 24 14" width="1.4em" height="0.8em"><path d="M2 13 A10 10 0 0 1 22 13" fill="none" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="11" r="1.8" fill="currentColor"/></svg>`;
183
221
  function slotHtml(slot, view) {
184
222
  switch (slot.kind) {
package/dist/model.js CHANGED
@@ -220,7 +220,7 @@ function layoutRow(l, played, previous, ctx) {
220
220
  let prev = previous;
221
221
  for (const cell of l.cells) {
222
222
  const model = layoutCell(cell, played[bar] ?? written[bar], prev);
223
- if (cell.kind !== "fermata")
223
+ if (cell.kind !== "fermata" && cell.kind !== "direction")
224
224
  bar++;
225
225
  if (model.kind === "slots")
226
226
  prev = model;
@@ -234,6 +234,7 @@ function layoutRow(l, played, previous, ctx) {
234
234
  opens: l.opens,
235
235
  closes: l.closes,
236
236
  repeat: l.repeat,
237
+ ending: l.ending,
237
238
  cells,
238
239
  };
239
240
  }
@@ -241,6 +242,8 @@ function layoutRow(l, played, previous, ctx) {
241
242
  function layoutCell(cell, played, previous) {
242
243
  if (cell.kind === "fermata")
243
244
  return { kind: "fermata" };
245
+ if (cell.kind === "direction")
246
+ return cell;
244
247
  const from = cell.kind === "slots" ? cell : previous;
245
248
  return withPlayed({
246
249
  kind: "slots",
package/dist/speech.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Spoken names: how a screen reader reads a chart as music. Built from the
3
3
  * parsed structure, never from the symbol's text.
4
4
  */
5
- import { degreeOf } from "barmark";
5
+ import { degreeOf, } from "barmark";
6
6
  const NUMBERS = {
7
7
  "1": "one",
8
8
  "2": "two",
@@ -118,10 +118,18 @@ export function speakSlot(slot, key) {
118
118
  return `${GROUPS[slot.members.length] ?? `group of ${slot.members.length}`}: ${slot.members.map((m) => speakSlot(m, key)).join(", ")}`;
119
119
  }
120
120
  }
121
+ /** "segno", "to coda", "dal segno al coda", "da capo al fine". */
122
+ function speakDirection(d) {
123
+ if (d.kind !== "jump")
124
+ return d.kind;
125
+ return `${d.to === "start" ? "da capo" : "dal segno"}${d.al ? ` al ${d.al}` : ""}`;
126
+ }
121
127
  /** "bar 9: G, hold, D, hold"; a fermata is held until cued. With `numerals`, each chord's degree comes first. */
122
128
  export function speakBar(barNumber, cell, numerals = false) {
123
129
  if (cell.kind === "fermata")
124
130
  return "fermata: hold until cued";
131
+ if (cell.kind === "direction")
132
+ return speakDirection(cell.direction);
125
133
  const meter = cell.meter ? `, in ${cell.meter.n}/${cell.meter.d}` : "";
126
134
  const cut = cell.cut ? ", cut on this pass" : "";
127
135
  return `bar${barNumber === null ? "" : ` ${barNumber}`}${meter}: ${cell.slots.map((s) => speakSlot(s, numerals ? cell.key : undefined)).join(", ")}${cut}`;
package/dist/styles.js CHANGED
@@ -116,6 +116,12 @@ const css = `
116
116
  .bm-tuplet::after{border-left:none;margin-left:.3em}
117
117
  .bm-meter{display:inline-flex;flex-direction:column;align-items:center;align-self:center;font:700 ${em(SIZES.meter)}/.95 ${SANS};color:var(--bm-muted)}
118
118
  .bm-fermata-glyph{display:block;color:var(--bm-ink)}
119
+ .bm-row>div.bm-direction{min-width:0;padding-inline:.35em;border-right:none;align-items:flex-start;color:var(--bm-ink)}
120
+ .bm-sign{display:inline-block;vertical-align:middle}
121
+ .bm-direction-words{font:italic ${em(SIZES.repeat)}/1.2 Georgia,serif;white-space:nowrap}
122
+ .bm-bar[data-ending]::after{content:attr(data-ending);position:absolute;left:.45em;top:.2em;font:italic .75em/1 Georgia,serif;color:var(--bm-muted)}
123
+ .bm-ending>.bm-bar{border-top:.06em solid var(--bm-muted)}
124
+ .bm-ending>.bm-bar[data-ending]{box-shadow:inset .06em 0 var(--bm-muted)}
119
125
  .bm-bar[data-tempo-change]::before{content:"";position:absolute;inset:.15em 0 auto;border-top:.08em dashed var(--barmark-directive,var(--bm-muted))}
120
126
  .bm-bar[data-cut]{outline:.08em dashed var(--bm-muted);outline-offset:-.25em}
121
127
  .bm-cut{position:absolute;top:.3em;right:.45em;font:${em(SIZES.cut)}/1 ${SANS};color:var(--bm-muted)}
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ChartDiagnostic, Directives, Key, Meter, Repeat, Slot } from "barmark";
1
+ import type { ChartDiagnostic, Direction, Directives, Key, Meter, Repeat, Slot } from "barmark";
2
2
  export type LayoutOptions = {
3
3
  rehearsalLetters?: boolean;
4
4
  };
@@ -30,6 +30,8 @@ export type BlockItem = {
30
30
  opens: boolean;
31
31
  closes: boolean;
32
32
  repeat: Repeat;
33
+ /** The passes an ending plays on; null for any other row. */
34
+ ending: number[] | null;
33
35
  cells: CellModel[];
34
36
  }
35
37
  /** `note` marks a `###` line, whose text is given without the marks. */
@@ -57,4 +59,9 @@ export type CellModel = {
57
59
  key?: Key;
58
60
  } | {
59
61
  kind: "fermata";
62
+ }
63
+ /** A segno, coda or jump between bars (barmark's grammar 5.2). */
64
+ | {
65
+ kind: "direction";
66
+ direction: Direction;
60
67
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barmark/render",
3
- "version": "0.7.2",
3
+ "version": "0.8.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.9.0"
50
+ "barmark": "^0.11.0"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "rm -rf dist && tsc -p tsconfig.build.json",