@barmark/render 0.3.0 → 0.5.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,45 @@ package uses [semantic versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.0] - 2026-11-02
10
+
11
+ The spoken name of a chord, for other tools. barmark.dev's /try page
12
+ uses it.
13
+
14
+ ### Added
15
+
16
+ - **`speakChord(record, key?)`**: the spoken name the chart gives a chord
17
+ ("C minor seven flat five"; with a key, its degree too), for screen
18
+ readers and other tools.
19
+
20
+ ## [0.4.0] - 2026-11-01
21
+
22
+ Chords as Nashville numbers or Roman numerals, and a little air between
23
+ rows. Depends on `barmark` ^0.6.0, for `degreeOf`.
24
+
25
+ ### Added
26
+
27
+ - **A numerals view**: `numerals: "nashville" | "roman"` (the `numerals`
28
+ attribute and property on `<barmark-chart>`). Each chord shows as its
29
+ degree in its bar's key, counted from the tonic against the major
30
+ scale (in A minor, C is ♭3). Nashville keeps the chord's quality and
31
+ superscript (Bm7 in D is 6m⁷, D/F♯ is 1/3); Roman shows minor and
32
+ diminished in lower case (vi⁷, vii°, ♭VII). Bars with no key, note
33
+ sets, N.C., `?` and `--` are unchanged. Screen readers hear the
34
+ numeral and the chord: "bar 5: five, A seven". Uses barmark 0.6.0's
35
+ `degreeOf`; each bar in the model now carries its `key`.
36
+
37
+ ### Changed
38
+
39
+ - **A small gap between rows** (0.35em), so the bar lines of one row no
40
+ longer run into the next.
41
+
42
+ ### Changed
43
+
44
+ - A little space (0.35 of the chord size) between consecutive bar rows,
45
+ and between bars that wrap onto another line on a narrow screen, so
46
+ one row's bar lines no longer run into the next row's.
47
+
9
48
  ## [0.3.0] - 2026-11-01
10
49
 
11
50
  Draws gradual tempo changes. Depends on `barmark` ^0.5.0, which reads
@@ -130,7 +169,9 @@ that needs no script or as the `<barmark-chart>` web component. Requires
130
169
  zoom without sideways scrolling, the element renders and re-renders, and
131
170
  a screen reader hears "bar 9: D". Run them with `bun run test:browser`.
132
171
 
