@heroiclands/package-build 24.0.1 → 24.0.2

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/assets/preview/gitea.ini +1 -1
  3. package/assets/styles/content-base.css +33 -0
  4. package/bin/content-commands.mjs +9 -7
  5. package/docs/api.md +6 -5
  6. package/docs/authoring/links-and-markup.md +164 -16
  7. package/docs/commands.md +16 -14
  8. package/docs/configuration.md +16 -0
  9. package/docs/content-format.md +4 -2
  10. package/docs/guides/writing-in-invented-scripts.md +68 -6
  11. package/docs/reference/format-details.md +143 -49
  12. package/docs/reference/note-types.md +47 -28
  13. package/docs/source-preview.md +2 -2
  14. package/engine/content-blocks.mjs +6 -2
  15. package/engine/content-format.yaml +2 -0
  16. package/engine/content-index.mjs +5 -1
  17. package/engine/content-render.mjs +2 -1
  18. package/engine/content-scripts.mjs +268 -21
  19. package/engine/content-scroll.mjs +389 -0
  20. package/engine/font-outlines.mjs +394 -0
  21. package/engine/language-data.mjs +154 -63
  22. package/engine/language-rows.mjs +32 -6
  23. package/engine/language-terms.mjs +32 -8
  24. package/engine/map-lexicon.mjs +31 -6
  25. package/engine/note-vocabulary.mjs +58 -6
  26. package/engine/pdf-fonts.mjs +1 -1
  27. package/engine/pdf-render.mjs +52 -11
  28. package/engine/run-rows.mjs +45 -3
  29. package/engine/script-direction.mjs +67 -0
  30. package/engine/script-emphasis-terms.mjs +11 -3
  31. package/engine/script-font-delivery.mjs +42 -9
  32. package/engine/script-fonts.mjs +7 -0
  33. package/engine/verse-metre.mjs +1 -0
  34. package/engine/written-in.mjs +208 -0
  35. package/package.json +1 -1
  36. package/site-theme/static/css/style.css +7 -0
  37. package/types/engine/content-scripts.d.mts +31 -6
  38. package/types/engine/content-scroll.d.mts +179 -0
  39. package/types/engine/font-outlines.d.mts +67 -0
  40. package/types/engine/language-data.d.mts +13 -0
  41. package/types/engine/language-rows.d.mts +4 -3
  42. package/types/engine/language-terms.d.mts +7 -0
  43. package/types/engine/map-lexicon.d.mts +4 -3
  44. package/types/engine/pdf-fonts.d.mts +10 -0
  45. package/types/engine/run-rows.d.mts +28 -6
  46. package/types/engine/script-direction.d.mts +10 -0
  47. package/types/engine/script-emphasis-terms.d.mts +10 -3
  48. package/types/engine/script-font-delivery.d.mts +5 -3
  49. package/types/engine/script-fonts.d.mts +9 -0
  50. package/types/engine/written-in.d.mts +76 -0
@@ -13,9 +13,9 @@
13
13
 
14
14
  /**
15
15
  * **Delivering the installed faces to each target.** A consumer that installs
16
- * `@heroiclands/fonts` and sets text in one of its faces with `font=` gets the
17
- * fonts on the site, in the Foundry module and in its books; without both,
18
- * nothing ships and nothing links to it.
16
+ * `@heroiclands/fonts` and sets text in one of its faces, with `font=` or by a
17
+ * language note's `data.writtenIn`, gets the fonts on the site, in the Foundry
18
+ * module and in its books; without both, nothing ships and nothing links to it.
19
19
  *
20
20
  * - **Site and Foundry** receive `fonts.css`, listing only the WOFF2 sources,
21
21
  * the WOFF2 files and `LICENSE`.
@@ -27,9 +27,12 @@
27
27
  import fs from "node:fs";
28
28
  import path from "node:path";
29
29
 
30
+ import YAML from "yaml";
31
+
30
32
  import { baseStyleLayer } from "./base-styles.mjs";
31
33
  import { scanRuns } from "./content-scripts.mjs";
32
34
  import { FONTS_MANIFEST, resolveScriptFonts } from "./script-fonts.mjs";
35
+ import { LANGUAGE_SUBTYPE } from "./word-terms.mjs";
33
36
 
34
37
  /**
35
38
  * Where the files land in the staged Foundry module, relative to its root.
@@ -65,10 +68,37 @@ const LICENSE = "LICENSE";
65
68
  /** The faces each configuration's notes set text in, found once. */
