@barmark/render 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,68 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@barmark/render` are recorded here. The format
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
5
+ package uses [semantic versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-10-31
10
+
11
+ The first release: barmark charts as an evenly spaced bar grid, as HTML
12
+ that needs no script or as the `<barmark-chart>` web component. Requires
13
+ `barmark` 0.2.0 or later.
14
+
15
+ ### Added
16
+
17
+ - **`layoutChart(text)`**: the chart as data to draw. Bar lines become rows
18
+ with their play-order bar numbers (numbering stops after an open repeat),
19
+ `%` becomes the bar it copies, and text lines, directives and lines with
20
+ errors keep their place.
21
+ - **Replays are drawn in full** with their own bar numbers. A replay's own
22
+ text lines (verse 2's words) replace the written section's in order;
23
+ `->` marks the bar the parser cut. Optional rehearsal letters (A, B, …,
24
+ AA) mark each section occurrence in play order.
25
+ - **`renderChart(text, options)`** returns the chart as a `<barmark-chart>`
26
+ element holding a declarative shadow root, and `renderChartUnwrapped`
27
+ the bare markup (`bm-` classes) for pages that bring the stylesheet.
28
+ Options: `theme`, `barNumbers`, `rehearsalLetters`, `diagnostics`. All
29
+ chart text is escaped.
30
+ - **Chords in canonical form,** with ♯ and ♭ glyphs and the extension as a
31
+ superscript; single notes ringed; note sets in braces with the root
32
+ underlined; groups under a tuplet count; fermatas, one-bar meters, N.C.,
33
+ silence and `?` each drawn distinctly.
34
+ - **Spoken names:** every bar reads to a screen reader as music ("bar 9: A
35
+ seven sus four, hold"), with sections labelled by their headings and
36
+ repeats spoken ("play 2 times").
37
+ - **Styles, light and dark.** The chart follows the system's colour scheme
38
+ (`theme: "auto"`), or is forced `light` or `dark`; printing is always
39
+ light, and Windows High Contrast keeps bar lines and marks visible. Both
40
+ palettes meet WCAG AA contrast. Everything is sized in em from
41
+ `--barmark-size`, and a section too wide for its container wraps its
42
+ rows rather than scrolling sideways. Pages restyle the chart through
43
+ `--barmark-font`, `--barmark-size`, `--barmark-ink`, `--barmark-paper`,
44
+ `--barmark-muted`, `--barmark-rule` and `--barmark-accent`, or
45
+ `::part()`.
46
+ - **Edwin ships with the package,** unmodified, with its OFL licence
47
+ (`@barmark/render/fonts/Edwin-Roman.otf`). Pass `fontUrl` to emit its
48
+ `@font-face` rule before the chart, or call `fontFaceCss(url)` once per
49
+ page. The family is registered as "barmark Edwin"; without it the chart
50
+ falls back to Georgia or another serif.
51
+ - **`<barmark-chart>`** (`import "@barmark/render/element"`): renders a
52
+ chart from its `src` URL or a child `<script type="text/barmark">`, and
53
+ again when either changes. Attributes (and matching properties):
54
+ `theme`, `bar-numbers="off"`, `rehearsal-letters`. It registers the font
55
+ itself, reuses a server-rendered shadow root (which still follows
56
+ `theme` and `bar-numbers`), fetches a `src` once, renders once per batch
57
+ of changes, and shows a failed load as an error inside the chart.
58
+
59
+ ### Technical
60
+
61
+ - **Browser tests** (Playwright, Chromium) in CI: the chart looks pixel
62
+ for pixel the same on a page with hostile CSS, themes follow the system
63
+ and the page's custom properties, long rows wrap on a phone and at 400%
64
+ zoom without sideways scrolling, the element renders and re-renders, and
65
+ a screen reader hears "bar 9: D". Run them with `bun run test:browser`.
66
+
67
+ [Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.1.0...HEAD
68
+ [0.1.0]: https://github.com/kevinpeckham/barmark/releases/tag/render-v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kevin Peckham
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,133 @@
1
- # Temporary Holding Version
1
+ # @barmark/render
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Renders [barmark](https://github.com/kevinpeckham/barmark) chord charts as
4
+ a clean, evenly spaced bar grid that looks the same on any page, follows
5
+ light and dark mode, scales with the reader's text size, and reads to a
6
+ screen reader as music.
7
+
8
+ ```
9
+ npm install @barmark/render
10
+ ```
11
+
12
+ ## As HTML (no browser JavaScript needed)
13
+
14
+ ```js
15
+ import { renderChart } from "@barmark/render";
16
+
17
+ const html = renderChart(chartText, { fontUrl: "/fonts/Edwin-Roman.otf" });
18
+ ```
19
+
20
+ `renderChart` returns a `<barmark-chart>` element holding a declarative
21
+ shadow root: put it in any page, server-rendered or static, and it shows
22
+ the chart with no script. The shadow root keeps the page's CSS out, so a
23
+ blog's resets and fonts cannot rearrange the bars. Browsers attach a
24
+ declarative shadow root only while parsing HTML: to insert the markup from
25
+ script, use `setHTMLUnsafe` (or the element below), not `innerHTML`.
26
+
27
+ `renderChartUnwrapped(text, options)` returns the same markup without the
28
+ shadow root (classes prefixed `bm-`), for pages that include `stylesheet`
29
+ themselves.
30
+
31
+ ## As a web component
32
+
33
+ ```html
34
+ <script type="module">import "@barmark/render/element";</script>
35
+
36
+ <barmark-chart src="/charts/peaceful-dreams.chart.md"></barmark-chart>
37
+
38
+ <barmark-chart theme="dark" rehearsal-letters>
39
+ <script type="text/barmark">
40
+ ## Verse
41
+ | D | A | Bm | G |
42
+ </script>
43
+ </barmark-chart>
44
+ ```
45
+
46
+ The element renders from its `src` URL, or from a child
47
+ `<script type="text/barmark">` (indentation is removed), and renders again
48
+ whenever either changes; it fetches a `src` once, and renders once
49
+ however many attributes change together. A chart rendered on the server
50
+ by `renderChart` keeps its music until it has a source of its own, and
51
+ still follows `theme` and `bar-numbers`.
52
+
53
+ The element registers the font itself, from the `fonts/` folder beside its
54
+ own module. If your bundler moves that file, register it yourself before
55
+ the element loads, with `<style data-barmark-font>` holding
56
+ `fontFaceCss(yourUrl)`; the element then leaves it alone.
57
+
58
+ ## Options and attributes
59
+
60
+ | `renderChart` option | Element attribute (property) | Default | |
61
+ |---|---|---|---|
62
+ | `theme` | `theme` (`theme`) | `"auto"` | `"auto"` follows the system; `"light"` or `"dark"` force one |
63
+ | `barNumbers` | `bar-numbers="off"` (`barNumbers`) | `true` | bar numbers at the start of each row |
64
+ | `rehearsalLetters` | `rehearsal-letters` (`rehearsalLetters`) | `false` | boxed A, B, C… on each section as played |
65
+ | `diagnostics` | | `false` | list the chart's errors and warnings below it |
66
+ | `fontUrl` | | none | emit Edwin's `@font-face` rule before the chart |
67
+
68
+ ## Styling on purpose
69
+
70
+ Set these custom properties on the element or anything above it:
71
+
72
+ | Property | What it sets |
73
+ |---|---|
74
+ | `--barmark-font` | the chart's typeface |
75
+ | `--barmark-size` | the base size everything scales from (default `1.25rem`) |
76
+ | `--barmark-ink` | chords and text |
77
+ | `--barmark-paper` | the background |
78
+ | `--barmark-muted` | holds, bar numbers, braces, meta lines |
79
+ | `--barmark-rule` | bar lines |
80
+ | `--barmark-accent` | errors |
81
+
82
+ ```css
83
+ barmark-chart {
84
+ --barmark-size: 1rem;
85
+ --barmark-paper: transparent;
86
+ }
87
+ barmark-chart::part(heading) {
88
+ color: teal;
89
+ }
90
+ ```
91
+
92
+ `::part()` names: `title`, `meta`, `heading`, `bar`, `chord`, `text`,
93
+ `bar-number`, `rehearsal`. These, the custom properties and the
94
+ attributes are the public surface: changing them is a breaking change.
95
+
96
+ ## Light, dark and print
97
+
98
+ Both palettes meet WCAG AA contrast (4.5:1 for text). `theme="auto"`
99
+ follows `prefers-color-scheme`; a page with its own dark-mode switch sets
100
+ `theme` or the colour properties. Printing always uses the light palette,
101
+ and Windows High Contrast keeps bar lines and marks visible.
102
+
103
+ ## Accessibility
104
+
105
+ - Everything is sized in em from `--barmark-size`, so text size and zoom
106
+ scale the chart; a section too wide for its container wraps its rows
107
+ instead of scrolling sideways.
108
+ - Every bar has a spoken name built from the parsed chart: "bar 9: A seven
109
+ sus four, hold", "triplet: D, E, F sharp minor", "note E", "C over E".
110
+ Sections are labelled by their headings, and repeats are spoken ("play
111
+ 2 times").
112
+
113
+ ## The font
114
+
115
+ Edwin, MuseScore's text font for music, ships unmodified in
116
+ `@barmark/render/fonts/Edwin-Roman.otf` with its SIL Open Font License
117
+ (`fonts/OFL.txt`). It is registered under the family name "barmark
118
+ Edwin". Serve the file and pass its URL as `fontUrl`, or call
119
+ `fontFaceCss(url)` once per page; without it the chart uses Georgia or
120
+ another serif.
121
+
122
+ ## What it draws
123
+
124
+ Chords appear in their canonical form whatever the author typed (`Cø`
125
+ shows as Cm⁷(♭5)). `%` is drawn as the bar it copies; a replayed section
126
+ is drawn in full with its own bar numbers, and its own lyrics replace the
127
+ written section's. A line with an error shows as written, underlined,
128
+ with its message. `layoutChart(text)` returns all of this as data, if
129
+ you want to draw it yourself.
130
+
131
+ ## License
132
+
133
+ MIT. Edwin is © MuseScore Limited, under the SIL Open Font License 1.1.
@@ -0,0 +1,18 @@
1
+ /** Chords and notes as display pieces: ♯ and ♭ glyphs, the suffix split for superscript. */
2
+ import type { ChordParts } from "barmark";
3
+ export type ChordPieces = {
4
+ root: string;
5
+ /** m, dim, aug or 5: set on the baseline. */
6
+ quality: string;
7
+ /** Extension, sus and tones: set as a superscript. */
8
+ superscript: string;
9
+ bass: string | null;
10
+ };
11
+ /** `F#` → `F♯`, `b9` → `♭9`, `Bbb` → `B𝄫`. */
12
+ export declare function glyphs(text: string): string;
13
+ export declare function typesetChord(parts: ChordParts): ChordPieces;
14
+ /** A note set's label (`E1`, `F#`) as name and octave. */
15
+ export declare function splitPitch(label: string): {
16
+ name: string;
17
+ octave: string | null;
18
+ };
package/dist/chord.js ADDED
@@ -0,0 +1,24 @@
1
+ const ACCIDENTALS = [
2
+ [/##/g, "𝄪"],
3
+ [/bb(?=\d|$|\))/g, "𝄫"],
4
+ [/#/g, "♯"],
5
+ [/(?<=[A-G]|^|\(|,)b/g, "♭"],
6
+ ];
7
+ /** `F#` → `F♯`, `b9` → `♭9`, `Bbb` → `B𝄫`. */
8
+ export function glyphs(text) {
9
+ return ACCIDENTALS.reduce((t, [re, g]) => t.replace(re, g), text);
10
+ }
11
+ export function typesetChord(parts) {
12
+ const tones = parts.tones.length ? `(${parts.tones.join(",")})` : "";
13
+ return {
14
+ root: glyphs(parts.root),
15
+ quality: parts.quality,
16
+ superscript: glyphs(`${parts.extension ?? ""}${parts.sus ?? ""}${tones}`),
17
+ bass: parts.bass ? glyphs(parts.bass) : null,
18
+ };
19
+ }
20
+ /** A note set's label (`E1`, `F#`) as name and octave. */
21
+ export function splitPitch(label) {
22
+ const m = /^([A-G](?:##|bb|#|b)?)(-?\d+)?$/.exec(label);
23
+ return { name: glyphs(m?.[1] ?? label), octave: m?.[2] ?? null };
24
+ }
@@ -0,0 +1,9 @@
1
+ import { type RenderOptions } from "./html.js";
2
+ type Attributes = {
3
+ getAttribute(name: string): string | null;
4
+ hasAttribute(name: string): boolean;
5
+ };
6
+ export declare function optionsFromAttributes(el: Attributes): RenderOptions;
7
+ /** Chart text from a script element: blank edges and common indentation removed. */
8
+ export declare function inlineChartText(text: string): string;
9
+ export {};
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `<barmark-chart>`: renders itself from a `src` URL or from a child
3
+ * `<script type="text/barmark">`, and again whenever either changes. A
4
+ * shadow root rendered on the server is reused. Importing this module
5
+ * registers the element (once) and the font (once per document).
6
+ */
7
+ import { FONT_FAMILY, fontFaceCss } from "./font.js";
8
+ import { loadErrorContent, shadowContent, THEMES } from "./html.js";
9
+ export function optionsFromAttributes(el) {
10
+ const theme = el.getAttribute("theme");
11
+ return {
12
+ theme: THEMES.find((t) => t === theme) ?? "auto",
13
+ barNumbers: el.getAttribute("bar-numbers") !== "off",
14
+ rehearsalLetters: el.hasAttribute("rehearsal-letters"),
15
+ };
16
+ }
17
+ /** Chart text from a script element: blank edges and common indentation removed. */
18
+ export function inlineChartText(text) {
19
+ const lines = text.replace(/^\s*\n|\n\s*$/g, "").split("\n");
20
+ const indents = lines.filter((l) => l.trim()).map((l) => /^[ \t]*/.exec(l)?.[0].length ?? 0);
21
+ const cut = Math.min(...indents, Number.POSITIVE_INFINITY);
22
+ return lines.map((l) => l.slice(Number.isFinite(cut) ? cut : 0).trimEnd()).join("\n");
23
+ }
24
+ function registerFont(doc) {
25
+ if (doc.querySelector("style[data-barmark-font]"))
26
+ return;
27
+ const style = doc.createElement("style");
28
+ style.dataset.barmarkFont = FONT_FAMILY;
29
+ style.textContent = fontFaceCss(new URL("../fonts/Edwin-Roman.otf", import.meta.url).href);
30
+ doc.head.append(style);
31
+ }
32
+ async function fetchText(src) {
33
+ const response = await fetch(src);
34
+ if (!response.ok)
35
+ throw new Error(`${response.status} ${response.statusText}`.trim());
36
+ return response.text();
37
+ }
38
+ function defineElement() {
39
+ class BarmarkChart extends HTMLElement {
40
+ static observedAttributes = ["src", "theme", "bar-numbers", "rehearsal-letters"];
41
+ #observer = new MutationObserver(() => this.#schedule());
42
+ #renders = 0;
43
+ #scheduled = false;
44
+ /** The text fetched for `src`, kept so a change of options does not fetch again. */
45
+ #fetched = null;
46
+ connectedCallback() {
47
+ registerFont(this.ownerDocument);
48
+ this.#observer.observe(this, { childList: true, subtree: true, characterData: true });
49
+ this.#schedule();
50
+ }
51
+ disconnectedCallback() {
52
+ this.#observer.disconnect();
53
+ }
54
+ attributeChangedCallback() {
55
+ if (this.isConnected)
56
+ this.#schedule();
57
+ }
58
+ get src() {
59
+ return this.getAttribute("src") ?? "";
60
+ }
61
+ set src(v) {
62
+ this.setAttribute("src", v);
63
+ }
64
+ get theme() {
65
+ return optionsFromAttributes(this).theme ?? "auto";
66
+ }
67
+ set theme(v) {
68
+ this.setAttribute("theme", v);
69
+ }
70
+ get barNumbers() {
71
+ return optionsFromAttributes(this).barNumbers ?? true;
72
+ }
73
+ set barNumbers(v) {
74
+ if (v)
75
+ this.removeAttribute("bar-numbers");
76
+ else
77
+ this.setAttribute("bar-numbers", "off");
78
+ }
79
+ get rehearsalLetters() {
80
+ return this.hasAttribute("rehearsal-letters");
81
+ }
82
+ set rehearsalLetters(v) {
83
+ this.toggleAttribute("rehearsal-letters", v);
84
+ }
85
+ /** Every change in one task (an upgrade sets each attribute in turn) renders once. */
86
+ #schedule() {
87
+ if (this.#scheduled)
88
+ return;
89
+ this.#scheduled = true;
90
+ queueMicrotask(() => {
91
+ this.#scheduled = false;
92
+ void this.#render();
93
+ });
94
+ }
95
+ async #render() {
96
+ const script = this.querySelector('script[type="text/barmark"]');
97
+ const options = optionsFromAttributes(this);
98
+ // With no source of its own, a server-rendered chart keeps its music and takes the options it can.
99
+ if (!this.src && !script)
100
+ return this.#restyle(options);
101
+ const render = ++this.#renders;
102
+ const content = await this.#content(script, options);
103
+ if (render !== this.#renders)
104
+ return;
105
+ (this.shadowRoot ?? this.attachShadow({ mode: "open" })).innerHTML = content;
106
+ }
107
+ async #content(script, options) {
108
+ if (!this.src)
109
+ return shadowContent(inlineChartText(script?.textContent ?? ""), options);
110
+ try {
111
+ return shadowContent(await this.#text(this.src), options);
112
+ }
113
+ catch (e) {
114
+ this.#fetched = null;
115
+ return loadErrorContent(`Could not load ${this.src}: ${e.message}`, options);
116
+ }
117
+ }
118
+ #text(src) {
119
+ if (this.#fetched?.src !== src)
120
+ this.#fetched = { src, text: fetchText(src) };
121
+ return this.#fetched.text;
122
+ }
123
+ #restyle(options) {
124
+ const chart = this.shadowRoot?.querySelector(".bm-chart");
125
+ chart?.setAttribute("data-theme", options.theme ?? "auto");
126
+ if (options.barNumbers === false)
127
+ chart?.setAttribute("data-bar-numbers", "off");
128
+ else
129
+ chart?.removeAttribute("data-bar-numbers");
130
+ }
131
+ }
132
+ customElements.define("barmark-chart", BarmarkChart);
133
+ }
134
+ if (typeof customElements !== "undefined" && !customElements.get("barmark-chart"))
135
+ defineElement();
@@ -0,0 +1,2 @@
1
+ /** Text from the chart, safe in element content and in quoted attributes. */
2
+ export declare const escapeHtml: (text: string) => string;
package/dist/escape.js ADDED
@@ -0,0 +1,9 @@
1
+ const ENTITIES = {
2
+ "&": "&amp;",
3
+ "<": "&lt;",
4
+ ">": "&gt;",
5
+ '"': "&quot;",
6
+ "'": "&#39;",
7
+ };
8
+ /** Text from the chart, safe in element content and in quoted attributes. */
9
+ export const escapeHtml = (text) => text.replace(/[&<>"']/g, (c) => ENTITIES[c] ?? c);
package/dist/font.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Edwin is registered on the host page, not in the shadow root (some
3
+ * browsers ignore @font-face there), under its own family name so it can
4
+ * never collide with a page's fonts. The file ships unmodified (OFL).
5
+ */
6
+ export declare const FONT_FAMILY = "barmark Edwin";
7
+ /** One @font-face rule for the font at `url`; the URL cannot close the string or the <style>. */
8
+ export declare function fontFaceCss(url: string): string;
package/dist/font.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Edwin is registered on the host page, not in the shadow root (some
3
+ * browsers ignore @font-face there), under its own family name so it can
4
+ * never collide with a page's fonts. The file ships unmodified (OFL).
5
+ */
6
+ export const FONT_FAMILY = "barmark Edwin";
7
+ /** One @font-face rule for the font at `url`; the URL cannot close the string or the <style>. */
8
+ export function fontFaceCss(url) {
9
+ const safe = url.replace(/["\\<>\n\r]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
10
+ return `@font-face{font-family:"${FONT_FAMILY}";src:url("${safe}") format("opentype");font-display:swap}`;
11
+ }
package/dist/html.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ export type RenderOptions = {
2
+ theme?: "auto" | "light" | "dark";
3
+ barNumbers?: boolean;
4
+ rehearsalLetters?: boolean;
5
+ /** List the chart's errors and warnings below it. */
6
+ diagnostics?: boolean;
7
+ /** Where the page serves Edwin-Roman.otf: emits its @font-face rule before the chart. */
8
+ fontUrl?: string;
9
+ };
10
+ export declare function renderChart(text: string, options?: RenderOptions): string;
11
+ export declare const THEMES: readonly ["auto", "light", "dark"];
12
+ /** What goes in the shadow root: the styles and the chart. */
13
+ export declare const shadowContent: (text: string, options?: RenderOptions) => string;
14
+ /** A chart that could not be loaded: the message, shown where the chart would be. */
15
+ export declare const loadErrorContent: (message: string, options?: RenderOptions) => string;
16
+ export declare function renderChartUnwrapped(text: string, options?: RenderOptions): string;