@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/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.
@@ -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
@@ -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
- * @throws {PackRoutingError} When the note routes nowhere.
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 {
@@ -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`, `trees` and `landing` emit nothing however they are declared. A
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.rel ?? page.base;
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: `site.trees[].section` is a *configuration* key of
330
- // the same name, and a nested `section:` inside some other block is not
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;