@heroiclands/package-build 0.6.1 → 3.0.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-content-build.md +965 -0
- package/CHANGELOG.md +68 -0
- package/CONTENT.md +824 -0
- package/MIGRATING.md +127 -0
- package/README.md +29 -17
- package/bin/content-build.mjs +1082 -0
- package/bin/package-build.mjs +2 -2
- package/bin/report.mjs +2 -2
- package/config.mjs +21 -18
- package/content-config.mjs +1424 -0
- package/coverage.mjs +1 -1
- package/e2e.mjs +1 -1
- package/engine/abbreviations.mjs +0 -0
- package/engine/base-compiler.mjs +608 -0
- package/engine/code-fences.mjs +311 -0
- package/engine/compendiums.mjs +353 -0
- package/engine/content-address.mjs +141 -0
- package/engine/content-links.mjs +438 -0
- package/engine/content-lint.mjs +204 -0
- package/engine/content-package.mjs +67 -0
- package/engine/content-slug.mjs +169 -0
- package/engine/content-tables.mjs +1216 -0
- package/engine/content-tree.mjs +56 -0
- package/engine/diagnostics.mjs +270 -0
- package/engine/field-reference.mjs +242 -0
- package/engine/field-spec.mjs +218 -0
- package/engine/foreign-catalog.mjs +450 -0
- package/engine/foreign-manifests.mjs +129 -0
- package/engine/frontmatter-lint.mjs +343 -0
- package/engine/frontmatter.mjs +258 -0
- package/engine/generate.mjs +305 -0
- package/engine/helpers.mjs +809 -0
- package/engine/ids.mjs +192 -0
- package/engine/index.mjs +142 -0
- package/engine/item-docs.mjs +153 -0
- package/engine/item-registry.mjs +129 -0
- package/engine/journals.mjs +374 -0
- package/engine/kb-manifest.mjs +473 -0
- package/engine/macros.mjs +333 -0
- package/engine/manifest-emit.mjs +339 -0
- package/engine/map-notes.mjs +1302 -0
- package/engine/pack-config.mjs +479 -0
- package/engine/pack-router.mjs +254 -0
- package/engine/prose-config.mjs +238 -0
- package/engine/prose-lint.mjs +294 -0
- package/engine/region-events.mjs +79 -0
- package/engine/scene-levels.mjs +420 -0
- package/engine/scenes.mjs +528 -0
- package/engine/site-build.mjs +778 -0
- package/engine/site-index.mjs +332 -0
- package/engine/web-wikilinks.mjs +330 -0
- package/engine/wikilink-syntax.mjs +105 -0
- package/engine/wikilinks.mjs +645 -0
- package/index.mjs +42 -25
- package/lang.mjs +1 -1
- package/manifest.mjs +79 -5
- package/markdownlint-config.mjs +45 -0
- package/package.json +78 -35
- package/prettier-config.mjs +34 -0
- package/sohl/actors.mjs +581 -0
- package/sohl/affiliation-standings.mjs +44 -0
- package/sohl/being-info.mjs +173 -0
- package/sohl/default-item-art.mjs +82 -0
- package/sohl/index.mjs +55 -0
- package/sohl/item-builders.mjs +114 -0
- package/sohl/item-fields.mjs +867 -0
- package/sohl/items.mjs +197 -0
- package/sohl/kb-passes.mjs +207 -0
- package/sohl/note-schemas.mjs +322 -0
- package/templates.mjs +1 -1
- package/types/config.d.mts +1 -1
- package/types/content-config.d.mts +571 -0
- package/types/engine/abbreviations.d.mts +44 -0
- package/types/engine/base-compiler.d.mts +320 -0
- package/types/engine/code-fences.d.mts +102 -0
- package/types/engine/compendiums.d.mts +77 -0
- package/types/engine/content-address.d.mts +61 -0
- package/types/engine/content-links.d.mts +87 -0
- package/types/engine/content-lint.d.mts +47 -0
- package/types/engine/content-package.d.mts +36 -0
- package/types/engine/content-slug.d.mts +59 -0
- package/types/engine/content-tables.d.mts +131 -0
- package/types/engine/content-tree.d.mts +12 -0
- package/types/engine/diagnostics.d.mts +152 -0
- package/types/engine/field-reference.d.mts +20 -0
- package/types/engine/field-spec.d.mts +181 -0
- package/types/engine/foreign-catalog.d.mts +103 -0
- package/types/engine/foreign-manifests.d.mts +43 -0
- package/types/engine/frontmatter-lint.d.mts +74 -0
- package/types/engine/frontmatter.d.mts +99 -0
- package/types/engine/generate.d.mts +56 -0
- package/types/engine/helpers.d.mts +348 -0
- package/types/engine/ids.d.mts +126 -0
- package/types/engine/index.d.mts +33 -0
- package/types/engine/item-docs.d.mts +73 -0
- package/types/engine/item-registry.d.mts +57 -0
- package/types/engine/journals.d.mts +121 -0
- package/types/engine/kb-manifest.d.mts +214 -0
- package/types/engine/macros.d.mts +201 -0
- package/types/engine/manifest-emit.d.mts +137 -0
- package/types/engine/map-notes.d.mts +283 -0
- package/types/engine/pack-config.d.mts +77 -0
- package/types/engine/pack-router.d.mts +53 -0
- package/types/engine/prose-config.d.mts +121 -0
- package/types/engine/prose-lint.d.mts +59 -0
- package/types/engine/region-events.d.mts +43 -0
- package/types/engine/scene-levels.d.mts +36 -0
- package/types/engine/scenes.d.mts +48 -0
- package/types/engine/site-build.d.mts +214 -0
- package/types/engine/site-index.d.mts +141 -0
- package/types/engine/web-wikilinks.d.mts +67 -0
- package/types/engine/wikilink-syntax.d.mts +101 -0
- package/types/engine/wikilinks.d.mts +172 -0
- package/types/index.d.mts +3 -0
- package/types/manifest.d.mts +52 -4
- package/types/markdownlint-config.d.mts +8 -0
- package/types/prettier-config.d.mts +1 -0
- package/types/sohl/actors.d.mts +65 -0
- package/types/sohl/affiliation-standings.d.mts +22 -0
- package/types/sohl/being-info.d.mts +78 -0
- package/types/sohl/default-item-art.d.mts +28 -0
- package/types/sohl/index.d.mts +7 -0
- package/types/sohl/item-builders.d.mts +11 -0
- package/types/sohl/item-fields.d.mts +20 -0
- package/types/sohl/items.d.mts +19 -0
- package/types/sohl/kb-passes.d.mts +71 -0
- package/types/sohl/note-schemas.d.mts +7 -0
package/coverage.mjs
CHANGED
package/e2e.mjs
CHANGED
|
@@ -406,7 +406,7 @@ export async function seedTestWorld({
|
|
|
406
406
|
throw new Error(
|
|
407
407
|
"Cannot seed a world without knowing which Foundry generation it " +
|
|
408
408
|
"is for. Declare `compatibility.minimum` at the top level of " +
|
|
409
|
-
"
|
|
409
|
+
"package-build.config.yaml.",
|
|
410
410
|
);
|
|
411
411
|
}
|
|
412
412
|
|
|
Binary file
|
|
@@ -0,0 +1,608 @@
|
|
|
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
|
+
* `BasePackCompiler` — the one compile loop every pack pass runs.
|
|
16
|
+
*
|
|
17
|
+
* Walking the content tree, rejecting what this build does not own, skipping
|
|
18
|
+
* drafts, expanding generated tables, converting wikilinks, writing the JSON
|
|
19
|
+
* and counting what failed are the same in every pass. They were written out
|
|
20
|
+
* once per pass — three times when this was filed, five by the time it landed —
|
|
21
|
+
* so a fix to any of them had to be made everywhere, and the passes drifted
|
|
22
|
+
* apart in exactly the places nobody was comparing (#1509).
|
|
23
|
+
*
|
|
24
|
+
* A pass now states only what makes it that pass:
|
|
25
|
+
*
|
|
26
|
+
* | Hook | What it decides |
|
|
27
|
+
* | ---- | --------------- |
|
|
28
|
+
* | {@link BasePackCompiler#selects} | Which notes this pack claims. **Required.** |
|
|
29
|
+
* | {@link BasePackCompiler#buildEntry} | One note → one document. **Required.** |
|
|
30
|
+
* | {@link BasePackCompiler#prepare} | Anything the walk needs first (an index, a prior pack's output). |
|
|
31
|
+
* | {@link BasePackCompiler#skipNote} | A further rejection the type filter cannot express. |
|
|
32
|
+
* | {@link BasePackCompiler#compileNote} | A note that emits *more* than its own document. |
|
|
33
|
+
* | {@link BasePackCompiler#onCompiled} | Per-note tallies for the summary. |
|
|
34
|
+
* | {@link BasePackCompiler#finish} | Work that needs every note first. |
|
|
35
|
+
* | {@link BasePackCompiler#reportCompiled} / {@link BasePackCompiler#reportDetail} | The pass's own log lines. |
|
|
36
|
+
*
|
|
37
|
+
* plus two static switches — `requiresId` (a note with no id is fatal, or
|
|
38
|
+
* merely skipped) and `convertsWikilinks` (whether the body reaching
|
|
39
|
+
* `buildEntry` is converted or exactly as authored).
|
|
40
|
+
*
|
|
41
|
+
* `selects` answers *which document type* a pass claims, and it is the same
|
|
42
|
+
* answer for every pack of that type. Which **pack of that type** a claimed
|
|
43
|
+
* note lands in is a second question, answered by the pack router from the
|
|
44
|
+
* note's own `pack:` declaration (#1566) — so a subclass never has to know that
|
|
45
|
+
* its type ships in more than one pack.
|
|
46
|
+
*
|
|
47
|
+
* **This is the extension point.** The pack list is data
|
|
48
|
+
* (`package-build.config.yaml`) and each entry names a document type; a consumer
|
|
49
|
+
* adding a document type this toolchain does not ship writes a subclass of this
|
|
50
|
+
* and registers it, rather than copying a pass and editing it. The contract the
|
|
51
|
+
* generator relies on stays small: construct, `await compile()`, read
|
|
52
|
+
* `errorCount` and `compiledCount`.
|
|
53
|
+
*
|
|
54
|
+
* **This class knows nothing about any game system.** Which types a pack claims
|
|
55
|
+
* arrives through `selects`, so the type membership stays in the one place that
|
|
56
|
+
* owns it — for the doc-carrying types, the single `docEntryTypes` set in
|
|
57
|
+
* `item-docs.mjs` that the compilers and the link manifest both read.
|
|
58
|
+
*
|
|
59
|
+
* @module
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
import fs from "fs";
|
|
63
|
+
import path from "path";
|
|
64
|
+
import log from "loglevel";
|
|
65
|
+
|
|
66
|
+
import {
|
|
67
|
+
walkMarkdownTree,
|
|
68
|
+
makeFilename,
|
|
69
|
+
resolveName,
|
|
70
|
+
buildContentLinkIndex,
|
|
71
|
+
convertNoteWikilinks,
|
|
72
|
+
collectContentDocs,
|
|
73
|
+
expandNoteTables,
|
|
74
|
+
} from "./helpers.mjs";
|
|
75
|
+
import { emitDiagnostic } from "./diagnostics.mjs";
|
|
76
|
+
import { contentPackage } from "./content-package.mjs";
|
|
77
|
+
import { assertTypeNotRetired, packForType } from "./ids.mjs";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The tallies one pass accumulates while walking the tree.
|
|
81
|
+
*
|
|
82
|
+
* @typedef {object} PassStats
|
|
83
|
+
* @property {number} compiled - Notes that became a document.
|
|
84
|
+
* @property {number} skippedDraft - Notes marked `draft: true`.
|
|
85
|
+
* @property {number} skippedNoId - Notes with no `id`, where that is tolerated.
|
|
86
|
+
* @property {number} skippedOther - Notes this pass does not claim.
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The shared walk → filter → expand → convert → build → write → count loop.
|
|
91
|
+
*
|
|
92
|
+
* Subclass it, implement {@link BasePackCompiler#selects} and
|
|
93
|
+
* {@link BasePackCompiler#buildEntry}, and override the hooks the pass needs.
|
|
94
|
+
*/
|
|
95
|
+
export class BasePackCompiler {
|
|
96
|
+
/**
|
|
97
|
+
* The pack this pass writes. Subclasses state their own.
|
|
98
|
+
*
|
|
99
|
+
* @type {string}
|
|
100
|
+
*/
|
|
101
|
+
static id = "";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The singular noun this pass calls one of its notes, in log messages —
|
|
105
|
+
* "item", "journal", "actor", "macro", "map". Capitalized for the
|
|
106
|
+
* missing-id error.
|
|
107
|
+
*
|
|
108
|
+
* @type {string}
|
|
109
|
+
*/
|
|
110
|
+
static label = "entry";
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether a claimed note with no `id` fails the build.
|
|
114
|
+
*
|
|
115
|
+
* True everywhere but the journals pass: a skipped document silently
|
|
116
|
+
* vanishes from the compendium while its knowledgebase page still builds,
|
|
117
|
+
* so the omission is invisible until someone looks for it.
|
|
118
|
+
*
|
|
119
|
+
* @type {boolean}
|
|
120
|
+
*/
|
|
121
|
+
static requiresId = true;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Whether the body handed to {@link BasePackCompiler#buildEntry} has had
|
|
125
|
+
* its generated tables expanded and its wikilinks converted.
|
|
126
|
+
*
|
|
127
|
+
* False for a pass whose output must be exactly what the author typed —
|
|
128
|
+
* the macros pass, whose `command` is executable source (#1514). A pass
|
|
129
|
+
* that says so also skips building the content-wide link index it would
|
|
130
|
+
* never read.
|
|
131
|
+
*
|
|
132
|
+
* @type {boolean}
|
|
133
|
+
*/
|
|
134
|
+
static convertsWikilinks = true;
|
|
135
|
+
|
|
136
|
+
/** @type {string} */
|
|
137
|
+
contentBase;
|
|
138
|
+
/** @type {string} */
|
|
139
|
+
outputDir;
|
|
140
|
+
/** @type {(path: string|null) => string|null} */
|
|
141
|
+
folderResolver;
|
|
142
|
+
/** @type {number} */
|
|
143
|
+
errorCount = 0;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The pack this pass writes, and the Foundry document type it holds.
|
|
147
|
+
*
|
|
148
|
+
* Supplied by the generator from the configured pack list. Left undefined
|
|
149
|
+
* by a caller constructing a compiler directly (the unit suite), which
|
|
150
|
+
* turns routing off: with one pack there is nothing to route between.
|
|
151
|
+
*
|
|
152
|
+
* @type {string|undefined}
|
|
153
|
+
*/
|
|
154
|
+
packName;
|
|
155
|
+
/** @type {string|undefined} */
|
|
156
|
+
docType;
|
|
157
|
+
/** @type {{resolve: Function}|undefined} */
|
|
158
|
+
router;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Whether this pass reports a note of its document type that routes
|
|
162
|
+
* nowhere.
|
|
163
|
+
*
|
|
164
|
+
* Every pack of a type claims the same notes, so all of them would report
|
|
165
|
+
* the same unroutable note. The **first configured pack of the type** owns
|
|
166
|
+
* the message, and the rest stay quiet — one error, named once, and the
|
|
167
|
+
* build still fails.
|
|
168
|
+
*
|
|
169
|
+
* @type {boolean}
|
|
170
|
+
*/
|
|
171
|
+
routingReporter = false;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Entries this pass wrote to its own pack. Zero from a non-empty content
|
|
175
|
+
* tree is a build failure, not a quiet no-op — see `generate.mjs`.
|
|
176
|
+
*
|
|
177
|
+
* @type {number}
|
|
178
|
+
*/
|
|
179
|
+
compiledCount = 0;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Wikilinks left as literal text because nothing in the tree (or in a
|
|
183
|
+
* vendored manifest) publishes their target.
|
|
184
|
+
*
|
|
185
|
+
* @type {number}
|
|
186
|
+
*/
|
|
187
|
+
unresolvedLinks = 0;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {object} options
|
|
191
|
+
* @param {string} options.contentBase - Root of the content tree.
|
|
192
|
+
* @param {string} options.dest - Where this pass writes its JSON.
|
|
193
|
+
* @param {(path: string|null) => string|null} [options.folderResolver] -
|
|
194
|
+
* Resolves a `sohl.folder` id against this pack's folder hierarchy.
|
|
195
|
+
* @param {string} [options.packName] - The pack this pass writes.
|
|
196
|
+
* @param {string} [options.docType] - The Foundry document type it holds.
|
|
197
|
+
* @param {{resolve: Function}} [options.router] - The pack router. Omit it
|
|
198
|
+
* — as the unit suite does — and every claimed note is compiled here.
|
|
199
|
+
* @param {boolean} [options.routingReporter] - Whether this pass reports a
|
|
200
|
+
* note of its type that routes nowhere.
|
|
201
|
+
*/
|
|
202
|
+
constructor({
|
|
203
|
+
contentBase,
|
|
204
|
+
dest,
|
|
205
|
+
folderResolver = () => null,
|
|
206
|
+
packName,
|
|
207
|
+
docType,
|
|
208
|
+
router,
|
|
209
|
+
routingReporter = false,
|
|
210
|
+
} = {}) {
|
|
211
|
+
if (!contentBase) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`${this.constructor.name} compiler requires \`contentBase\``,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (!fs.existsSync(contentBase)) {
|
|
217
|
+
throw new Error(`Content tree not found at ${contentBase}`);
|
|
218
|
+
}
|
|
219
|
+
Object.defineProperty(this, "contentBase", {
|
|
220
|
+
value: contentBase,
|
|
221
|
+
writable: false,
|
|
222
|
+
});
|
|
223
|
+
Object.defineProperty(this, "outputDir", {
|
|
224
|
+
value: dest,
|
|
225
|
+
writable: false,
|
|
226
|
+
});
|
|
227
|
+
Object.defineProperty(this, "folderResolver", {
|
|
228
|
+
value: folderResolver,
|
|
229
|
+
writable: false,
|
|
230
|
+
});
|
|
231
|
+
this.packName = packName;
|
|
232
|
+
this.docType = docType;
|
|
233
|
+
this.router = router;
|
|
234
|
+
this.routingReporter = routingReporter;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Whether this pass's pack is the one a claimed note belongs in.
|
|
239
|
+
*
|
|
240
|
+
* @param {object} fm - The note's frontmatter.
|
|
241
|
+
* @returns {boolean} True to compile it here.
|
|
242
|
+
* @throws {import("./pack-router.mjs").PackRoutingError} When the note
|
|
243
|
+
* routes to no pack at all — a build failure, never a silent drop.
|
|
244
|
+
*/
|
|
245
|
+
routesHere(fm) {
|
|
246
|
+
if (!this.router || !this.packName || !this.docType) return true;
|
|
247
|
+
return this.router.resolve(fm, this.docType) === this.packName;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Whether this pass claims a note. **Required.**
|
|
252
|
+
*
|
|
253
|
+
* Called only for a note of the configured content package, so a subclass
|
|
254
|
+
* decides on `type` alone.
|
|
255
|
+
*
|
|
256
|
+
* @param {object} fm - The note's frontmatter.
|
|
257
|
+
* @returns {boolean} True to compile it.
|
|
258
|
+
*/
|
|
259
|
+
// eslint-disable-next-line no-unused-vars
|
|
260
|
+
selects(fm) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`${this.constructor.name} must implement selects(fm) — which notes this pack claims`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* A further rejection the type filter cannot express, applied after the
|
|
268
|
+
* id check. The journals pass uses it to skip a doc-carrying note with no
|
|
269
|
+
* prose: there is no documentation to compile, and the document's own pass
|
|
270
|
+
* leaves its pointer empty to match.
|
|
271
|
+
*
|
|
272
|
+
* @param {object} fm - The note's frontmatter.
|
|
273
|
+
* @param {string} body - The note body, as authored.
|
|
274
|
+
* @returns {boolean} True to skip the note.
|
|
275
|
+
*/
|
|
276
|
+
// eslint-disable-next-line no-unused-vars
|
|
277
|
+
skipNote(fm, body) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* What one note is called in this pass's log lines. The items pass names
|
|
283
|
+
* the item's type, which is more use than "item".
|
|
284
|
+
*
|
|
285
|
+
* @param {object} fm - The note's frontmatter.
|
|
286
|
+
* @returns {string} The label.
|
|
287
|
+
*/
|
|
288
|
+
// eslint-disable-next-line no-unused-vars
|
|
289
|
+
noteLabel(fm) {
|
|
290
|
+
return this.constructor.label;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Everything the walk needs before it starts: the content-wide link index
|
|
295
|
+
* and table-search corpus here, plus whatever a subclass adds (a prior
|
|
296
|
+
* pack's output, an index of cross-references).
|
|
297
|
+
*
|
|
298
|
+
* @returns {Promise<void>}
|
|
299
|
+
*/
|
|
300
|
+
async prepare() {
|
|
301
|
+
if (this.constructor.convertsWikilinks) {
|
|
302
|
+
this.linkIndex = buildContentLinkIndex(
|
|
303
|
+
this.contentBase,
|
|
304
|
+
this.router,
|
|
305
|
+
);
|
|
306
|
+
this.contentDocs = collectContentDocs(this.contentBase);
|
|
307
|
+
}
|
|
308
|
+
this.unresolvedLinks = 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The body {@link BasePackCompiler#buildEntry} receives.
|
|
313
|
+
*
|
|
314
|
+
* Generated tables expand before wikilinks are converted, so a cell a
|
|
315
|
+
* table emits is resolved along with the authored links.
|
|
316
|
+
*
|
|
317
|
+
* @param {object} fm - The note's frontmatter.
|
|
318
|
+
* @param {string} body - The note body, as authored.
|
|
319
|
+
* @returns {string} The converted markdown, or `body` itself for a pass
|
|
320
|
+
* that does not convert.
|
|
321
|
+
*/
|
|
322
|
+
convertBody(fm, body) {
|
|
323
|
+
if (!this.constructor.convertsWikilinks) return body;
|
|
324
|
+
const name = resolveName(fm);
|
|
325
|
+
const { absPath, bodyLine, bodyColumn } = this.currentNote ?? {};
|
|
326
|
+
const { markdown: tabulated, lineMap } = expandNoteTables(body, {
|
|
327
|
+
docs: this.contentDocs,
|
|
328
|
+
name,
|
|
329
|
+
pkg: fm.package,
|
|
330
|
+
fm,
|
|
331
|
+
bodyLine,
|
|
332
|
+
});
|
|
333
|
+
const { markdown, unresolved } = convertNoteWikilinks(tabulated, {
|
|
334
|
+
type: fm.type,
|
|
335
|
+
id: fm.id,
|
|
336
|
+
// Where this note is, so a link that resolves nowhere is reported
|
|
337
|
+
// at a position an author can open rather than by note name (#17).
|
|
338
|
+
file: absPath,
|
|
339
|
+
bodyLine,
|
|
340
|
+
bodyColumn,
|
|
341
|
+
lineMap,
|
|
342
|
+
// A `[[#slug]]` self-link addresses the source note, which has no
|
|
343
|
+
// entry in the index — so where its own documents landed has to
|
|
344
|
+
// travel with it (#1566).
|
|
345
|
+
pack: this.router?.resolveOrNull(fm, packForType(fm.type).docType),
|
|
346
|
+
docPack: this.router?.resolveOrNull(fm, "JournalEntry"),
|
|
347
|
+
index: this.linkIndex,
|
|
348
|
+
name,
|
|
349
|
+
});
|
|
350
|
+
this.unresolvedLinks += unresolved.length;
|
|
351
|
+
return markdown;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Reports a warning about the note being compiled.
|
|
356
|
+
*
|
|
357
|
+
* The file comes from the walk, so no caller has to carry it; a `position`
|
|
358
|
+
* is used when the caller could establish one and omitted otherwise —
|
|
359
|
+
* naming the file alone beats naming a line that is not the problem.
|
|
360
|
+
*
|
|
361
|
+
* @param {string} message - What is wrong, in one sentence.
|
|
362
|
+
* @param {{line?: number, column?: number}} [position] - Where, if known.
|
|
363
|
+
* @returns {void}
|
|
364
|
+
*/
|
|
365
|
+
noteWarn(message, position) {
|
|
366
|
+
emitDiagnostic({
|
|
367
|
+
file: this.currentNote?.absPath,
|
|
368
|
+
line: position?.line,
|
|
369
|
+
column: position?.column,
|
|
370
|
+
severity: "warning",
|
|
371
|
+
message,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Reports an error about the note being compiled.
|
|
377
|
+
*
|
|
378
|
+
* @param {string} message - What is wrong, in one sentence.
|
|
379
|
+
* @param {{line?: number, column?: number}} [position] - Where, if known.
|
|
380
|
+
* @returns {void}
|
|
381
|
+
*/
|
|
382
|
+
noteError(message, position) {
|
|
383
|
+
emitDiagnostic({
|
|
384
|
+
file: this.currentNote?.absPath,
|
|
385
|
+
line: position?.line,
|
|
386
|
+
column: position?.column,
|
|
387
|
+
severity: "error",
|
|
388
|
+
message,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* One note → one document. **Required.**
|
|
394
|
+
*
|
|
395
|
+
* @param {object} fm - The note's frontmatter.
|
|
396
|
+
* @param {string} markdown - The body, from
|
|
397
|
+
* {@link BasePackCompiler#convertBody}.
|
|
398
|
+
* @returns {object} The document, keyed for the pack.
|
|
399
|
+
*/
|
|
400
|
+
// eslint-disable-next-line no-unused-vars
|
|
401
|
+
buildEntry(fm, markdown) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
`${this.constructor.name} must implement buildEntry(fm, markdown)`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Write one document into a directory, named for its name and id.
|
|
409
|
+
*
|
|
410
|
+
* @param {string} dir - The destination directory.
|
|
411
|
+
* @param {object} doc - The document.
|
|
412
|
+
*/
|
|
413
|
+
writeTo(dir, doc) {
|
|
414
|
+
fs.writeFileSync(
|
|
415
|
+
path.join(dir, makeFilename(doc.name, doc._id)),
|
|
416
|
+
JSON.stringify(doc, null, 2),
|
|
417
|
+
"utf8",
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Write one document into this pass's own pack.
|
|
423
|
+
*
|
|
424
|
+
* @param {object} doc - The document.
|
|
425
|
+
*/
|
|
426
|
+
writeEntry(doc) {
|
|
427
|
+
this.writeTo(this.outputDir, doc);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Compile one claimed note. The default builds its document and writes it;
|
|
432
|
+
* a pass whose note emits more than that (the scenes pass, which also
|
|
433
|
+
* bundles an Adventure) overrides this.
|
|
434
|
+
*
|
|
435
|
+
* @param {object} fm - The note's frontmatter.
|
|
436
|
+
* @param {string} markdown - The body, from
|
|
437
|
+
* {@link BasePackCompiler#convertBody}.
|
|
438
|
+
* @returns {object} The document written to this pass's own pack.
|
|
439
|
+
*/
|
|
440
|
+
compileNote(fm, markdown) {
|
|
441
|
+
const doc = this.buildEntry(fm, markdown);
|
|
442
|
+
this.writeEntry(doc);
|
|
443
|
+
return doc;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* A note compiled successfully — where a pass keeps its own tallies.
|
|
448
|
+
*
|
|
449
|
+
* @param {object} fm - The note's frontmatter.
|
|
450
|
+
* @param {object} doc - The document just written.
|
|
451
|
+
*/
|
|
452
|
+
// eslint-disable-next-line no-unused-vars
|
|
453
|
+
onCompiled(fm, doc) {}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Work that needs every note compiled first, before the summary is logged.
|
|
457
|
+
*
|
|
458
|
+
* @param {PassStats} stats - The pass's tallies.
|
|
459
|
+
* @returns {Promise<void>}
|
|
460
|
+
*/
|
|
461
|
+
// eslint-disable-next-line no-unused-vars
|
|
462
|
+
async finish(stats) {}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* The pass's headline count.
|
|
466
|
+
*
|
|
467
|
+
* @param {PassStats} stats - The pass's tallies.
|
|
468
|
+
*/
|
|
469
|
+
reportCompiled(stats) {
|
|
470
|
+
const label = this.constructor.label;
|
|
471
|
+
log.info(
|
|
472
|
+
`Compiled ${stats.compiled} ${label}${stats.compiled === 1 ? "" : "s"}`,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* The pass's own trailing detail line, which names what it rejected in the
|
|
478
|
+
* terms that pass uses.
|
|
479
|
+
*
|
|
480
|
+
* @param {PassStats} stats - The pass's tallies.
|
|
481
|
+
*/
|
|
482
|
+
reportDetail(stats) {
|
|
483
|
+
log.debug(
|
|
484
|
+
`Skipped ${stats.skippedOther} file(s) this pack does not claim`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Log what the pass did.
|
|
490
|
+
*
|
|
491
|
+
* @param {PassStats} stats - The pass's tallies.
|
|
492
|
+
*/
|
|
493
|
+
report(stats) {
|
|
494
|
+
this.reportCompiled(stats);
|
|
495
|
+
if (this.unresolvedLinks) {
|
|
496
|
+
log.info(
|
|
497
|
+
`${this.unresolvedLinks} wikilink(s) left as literal text (no target in the content tree)`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
if (stats.skippedNoId) {
|
|
501
|
+
log.info(`Skipped ${stats.skippedNoId} note(s) missing id`);
|
|
502
|
+
}
|
|
503
|
+
if (stats.skippedDraft) {
|
|
504
|
+
log.info(`Skipped ${stats.skippedDraft} draft(s)`);
|
|
505
|
+
}
|
|
506
|
+
this.reportDetail(stats);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Walk the content tree and compile every note this pass claims.
|
|
511
|
+
*
|
|
512
|
+
* @returns {Promise<void>}
|
|
513
|
+
*/
|
|
514
|
+
async compile() {
|
|
515
|
+
/** @type {PassStats} */
|
|
516
|
+
const stats = {
|
|
517
|
+
compiled: 0,
|
|
518
|
+
skippedDraft: 0,
|
|
519
|
+
skippedNoId: 0,
|
|
520
|
+
skippedOther: 0,
|
|
521
|
+
};
|
|
522
|
+
await this.prepare();
|
|
523
|
+
|
|
524
|
+
const label = this.constructor.label;
|
|
525
|
+
const Label = label.charAt(0).toUpperCase() + label.slice(1);
|
|
526
|
+
|
|
527
|
+
for (const {
|
|
528
|
+
frontmatter: fm,
|
|
529
|
+
body,
|
|
530
|
+
absPath,
|
|
531
|
+
bodyLine,
|
|
532
|
+
bodyColumn,
|
|
533
|
+
} of walkMarkdownTree(this.contentBase)) {
|
|
534
|
+
// Which note this pass is on, so anything it calls can report a
|
|
535
|
+
// position without every method having to be handed one (#17).
|
|
536
|
+
this.currentNote = { absPath, bodyLine, bodyColumn };
|
|
537
|
+
if (!fm || fm.package !== contentPackage()) {
|
|
538
|
+
stats.skippedOther++;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
// Checked before `selects`, and therefore for every note this
|
|
542
|
+
// package owns rather than only the ones some pass claims. A
|
|
543
|
+
// retired type is claimed by no pass, so the alternative is not a
|
|
544
|
+
// wrong document — it is no document, skipped as quietly as the
|
|
545
|
+
// thousands of notes that legitimately belong to another pass.
|
|
546
|
+
assertTypeNotRetired(fm.type, absPath);
|
|
547
|
+
if (!this.selects(fm)) {
|
|
548
|
+
stats.skippedOther++;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (fm.draft === true) {
|
|
552
|
+
stats.skippedDraft++;
|
|
553
|
+
log.debug(`Skipping draft: ${absPath}`);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (!fm.id) {
|
|
557
|
+
if (this.constructor.requiresId) {
|
|
558
|
+
throw new Error(`${Label} missing id: ${absPath}`);
|
|
559
|
+
}
|
|
560
|
+
stats.skippedNoId++;
|
|
561
|
+
this.noteWarn(`${label} note has no id, skipping`);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
// Which pack of this type takes it. Applied after the draft and
|
|
565
|
+
// id checks — a draft is not compiled anywhere, so its declaration
|
|
566
|
+
// is nobody's business — and before `skipNote`, so a note this pack
|
|
567
|
+
// does not own never reaches this pass's own rejection rules.
|
|
568
|
+
try {
|
|
569
|
+
if (!this.routesHere(fm)) {
|
|
570
|
+
stats.skippedOther++;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
} catch (err) {
|
|
574
|
+
if (this.routingReporter) {
|
|
575
|
+
this.errorCount++;
|
|
576
|
+
this.noteError(err.message, err.position);
|
|
577
|
+
}
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (this.skipNote(fm, body)) {
|
|
581
|
+
stats.skippedOther++;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
log.debug(
|
|
586
|
+
`Processing ${this.noteLabel(fm)}: ${resolveName(fm)} (${absPath})`,
|
|
587
|
+
);
|
|
588
|
+
try {
|
|
589
|
+
const doc = this.compileNote(fm, this.convertBody(fm, body));
|
|
590
|
+
stats.compiled++;
|
|
591
|
+
this.onCompiled(fm, doc);
|
|
592
|
+
} catch (err) {
|
|
593
|
+
this.errorCount++;
|
|
594
|
+
// `position` is set by whatever failed if it knew where — an
|
|
595
|
+
// unresolved address, a bad table directive — so the report
|
|
596
|
+
// points at the line rather than at the note (#17).
|
|
597
|
+
this.noteError(
|
|
598
|
+
`${this.noteLabel(fm)} failed to compile: ${err.message}`,
|
|
599
|
+
err.position,
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
this.compiledCount = stats.compiled;
|
|
605
|
+
await this.finish(stats);
|
|
606
|
+
this.report(stats);
|
|
607
|
+
}
|
|
608
|
+
}
|