@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.
- package/CHANGELOG.md +14 -0
- package/assets/preview/gitea.ini +1 -1
- package/assets/styles/content-base.css +33 -0
- package/bin/content-commands.mjs +9 -7
- package/docs/api.md +6 -5
- package/docs/authoring/links-and-markup.md +164 -16
- package/docs/commands.md +16 -14
- package/docs/configuration.md +16 -0
- package/docs/content-format.md +4 -2
- package/docs/guides/writing-in-invented-scripts.md +68 -6
- package/docs/reference/format-details.md +143 -49
- package/docs/reference/note-types.md +47 -28
- package/docs/source-preview.md +2 -2
- package/engine/content-blocks.mjs +6 -2
- package/engine/content-format.yaml +2 -0
- package/engine/content-index.mjs +5 -1
- package/engine/content-render.mjs +2 -1
- package/engine/content-scripts.mjs +268 -21
- package/engine/content-scroll.mjs +389 -0
- package/engine/font-outlines.mjs +394 -0
- package/engine/language-data.mjs +154 -63
- package/engine/language-rows.mjs +32 -6
- package/engine/language-terms.mjs +32 -8
- package/engine/map-lexicon.mjs +31 -6
- package/engine/note-vocabulary.mjs +58 -6
- package/engine/pdf-fonts.mjs +1 -1
- package/engine/pdf-render.mjs +52 -11
- package/engine/run-rows.mjs +45 -3
- package/engine/script-direction.mjs +67 -0
- package/engine/script-emphasis-terms.mjs +11 -3
- package/engine/script-font-delivery.mjs +42 -9
- package/engine/script-fonts.mjs +7 -0
- package/engine/verse-metre.mjs +1 -0
- package/engine/written-in.mjs +208 -0
- package/package.json +1 -1
- package/site-theme/static/css/style.css +7 -0
- package/types/engine/content-scripts.d.mts +31 -6
- package/types/engine/content-scroll.d.mts +179 -0
- package/types/engine/font-outlines.d.mts +67 -0
- package/types/engine/language-data.d.mts +13 -0
- package/types/engine/language-rows.d.mts +4 -3
- package/types/engine/language-terms.d.mts +7 -0
- package/types/engine/map-lexicon.d.mts +4 -3
- package/types/engine/pdf-fonts.d.mts +10 -0
- package/types/engine/run-rows.d.mts +28 -6
- package/types/engine/script-direction.d.mts +10 -0
- package/types/engine/script-emphasis-terms.d.mts +10 -3
- package/types/engine/script-font-delivery.d.mts +5 -3
- package/types/engine/script-fonts.d.mts +9 -0
- 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=`
|
|
17
|
-
* fonts on the site, in the Foundry
|
|
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
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
-
|
|
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)
|
package/engine/script-fonts.mjs
CHANGED
|
@@ -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
|
}
|
package/engine/verse-metre.mjs
CHANGED
|
@@ -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.
|
|
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,
|
|
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
|
|
249
|
-
* `lang`, `font` and `map` are the ones it is set with: its own,
|
|
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
|
+
};
|