@barmark/render 0.2.1 → 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 +44 -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 +36 -13
- package/dist/index.d.ts +1 -0
- package/dist/model.js +8 -2
- package/dist/speech.d.ts +6 -5
- package/dist/speech.js +23 -8
- package/dist/styles.js +6 -1
- package/dist/types.d.ts +5 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,47 @@ 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
|
+
|
|
37
|
+
## [0.3.0] - 2026-11-01
|
|
38
|
+
|
|
39
|
+
Draws gradual tempo changes. Depends on `barmark` ^0.5.0, which reads
|
|
40
|
+
them.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- Gradual tempo changes. A `tempo: rit. to 90` line reads
|
|
45
|
+
"→ Tempo rit. to ♩ = 90" (`a tempo` reads as itself), and every bar
|
|
46
|
+
played inside a rit. or accel. has a dashed line along its top, in
|
|
47
|
+
the directive colour, with `data-tempo-change="rit"` or `"accel"`.
|
|
48
|
+
Uses barmark 0.5.0's `tempoChange`.
|
|
49
|
+
|
|
9
50
|
## [0.2.1] - 2026-11-01
|
|
10
51
|
|
|
11
52
|
Two fixes found while moving barmark.dev to the element. Depends on
|
|
@@ -117,7 +158,9 @@ that needs no script or as the `<barmark-chart>` web component. Requires
|
|
|
117
158
|
zoom without sideways scrolling, the element renders and re-renders, and
|
|
118
159
|
a screen reader hears "bar 9: D". Run them with `bun run test:browser`.
|
|
119
160
|
|
|
120
|
-
[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
|
|
163
|
+
[0.3.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.2.1...render-v0.3.0
|
|
121
164
|
[0.2.1]: https://github.com/kevinpeckham/barmark/compare/render-v0.2.0...render-v0.2.1
|
|
122
165
|
[0.2.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.1.0...render-v0.2.0
|
|
123
166
|
[0.1.0]: https://github.com/kevinpeckham/barmark/releases/tag/render-v0.1.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) {
|
|
@@ -41,16 +46,27 @@ function headerHtml(model) {
|
|
|
41
46
|
return "";
|
|
42
47
|
return `<header class="bm-header">${title}${meta ? `<p class="bm-meta" part="meta">${meta}</p>` : ""}</header>`;
|
|
43
48
|
}
|
|
49
|
+
/** "rit. to ♩ = 90", "accel.", "a tempo". */
|
|
50
|
+
function tempoChangeText(c) {
|
|
51
|
+
if (c.kind === "a tempo")
|
|
52
|
+
return c.kind;
|
|
53
|
+
return `${c.kind}.${c.to === null ? "" : ` to ♩ = ${c.to}`}`;
|
|
54
|
+
}
|
|
44
55
|
/** "D major · 4/4 · ♩ = 145 · swing", escaped; labelled, "Key D major · Time 4/4 · …". */
|
|
45
56
|
function metaText(d, labelled = false) {
|
|
46
57
|
return [
|
|
47
58
|
["Key", d.key ? `${glyphs(d.key.tonic)} ${d.key.mode}` : ""],
|
|
48
59
|
["Time", d.time ? `${d.time.n}/${d.time.d}` : ""],
|
|
49
60
|
["Tempo", d.tempo ? `♩ = ${d.tempo}` : ""],
|
|
61
|
+
// "a tempo" names itself; "Tempo a tempo" would stutter.
|
|
62
|
+
[
|
|
63
|
+
d.tempoChange?.kind === "a tempo" ? "" : "Tempo",
|
|
64
|
+
d.tempoChange ? tempoChangeText(d.tempoChange) : "",
|
|
65
|
+
],
|
|
50
66
|
["Feel", d.feel ?? ""],
|
|
51
67
|
]
|
|
52
68
|
.filter(([, value]) => value)
|
|
53
|
-
.map(([label, value]) => escapeHtml(labelled ? `${label} ${value}` : (value ?? "")))
|
|
69
|
+
.map(([label, value]) => escapeHtml(labelled && label ? `${label} ${value}` : (value ?? "")))
|
|
54
70
|
.join(" · ");
|
|
55
71
|
}
|
|
56
72
|
function blockHtml(block, settings) {
|
|
@@ -129,7 +145,7 @@ function rowHtml(row, columns, settings) {
|
|
|
129
145
|
: "";
|
|
130
146
|
let n = row.barNumber;
|
|
131
147
|
const cells = row.cells.map((cell, i) => {
|
|
132
|
-
const html = cellHtml(cell, n, repeatMarks(row, i));
|
|
148
|
+
const html = cellHtml(cell, n, repeatMarks(row, i), settings.numerals);
|
|
133
149
|
if (n !== null && cell.kind === "slots")
|
|
134
150
|
n++;
|
|
135
151
|
return html;
|
|
@@ -150,21 +166,23 @@ function rowRepeatHtml(row) {
|
|
|
150
166
|
return row.closes ? `<span class="bm-sr">repeat end, play 2 times</span>` : "";
|
|
151
167
|
return repeatHtml(row.repeat).trim();
|
|
152
168
|
}
|
|
153
|
-
function cellHtml(cell, barNumber, marks) {
|
|
154
|
-
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>`;
|
|
155
172
|
if (cell.kind === "fermata")
|
|
156
173
|
return `<div class="bm-bar bm-fermata" part="bar"${marks}>${spoken}<span aria-hidden="true">${FERMATA}</span></div>`;
|
|
157
174
|
const meter = cell.meter
|
|
158
175
|
? `<span class="bm-meter"><span>${cell.meter.n}</span><span>${cell.meter.d}</span></span>`
|
|
159
176
|
: "";
|
|
160
177
|
const cut = cell.cut ? `<span class="bm-cut">cut</span>` : "";
|
|
161
|
-
|
|
178
|
+
const change = cell.tempoChange ? ` data-tempo-change="${cell.tempoChange}"` : "";
|
|
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>`;
|
|
162
180
|
}
|
|
163
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>`;
|
|
164
|
-
function slotHtml(slot) {
|
|
182
|
+
function slotHtml(slot, view) {
|
|
165
183
|
switch (slot.kind) {
|
|
166
184
|
case "chord":
|
|
167
|
-
return chordHtml(slot.chord);
|
|
185
|
+
return chordHtml(slot.chord, view);
|
|
168
186
|
case "hold":
|
|
169
187
|
return `<span class="bm-hold">/</span>`;
|
|
170
188
|
case "nc":
|
|
@@ -174,17 +192,22 @@ function slotHtml(slot) {
|
|
|
174
192
|
case "unknown":
|
|
175
193
|
return `<span class="bm-unknown">?</span>`;
|
|
176
194
|
case "group":
|
|
177
|
-
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>`;
|
|
178
196
|
}
|
|
179
197
|
}
|
|
180
|
-
function chordHtml(record) {
|
|
198
|
+
function chordHtml(record, view) {
|
|
181
199
|
if (record.kind === "notes" || !record.parts)
|
|
182
200
|
return notesHtml(record);
|
|
183
|
-
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) {
|
|
184
207
|
const quality = p.quality ? `<span class="bm-quality">${p.quality}</span>` : "";
|
|
185
208
|
const sup = p.superscript ? `<sup>${escapeHtml(p.superscript)}</sup>` : "";
|
|
186
209
|
const bass = p.bass ? `<span class="bm-bass">/${escapeHtml(p.bass)}</span>` : "";
|
|
187
|
-
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>`;
|
|
188
211
|
}
|
|
189
212
|
/** A single note is ringed; a set sits in faint braces with its root underlined. */
|
|
190
213
|
function notesHtml(record) {
|
package/dist/index.d.ts
CHANGED
package/dist/model.js
CHANGED
|
@@ -239,10 +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
|
+
}
|
|
249
|
+
/** What the parser says about the bar as played: inside a rit. or accel., and its key. */
|
|
250
|
+
function withPlayed(cell, played) {
|
|
251
|
+
const change = played?.tempoChange?.kind;
|
|
252
|
+
const key = played?.key;
|
|
253
|
+
return { ...cell, ...(change ? { tempoChange: change } : {}), ...(key ? { key } : {}) };
|
|
248
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}
|
|
@@ -111,6 +115,7 @@ const css = `
|
|
|
111
115
|
.bm-tuplet::after{border-left:none;margin-left:.3em}
|
|
112
116
|
.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)}
|
|
113
117
|
.bm-fermata-glyph{display:block;color:var(--bm-ink)}
|
|
118
|
+
.bm-bar[data-tempo-change]::before{content:"";position:absolute;inset:.15em 0 auto;border-top:.08em dashed var(--barmark-directive,var(--bm-muted))}
|
|
114
119
|
.bm-bar[data-cut]{outline:.08em dashed var(--bm-muted);outline-offset:-.25em}
|
|
115
120
|
.bm-cut{position:absolute;top:.3em;right:.45em;font:${em(SIZES.cut)}/1 ${SANS};color:var(--bm-muted)}
|
|
116
121
|
.bm-text{grid-column:1/-1;margin:.2em 0 .5em ${at(SIZES.text, GUTTER)};font-family:${TEXT};font-size:${em(SIZES.text)}}
|
|
@@ -127,7 +132,7 @@ const css = `
|
|
|
127
132
|
}
|
|
128
133
|
${FIT_STEPS.map((w) => {
|
|
129
134
|
const g = `.bm-grid[data-fit="${w}"]`;
|
|
130
|
-
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}}`;
|
|
131
136
|
}).join("\n")}
|
|
132
137
|
`;
|
|
133
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
|
};
|
|
@@ -51,6 +51,10 @@ export type CellModel = {
|
|
|
51
51
|
meter: Meter | null;
|
|
52
52
|
slots: Slot[];
|
|
53
53
|
cut: boolean;
|
|
54
|
+
/** Set on a bar played inside a rit. or accel. */
|
|
55
|
+
tempoChange?: "rit" | "accel";
|
|
56
|
+
/** The key the bar is played in, when the chart names one. */
|
|
57
|
+
key?: Key;
|
|
54
58
|
} | {
|
|
55
59
|
kind: "fermata";
|
|
56
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",
|