@barmark/render 0.3.0 → 0.4.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 +30 -1
- package/README.md +13 -0
- package/dist/chord.d.ts +10 -1
- package/dist/chord.js +37 -0
- package/dist/element.d.ts +6 -0
- package/dist/element.js +14 -1
- package/dist/html.d.ts +3 -0
- package/dist/html.js +23 -12
- package/dist/index.d.ts +1 -0
- package/dist/model.js +5 -4
- package/dist/speech.d.ts +6 -5
- package/dist/speech.js +23 -8
- package/dist/styles.js +5 -1
- package/dist/types.d.ts +3 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,34 @@ package uses [semantic versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0] - 2026-11-01
|
|
10
|
+
|
|
11
|
+
Chords as Nashville numbers or Roman numerals, and a little air between
|
|
12
|
+
rows. Depends on `barmark` ^0.6.0, for `degreeOf`.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **A numerals view**: `numerals: "nashville" | "roman"` (the `numerals`
|
|
17
|
+
attribute and property on `<barmark-chart>`). Each chord shows as its
|
|
18
|
+
degree in its bar's key, counted from the tonic against the major
|
|
19
|
+
scale (in A minor, C is ♭3). Nashville keeps the chord's quality and
|
|
20
|
+
superscript (Bm7 in D is 6m⁷, D/F♯ is 1/3); Roman shows minor and
|
|
21
|
+
diminished in lower case (vi⁷, vii°, ♭VII). Bars with no key, note
|
|
22
|
+
sets, N.C., `?` and `--` are unchanged. Screen readers hear the
|
|
23
|
+
numeral and the chord: "bar 5: five, A seven". Uses barmark 0.6.0's
|
|
24
|
+
`degreeOf`; each bar in the model now carries its `key`.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **A small gap between rows** (0.35em), so the bar lines of one row no
|
|
29
|
+
longer run into the next.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- A little space (0.35 of the chord size) between consecutive bar rows,
|
|
34
|
+
and between bars that wrap onto another line on a narrow screen, so
|
|
35
|
+
one row's bar lines no longer run into the next row's.
|
|
36
|
+
|
|
9
37
|
## [0.3.0] - 2026-11-01
|
|
10
38
|
|
|
11
39
|
Draws gradual tempo changes. Depends on `barmark` ^0.5.0, which reads
|
|
@@ -130,7 +158,8 @@ that needs no script or as the `<barmark-chart>` web component. Requires
|
|
|
130
158
|
zoom without sideways scrolling, the element renders and re-renders, and
|
|
131
159
|
a screen reader hears "bar 9: D". Run them with `bun run test:browser`.
|
|
132
160
|
|
|
133
|
-
[Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.
|
|
161
|
+
[Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.4.0...HEAD
|
|
162
|
+
[0.4.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.3.0...render-v0.4.0
|
|
134
163
|
[0.3.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.2.1...render-v0.3.0
|
|
135
164
|
[0.2.1]: https://github.com/kevinpeckham/barmark/compare/render-v0.2.0...render-v0.2.1
|
|
136
165
|
[0.2.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.1.0...render-v0.2.0
|
package/README.md
CHANGED
|
@@ -75,10 +75,23 @@ 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
|
+
|
|
82
95
|
## Styling on purpose
|
|
83
96
|
|
|
84
97
|
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
|
|
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 = {
|
|
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
|
|
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
|
|
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
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
|
|
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
|
-
/**
|
|
250
|
-
function
|
|
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
|
-
|
|
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
|
|
5
|
+
import { type ChordRecord, type Key, type Slot } from "barmark";
|
|
6
6
|
import type { CellModel } from "./types.js";
|
|
7
|
-
|
|
8
|
-
export declare function
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"version": "0.4.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.6.0"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|