@heroiclands/package-build 24.1.0 → 25.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +2 -0
- package/bin/content-commands.mjs +183 -15
- package/bin/package-build.mjs +5 -170
- package/docs/api.md +23 -11
- package/docs/authoring/links-and-markup.md +8 -0
- package/docs/commands.md +91 -0
- package/docs/guides/writing-in-invented-scripts.md +23 -15
- package/docs/reference/format-details.md +25 -0
- package/docs/reference/note-types.md +1 -0
- package/engine/build-assets.mjs +179 -0
- package/engine/calendar-artifacts.mjs +7 -3
- package/engine/compendiums.mjs +16 -11
- package/engine/content-charset.mjs +12 -4
- package/engine/content-format.yaml +1 -0
- package/engine/content-scripts.mjs +94 -14
- package/engine/content-session.mjs +62 -14
- package/engine/content-timings.mjs +67 -0
- package/engine/index.mjs +3 -0
- package/engine/language-data.mjs +4 -1
- package/engine/language-terms.mjs +10 -0
- package/engine/map-lexicon.mjs +12 -1
- package/engine/roman-characters.mjs +113 -0
- package/engine/script-font-delivery.mjs +22 -15
- package/package.json +1 -1
- package/types/engine/build-assets.d.mts +23 -0
- package/types/engine/calendar-artifacts.d.mts +5 -1
- package/types/engine/content-charset.d.mts +8 -0
- package/types/engine/content-scripts.d.mts +5 -0
- package/types/engine/content-session.d.mts +19 -1
- package/types/engine/content-timings.d.mts +14 -0
- package/types/engine/index.d.mts +1 -0
- package/types/engine/roman-characters.d.mts +45 -0
- package/types/engine/script-font-delivery.d.mts +8 -4
|
@@ -158,6 +158,7 @@ language note says which, as one map and one face:
|
|
|
158
158
|
|
|
159
159
|
```yaml
|
|
160
160
|
data:
|
|
161
|
+
roman: "ABDEFGHIKLMNOPRSTUVWYZabdefghiklmnoprstuvwyzÁÉÍÓÚÝáéíóúýËëÀà0123456789."
|
|
161
162
|
writtenIn: { map: semaron, font: semaron-kuriale }
|
|
162
163
|
```
|
|
163
164
|
|
|
@@ -300,7 +301,6 @@ data:
|
|
|
300
301
|
- { from: "y", to: "", before: vowel } # U+F0245
|
|
301
302
|
- ["y", ""] # U+F0237
|
|
302
303
|
- [".", ""] # U+F0268
|
|
303
|
-
- [",", ""] # U+F0268
|
|
304
304
|
fonts: [{ font: semaron-kuriale }]
|
|
305
305
|
---
|
|
306
306
|
|
|
@@ -339,8 +339,8 @@ them, none included. A mapping that drops a language's vowels writes `[a, ""]`.
|
|
|
339
339
|
`y` is the consonant letter before a vowel and the vowel `i` elsewhere.
|
|
340
340
|
- **Capitals and marks need rows of their own.** A row matches the character as
|
|
341
341
|
written, so `L` and `l` are two rows, and a script with no capitals gives both
|
|
342
|
-
the same code point.
|
|
343
|
-
|
|
342
|
+
the same code point. A period is a character too and needs its own row.
|
|
343
|
+
A language's `data.roman` decides which source characters may appear.
|
|
344
344
|
- **A character no row matches passes through unchanged,** and the face-coverage
|
|
345
345
|
check then reports it. A mapping for a face that draws nothing on roman code
|
|
346
346
|
points lists every letter the spelling uses.
|
|
@@ -381,14 +381,19 @@ A row with a fault is left out of the map and the rest apply.
|
|
|
381
381
|
|
|
382
382
|
## How a mapping treats punctuation
|
|
383
383
|
|
|
384
|
+
The language note's `data.roman` lists its exact permitted Roman source
|
|
385
|
+
characters. Include capitals, accents, digits and punctuation explicitly;
|
|
386
|
+
`package-build lint` checks them before mapping. A map cannot admit a character
|
|
387
|
+
that the language's inventory excludes. There are no automatic punctuation
|
|
388
|
+
substitutions.
|
|
389
|
+
|
|
384
390
|
The toolchain is used with one convention for marks: **mood, questions and
|
|
385
391
|
quotation are carried by words, not marks,** unless a language's note rules
|
|
386
392
|
otherwise. A mapping therefore lists the marks its script has and nothing more.
|
|
387
|
-
In the sample,
|
|
393
|
+
In the sample, only a period writes the script's pause point:
|
|
388
394
|
|
|
389
395
|
```yaml
|
|
390
396
|
- [".", "\U000F0268"] # U+F0268
|
|
391
|
-
- [",", "\U000F0268"] # U+F0268
|
|
392
397
|
```
|
|
393
398
|
|
|
394
399
|
(Formatting rewrites the escapes, as above.) A character with no row passes
|
|
@@ -398,10 +403,11 @@ mapped run is an error that names the mark and the mapping:
|
|
|
398
403
|
> `` `?` (U+003F) is not drawn by the font `semaron-kuriale`, and the map `semaron` has no row for it ``
|
|
399
404
|
|
|
400
405
|
The remedy is to write the question or the quotation in words, as the language
|
|
401
|
-
does, and leave the mark out of the run. A language whose note
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
406
|
+
does, and leave the mark out of the run. A language whose note permits a mark
|
|
407
|
+
includes it in `data.roman`; its script needs a row for that mark as well.
|
|
408
|
+
Vylari permits period alone as punctuation. Its lexical catch is written with
|
|
409
|
+
a grave-accented vowel, as in `kàralzon`, and that vowel needs its own mapping
|
|
410
|
+
row; an apostrophe is not another spelling of it.
|
|
405
411
|
|
|
406
412
|
## The dictionary first
|
|
407
413
|
|
|
@@ -793,7 +799,7 @@ paragraphs: indentation, line breaks and spans survive formatting untouched.
|
|
|
793
799
|
|
|
794
800
|
````markdown
|
|
795
801
|
```verse {lang=vyl font=semaron-kuriale map=semaron .nativefont}
|
|
796
|
-
Lokazor lonak
|
|
802
|
+
Lokazor lonak.
|
|
797
803
|
lokamen zaron.
|
|
798
804
|
```
|
|
799
805
|
````
|
|
@@ -1114,6 +1120,7 @@ name: { full: Vylari Language }
|
|
|
1114
1120
|
type: skill
|
|
1115
1121
|
subType: language
|
|
1116
1122
|
data:
|
|
1123
|
+
roman: "ABDEFGHIKLMNOPRSTUVWYZabdefghiklmnoprstuvwyzÁÉÍÓÚÝáéíóúýËëÀà0123456789."
|
|
1117
1124
|
writtenIn: { map: semaron, font: semaron-kuriale }
|
|
1118
1125
|
core: true
|
|
1119
1126
|
fields:
|
|
@@ -1177,7 +1184,7 @@ The lintel over the door of the oath road is cut with a couplet, and the
|
|
|
1177
1184
|
stonecutter set the couplet in the script of the court, so that every traveller
|
|
1178
1185
|
could read it as the court reads it.
|
|
1179
1186
|
|
|
1180
|
-
[Lokazor lonak thiruthrimar
|
|
1187
|
+
[Lokazor lonak thiruthrimar. lokamen zaron.]{lang=vyl font=semaron-kuriale map=semaron .nativefont}
|
|
1181
1188
|
```
|
|
1182
1189
|
|
|
1183
1190
|
Linting reports the unknown word, at its first character:
|
|
@@ -1198,6 +1205,7 @@ name: { full: Vylari Language }
|
|
|
1198
1205
|
type: skill
|
|
1199
1206
|
subType: language
|
|
1200
1207
|
data:
|
|
1208
|
+
roman: "ABDEFGHIKLMNOPRSTUVWYZabdefghiklmnoprstuvwyzÁÉÍÓÚÝáéíóúýËëÀà0123456789."
|
|
1201
1209
|
writtenIn: { map: semaron, font: semaron-kuriale }
|
|
1202
1210
|
core: true
|
|
1203
1211
|
fields:
|
|
@@ -1251,7 +1259,7 @@ The lint is now clean, and the conversion can be read. `package-build transliter
|
|
|
1251
1259
|
prints the code points the build writes for the couplet:
|
|
1252
1260
|
|
|
1253
1261
|
```console
|
|
1254
|
-
$ package-build transliterate semaron --lang vyl --hex "Lokazor lonak
|
|
1262
|
+
$ package-build transliterate semaron --lang vyl --hex "Lokazor lonak. lokamen zaron."
|
|
1255
1263
|
U+F020A U+F023D U+F0239 U+F0230 U+F0246 U+F023D U+F023F U+0020 U+F023A U+F023D U+F023C U+F0230 U+F0239 U+F0268 U+0020 U+F023A U+F023D U+F0239 U+F0230 U+F023B U+F0233 U+F023C U+0020 U+F0246 U+F0248 U+F023F U+F023D U+F023C U+F0268
|
|
1256
1264
|
```
|
|
1257
1265
|
|
|
@@ -1264,8 +1272,8 @@ The site sets the run as
|
|
|
1264
1272
|
```
|
|
1265
1273
|
|
|
1266
1274
|
where the characters are the code points above: `Lokazor` begins with the
|
|
1267
|
-
capital `L` of the rows, `zaron` is the exception's long vowel, and
|
|
1268
|
-
|
|
1275
|
+
capital `L` of the rows, `zaron` is the exception's long vowel, and each period
|
|
1276
|
+
writes the script's pause point.
|
|
1269
1277
|
|
|
1270
1278
|
A poem in columns and a poem with its metre checked sit on one note. Both parse
|
|
1271
1279
|
with no finding: the first sets native Vylari in column one and English over Roman
|
|
@@ -1341,7 +1349,7 @@ The door is marked with the same words as the lintel. Although Semaron's
|
|
|
1341
1349
|
convention is rubric, this Vylari span uses no Markdown emphasis: spans with a
|
|
1342
1350
|
non-English language cannot contain bold or italic markup.
|
|
1343
1351
|
|
|
1344
|
-
[Lokazor lonak
|
|
1352
|
+
[Lokazor lonak. zaron.]{lang=vyl font=semaron-kuriale map=semaron .nativefont}
|
|
1345
1353
|
```
|
|
1346
1354
|
|
|
1347
1355
|
Finally, the core report lists what the language still lacks:
|
|
@@ -2002,6 +2002,30 @@ in that column are grey; each row resets that choice. `numstep` counts logical
|
|
|
2002
2002
|
rows, and `***` separates stanzas. Language and native script choices remain
|
|
2003
2003
|
on the authored inline runs, independently for every reading.
|
|
2004
2004
|
|
|
2005
|
+
**A language note declares its Roman source inventory in `data.roman`.** It is
|
|
2006
|
+
one literal NFC string containing each permitted character once, including every
|
|
2007
|
+
capital, accent, digit and punctuation mark the language uses. For example,
|
|
2008
|
+
`roman: "kaKàÀ.1"` permits exactly those seven characters. There is no range
|
|
2009
|
+
syntax, automatic case expansion, or inventory inherited from a parent language.
|
|
2010
|
+
An empty string explicitly declares that the language has no written characters.
|
|
2011
|
+
Spaces, tabs and line breaks are structural and are not listed in the inventory.
|
|
2012
|
+
A missing declaration on a configured language note is an error even when no
|
|
2013
|
+
passage uses the language. A directly typed native sign is separate from
|
|
2014
|
+
Roman input only when the run explicitly selects `.nativefont`, the character
|
|
2015
|
+
belongs to a recognized setting-script encoding, and the selected face draws it.
|
|
2016
|
+
A font's ability to draw Roman punctuation does not admit that punctuation.
|
|
2017
|
+
|
|
2018
|
+
`package-build lint` checks the visible source of language-tagged spans, divs,
|
|
2019
|
+
verse and scroll fences against that inventory before any map or native-word
|
|
2020
|
+
replacement can hide a character. Both the configured name and its BCP 47 tag
|
|
2021
|
+
select the same inventory. A nested language selects its own; link destinations,
|
|
2022
|
+
attributes, column numbers, indentation periods and `***` stanza separators are
|
|
2023
|
+
structure. Escaping a character does not admit it. Source spelling must be NFC;
|
|
2024
|
+
errors retain the original file position. Unassociated external tags such as
|
|
2025
|
+
`en` and `zxx` keep their ordinary behavior. Standalone `package-build markdown`
|
|
2026
|
+
checks markup without loading language inventories. Structured frontmatter words
|
|
2027
|
+
are separate from prose runs.
|
|
2028
|
+
|
|
2005
2029
|
**A face draws the code points its `fonts.json` entry lists in `codepoints`,
|
|
2006
2030
|
and whitespace.** After the map, any other character in a run set in a face is
|
|
2007
2031
|
an error, located at the source character it came from; through a map, at the
|
|
@@ -6095,6 +6119,7 @@ SoHL sets `system.quantity` to `1` for each compendium item.
|
|
|
6095
6119
|
| `family` | `Address` | `language` only: the parent language, naming a skill note with `subType: language`; this language reads the parent's words as its own — see [a language's words](#a-languages-words) |
|
|
6096
6120
|
| `abbreviation` | `string` | `language` only: the tag a dictionary prints for it, as `Prov.`; its full name where unstated — see [dictionaries and glossaries](#dictionaries-and-glossaries) |
|
|
6097
6121
|
| `core` | `boolean` | `language` only: `true` where the language must cover the [core vocabulary](#the-core-vocabulary) |
|
|
6122
|
+
| `roman` | literal NFC string, may be empty | Exact permitted Roman source characters, including both cases, accents, digits and punctuation; required on each configured language note. No family inheritance or range syntax. |
|
|
6098
6123
|
| `grammar` | `{ plural?, verbs?, agreement?, participles? }` | Optional noun plurals, one verb pattern, agreement and participles; supplied shapes only are checked |
|
|
6099
6124
|
| `grammar.plural` | `{ ending, becomes }[]` | Literal suffix replacements in authored order; the first match wins |
|
|
6100
6125
|
| `grammar.plural[].ending` | `string`, required, may be empty | Bare suffix to replace; empty matches every noun |
|
|
@@ -468,6 +468,7 @@ These fields are accepted by every note type. A field's value can still be irrel
|
|
|
468
468
|
| `data.fields` | list of `{ name, holds }` | The fields the language's words are grouped under. A language skill note only. |
|
|
469
469
|
| `data.words` | list of `{ form, class, field, gloss, … }` | The words this language states, one map each, in authored order; its automatic dictionary sorts them alphabetically. A language skill note only. See [a language's words](format-details.md#a-languages-words). |
|
|
470
470
|
| `data.affixes` | list of `{ form, gloss, attaches?, examples? }` | The affixes a word's `built` pieces may name, each with its gloss. A language skill note only. |
|
|
471
|
+
| `data.roman` | a literal NFC string of permitted characters, including case, accents, digits and punctuation | The exact Roman source characters this language permits, each once. No ranges, case expansion or inherited inventory; spaces and line breaks are structural. An empty string declares no written characters. Required on each configured language note. |
|
|
471
472
|
| `data.grammar` | a map — `{ plural?, verbs?, agreement?, participles?, native? }` | Optional noun plurals, one verb pattern, agreement, participles and the endings' logographic writings, for dictionary presentation, the mapped-run word check and logographic maps. Only the shape of supplied keys is checked; no word must follow or state a rule. A language skill note only. |
|
|
472
473
|
| `data.tongues` | list of `{ tag, meaning, language? }` | The tags a register row's `tongue` may name, each with what it means. A language skill note only. |
|
|
473
474
|
| `data.register` | list of `{ name, tongue?, kind?, built?, gloss?, why? }` | Every name the setting gives in the language's reach, with its tongue and pieces; the notes carrying each name are derived. A language skill note only. |
|
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
import { loadPackageBuildConfig } from "../config.mjs";
|
|
18
|
+
import { loadPackConfig, packConfigPath } from "./pack-config.mjs";
|
|
19
|
+
import { checkForeignAssetBindings } from "./asset-bindings.mjs";
|
|
20
|
+
import { formatDiagnostic, positionOfYamlPath } from "./diagnostics.mjs";
|
|
21
|
+
import { stageAssets } from "../stage.mjs";
|
|
22
|
+
import { rowsStagingPictureRoots, stagePackageAssets } from "./asset-shipping.mjs";
|
|
23
|
+
import { baseStyleStageEntry } from "./base-styles.mjs";
|
|
24
|
+
import { checkScriptFonts, copyScriptFonts, STAGED_FONTS_DIR } from "./script-font-delivery.mjs";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A finding for each `packageBuild.assets` row that would stage a picture root
|
|
28
|
+
* itself, located at the row's `from` when the configuration is YAML.
|
|
29
|
+
*
|
|
30
|
+
* @param {readonly {from: string, to: string}[]} rows - The configured rows.
|
|
31
|
+
* @param {{rootDir: string, assetsBase: string}} opts - Where they resolve.
|
|
32
|
+
* @returns {object[]} The findings.
|
|
33
|
+
*/
|
|
34
|
+
function pictureRootRowFindings(rows, { rootDir, assetsBase }) {
|
|
35
|
+
const offending = rowsStagingPictureRoots(rows, { rootDir, assetsBase });
|
|
36
|
+
if (!offending.length) return [];
|
|
37
|
+
const file = packConfigPath();
|
|
38
|
+
const text =
|
|
39
|
+
file.endsWith(".yaml") || file.endsWith(".yml") ? fs.readFileSync(file, "utf8") : "";
|
|
40
|
+
return offending.map(({ index, from, root }) => ({
|
|
41
|
+
file,
|
|
42
|
+
...(text ? positionOfYamlPath(text, ["packageBuild", "assets", index, "from"]) : {}),
|
|
43
|
+
severity: "error",
|
|
44
|
+
message:
|
|
45
|
+
`\`packageBuild.assets[${index}]\` copies \`${from}\`, which reaches ${root}/ — ` +
|
|
46
|
+
"the stage copies the icon and image roots itself, flat and filtered by " +
|
|
47
|
+
"each file's provenance, so a row copying them would ship every file " +
|
|
48
|
+
"unfiltered. Remove the row",
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The repository's `packageBuild.assetTransform`, loaded.
|
|
54
|
+
*
|
|
55
|
+
* One loader for every command that writes a picture, so the stage and the CDN
|
|
56
|
+
* hold the same bytes for the same file.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} config - The resolved package-build configuration.
|
|
59
|
+
* @returns {Promise<((sourcePath: string) => string|null|undefined)|undefined>}
|
|
60
|
+
* The transform, or `undefined` when the repository names none.
|
|
61
|
+
*/
|
|
62
|
+
export async function loadAssetTransform(config) {
|
|
63
|
+
if (!config.assetTransform) return undefined;
|
|
64
|
+
const module = await import(pathToFileURL(config.assetTransform).href).catch((err) => {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`cannot load \`packageBuild.assetTransform\` (${config.assetTransform}): ${err.message}`,
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
const transform = module.transform;
|
|
70
|
+
if (typeof transform !== "function") {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`\`packageBuild.assetTransform\` ` +
|
|
73
|
+
`(${config.assetTransform}) exports no \`transform\` ` +
|
|
74
|
+
`function. It must export ` +
|
|
75
|
+
`\`transform(sourcePath) -> string | null\`.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return transform;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Stage static files, provenance-filtered pictures, base styles and fonts.
|
|
83
|
+
* @param {object} [options]
|
|
84
|
+
* @param {ReturnType<typeof loadPackageBuildConfig>} [options.config]
|
|
85
|
+
* @param {import("../content-config.mjs").ContentBuildConfig} [options.packConfig]
|
|
86
|
+
* @param {import("./content-session.mjs").ContentSession} [options.session]
|
|
87
|
+
*/
|
|
88
|
+
export async function stagePackageBuildAssets({
|
|
89
|
+
config = loadPackageBuildConfig(),
|
|
90
|
+
packConfig = loadPackConfig(),
|
|
91
|
+
session,
|
|
92
|
+
} = {}) {
|
|
93
|
+
const assetsBase = packConfig.paths.assets;
|
|
94
|
+
const findings = [
|
|
95
|
+
...pictureRootRowFindings(config.assets, {
|
|
96
|
+
rootDir: config.rootDir,
|
|
97
|
+
assetsBase,
|
|
98
|
+
}),
|
|
99
|
+
...checkForeignAssetBindings(packConfig, { assets: config.assets }),
|
|
100
|
+
];
|
|
101
|
+
if (findings.length) {
|
|
102
|
+
const error = new Error(findings.map(formatDiagnostic).join("\n"));
|
|
103
|
+
error.located = true;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const transform = await loadAssetTransform(config);
|
|
108
|
+
|
|
109
|
+
// `to:` is relative to the staged package root, so a
|
|
110
|
+
// repository's table reads `lang`, not `build/stage/lang`.
|
|
111
|
+
const entries = config.assets.map(({ from, to }) => [from, path.join(config.stageDir, to)]);
|
|
112
|
+
let { entries: count, files } = stageAssets(entries, {
|
|
113
|
+
cwd: config.rootDir,
|
|
114
|
+
transform,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// The picture roots, staged by the toolchain rather than by a row:
|
|
118
|
+
// flat, filtered by each file's provenance, and listed in the
|
|
119
|
+
// package's asset set.
|
|
120
|
+
const pictures = stagePackageAssets({
|
|
121
|
+
assetsBase,
|
|
122
|
+
contentPackage: packConfig.contentPackage,
|
|
123
|
+
stageDir: path.resolve(config.rootDir, config.stageDir),
|
|
124
|
+
// The manifest's title names the set; a package with none is
|
|
125
|
+
// named by its content package.
|
|
126
|
+
title:
|
|
127
|
+
typeof config.manifest?.title === "string" && config.manifest.title.trim() ?
|
|
128
|
+
config.manifest.title
|
|
129
|
+
: packConfig.contentPackage,
|
|
130
|
+
transform,
|
|
131
|
+
shippingLicenses: config.shippingLicenses,
|
|
132
|
+
});
|
|
133
|
+
if (pictures.findings.length) {
|
|
134
|
+
const error = new Error(pictures.findings.map(formatDiagnostic).join("\n"));
|
|
135
|
+
error.located = true;
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
files += pictures.staged.length;
|
|
139
|
+
if (pictures.staged.length || pictures.excluded.length) {
|
|
140
|
+
console.log(
|
|
141
|
+
`Pictures: ${pictures.staged.length} staged, ` +
|
|
142
|
+
`${pictures.excluded.length} left out by provenance.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Staged in its own pass, without the repository's transform: the
|
|
147
|
+
// sheet is this toolchain's file rather than one of the
|
|
148
|
+
// repository's assets, and a `transform` written to retheme a
|
|
149
|
+
// package's own SVGs has no business being handed it.
|
|
150
|
+
if (config.baseStyles) {
|
|
151
|
+
const staged = stageAssets([baseStyleStageEntry(config.stageDir)], {
|
|
152
|
+
cwd: config.rootDir,
|
|
153
|
+
});
|
|
154
|
+
count += staged.entries;
|
|
155
|
+
files += staged.files;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The installed fonts, when the package has installed the fonts
|
|
159
|
+
// package and a note sets text in one of its faces: the
|
|
160
|
+
// stylesheet, the WOFF2 files and the licence, which the
|
|
161
|
+
// manifest's `styles` names.
|
|
162
|
+
const fontFindings = checkScriptFonts(packConfig, session);
|
|
163
|
+
if (fontFindings.length) {
|
|
164
|
+
const error = new Error(fontFindings.map(formatDiagnostic).join("\n"));
|
|
165
|
+
error.located = true;
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
const fonts = copyScriptFonts(
|
|
169
|
+
packConfig,
|
|
170
|
+
path.resolve(config.rootDir, config.stageDir, STAGED_FONTS_DIR),
|
|
171
|
+
["woff2"],
|
|
172
|
+
session,
|
|
173
|
+
);
|
|
174
|
+
if (fonts) {
|
|
175
|
+
count += 1;
|
|
176
|
+
files += fonts;
|
|
177
|
+
}
|
|
178
|
+
console.log(`✅ Static assets staged (${count} entries, ${files} files).`);
|
|
179
|
+
}
|
|
@@ -77,16 +77,20 @@ function readIndex(file) {
|
|
|
77
77
|
*
|
|
78
78
|
* @param {object} opts
|
|
79
79
|
* @param {object} opts.config - Resolved content configuration.
|
|
80
|
+
* @param {Map<string, {raw: string}>} [opts.sources] - Captured authored text for diagnostics.
|
|
80
81
|
* @param {string} opts.calendariaVersion - Version whose import format is targeted.
|
|
81
82
|
* @returns {{directory: string, calendars: number, files: number}}
|
|
82
83
|
*/
|
|
83
|
-
export function emitCalendarArtifacts({ config, calendariaVersion } = {}) {
|
|
84
|
+
export function emitCalendarArtifacts({ config, calendariaVersion, sources } = {}) {
|
|
84
85
|
if (!config) throw new Error("calendar emission requires a content configuration");
|
|
85
86
|
if (!/^\d+\.\d+\.\d+$/.test(calendariaVersion ?? "")) {
|
|
86
87
|
throw new Error("calendar emission requires a Calendaria version such as 1.4.2");
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
const root = config.rootDir;
|
|
91
|
+
const rawSource = (file) =>
|
|
92
|
+
sources?.get(path.resolve(root, file))?.raw ??
|
|
93
|
+
fs.readFileSync(path.resolve(root, file), "utf8");
|
|
90
94
|
const contentBase = path.resolve(root, config.paths.content);
|
|
91
95
|
const indexFile = path.resolve(
|
|
92
96
|
root,
|
|
@@ -108,7 +112,7 @@ export function emitCalendarArtifacts({ config, calendariaVersion } = {}) {
|
|
|
108
112
|
);
|
|
109
113
|
const names = new Map();
|
|
110
114
|
for (const note of calendars) {
|
|
111
|
-
const raw =
|
|
115
|
+
const raw = rawSource(note.file);
|
|
112
116
|
const shortcode = note.fm.shortcode;
|
|
113
117
|
if (typeof shortcode !== "string" || !/^[a-z0-9]+$/.test(shortcode)) {
|
|
114
118
|
failAt(
|
|
@@ -135,7 +139,7 @@ export function emitCalendarArtifacts({ config, calendariaVersion } = {}) {
|
|
|
135
139
|
failAt(
|
|
136
140
|
note.file,
|
|
137
141
|
"calendar notes require a world note declaring data.year",
|
|
138
|
-
|
|
142
|
+
rawSource(note.file),
|
|
139
143
|
["subType"],
|
|
140
144
|
);
|
|
141
145
|
}
|
package/engine/compendiums.mjs
CHANGED
|
@@ -96,6 +96,7 @@ export async function compilePacks({
|
|
|
96
96
|
contentBase: config.paths.content,
|
|
97
97
|
skipDirectories: config.skipDirectories,
|
|
98
98
|
});
|
|
99
|
+
const measured = (name, work) => (session ? session.timings.measure(name, work) : work());
|
|
99
100
|
const packNames = sourcePacks.filter((name) => !packName || name === packName);
|
|
100
101
|
|
|
101
102
|
// A prebuilt pack's JSON is checked in rather than generated, so it is
|
|
@@ -114,12 +115,14 @@ export async function compilePacks({
|
|
|
114
115
|
// content tree, and a package that ships only prebuilt packs has none at
|
|
115
116
|
// all — so running it would fail the build over a tree nothing reads.
|
|
116
117
|
if (needsGeneration) {
|
|
117
|
-
const errors = await
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
const errors = await measured("rendering/documents", () =>
|
|
119
|
+
generatePacksJson({
|
|
120
|
+
only: packName,
|
|
121
|
+
config,
|
|
122
|
+
session,
|
|
123
|
+
reportPreparation,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
123
126
|
if (errors > 0) {
|
|
124
127
|
throw new Error(
|
|
125
128
|
`Pack JSON generation reported ${errors} error(s); refusing to ` +
|
|
@@ -141,11 +144,13 @@ export async function compilePacks({
|
|
|
141
144
|
|
|
142
145
|
const stage = path.join(stageDest, name);
|
|
143
146
|
log.info(`Pack ${name}: compiling to LevelDB at ${stage}`);
|
|
144
|
-
await
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
await measured("pack compilation", () =>
|
|
148
|
+
compilePack(source, stage, {
|
|
149
|
+
recursive: true,
|
|
150
|
+
log: false,
|
|
151
|
+
transformEntry: cleanPackEntry,
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
149
154
|
|
|
150
155
|
// A Scene's map image lives on an embedded Level, stored under its own
|
|
151
156
|
// LevelDB key. Nothing in Foundry ties the two together on read: a
|
|
@@ -176,6 +176,17 @@ const SCRIPT_BLOCKS = Object.freeze([
|
|
|
176
176
|
[0x13430, 0x1345f], // the Egyptian format controls Zethu uses
|
|
177
177
|
]);
|
|
178
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Whether a code point belongs to a declared setting-script encoding block.
|
|
181
|
+
* This identifies native input independently of a language's Roman inventory;
|
|
182
|
+
* a selected native face must still draw the individual character.
|
|
183
|
+
* @param {number} cp - A Unicode code point.
|
|
184
|
+
* @returns {boolean}
|
|
185
|
+
*/
|
|
186
|
+
export function isSettingScriptCodePoint(cp) {
|
|
187
|
+
return SCRIPT_BLOCKS.some(([from, to]) => cp >= from && cp <= to);
|
|
188
|
+
}
|
|
189
|
+
|
|
179
190
|
/**
|
|
180
191
|
* Whether the charset admits a code point anywhere in a note.
|
|
181
192
|
*
|
|
@@ -184,10 +195,7 @@ const SCRIPT_BLOCKS = Object.freeze([
|
|
|
184
195
|
*/
|
|
185
196
|
export function isAllowedCodePoint(cp) {
|
|
186
197
|
return (
|
|
187
|
-
isLetterTier(cp) ||
|
|
188
|
-
TYPOGRAPHY.has(cp) ||
|
|
189
|
-
NOTATION.has(cp) ||
|
|
190
|
-
SCRIPT_BLOCKS.some(([from, to]) => cp >= from && cp <= to)
|
|
198
|
+
isLetterTier(cp) || TYPOGRAPHY.has(cp) || NOTATION.has(cp) || isSettingScriptCodePoint(cp)
|
|
191
199
|
);
|
|
192
200
|
}
|
|
193
201
|
|