@heroiclands/package-build 24.0.4 → 24.1.1
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 +46 -0
- package/README.md +8 -0
- package/assets/styles/content-base.css +25 -10
- package/bin/content-commands.mjs +183 -15
- package/bin/package-build.mjs +5 -170
- package/docs/authoring/links-and-markup.md +215 -179
- package/docs/commands.md +88 -2
- package/docs/configuration.md +6 -3
- package/docs/content-format.md +4 -3
- package/docs/diagnostics.md +57 -29
- package/docs/guides/book.md +1 -1
- package/docs/guides/module-project.md +2 -2
- package/docs/guides/site.md +2 -2
- package/docs/guides/writing-in-invented-scripts.md +175 -125
- package/docs/reference/format-details.md +53 -35
- package/docs/reference/note-types.md +10 -10
- package/docs/reference/theme-pages.md +18 -9
- package/engine/build-assets.mjs +179 -0
- package/engine/calendar-artifacts.mjs +7 -3
- package/engine/compendiums.mjs +16 -11
- package/engine/content-render.mjs +23 -2
- package/engine/content-scripts.mjs +348 -140
- package/engine/content-session.mjs +62 -14
- package/engine/content-spans.mjs +13 -1
- package/engine/content-timings.mjs +67 -0
- package/engine/content-verse.mjs +158 -373
- package/engine/note-vocabulary.mjs +5 -5
- package/engine/pdf-render.mjs +63 -44
- package/engine/run-rows.mjs +18 -20
- package/engine/script-font-delivery.mjs +22 -15
- package/engine/verse-metre.mjs +36 -8
- package/engine/written-in.mjs +9 -13
- package/package.json +1 -1
- package/site-theme/static/css/style.css +19 -10
- package/types/engine/build-assets.d.mts +23 -0
- package/types/engine/calendar-artifacts.d.mts +5 -1
- package/types/engine/content-scripts.d.mts +18 -7
- package/types/engine/content-session.d.mts +19 -1
- package/types/engine/content-timings.d.mts +14 -0
- package/types/engine/content-verse.d.mts +37 -163
- package/types/engine/run-rows.d.mts +16 -9
- package/types/engine/script-font-delivery.d.mts +8 -4
- package/types/engine/verse-metre.d.mts +4 -3
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
/** One invocation's authored sources and context-specific prepared views. */
|
|
15
15
|
import path from "node:path";
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import { DEPLOYED_ASSET_TYPES } from "./asset-types.mjs";
|
|
18
|
+
import { createContentTimings } from "./content-timings.mjs";
|
|
16
19
|
import * as contentIndex from "./content-index.mjs";
|
|
17
20
|
import * as blocks from "./content-blocks.mjs";
|
|
18
21
|
import * as alerts from "./content-alerts.mjs";
|
|
@@ -52,6 +55,8 @@ import { lintNoteStates } from "./stub-lint.mjs";
|
|
|
52
55
|
* @property {() => Promise<import("./content-index.mjs").PreparedContentIndex>} publication
|
|
53
56
|
* @property {() => ReturnType<import("./compile-corpus.mjs").buildCompileCorpus>} compileCorpus
|
|
54
57
|
* @property {(targets: readonly ContentTarget[]) => Promise<ContentSession>} prepare
|
|
58
|
+
* @property {ReturnType<typeof createContentTimings>} timings
|
|
59
|
+
* @property {() => void} assertUnchanged
|
|
55
60
|
* @property {() => void} dispose
|
|
56
61
|
*/
|
|
57
62
|
|
|
@@ -71,6 +76,8 @@ export const CONTENT_TARGETS = Object.freeze(["index", "lint", "links", "compile
|
|
|
71
76
|
* @param {string} [options.assetsBase] - Assets root, defaulting to configuration.
|
|
72
77
|
* @param {readonly string[]} [options.skipDirectories] - Resolved content scope.
|
|
73
78
|
* @param {ReturnType<typeof routerFor>} [options.router] - One router shared by this session's compile.
|
|
79
|
+
* @param {boolean} [options.trackChanges] - Snapshot physical input metadata for build phase guards.
|
|
80
|
+
* @param {ReturnType<typeof createContentTimings>} [options.timings] - Invocation stage accounting.
|
|
74
81
|
* @returns {Promise<ContentSession>} Captured sources and lazy prepared views; dispose after use.
|
|
75
82
|
*/
|
|
76
83
|
export async function createContentSession({
|
|
@@ -79,14 +86,36 @@ export async function createContentSession({
|
|
|
79
86
|
assetsBase = config.paths.assets,
|
|
80
87
|
skipDirectories = config.skipDirectories,
|
|
81
88
|
router = routerFor(config),
|
|
89
|
+
timings = createContentTimings(),
|
|
90
|
+
trackChanges = false,
|
|
82
91
|
} = {}) {
|
|
83
92
|
const tree = path.resolve(contentBase);
|
|
84
93
|
const assets = path.resolve(assetsBase);
|
|
85
94
|
const scope = [...skipDirectories];
|
|
86
|
-
const inventory =
|
|
95
|
+
const inventory = timings.sync("source capture", () =>
|
|
96
|
+
captureContentSources(tree, assets, scope),
|
|
97
|
+
);
|
|
98
|
+
const fingerprint = (file) => {
|
|
99
|
+
const stat = fs.statSync(file, { throwIfNoEntry: false });
|
|
100
|
+
return stat ? `${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}` : "absent";
|
|
101
|
+
};
|
|
102
|
+
const fingerprints =
|
|
103
|
+
trackChanges ?
|
|
104
|
+
new Map(
|
|
105
|
+
[
|
|
106
|
+
...new Set([
|
|
107
|
+
tree,
|
|
108
|
+
...DEPLOYED_ASSET_TYPES.map((entry) => path.resolve(assets, entry.root)),
|
|
109
|
+
...inventory.paths.map((entry) => entry.file),
|
|
110
|
+
]),
|
|
111
|
+
].map((file) => [file, fingerprint(file)]),
|
|
112
|
+
)
|
|
113
|
+
: undefined;
|
|
87
114
|
const parsed = new Map();
|
|
88
115
|
const problems = [];
|
|
89
|
-
const foreign =
|
|
116
|
+
const foreign = timings.sync("dependency indexes", () =>
|
|
117
|
+
loadForeignIndexes(config, [config.contentPackage]),
|
|
118
|
+
);
|
|
90
119
|
const syntaxCache = new Map();
|
|
91
120
|
const prepared = new Map();
|
|
92
121
|
let disposed = false;
|
|
@@ -104,23 +133,31 @@ export async function createContentSession({
|
|
|
104
133
|
});
|
|
105
134
|
return syntaxCache.get(text);
|
|
106
135
|
};
|
|
107
|
-
const records =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
136
|
+
const records = timings.sync("index derivation", () =>
|
|
137
|
+
contentIndex.indexRecordsFor({
|
|
138
|
+
config,
|
|
139
|
+
contentBase: tree,
|
|
140
|
+
assetsBase: assets,
|
|
141
|
+
skipDirectories: scope,
|
|
142
|
+
sources: inventory.indexSources,
|
|
143
|
+
parsed,
|
|
144
|
+
problems,
|
|
145
|
+
foreignIndex: foreign.index,
|
|
146
|
+
syntax,
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
118
149
|
const memo = (key, make) => {
|
|
119
150
|
active();
|
|
120
|
-
if (!prepared.has(key))
|
|
151
|
+
if (!prepared.has(key))
|
|
152
|
+
prepared.set(
|
|
153
|
+
key,
|
|
154
|
+
Promise.resolve().then(() => timings.measure(key, make)),
|
|
155
|
+
);
|
|
156
|
+
else timings.hit(key);
|
|
121
157
|
return prepared.get(key);
|
|
122
158
|
};
|
|
123
159
|
const session = {
|
|
160
|
+
timings,
|
|
124
161
|
config,
|
|
125
162
|
contentBase: tree,
|
|
126
163
|
assetsBase: assets,
|
|
@@ -133,6 +170,17 @@ export async function createContentSession({
|
|
|
133
170
|
problems,
|
|
134
171
|
foreign,
|
|
135
172
|
syntax,
|
|
173
|
+
/** Refuse a stale capture after any build phase changes an input. */
|
|
174
|
+
assertUnchanged() {
|
|
175
|
+
active();
|
|
176
|
+
if (!fingerprints)
|
|
177
|
+
throw new Error("Content change tracking was not enabled for this session");
|
|
178
|
+
for (const [file, before] of fingerprints)
|
|
179
|
+
if (fingerprint(file) !== before)
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Content sources changed during the build: ${file}. Finish source generation before content build.`,
|
|
182
|
+
);
|
|
183
|
+
},
|
|
136
184
|
/** Refuse use with a different configuration, root, scope or router. */
|
|
137
185
|
assertContext(options = {}) {
|
|
138
186
|
active();
|
package/engine/content-spans.mjs
CHANGED
|
@@ -53,7 +53,8 @@ export function spanMarkdownPlugin(md, { spoilers = false, source = false } = {}
|
|
|
53
53
|
/@[A-Za-z]+$/.test(state.src.slice(0, start)) ||
|
|
54
54
|
state.src[start + 1] === "^" ||
|
|
55
55
|
state.src[start + 1] === "!" ||
|
|
56
|
-
["!", "
|
|
56
|
+
["!", "]"].includes(state.src[start - 1]) ||
|
|
57
|
+
(state.src[start - 1] === "[" && state.linkLevel === 0)
|
|
57
58
|
)
|
|
58
59
|
return false;
|
|
59
60
|
const end = state.md.helpers.parseLinkLabel(state, start, false);
|
|
@@ -91,6 +92,17 @@ export function spanMarkdownPlugin(md, { spoilers = false, source = false } = {}
|
|
|
91
92
|
finish = close + 1;
|
|
92
93
|
}
|
|
93
94
|
}
|
|
95
|
+
// A generated book link may put its language/script span directly
|
|
96
|
+
// inside the label. Preserve the literal class-only bracket spelling
|
|
97
|
+
// there for ordinary spans, including names used by the first-use audit.
|
|
98
|
+
if (
|
|
99
|
+
state.src[start - 1] === "[" &&
|
|
100
|
+
!["lang", "font", "map"].some((key) => Object.hasOwn(parsed.values, key)) &&
|
|
101
|
+
!parsed.classes.some(
|
|
102
|
+
(name) => name === "script" || /^font-(?:mono|serif|sans)$/.test(name),
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
return false;
|
|
94
106
|
const problems = [...parsed.problems, ...refusedAttributes(parsed.values)];
|
|
95
107
|
if (parsed.classes.includes("glyph")) problems.push(...glyphProblems(parsed));
|
|
96
108
|
else if (parsed.classes.includes("cartouche")) problems.push(...cartoucheProblems(parsed));
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
/** Sequential, nested stage accounting for one content invocation. */
|
|
15
|
+
export function createContentTimings() {
|
|
16
|
+
const started = performance.now();
|
|
17
|
+
let changed = started;
|
|
18
|
+
const stack = ["orchestration"];
|
|
19
|
+
const stages = new Map();
|
|
20
|
+
const hits = new Map();
|
|
21
|
+
const account = () => {
|
|
22
|
+
const now = performance.now();
|
|
23
|
+
const name = stack.at(-1);
|
|
24
|
+
stages.set(name, (stages.get(name) ?? 0) + now - changed);
|
|
25
|
+
changed = now;
|
|
26
|
+
};
|
|
27
|
+
const begin = (name) => {
|
|
28
|
+
account();
|
|
29
|
+
stack.push(name);
|
|
30
|
+
return () => {
|
|
31
|
+
account();
|
|
32
|
+
stack.pop();
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
/** @template T @param {string} name @param {() => T} work @returns {T} */
|
|
37
|
+
sync(name, work) {
|
|
38
|
+
const stop = begin(name);
|
|
39
|
+
try {
|
|
40
|
+
return work();
|
|
41
|
+
} finally {
|
|
42
|
+
stop();
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
/** @template T @param {string} name @param {() => Promise<T>|T} work @returns {Promise<T>} */
|
|
46
|
+
async measure(name, work) {
|
|
47
|
+
const stop = begin(name);
|
|
48
|
+
try {
|
|
49
|
+
return await work();
|
|
50
|
+
} finally {
|
|
51
|
+
stop();
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
/** @param {string} name */
|
|
55
|
+
hit(name) {
|
|
56
|
+
hits.set(name, (hits.get(name) ?? 0) + 1);
|
|
57
|
+
},
|
|
58
|
+
snapshot() {
|
|
59
|
+
account();
|
|
60
|
+
return {
|
|
61
|
+
totalMs: changed - started,
|
|
62
|
+
stages: Object.fromEntries(stages),
|
|
63
|
+
hits: Object.fromEntries(hits),
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|