@heroiclands/package-build 23.12.6 → 23.12.7
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 +12 -0
- package/assets/styles/content-base.css +25 -0
- package/bin/content-commands.mjs +264 -3
- package/bin/package-build.mjs +25 -0
- package/content-config.mjs +72 -0
- package/docs/api.md +10 -9
- package/docs/authoring/links-and-markup.md +439 -47
- package/docs/commands.md +127 -0
- package/docs/configuration.md +66 -3
- package/docs/content-format.md +3 -1
- package/docs/guides/book.md +11 -0
- package/docs/guides/module-project.md +16 -0
- package/docs/guides/site.md +19 -0
- package/docs/reference/format-details.md +372 -6
- package/docs/reference/note-types.md +36 -11
- package/engine/base-compiler.mjs +2 -0
- package/engine/bundles.mjs +7 -2
- package/engine/compile-corpus.mjs +3 -1
- package/engine/content-charset.mjs +21 -2
- package/engine/content-format.yaml +1 -0
- package/engine/content-index.mjs +12 -0
- package/engine/content-poetry.mjs +22 -4
- package/engine/content-scripts.mjs +1219 -0
- package/engine/content-session.mjs +2 -0
- package/engine/data-keys.mjs +5 -1
- package/engine/dictionary-layout.mjs +18 -12
- package/engine/dictionary-rows.mjs +11 -2
- package/engine/helpers.mjs +8 -2
- package/engine/index-records.mjs +1 -0
- package/engine/journals.mjs +14 -3
- package/engine/language-data.mjs +137 -1
- package/engine/language-tag.mjs +103 -0
- package/engine/language-terms.mjs +16 -4
- package/engine/map-lexicon.mjs +762 -0
- package/engine/markup-validation.mjs +17 -3
- package/engine/names.mjs +1 -1
- package/engine/note-vocabulary.mjs +113 -0
- package/engine/pdf-build.mjs +28 -10
- package/engine/pdf-render.mjs +116 -11
- package/engine/run-rows.mjs +119 -0
- package/engine/run-words.mjs +266 -0
- package/engine/scenes.mjs +1 -0
- package/engine/script-font-delivery.mjs +321 -0
- package/engine/script-fonts.mjs +172 -0
- package/engine/site-build.mjs +10 -3
- package/engine/site-config.mjs +5 -0
- package/engine/site-root.mjs +19 -5
- package/engine/sql-tables.mjs +111 -1
- package/engine/text-maps.mjs +607 -0
- package/engine/verb-pattern.mjs +99 -0
- package/engine/word-pattern.mjs +27 -0
- package/engine/word-rows.mjs +8 -2
- package/engine/word-terms.mjs +44 -0
- package/hugo-theme/layouts/_default/baseof.html +3 -0
- package/hugo-theme/static/css/style.css +10 -0
- package/manifest.mjs +9 -1
- package/package.json +1 -1
- package/types/content-config.d.mts +26 -0
- package/types/engine/compile-corpus.d.mts +2 -1
- package/types/engine/content-index.d.mts +11 -3
- package/types/engine/content-poetry.d.mts +7 -0
- package/types/engine/content-scripts.d.mts +226 -0
- package/types/engine/data-keys.d.mts +2 -2
- package/types/engine/dictionary-rows.d.mts +15 -0
- package/types/engine/helpers.d.mts +5 -1
- package/types/engine/journals.d.mts +7 -2
- package/types/engine/language-data.d.mts +2 -0
- package/types/engine/language-tag.d.mts +8 -0
- package/types/engine/language-terms.d.mts +2 -1
- package/types/engine/map-lexicon.d.mts +274 -0
- package/types/engine/markup-validation.d.mts +7 -2
- package/types/engine/pdf-build.d.mts +26 -3
- package/types/engine/pdf-render.d.mts +17 -2
- package/types/engine/run-rows.d.mts +89 -0
- package/types/engine/run-words.d.mts +86 -0
- package/types/engine/script-font-delivery.d.mts +134 -0
- package/types/engine/script-fonts.d.mts +168 -0
- package/types/engine/site-build.d.mts +2 -1
- package/types/engine/site-root.d.mts +11 -3
- package/types/engine/text-maps.d.mts +175 -0
- package/types/engine/verb-pattern.d.mts +65 -0
- package/types/engine/word-pattern.d.mts +13 -0
- package/types/engine/word-terms.d.mts +6 -0
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { scanBlocks } from "./content-blocks.mjs";
|
|
15
15
|
import { scanAlerts } from "./content-alerts.mjs";
|
|
16
|
+
import { scanRuns } from "./content-scripts.mjs";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Validate authored block syntax with positions in the original file.
|
|
@@ -20,20 +21,33 @@ import { scanAlerts } from "./content-alerts.mjs";
|
|
|
20
21
|
* @param {string} source - Complete Markdown source, optionally with frontmatter.
|
|
21
22
|
* @param {string} file - Diagnostic filename.
|
|
22
23
|
* @param {import("./content-session.mjs").ContentSyntax} [syntax] - Exact-text scanner projections.
|
|
23
|
-
* @
|
|
24
|
+
* @param {import("./content-scripts.mjs").RunContext} [runs] - The package's
|
|
25
|
+
* languages, faces and maps. Given, text marked with `lang=`, `font=` and
|
|
26
|
+
* `map=` is checked against them; a caller with no package configuration
|
|
27
|
+
* leaves it out.
|
|
28
|
+
* @returns {Array<{file: string, line: number, column: number, severity: string, message: string}>} Syntax errors,
|
|
29
|
+
* then warnings.
|
|
24
30
|
*/
|
|
25
|
-
export function markupFindings(source, file, syntax) {
|
|
31
|
+
export function markupFindings(source, file, syntax, runs) {
|
|
26
32
|
const body = String(source ?? "").replace(
|
|
27
33
|
/^\uFEFF?---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/,
|
|
28
34
|
(frontmatter) => frontmatter.replace(/[^\r\n]/g, " "),
|
|
29
35
|
);
|
|
30
36
|
const scanned = syntax?.(body);
|
|
31
|
-
|
|
37
|
+
const marked = runs ? scanRuns(body, runs, syntax) : undefined;
|
|
38
|
+
const errors = [
|
|
32
39
|
...(scanned?.blocks ?? scanBlocks(body)).errors,
|
|
33
40
|
...(scanned?.alerts ?? scanAlerts(body)).errors,
|
|
41
|
+
...(marked?.errors ?? []),
|
|
34
42
|
].map((error) => ({
|
|
35
43
|
file,
|
|
36
44
|
...error,
|
|
37
45
|
severity: "error",
|
|
38
46
|
}));
|
|
47
|
+
const warnings = (marked?.warnings ?? []).map((warning) => ({
|
|
48
|
+
file,
|
|
49
|
+
...warning,
|
|
50
|
+
severity: "warning",
|
|
51
|
+
}));
|
|
52
|
+
return [...errors, ...warnings];
|
|
39
53
|
}
|
package/engine/names.mjs
CHANGED
|
@@ -19,8 +19,8 @@ import { formatLocator } from "./diagnostics.mjs";
|
|
|
19
19
|
import { FOLDER_TYPE } from "./folder-notes.mjs";
|
|
20
20
|
import { isNoteRecord } from "./index-records.mjs";
|
|
21
21
|
import { NOTE_VOCABULARY } from "./note-vocabulary.mjs";
|
|
22
|
+
import { WORD } from "./word-pattern.mjs";
|
|
22
23
|
|
|
23
|
-
const WORD = /[\p{L}\p{M}\p{N}]+(?:[’'–-][\p{L}\p{M}\p{N}]+)*/gu;
|
|
24
24
|
const compare = (a, b) =>
|
|
25
25
|
a < b ? -1
|
|
26
26
|
: a > b ? 1
|
|
@@ -153,6 +153,7 @@ import { checkCitedPopulations, checkPopulation } from "./populations.mjs";
|
|
|
153
153
|
import { MARKET_CLASSES, checkMarket } from "./market-class.mjs";
|
|
154
154
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
155
155
|
import { checkSettlementTables } from "./settlement-tables.mjs";
|
|
156
|
+
import { checkMapField } from "./text-maps.mjs";
|
|
156
157
|
|
|
157
158
|
/**
|
|
158
159
|
* One `data:` key a note type may carry.
|
|
@@ -1514,6 +1515,47 @@ export const BEING_ARCHETYPES = Object.freeze({
|
|
|
1514
1515
|
guildsperson: "Has professional training, standing, or connections that open doors.",
|
|
1515
1516
|
});
|
|
1516
1517
|
|
|
1518
|
+
/**
|
|
1519
|
+
* One row of a mapping note's `data.map.rows` or of a face's own `rows`.
|
|
1520
|
+
*
|
|
1521
|
+
* @type {object}
|
|
1522
|
+
*/
|
|
1523
|
+
const MAP_ROW_ENTRY = Object.freeze({
|
|
1524
|
+
kind: "list-or-map",
|
|
1525
|
+
shape: "`[from, to]` or `{ from, to, before?, at? }`",
|
|
1526
|
+
entries: Object.freeze({ ...TEXT, shape: "a string" }),
|
|
1527
|
+
fields: Object.freeze([
|
|
1528
|
+
{
|
|
1529
|
+
name: "from",
|
|
1530
|
+
...TEXT,
|
|
1531
|
+
shape: "a string of one or more characters",
|
|
1532
|
+
required: true,
|
|
1533
|
+
describe: "The one or more characters the row matches.",
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
name: "to",
|
|
1537
|
+
...TEXT,
|
|
1538
|
+
shape: "a string, which may be empty",
|
|
1539
|
+
required: true,
|
|
1540
|
+
allowEmpty: true,
|
|
1541
|
+
describe: "What the row writes in their place; empty to drop them.",
|
|
1542
|
+
},
|
|
1543
|
+
{
|
|
1544
|
+
name: "before",
|
|
1545
|
+
...TEXT,
|
|
1546
|
+
allowed: Object.freeze(["vowel", "consonant"]),
|
|
1547
|
+
describe:
|
|
1548
|
+
"`vowel` or `consonant`: what must follow the match for the " + "row to apply.",
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
name: "at",
|
|
1552
|
+
...TEXT,
|
|
1553
|
+
allowed: Object.freeze(["start", "end"]),
|
|
1554
|
+
describe: "`start` or `end`: where the match must sit for the row to " + "apply.",
|
|
1555
|
+
},
|
|
1556
|
+
]),
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1517
1559
|
/**
|
|
1518
1560
|
* Every note type this toolchain compiles, and the closed vocabulary it
|
|
1519
1561
|
* declares.
|
|
@@ -2148,6 +2190,77 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2148
2190
|
"The culture a setting guide introduces, as a culture lore note. " +
|
|
2149
2191
|
"Required on `settingguide`; no other subType states it.",
|
|
2150
2192
|
},
|
|
2193
|
+
{
|
|
2194
|
+
name: "map",
|
|
2195
|
+
kind: "map",
|
|
2196
|
+
shape: "a map — `{ name, vowels?, fonts, rows }`",
|
|
2197
|
+
check: checkMapField,
|
|
2198
|
+
fields: Object.freeze([
|
|
2199
|
+
{
|
|
2200
|
+
name: "name",
|
|
2201
|
+
...TEXT,
|
|
2202
|
+
required: true,
|
|
2203
|
+
describe:
|
|
2204
|
+
"What `map=` names the map by: lowercase letters, digits and `-`, " +
|
|
2205
|
+
"unique in the package.",
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
name: "vowels",
|
|
2209
|
+
...TEXT,
|
|
2210
|
+
describe:
|
|
2211
|
+
"The characters a row's `before: vowel` reads as vowels; any other " +
|
|
2212
|
+
"letter is a consonant.",
|
|
2213
|
+
},
|
|
2214
|
+
{
|
|
2215
|
+
name: "rows",
|
|
2216
|
+
kind: "list",
|
|
2217
|
+
shape: "list of `[from, to]` or `{ from, to, before?, at? }`",
|
|
2218
|
+
required: true,
|
|
2219
|
+
entries: MAP_ROW_ENTRY,
|
|
2220
|
+
describe:
|
|
2221
|
+
"The rewrites, each `[from, to]` or `{ from, to, before?, at? }`: " +
|
|
2222
|
+
"`before` is `vowel` or `consonant`, `at` is `start` or `end`. The " +
|
|
2223
|
+
"longest `from` wins, and a character no row matches passes through.",
|
|
2224
|
+
},
|
|
2225
|
+
{
|
|
2226
|
+
name: "fonts",
|
|
2227
|
+
kind: "list",
|
|
2228
|
+
shape: "list of `{ font, rows? }`",
|
|
2229
|
+
required: true,
|
|
2230
|
+
entries: Object.freeze({
|
|
2231
|
+
kind: "map",
|
|
2232
|
+
shape: "a map — `{ font, rows? }`",
|
|
2233
|
+
fields: Object.freeze([
|
|
2234
|
+
{
|
|
2235
|
+
name: "font",
|
|
2236
|
+
...TEXT,
|
|
2237
|
+
required: true,
|
|
2238
|
+
shape: "an installed face's slug",
|
|
2239
|
+
describe:
|
|
2240
|
+
"A face the map is drawn in, by the slug `font=` names it by.",
|
|
2241
|
+
},
|
|
2242
|
+
{
|
|
2243
|
+
name: "rows",
|
|
2244
|
+
kind: "list",
|
|
2245
|
+
shape: "list of `[from, to]` or `{ from, to, before?, at? }`",
|
|
2246
|
+
entries: MAP_ROW_ENTRY,
|
|
2247
|
+
describe:
|
|
2248
|
+
"Rows for this face alone, tried before the shared `rows`; " +
|
|
2249
|
+
"the same shapes and conditions.",
|
|
2250
|
+
},
|
|
2251
|
+
]),
|
|
2252
|
+
}),
|
|
2253
|
+
describe:
|
|
2254
|
+
"The faces the map is drawn in, at least one, each with any rows of " +
|
|
2255
|
+
"its own. A run in another face is refused, and each face must draw " +
|
|
2256
|
+
"every character its rows and the lexicon's native forms write.",
|
|
2257
|
+
},
|
|
2258
|
+
]),
|
|
2259
|
+
describe:
|
|
2260
|
+
"A mapping, which text marked `map=` with its `name` is rewritten by " +
|
|
2261
|
+
"before it is set. See " +
|
|
2262
|
+
"[mapping notes](format-details.md#mapping-notes).",
|
|
2263
|
+
},
|
|
2151
2264
|
]),
|
|
2152
2265
|
}),
|
|
2153
2266
|
|
package/engine/pdf-build.mjs
CHANGED
|
@@ -81,6 +81,7 @@ import { fileURLToPath } from "node:url";
|
|
|
81
81
|
import YAML from "yaml";
|
|
82
82
|
|
|
83
83
|
import { CONFIG_FILENAMES, loadPackConfig, packConfigPath } from "./pack-config.mjs";
|
|
84
|
+
import { runContext } from "./content-scripts.mjs";
|
|
84
85
|
import { routerFor } from "./pack-router.mjs";
|
|
85
86
|
import { publishesContentPages } from "../content-config.mjs";
|
|
86
87
|
import { indexRecordsFor } from "./content-index.mjs";
|
|
@@ -160,6 +161,7 @@ import { resolveIconGlyphs } from "./pdf-fonts.mjs";
|
|
|
160
161
|
import { drawMapsFrom } from "./derived-sections.mjs";
|
|
161
162
|
import { generatedDrawing, markGenerated, splitGenerated } from "./generated-sections.mjs";
|
|
162
163
|
import { resolveAssetReplacement } from "./asset-replacement.mjs";
|
|
164
|
+
import { typstScriptFontPath } from "./script-font-delivery.mjs";
|
|
163
165
|
|
|
164
166
|
/**
|
|
165
167
|
* The file on disk an authored image pathname names, or `null`.
|
|
@@ -832,7 +834,7 @@ export async function buildPdf({
|
|
|
832
834
|
* message: string}} The table, or null with why it could not be read.
|
|
833
835
|
*/
|
|
834
836
|
const measureAll = (written) => {
|
|
835
|
-
const fontArgs = typstFontArgs(resolved.pdf);
|
|
837
|
+
const fontArgs = typstFontArgs(resolved.pdf, resolved);
|
|
836
838
|
const measured = new Map();
|
|
837
839
|
for (const { entry } of toRender) {
|
|
838
840
|
const result = readPages(
|
|
@@ -906,7 +908,7 @@ export async function buildPdf({
|
|
|
906
908
|
if (!wanted) continue;
|
|
907
909
|
const result = pass.written.get(entry.id);
|
|
908
910
|
imageReports.push(...(result.imageReports ?? []));
|
|
909
|
-
const ok = compile ? compileBook(result, resolved.pdf, findings) : true;
|
|
911
|
+
const ok = compile ? compileBook(result, resolved.pdf, findings, resolved) : true;
|
|
910
912
|
if (!ok) built = false;
|
|
911
913
|
books.push({
|
|
912
914
|
id: entry.id,
|
|
@@ -1616,6 +1618,7 @@ async function buildBook(book, shared) {
|
|
|
1616
1618
|
url: site,
|
|
1617
1619
|
elsewhere,
|
|
1618
1620
|
findings,
|
|
1621
|
+
runs: runContext(resolved, records),
|
|
1619
1622
|
file: page.file,
|
|
1620
1623
|
bodyLine: page.bodyLine,
|
|
1621
1624
|
prepared: true,
|
|
@@ -1706,6 +1709,7 @@ async function buildBook(book, shared) {
|
|
|
1706
1709
|
elsewhere,
|
|
1707
1710
|
indexMentions: true,
|
|
1708
1711
|
findings,
|
|
1712
|
+
runs: runContext(resolved, records),
|
|
1709
1713
|
file,
|
|
1710
1714
|
}),
|
|
1711
1715
|
);
|
|
@@ -1737,6 +1741,7 @@ async function buildBook(book, shared) {
|
|
|
1737
1741
|
elsewhere,
|
|
1738
1742
|
indexMentions: true,
|
|
1739
1743
|
findings,
|
|
1744
|
+
runs: runContext(resolved, records),
|
|
1740
1745
|
file,
|
|
1741
1746
|
});
|
|
1742
1747
|
} catch {
|
|
@@ -1910,6 +1915,7 @@ async function buildBook(book, shared) {
|
|
|
1910
1915
|
copyright: shared.copyright,
|
|
1911
1916
|
license: shared.license,
|
|
1912
1917
|
index,
|
|
1918
|
+
runs: runContext(resolved, records),
|
|
1913
1919
|
});
|
|
1914
1920
|
// Last, over the whole document: a reference can only be checked once every
|
|
1915
1921
|
// declaration is in one string, and Typst treats a dangling one as fatal.
|
|
@@ -1932,10 +1938,11 @@ async function buildBook(book, shared) {
|
|
|
1932
1938
|
* @param {{typ: string, pdf: string}} written - From {@link buildBook}.
|
|
1933
1939
|
* @param {object} pdf - The resolved `pdf:` block.
|
|
1934
1940
|
* @param {object[]} findings - Collected here rather than thrown.
|
|
1941
|
+
* @param {object} config - The resolved configuration.
|
|
1935
1942
|
* @returns {boolean} Whether the PDF was written.
|
|
1936
1943
|
*/
|
|
1937
|
-
function compileBook(written, pdf, findings) {
|
|
1938
|
-
const compiled = compileTypst(written.typ, written.pdf, pdf);
|
|
1944
|
+
function compileBook(written, pdf, findings, config) {
|
|
1945
|
+
const compiled = compileTypst(written.typ, written.pdf, pdf, config);
|
|
1939
1946
|
findings.push(...compiled.findings);
|
|
1940
1947
|
if (!compiled.ok) {
|
|
1941
1948
|
findings.push({ file: written.typ, severity: "error", message: compiled.message });
|
|
@@ -1979,10 +1986,13 @@ export const BOOK_FONTS_PATH = path.join(
|
|
|
1979
1986
|
* @param {string} typPath - The `.typ` file.
|
|
1980
1987
|
* @param {string} pdfPath - Where the PDF goes.
|
|
1981
1988
|
* @param {object} [pdf] - The resolved `pdf:` block.
|
|
1989
|
+
* @param {{rootDir?: string, paths?: {content?: string}}} [config] - The
|
|
1990
|
+
* resolved configuration; the installed faces are searched when a note sets
|
|
1991
|
+
* text in one.
|
|
1982
1992
|
* @returns {string[]} The arguments, in order.
|
|
1983
1993
|
*/
|
|
1984
|
-
export function typstArgs(typPath, pdfPath, pdf = {}) {
|
|
1985
|
-
return ["compile", ...typstFontArgs(pdf), typPath, pdfPath];
|
|
1994
|
+
export function typstArgs(typPath, pdfPath, pdf = {}, config) {
|
|
1995
|
+
return ["compile", ...typstFontArgs(pdf, config), typPath, pdfPath];
|
|
1986
1996
|
}
|
|
1987
1997
|
|
|
1988
1998
|
/**
|
|
@@ -1990,14 +2000,20 @@ export function typstArgs(typPath, pdfPath, pdf = {}) {
|
|
|
1990
2000
|
* the page measurement so both lay the book out in the same metrics.
|
|
1991
2001
|
*
|
|
1992
2002
|
* @param {object} [pdf] - The resolved `pdf:` block.
|
|
2003
|
+
* @param {{rootDir?: string, paths?: {content?: string}}} [config] - The
|
|
2004
|
+
* resolved configuration; the installed faces are searched when a note sets
|
|
2005
|
+
* text in one.
|
|
1993
2006
|
* @returns {string[]} The arguments, in order.
|
|
1994
2007
|
*/
|
|
1995
|
-
export function typstFontArgs(pdf = {}) {
|
|
2008
|
+
export function typstFontArgs(pdf = {}, config) {
|
|
1996
2009
|
// One flag, the paths joined by the platform's separator: the compiler
|
|
1997
2010
|
// takes a list, and the shipped faces come first so a consumer's own
|
|
1998
|
-
// directory
|
|
2011
|
+
// directory and the installed script fonts extend the set rather than
|
|
2012
|
+
// standing in for it.
|
|
1999
2013
|
const fontPaths = [BOOK_FONTS_PATH];
|
|
2000
2014
|
if (pdf.fonts?.path) fontPaths.push(pdf.fonts.path);
|
|
2015
|
+
const scriptFonts = config && typstScriptFontPath(config);
|
|
2016
|
+
if (scriptFonts) fontPaths.push(scriptFonts);
|
|
2001
2017
|
return ["--ignore-system-fonts", "--font-path", fontPaths.join(path.delimiter)];
|
|
2002
2018
|
}
|
|
2003
2019
|
|
|
@@ -2044,12 +2060,14 @@ export function typstWarnings(output) {
|
|
|
2044
2060
|
* @param {string} typPath - The `.typ` file.
|
|
2045
2061
|
* @param {string} pdfPath - Where the PDF goes.
|
|
2046
2062
|
* @param {object} pdf - The resolved `pdf:` block.
|
|
2063
|
+
* @param {{rootDir?: string, paths?: {content?: string}}} [config] - The
|
|
2064
|
+
* resolved configuration, for the installed faces.
|
|
2047
2065
|
* @returns {{ok: boolean, message: string, findings: object[]}} What happened,
|
|
2048
2066
|
* and what the compiler warned about on the way.
|
|
2049
2067
|
*/
|
|
2050
|
-
export function compileTypst(typPath, pdfPath, pdf = {}) {
|
|
2068
|
+
export function compileTypst(typPath, pdfPath, pdf = {}, config) {
|
|
2051
2069
|
const binary = pdf.binary || "typst";
|
|
2052
|
-
const args = typstArgs(typPath, pdfPath, pdf);
|
|
2070
|
+
const args = typstArgs(typPath, pdfPath, pdf, config);
|
|
2053
2071
|
let result;
|
|
2054
2072
|
try {
|
|
2055
2073
|
result = spawnSync(binary, args, { encoding: "utf8" });
|
package/engine/pdf-render.mjs
CHANGED
|
@@ -91,6 +91,7 @@ import { indexChapter, indexMarker, indexPreamble } from "./pdf-index.mjs";
|
|
|
91
91
|
import { slugify } from "./content-slug.mjs";
|
|
92
92
|
import { alertMarkdownPlugin, scanAlerts, ALERT_TYPES } from "./content-alerts.mjs";
|
|
93
93
|
import { spanMarkdownPlugin, scanSpans } from "./content-spans.mjs";
|
|
94
|
+
import { GENERIC_FONTS, renderRuns, scanRuns } from "./content-scripts.mjs";
|
|
94
95
|
import { scanFigures } from "./content-figures.mjs";
|
|
95
96
|
import { BLOCK_CONTAINERS, scanBlocks, BLOCK_NAMES } from "./content-blocks.mjs";
|
|
96
97
|
import { inlineVerse, poetryStanzas, scanPoetry } from "./content-poetry.mjs";
|
|
@@ -370,7 +371,11 @@ function reportUnrenderable(source, definitions, opts, original) {
|
|
|
370
371
|
);
|
|
371
372
|
}
|
|
372
373
|
}
|
|
373
|
-
for (const error of [
|
|
374
|
+
for (const error of [
|
|
375
|
+
...scanSpans(source).errors,
|
|
376
|
+
...scanRuns(source, opts.runs).errors,
|
|
377
|
+
...markupAnchorFindings(source),
|
|
378
|
+
])
|
|
374
379
|
report(linePosition(error.line, error.column, opts), "error", error.message);
|
|
375
380
|
for (const error of scanFigures(source).errors)
|
|
376
381
|
report(linePosition(error.line, error.column, opts), "error", error.message);
|
|
@@ -462,6 +467,9 @@ function reportUnrenderable(source, definitions, opts, original) {
|
|
|
462
467
|
* Typst and is told nothing.
|
|
463
468
|
* @param {string} [opts.file] - The file the markdown came from, named in every
|
|
464
469
|
* finding.
|
|
470
|
+
* @param {import("./content-scripts.mjs").RunContext} [opts.runs] - The
|
|
471
|
+
* package's languages, faces and maps, which text marked with `lang=`,
|
|
472
|
+
* `font=` and `map=` is checked against and set by.
|
|
465
473
|
* @param {number} [opts.bodyLine] - The 1-based file line the body starts on, so
|
|
466
474
|
* a finding's line is the file's rather than the body's.
|
|
467
475
|
* @param {number} [opts.bodyColumn] - The 1-based column the body starts at.
|
|
@@ -497,7 +505,7 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
497
505
|
} = opts;
|
|
498
506
|
const given = opts.nested ? String(markdown ?? "") : stripHtmlComments(markdown);
|
|
499
507
|
const separated = opts.footnoteDefinitions === undefined ? separateFootnotes(given) : null;
|
|
500
|
-
const
|
|
508
|
+
const authored = separated?.markdown ?? given;
|
|
501
509
|
const definitions = opts.footnoteDefinitions ?? separated?.definitions ?? "";
|
|
502
510
|
const footnoteState = opts.footnoteState ?? { seen: new Set() };
|
|
503
511
|
const sharedOptions = {
|
|
@@ -508,7 +516,13 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
508
516
|
};
|
|
509
517
|
// Read once, over the body as it arrived: every position a finding carries
|
|
510
518
|
// is an offset into *this* string, and the renderer below walks slices of it.
|
|
511
|
-
if (!opts.nested) reportUnrenderable(
|
|
519
|
+
if (!opts.nested) reportUnrenderable(authored, definitions, opts, given);
|
|
520
|
+
// Text marked with a language, a font or a map is rewritten once, ahead of
|
|
521
|
+
// every other pass, line for line; the nested calls below read the result.
|
|
522
|
+
const source =
|
|
523
|
+
opts.nested ? authored : (
|
|
524
|
+
renderRuns(authored, opts.runs, undefined, { target: "book" }).markdown
|
|
525
|
+
);
|
|
512
526
|
// One map for the whole body, not one per block: a heading inside a
|
|
513
527
|
// blockquote or a list item shares the entry's anchor namespace with every
|
|
514
528
|
// other heading in the same body, because `sectionLabel` scopes by entry
|
|
@@ -584,7 +598,9 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
584
598
|
insideAdmonition: false,
|
|
585
599
|
});
|
|
586
600
|
const anchor = block.id ? ` <${sectionLabel(anchorPrefix, block.id)}>` : "";
|
|
587
|
-
output.push(
|
|
601
|
+
output.push(
|
|
602
|
+
`\n#block(width: 100%)[${scriptWrapped(content, block.classes, block.attributes)}]${anchor}\n`,
|
|
603
|
+
);
|
|
588
604
|
cursor = block.end + 1;
|
|
589
605
|
continue;
|
|
590
606
|
}
|
|
@@ -771,7 +787,9 @@ function renderBlock(tokens, i, out, ctx) {
|
|
|
771
787
|
`${token.markup}${token.info}\n${token.content}${token.markup}`,
|
|
772
788
|
).blocks[0];
|
|
773
789
|
const anchor = poem?.id ? ` <${sectionLabel(ctx.anchorPrefix, poem.id)}>` : "";
|
|
774
|
-
out.push(
|
|
790
|
+
out.push(
|
|
791
|
+
`\n#block(width: 100%)[${scriptWrapped(content, poem?.classes, poem?.attributes)}]${anchor}\n`,
|
|
792
|
+
);
|
|
775
793
|
} else out.push(rawBlock(token.content, token.info?.trim() || ""));
|
|
776
794
|
return 1;
|
|
777
795
|
}
|
|
@@ -1215,6 +1233,43 @@ function rawBlock(content, info) {
|
|
|
1215
1233
|
return `\n${ticks}${lang}\n${text}\n${ticks}\n\n`;
|
|
1216
1234
|
}
|
|
1217
1235
|
|
|
1236
|
+
/**
|
|
1237
|
+
* The language code Typst's `text(lang:)` takes for a BCP 47 tag.
|
|
1238
|
+
*
|
|
1239
|
+
* Typst takes an ISO 639 code and applies the hyphenation of that language, so
|
|
1240
|
+
* a real language passes its primary subtag and an invented one (`art-x-…`),
|
|
1241
|
+
* which has no patterns, leaves the book's language alone.
|
|
1242
|
+
*
|
|
1243
|
+
* @param {string|null|undefined} tag - A BCP 47 tag.
|
|
1244
|
+
* @returns {string|undefined} The code, or `undefined` for none.
|
|
1245
|
+
*/
|
|
1246
|
+
function typstLanguage(tag) {
|
|
1247
|
+
const primary = /^([a-z]{2,3})(?:-|$)/i.exec(tag ?? "")?.[1]?.toLowerCase();
|
|
1248
|
+
return primary && primary !== "art" ? primary : undefined;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/** The classes that set a run in a font: a face's run carries `script`. */
|
|
1252
|
+
const FONT_CLASSES = Object.freeze(["script", ...Object.values(GENERIC_FONTS)]);
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Wrap a block's Typst in its font, its language and its direction.
|
|
1256
|
+
*
|
|
1257
|
+
* @param {string} content - The block's Typst.
|
|
1258
|
+
* @param {string[]|undefined} classes - The classes the block carries.
|
|
1259
|
+
* @param {Record<string, string>|undefined} attributes - Its `lang` and `dir`.
|
|
1260
|
+
* @returns {string} The content, unchanged when the block is marked with none.
|
|
1261
|
+
*/
|
|
1262
|
+
function scriptWrapped(content, classes = [], attributes = {}) {
|
|
1263
|
+
const lang = typstLanguage(attributes.lang);
|
|
1264
|
+
const direction = attributes.dir === "rtl" ? "#set text(dir: rtl)\n" : "";
|
|
1265
|
+
if (!classes.some((name) => FONT_CLASSES.includes(name)) && !lang && !direction) return content;
|
|
1266
|
+
const list = classes.map((name) => `"${escapeTypstString(name)}"`);
|
|
1267
|
+
const arguments_ =
|
|
1268
|
+
`classes: (${list.join(", ")}${list.length === 1 ? "," : ""})` +
|
|
1269
|
+
(lang ? `, lang: "${lang}"` : "");
|
|
1270
|
+
return `${direction}#book-script-text(${arguments_})[${content}]`;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1218
1273
|
/**
|
|
1219
1274
|
* Render an inline token's children.
|
|
1220
1275
|
*
|
|
@@ -1248,9 +1303,15 @@ function renderInline(token, ctx) {
|
|
|
1248
1303
|
const values = classes
|
|
1249
1304
|
.map((value) => '"' + escapeTypstString(value) + '"')
|
|
1250
1305
|
.join(", ");
|
|
1306
|
+
const lang = typstLanguage(child.attrGet?.("lang"));
|
|
1251
1307
|
out.push(
|
|
1252
|
-
classes.length ?
|
|
1253
|
-
"#book-span(classes: (" +
|
|
1308
|
+
classes.length || lang ?
|
|
1309
|
+
"#book-span(classes: (" +
|
|
1310
|
+
values +
|
|
1311
|
+
(classes.length === 1 ? "," : "") +
|
|
1312
|
+
")" +
|
|
1313
|
+
(lang ? `, lang: "${lang}"` : "") +
|
|
1314
|
+
")["
|
|
1254
1315
|
: "#box[",
|
|
1255
1316
|
);
|
|
1256
1317
|
break;
|
|
@@ -1632,6 +1693,30 @@ function renderIcon(token, ctx) {
|
|
|
1632
1693
|
*/
|
|
1633
1694
|
const WIDE_TABLE_COLUMNS = 3;
|
|
1634
1695
|
|
|
1696
|
+
/**
|
|
1697
|
+
* The `book-script-fonts` dictionary: each installed face's class, and each
|
|
1698
|
+
* generic family's, to its font family and OpenType features.
|
|
1699
|
+
*
|
|
1700
|
+
* @param {import("./content-scripts.mjs").RunContext} runs - The installed faces.
|
|
1701
|
+
* @param {{serif?: string, sans?: string, mono?: string}} families - The book's
|
|
1702
|
+
* own serif, sans and mono, which `font=serif`, `sans` and `mono` set.
|
|
1703
|
+
* @returns {string} Typst markup.
|
|
1704
|
+
*/
|
|
1705
|
+
function scriptFontsTypst(runs, families) {
|
|
1706
|
+
const entry = (name, family, features = []) =>
|
|
1707
|
+
`"${escapeTypstString(name)}": (family: "${escapeTypstString(family)}", ` +
|
|
1708
|
+
`features: (${features.map((feature) => `"${escapeTypstString(feature)}"`).join(", ")}` +
|
|
1709
|
+
`${features.length === 1 ? "," : ""}))`;
|
|
1710
|
+
const faces = [];
|
|
1711
|
+
for (const face of runs.faces?.values() ?? [])
|
|
1712
|
+
if (face.class && face.family) faces.push(entry(face.class, face.family, face.features));
|
|
1713
|
+
for (const [generic, name] of Object.entries(GENERIC_FONTS)) {
|
|
1714
|
+
const family = families[/** @type {"serif"|"sans"|"mono"} */ (generic)];
|
|
1715
|
+
if (family) faces.push(entry(name, family));
|
|
1716
|
+
}
|
|
1717
|
+
return `#let book-script-fonts = (${faces.length ? faces.join(", ") : ":"})`;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1635
1720
|
/**
|
|
1636
1721
|
* The Typst definitions the book's page furniture is drawn with.
|
|
1637
1722
|
*
|
|
@@ -1669,20 +1754,37 @@ const WIDE_TABLE_COLUMNS = 3;
|
|
|
1669
1754
|
* carries are not the same set, and a missing glyph on 2,000 feet is a
|
|
1670
1755
|
* tofu box on every page of the book.
|
|
1671
1756
|
*
|
|
1757
|
+
* @param {import("./content-scripts.mjs").RunContext} [runs] - The installed
|
|
1758
|
+
* faces, each of whose classes sets its family.
|
|
1759
|
+
* @param {{serif?: string, sans?: string, mono?: string}} [families] - The
|
|
1760
|
+
* book's own serif, sans and mono, which `font=serif`, `sans` and `mono` set.
|
|
1672
1761
|
* @returns {string} Typst markup.
|
|
1673
1762
|
*/
|
|
1674
|
-
export function bookTypstPreamble() {
|
|
1763
|
+
export function bookTypstPreamble(runs = {}, families = {}) {
|
|
1675
1764
|
return [
|
|
1676
1765
|
"#let book-margin = 1.9cm",
|
|
1677
1766
|
"#let book-page-width = 8.5in",
|
|
1678
1767
|
"#let book-page-height = 11in",
|
|
1679
1768
|
"#let book-text-width = book-page-width - 2 * book-margin",
|
|
1680
1769
|
"#let book-text-height = book-page-height - 2 * book-margin",
|
|
1770
|
+
// Each face's class, and each generic family's, sets its font family
|
|
1771
|
+
// and OpenType features.
|
|
1772
|
+
scriptFontsTypst(runs, families),
|
|
1773
|
+
"#let book-script-text(body, classes: (), lang: none) = {\n" +
|
|
1774
|
+
" let styled = body\n" +
|
|
1775
|
+
" for name in classes {\n" +
|
|
1776
|
+
" if name in book-script-fonts {\n" +
|
|
1777
|
+
" let face = book-script-fonts.at(name)\n" +
|
|
1778
|
+
" styled = text(font: face.family, features: face.features, styled)\n" +
|
|
1779
|
+
" }\n" +
|
|
1780
|
+
" }\n" +
|
|
1781
|
+
" if lang != none { text(lang: lang, styled) } else { styled }\n" +
|
|
1782
|
+
"}",
|
|
1681
1783
|
// Span classes reach the book intact; name styling sets an absolute
|
|
1682
1784
|
// weight so it remains bold inside an ordinary strong phrase.
|
|
1683
|
-
"#let book-span(body, classes: ()) = {\n" +
|
|
1785
|
+
"#let book-span(body, classes: (), lang: none) = {\n" +
|
|
1684
1786
|
' let styled = if classes.contains("name") { text(weight: "bold", body) } else { body }\n' +
|
|
1685
|
-
" box(styled)\n" +
|
|
1787
|
+
" box(book-script-text(styled, classes: classes, lang: lang))\n" +
|
|
1686
1788
|
"}",
|
|
1687
1789
|
// Use the file's natural measure for auto. Every requested width is
|
|
1688
1790
|
// bounded by the available measure and by the height of a page.
|
|
@@ -2001,6 +2103,8 @@ function authorLine({ name, role }) {
|
|
|
2001
2103
|
* @param {object} [opts.index] - As {@link module:engine/pdf-index.indexTerms}:
|
|
2002
2104
|
* `owns` limits the index to the notes this book is home to, and `foreign`
|
|
2003
2105
|
* (present only under `pdf.indexSeeAlso`) lists notes another book prints.
|
|
2106
|
+
* @param {import("./content-scripts.mjs").RunContext} [opts.runs] - The
|
|
2107
|
+
* installed faces, which the preamble maps to font families.
|
|
2004
2108
|
* @returns {string} A complete `.typ` document.
|
|
2005
2109
|
*/
|
|
2006
2110
|
export function renderBook({
|
|
@@ -2019,6 +2123,7 @@ export function renderBook({
|
|
|
2019
2123
|
copyright = "",
|
|
2020
2124
|
license = "",
|
|
2021
2125
|
index = {},
|
|
2126
|
+
runs = {},
|
|
2022
2127
|
} = {}) {
|
|
2023
2128
|
// The two halves of one superfamily, chosen together: matched metrics are
|
|
2024
2129
|
// most of why the sans can carry every heading over a serif body without
|
|
@@ -2030,7 +2135,7 @@ export function renderBook({
|
|
|
2030
2135
|
const mono = fonts.mono || "DejaVu Sans Mono";
|
|
2031
2136
|
const out = [];
|
|
2032
2137
|
|
|
2033
|
-
out.push(bookTypstPreamble());
|
|
2138
|
+
out.push(bookTypstPreamble(runs, { serif, sans, mono }));
|
|
2034
2139
|
out.push(indexPreamble({ seeAlso: index.foreign !== undefined }));
|
|
2035
2140
|
out.push("");
|
|
2036
2141
|
out.push(`#set document(title: "${escapeTypstString(title)}")`);
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
* Spans, divs and poetry fences marked with `lang=`, `font=` or `map=`, projected
|
|
16
|
+
* from own-note index records into the SQL `runs` relation. Every value is a
|
|
17
|
+
* column value and none is a key, so two notes writing one tag in different
|
|
18
|
+
* case are two rows. Documentation records and assets contribute no rows.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { canonicalKey, readCanonicalKey } from "./address.mjs";
|
|
24
|
+
import { encodeAddresses } from "./address-values.mjs";
|
|
25
|
+
import { isNoteRecord } from "./index-records.mjs";
|
|
26
|
+
import { scanRuns } from "./content-scripts.mjs";
|
|
27
|
+
|
|
28
|
+
/** Every column of the `runs` relation, in order. */
|
|
29
|
+
export const RUN_COLUMNS = Object.freeze([
|
|
30
|
+
"owner",
|
|
31
|
+
"kind",
|
|
32
|
+
"lang",
|
|
33
|
+
"tag",
|
|
34
|
+
"font",
|
|
35
|
+
"map",
|
|
36
|
+
"line",
|
|
37
|
+
"column",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One run as an index record carries it.
|
|
42
|
+
*
|
|
43
|
+
* @typedef {object} RecordRun
|
|
44
|
+
* @property {"span"|"block"|"poetry"} kind - The construct.
|
|
45
|
+
* @property {string|null} lang - The language it is set in, as written: its
|
|
46
|
+
* own `lang=`, else the one the div or fence around it states.
|
|
47
|
+
* @property {string|null} tag - The BCP 47 tag it resolves to, or null.
|
|
48
|
+
* @property {string|null} font - Its font, as written and inherited the same way.
|
|
49
|
+
* @property {string|null} map - Its map, as written and inherited the same way.
|
|
50
|
+
* @property {number} line - 1-based file line of its opening.
|
|
51
|
+
* @property {number} column - 1-based UTF-16 file column of its opening.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {RecordRun & {owner: string|null}} RunRow
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A note body's runs, at file positions, as an index record carries them.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} body - The note's body.
|
|
62
|
+
* @param {object} options - Options.
|
|
63
|
+
* @param {import("./content-scripts.mjs").RunContext} [options.context] - The
|
|
64
|
+
* configured languages, which give a declared name its tag.
|
|
65
|
+
* @param {number} [options.bodyLine] - The 1-based file line the body starts on.
|
|
66
|
+
* @param {number} [options.bodyColumn] - The 1-based column of its first character.
|
|
67
|
+
* @param {import("./content-session.mjs").ContentSyntax} [options.syntax] -
|
|
68
|
+
* Session scanner projections.
|
|
69
|
+
* @returns {RecordRun[]} One entry per run, in source order.
|
|
70
|
+
*/
|
|
71
|
+
export function recordRuns(body, { context, bodyLine = 1, bodyColumn = 1, syntax } = {}) {
|
|
72
|
+
return scanRuns(body, context, syntax).runs.map((run) => ({
|
|
73
|
+
kind: run.kind,
|
|
74
|
+
lang: run.lang ?? null,
|
|
75
|
+
tag: run.tag ?? null,
|
|
76
|
+
font: run.font ?? null,
|
|
77
|
+
map: run.map ?? null,
|
|
78
|
+
line: bodyLine + run.line - 1,
|
|
79
|
+
column: run.column + (run.line === 1 ? bodyColumn - 1 : 0),
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The containing page's NOTE identity.
|
|
85
|
+
*
|
|
86
|
+
* @param {Record<string, any>} record - An own-note index record.
|
|
87
|
+
* @returns {string|null} The page Address, or null for an unaddressable record.
|
|
88
|
+
*/
|
|
89
|
+
export function ownerOf(record) {
|
|
90
|
+
const parts = readCanonicalKey(encodeAddresses(record.address?.canonical));
|
|
91
|
+
return parts ? canonicalKey(parts.package, "note", parts.type, parts.shortcode) : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Project recorded runs without reading sources or changing records.
|
|
96
|
+
*
|
|
97
|
+
* @param {Iterable<Record<string, any>>} records - Index records, already
|
|
98
|
+
* narrowed to the audience.
|
|
99
|
+
* @returns {RunRow[]} One row per run, in record then source order.
|
|
100
|
+
*/
|
|
101
|
+
export function runsRows(records) {
|
|
102
|
+
const rows = [];
|
|
103
|
+
for (const record of records) {
|
|
104
|
+
if (!record || !isNoteRecord(record) || !Array.isArray(record.runs)) continue;
|
|
105
|
+
const owner = ownerOf(record);
|
|
106
|
+
for (const run of record.runs)
|
|
107
|
+
rows.push({
|
|
108
|
+
owner,
|
|
109
|
+
kind: run.kind,
|
|
110
|
+
lang: run.lang ?? null,
|
|
111
|
+
tag: run.tag ?? null,
|
|
112
|
+
font: run.font ?? null,
|
|
113
|
+
map: run.map ?? null,
|
|
114
|
+
line: run.line,
|
|
115
|
+
column: run.column,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return rows;
|
|
119
|
+
}
|