@heroiclands/package-build 0.6.0 → 3.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.
Files changed (127) hide show
  1. package/CHANGELOG-content-build.md +965 -0
  2. package/CHANGELOG.md +95 -0
  3. package/CONTENT.md +824 -0
  4. package/MIGRATING.md +127 -0
  5. package/README.md +29 -17
  6. package/bin/content-build.mjs +1082 -0
  7. package/bin/package-build.mjs +2 -2
  8. package/bin/report.mjs +2 -2
  9. package/config.mjs +21 -18
  10. package/content-config.mjs +1424 -0
  11. package/coverage.mjs +1 -1
  12. package/e2e.mjs +1 -1
  13. package/engine/abbreviations.mjs +0 -0
  14. package/engine/base-compiler.mjs +608 -0
  15. package/engine/code-fences.mjs +311 -0
  16. package/engine/compendiums.mjs +353 -0
  17. package/engine/content-address.mjs +141 -0
  18. package/engine/content-links.mjs +438 -0
  19. package/engine/content-lint.mjs +204 -0
  20. package/engine/content-package.mjs +67 -0
  21. package/engine/content-slug.mjs +169 -0
  22. package/engine/content-tables.mjs +1216 -0
  23. package/engine/content-tree.mjs +56 -0
  24. package/engine/diagnostics.mjs +270 -0
  25. package/engine/field-reference.mjs +242 -0
  26. package/engine/field-spec.mjs +218 -0
  27. package/engine/foreign-catalog.mjs +450 -0
  28. package/engine/foreign-manifests.mjs +129 -0
  29. package/engine/frontmatter-lint.mjs +343 -0
  30. package/engine/frontmatter.mjs +258 -0
  31. package/engine/generate.mjs +305 -0
  32. package/engine/helpers.mjs +809 -0
  33. package/engine/ids.mjs +192 -0
  34. package/engine/index.mjs +142 -0
  35. package/engine/item-docs.mjs +153 -0
  36. package/engine/item-registry.mjs +129 -0
  37. package/engine/journals.mjs +374 -0
  38. package/engine/kb-manifest.mjs +473 -0
  39. package/engine/macros.mjs +333 -0
  40. package/engine/manifest-emit.mjs +339 -0
  41. package/engine/map-notes.mjs +1302 -0
  42. package/engine/pack-config.mjs +479 -0
  43. package/engine/pack-router.mjs +254 -0
  44. package/engine/prose-config.mjs +238 -0
  45. package/engine/prose-lint.mjs +294 -0
  46. package/engine/region-events.mjs +79 -0
  47. package/engine/scene-levels.mjs +420 -0
  48. package/engine/scenes.mjs +528 -0
  49. package/engine/site-build.mjs +778 -0
  50. package/engine/site-index.mjs +332 -0
  51. package/engine/web-wikilinks.mjs +330 -0
  52. package/engine/wikilink-syntax.mjs +105 -0
  53. package/engine/wikilinks.mjs +645 -0
  54. package/index.mjs +42 -25
  55. package/lang.mjs +1 -1
  56. package/manifest.mjs +79 -5
  57. package/markdownlint-config.mjs +45 -0
  58. package/package.json +78 -34
  59. package/prettier-config.mjs +34 -0
  60. package/sohl/actors.mjs +581 -0
  61. package/sohl/affiliation-standings.mjs +44 -0
  62. package/sohl/being-info.mjs +173 -0
  63. package/sohl/default-item-art.mjs +82 -0
  64. package/sohl/index.mjs +55 -0
  65. package/sohl/item-builders.mjs +114 -0
  66. package/sohl/item-fields.mjs +867 -0
  67. package/sohl/items.mjs +197 -0
  68. package/sohl/kb-passes.mjs +207 -0
  69. package/sohl/note-schemas.mjs +322 -0
  70. package/templates.mjs +1 -1
  71. package/types/config.d.mts +1 -1
  72. package/types/content-config.d.mts +571 -0
  73. package/types/engine/abbreviations.d.mts +44 -0
  74. package/types/engine/base-compiler.d.mts +320 -0
  75. package/types/engine/code-fences.d.mts +102 -0
  76. package/types/engine/compendiums.d.mts +77 -0
  77. package/types/engine/content-address.d.mts +61 -0
  78. package/types/engine/content-links.d.mts +87 -0
  79. package/types/engine/content-lint.d.mts +47 -0
  80. package/types/engine/content-package.d.mts +36 -0
  81. package/types/engine/content-slug.d.mts +59 -0
  82. package/types/engine/content-tables.d.mts +131 -0
  83. package/types/engine/content-tree.d.mts +12 -0
  84. package/types/engine/diagnostics.d.mts +152 -0
  85. package/types/engine/field-reference.d.mts +20 -0
  86. package/types/engine/field-spec.d.mts +181 -0
  87. package/types/engine/foreign-catalog.d.mts +103 -0
  88. package/types/engine/foreign-manifests.d.mts +43 -0
  89. package/types/engine/frontmatter-lint.d.mts +74 -0
  90. package/types/engine/frontmatter.d.mts +99 -0
  91. package/types/engine/generate.d.mts +56 -0
  92. package/types/engine/helpers.d.mts +348 -0
  93. package/types/engine/ids.d.mts +126 -0
  94. package/types/engine/index.d.mts +33 -0
  95. package/types/engine/item-docs.d.mts +73 -0
  96. package/types/engine/item-registry.d.mts +57 -0
  97. package/types/engine/journals.d.mts +121 -0
  98. package/types/engine/kb-manifest.d.mts +214 -0
  99. package/types/engine/macros.d.mts +201 -0
  100. package/types/engine/manifest-emit.d.mts +137 -0
  101. package/types/engine/map-notes.d.mts +283 -0
  102. package/types/engine/pack-config.d.mts +77 -0
  103. package/types/engine/pack-router.d.mts +53 -0
  104. package/types/engine/prose-config.d.mts +121 -0
  105. package/types/engine/prose-lint.d.mts +59 -0
  106. package/types/engine/region-events.d.mts +43 -0
  107. package/types/engine/scene-levels.d.mts +36 -0
  108. package/types/engine/scenes.d.mts +48 -0
  109. package/types/engine/site-build.d.mts +214 -0
  110. package/types/engine/site-index.d.mts +141 -0
  111. package/types/engine/web-wikilinks.d.mts +67 -0
  112. package/types/engine/wikilink-syntax.d.mts +101 -0
  113. package/types/engine/wikilinks.d.mts +172 -0
  114. package/types/index.d.mts +3 -0
  115. package/types/manifest.d.mts +52 -4
  116. package/types/markdownlint-config.d.mts +8 -0
  117. package/types/prettier-config.d.mts +1 -0
  118. package/types/sohl/actors.d.mts +65 -0
  119. package/types/sohl/affiliation-standings.d.mts +22 -0
  120. package/types/sohl/being-info.d.mts +78 -0
  121. package/types/sohl/default-item-art.d.mts +28 -0
  122. package/types/sohl/index.d.mts +7 -0
  123. package/types/sohl/item-builders.d.mts +11 -0
  124. package/types/sohl/item-fields.d.mts +20 -0
  125. package/types/sohl/items.d.mts +19 -0
  126. package/types/sohl/kb-passes.d.mts +71 -0
  127. package/types/sohl/note-schemas.d.mts +7 -0