66
69
  const usedFaces = new WeakMap();
67
70
 
71
+ /** A note's frontmatter block. */
72
+ const FRONTMATTER = /^\uFEFF?---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
73
+
74
+ /**
75
+ * The face a language note names in `data.writtenIn`, where it is installed.
76
+ *
77
+ * @param {string} source - The note's file.
78
+ * @param {import("./script-fonts.mjs").ScriptFonts} installed - The fonts package.
79
+ * @returns {string|undefined} The face's slug.
80
+ */
81
+ function writtenInFace(source, installed) {
82
+ const yaml = FRONTMATTER.exec(source)?.[1];
83
+ if (yaml === undefined) return undefined;
84
+ let fm;
85
+ try {
86
+ fm = YAML.parse(yaml);
87
+ } catch {
88
+ return undefined;
89
+ }
90
+ if (fm?.type !== "skill" || String(fm.subType ?? "").toLowerCase() !== LANGUAGE_SUBTYPE)
91
+ return undefined;
92
+ const font = fm.data?.writtenIn?.font;
93
+ return typeof font === "string" && installed.faces.has(font) ? font : undefined;
94
+ }
95
+
68
96
  /**
69
- * The faces the package's notes set text in with `font=`, read from the
70
- * notes themselves: every Markdown file under the content tree that writes
71
- * `font=` is scanned for its runs.
97
+ * The faces the package's notes set text in, read from the notes
98
+ * themselves: every Markdown file under the content tree that writes `font=`
99
+ * is scanned for its runs, and the face every language note names in
100
+ * `data.writtenIn` counts, since a run marked with the language alone is set
101
+ * in it.
72
102
  *
73
103
  * @param {DeliveryConfig} config - The resolved configuration.
74
104
  * @param {import("./script-fonts.mjs").ScriptFonts} installed - The fonts package.
@@ -90,10 +120,13 @@ export function facesUsed(config, installed) {
90
120
  if (entry.isDirectory()) walk(full);
91
121
  else if (entry.isFile() && entry.name.endsWith(".md")) {
92
122
  const source = fs.readFileSync(full, "utf8");
123
+ if (/^\s*writtenIn:/m.test(source)) {
124
+ const face = writtenInFace(source, installed);
125
+ if (face) used.add(face);
126
+ }
93
127
  if (!/\bfont=/.test(source)) continue;
94
- const body = source.replace(
95
- /^\uFEFF?---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/,
96
- (frontmatter) => frontmatter.replace(/[^\r\n]/g, " "),
128
+ const body = source.replace(FRONTMATTER, (frontmatter) =>
129
+ frontmatter.replace(/[^\r\n]/g, " "),
97
130
  );
98
131
  const ctx = { languages: config.languages ?? {}, faces: installed.faces };
99
132
  for (const run of scanRuns(body, ctx).runs)
@@ -68,6 +68,9 @@ export const FONTS_MANIFEST = "fonts.json";
68
68
  * @property {string} direction - `ltr` or `rtl`.
69
69
  * @property {string[]} features - OpenType features it turns on.
70
70
  * @property {ReadonlySet<number>} draws - Every code point it draws.
71
+ * @property {string|undefined} file - The absolute path of its TrueType file,
72
+ * which the book draws a `scroll` fence's outlines from; `undefined` when the
73
+ * manifest lists none.
71
74
  */
72
75
 
