@heroiclands/package-build 22.2.0 → 22.3.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 +8 -0
- package/CONTENT.md +27 -32
- package/bin/content-build.mjs +0 -3
- package/content-config.mjs +42 -40
- package/docs/api.md +25 -25
- package/docs/configuration.md +19 -29
- package/docs/content-format.md +36 -6
- package/engine/foundry-entries.mjs +12 -1
- package/engine/frontmatter-lint.mjs +2 -3
- package/engine/helpers.mjs +4 -1
- package/engine/ids.mjs +12 -0
- package/engine/note-claims.mjs +21 -0
- package/engine/pack-router.mjs +82 -5
- package/engine/pdf-build.mjs +2 -2
- package/engine/retired-fields.mjs +2 -3
- package/engine/site-build.mjs +59 -218
- package/engine/site-index.mjs +18 -62
- package/engine/web-wikilinks.mjs +2 -16
- package/engine/wikilinks.mjs +35 -14
- package/package.json +1 -1
- package/sohl/kb-passes.mjs +14 -85
- package/types/content-config.d.mts +6 -6
- package/types/engine/ids.d.mts +11 -0
- package/types/engine/pack-router.d.mts +20 -2
- package/types/engine/site-build.d.mts +12 -62
- package/types/engine/site-index.d.mts +3 -28
- package/types/engine/web-wikilinks.d.mts +1 -1
- package/types/engine/wikilinks.d.mts +6 -3
- package/types/sohl/kb-passes.d.mts +5 -36
package/engine/ids.mjs
CHANGED
|
@@ -86,6 +86,18 @@ export const MAP_SUBTYPES = Object.freeze(["battlemap", "localmap", "regionalmap
|
|
|
86
86
|
*/
|
|
87
87
|
export const JOURNAL_TYPES = Object.freeze(new Set(["doc", "place", "lore", "scenario"]));
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The `pack:` value that routes a note's document into no compendium.
|
|
91
|
+
*
|
|
92
|
+
* One reading, on the key that already decides which compendium receives a
|
|
93
|
+
* note: the note publishes to the site and compiles into nothing. Declared
|
|
94
|
+
* here, beside the pack table, so the configuration can refuse a pack of this
|
|
95
|
+
* name and the router can read the value from one spelling.
|
|
96
|
+
*
|
|
97
|
+
* @type {string}
|
|
98
|
+
*/
|
|
99
|
+
export const NO_PACK = "none";
|
|
100
|
+
|
|
89
101
|
/**
|
|
90
102
|
* Content type → the pack its documents compile into, and the document type
|
|
91
103
|
* that pack holds.
|
package/engine/note-claims.mjs
CHANGED
|
@@ -91,6 +91,7 @@ import { JOURNAL_TYPES, MAP_TYPES, PACK_BY_TYPE, RETIRED_TYPES, currentType } fr
|
|
|
91
91
|
import { itemTypes } from "./item-registry.mjs";
|
|
92
92
|
import { docEntryTypes } from "./item-docs.mjs";
|
|
93
93
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
94
|
+
import { declaresNoPack } from "./pack-router.mjs";
|
|
94
95
|
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
95
96
|
import { noteTypesFor, subtypeRow } from "./document-subtypes.mjs";
|
|
96
97
|
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
@@ -641,6 +642,12 @@ export function unclaimedNoteFindings(config = loadPackConfig(), sources, { reco
|
|
|
641
642
|
// {@link claimedNoteTypes} states: its JSON is checked in, it has no pass,
|
|
642
643
|
// and no note is routed into it.
|
|
643
644
|
const configured = new Set((config.packs ?? []).filter((p) => !p.prebuilt).map((p) => p.type));
|
|
645
|
+
// Every system a pack declares, plus the shared position: a note that
|
|
646
|
+
// answers `pack: none` for all of them compiles into nothing anywhere.
|
|
647
|
+
const routed = [
|
|
648
|
+
undefined,
|
|
649
|
+
...new Set((config.packs ?? []).map((p) => p.system).filter(Boolean)),
|
|
650
|
+
];
|
|
644
651
|
|
|
645
652
|
// The corpus this compile derived once, required rather than
|
|
646
653
|
// derived here: this module is imported *by* the content index, so it
|
|
@@ -681,6 +688,20 @@ export function unclaimedNoteFindings(config = loadPackConfig(), sources, { reco
|
|
|
681
688
|
// Every document it produces has somewhere to go.
|
|
682
689
|
if (produces.length && !missing.length) continue;
|
|
683
690
|
|
|
691
|
+
// `pack: none`, on a type whose only document is the JournalEntry its
|
|
692
|
+
// prose becomes: the note compiles into nothing **by declaration**, so
|
|
693
|
+
// a missing JournalEntry pack drops nothing it meant to keep. A type
|
|
694
|
+
// that also produces an Item or an Actor is not excused here — the
|
|
695
|
+
// router refuses that declaration by name when a pass asks it, and
|
|
696
|
+
// where no pass does, the finding below still says nothing claims the
|
|
697
|
+
// note.
|
|
698
|
+
if (
|
|
699
|
+
produces.every((docType) => docType === "JournalEntry") &&
|
|
700
|
+
routed.every((system) => declaresNoPack(fm, system))
|
|
701
|
+
) {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
|
|
684
705
|
// Some do and some do not: the note compiles, and one of its documents
|
|
685
706
|
// is dropped in silence. A type nothing produces at all falls past this
|
|
686
707
|
// to the unclaimed messages below, where `produces` being empty is itself
|
package/engine/pack-router.mjs
CHANGED
|
@@ -65,11 +65,22 @@
|
|
|
65
65
|
* a shared value that cannot describe a system-specific pack, so it does not
|
|
66
66
|
* answer and that system falls through to its own default. Both were silent
|
|
67
67
|
* losses of one system's whole document set.
|
|
68
|
+
* - **`pack: none` routes the document nowhere, on purpose.** The note is
|
|
69
|
+
* walked, indexed, published and linkable, and no compendium receives it:
|
|
70
|
+
* {@link createPackRouter}'s `resolve` answers `undefined`, which no pass's
|
|
71
|
+
* name equals, so every pass passes over the note as one it does not own.
|
|
72
|
+
* It is accepted only on a type whose sole document is the JournalEntry its
|
|
73
|
+
* prose becomes — `doc` and the other journal-only types. On a type that
|
|
74
|
+
* compiles an Item, an Actor, a Macro, a Scene or an Adventure it is refused
|
|
75
|
+
* by name, because there the declaration would drop the document the type
|
|
76
|
+
* exists to produce. Read per system like any other `pack:`, so a block may
|
|
77
|
+
* say `none` for one system and the shared declaration may name a pack for
|
|
78
|
+
* the rest.
|
|
68
79
|
*
|
|
69
80
|
* @module
|
|
70
81
|
*/
|
|
71
82
|
|
|
72
|
-
import { packForType } from "./ids.mjs";
|
|
83
|
+
import { NO_PACK, packForType } from "./ids.mjs";
|
|
73
84
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
74
85
|
import { blockProperty, systemBlock } from "./system-block.mjs";
|
|
75
86
|
|
|
@@ -95,6 +106,35 @@ export class PackRoutingError extends Error {
|
|
|
95
106
|
*/
|
|
96
107
|
export const PACK_FIELD = "pack";
|
|
97
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The `pack:` value that routes a note's document into no compendium — see
|
|
111
|
+
* {@link module:engine/ids.NO_PACK}. Re-exported here because this is the
|
|
112
|
+
* module that reads it; a configured pack may not take the name, so the value
|
|
113
|
+
* can never be mistaken for one.
|
|
114
|
+
*/
|
|
115
|
+
export { NO_PACK };
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Whether a note declares `pack: none` for one system's document.
|
|
119
|
+
*
|
|
120
|
+
* The same reading {@link createPackRouter}'s `resolve` applies: the system's
|
|
121
|
+
* block wins where it declares a pack, and the shared top-level value stands
|
|
122
|
+
* otherwise. Asked with no system, it reads the shared value alone, which is
|
|
123
|
+
* what every single-system build and the content index read.
|
|
124
|
+
*
|
|
125
|
+
* Pure, so the readers that never route — the Foundry-address pass, the link
|
|
126
|
+
* index, the unclaimed-type check — can ask it without a router.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} fm - The note's frontmatter.
|
|
129
|
+
* @param {string} [system] - The system whose document is asked about.
|
|
130
|
+
* @returns {boolean} True when the answer for that document is `none`.
|
|
131
|
+
*/
|
|
132
|
+
export function declaresNoPack(fm, system) {
|
|
133
|
+
const declared =
|
|
134
|
+
system === undefined ? fm?.[PACK_FIELD] : blockProperty(fm, system, PACK_FIELD);
|
|
135
|
+
return declared === NO_PACK;
|
|
136
|
+
}
|
|
137
|
+
|
|
98
138
|
/**
|
|
99
139
|
* Build the router for one configured pack list.
|
|
100
140
|
*
|
|
@@ -103,7 +143,7 @@ export const PACK_FIELD = "pack";
|
|
|
103
143
|
*
|
|
104
144
|
* @param {readonly object[]} packs - The resolved `packs` list from
|
|
105
145
|
* `defineConfig`.
|
|
106
|
-
* @returns {{resolve: (fm: object, docType: string, system?: string) => string,
|
|
146
|
+
* @returns {{resolve: (fm: object, docType: string, system?: string) => string|undefined,
|
|
107
147
|
* resolveOrNull: (fm: object, docType: string, system?: string) => string|undefined,
|
|
108
148
|
* packsOfType: (docType: string) => string[],
|
|
109
149
|
* defaultOf: (docType: string) => string|undefined}} The router.
|
|
@@ -179,6 +219,14 @@ export function createPackRouter(packs) {
|
|
|
179
219
|
/** @param {object} fm */
|
|
180
220
|
const noteLabel = (fm) => fm?.name?.full ?? fm?.shortcode ?? fm?.id ?? "a note";
|
|
181
221
|
|
|
222
|
+
/**
|
|
223
|
+
* `"a"` or `"an"`, so a document class reads as English in a message.
|
|
224
|
+
*
|
|
225
|
+
* @param {string} word - The word the article precedes.
|
|
226
|
+
* @returns {string} The article.
|
|
227
|
+
*/
|
|
228
|
+
const article = (word) => (/^[AEIOUaeiou]/.test(word) ? "an" : "a");
|
|
229
|
+
|
|
182
230
|
/**
|
|
183
231
|
* The pack a note declaring none is routed to.
|
|
184
232
|
*
|
|
@@ -212,8 +260,11 @@ export function createPackRouter(packs) {
|
|
|
212
260
|
* @param {string} [system] - The system whose document is being routed. Its
|
|
213
261
|
* block's `pack:` wins over the shared one; without it only the shared
|
|
214
262
|
* declaration is read, which is every single-system build.
|
|
215
|
-
* @returns {string} The pack name
|
|
216
|
-
*
|
|
263
|
+
* @returns {string|undefined} The pack name — or `undefined` where the
|
|
264
|
+
* note declares `pack: none`, which no pass's name equals, so every pass
|
|
265
|
+
* passes over the note as one it does not own.
|
|
266
|
+
* @throws {PackRoutingError} When the note routes nowhere, or declares
|
|
267
|
+
* `pack: none` on a type that compiles a document beside its prose.
|
|
217
268
|
*/
|
|
218
269
|
function resolve(fm, docType, system) {
|
|
219
270
|
const inBlock = system === undefined ? undefined : systemBlock(fm, system)?.[PACK_FIELD];
|
|
@@ -225,6 +276,29 @@ export function createPackRouter(packs) {
|
|
|
225
276
|
// JournalEntry — is not what the author was addressing.
|
|
226
277
|
const ownDocType = packForType(fm?.type).docType;
|
|
227
278
|
|
|
279
|
+
// `none`: the note compiles into no document. Only a type whose own
|
|
280
|
+
// document *is* the JournalEntry may say so — for every other type
|
|
281
|
+
// the declaration would drop the Item, Actor, Macro, Scene or
|
|
282
|
+
// Adventure the type exists to produce, and its prose with it. The
|
|
283
|
+
// document class comes from the same table every pass routes by, so
|
|
284
|
+
// the refusal names what the compile would have written. Asked only
|
|
285
|
+
// by the pass that writes the note's own document, as every `pack:`
|
|
286
|
+
// is, so the refusal is reported once rather than once per pass.
|
|
287
|
+
if (declared === NO_PACK && docType === ownDocType) {
|
|
288
|
+
if (ownDocType !== "JournalEntry") {
|
|
289
|
+
const spelled = authoredInBlock ? `${system}.pack: ${NO_PACK}` : `pack: ${NO_PACK}`;
|
|
290
|
+
throw new PackRoutingError(
|
|
291
|
+
`${noteLabel(fm)} declares \`${spelled}\`, but a ${fm?.type} note ` +
|
|
292
|
+
`compiles into ${article(ownDocType)} ${ownDocType}, and ` +
|
|
293
|
+
`\`pack: ${NO_PACK}\` would drop it. Only a type whose sole ` +
|
|
294
|
+
`document is the JournalEntry its prose becomes may ` +
|
|
295
|
+
`declare it — name the pack the ${ownDocType} goes to, or ` +
|
|
296
|
+
`leave \`pack:\` unset for the default.`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
228
302
|
// **A declaration naming another system's pack is not this system's
|
|
229
303
|
// answer**, and which of two things that means depends on where
|
|
230
304
|
// it was written.
|
|
@@ -328,7 +402,10 @@ export function createPackRouter(packs) {
|
|
|
328
402
|
* @param {object} fm - The note's frontmatter.
|
|
329
403
|
* @param {string} docType - The document type being addressed.
|
|
330
404
|
* @param {string} [system] - The system whose document is addressed.
|
|
331
|
-
* @returns {string|undefined} The pack name, or `undefined
|
|
405
|
+
* @returns {string|undefined} The pack name, or `undefined` — for a
|
|
406
|
+
* note that routes nowhere and for one declaring `pack: none`
|
|
407
|
+
* alike. A caller that must tell the two apart asks
|
|
408
|
+
* {@link declaresNoPack}.
|
|
332
409
|
*/
|
|
333
410
|
resolveOrNull(fm, docType, system) {
|
|
334
411
|
try {
|
package/engine/pdf-build.mjs
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* ## `publish.site` is the switch, and it is the only switch
|
|
36
36
|
*
|
|
37
37
|
* `homepage` mode fences the content surfaces off: the tree is not walked and
|
|
38
|
-
* `sections
|
|
38
|
+
* `sections` and `landing` emit nothing however they are declared. A
|
|
39
39
|
* PDF of the content tree is a content surface by any reading — arguably the
|
|
40
40
|
* most portable one there is — so it is fenced on exactly the same terms, by
|
|
41
41
|
* asking the same {@link module:content-config.publishesContentPages} the
|
|
@@ -481,7 +481,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
481
481
|
* @returns {string} Typst markup.
|
|
482
482
|
*/
|
|
483
483
|
const renderPage = (page, headingOffset, anchorPrefix) => {
|
|
484
|
-
const src = page.relPath ?? page.
|
|
484
|
+
const src = page.relPath ?? page.base;
|
|
485
485
|
const wikiErrors = [];
|
|
486
486
|
const { markdown, errors } = expandContentTables(page.body, {
|
|
487
487
|
docs: universe.get(page.pkg) ?? [],
|
|
@@ -326,9 +326,8 @@ export function assertNoSectionField(fm, { file, absPath } = {}) {
|
|
|
326
326
|
if (!fm || typeof fm !== "object" || !Object.hasOwn(fm, "section")) return;
|
|
327
327
|
|
|
328
328
|
const err = new Error(`${sectionRetiredMessage(file)}.`);
|
|
329
|
-
// Anchored at column 1: `
|
|
330
|
-
//
|
|
331
|
-
// this field — a finding about the top-level one must not open on it.
|
|
329
|
+
// Anchored at column 1: a nested `section:` inside some other block is
|
|
330
|
+
// not this field — a finding about the top-level one must not open on it.
|
|
332
331
|
const position = locateFrontmatterKey(absPath, "section", undefined, { topLevel: true });
|
|
333
332
|
if (position) err.position = position;
|
|
334
333
|
throw err;
|