@@ -0,0 +1,320 @@
1
+ /**
2
+ * The tallies one pass accumulates while walking the tree.
3
+ *
4
+ * @typedef {object} PassStats
5
+ * @property {number} compiled - Notes that became a document.
6
+ * @property {number} skippedDraft - Notes marked `draft: true`.
7
+ * @property {number} skippedNoId - Notes with no `id`, where that is tolerated.
8
+ * @property {number} skippedOther - Notes this pass does not claim.
9
+ */
10
+ /**
11
+ * The shared walk → filter → expand → convert → build → write → count loop.
12
+ *
13
+ * Subclass it, implement {@link BasePackCompiler#selects} and
14
+ * {@link BasePackCompiler#buildEntry}, and override the hooks the pass needs.
15
+ */
16
+ export class BasePackCompiler {
17
+ /**
18
+ * The pack this pass writes. Subclasses state their own.
19
+ *
20
+ * @type {string}
21
+ */
22
+ static id: string;
23
+ /**
24
+ * The singular noun this pass calls one of its notes, in log messages —
25
+ * "item", "journal", "actor", "macro", "map". Capitalized for the
26
+ * missing-id error.
27
+ *
28
+ * @type {string}
29
+ */
30
+ static label: string;
31
+ /**
32
+ * Whether a claimed note with no `id` fails the build.
33
+ *
34
+ * True everywhere but the journals pass: a skipped document silently
35
+ * vanishes from the compendium while its knowledgebase page still builds,
36
+ * so the omission is invisible until someone looks for it.
37
+ *
38
+ * @type {boolean}
39
+ */
40
+ static requiresId: boolean;
41
+ /**
42
+ * Whether the body handed to {@link BasePackCompiler#buildEntry} has had
43
+ * its generated tables expanded and its wikilinks converted.
44
+ *
45
+ * False for a pass whose output must be exactly what the author typed —
46
+ * the macros pass, whose `command` is executable source (#1514). A pass
47
+ * that says so also skips building the content-wide link index it would
48
+ * never read.
49
+ *
50
+ * @type {boolean}
51
+ */
52
+ static convertsWikilinks: boolean;
53
+ /**
54
+ * @param {object} options
55
+ * @param {string} options.contentBase - Root of the content tree.
56
+ * @param {string} options.dest - Where this pass writes its JSON.
57
+ * @param {(path: string|null) => string|null} [options.folderResolver] -
58
+ * Resolves a `sohl.folder` id against this pack's folder hierarchy.
59
+ * @param {string} [options.packName] - The pack this pass writes.
60
+ * @param {string} [options.docType] - The Foundry document type it holds.
61
+ * @param {{resolve: Function}} [options.router] - The pack router. Omit it
62
+ * — as the unit suite does — and every claimed note is compiled here.
63
+ * @param {boolean} [options.routingReporter] - Whether this pass reports a
64
+ * note of its type that routes nowhere.
65
+ */
66
+ constructor({ contentBase, dest, folderResolver, packName, docType, router, routingReporter, }?: {
67
+ contentBase: string;
68
+ dest: string;
69
+ folderResolver?: ((path: string | null) => string | null) | undefined;
70
+ packName?: string | undefined;
71
+ docType?: string | undefined;
72
+ router?: {
73
+ resolve: Function;
74
+ } | undefined;
75
+ routingReporter?: boolean | undefined;
76
+ });
77
+ /** @type {string} */
78
+ contentBase: string;
79
+ /** @type {string} */
80
+ outputDir: string;
81
+ /** @type {(path: string|null) => string|null} */
82
+ folderResolver: (path: string | null) => string | null;
83
+ /** @type {number} */
84
+ errorCount: number;
85
+ /**
86
+ * The pack this pass writes, and the Foundry document type it holds.
87
+ *
88
+ * Supplied by the generator from the configured pack list. Left undefined
89
+ * by a caller constructing a compiler directly (the unit suite), which
90
+ * turns routing off: with one pack there is nothing to route between.
91
+ *
92
+ * @type {string|undefined}
93
+ */
94
+ packName: string | undefined;
95
+ /** @type {string|undefined} */
96
+ docType: string | undefined;
97
+ /** @type {{resolve: Function}|undefined} */
98
+ router: {
99
+ resolve: Function;
100
+ } | undefined;
101
+ /**
102
+ * Whether this pass reports a note of its document type that routes
103
+ * nowhere.
104
+ *
105
+ * Every pack of a type claims the same notes, so all of them would report
106
+ * the same unroutable note. The **first configured pack of the type** owns
107
+ * the message, and the rest stay quiet — one error, named once, and the
108
+ * build still fails.
109
+ *
110
+ * @type {boolean}
111
+ */
112
+ routingReporter: boolean;
113
+ /**
114
+ * Entries this pass wrote to its own pack. Zero from a non-empty content
115
+ * tree is a build failure, not a quiet no-op — see `generate.mjs`.
116
+ *
117
+ * @type {number}
118
+ */
119
+ compiledCount: number;
120
+ /**
121
+ * Wikilinks left as literal text because nothing in the tree (or in a
122
+ * vendored manifest) publishes their target.
123
+ *
124
+ * @type {number}
125
+ */
126
+ unresolvedLinks: number;
127
+ /**
128
+ * Whether this pass's pack is the one a claimed note belongs in.
129
+ *
130
+ * @param {object} fm - The note's frontmatter.
131
+ * @returns {boolean} True to compile it here.
132
+ * @throws {import("./pack-router.mjs").PackRoutingError} When the note
133
+ * routes to no pack at all — a build failure, never a silent drop.
134
+ */
135
+ routesHere(fm: object): boolean;
136
+ /**
137
+ * Whether this pass claims a note. **Required.**
138
+ *
139
+ * Called only for a note of the configured content package, so a subclass
140
+ * decides on `type` alone.
141
+ *
142
+ * @param {object} fm - The note's frontmatter.
143
+ * @returns {boolean} True to compile it.
144
+ */
145
+ selects(fm: object): boolean;
146
+ /**
147
+ * A further rejection the type filter cannot express, applied after the
148
+ * id check. The journals pass uses it to skip a doc-carrying note with no
149
+ * prose: there is no documentation to compile, and the document's own pass
150
+ * leaves its pointer empty to match.
151
+ *
152
+ * @param {object} fm - The note's frontmatter.
153
+ * @param {string} body - The note body, as authored.
154
+ * @returns {boolean} True to skip the note.
155
+ */
156
+ skipNote(fm: object, body: string): boolean;
157
+ /**
158
+ * What one note is called in this pass's log lines. The items pass names
159
+ * the item's type, which is more use than "item".
160
+ *
161
+ * @param {object} fm - The note's frontmatter.
162
+ * @returns {string} The label.
163
+ */
164
+ noteLabel(fm: object): string;
165
+ /**
166
+ * Everything the walk needs before it starts: the content-wide link index
167
+ * and table-search corpus here, plus whatever a subclass adds (a prior
168
+ * pack's output, an index of cross-references).
169
+ *
170
+ * @returns {Promise<void>}
171
+ */
172
+ prepare(): Promise<void>;
173
+ linkIndex: {
174
+ byShortcode: Map<any, any>;
175
+ byAlias: Map<any, any>;
176
+ } | undefined;
177
+ contentDocs: {
178
+ fm: object;
179
+ path: string;
180
+ tld: string;
181
+ folder: string;
182
+ absPath: string;
183
+ }[] | undefined;
184
+ /**
185
+ * The body {@link BasePackCompiler#buildEntry} receives.
186
+ *
187
+ * Generated tables expand before wikilinks are converted, so a cell a
188
+ * table emits is resolved along with the authored links.
189
+ *
190
+ * @param {object} fm - The note's frontmatter.
191
+ * @param {string} body - The note body, as authored.
192
+ * @returns {string} The converted markdown, or `body` itself for a pass
193
+ * that does not convert.
194
+ */
195
+ convertBody(fm: object, body: string): string;
196
+ /**
197
+ * Reports a warning about the note being compiled.
198
+ *
199
+ * The file comes from the walk, so no caller has to carry it; a `position`
200
+ * is used when the caller could establish one and omitted otherwise —
201
+ * naming the file alone beats naming a line that is not the problem.
202
+ *
203
+ * @param {string} message - What is wrong, in one sentence.
204
+ * @param {{line?: number, column?: number}} [position] - Where, if known.
205
+ * @returns {void}
206
+ */
207
+ noteWarn(message: string, position?: {
208
+ line?: number;
209
+ column?: number;
210
+ }): void;
211
+ /**
212
+ * Reports an error about the note being compiled.
213
+ *
214
+ * @param {string} message - What is wrong, in one sentence.
215
+ * @param {{line?: number, column?: number}} [position] - Where, if known.
216
+ * @returns {void}
217
+ */
218
+ noteError(message: string, position?: {
219
+ line?: number;
220
+ column?: number;
221
+ }): void;
222
+ /**
223
+ * One note → one document. **Required.**
224
+ *
225
+ * @param {object} fm - The note's frontmatter.
226
+ * @param {string} markdown - The body, from
227
+ * {@link BasePackCompiler#convertBody}.
228
+ * @returns {object} The document, keyed for the pack.
229
+ */
230
+ buildEntry(fm: object, markdown: string): object;
231
+ /**
232
+ * Write one document into a directory, named for its name and id.
233
+ *
234
+ * @param {string} dir - The destination directory.
235
+ * @param {object} doc - The document.
236
+ */
237
+ writeTo(dir: string, doc: object): void;
238
+ /**
239
+ * Write one document into this pass's own pack.
240
+ *
241
+ * @param {object} doc - The document.
242
+ */
243
+ writeEntry(doc: object): void;
244
+ /**
245
+ * Compile one claimed note. The default builds its document and writes it;
246
+ * a pass whose note emits more than that (the scenes pass, which also
247
+ * bundles an Adventure) overrides this.
248
+ *
249
+ * @param {object} fm - The note's frontmatter.
250
+ * @param {string} markdown - The body, from
251
+ * {@link BasePackCompiler#convertBody}.
252
+ * @returns {object} The document written to this pass's own pack.
253
+ */
254
+ compileNote(fm: object, markdown: string): object;
255
+ /**
256
+ * A note compiled successfully — where a pass keeps its own tallies.
257
+ *
258
+ * @param {object} fm - The note's frontmatter.
259
+ * @param {object} doc - The document just written.
260
+ */
261
+ onCompiled(fm: object, doc: object): void;
262
+ /**
263
+ * Work that needs every note compiled first, before the summary is logged.
264
+ *
265
+ * @param {PassStats} stats - The pass's tallies.
266
+ * @returns {Promise<void>}
267
+ */
268
+ finish(stats: PassStats): Promise<void>;
269
+ /**
270
+ * The pass's headline count.
271
+ *
272
+ * @param {PassStats} stats - The pass's tallies.
273
+ */
274
+ reportCompiled(stats: PassStats): void;
275
+ /**
276
+ * The pass's own trailing detail line, which names what it rejected in the
277
+ * terms that pass uses.
278
+ *
279
+ * @param {PassStats} stats - The pass's tallies.
280
+ */
281
+ reportDetail(stats: PassStats): void;
282
+ /**
283
+ * Log what the pass did.
284
+ *
285
+ * @param {PassStats} stats - The pass's tallies.
286
+ */
287
+ report(stats: PassStats): void;
288
+ /**
289
+ * Walk the content tree and compile every note this pass claims.
290
+ *
291
+ * @returns {Promise<void>}
292
+ */
293
+ compile(): Promise<void>;
294
+ currentNote: {
295
+ absPath: string;
296
+ bodyLine: number | undefined;
297
+ bodyColumn: number | undefined;
298
+ } | undefined;
299
+ }
300
+ /**
301
+ * The tallies one pass accumulates while walking the tree.
302
+ */
303
+ export type PassStats = {
304
+ /**
305
+ * - Notes that became a document.
306
+ */
307
+ compiled: number;
308
+ /**
309
+ * - Notes marked `draft: true`.
310
+ */
311
+ skippedDraft: number;
312
+ /**
313
+ * - Notes with no `id`, where that is tolerated.
314
+ */
315
+ skippedNoId: number;
316
+ /**
317
+ * - Notes this pass does not claim.
318
+ */
319
+ skippedOther: number;
320
+ };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Every code region in a markdown body, as character offsets into it.
3
+ *
4
+ * @param {string | null | undefined} markdown - The body (frontmatter already
5
+ * stripped). An absent body has no code in it.
6
+ * @param {object} [options]
7
+ * @param {boolean} [options.spans=true] - Include inline code spans. Set false
8
+ * to consider only block-level code.
9
+ * @returns {Array<{start: number, end: number}>} Non-overlapping regions, in
10
+ * source order. `end` is exclusive.
11
+ */
12
+ export function codeRegions(markdown: string | null | undefined, { spans }?: {
13
+ spans?: boolean | undefined;
14
+ }): Array<{
15
+ start: number;
16
+ end: number;
17
+ }>;
18
+ /**
19
+ * `String.prototype.replace`, skipping anything inside code.
20
+ *
21
+ * A match inside a code region is returned as it was written, so the block
22
+ * stays verbatim; the replacer is not called for it at all, which matters when
23
+ * it records a side effect (an unresolved link, say).
24
+ *
25
+ * @param {string} markdown - The body.
26
+ * @param {RegExp} pattern - A **global** pattern to rewrite.
27
+ * @param {(...args: Array<any>) => string} replacer - As `replace` takes.
28
+ * @param {object} [options] - Passed to {@link codeRegions}.
29
+ * @returns {string} The rewritten body.
30
+ */
31
+ export function replaceOutsideCode(markdown: string, pattern: RegExp, replacer: (...args: Array<any>) => string, options?: object): string;
32
+ /**
33
+ * `String.prototype.matchAll`, skipping anything inside code.
34
+ *
35
+ * @param {string} markdown - The body.
36
+ * @param {RegExp} pattern - A **global** pattern to search for.
37
+ * @param {object} [options] - Passed to {@link codeRegions}.
38
+ * @returns {Array<RegExpMatchArray>} The matches outside code, in source order.
39
+ */
40
+ export function matchAllOutsideCode(markdown: string, pattern: RegExp, options?: object): Array<RegExpMatchArray>;
41
+ /**
42
+ * Run `transform` over a whole Markdown body while leaving code untouched.
43
+ *
44
+ * {@link replaceOutsideCode} is the right tool when the caller has a pattern.
45
+ * This is for the other shape: a transform that rewrites the **whole** body — a
46
+ * link rewriter, a path fixer — and must simply never see code. Each code run is
47
+ * stashed and replaced with a `\u0000<index>\u0000` sentinel; a NUL never occurs
48
+ * in Markdown source, so the sentinel cannot collide with prose and survives the
49
+ * transform unchanged before being restored.
50
+ *
51
+ * **Which runs count as code is {@link codeRegions}' rule, not a second copy of
52
+ * it.** The knowledgebase build carried its own regex once, and it was weaker in
53
+ * two ways that both corrupted the one page whose subject *is* link syntax — so
54
+ * its examples were exactly the input a looser rule mangles (SoHL#1665). A
55
+ * single-backtick span was allowed to cross newlines, so one odd backtick paired
56
+ * with another paragraphs away and every span after it paired wrongly: prose was
57
+ * masked as code while real spans were left exposed. And only three-backtick
58
+ * fences were recognised, so a four-backtick example holding a three-backtick
59
+ * block — the documented "fences of any length" case (#1505) — leaked its
60
+ * contents.
61
+ *
62
+ * @param {string} body - The markdown body.
63
+ * @param {(masked: string) => string} transform - Applied to the masked body.
64
+ * @returns {string} The transformed body, with every code run restored verbatim.
65
+ */
66
+ export function protectCode(body: string, transform: (masked: string) => string): string;
67
+ /**
68
+ * Where **code** lives in a markdown body, so a rewriter can leave it alone.
69
+ *
70
+ * A code block is verbatim: its contents are shown to the reader exactly as
71
+ * written. Every build-time rewriter that pattern-matches a body therefore has
72
+ * to know where code is — otherwise a source listing that happens to contain
73
+ * the rewriter's syntax is silently corrupted. Wikilink conversion met this as
74
+ * `[[0]]` inside a fence being turned into a link (#1505), and it depended on
75
+ * the surrounding literal's shape (`[[1,2],[3,4]]` survived), so the corruption
76
+ * looked arbitrary.
77
+ *
78
+ * Three forms are recognised, which is every form the content tree uses:
79
+ *
80
+ * - **Fenced blocks** — three or more backticks or tildes, closed by the same
81
+ * character at least as long, or by the end of the document. A longer fence
82
+ * contains a shorter one, so a markdown sample can quote a code sample. The
83
+ * opening line, info string and all, is part of the block.
84
+ * - **Indented blocks** — four columns past the enclosing context, following a
85
+ * blank line (an indent cannot interrupt a paragraph).
86
+ * - **Inline spans** — a run of backticks closed by a run of equal length,
87
+ * within one paragraph.
88
+ *
89
+ * The fence syntax is the one `expandContentTables` already reads, and
90
+ * this module owns it now so the two can never disagree.
91
+ *
92
+ * **Known limits.** This is a scanner, not a markdown parser, and it errs
93
+ * towards treating something as code — a false positive leaves an author's text
94
+ * as written, a false negative rewrites it. List nesting is tracked only to the
95
+ * innermost open marker, so an indented block inside a deeply nested list may be
96
+ * read as list prose; a backslash-escaped backtick still counts as a span
97
+ * delimiter; and HTML blocks are not code (they are not verbatim in markdown
98
+ * either). None of these can turn code into a link — the failure this exists to
99
+ * prevent.
100
+ */
101
+ /** A fence line, capturing its indent, its marker, and its info string. */
102
+ export const FENCE_LINE: RegExp;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Generates each pack's per-entry JSON from `assets/content/` into
3
+ * `build/packs-json/<name>/`, then builds the LevelDB output from it. No
4
+ * committed JSON and no vault access. Destination: `<stageDest>/<name>/`.
5
+ *
6
+ * @param {object} opts
7
+ * @param {object} [opts.config] The resolved build configuration, which
8
+ * the two path arguments below default from. Supplying one is how a caller
9
+ * compiles a package other than this repository's (#1508).
10
+ * @param {string[]} [opts.sourcePacks] Every pack compiled from the content
11
+ * tree, in compile order. Defaults to the configured pack directories.
12
+ * @param {string} [opts.stageDest] Directory the LevelDB packs are built
13
+ * into, one subdirectory per pack. Defaults to the configured stage.
14
+ * @param {string} [opts.packName] Restrict the run to a single pack.
15
+ * @throws {Error} If pack JSON generation reported any error. Packs compiled
16
+ * from incomplete or empty JSON ship blank or short compendiums, and the
17
+ * omission is invisible until a player looks for content that is not there
18
+ * (#1502) — so this is fatal, not a warning, and the caller is expected to
19
+ * turn it into a failing exit code.
20
+ * @throws {Error} If a compiled pack ships a Scene that has lost its embedded
21
+ * Level (#1538). Fatal for the same reason: Foundry reads a missing Level
22
+ * record as "no levels" and persists that on the next world launch, so the
23
+ * map image is gone before anyone notices it was ever at risk. See
24
+ * {@link verifyPackSceneLevels}.
25
+ */
26
+ export function compilePacks({ config, sourcePacks, stageDest, packName, }?: {
27
+ config?: object | undefined;
28
+ sourcePacks?: string[] | undefined;
29
+ stageDest?: string | undefined;
30
+ packName?: string | undefined;
31
+ }): Promise<void>;
32
+ /**
33
+ * Cleans and formats source JSON files, removing unnecessary permissions and flags and adding the proper spacing.
34
+ * @param {object} opts
35
+ * @param {object} [opts.config] The resolved build configuration, which
36
+ * `packDest` defaults from.
37
+ * @param {string} [opts.packDest] Directory holding the extracted per-entry
38
+ * JSON, one subdirectory per pack.
39
+ * @param {string} [opts.packName] Name of pack to clean. If none provided, all packs will be cleaned.
40
+ * @param {string} [opts.entryName] Name of a specific entry to clean.
41
+ *
42
+ * - `npm run build:clean` - Clean all source JSON files.
43
+ * - `npm run build:clean -- classes` - Only clean the source files for the specified compendium.
44
+ * - `npm run build:clean -- classes Barbarian` - Only clean a single item from the specified compendium.
45
+ */
46
+ export function cleanPacks({ config, packDest, packName, entryName, }?: {
47
+ config?: object | undefined;
48
+ packDest?: string | undefined;
49
+ packName?: string | undefined;
50
+ entryName?: string | undefined;
51
+ }): Promise<void>;
52
+ /**
53
+ * Extracts compiled LevelDB packs back to per-entry JSON, rebuilding the folder
54
+ * hierarchy as directories.
55
+ *
56
+ * @param {object} opts
57
+ * @param {Array<{name: string}>} opts.packs The packs the shipped Foundry
58
+ * package declares — the manifest's `packs` array.
59
+ * @param {object} [opts.config] The resolved build configuration, which
60
+ * the two directories below default from.
61
+ * @param {string} [opts.stageDest] Directory holding the compiled LevelDB
62
+ * packs, one subdirectory per pack.
63
+ * @param {string} [opts.packDest] Directory the extracted JSON is written
64
+ * to, one subdirectory per pack.
65
+ * @param {string} [opts.packName] Restrict the run to a single pack.
66
+ * @param {string} [opts.entryName] Restrict the run to a single entry.
67
+ */
68
+ export function unpackPacks({ packs, config, stageDest, packDest, packName, entryName, }: {
69
+ packs: Array<{
70
+ name: string;
71
+ }>;
72
+ config?: object | undefined;
73
+ stageDest?: string | undefined;
74
+ packDest?: string | undefined;
75
+ packName?: string | undefined;
76
+ entryName?: string | undefined;
77
+ }): Promise<void>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The URL section a note routes to.
3
+ *
4
+ * A `doc` is narrative content whose only identity is its subtype label, so it
5
+ * routes by `category`; every other type names its own section.
6
+ *
7
+ * @param {object} fm - Parsed frontmatter.
8
+ * @returns {string|undefined} The section, or `undefined` when the note has
9
+ * none — a `doc` with no category has no address and is not published.
10
+ */
11
+ export function sectionOf(fm: object): string | undefined;
12
+ /**
13
+ * A note's address below the knowledgebase mount, e.g. `affliction/aconite/`.
14
+ *
15
+ * A `README.md` **is** its section's landing page rather than a page within it,
16
+ * so it addresses the section itself and has no slug of its own.
17
+ *
18
+ * @param {object} fm - Parsed frontmatter.
19
+ * @param {string} name - The note's display name; the slug derives from it
20
+ * (#1278), never from the shortcode, which is identity rather than
21
+ * presentation.
22
+ * @param {boolean} isReadme - Whether the file is a `README.md`.
23
+ * @returns {string} The section-relative address, with a trailing slash.
24
+ * @throws {Error} When the name yields no usable slug.
25
+ */
26
+ export function contentAddress(fm: object, name: string, isReadme: boolean): string;
27
+ /**
28
+ * A note's address relative to its **package**, e.g. `kb/affliction/aconite/`.
29
+ *
30
+ * This is the form the link manifest records and the site build emits pages at,
31
+ * and it is one function because those two must agree — a manifest asserting an
32
+ * address the site does not publish resolves at build time and 404s for the
33
+ * reader, which is the failure this module exists to prevent.
34
+ *
35
+ * @param {object} fm - Parsed frontmatter.
36
+ * @param {string} name - The note's display name; a page slug derives from it
37
+ * (#1278), never from the shortcode, which is identity rather than
38
+ * presentation.
39
+ * @param {object} [options] - Options.
40
+ * @param {boolean} [options.isReadme] - Whether the file is a `README.md`.
41
+ * @param {{prefix?: string, landing?: string}} [options.scheme] - The
42
+ * repository's address scheme; defaults to {@link DEFAULT_ADDRESS_SCHEME}.
43
+ * @returns {string} The package-relative address, with a trailing slash and no
44
+ * leading one.
45
+ * @throws {Error} When the note has no address — no section, a landing page
46
+ * naming no section, or a name yielding no usable slug. Each is a note that
47
+ * is not published, and inventing an address for one would put a dead entry
48
+ * in the manifest.
49
+ */
50
+ export function packageAddress(fm: object, name: string, { isReadme, scheme }?: {
51
+ isReadme?: boolean | undefined;
52
+ scheme?: {
53
+ prefix?: string;
54
+ landing?: string;
55
+ } | undefined;
56
+ }): string;
57
+ /** The knowledgebase's mount within this package's site (#1470). */
58
+ export const KB_PREFIX: "kb/";
59
+ import { DEFAULT_ADDRESS_SCHEME } from "../content-config.mjs";
60
+ import { LANDING_RULES } from "../content-config.mjs";
61
+ export { DEFAULT_ADDRESS_SCHEME, LANDING_RULES };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Every `{#anchor}` a note declares on a heading.
3
+ *
4
+ * @param {string} body - The note's markdown body.
5
+ * @returns {Set<string>} The declared anchor slugs.
6
+ */
7
+ export function anchorsOf(body: string): Set<string>;
8
+ /**
9
+ * Load a content tree and build the index a link resolves against.
10
+ *
11
+ * The index mirrors what both builds construct, including the two addresses a
12
+ * doc-carrying note answers to: `type/shortcode` for the document, and
13
+ * `doc<type>/shortcode` for the JournalEntry its prose compiles into. Once a
14
+ * manifest publishes `doc<type>` entries that prefix is a *known type*, and the
15
+ * virtual reading that used to answer for it no longer fires — a real type owns
16
+ * its own name — so the note is indexed under both.
17
+ *
18
+ * @param {string} contentBase - Root of the content tree.
19
+ * @param {object} [opts]
20
+ * @param {string} [opts.manifestDir] - Where vendored foreign manifests live.
21
+ * Omitted, no cross-package address resolves.
22
+ * @param {readonly string[]} [opts.skipDirectories] - Passed to the walk.
23
+ * @returns {object} The notes, the index, and the resolvers built over it.
24
+ */
25
+ export function buildLinkIndex(contentBase: string, { manifestDir, skipDirectories }?: {
26
+ manifestDir?: string | undefined;
27
+ skipDirectories?: readonly string[] | undefined;
28
+ }): object;
29
+ /**
30
+ * Every link in a tree that lands nowhere.
31
+ *
32
+ * @param {ReturnType<typeof buildLinkIndex>} index - The built index.
33
+ * @returns {{deadAnchors: object[], deadAddresses: object[],
34
+ * frontmatterLinks: object[], usedManifest: Set<string>}} The findings, and
35
+ * which addresses a foreign manifest answered.
36
+ */
37
+ export function auditLinks(index: ReturnType<typeof buildLinkIndex>): {
38
+ deadAnchors: object[];
39
+ deadAddresses: object[];
40
+ frontmatterLinks: object[];
41
+ usedManifest: Set<string>;
42
+ };
43
+ /**
44
+ * Walk a corpus from its root and report what nothing links to.
45
+ *
46
+ * A documentation set is a **book, not a pile of notes**: it has a page one,
47
+ * and everything in it should follow from that page by reading. A note with no
48
+ * inbound link still compiles into a pack and still publishes — it is simply
49
+ * impossible to arrive at. Nothing else in either build notices, because every
50
+ * other check asks whether a link *lands*, never whether a document is
51
+ * *reached*.
52
+ *
53
+ * **Which documents belong to the corpus is the caller's to say.** A
54
+ * repository's corpora are its own — one publishes rules and a user guide,
55
+ * another a setting gazetteer — so `scope` decides membership and this decides
56
+ * only reachability. Links out of the corpus are followed as real links; they
57
+ * are simply not pages of it.
58
+ *
59
+ * **`stopAt` marks a page walked *to* but not *through*.** An index page links
60
+ * to nearly everything it covers, so traversing one makes the whole check
61
+ * vacuous: a chapter could stop linking one of its own pages and the walk would
62
+ * still reach it by way of the index. Reachability has to hold along the
63
+ * reading path, which is why the exception exists and why it is deliberately
64
+ * narrow.
65
+ *
66
+ * @param {ReturnType<typeof buildLinkIndex>} index - The built index.
67
+ * @param {object} opts
68
+ * @param {string} opts.root - The corpus's entry page, as a tree-relative path.
69
+ * @param {(note: object) => boolean} opts.scope - Whether a note belongs to the
70
+ * corpus.
71
+ * @param {(note: object) => boolean} [opts.stopAt] - Whether a note is walked
72
+ * to but not through.
73
+ * @returns {{root: object, reached: Set<object>, orphans: object[]}} The root,
74
+ * everything reached from it, and the corpus members that were not.
75
+ * @throws {Error} When no note sits at `root` — a corpus with no page one
76
+ * cannot be walked, and silently reporting every page as an orphan would
77
+ * bury the actual mistake.
78
+ */
79
+ export function walkReachability(index: ReturnType<typeof buildLinkIndex>, { root, scope, stopAt }: {
80
+ root: string;
81
+ scope: (note: object) => boolean;
82
+ stopAt?: ((note: object) => boolean) | undefined;
83
+ }): {
84
+ root: object;
85
+ reached: Set<object>;
86
+ orphans: object[];
87
+ };