73
76
  /**
@@ -164,6 +167,10 @@ export function resolveScriptFonts(rootDir) {
164
167
  direction: entry.direction === "rtl" ? "rtl" : "ltr",
165
168
  features: Object.freeze([...(entry.features ?? [])]),
166
169
  draws,
170
+ file:
171
+ typeof entry.files?.ttf === "string" ?
172
+ path.join(root, entry.files.ttf)
173
+ : undefined,
167
174
  }),
168
175
  );
169
176
  }
@@ -129,6 +129,7 @@ export function stressIndex(word, rule, pairs) {
129
129
  const count = nuclei.length;
130
130
  if (count <= 1) return 0;
131
131
  if (count === 2) return rule.two === "last" ? 1 : 0;
132
+ if (rule.longer === "first") return 0;
132
133
  if (rule.longer === "antepenultimate") return count - 3;
133
134
  if (rule.longer === "final") return count - 1;
134
135
  return count - 2;
@@ -0,0 +1,208 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * **The script a language is written in.** A language note — a `skill` of
16
+ * `subType: language` — names the map and the face its text is written in:
17
+ *
18
+ * ```yaml
19
+ * data:
20
+ * writtenIn: { map: semaron, font: semaron-kuriale }
21
+ * ```
22
+ *
23
+ * A run marked with the language's name takes them where it states neither:
24
+ * `[Zaron tholmeza]{lang=vyl}` is mapped by `semaron` and set in
25
+ * `semaron-kuriale`. A run that states one of the two takes the other from the
26
+ * default only where they agree, so the run is never set in a face its map is
27
+ * not drawn in:
28
+ *
29
+ * - **`map=` alone** takes the default `font` where that map is drawn in it.
30
+ * - **`font=` alone** takes the default `map` where the map is drawn in that
31
+ * face.
32
+ * - **Both** take nothing.
33
+ *
34
+ * The default belongs to the run's own language, after a div or fence around
35
+ * it has given it its keys: a span marked `lang=en` inside a div of a language
36
+ * with a default takes no face from it.
37
+ *
38
+ * @module
39
+ */
40
+
41
+ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
42
+ import { nearest } from "./near-miss.mjs";
43
+ import { compileMap } from "./text-maps.mjs";
44
+ import { LANGUAGE_SUBTYPE } from "./word-terms.mjs";
45
+
46
+ /** The `data` key a language note names its script in. */
47
+ export const WRITTEN_IN_KEY = "writtenIn";
48
+
49
+ /**
50
+ * A language's script.
51
+ *
52
+ * @typedef {object} WrittenIn
53
+ * @property {string} map - The `data.map.name` of the mapping note.
54
+ * @property {string} font - The slug of the face, one the map is drawn in.
55
+ */
56
+
57
+ /** Whether a value is a plain map. */
58
+ const isMap = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
59
+
60
+ /**
61
+ * Whether a map is drawn in a face: it lists the face, or lists none.
62
+ *
63
+ * @param {import("./text-maps.mjs").TextMap} map - The map.
64
+ * @param {string} font - The face's slug.
65
+ * @returns {boolean} Whether the map may set text in it.
66
+ */
67
+ export function drawnIn(map, font) {
68
+ return !map.fonts.size || map.fonts.has(font);
69
+ }
70
+
71
+ /** A list of names as prose: `a`, `a` and `b`, `a`, `b` and `c`. */
72
+ function said(names) {
73
+ const quoted = names.map((name) => `\`${name}\``);
74
+ return quoted.length < 2 ?
75
+ quoted.join("")
76
+ : `${quoted.slice(0, -1).join(", ")} and ${quoted.at(-1)}`;
77
+ }
78
+
79
+ /**
80
+ * The script each configured language's note names, where the map is known
81
+ * and is drawn in the face. A default that names an unknown map or a face its
82
+ * map is not drawn in is the note's finding, and fills no run.
83
+ *
84
+ * @param {ReadonlyMap<string, Record<string, any>>} byName - Each configured
85
+ * language's note record, by language name, from
86
+ * {@link module:engine/map-lexicon.languageRecordsByName}.
87
+ * @param {ReadonlyMap<string, import("./text-maps.mjs").TextMap>} maps - The
88
+ * maps by name.
89
+ * @returns {Map<string, WrittenIn>} The scripts, by language name.
90
+ */
91
+ export function writtenInOf(byName, maps) {
92
+ const out = new Map();
93
+ for (const [name, record] of byName ?? []) {
94
+ const value = record?.data?.[WRITTEN_IN_KEY];
95
+ if (!isMap(value) || typeof value.map !== "string" || typeof value.font !== "string")
96
+ continue;
97
+ const map = maps?.get(value.map);
98
+ if (!map || !drawnIn(map, value.font)) continue;
99
+ out.set(name, Object.freeze({ map: value.map, font: value.font }));
100
+ }
101
+ return out;
102
+ }
103
+
104
+ /**
105
+ * The keys a run takes from its language's script: each of `map` and `font`
106
+ * the run does not state, where it agrees with the one the run does.
107
+ *
108
+ * @param {{lang?: string, map?: string, font?: string}} keys - The run's keys,
109
+ * its own and those it inherits.
110
+ * @param {ReadonlyMap<string, WrittenIn>|undefined} scripts - Each language's
111
+ * script, from {@link writtenInOf}.
112
+ * @param {ReadonlyMap<string, import("./text-maps.mjs").TextMap>|undefined} maps -
113
+ * The maps by name.
114
+ * @returns {{map?: string, font?: string}} What the run takes; empty when it
115
+ * takes nothing.
116
+ */
117
+ export function fromWrittenIn(keys, scripts, maps) {
118
+ if (keys.lang === undefined || (keys.map !== undefined && keys.font !== undefined)) return {};
119
+ const script = scripts?.get(keys.lang);
120
+ if (!script) return {};
121
+ if (keys.map === undefined && keys.font === undefined) return { ...script };
122
+ if (keys.map === undefined) {
123
+ const map = maps?.get(script.map);
124
+ return map && drawnIn(map, /** @type {string} */ (keys.font)) ? { map: script.map } : {};
125
+ }
126
+ const map = maps?.get(keys.map);
127
+ return map && drawnIn(map, script.font) ? { font: script.font } : {};
128
+ }
129
+
130
+ /** The maps each lint index's notes declare, read once. */
131
+ const declaredMaps = new WeakMap();
132
+
133
+ /**
134
+ * Each map the index's notes declare, by name; the first note to declare a
135
+ * name keeps it.
136
+ *
137
+ * @param {{notes?: Iterable<{fm?: Record<string, any>}>}} index - The lint index.
138
+ * @returns {Map<string, import("./text-maps.mjs").TextMap>} The maps.
139
+ */
140
+ function mapsIn(index) {
141
+ const known = declaredMaps.get(index);
142
+ if (known) return known;
143
+ const maps = new Map();
144
+ for (const note of index.notes ?? []) {
145
+ const map = compileMap(note?.fm?.data?.map);
146
+ if (map && !maps.has(map.name)) maps.set(map.name, map);
147
+ }
148
+ declaredMaps.set(index, maps);
149
+ return maps;
150
+ }
151
+
152
+ /**
153
+ * Check a note's `data.writtenIn`: that the note is a language, that `map`
154
+ * names a map a mapping note declares, and that the map is drawn in `font`.
155
+ * The data-key check owns the keys and their kinds.
156
+ *
157
+ * @param {{file: string, raw?: string, fm?: Record<string, any>}} note - The note.
158
+ * @param {{index?: {notes?: Iterable<object>}}} [context] - The lint index;
159
+ * without one, only the note's `subType` is checked.
160
+ * @returns {object[]} The findings, each at its key or value.
161
+ */
162
+ export function checkWrittenIn(note, { index } = {}) {
163
+ const value = note?.fm?.data?.[WRITTEN_IN_KEY];
164
+ if (value === undefined || value === null) return [];
165
+ const raw = note.raw ?? "";
166
+ /**
167
+ * @param {Array<string|number>} keyPath - Where the fault is.
168
+ * @param {string} message - What is wrong.
169
+ * @param {boolean} [key] - Locate the key rather than its value.
170
+ */
171
+ const finding = (keyPath, message, key = false) => ({
172
+ file: note.file,
173
+ ...positionOfFrontmatterPath(raw, ["data", WRITTEN_IN_KEY, ...keyPath], { key }),
174
+ severity: "error",
175
+ message,
176
+ });
177
+ if (String(note.fm?.subType ?? "").toLowerCase() !== LANGUAGE_SUBTYPE)
178
+ return [
179
+ finding(
180
+ [],
181
+ `\`data.${WRITTEN_IN_KEY}\` names the script a language is written in, and this ` +
182
+ `note's \`subType\` is not \`${LANGUAGE_SUBTYPE}\``,
183
+ true,
184
+ ),
185
+ ];
186
+ if (!index || !isMap(value) || typeof value.map !== "string") return [];
187
+ const maps = mapsIn(index);
188
+ const map = maps.get(value.map);
189
+ if (!map) {
190
+ const near = nearest(value.map, [...maps.keys()]);
191
+ return [
192
+ finding(
193
+ ["map"],
194
+ `\`data.${WRITTEN_IN_KEY}.map\` is \`${value.map}\`, which no mapping note's ` +
195
+ "`data.map.name` declares" +
196
+ (near ? `; did you mean \`${near}\`?` : ""),
197
+ ),
198
+ ];
199
+ }
200
+ if (typeof value.font !== "string" || drawnIn(map, value.font)) return [];
201
+ return [
202
+ finding(
203
+ ["font"],
204
+ `\`data.${WRITTEN_IN_KEY}.font\` is \`${value.font}\`, which the map \`${map.name}\` ` +
205
+ `is not drawn in; it is drawn in ${said([...map.fonts.keys()])}`,
206
+ ),
207
+ ];
208
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "24.0.1",
3
+ "version": "24.0.2",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -3114,6 +3114,13 @@ summary.info-sidebar-title::after {
3114
3114
  and `mono` set the site's own serif and mono, and the reader's sans. */
3115
3115
  .script-block { margin:1rem 0; line-height:1.7; }
3116
3116
  .script-block[dir="rtl"], .verse[dir="rtl"] { text-align:start; }
3117
+ /* A scroll: columns top to bottom, read from the right; the line height is the
3118
+ column pitch, one em and half an em between columns. A panel wider than the
3119
+ page scrolls sideways from the right. */
3120
+ .single-content .scroll { writing-mode:vertical-rl; line-height:1.5; inline-size:max-content; max-block-size:100%; overflow-x:auto; overflow-y:hidden; margin:1rem 0; }
3121
+ .single-content .scroll[data-rows] { inline-size:calc(var(--scroll-rows) * 1em); }
3122
+ .single-content .scroll[data-cols] { block-size:calc(var(--scroll-cols) * 1.5em); }
3123
+ .single-content .scroll p { margin:0; text-indent:0; }
3117
3124
  .font-serif { font-family:var(--font-body); }
3118
3125
  .font-sans { font-family:sans-serif; }
3119
3126
  .font-mono { font-family:var(--font-mono); }
@@ -63,6 +63,10 @@ export function scanRuns(source: string, ctx?: RunContext, syntax?: import("./co
63
63
  * - **A div or fence in a right-to-left face** carries `dir=rtl`. No direction
64
64
  * mark, `<bdo>` or `<bdi>` is ever written: the code points carry their own
65
65
  * direction.
66
+ * - **A scroll fence's body** is rewritten to one line per paragraph, its
67
+ * lines joined by what the map writes for a space, and left unescaped,
68
+ * since the body is raw. Its layout, each character with the source text it
69
+ * was written from, is returned for the book, which draws it.
66
70
  *
67
71
  * A run with a finding is left as written; {@link scanRuns} reports it.
68
72
  *
@@ -72,8 +76,9 @@ export function scanRuns(source: string, ctx?: RunContext, syntax?: import("./co
72
76
  * scanner projections.
73
77
  * @param {{target?: "site"|"foundry"|"book"}} [options] - The target the
74
78
  * Markdown is rendered for; only `site` writes the search marker.
75
- * @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>}}
76
- * The rewritten Markdown, and the findings of {@link scanRuns}.
79
+ * @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>, scrolls: Map<string, import("./content-scroll.mjs").ScrollLayout>}}
80
+ * The rewritten Markdown, the findings of {@link scanRuns}, and each
81
+ * rewritten scroll fence's layout, by its rewritten body.
77
82
  */
78
83
  export function renderRuns(source: string, ctx?: RunContext, syntax?: import("./content-session.mjs").ContentSyntax, options?: {
79
84
  target?: "site" | "foundry" | "book";
@@ -84,6 +89,7 @@ export function renderRuns(source: string, ctx?: RunContext, syntax?: import("./
84
89
  column: number;
85
90
  message: string;
86
91
  }>;
92
+ scrolls: Map<string, import("./content-scroll.mjs").ScrollLayout>;
87
93
  };
88
94
  /** The keys this module reads, which no construct passes through as written. */
89
95
  export const RUN_KEYS: readonly string[];
@@ -184,6 +190,11 @@ export type RunDetail = {
184
190
  * - The emphasis it holds, by absolute offset.
185
191
  */
186
192
  emphasis: Delimiter[];
193
+ /**
194
+ * - A scroll fence whose opening has a finding,
195
+ * which is left as written.
196
+ */
197
+ unrolled?: boolean | undefined;
187
198
  };
188
199
  /**
189
200
  * What a run is checked against.
@@ -208,6 +219,12 @@ export type RunContext = {
208
219
  * {@link module :engine/text-maps.mapsFromRecords}.
209
220
  */
210
221
  maps?: ReadonlyMap<string, import("./text-maps.mjs").TextMap> | undefined;
222
+ /**
223
+ * -
224
+ * The script each configured language's note names, by language name, from
225
+ * {@link module :engine/written-in.writtenInOf}.
226
+ */
227
+ writtenIn?: ReadonlyMap<string, import("./written-in.mjs").WrittenIn> | undefined;
211
228
  /**
212
229
  * -
213
230
  * Each configured language's lexicon by name, from
@@ -243,17 +260,25 @@ export type RunContext = {
243
260
  * from {@link module :engine/script-emphasis.languageEmphasesOf}.
244
261
  */
245
262
  languageEmphases?: ReadonlyMap<string, import("./script-emphasis.mjs").LanguageEmphasis> | undefined;
263
+ /**
264
+ * - Each face whose
265
+ * script is written in columns, from the script notes, by
266
+ * {@link module :engine/script-direction.directionsFromRecords}.
267
+ */
268
+ directions?: ReadonlyMap<string, "vertical-rl"> | undefined;
246
269
  };
247
270
  /**
248
- * One span, div or verse fence carrying `lang=`, `font=` or `map=`. Its
249
- * `lang`, `font` and `map` are the ones it is set with: its own, else the
250
- * innermost div or fence around it that states one.
271
+ * One span, div, verse fence or scroll fence carrying `lang=`, `font=` or
272
+ * `map=`. Its `lang`, `font` and `map` are the ones it is set with: its own,
273
+ * else the innermost div or fence around it that states one, else its
274
+ * language's script. A scroll fence that still names no face takes the face of
275
+ * its map where the map is drawn in one.
251
276
  */
252
277
  export type Run = {
253
278
  /**
254
279
  * - Which construct.
255
280
  */
256
- kind: "span" | "block" | "verse";
281
+ kind: "span" | "block" | "verse" | "scroll";
257
282
  /**
258
283
  * - The language as written.
259
284
  */
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Read every `scroll` fence in a source, and every fault in its opening.
3
+ *
4
+ * A fence inside a block quote or an HTML block is not read, and neither is a
5
+ * fence quoted inside another fence.
6
+ *
7
+ * @param {string} source - Markdown.
8
+ * @returns {{blocks: Scroll[], errors: Array<{line: number, column: number, message: string}>}}
9
+ * The fences, and each finding at its 1-based position.
10
+ */
11
+ export function scanScrolls(source: string): {
12
+ blocks: Scroll[];
13
+ errors: Array<{
14
+ line: number;
15
+ column: number;
16
+ message: string;
17
+ }>;
18
+ };
19
+ /**
20
+ * A scroll's paragraphs: each run of nonblank lines, its lines trimmed and
21
+ * joined by a space.
22
+ *
23
+ * @param {string[]} lines - The body's lines.
24
+ * @returns {string[]} The paragraphs, in order.
25
+ */
26
+ export function scrollParagraphs(lines: string[]): string[];
27
+ /**
28
+ * Replace each well-formed `scroll` fence with the HTML block the site, the
29
+ * preview and Foundry show: a `<div class="scroll">` holding a `<p>` for each
30
+ * paragraph, its size carried as `--scroll-rows` and `--scroll-cols`. A fence
31
+ * with a finding in its opening is left as a code fence.
32
+ *
33
+ * @param {string} source - Markdown, its marked text already rewritten.
34
+ * @returns {{markdown: string}} The Markdown.
35
+ */
36
+ export function renderScroll(source: string): {
37
+ markdown: string;
38
+ };
39
+ /**
40
+ * One character of a scroll as the book draws it.
41
+ *
42
+ * @typedef {object} ScrollCell
43
+ * @property {string} text - The character, one code point.
44
+ * @property {string} roman - The source text it was written from: the text of
45
+ * the piece of the map it begins, with the text of any piece before it that
46
+ * wrote nothing; `""` for a later character of the same piece.
47
+ * @property {"rubric"|"overline"} [emphasis] - The convention it is set by.
48
+ */
49
+ /**
50
+ * A scroll's text, as the book lays it out.
51
+ *
52
+ * @typedef {object} ScrollLayout
53
+ * @property {ScrollCell[][]} paragraphs - Each paragraph's characters.
54
+ * @property {import("./script-fonts.mjs").Face|undefined} face - The face it
55
+ * is drawn in.
56
+ */
57
+ /**
58
+ * Lay a scroll's paragraphs into columns and the columns into panels.
59
+ *
60
+ * Each paragraph starts a fresh column, filled from the top, `rows` characters
61
+ * to a column; the columns are read from the right and grouped `cols` to a
62
+ * panel. Without `rows`, a column is as long as the longest paragraph, up to
63
+ * {@link BOOK_SCROLL_ROWS}; without `cols`, each column is a panel of its own,
64
+ * so the book sets as many beside one another as fit its measure.
65
+ *
66
+ * @param {ScrollCell[][]} paragraphs - The characters of each paragraph.
67
+ * @param {{rows?: number, cols?: number}} size - The fence's size.
68
+ * @returns {{rows: number, panels: ScrollCell[][][]}} The column length, and
69
+ * each panel's columns, the first column the rightmost.
70
+ */
71
+ export function scrollPanels(paragraphs: ScrollCell[][], { rows, cols }?: {
72
+ rows?: number;
73
+ cols?: number;
74
+ }): {
75
+ rows: number;
76
+ panels: ScrollCell[][][];
77
+ };
78
+ /**
79
+ * The roman text a panel was written from, as its alt text: each paragraph's
80
+ * source text, a space between paragraphs, its spacing collapsed.
81
+ *
82
+ * @param {ScrollCell[][]} panel - The panel's columns.
83
+ * @param {ScrollCell[][]} paragraphs - The scroll's paragraphs, so a column
84
+ * that begins a paragraph is told from one that continues it.
85
+ * @returns {string} The text.
86
+ */
87
+ export function panelRoman(panel: ScrollCell[][], paragraphs: ScrollCell[][]): string;
88
+ /**
89
+ * The keys a scroll fence's attribute block takes. `lang`, `font` and `map`
90
+ * are read by `scanRuns`; `data-pagefind-ignore` is written on the site by its
91
+ * rewrite of a fence set in a face, and reaches the element as written.
92
+ */
93
+ export const SCROLL_KEYS: readonly string[];
94
+ /**
95
+ * The distance from one column to the next, in ems: the character's one em
96
+ * and a gap of half an em.
97
+ */
98
+ export const SCROLL_PITCH: 1.5;
99
+ /**
100
+ * The longest column the book sets when a fence states no `rows=`: each
101
+ * paragraph is one column as long as the longest, up to this many characters.
102
+ */
103
+ export const BOOK_SCROLL_ROWS: 40;
104
+ /** The class every scroll carries. */
105
+ export const SCROLL_CLASS: "scroll";
106
+ /**
107
+ * One `scroll` fence.
108
+ */
109
+ export type Scroll = {
110
+ /**
111
+ * - The 0-based line of its opening.
112
+ */
113
+ start: number;
114
+ /**
115
+ * - The 0-based line of its closing.
116
+ */
117
+ end: number;
118
+ /**
119
+ * - Its fence marker.
120
+ */
121
+ fence: string;
122
+ /**
123
+ * - The opening line's indentation.
124
+ */
125
+ indent: string;
126
+ /**
127
+ * - Its `key=value` attributes.
128
+ */
129
+ attributes: Record<string, string>;
130
+ /**
131
+ * - Its classes.
132
+ */
133
+ classes: string[];
134
+ /**
135
+ * - Its `rows=`, where it is valid.
136
+ */
137
+ rows: number | undefined;
138
+ /**
139
+ * - Its `cols=`, where it is valid.
140
+ */
141
+ cols: number | undefined;
142
+ /**
143
+ * - Whether its opening has a finding.
144
+ */
145
+ invalid: boolean;
146
+ };
147
+ /**
148
+ * One character of a scroll as the book draws it.
149
+ */
150
+ export type ScrollCell = {
151
+ /**
152
+ * - The character, one code point.
153
+ */
154
+ text: string;
155
+ /**
156
+ * - The source text it was written from: the text of
157
+ * the piece of the map it begins, with the text of any piece before it that
158
+ * wrote nothing; `""` for a later character of the same piece.
159
+ */
160
+ roman: string;
161
+ /**
162
+ * - The convention it is set by.
163
+ */
164
+ emphasis?: "rubric" | "overline" | undefined;
165
+ };
166
+ /**
167
+ * A scroll's text, as the book lays it out.
168
+ */
169
+ export type ScrollLayout = {
170
+ /**
171
+ * - Each paragraph's characters.
172
+ */
173
+ paragraphs: ScrollCell[][];
174
+ /**
175
+ * - The face it
176
+ * is drawn in.
177
+ */
178
+ face: import("./script-fonts.mjs").Face | undefined;
179
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The outlines of a TrueType face, read once per file.
3
+ *
4
+ * @param {string} file - The `.ttf`.
5
+ * @returns {Outlines} Its outlines.
6
+ * @throws {Error} When the file is not a TrueType face with outlines.
7
+ */
8
+ export function fontOutlines(file: string): Outlines;
9
+ /**
10
+ * One panel of a scroll as SVG: its columns read from the right, `rows` ems
11
+ * tall, each character centred in its column, in red where it is set in the
12
+ * rubric and with a line beside it, on the right, where it is overlined.
13
+ *
14
+ * @param {import("./content-scroll.mjs").ScrollCell[][]} panel - Its columns,
15
+ * the first the rightmost.
16
+ * @param {number} rows - The characters in a column.
17
+ * @param {Outlines} outlines - The face.
18
+ * @returns {string} The SVG.
19
+ */
20
+ export function scrollPanelSvg(panel: import("./content-scroll.mjs").ScrollCell[][], rows: number, outlines: Outlines): string;
21
+ /** The book's rubric red, which its preamble names `book-rubric`. */
22
+ export const BOOK_RUBRIC: "#9c281b";
23
+ /** The book's ink, which its preamble names `book-ink`. */
24
+ export const BOOK_INK: "#241f1a";
25
+ /**
26
+ * A point of a contour.
27
+ */
28
+ export type Point = {
29
+ x: number;
30
+ y: number;
31
+ on: boolean;
32
+ };
33
+ /**
34
+ * One glyph, in font units, its y axis pointing down from the baseline.
35
+ */
36
+ export type Glyph = {
37
+ /**
38
+ * - Its glyph index.
39
+ */
40
+ index: number;
41
+ /**
42
+ * - Its outline as SVG path data; `""` for none.
43
+ */
44
+ d: string;
45
+ /**
46
+ * - Its horizontal advance.
47
+ */
48
+ advance: number;
49
+ /**
50
+ * - From the top of its cell to its baseline.
51
+ */
52
+ top: number;
53
+ };
54
+ /**
55
+ * A face's outlines.
56
+ */
57
+ export type Outlines = {
58
+ /**
59
+ * - Its units per em.
60
+ */
61
+ unitsPerEm: number;
62
+ /**
63
+ * - The glyph a code
64
+ * point is drawn with, or `undefined` when the face does not draw it.
65
+ */
66
+ glyph: (point: number) => Glyph | undefined;
67
+ };