tonus 0.6.1 → 0.8.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.
@@ -1,5 +1,5 @@
1
1
  import { type Imprint } from "../imprint.js";
2
- import { type Prosody } from "./prosody.js";
2
+ import { type Metrics } from "./metrics.js";
3
3
  import { type Cadence } from "./cadence.js";
4
4
  import { type Modulation } from "./modulation.js";
5
5
  import { type ChantTabulaRow } from "./tabula.js";
@@ -28,7 +28,7 @@ export interface Score {
28
28
  phrases: IRPhrase[];
29
29
  errors: ParseError[];
30
30
  tabula: ChantTabulaRow[];
31
- prosody: Prosody;
31
+ metrics: Metrics;
32
32
  /** Mode-specific cadence at each phrase-ending divisio. */
33
33
  cadences: Cadence[];
34
34
  /** Passages where the tonal centre leans away from the home mode. */
@@ -38,7 +38,7 @@ export interface Score {
38
38
  /**
39
39
  * Score builder (`tonus.notatio`). Parses a chant's GABC into a musical
40
40
  * IR — phrases, syllables, notes with tuned pitches, arsis/thesis
41
- * rhythm, prosody, imprint, and a tabula. Options: a temperamentum
41
+ * rhythm, metrics, imprint, and a tabula. Options: a temperamentum
42
42
  * (tuning), a pondus (articulation weight, style name or opts), and an
43
43
  * accentus (phrasing, style name or opts).
44
44
  * @throws Error on invalid Chant input or unparseable GABC.
@@ -6,7 +6,7 @@ import { buildIR } from "./ir.js";
6
6
  import { buildRatios } from "../temper/scale.js";
7
7
  import { computeMeta } from "./meta.js";
8
8
  import { computeImprint } from "../imprint.js";
9
- import { computeProsody } from "./prosody.js";
9
+ import { computeMetrics } from "./metrics.js";
10
10
  import { detectCadences } from "./cadence.js";
11
11
  import { detectModulations } from "./modulation.js";
12
12
  import { computeTabula } from "./tabula.js";
@@ -33,7 +33,7 @@ function resolveAccentus(input) {
33
33
  /**
34
34
  * Score builder (`tonus.notatio`). Parses a chant's GABC into a musical
35
35
  * IR — phrases, syllables, notes with tuned pitches, arsis/thesis
36
- * rhythm, prosody, imprint, and a tabula. Options: a temperamentum
36
+ * rhythm, metrics, imprint, and a tabula. Options: a temperamentum
37
37
  * (tuning), a pondus (articulation weight, style name or opts), and an
38
38
  * accentus (phrasing, style name or opts).
39
39
  * @throws Error on invalid Chant input or unparseable GABC.
@@ -98,7 +98,7 @@ export function buildScore(chant, opts) {
98
98
  phrases: ir.phrases,
99
99
  errors: ir.errors,
100
100
  tabula,
101
- prosody: computeProsody(ir.phrases),
101
+ metrics: computeMetrics(ir.phrases),
102
102
  cadences,
103
103
  modulations,
104
104
  imprint: computeImprint(ir.phrases, scale, {
@@ -34,14 +34,14 @@ export declare const STROKE: {
34
34
  };
35
35
  /** The type scale, and it is THE PAGE'S: a modular scale on 15px stepped by
36
36
  * the Pythagorean minor third, 32:27 — the same six `--micro` … `--display-size`
37
- * that ../../../../docs/styles.css declares. So a wheel's label and a table's
37
+ * that the site's stylesheet declares. So a wheel's label and a table's
38
38
  * label at the same step are the same size.
39
39
  *
40
40
  * It was 9/11/13.5/17/21/26, derived from what the diagrams happened to use.
41
41
  * That was honest when the page had no scale of its own; once the page took
42
42
  * one, the two agreed on `title` alone and every drawn label sat 1.5–2px under
43
- * the prose around it. ../../../../scripts/vendor-ink.mjs asserts the pairing
44
- * now, so this cannot drift again silently.
43
+ * the prose around it. The site's vendor step asserts the pairing now, so this
44
+ * cannot drift again silently.
45
45
  *
46
46
  * Nothing inside tonus reads STEP — the site's diagrams are its only consumer
47
47
  * — so moving it changes no emitted SVG for a package consumer. */
@@ -63,7 +63,7 @@ export declare const HOUSE_SERIF = "Junicode, 'Crimson Pro', Georgia, serif";
63
63
  export declare const HOUSE_MONO = "'IBM Plex Mono', ui-monospace, Menlo, monospace";
64
64
  /** The fourth stack, and NOT a fourth face: neither text face carries the
65
65
  * planetary or musical signs (see fonts/README.md), so a figure that draws one
66
- * falls back to the system. Mirrors `--symbol` in ../../../../docs/styles.css
66
+ * falls back to the system. Mirrors `--symbol` in the site's stylesheet
67
67
  * — the same sharing-by-numbers seam as HOUSE_SERIF and HOUSE_MONO, and it was duplicated
68
68
  * inline in mutatio.js as a local SIGN_FACE before this existed. */
69
69
  export declare const HOUSE_SYMBOL: string;
@@ -79,9 +79,9 @@ export declare const HOUSE_SYMBOL: string;
79
79
  * of digits must line up; that is the setting that does work.
80
80
  *
81
81
  * Nothing in the library's own emitters uses this yet. It lives here because
82
- * this file is the ONE definition of the ink system and the site vendors it
83
- * (../../../../scripts/vendor-ink.mjs) — a second copy in the site would be
84
- * the drift this module exists to prevent. */
82
+ * this file is the ONE definition of the ink system and the site vendors it
83
+ * a second copy in the site would be the drift this module exists to
84
+ * prevent. */
85
85
  export declare const FIGURES: {
86
86
  readonly family: "Junicode, 'Crimson Pro', Georgia, serif";
87
87
  readonly features: "";
@@ -24,9 +24,8 @@
24
24
  // draw.
25
25
  //
26
26
  // ─── HOW THIS MEETS THE PAGE ───────────────────────────────────────────────
27
- // The documentation site keeps a system of its own, stated at the top of
28
- // ../../../../docs/styles.css. The two are separate and they AGREE BY SHARING
29
- // NUMBERS,
27
+ // The site (orreliquum-next) keeps a system of its own, stated at the top of
28
+ // its stylesheet. The two are separate and they AGREE BY SHARING NUMBERS,
30
29
  // not by one importing the other:
31
30
  //
32
31
  // STEP mirrors the six CSS type steps (--micro … --display-size)
@@ -92,14 +91,14 @@ export const STROKE = {
92
91
  };
93
92
  /** The type scale, and it is THE PAGE'S: a modular scale on 15px stepped by
94
93
  * the Pythagorean minor third, 32:27 — the same six `--micro` … `--display-size`
95
- * that ../../../../docs/styles.css declares. So a wheel's label and a table's
94
+ * that the site's stylesheet declares. So a wheel's label and a table's
96
95
  * label at the same step are the same size.
97
96
  *
98
97
  * It was 9/11/13.5/17/21/26, derived from what the diagrams happened to use.
99
98
  * That was honest when the page had no scale of its own; once the page took
100
99
  * one, the two agreed on `title` alone and every drawn label sat 1.5–2px under
101
- * the prose around it. ../../../../scripts/vendor-ink.mjs asserts the pairing
102
- * now, so this cannot drift again silently.
100
+ * the prose around it. The site's vendor step asserts the pairing now, so this
101
+ * cannot drift again silently.
103
102
  *
104
103
  * Nothing inside tonus reads STEP — the site's diagrams are its only consumer
105
104
  * — so moving it changes no emitted SVG for a package consumer. */
@@ -128,7 +127,7 @@ export const HOUSE_SERIF = "Junicode, 'Crimson Pro', Georgia, serif";
128
127
  export const HOUSE_MONO = "'IBM Plex Mono', ui-monospace, Menlo, monospace";
129
128
  /** The fourth stack, and NOT a fourth face: neither text face carries the
130
129
  * planetary or musical signs (see fonts/README.md), so a figure that draws one
131
- * falls back to the system. Mirrors `--symbol` in ../../../../docs/styles.css
130
+ * falls back to the system. Mirrors `--symbol` in the site's stylesheet
132
131
  * — the same sharing-by-numbers seam as HOUSE_SERIF and HOUSE_MONO, and it was duplicated
133
132
  * inline in mutatio.js as a local SIGN_FACE before this existed. */
134
133
  export const HOUSE_SYMBOL = "'Apple Symbols', 'Segoe UI Symbol', "
@@ -145,9 +144,9 @@ export const HOUSE_SYMBOL = "'Apple Symbols', 'Segoe UI Symbol', "
145
144
  * of digits must line up; that is the setting that does work.
146
145
  *
147
146
  * Nothing in the library's own emitters uses this yet. It lives here because
148
- * this file is the ONE definition of the ink system and the site vendors it
149
- * (../../../../scripts/vendor-ink.mjs) — a second copy in the site would be
150
- * the drift this module exists to prevent. */
147
+ * this file is the ONE definition of the ink system and the site vendors it
148
+ * a second copy in the site would be the drift this module exists to
149
+ * prevent. */
151
150
  export const FIGURES = {
152
151
  family: HOUSE_SERIF,
153
152
  features: "",
@@ -48,7 +48,7 @@ export interface Arcus {
48
48
  */
49
49
  archIndex: number;
50
50
  }
51
- export interface Prosody {
51
+ export interface Metrics {
52
52
  noteCount: number;
53
53
  syllableCount: number;
54
54
  phraseCount: number;
@@ -69,5 +69,5 @@ export interface Prosody {
69
69
  cadenceWeight: number;
70
70
  cadenceDistribution: CadenceDistribution;
71
71
  }
72
- export declare function computeProsody(phrases: Phrase[]): Prosody;
73
- //# sourceMappingURL=prosody.d.ts.map
72
+ export declare function computeMetrics(phrases: Phrase[]): Metrics;
73
+ //# sourceMappingURL=metrics.d.ts.map
@@ -1,4 +1,4 @@
1
- export function computeProsody(phrases) {
1
+ export function computeMetrics(phrases) {
2
2
  let phraseCount = 0;
3
3
  let noteCount = 0;
4
4
  let syllableCount = 0;
@@ -175,4 +175,4 @@ export function computeProsody(phrases) {
175
175
  cadenceDistribution: cadDist,
176
176
  };
177
177
  }
178
- //# sourceMappingURL=prosody.js.map
178
+ //# sourceMappingURL=metrics.js.map
@@ -163,7 +163,7 @@ const CADENCE_VELOCITY_FACTOR = 0.5;
163
163
  const CADENCE_DURATION_FACTOR = 0.6;
164
164
  // The phrasing-side reading of the divisio (bar-line) hierarchy. This is the
165
165
  // same bar hierarchy tabulated canonically in ../../../docs/api/score.md and
166
- // weighted for analysis by prosody.ts's cadenceWeight ladder — but here the
166
+ // weighted for analysis by metrics.ts's cadenceWeight ladder — but here the
167
167
  // weights differ on purpose:
168
168
  // this is a *shaping* factor, so the virgula (`) gets 0 (a breath, no cadential
169
169
  // stress), where the analytic ladder still counts it. Do not unify the three.
@@ -2,7 +2,13 @@
2
2
  // begin at the thumb tip, come down the thumb, cross the finger BASES
3
3
  // index→little, climb the little finger, return across the finger TIPS
4
4
  // little→index, descend the index, then spiral inward; ee (76) floats above the
5
- // middle fingertip (region "super"). The two b/♮ pairs (58/59, 70/71) share a
5
+ // middle fingertip (region "super"). THE INWARD TURN IS ONE TURN: after g on
6
+ // the index the line runs in to aa, out to bb/♮♮ on the ring, then UP that
7
+ // finger to cc before crossing to the middle for dd and ee. Reading it with
8
+ // cc on the middle and dd on the ring makes the line cross three times where
9
+ // the hand turns once. The figure that DRAWS the turn lives in the site
10
+ // (orreliquum-next), and its LOCUS table must agree with the fingers named
11
+ // here; the test that holds the two together lives there beside it. The two b/♮ pairs (58/59, 70/71) share a
6
12
  // locus by design — one joint, two readings (fa in molle, mi in durum), the
7
13
  // tradition, not a duplication to fix. They are one distinction, named for the
8
14
  // two shapes of the letter: b rotundum (the round b) against b quadratum (the
@@ -31,8 +37,8 @@ export const GUIDONIAN_DATA = new Map([
31
37
  [69, { hand: { finger: "middle", region: "mid" }, name: ["aa", "Alamire"], variants: [{ hexachord: "durum", solmization: "RE" }, { hexachord: "naturale", solmization: "LA" }, { hexachord: "molle", solmization: "MI" }] }],
32
38
  [70, { hand: { finger: "ring", region: "mid" }, name: ["bb", "Befa"], variants: [{ hexachord: "molle", solmization: "FA" }] }],
33
39
  [71, { hand: { finger: "ring", region: "mid" }, name: ["♮♮", "Bemi"], variants: [{ hexachord: "durum", solmization: "MI" }] }],
34
- [72, { hand: { finger: "middle", region: "top" }, name: ["cc", "Cesolfa"], variants: [{ hexachord: "durum", solmization: "FA" }, { hexachord: "naturale", solmization: "UT" }, { hexachord: "molle", solmization: "SOL" }] }],
35
- [74, { hand: { finger: "ring", region: "top" }, name: ["dd", "Delasol"], variants: [{ hexachord: "durum", solmization: "SOL" }, { hexachord: "naturale", solmization: "RE" }, { hexachord: "molle", solmization: "LA" }] }],
40
+ [72, { hand: { finger: "ring", region: "top" }, name: ["cc", "Cesolfa"], variants: [{ hexachord: "durum", solmization: "FA" }, { hexachord: "naturale", solmization: "UT" }, { hexachord: "molle", solmization: "SOL" }] }],
41
+ [74, { hand: { finger: "middle", region: "top" }, name: ["dd", "Delasol"], variants: [{ hexachord: "durum", solmization: "SOL" }, { hexachord: "naturale", solmization: "RE" }, { hexachord: "molle", solmization: "LA" }] }],
36
42
  [76, { hand: { finger: "middle", region: "super" }, name: ["ee", "Ela"], variants: [{ hexachord: "durum", solmization: "LA" }, { hexachord: "naturale", solmization: "MI" }] }],
37
43
  ]);
38
44
  //# sourceMappingURL=guido.js.map
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ import type { Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, Accentus
17
17
  import type { InscriptioOpts, Inscriptio, NoteGeometry, FontSpec, FontSlot, FontEmbed } from "./engines/score/inscriptio.js";
18
18
  import type { ChantTabulaRow } from "./engines/score/tabula.js";
19
19
  import type { Imprint, Attractor, VowelAttractor, ModalAffinity } from "./engines/imprint.js";
20
- import type { Prosody, RhythmicProfile, NoteRange, CadenceDistribution } from "./engines/score/prosody.js";
20
+ import type { Metrics, RhythmicProfile, NoteRange, CadenceDistribution } from "./engines/score/metrics.js";
21
21
  import type { Harmony, HarmoniaOpts, VoicedBody, VoicedAspect, Frame, Author } from "./engines/harmonia/api.js";
22
22
  import type { HarmonyTabulaRow } from "./engines/harmonia/tabula.js";
23
23
  import type { PlanetVowel } from "./engines/harmonia/data/vowels.js";
@@ -58,5 +58,5 @@ export { CADENTIAE, CADENTIAE_POPULATION } from "./data/cadentiae.js";
58
58
  export type { CadentiaFamilia } from "./data/cadentiae.js";
59
59
  export { ZODIACA } from "./engines/harmonia/data/zodiac.js";
60
60
  export { CENSUS_GROUPS, CENSUS_ORDER } from "./data/census.js";
61
- export type { Feast, FeastQuery, Pascha, Season, Grade, Chant, CantusQuery, OrdinaryChant, PropriumQuery, OrdinariumQuery, OfficiumQuery, PsalmusQuery, Corpus, GenusCount, ModeCount, SharedCount, CorpusLedger, CorpusFullCount, CorpusQuery, Census, CensusQuery, CensusBy, CensusGroup, CensusGroupProfile, CensusNeighbor, Temperamentum, TemperamentumInput, TemperamentumOpts, Tuning, Pitch, PitchInput, Step, Neume, NeumeShape, Interval, ModeData, CadenceFigure, Modus, TunedNote, GamutOptions, Tonus, TonusOpts, Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, AccentusOpts, Cadence, CadenceTarget, CadenceApproach, Modulation, InscriptioOpts, Inscriptio, NoteGeometry, FontSpec, FontSlot, FontEmbed, ChantTabulaRow, Note, Performance, Phrase, Syllable, LyricRun, RestEvent, ParseError, ArsisThesis, RhythmicType, CompoundBeat, VoicedPitch, Cosmos, CosmosQuery, Body, BodyName, Aspect, Imprint, Attractor, VowelAttractor, ModalAffinity, Prosody, RhythmicProfile, NoteRange, CadenceDistribution, Harmony, HarmoniaOpts, VoicedBody, VoicedAspect, Frame, Author, HarmonyTabulaRow, PlanetVowel, };
61
+ export type { Feast, FeastQuery, Pascha, Season, Grade, Chant, CantusQuery, OrdinaryChant, PropriumQuery, OrdinariumQuery, OfficiumQuery, PsalmusQuery, Corpus, GenusCount, ModeCount, SharedCount, CorpusLedger, CorpusFullCount, CorpusQuery, Census, CensusQuery, CensusBy, CensusGroup, CensusGroupProfile, CensusNeighbor, Temperamentum, TemperamentumInput, TemperamentumOpts, Tuning, Pitch, PitchInput, Step, Neume, NeumeShape, Interval, ModeData, CadenceFigure, Modus, TunedNote, GamutOptions, Tonus, TonusOpts, Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, AccentusOpts, Cadence, CadenceTarget, CadenceApproach, Modulation, InscriptioOpts, Inscriptio, NoteGeometry, FontSpec, FontSlot, FontEmbed, ChantTabulaRow, Note, Performance, Phrase, Syllable, LyricRun, RestEvent, ParseError, ArsisThesis, RhythmicType, CompoundBeat, VoicedPitch, Cosmos, CosmosQuery, Body, BodyName, Aspect, Imprint, Attractor, VowelAttractor, ModalAffinity, Metrics, RhythmicProfile, NoteRange, CadenceDistribution, Harmony, HarmoniaOpts, VoicedBody, VoicedAspect, Frame, Author, HarmonyTabulaRow, PlanetVowel, };
62
62
  //# sourceMappingURL=index.d.ts.map
package/docs/api/chant.md CHANGED
@@ -601,7 +601,7 @@ phrase punctuation ([score.md](score.md#the-tabula)).
601
601
 
602
602
  Because the encoding is textual, lyrics and neumes stay aligned syllable
603
603
  by syllable, which is what lets `tonus.notatio` reconstruct syllables,
604
- neumes, and prosody without images.
604
+ neumes, and metrics without images.
605
605
 
606
606
  ### The Mass: proper and ordinary
607
607
 
package/docs/api/index.md CHANGED
@@ -173,7 +173,7 @@ the list resolve their pitches through the ones before.
173
173
  - [The tabula](score.md#the-tabula)
174
174
  - [Rendering — `inscriptio`](score.md#rendering) · [theme](score.md#theme--faces-and-ink) · [The analysis tracks](score.md#the-analysis-tracks)
175
175
  - [The imprint](score.md#the-imprint)
176
- - [Prosody](score.md#prosody)
176
+ - [Metrics](score.md#metrics)
177
177
  - [Cadences](score.md#cadences) · [One spine, two annotations](score.md#one-spine-two-annotations)
178
178
  - [Modulations](score.md#modulations)
179
179
  - [Theory & Context](score.md#theory--context) · [Sources](score.md#sources)
package/docs/api/score.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `tonus.notatio` renders a chant into a score: the analyzed, tuned, and
4
4
  rhythm-classified reading of one GABC melody. The score is data: `phrases`,
5
- `tabula`, `prosody`, `cadences`, `modulations`, and `imprint`. The
5
+ `tabula`, `metrics`, `cadences`, `modulations`, and `imprint`. The
6
6
  standalone `tonus.inscriptio(score)` draws it to SVG.
7
7
 
8
8
  - [Score](#score)
@@ -15,7 +15,7 @@ standalone `tonus.inscriptio(score)` draws it to SVG.
15
15
  - [theme — faces and ink](#theme--faces-and-ink)
16
16
  - [The analysis tracks](#the-analysis-tracks)
17
17
  - [The imprint](#the-imprint)
18
- - [Prosody](#prosody)
18
+ - [Metrics](#metrics)
19
19
  - [Cadences](#cadences)
20
20
  - [One spine, two annotations](#one-spine-two-annotations)
21
21
  - [`finality` — how often this family closes](#finality--how-often-this-family-closes)
@@ -54,7 +54,7 @@ notation, signs of punctuation rather than measure:
54
54
  | `::` | divisio finalis (double bar) |
55
55
 
56
56
  This hierarchy is read three ways in the engine, each weighting the bars for its
57
- own end: an analytic cadence weight (prosody), a phrasing strength (which zeroes
57
+ own end: an analytic cadence weight (metrics), a phrasing strength (which zeroes
58
58
  the virgula), and a rest duration (the divisio's pause length). Each weighting
59
59
  is documented at its table in the code.
60
60
 
@@ -64,7 +64,7 @@ interface Score {
64
64
  phrases: Phrase[];
65
65
  errors: ParseError[];
66
66
  tabula: ChantTabulaRow[];
67
- prosody: Prosody;
67
+ metrics: Metrics;
68
68
  cadences: Cadence[];
69
69
  modulations: Modulation[];
70
70
  imprint: Imprint;
@@ -665,16 +665,16 @@ interface ModalAffinity {
665
665
  }
666
666
  ```
667
667
 
668
- ## Prosody
668
+ ## Metrics
669
669
 
670
- `score.prosody` measures the chant's shape — counts, range, melisma,
670
+ `score.metrics` measures the chant's shape — counts, range, melisma,
671
671
  melodic motion, contour, tessitura, rhythm, cadence. It is chant-specific;
672
- `Harmony` has no prosody. For _Puer natus est_: ambitus 10 semitones, melisma
672
+ `Harmony` has no metrics. For _Puer natus est_: ambitus 10 semitones, melisma
673
673
  ratio 2.04 notes per syllable, tessitura ~5 semitones above the final, a near-
674
674
  perfect melodic arch, mostly stepwise motion (leap rate ~5%).
675
675
 
676
676
  ```ts
677
- interface Prosody {
677
+ interface Metrics {
678
678
  noteCount: number;
679
679
  syllableCount: number;
680
680
  phraseCount: number;
@@ -730,7 +730,7 @@ interface CadenceDistribution {
730
730
 
731
731
  ## Cadences
732
732
 
733
- `score.cadences` names the melodic close of each phrase — where prosody
733
+ `score.cadences` names the melodic close of each phrase — where metrics
734
734
  only counts the divisio bars, this identifies the figure. One `Cadence` per
735
735
  phrase-ending divisio: its resolution `target`, the melodic `approach`, and the
736
736
  `divisio` that tells medial from final (the double bar `::` is the final
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tonus",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "Medieval music analysis and performance: GABC plainchant exports, liturgical calendar, tuning systems, ephemeris, and the harmony of the spheres",
6
6
  "author": "Jeffrey Pierce <jeffrey@jeffreypierce.net>",
@@ -50,10 +50,6 @@
50
50
  "clean": "rm -rf dist",
51
51
  "test": "npm run build && node --test --test-reporter=spec tests/*.test.mjs",
52
52
  "prepublishOnly": "npm run clean && npm run build",
53
- "check:links": "node scripts/check-doc-links.mjs",
54
- "site": "npm run build && node scripts/vendor-ink.mjs && rm -rf docs/dist && cp -R dist docs/dist",
55
- "serve": "npm run site && node scripts/serve.mjs",
56
- "diagrams": "npm run site && node scripts/diagram-lab.mjs",
57
53
  "lab": "npm run build && node scripts/render-lab.mjs",
58
54
  "stress": "npm run build && node scripts/render-stress.mjs",
59
55
  "glyphs": "node scripts/extract-smufl-glyphs.mjs"