@barmark/render 0.0.0 → 0.2.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 +104 -0
- package/LICENSE +21 -0
- package/README.md +158 -1
- package/dist/chord.d.ts +18 -0
- package/dist/chord.js +24 -0
- package/dist/element.d.ts +16 -0
- package/dist/element.js +162 -0
- package/dist/escape.d.ts +2 -0
- package/dist/escape.js +9 -0
- package/dist/font.d.ts +8 -0
- package/dist/font.js +11 -0
- package/dist/html.d.ts +16 -0
- package/dist/html.js +200 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +248 -0
- package/dist/speech.d.ts +10 -0
- package/dist/speech.js +113 -0
- package/dist/styles.d.ts +16 -0
- package/dist/styles.js +130 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +1 -0
- package/fonts/Edwin-Roman.otf +0 -0
- package/fonts/OFL.txt +93 -0
- package/fonts/README.md +16 -0
- package/package.json +56 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
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.2.0] - 2026-11-01
|
|
10
|
+
|
|
11
|
+
Easier to read, and the bar numbers easier to scan. Every size below was
|
|
12
|
+
chosen on barmark.dev. The chart looks different, so check pages that
|
|
13
|
+
style it. Requires `barmark` 0.2.0 or later.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **Chords are 16px by default** (`--barmark-size` was `1.25rem`, now `1rem`),
|
|
18
|
+
and no label is under 12px: the meta line, headings, bar numbers, lyrics,
|
|
19
|
+
repeat counts, triplet marks and diagnostics are all larger in proportion.
|
|
20
|
+
- **Bar numbers sit flush left** in a fixed gutter, on their row's chord
|
|
21
|
+
baseline. Section headings, lyrics, note lines and mid-chart changes
|
|
22
|
+
start at the first bar line, so a `3/4` never sits among the numbers.
|
|
23
|
+
- **Chords start at the left** of their share of the bar instead of centred,
|
|
24
|
+
and no bar is narrower than a bar holding D7sus4.
|
|
25
|
+
- **A change written mid-chart names itself** and marks where it takes
|
|
26
|
+
effect: "→ Time 3/4 · Tempo ♩ = 120", with a rule, not a lone "3/4".
|
|
27
|
+
- **`###` note lines are italic** at weight 400 (Edwin has no bold, so 600 was
|
|
28
|
+
faked), and lyrics and note lines use Georgia, which has a true italic.
|
|
29
|
+
- **A single ringed note** has room inside its ring and sits on the chord
|
|
30
|
+
baseline (it was 1px low).
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **A `text` property on `<barmark-chart>`** for live editing: set it and the
|
|
35
|
+
chart redraws, batched with attribute changes. While it is a string it
|
|
36
|
+
wins over `src` (no fetch) and the script child; `null` returns to them.
|
|
37
|
+
Text set before the element is defined is kept.
|
|
38
|
+
- **`--barmark-text-font` and `--barmark-label-font`**: the fonts for lyrics
|
|
39
|
+
and note lines, and for the meta line, mid-chart changes, headings and
|
|
40
|
+
diagnostics.
|
|
41
|
+
- **`--barmark-directive` and `--barmark-note`**: the colours of mid-chart
|
|
42
|
+
changes and of note lines.
|
|
43
|
+
|
|
44
|
+
## [0.1.0] - 2026-10-31
|
|
45
|
+
|
|
46
|
+
The first release: barmark charts as an evenly spaced bar grid, as HTML
|
|
47
|
+
that needs no script or as the `<barmark-chart>` web component. Requires
|
|
48
|
+
`barmark` 0.2.0 or later.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- **`layoutChart(text)`**: the chart as data to draw. Bar lines become rows
|
|
53
|
+
with their play-order bar numbers (numbering stops after an open repeat),
|
|
54
|
+
`%` becomes the bar it copies, and text lines, directives and lines with
|
|
55
|
+
errors keep their place.
|
|
56
|
+
- **Replays are drawn in full** with their own bar numbers. A replay's own
|
|
57
|
+
text lines (verse 2's words) replace the written section's in order;
|
|
58
|
+
`->` marks the bar the parser cut. Optional rehearsal letters (A, B, …,
|
|
59
|
+
AA) mark each section occurrence in play order.
|
|
60
|
+
- **`renderChart(text, options)`** returns the chart as a `<barmark-chart>`
|
|
61
|
+
element holding a declarative shadow root, and `renderChartUnwrapped`
|
|
62
|
+
the bare markup (`bm-` classes) for pages that bring the stylesheet.
|
|
63
|
+
Options: `theme`, `barNumbers`, `rehearsalLetters`, `diagnostics`. All
|
|
64
|
+
chart text is escaped.
|
|
65
|
+
- **Chords in canonical form,** with ♯ and ♭ glyphs and the extension as a
|
|
66
|
+
superscript; single notes ringed; note sets in braces with the root
|
|
67
|
+
underlined; groups under a tuplet count; fermatas, one-bar meters, N.C.,
|
|
68
|
+
silence and `?` each drawn distinctly.
|
|
69
|
+
- **Spoken names:** every bar reads to a screen reader as music ("bar 9: A
|
|
70
|
+
seven sus four, hold"), with sections labelled by their headings and
|
|
71
|
+
repeats spoken ("play 2 times").
|
|
72
|
+
- **Styles, light and dark.** The chart follows the system's colour scheme
|
|
73
|
+
(`theme: "auto"`), or is forced `light` or `dark`; printing is always
|
|
74
|
+
light, and Windows High Contrast keeps bar lines and marks visible. Both
|
|
75
|
+
palettes meet WCAG AA contrast. Everything is sized in em from
|
|
76
|
+
`--barmark-size`, and a section too wide for its container wraps its
|
|
77
|
+
rows rather than scrolling sideways. Pages restyle the chart through
|
|
78
|
+
`--barmark-font`, `--barmark-size`, `--barmark-ink`, `--barmark-paper`,
|
|
79
|
+
`--barmark-muted`, `--barmark-rule` and `--barmark-accent`, or
|
|
80
|
+
`::part()`.
|
|
81
|
+
- **Edwin ships with the package,** unmodified, with its OFL licence
|
|
82
|
+
(`@barmark/render/fonts/Edwin-Roman.otf`). Pass `fontUrl` to emit its
|
|
83
|
+
`@font-face` rule before the chart, or call `fontFaceCss(url)` once per
|
|
84
|
+
page. The family is registered as "barmark Edwin"; without it the chart
|
|
85
|
+
falls back to Georgia or another serif.
|
|
86
|
+
- **`<barmark-chart>`** (`import "@barmark/render/element"`): renders a
|
|
87
|
+
chart from its `src` URL or a child `<script type="text/barmark">`, and
|
|
88
|
+
again when either changes. Attributes (and matching properties):
|
|
89
|
+
`theme`, `bar-numbers="off"`, `rehearsal-letters`. It registers the font
|
|
90
|
+
itself, reuses a server-rendered shadow root (which still follows
|
|
91
|
+
`theme` and `bar-numbers`), fetches a `src` once, renders once per batch
|
|
92
|
+
of changes, and shows a failed load as an error inside the chart.
|
|
93
|
+
|
|
94
|
+
### Technical
|
|
95
|
+
|
|
96
|
+
- **Browser tests** (Playwright, Chromium) in CI: the chart looks pixel
|
|
97
|
+
for pixel the same on a page with hostile CSS, themes follow the system
|
|
98
|
+
and the page's custom properties, long rows wrap on a phone and at 400%
|
|
99
|
+
zoom without sideways scrolling, the element renders and re-renders, and
|
|
100
|
+
a screen reader hears "bar 9: D". Run them with `bun run test:browser`.
|
|
101
|
+
|
|
102
|
+
[Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.2.0...HEAD
|
|
103
|
+
[0.2.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.1.0...render-v0.2.0
|
|
104
|
+
[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 +1,158 @@
|
|
|
1
|
-
|
|
1
|
+
# @barmark/render
|
|
2
|
+
|
|
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 `text` property, its `src` URL, or a child
|
|
47
|
+
`<script type="text/barmark">` (indentation is removed), and renders again
|
|
48
|
+
whenever one changes; it fetches a `src` once, and renders once however
|
|
49
|
+
many properties and attributes change together. A chart rendered on the
|
|
50
|
+
server by `renderChart` keeps its music until it has a source of its own,
|
|
51
|
+
and still follows `theme` and `bar-numbers`.
|
|
52
|
+
|
|
53
|
+
For a live editor, set `text`:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
const chart = document.querySelector("barmark-chart");
|
|
57
|
+
editor.addEventListener("input", () => { chart.text = editor.value; });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
While `text` is a string it wins over `src` and the script child, and no
|
|
61
|
+
fetch happens; set it to `null` to go back to them. Any string is safe:
|
|
62
|
+
the chart escapes it, and problems show as diagnostics, never errors.
|
|
63
|
+
There is no `text` attribute: chart text belongs in the property or the
|
|
64
|
+
script child.
|
|
65
|
+
|
|
66
|
+
The element registers the font itself, from the `fonts/` folder beside its
|
|
67
|
+
own module. If your bundler moves that file, register it yourself before
|
|
68
|
+
the element loads, with `<style data-barmark-font>` holding
|
|
69
|
+
`fontFaceCss(yourUrl)`; the element then leaves it alone.
|
|
70
|
+
|
|
71
|
+
## Options and attributes
|
|
72
|
+
|
|
73
|
+
| `renderChart` option | Element attribute (property) | Default | |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| `theme` | `theme` (`theme`) | `"auto"` | `"auto"` follows the system; `"light"` or `"dark"` force one |
|
|
76
|
+
| `barNumbers` | `bar-numbers="off"` (`barNumbers`) | `true` | bar numbers at the start of each row |
|
|
77
|
+
| `rehearsalLetters` | `rehearsal-letters` (`rehearsalLetters`) | `false` | boxed A, B, C… on each section as played |
|
|
78
|
+
| `diagnostics` | | `false` | list the chart's errors and warnings below it |
|
|
79
|
+
| `fontUrl` | | none | emit Edwin's `@font-face` rule before the chart |
|
|
80
|
+
| (the `text` argument) | (`text`) | `null` | the chart to draw; wins over `src` and the script child |
|
|
81
|
+
|
|
82
|
+
## Styling on purpose
|
|
83
|
+
|
|
84
|
+
Set these custom properties on the element or anything above it:
|
|
85
|
+
|
|
86
|
+
| Property | What it sets |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `--barmark-font` | the chords' typeface |
|
|
89
|
+
| `--barmark-text-font` | lyrics and `###` note lines (default Georgia) |
|
|
90
|
+
| `--barmark-label-font` | the meta line, mid-chart changes, headings and diagnostics (default the system sans) |
|
|
91
|
+
| `--barmark-size` | the chord size everything scales from (default `1rem`) |
|
|
92
|
+
| `--barmark-ink` | chords and text |
|
|
93
|
+
| `--barmark-paper` | the background |
|
|
94
|
+
| `--barmark-muted` | holds, bar numbers, braces, meta lines |
|
|
95
|
+
| `--barmark-rule` | bar lines |
|
|
96
|
+
| `--barmark-accent` | errors |
|
|
97
|
+
| `--barmark-directive` | mid-chart changes (default the muted colour) |
|
|
98
|
+
| `--barmark-note` | `###` note lines (default the ink colour) |
|
|
99
|
+
|
|
100
|
+
```css
|
|
101
|
+
barmark-chart {
|
|
102
|
+
--barmark-size: 0.9rem;
|
|
103
|
+
--barmark-paper: transparent;
|
|
104
|
+
--barmark-text-font: "Noto Serif", Georgia, serif;
|
|
105
|
+
}
|
|
106
|
+
barmark-chart::part(heading) {
|
|
107
|
+
color: teal;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`::part()` names: `title`, `meta`, `heading`, `bar`, `chord`, `text`,
|
|
112
|
+
`bar-number`, `rehearsal`. These, the custom properties and the
|
|
113
|
+
attributes are the public surface: changing them is a breaking change.
|
|
114
|
+
|
|
115
|
+
## Light, dark and print
|
|
116
|
+
|
|
117
|
+
Both palettes meet WCAG AA contrast (4.5:1 for text). `theme="auto"`
|
|
118
|
+
follows `prefers-color-scheme`; a page with its own dark-mode switch sets
|
|
119
|
+
`theme` or the colour properties. Printing always uses the light palette,
|
|
120
|
+
and Windows High Contrast keeps bar lines and marks visible.
|
|
121
|
+
|
|
122
|
+
## Accessibility
|
|
123
|
+
|
|
124
|
+
- Everything is sized in em from `--barmark-size`, so text size and zoom
|
|
125
|
+
scale the chart; a section too wide for its container wraps its rows
|
|
126
|
+
instead of scrolling sideways.
|
|
127
|
+
- Every bar has a spoken name built from the parsed chart: "bar 9: A seven
|
|
128
|
+
sus four, hold", "triplet: D, E, F sharp minor", "note E", "C over E".
|
|
129
|
+
Sections are labelled by their headings, and repeats are spoken ("play
|
|
130
|
+
2 times").
|
|
131
|
+
|
|
132
|
+
## The font
|
|
133
|
+
|
|
134
|
+
Edwin, MuseScore's text font for music, ships unmodified in
|
|
135
|
+
`@barmark/render/fonts/Edwin-Roman.otf` with its SIL Open Font License
|
|
136
|
+
(`fonts/OFL.txt`). It is registered under the family name "barmark
|
|
137
|
+
Edwin". Serve the file and pass its URL as `fontUrl`, or call
|
|
138
|
+
`fontFaceCss(url)` once per page; without it the chart uses Georgia or
|
|
139
|
+
another serif.
|
|
140
|
+
|
|
141
|
+
## What it draws
|
|
142
|
+
|
|
143
|
+
Bar numbers sit in a gutter at the left edge; section headings, lyrics,
|
|
144
|
+
note lines and mid-chart changes start at the first bar line. Chords start
|
|
145
|
+
at the left of their share of the bar. A change written mid-chart names
|
|
146
|
+
itself: `time: 3/4, tempo: 120` draws as "→ Time 3/4 · Tempo ♩ = 120".
|
|
147
|
+
At the default size chords are 16px and no label is under 12px.
|
|
148
|
+
|
|
149
|
+
Chords appear in their canonical form whatever the author typed (`Cø`
|
|
150
|
+
shows as Cm⁷(♭5)). `%` is drawn as the bar it copies; a replayed section
|
|
151
|
+
is drawn in full with its own bar numbers, and its own lyrics replace the
|
|
152
|
+
written section's. A line with an error shows as written, underlined,
|
|
153
|
+
with its message. `layoutChart(text)` returns all of this as data, if
|
|
154
|
+
you want to draw it yourself.
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT. Edwin is © MuseScore Limited, under the SIL Open Font License 1.1.
|
package/dist/chord.d.ts
ADDED
|
@@ -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,16 @@
|
|
|
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
|
+
export type ChartSource = "text" | "src" | "script" | "none";
|
|
8
|
+
/** What a chart draws: its `text` while that is a string, else `src`, else its script child. */
|
|
9
|
+
export declare function chartSource(from: {
|
|
10
|
+
text: string | null;
|
|
11
|
+
src: string;
|
|
12
|
+
script: boolean;
|
|
13
|
+
}): ChartSource;
|
|
14
|
+
/** Chart text from a script element: blank edges and common indentation removed. */
|
|
15
|
+
export declare function inlineChartText(text: string): string;
|
|
16
|
+
export {};
|
package/dist/element.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
*/
|
|
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
|
+
/** What a chart draws: its `text` while that is a string, else `src`, else its script child. */
|
|
18
|
+
export function chartSource(from) {
|
|
19
|
+
if (from.text !== null)
|
|
20
|
+
return "text";
|
|
21
|
+
if (from.src)
|
|
22
|
+
return "src";
|
|
23
|
+
return from.script ? "script" : "none";
|
|
24
|
+
}
|
|
25
|
+
/** Chart text from a script element: blank edges and common indentation removed. */
|
|
26
|
+
export function inlineChartText(text) {
|
|
27
|
+
const lines = text.replace(/^\s*\n|\n\s*$/g, "").split("\n");
|
|
28
|
+
const indents = lines.filter((l) => l.trim()).map((l) => /^[ \t]*/.exec(l)?.[0].length ?? 0);
|
|
29
|
+
const cut = Math.min(...indents, Number.POSITIVE_INFINITY);
|
|
30
|
+
return lines.map((l) => l.slice(Number.isFinite(cut) ? cut : 0).trimEnd()).join("\n");
|
|
31
|
+
}
|
|
32
|
+
function registerFont(doc) {
|
|
33
|
+
if (doc.querySelector("style[data-barmark-font]"))
|
|
34
|
+
return;
|
|
35
|
+
const style = doc.createElement("style");
|
|
36
|
+
style.dataset.barmarkFont = FONT_FAMILY;
|
|
37
|
+
style.textContent = fontFaceCss(new URL("../fonts/Edwin-Roman.otf", import.meta.url).href);
|
|
38
|
+
doc.head.append(style);
|
|
39
|
+
}
|
|
40
|
+
async function fetchText(src) {
|
|
41
|
+
const response = await fetch(src);
|
|
42
|
+
if (!response.ok)
|
|
43
|
+
throw new Error(`${response.status} ${response.statusText}`.trim());
|
|
44
|
+
return response.text();
|
|
45
|
+
}
|
|
46
|
+
function defineElement() {
|
|
47
|
+
class BarmarkChart extends HTMLElement {
|
|
48
|
+
static observedAttributes = ["src", "theme", "bar-numbers", "rehearsal-letters"];
|
|
49
|
+
#observer = new MutationObserver(() => this.#schedule());
|
|
50
|
+
#renders = 0;
|
|
51
|
+
#scheduled = false;
|
|
52
|
+
/** The text fetched for `src`, kept so a change of options does not fetch again. */
|
|
53
|
+
#fetched = null;
|
|
54
|
+
#text = null;
|
|
55
|
+
connectedCallback() {
|
|
56
|
+
// Text set before the element was defined landed on the instance: route it through the setter.
|
|
57
|
+
if (Object.hasOwn(this, "text")) {
|
|
58
|
+
const text = this.text ?? null;
|
|
59
|
+
delete this.text;
|
|
60
|
+
this.text = text;
|
|
61
|
+
}
|
|
62
|
+
registerFont(this.ownerDocument);
|
|
63
|
+
this.#observer.observe(this, { childList: true, subtree: true, characterData: true });
|
|
64
|
+
this.#schedule();
|
|
65
|
+
}
|
|
66
|
+
disconnectedCallback() {
|
|
67
|
+
this.#observer.disconnect();
|
|
68
|
+
}
|
|
69
|
+
attributeChangedCallback() {
|
|
70
|
+
if (this.isConnected)
|
|
71
|
+
this.#schedule();
|
|
72
|
+
}
|
|
73
|
+
/** Chart text to draw; while it is a string it wins over `src` and a script child. */
|
|
74
|
+
get text() {
|
|
75
|
+
return this.#text;
|
|
76
|
+
}
|
|
77
|
+
set text(v) {
|
|
78
|
+
this.#text = v === null || v === undefined ? null : String(v);
|
|
79
|
+
if (this.isConnected)
|
|
80
|
+
this.#schedule();
|
|
81
|
+
}
|
|
82
|
+
get src() {
|
|
83
|
+
return this.getAttribute("src") ?? "";
|
|
84
|
+
}
|
|
85
|
+
set src(v) {
|
|
86
|
+
this.setAttribute("src", v);
|
|
87
|
+
}
|
|
88
|
+
get theme() {
|
|
89
|
+
return optionsFromAttributes(this).theme ?? "auto";
|
|
90
|
+
}
|
|
91
|
+
set theme(v) {
|
|
92
|
+
this.setAttribute("theme", v);
|
|
93
|
+
}
|
|
94
|
+
get barNumbers() {
|
|
95
|
+
return optionsFromAttributes(this).barNumbers ?? true;
|
|
96
|
+
}
|
|
97
|
+
set barNumbers(v) {
|
|
98
|
+
if (v)
|
|
99
|
+
this.removeAttribute("bar-numbers");
|
|
100
|
+
else
|
|
101
|
+
this.setAttribute("bar-numbers", "off");
|
|
102
|
+
}
|
|
103
|
+
get rehearsalLetters() {
|
|
104
|
+
return this.hasAttribute("rehearsal-letters");
|
|
105
|
+
}
|
|
106
|
+
set rehearsalLetters(v) {
|
|
107
|
+
this.toggleAttribute("rehearsal-letters", v);
|
|
108
|
+
}
|
|
109
|
+
/** Every change in one task (an upgrade sets each attribute in turn) renders once. */
|
|
110
|
+
#schedule() {
|
|
111
|
+
if (this.#scheduled)
|
|
112
|
+
return;
|
|
113
|
+
this.#scheduled = true;
|
|
114
|
+
queueMicrotask(() => {
|
|
115
|
+
this.#scheduled = false;
|
|
116
|
+
void this.#render();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async #render() {
|
|
120
|
+
const script = this.querySelector('script[type="text/barmark"]');
|
|
121
|
+
const options = optionsFromAttributes(this);
|
|
122
|
+
const source = chartSource({ text: this.#text, src: this.src, script: script !== null });
|
|
123
|
+
// With no source of its own, a server-rendered chart keeps its music and takes the options it can.
|
|
124
|
+
if (source === "none")
|
|
125
|
+
return this.#restyle(options);
|
|
126
|
+
const render = ++this.#renders;
|
|
127
|
+
const content = await this.#content(source, script, options);
|
|
128
|
+
if (render !== this.#renders)
|
|
129
|
+
return;
|
|
130
|
+
(this.shadowRoot ?? this.attachShadow({ mode: "open" })).innerHTML = content;
|
|
131
|
+
}
|
|
132
|
+
async #content(source, script, options) {
|
|
133
|
+
if (source === "text")
|
|
134
|
+
return shadowContent(this.#text ?? "", options);
|
|
135
|
+
if (source !== "src")
|
|
136
|
+
return shadowContent(inlineChartText(script?.textContent ?? ""), options);
|
|
137
|
+
try {
|
|
138
|
+
return shadowContent(await this.#fetch(this.src), options);
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
this.#fetched = null;
|
|
142
|
+
return loadErrorContent(`Could not load ${this.src}: ${e.message}`, options);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
#fetch(src) {
|
|
146
|
+
if (this.#fetched?.src !== src)
|
|
147
|
+
this.#fetched = { src, text: fetchText(src) };
|
|
148
|
+
return this.#fetched.text;
|
|
149
|
+
}
|
|
150
|
+
#restyle(options) {
|
|
151
|
+
const chart = this.shadowRoot?.querySelector(".bm-chart");
|
|
152
|
+
chart?.setAttribute("data-theme", options.theme ?? "auto");
|
|
153
|
+
if (options.barNumbers === false)
|
|
154
|
+
chart?.setAttribute("data-bar-numbers", "off");
|
|
155
|
+
else
|
|
156
|
+
chart?.removeAttribute("data-bar-numbers");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
customElements.define("barmark-chart", BarmarkChart);
|
|
160
|
+
}
|
|
161
|
+
if (typeof customElements !== "undefined" && !customElements.get("barmark-chart"))
|
|
162
|
+
defineElement();
|
package/dist/escape.d.ts
ADDED
package/dist/escape.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const ENTITIES = {
|
|
2
|
+
"&": "&",
|
|
3
|
+
"<": "<",
|
|
4
|
+
">": ">",
|
|
5
|
+
'"': """,
|
|
6
|
+
"'": "'",
|
|
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;
|