133
- [Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.3.0...HEAD
172
+ [Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.5.0...HEAD
173
+ [0.5.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.4.0...render-v0.5.0
174
+ [0.4.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.3.0...render-v0.4.0
134
175
  [0.3.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.2.1...render-v0.3.0
135
176
  [0.2.1]: https://github.com/kevinpeckham/barmark/compare/render-v0.2.0...render-v0.2.1
136
177
  [0.2.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.1.0...render-v0.2.0
package/README.md CHANGED
@@ -75,10 +75,38 @@ the element loads, with `<style data-barmark-font>` holding
75
75
  | `theme` | `theme` (`theme`) | `"auto"` | `"auto"` follows the system; `"light"` or `"dark"` force one |
76
76
  | `barNumbers` | `bar-numbers="off"` (`barNumbers`) | `true` | bar numbers at the start of each row |
77
77
  | `rehearsalLetters` | `rehearsal-letters` (`rehearsalLetters`) | `false` | boxed A, B, C… on each section as played |
78
+ | `numerals` | `numerals` (`numerals`) | none | `"nashville"` (6m⁷) or `"roman"` (vi⁷): each chord as its degree in its bar's key |
78
79
  | `diagnostics` | | `false` | list the chart's errors and warnings below it |
79
80
  | `fontUrl` | | none | emit Edwin's `@font-face` rule before the chart |
80
81
  | (the `text` argument) | (`text`) | `null` | the chart to draw; wins over `src` and the script child |
81
82
 
83
+ ### Numerals
84
+
85
+ With `numerals`, each chord shows as its degree in the key its bar is
86
+ played in, so a modulation renumbers from its next bar. Degrees count
87
+ from the key's tonic against the major scale, in every mode: in `key:
88
+ Am`, Am is 1m and C is ♭3. Nashville keeps the chord's quality and
89
+ superscript after the number (Bm7 in D is 6m⁷, D/F♯ is 1/3); Roman
90
+ shows minor and diminished in lower case (ii, vii°, ♭VII). A bar with
91
+ no key, note sets, N.C., `?` and `--` look as they always do, and a
92
+ screen reader hears both: "bar 5: five, A seven". Numbered chords carry
93
+ `data-numeral`. It's a view: the music and bar numbers don't change.
94
+
95
+ ### Spoken names
96
+
97
+ `speakChord(record, key?)` gives a chord the name a screen reader hears
98
+ in the chart, for other tools that want to say a chord aloud:
99
+
100
+ ```ts
101
+ import { parseChord } from "barmark";
102
+ import { speakChord } from "@barmark/render";
103
+
104
+ const r = parseChord("Cø");
105
+ if (r.ok) speakChord(r.value); // "C minor seven flat five"
106
+ ```
107
+
108
+ With a key, it speaks the degree too, as the numerals view does.
109
+
82
110
  ## Styling on purpose
83
111
 
84
112
  Set these custom properties on the element or anything above it:
package/dist/chord.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Chords and notes as display pieces: ♯ and ♭ glyphs, the suffix split for superscript. */
2
- import type { ChordParts } from "barmark";
2
+ import { type ChordParts, type Key } from "barmark";
3
3
  export type ChordPieces = {
4
4
  root: string;
5
5
  /** m, dim, aug or 5: set on the baseline. */
@@ -11,6 +11,15 @@ export type ChordPieces = {
11
11
  /** `F#` → `F♯`, `b9` → `♭9`, `Bbb` → `B𝄫`. */
12
12
  export declare function glyphs(text: string): string;
13
13
  export declare function typesetChord(parts: ChordParts): ChordPieces;
14
+ /** How chords read in a key: Nashville numbers (6m⁷) or Roman numerals (vi⁷). */
15
+ export type Numerals = "nashville" | "roman";
16
+ export declare const NUMERALS: readonly Numerals[];
17
+ /**
18
+ * A chord as its degree in `key`: Nashville keeps the chord's quality and
19
+ * superscript after the number; Roman shows minor and diminished in lower
20
+ * case. A slash bass is a number in both. Null if the root has no degree.
21
+ */
22
+ export declare function typesetNumeral(parts: ChordParts, key: Key, style: Numerals): ChordPieces | null;
14
23
  /** A note set's label (`E1`, `F#`) as name and octave. */
15
24
  export declare function splitPitch(label: string): {
16
25
  name: string;
package/dist/chord.js CHANGED
@@ -1,3 +1,5 @@
1
+ /** Chords and notes as display pieces: ♯ and ♭ glyphs, the suffix split for superscript. */
2
+ import { degreeOf } from "barmark";
1
3
  const ACCIDENTALS = [
2
4
  [/##/g, "𝄪"],
3
5
  [/bb(?=\d|$|\))/g, "𝄫"],
@@ -17,6 +19,41 @@ export function typesetChord(parts) {
17
19
  bass: parts.bass ? glyphs(parts.bass) : null,
18
20
  };
19
21
  }
22
+ export const NUMERALS = ["nashville", "roman"];
23
+ const ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII"];
24
+ /** In Roman numerals case carries minor, so only diminished and augmented keep a mark. */
25
+ const ROMAN_QUALITY = {
26
+ "": "",
27
+ m: "",
28
+ dim: "°",
29
+ aug: "+",
30
+ "5": "5",
31
+ };
32
+ const accidentals = (alter) => (alter < 0 ? "b" : "#").repeat(Math.abs(alter));
33
+ /** `{ degree: 7, alter: -1 }` → `♭7`. */
34
+ const degreeText = (d) => glyphs(`${accidentals(d.alter)}${d.degree}`);
35
+ /**
36
+ * A chord as its degree in `key`: Nashville keeps the chord's quality and
37
+ * superscript after the number; Roman shows minor and diminished in lower
38
+ * case. A slash bass is a number in both. Null if the root has no degree.
39
+ */
40
+ export function typesetNumeral(parts, key, style) {
41
+ const degree = degreeOf(parts.root, key);
42
+ if (!degree)
43
+ return null;
44
+ const pieces = typesetChord(parts);
45
+ const bass = parts.bass ? degreeOf(parts.bass, key) : null;
46
+ const withBass = { ...pieces, bass: bass ? degreeText(bass) : pieces.bass };
47
+ if (style === "nashville")
48
+ return { ...withBass, root: degreeText(degree) };
49
+ const numeral = ROMAN[degree.degree - 1] ?? "";
50
+ const lower = parts.quality === "m" || parts.quality === "dim";
51
+ return {
52
+ ...withBass,
53
+ root: glyphs(accidentals(degree.alter)) + (lower ? numeral.toLowerCase() : numeral),
54
+ quality: ROMAN_QUALITY[parts.quality],
55
+ };
56
+ }
20
57
  /** A note set's label (`E1`, `F#`) as name and octave. */
21
58
  export function splitPitch(label) {
22
59
  const m = /^([A-G](?:##|bb|#|b)?)(-?\d+)?$/.exec(label);
package/dist/element.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * `<barmark-chart>`: renders itself from its `text` property, a `src` URL
3
+ * or a child `<script type="text/barmark">`, and again whenever one
4
+ * changes. A shadow root rendered on the server is reused. Importing this
5
+ * module registers the element (once) and the font (once per document).
6
+ */
1
7
  import { type RenderOptions } from "./html.js";
2
8
  type Attributes = {
3
9
  getAttribute(name: string): string | null;
package/dist/element.js CHANGED
@@ -4,14 +4,17 @@
4
4
  * changes. A shadow root rendered on the server is reused. Importing this
5
5
  * module registers the element (once) and the font (once per document).
6
6
  */
7
+ import { NUMERALS } from "./chord.js";
7
8
  import { FONT_FAMILY, fontFaceCss } from "./font.js";
8
9
  import { loadErrorContent, shadowContent, THEMES } from "./html.js";
9
10
  export function optionsFromAttributes(el) {
10
11
  const theme = el.getAttribute("theme");
12
+ const numerals = NUMERALS.find((n) => n === el.getAttribute("numerals"));
11
13
  return {
12
14
  theme: THEMES.find((t) => t === theme) ?? "auto",
13
15
  barNumbers: el.getAttribute("bar-numbers") !== "off",
14
16
  rehearsalLetters: el.hasAttribute("rehearsal-letters"),
17
+ ...(numerals ? { numerals } : {}),
15
18
  };
16
19
  }
17
20
  /** What a chart draws: its `text` while that is a string, else `src`, else its script child. */
@@ -45,7 +48,7 @@ async function fetchText(src) {
45
48
  }
46
49
  function defineElement() {
47
50
  class BarmarkChart extends HTMLElement {
48
- static observedAttributes = ["src", "theme", "bar-numbers", "rehearsal-letters"];
51
+ static observedAttributes = ["src", "theme", "bar-numbers", "rehearsal-letters", "numerals"];
49
52
  #observer = new MutationObserver(() => this.#schedule());
50
53
  #renders = 0;
51
54
  #scheduled = false;
@@ -106,6 +109,16 @@ function defineElement() {
106
109
  set rehearsalLetters(v) {
107
110
  this.toggleAttribute("rehearsal-letters", v);
108
111
  }
112
+ /** "nashville", "roman", or null for chord names. */
113
+ get numerals() {
114
+ return optionsFromAttributes(this).numerals ?? null;
115
+ }
116
+ set numerals(v) {
117
+ if (v === null)
118
+ this.removeAttribute("numerals");
119
+ else
120
+ this.setAttribute("numerals", v);
121
+ }
109
122
  /** Every change in one task (an upgrade sets each attribute in turn) renders once. */
110
123
  #schedule() {
111
124
  if (this.#scheduled)
package/dist/html.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type Numerals } from "./chord.js";
1
2
  export type RenderOptions = {
2
3
  theme?: "auto" | "light" | "dark";
3
4
  barNumbers?: boolean;
@@ -6,6 +7,8 @@ export type RenderOptions = {
6
7
  diagnostics?: boolean;
7
8
  /** Where the page serves Edwin-Roman.otf: emits its @font-face rule before the chart. */
8
9
  fontUrl?: string;
10
+ /** Show each chord as its degree in the bar's key, where the chart names one. Off by default. */
11
+ numerals?: Numerals;
9
12
  };
10
13
  export declare function renderChart(text: string, options?: RenderOptions): string;
11
14
  export declare const THEMES: readonly ["auto", "light", "dark"];
package/dist/html.js CHANGED
@@ -1,4 +1,4 @@
1
- import { glyphs, splitPitch, typesetChord } from "./chord.js";
1
+ import { glyphs, NUMERALS, splitPitch, typesetChord, typesetNumeral, } from "./chord.js";
2
2
  import { escapeHtml } from "./escape.js";
3
3
  import { fontFaceCss } from "./font.js";
4
4
  import { layoutChart } from "./model.js";
@@ -14,11 +14,13 @@ function elementAttributes(options) {
14
14
  theme === "auto" ? "" : ` theme="${theme}"`,
15
15
  options.barNumbers === false ? ' bar-numbers="off"' : "",
16
16
  options.rehearsalLetters ? " rehearsal-letters" : "",
17
+ numeralsOf(options) ? ` numerals="${numeralsOf(options)}"` : "",
17
18
  ].join("");
18
19
  }
19
20
  export const THEMES = ["auto", "light", "dark"];
20
21
  /** Only a known theme reaches the markup: hosts may pass any string through. */
21
22
  const themeOf = (options) => THEMES.find((t) => t === options.theme) ?? "auto";
23
+ const numeralsOf = (options) => NUMERALS.find((n) => n === options.numerals) ?? null;
22
24
  /** What goes in the shadow root: the styles and the chart. */
23
25
  export const shadowContent = (text, options = {}) => `<style>${stylesheet}</style>${chartHtml(text, options)}`;
24
26
  /** A chart that could not be loaded: the message, shown where the chart would be. */
@@ -29,7 +31,10 @@ export function renderChartUnwrapped(text, options = {}) {
29
31
  }
30
32
  function chartHtml(text, options) {
31
33
  const model = layoutChart(text, { rehearsalLetters: options.rehearsalLetters ?? false });
32
- const settings = { barNumbers: options.barNumbers ?? true };
34
+ const settings = {
35
+ barNumbers: options.barNumbers ?? true,
36
+ numerals: numeralsOf(options),
37
+ };
33
38
  return `<div class="bm-chart" data-theme="${themeOf(options)}"${settings.barNumbers ? "" : ' data-bar-numbers="off"'}>${headerHtml(model)}${model.blocks.map((b) => blockHtml(b, settings)).join("")}${options.diagnostics ? diagnosticsHtml(model) : ""}</div>`;
34
39
  }
35
40
  function headerHtml(model) {
@@ -140,7 +145,7 @@ function rowHtml(row, columns, settings) {
140
145
  : "";
141
146
  let n = row.barNumber;
142
147
  const cells = row.cells.map((cell, i) => {
143
- const html = cellHtml(cell, n, repeatMarks(row, i));
148
+ const html = cellHtml(cell, n, repeatMarks(row, i), settings.numerals);
144
149
  if (n !== null && cell.kind === "slots")
145
150
  n++;
146
151
  return html;
@@ -161,8 +166,9 @@ function rowRepeatHtml(row) {
161
166
  return row.closes ? `<span class="bm-sr">repeat end, play 2 times</span>` : "";
162
167
  return repeatHtml(row.repeat).trim();
163
168
  }
164
- function cellHtml(cell, barNumber, marks) {
165
- const spoken = `<span class="bm-sr">${escapeHtml(speakBar(barNumber, cell))}</span>`;
169
+ function cellHtml(cell, barNumber, marks, numerals) {
170
+ const view = numerals && cell.kind === "slots" && cell.key ? { style: numerals, key: cell.key } : null;
171
+ const spoken = `<span class="bm-sr">${escapeHtml(speakBar(barNumber, cell, view !== null))}</span>`;
166
172
  if (cell.kind === "fermata")
167
173
  return `<div class="bm-bar bm-fermata" part="bar"${marks}>${spoken}<span aria-hidden="true">${FERMATA}</span></div>`;
168
174
  const meter = cell.meter
@@ -170,13 +176,13 @@ function cellHtml(cell, barNumber, marks) {
170
176
  : "";
171
177
  const cut = cell.cut ? `<span class="bm-cut">cut</span>` : "";
172
178
  const change = cell.tempoChange ? ` data-tempo-change="${cell.tempoChange}"` : "";
173
- 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(slotHtml).join("")}${cut}</span></div>`;
179
+ 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>`;
174
180
  }
175
181
  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>`;
176
- function slotHtml(slot) {
182
+ function slotHtml(slot, view) {
177
183
  switch (slot.kind) {
178
184
  case "chord":
179
- return chordHtml(slot.chord);
185
+ return chordHtml(slot.chord, view);
180
186
  case "hold":
181
187
  return `<span class="bm-hold">/</span>`;
182
188
  case "nc":
@@ -186,17 +192,22 @@ function slotHtml(slot) {
186
192
  case "unknown":
187
193
  return `<span class="bm-unknown">?</span>`;
188
194
  case "group":
189
- return `<span class="bm-group" data-count="${slot.members.length}"><span class="bm-tuplet">${slot.members.length}</span>${slot.members.map(slotHtml).join("")}</span>`;
195
+ return `<span class="bm-group" data-count="${slot.members.length}"><span class="bm-tuplet">${slot.members.length}</span>${slot.members.map((m) => slotHtml(m, view)).join("")}</span>`;
190
196
  }
191
197
  }
192
- function chordHtml(record) {
198
+ function chordHtml(record, view) {
193
199
  if (record.kind === "notes" || !record.parts)
194
200
  return notesHtml(record);
195
- const p = typesetChord(record.parts);
201
+ const numeral = view ? typesetNumeral(record.parts, view.key, view.style) : null;
202
+ if (!numeral)
203
+ return piecesHtml(typesetChord(record.parts), "");
204
+ return piecesHtml(numeral, ` data-numeral="${view?.style}"`);
205
+ }
206
+ function piecesHtml(p, attributes) {
196
207
  const quality = p.quality ? `<span class="bm-quality">${p.quality}</span>` : "";
197
208
  const sup = p.superscript ? `<sup>${escapeHtml(p.superscript)}</sup>` : "";
198
209
  const bass = p.bass ? `<span class="bm-bass">/${escapeHtml(p.bass)}</span>` : "";
199
- return `<span class="bm-chord" part="chord">${escapeHtml(p.root)}${quality}${sup}${bass}</span>`;
210
+ return `<span class="bm-chord" part="chord"${attributes}>${escapeHtml(p.root)}${quality}${sup}${bass}</span>`;
200
211
  }
201
212
  /** A single note is ringed; a set sits in faint braces with its root underlined. */
202
213
  function notesHtml(record) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ export type { Numerals } from "./chord.js";
1
2
  export { FONT_FAMILY, fontFaceCss } from "./font.js";
2
3
  export { type RenderOptions, renderChart, renderChartUnwrapped } from "./html.js";
3
4
  export { layoutChart } from "./model.js";
5
+ export { speakChord } from "./speech.js";
4
6
  export { stylesheet } from "./styles.js";
5
7
  export type * from "./types.js";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { FONT_FAMILY, fontFaceCss } from "./font.js";
2
2
  export { renderChart, renderChartUnwrapped } from "./html.js";
3
3
  export { layoutChart } from "./model.js";
4
+ export { speakChord } from "./speech.js";
4
5
  export { stylesheet } from "./styles.js";
package/dist/model.js CHANGED
@@ -239,15 +239,16 @@ function layoutCell(cell, played, previous) {
239
239
  if (cell.kind === "fermata")
240
240
  return { kind: "fermata" };
241
241
  const from = cell.kind === "slots" ? cell : previous;
242
- return withTempoChange({
242
+ return withPlayed({
243
243
  kind: "slots",
244
244
  meter: from?.meter ?? null,
245
245
  slots: played?.slots ?? (cell.kind === "slots" ? cell.slots : []),
246
246
  cut: false,
247
247
  }, played);
248
248
  }
249
- /** Marks a bar played inside a rit. or accel. */
250
- function withTempoChange(cell, played) {
249
+ /** What the parser says about the bar as played: inside a rit. or accel., and its key. */
250
+ function withPlayed(cell, played) {
251
251
  const change = played?.tempoChange?.kind;
252
- return change ? { ...cell, tempoChange: change } : cell;
252
+ const key = played?.key;
253
+ return { ...cell, ...(change ? { tempoChange: change } : {}), ...(key ? { key } : {}) };
253
254
  }
package/dist/speech.d.ts CHANGED
@@ -2,9 +2,10 @@
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 type { ChordRecord, Slot } from "barmark";
5
+ import { type ChordRecord, type Key, type Slot } from "barmark";
6
6
  import type { CellModel } from "./types.js";
7
- export declare function speakChord(record: ChordRecord): string;
8
- export declare function speakSlot(slot: Slot): string;
9
- /** "bar 9: G, hold, D, hold"; a fermata is held until cued. */
10
- export declare function speakBar(barNumber: number | null, cell: CellModel): string;
7
+ /** A chord's name; with a key, its degree first: "five, A seven". */
8
+ export declare function speakChord(record: ChordRecord, key?: Key): string;
9
+ export declare function speakSlot(slot: Slot, key?: Key): string;
10
+ /** "bar 9: G, hold, D, hold"; a fermata is held until cued. With `numerals`, each chord's degree comes first. */
11
+ export declare function speakBar(barNumber: number | null, cell: CellModel, numerals?: boolean): string;
package/dist/speech.js CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Spoken names: how a screen reader reads a chart as music. Built from the
3
+ * parsed structure, never from the symbol's text.
4
+ */
5
+ import { degreeOf } from "barmark";
1
6
  const NUMBERS = {
2
7
  "1": "one",
3
8
  "2": "two",
@@ -82,15 +87,25 @@ function speakNotes(record) {
82
87
  const spoken = labels.map((l) => l.endsWith("!") ? `${speakPitch(l.slice(0, -1))} (root)` : speakPitch(l));
83
88
  return `${spoken.length === 1 ? "note" : "notes"} ${spoken.join(", ")}`;
84
89
  }
85
- export function speakChord(record) {
90
+ /** "five", "flat seven": a root's degree in a key. */
91
+ function speakDegree(root, key) {
92
+ const d = degreeOf(root, key);
93
+ if (!d)
94
+ return "";
95
+ const accidental = ACCIDENTAL[(d.alter < 0 ? "b" : "#").repeat(Math.abs(d.alter))] ?? "";
96
+ return `${accidental.trim()} ${number(String(d.degree))}`.trim();
97
+ }
98
+ /** A chord's name; with a key, its degree first: "five, A seven". */
99
+ export function speakChord(record, key) {
86
100
  if (record.kind === "notes" || !record.parts)
87
101
  return speakNotes(record);
88
- return speakParts(record.parts);
102
+ const degree = key ? speakDegree(record.parts.root, key) : "";
103
+ return `${degree ? `${degree}, ` : ""}${speakParts(record.parts)}`;
89
104
  }
90
- export function speakSlot(slot) {
105
+ export function speakSlot(slot, key) {
91
106
  switch (slot.kind) {
92
107
  case "chord":
93
- return speakChord(slot.chord);
108
+ return speakChord(slot.chord, key);
94
109
  case "hold":
95
110
  return "hold";
96
111
  case "nc":
@@ -100,14 +115,14 @@ export function speakSlot(slot) {
100
115
  case "unknown":
101
116
  return "unknown chord";
102
117
  case "group":
103
- return `${GROUPS[slot.members.length] ?? `group of ${slot.members.length}`}: ${slot.members.map(speakSlot).join(", ")}`;
118
+ return `${GROUPS[slot.members.length] ?? `group of ${slot.members.length}`}: ${slot.members.map((m) => speakSlot(m, key)).join(", ")}`;
104
119
  }
105
120
  }
106
- /** "bar 9: G, hold, D, hold"; a fermata is held until cued. */
107
- export function speakBar(barNumber, cell) {
121
+ /** "bar 9: G, hold, D, hold"; a fermata is held until cued. With `numerals`, each chord's degree comes first. */
122
+ export function speakBar(barNumber, cell, numerals = false) {
108
123
  if (cell.kind === "fermata")
109
124
  return "fermata: hold until cued";
110
125
  const meter = cell.meter ? `, in ${cell.meter.n}/${cell.meter.d}` : "";
111
126
  const cut = cell.cut ? ", cut on this pass" : "";
112
- return `bar${barNumber === null ? "" : ` ${barNumber}`}${meter}: ${cell.slots.map(speakSlot).join(", ")}${cut}`;
127
+ return `bar${barNumber === null ? "" : ` ${barNumber}`}${meter}: ${cell.slots.map((s) => speakSlot(s, numerals ? cell.key : undefined)).join(", ")}${cut}`;
113
128
  }
package/dist/styles.js CHANGED
@@ -31,6 +31,8 @@ const SIZES = {
31
31
  const GUTTER = 2.06;
32
32
  export const MIN_BAR = 4.68;
33
33
  const HEADING_GAP = 0.645;
34
+ /** Space between consecutive bar rows, so one row's bar lines don't run into the next. */
35
+ const ROW_GAP = 0.35;
34
36
  /**
35
37
  * The triplet bracket, in chord sizes: its line sits this far below the top
36
38
  * of the number, with hooks this long, drawn either side of the number so
@@ -83,6 +85,8 @@ const css = `
83
85
  .bm-grid{display:grid;grid-template-columns:${em(GUTTER)} repeat(var(--bm-cols),auto) auto;width:max-content;max-width:100%}
84
86
  .bm-chart[data-bar-numbers="off"] .bm-grid{grid-template-columns:repeat(var(--bm-cols),auto) auto}
85
87
  .bm-row{display:contents}
88
+ .bm-row+.bm-row>*{margin-top:${em(ROW_GAP)}}
89
+ .bm-row+.bm-row>.bm-bar-number{margin-top:${at(SIZES.barNumber, ROW_GAP)}}
86
90
  .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)}
87
91
  .bm-bar-number::before{content:"\\200b";font:${em(1 / SIZES.barNumber)}/1.3 ${CHART_FONT}}
88
92
  .bm-chart[data-bar-numbers="off"] :is(.bm-heading,.bm-text,.bm-directive){margin-left:0}
@@ -128,7 +132,7 @@ const css = `
128
132
  }
129
133
  ${FIT_STEPS.map((w) => {
130
134
  const g = `.bm-grid[data-fit="${w}"]`;
131
- return `@container bm (max-width:${w}em){${g}{display:block}${g}>.bm-row{display:flex;flex-wrap:wrap}${g} .bm-empty{display:none}${g} .bm-bar{flex:0 1 auto;max-width:100%;border-left:.075em solid var(--bm-rule);margin-left:-.075em}${g} :is(.bm-bar-body,.bm-group,.bm-notes){flex-wrap:wrap}${g} .bm-bar-body>*{flex:1 1 auto;min-width:auto}}`;
135
+ return `@container bm (max-width:${w}em){${g}{display:block}${g}>.bm-row{display:flex;flex-wrap:wrap;row-gap:${em(ROW_GAP)}}${g} .bm-empty{display:none}${g} .bm-bar{flex:0 1 auto;max-width:100%;border-left:.075em solid var(--bm-rule);margin-left:-.075em}${g} :is(.bm-bar-body,.bm-group,.bm-notes){flex-wrap:wrap}${g} .bm-bar-body>*{flex:1 1 auto;min-width:auto}}`;
132
136
  }).join("\n")}
133
137
  `;
134
138
  /** Collapse the readable source above: whitespace around punctuation and between rules. */
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ChartDiagnostic, Directives, Meter, Repeat, Slot } from "barmark";
1
+ import type { ChartDiagnostic, Directives, Key, Meter, Repeat, Slot } from "barmark";
2
2
  export type LayoutOptions = {
3
3
  rehearsalLetters?: boolean;
4
4
  };
@@ -53,6 +53,8 @@ export type CellModel = {
53
53
  cut: boolean;
54
54
  /** Set on a bar played inside a rit. or accel. */
55
55
  tempoChange?: "rit" | "accel";
56
+ /** The key the bar is played in, when the chart names one. */
57
+ key?: Key;
56
58
  } | {
57
59
  kind: "fermata";
58
60
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barmark/render",
3
- "version": "0.3.0",
3
+ "version": "0.5.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.5.0"
50
+ "barmark": "^0.6.0"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "rm -rf dist && tsc -p tsconfig.build.json",