@heroiclands/package-build 22.21.0 → 22.22.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 (110) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/assets/styles/content-base.css +162 -0
  3. package/bin/content-commands.mjs +26 -3
  4. package/bin/package-build.mjs +15 -2
  5. package/ci/content-format-reference.mjs +68 -1
  6. package/config.mjs +26 -0
  7. package/content-config.mjs +73 -4
  8. package/docs/api.md +36 -18
  9. package/docs/authoring/dates-and-calendars.md +4 -2
  10. package/docs/authoring/frontmatter.md +2 -2
  11. package/docs/authoring/links-and-markup.md +237 -28
  12. package/docs/configuration.md +27 -1
  13. package/docs/content-format.md +3 -1
  14. package/docs/guides/reference-pages.md +15 -0
  15. package/docs/reference/format-details.md +224 -46
  16. package/docs/reference/note-types.md +21 -4
  17. package/engine/actor-compiler.mjs +23 -0
  18. package/engine/anchored-sections.mjs +35 -4
  19. package/engine/anchors.mjs +10 -20
  20. package/engine/base-compiler.mjs +86 -21
  21. package/engine/base-styles.mjs +149 -0
  22. package/engine/bundles.mjs +0 -1
  23. package/engine/compendiums.mjs +8 -1
  24. package/engine/content-blocks.mjs +410 -0
  25. package/engine/content-captions.mjs +57 -8
  26. package/engine/content-footnotes.mjs +189 -0
  27. package/engine/content-format-check.mjs +15 -12
  28. package/engine/content-index.mjs +2 -2
  29. package/engine/content-links.mjs +14 -6
  30. package/engine/content-tables.mjs +45 -4
  31. package/engine/content-tasklists.mjs +189 -0
  32. package/engine/docs-checks.mjs +4 -4
  33. package/engine/extension-attributes.mjs +48 -2
  34. package/engine/frontmatter-lint.mjs +47 -64
  35. package/engine/frontmatter.mjs +3 -3
  36. package/engine/heading-attributes.mjs +308 -0
  37. package/engine/helpers.mjs +14 -13
  38. package/engine/homepage.mjs +8 -8
  39. package/engine/index.mjs +3 -0
  40. package/engine/infobox-render.mjs +15 -3
  41. package/engine/infobox.mjs +83 -8
  42. package/engine/journals.mjs +36 -17
  43. package/engine/macros.mjs +1 -1
  44. package/engine/markdown-expressions.mjs +178 -53
  45. package/engine/note-dates.mjs +49 -0
  46. package/engine/note-frontmatter.mjs +7 -16
  47. package/engine/note-vocabulary.mjs +172 -37
  48. package/engine/page-lists.mjs +358 -0
  49. package/engine/pdf-build.mjs +76 -42
  50. package/engine/pdf-render.mjs +422 -49
  51. package/engine/rank-ladder.mjs +198 -0
  52. package/engine/reckoning-markers.mjs +1 -1
  53. package/engine/retired-fields.mjs +8 -13
  54. package/engine/scenes.mjs +10 -3
  55. package/engine/site-build.mjs +41 -12
  56. package/engine/site-config.mjs +10 -1
  57. package/engine/site-index.mjs +18 -2
  58. package/engine/site-preview.mjs +18 -7
  59. package/engine/social-ties.mjs +52 -14
  60. package/engine/sql-tables.mjs +66 -9
  61. package/engine/standings.mjs +89 -6
  62. package/engine/system-block.mjs +12 -14
  63. package/engine/web-wikilinks.mjs +48 -1
  64. package/hm3/actors.mjs +6 -3
  65. package/manifest.mjs +13 -0
  66. package/package.json +1 -1
  67. package/sohl/actors.mjs +22 -99
  68. package/sohl/item-fields.mjs +4 -5
  69. package/sohl/items.mjs +1 -1
  70. package/sohl/note-schemas.mjs +11 -21
  71. package/types/config.d.mts +9 -0
  72. package/types/engine/actor-compiler.d.mts +11 -0
  73. package/types/engine/anchored-sections.d.mts +24 -0
  74. package/types/engine/base-compiler.d.mts +19 -0
  75. package/types/engine/base-styles.d.mts +93 -0
  76. package/types/engine/content-blocks.d.mts +87 -0
  77. package/types/engine/content-captions.d.mts +14 -1
  78. package/types/engine/content-footnotes.d.mts +73 -0
  79. package/types/engine/content-format-check.d.mts +8 -6
  80. package/types/engine/content-tables.d.mts +12 -4
  81. package/types/engine/content-tasklists.d.mts +72 -0
  82. package/types/engine/extension-attributes.d.mts +22 -1
  83. package/types/engine/frontmatter.d.mts +1 -1
  84. package/types/engine/heading-attributes.d.mts +174 -0
  85. package/types/engine/helpers.d.mts +7 -4
  86. package/types/engine/homepage.d.mts +7 -7
  87. package/types/engine/index.d.mts +1 -0
  88. package/types/engine/infobox-render.d.mts +6 -1
  89. package/types/engine/journals.d.mts +14 -7
  90. package/types/engine/markdown-expressions.d.mts +23 -0
  91. package/types/engine/note-frontmatter.d.mts +1 -4
  92. package/types/engine/note-vocabulary.d.mts +82 -6
  93. package/types/engine/page-lists.d.mts +126 -0
  94. package/types/engine/pdf-render.d.mts +72 -2
  95. package/types/engine/rank-ladder.d.mts +32 -0
  96. package/types/engine/retired-fields.d.mts +4 -6
  97. package/types/engine/site-build.d.mts +13 -1
  98. package/types/engine/site-config.d.mts +9 -0
  99. package/types/engine/site-index.d.mts +13 -1
  100. package/types/engine/site-preview.d.mts +1 -1
  101. package/types/engine/sql-tables.d.mts +31 -7
  102. package/types/engine/standings.d.mts +21 -3
  103. package/types/engine/system-block.d.mts +11 -12
  104. package/types/engine/web-wikilinks.d.mts +3 -1
  105. package/types/hm3/actors.d.mts +3 -3
  106. package/types/sohl/actors.d.mts +6 -7
  107. package/engine/content-admonitions.mjs +0 -114
  108. package/engine/content-secrets.mjs +0 -112
  109. package/types/engine/content-admonitions.d.mts +0 -24
  110. package/types/engine/content-secrets.d.mts +0 -17
package/sohl/actors.mjs CHANGED
@@ -18,10 +18,9 @@
18
18
  * reference translation through the system's map, embedding and its stable ids,
19
19
  * the anchored prose sections — lives in
20
20
  * {@link module:engine/actor-compiler}, where a second system reaches it.
21
- * What is left here is SoHL's data model: the body structure and its
22
- * movement profiles, the attributes-and-items frontmatter that becomes embedded
23
- * documents, the opening mastery level a skill is baked with, and the `system`
24
- * block itself.
21
+ * What is left here is SoHL's data model: the `sohl.items` frontmatter that
22
+ * becomes embedded documents, the opening mastery level a skill is baked with,
23
+ * and the `system` block itself.
25
24
  *
26
25
  * One content type today, and the actor subtype it produces is **declared**
27
26
  * rather than assumed to be the same word: `sohl/document-subtypes.mjs` maps
@@ -29,10 +28,16 @@
29
28
  * `ACTOR_VAULT_TYPE = "being"` here and emitted `type: "being"` several hundred
30
29
  * lines below, which made the two vocabularies agree by coincidence.
31
30
  *
32
- * Attributes (`sohl.attributes` map) become embedded attribute items with
33
- * `scoreBase` set from the map value. Each entry in `sohl.items` is similarly
34
- * resolved by `(type, shortcode)` and deep-merged with the entry's other
35
- * properties. `sohl.skills` is ignored.
31
+ * Each entry in `sohl.items` is resolved by `(type, shortcode)` and deep-merged
32
+ * with the entry's other properties, so an attribute carries its `scoreBase`
33
+ * and a skill its `masteryLevelBase` as that entry's own `system` overlay.
34
+ *
35
+ * **A being's own document fields are authored under `sohl.system`**, which
36
+ * {@link module:engine/system-block.mergeSystemData} writes at the DataModel's
37
+ * own paths: a being's body is `sohl.system.body`, with
38
+ * `sohl.system.currentMoveMedium` and `sohl.system.movementProfiles` beside it.
39
+ * So this pass writes no body itself, and an incorporeal being authors none and
40
+ * keeps the schema's empty one.
36
41
  *
37
42
  * **A membership is derived, never authored here.** `data.affiliations` names
38
43
  * the bodies a being belongs to and the standing it holds in each, and each
@@ -105,60 +110,12 @@ function defaultActorImg(subType) {
105
110
  return img;
106
111
  }
107
112
 
108
- /**
109
- * Normalize a being's persisted `system.body` from a `sohl.body` block. The
110
- * authoring frontmatter mirrors the schema field-for-field: `sohl.body` nests
111
- * `structure` / `weight` / `reachBase` / `bodyScaleBase` / `personalFatigue`,
112
- * exactly like `system.body`.
113
- */
114
- function normalizeBody(bodyObj) {
115
- const b = bodyObj && typeof bodyObj === "object" ? bodyObj : {};
116
- const weight = b.weight || {};
117
- return {
118
- structure: b.structure ?? { parts: [], adjacent: [] },
119
- weight: {
120
- base: weight.base == null ? null : Number(weight.base),
121
- calc: String(weight.calc ?? "0"),
122
- },
123
- reachBase: Number(b.reachBase ?? 0) || 0,
124
- bodyScaleBase: Number(b.bodyScaleBase ?? 1) || 1,
125
- personalFatigue: String(b.personalFatigue ?? "enc"),
126
- };
127
- }
128
-
129
- /** Normalize per-medium movement profiles from a `sohl.movementProfiles` list. */
130
- function normalizeMovementProfiles(list) {
131
- return (Array.isArray(list) ? list : []).map((p) => ({
132
- medium: String(p.medium ?? "terrestrial"),
133
- feetPerRound: Number(p.feetPerRound ?? 0) || 0,
134
- leaguesPerWatch: Number(p.leaguesPerWatch ?? 0) || 0,
135
- encumbrance: String(p.encumbrance ?? "0"),
136
- strMod: String(p.strMod ?? "0"),
137
- disabled: Boolean(p.disabled ?? false),
138
- }));
139
- }
140
-
141
- /**
142
- * Extract a being's body (+ its movement) from a `sohl` block that mirrors the
143
- * schema: `sohl.body` (nested → `system.body`) and the flat
144
- * `sohl.currentMoveMedium` / `sohl.movementProfiles` (→ the base-actor movement
145
- * fields; movement is a universal actor capability, not part of the body).
146
- */
147
- function extractBodyAndMovement(fm) {
148
- return {
149
- body: normalizeBody(sohlField(fm, "body", {})),
150
- currentMoveMedium: String(sohlField(fm, "currentMoveMedium", "none")),
151
- movementProfiles: normalizeMovementProfiles(sohlField(fm, "movementProfiles", [])),
152
- };
153
- }
154
-
155
113
  /**
156
114
  * SoHL's Actor compile pass.
157
115
  *
158
116
  * Declares SoHL's note-type → document-subtype map and builds a `being` note's
159
- * document: the body structure and its movement profiles, the embedded
160
- * attribute and item documents the frontmatter names, and the `system` block.
161
- * Everything else is {@link module:engine/actor-compiler}'s.
117
+ * document: the embedded item documents the frontmatter names, and the `system`
118
+ * block. Everything else is {@link module:engine/actor-compiler}'s.
162
119
  */
163
120
  export class Actors extends SystemActorCompiler {
164
121
  /**
@@ -184,30 +141,12 @@ export class Actors extends SystemActorCompiler {
184
141
  }
185
142
 
186
143
  /**
187
- * Build all embedded items for an actor: one per `sohl.attributes`
188
- * entry plus one per `sohl.items` entry. `sohl.skills` is ignored.
144
+ * Build all embedded items for an actor: one per `sohl.items` entry, plus
145
+ * one per body the being belongs to.
189
146
  */
190
147
  buildEmbeddedItems(itemsMap, actorId, fm, ctx) {
191
148
  const items = [];
192
149
 
193
- const attributes = sohlField(fm, "attributes", null);
194
- if (attributes && typeof attributes === "object") {
195
- for (const [shortcode, value] of Object.entries(attributes)) {
196
- const overlay = { system: { scoreBase: Number(value) || 0 } };
197
- const embedded = this.resolveEmbedded(
198
- itemsMap,
199
- actorId,
200
- "attribute",
201
- shortcode,
202
- overlay,
203
- `attr:${shortcode}`,
204
- ctx,
205
- { fmKey: "attributes" },
206
- );
207
- if (embedded) items.push(embedded);
208
- }
209
- }
210
-
211
150
  const sohlItems = sohlField(fm, "items", null);
212
151
  if (Array.isArray(sohlItems)) {
213
152
  sohlItems.forEach((entry, index) => {
@@ -333,8 +272,8 @@ export class Actors extends SystemActorCompiler {
333
272
  *
334
273
  * **The scores used are the ones just written.** `SkillLogic` resolves
335
274
  * `attr.<code>` to an attribute's *effective* score, after active effects;
336
- * all this pass has is the `scoreBase` it set from `sohl.attributes`. For a
337
- * compiled being carrying no attribute-altering effects the two agree,
275
+ * all this pass has is the `scoreBase` on each embedded attribute item. For
276
+ * a compiled being carrying no attribute-altering effects the two agree,
338
277
  * which is every being in content today. One that did carry such an effect
339
278
  * would bake a Skill Base its client then disagrees with — that is the
340
279
  * limit of doing this at build time, and the point to revisit if it bites.
@@ -387,6 +326,9 @@ export class Actors extends SystemActorCompiler {
387
326
  const { value: authoredFolder, isAddress } = folderField(fm);
388
327
  const folder = this.folderResolver(authoredFolder, { isAddress });
389
328
 
329
+ this.checkAnchoredSection(body || "", "appearance", ctx);
330
+ this.checkAnchoredSection(body || "", "dossier", ctx);
331
+
390
332
  const system = {
391
333
  // The frontmatter shortcode is the actor's stable `(type, shortcode)`
392
334
  // key — and, for a being that is an archetype, its archetype
@@ -411,25 +353,6 @@ export class Actors extends SystemActorCompiler {
411
353
  dossier: withDraftNotice(fm, renderSection(body || "", "dossier")),
412
354
  };
413
355
 
414
- // Fill `system.body` (+ the base-actor movement fields) from the being's
415
- // frontmatter, rather than embedding a corpus item. The `sohl`
416
- // block mirrors `system` field-for-field: `sohl.body` nests the body
417
- // (`structure` / `weight` / …), with `currentMoveMedium` /
418
- // `movementProfiles` flat alongside it. An **incorporeal** being omits
419
- // `sohl.body` and keeps the schema's empty body.
420
- const bodyField = sohlField(fm, "body", null);
421
- if (bodyField && typeof bodyField === "object") {
422
- const bodyData = extractBodyAndMovement(fm);
423
- system.body = bodyData.body;
424
- system.currentMoveMedium = bodyData.currentMoveMedium;
425
- system.movementProfiles = bodyData.movementProfiles;
426
- } else if (bodyField != null) {
427
- this.noteError(
428
- `${ctx}: sohl.body must be an inline object (structure/weight/…), got ${typeof bodyField}`,
429
- );
430
- this.errorCount++;
431
- }
432
-
433
356
  // Being-only combat grouping (mirrors `system.defaultCombatGroup`).
434
357
  const defaultCombatGroup = sohlField(fm, "defaultCombatGroup", undefined);
435
358
  if (defaultCombatGroup !== undefined) {
@@ -391,11 +391,10 @@ export const ITEM_FIELDS = Object.freeze({
391
391
  ...STRING,
392
392
  default: "",
393
393
  topLevelMeans:
394
- "the note's own title — the heading its page is published under, " +
395
- "which has nothing to do with the style of address an office " +
396
- "carries. Author this on the membership instead: `sohl.system.title` " +
397
- "on the item, or the `system.title` of the entry in a being's " +
398
- "`sohl.items`.",
394
+ "An office's style of address belongs to the membership that " +
395
+ "holds the office, so nothing outside the item supplies it. " +
396
+ "Author it at `sohl.system.title` on the item, or at the " +
397
+ "`system.title` of the entry in a being's `sohl.items`.",
399
398
  describe: "The style of address the office carries.",
400
399
  },
401
400
  {
package/sohl/items.mjs CHANGED
@@ -93,7 +93,7 @@ export class Items extends SystemItemCompiler {
93
93
  // it — an undeclared `system` key is discarded at construction
94
94
  // without a warning.
95
95
  templatePriority: systemTemplatePriority(fm, label),
96
- actionDefs: Array.isArray(fm.actionDefs) ? fm.actionDefs : [],
96
+ actionDefs: [],
97
97
  notes: "",
98
98
  docHtml: description || "",
99
99
  };
@@ -99,37 +99,27 @@ const MACRO_FIELDS = Object.freeze([
99
99
  const VEHICLE_FIELDS = Object.freeze([]);
100
100
 
101
101
  /**
102
- * A `being` note — an actor, with its body, movement and embedded items.
102
+ * A `being` note — an actor, with its embedded items and its combat grouping.
103
+ *
104
+ * **A being's own document fields are authored under `sohl.system`**, which the
105
+ * passthrough writes at the DataModel's own paths — a body and the movement that
106
+ * goes with it are `sohl.system.body`, `sohl.system.currentMoveMedium` and
107
+ * `sohl.system.movementProfiles`, written together — so this declaration states
108
+ * none of them. **Attributes and skills are embedded items**, each named by the
109
+ * catalogue entry it copies, so both are `sohl.items` entries with the score or
110
+ * the mastery level as that entry's own `system` overlay.
111
+ *
112
+ * What is left is the two keys the pass reads off the block itself.
103
113
  *
104
114
  * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
105
115
  */
106
116
  const BEING_FIELDS = Object.freeze([
107
- {
108
- name: "body",
109
- ...MAP,
110
- describe: "The being's body structure, by part shortcode.",
111
- },
112
- {
113
- name: "attributes",
114
- ...MAP,
115
- describe: "Attribute scores, by attribute shortcode.",
116
- },
117
117
  {
118
118
  name: "items",
119
119
  ...LIST,
120
120
  describe:
121
121
  "Items embedded on the being, each addressed by (type, shortcode) or carrying enough fields to stand alone.",
122
122
  },
123
- {
124
- name: "currentMoveMedium",
125
- ...STRING,
126
- describe: "Which medium the being is currently moving through.",
127
- },
128
- {
129
- name: "movementProfiles",
130
- ...LIST,
131
- describe: "Movement rates, one profile per medium.",
132
- },
133
123
  {
134
124
  name: "defaultCombatGroup",
135
125
  ...AS_AUTHORED,
@@ -102,6 +102,9 @@ export const DERIVED_MANIFEST_KEYS: Readonly<Record<string, string>>;
102
102
  * @property {string} stageDir The staged package root, relative to
103
103
  * `rootDir`. Every asset `to:` lands under it.
104
104
  * @property {readonly Readonly<AssetSpec>[]} assets
105
+ * @property {boolean} baseStyles Whether the package takes the shared base
106
+ * stylesheet, staged into `stageDir` and
107
+ * named first in the manifest's `styles`.
105
108
  * @property {Readonly<Record<string, unknown>>} manifest The manifest
106
109
  * specification, emitted as declared.
107
110
  * @property {string|null} manifestFlags Module to load a `flags` function
@@ -235,6 +238,12 @@ export type PackageBuildConfig = {
235
238
  */
236
239
  stageDir: string;
237
240
  assets: readonly Readonly<AssetSpec>[];
241
+ /**
242
+ * Whether the package takes the shared base
243
+ * stylesheet, staged into `stageDir` and
244
+ * named first in the manifest's `styles`.
245
+ */
246
+ baseStyles: boolean;
238
247
  /**
239
248
  * The manifest
240
249
  * specification, emitted as declared.
@@ -248,6 +248,17 @@ export class SystemActorCompiler extends BasePackCompiler {
248
248
  img: string;
249
249
  token: string;
250
250
  };
251
+ /**
252
+ * Report an `{#appearance}` or `{#dossier}` anchor on a heading
253
+ * {@link extractAnchorSection} cannot read, rather than letting the field
254
+ * it feeds compile blank with nothing to say why.
255
+ *
256
+ * @param {string} body - The note body.
257
+ * @param {string} anchorId - `"appearance"` or `"dossier"`.
258
+ * @param {string} ctx - The actor's label, for the message.
259
+ * @returns {void}
260
+ */
261
+ checkAnchoredSection(body: string, anchorId: string, ctx: string): void;
251
262
  /** @type {readonly string[]} */
252
263
  itemsSourceDirs: readonly string[];
253
264
  foreignSourceDirs: any;
@@ -6,6 +6,11 @@
6
6
  * heading exists. Fenced code blocks are respected so `# foo` inside
7
7
  * ``` blocks does not trigger a match.
8
8
  *
9
+ * An anchor declared on a lower heading is found by {@link collectAnchors}
10
+ * but not by the `#\s+` match below, so it extracts as absent rather than as
11
+ * wrong — {@link misplacedAnchorSection} is the check a caller runs first to
12
+ * tell the two apart.
13
+ *
9
14
  * @param {string} body - The note body.
10
15
  * @param {string} anchorId - The anchor to find.
11
16
  * @returns {string} The section's markdown, or "".
@@ -19,3 +24,22 @@ export function extractAnchorSection(body: string, anchorId: string): string;
19
24
  * @returns {string} The rendered HTML, or "".
20
25
  */
21
26
  export function renderSection(body: string, anchorId: string): string;
27
+ /**
28
+ * Where an `{#appearance}` or `{#dossier}` anchor sits on a heading below the
29
+ * top level, which {@link extractAnchorSection} cannot read.
30
+ *
31
+ * {@link module:engine/anchors.collectAnchors} finds the anchor at any
32
+ * level, because a lower heading legitimately starts a journal page. It does
33
+ * not feed an actor field: an H1-only extractor reads nothing from one, and a
34
+ * caller that trusts the empty result as "no such section" rather than
35
+ * "found, but unreadable" compiles a blank field with nothing to say why.
36
+ *
37
+ * @param {string} body - The note body.
38
+ * @param {string} anchorId - The anchor to check, case-insensitively.
39
+ * @param {number} [bodyLine=1] - The 1-based file line the body starts on.
40
+ * @returns {{line: number}|null} The misplaced anchor's line, or `null` when
41
+ * it is absent or already on an H1.
42
+ */
43
+ export function misplacedAnchorSection(body: string, anchorId: string, bodyLine?: number): {
44
+ line: number;
45
+ } | null;
@@ -378,6 +378,25 @@ export class BasePackCompiler {
378
378
  linkIndex: any;
379
379
  sqlTables: any;
380
380
  worldPresent: unknown;
381
+ /**
382
+ * Report every finding {@link BasePackCompiler#convertBody} collected
383
+ * across its passes, and decline the note if there were any.
384
+ *
385
+ * Reported directly, one `noteError` per finding, rather than thrown as a
386
+ * single wrapped message: the site build and the book report the same way,
387
+ * each finding at its own line, and a note with three problems meets all
388
+ * three in the run that found them instead of one per rebuild. The thrown
389
+ * sentinel carries `alreadyReported` so the compile loop's generic catch
390
+ * does not add a fourth, vaguer line on top of the three already said.
391
+ *
392
+ * @param {readonly {message: string, line?: number, column?: number}[]} findings
393
+ * @throws {Error} When `findings` is non-empty, after reporting all of them.
394
+ */
395
+ reportConvertBodyFindings(findings: readonly {
396
+ message: string;
397
+ line?: number;
398
+ column?: number;
399
+ }[]): void;
381
400
  /**
382
401
  * The body {@link BasePackCompiler#buildEntry} receives.
383
402
  *
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The cascade layer the sheet loads into, per package kind.
3
+ *
4
+ * Foundry declares its layer order in `css/foundry2.css`, ending
5
+ * `… layouts, system, modules, exceptions` — the eighth and ninth being, by its
6
+ * own comments, default game system styles and default module styles. That is
7
+ * what this sheet is, so it takes the layer its kind is given and is therefore
8
+ * the weakest thing a package can load:
9
+ *
10
+ * - **An unlayered package sheet beats it**, because an unlayered rule beats
11
+ * every layered one whatever its specificity.
12
+ * - **A package sheet naming any other layer beats it too**, because a layer
13
+ * Foundry's order does not mention is appended after the ones it does.
14
+ *
15
+ * So a package overrides the base sheet from its own, whichever way it declares
16
+ * one, and the override needs no `!important` and no specificity contest.
17
+ *
18
+ * @param {"system"|"module"} artifact - Which artifact is shipped.
19
+ * @returns {string} The layer name.
20
+ */
21
+ export function baseStyleLayer(artifact: "system" | "module"): string;
22
+ /**
23
+ * The manifest `styles` entry for the base sheet.
24
+ *
25
+ * Foundry's `styles` is an array of `{layer?, src}`, and a bare string is
26
+ * migrated into that shape on read, so the object form is written directly.
27
+ *
28
+ * @param {"system"|"module"} artifact - Which artifact is shipped.
29
+ * @returns {{src: string, layer: string}} The entry.
30
+ */
31
+ export function baseStyleEntry(artifact: "system" | "module"): {
32
+ src: string;
33
+ layer: string;
34
+ };
35
+ /**
36
+ * The absolute path of the sheet as installed, so the stage can copy it.
37
+ *
38
+ * Resolved against this module rather than against the consumer's working
39
+ * directory, because the file belongs to this package and arrives through
40
+ * `node_modules`.
41
+ *
42
+ * @returns {string} Absolute path of the shipped stylesheet.
43
+ */
44
+ export function baseStylesheetSource(): string;
45
+ /**
46
+ * The `[source, destination]` pair that stages the sheet.
47
+ *
48
+ * The destination is relative, in the same spelling the repository's own asset
49
+ * table uses, so the caller resolves both against the repository root exactly
50
+ * as it resolves every other entry.
51
+ *
52
+ * @param {string} stageDir - The configured stage directory, relative to the
53
+ * repository root.
54
+ * @returns {[string, string]} The pair, for `stageAssets`.
55
+ */
56
+ export function baseStyleStageEntry(stageDir: string): [string, string];
57
+ /**
58
+ * The manifest's `styles`, with the base sheet named first.
59
+ *
60
+ * **First rather than last**, because `styles` is load order: a package's own
61
+ * sheet has to be read after the base one for an override written at equal
62
+ * specificity in an equal layer to take. The cascade layer makes the override
63
+ * work anyway, and the order makes it work for the right reason.
64
+ *
65
+ * A declared entry addressing the same file is not duplicated — a repository
66
+ * that named the staged path itself gets one entry, keeping whatever layer it
67
+ * chose, so declaring it is a way of placing it rather than an error.
68
+ *
69
+ * @param {object} options
70
+ * @param {unknown} [options.declared] - The declared `styles`, whatever shape
71
+ * it was written in; anything that is not a list is returned untouched.
72
+ * @param {"system"|"module"} options.artifact - Which artifact is shipped.
73
+ * @param {boolean} [options.enabled] - Whether the package takes the base
74
+ * sheet. `false` returns the declared list unchanged.
75
+ * @returns {unknown} The `styles` to emit, or `undefined` when there is
76
+ * nothing to emit at all.
77
+ */
78
+ export function withBaseStyle({ declared, artifact, enabled }: {
79
+ declared?: unknown;
80
+ artifact: "system" | "module";
81
+ enabled?: boolean | undefined;
82
+ }): unknown;
83
+ /**
84
+ * Where the shipped sheet lands inside the staged package.
85
+ *
86
+ * Under `styles/` beside a package's own sheet, and named for the toolchain so
87
+ * nothing a repository writes can collide with it. The path is relative to the
88
+ * staged package root, which is what both the stage entry and the manifest's
89
+ * `styles` are addressed in.
90
+ *
91
+ * @type {string}
92
+ */
93
+ export const BASE_STYLESHEET_DEST: string;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Find named blocks, leaving code examples alone.
3
+ *
4
+ * A block reaches `blocks` only when it is well formed and closed; everything
5
+ * else is a finding, and the surrounding blocks are unaffected.
6
+ *
7
+ * @param {string} source - Markdown that may contain named blocks.
8
+ * @returns {{blocks: Array<{name: string, title: string, id: string, classes: string[], attributes: Record<string, string>, start: number, end: number, body: string}>, errors: Array<{line: number, column: number, message: string}>}}
9
+ */
10
+ export function scanBlocks(source: string): {
11
+ blocks: Array<{
12
+ name: string;
13
+ title: string;
14
+ id: string;
15
+ classes: string[];
16
+ attributes: Record<string, string>;
17
+ start: number;
18
+ end: number;
19
+ body: string;
20
+ }>;
21
+ errors: Array<{
22
+ line: number;
23
+ column: number;
24
+ message: string;
25
+ }>;
26
+ };
27
+ /**
28
+ * Render named blocks for one publishing surface.
29
+ *
30
+ * `foundry` emits a `<section>` whose heading is its first line; `web` emits a
31
+ * `<details>` the reader opens. Neither carries an inline `style`.
32
+ *
33
+ * The body is left as Markdown between the wrapper's tags, each separated from
34
+ * its tag by a blank line — see the module docs for why that is what lets the
35
+ * surrounding render treat the body as real content rather than an opaque
36
+ * fragment.
37
+ *
38
+ * A finding does not stop the well-formed blocks around it from rendering.
39
+ *
40
+ * @param {string} source - Markdown containing named blocks.
41
+ * @param {"foundry"|"web"} target - Publishing surface.
42
+ * @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>}}
43
+ */
44
+ export function renderBlocks(source: string, target: "foundry" | "web"): {
45
+ markdown: string;
46
+ errors: Array<{
47
+ line: number;
48
+ column: number;
49
+ message: string;
50
+ }>;
51
+ };
52
+ /**
53
+ * Wrap each withheld section in the disclosure a `secret` block renders as.
54
+ *
55
+ * The section's own Markdown is left as Markdown, with a blank line either side
56
+ * of it, so the page's renderer still reads it: the heading keeps its id, the
57
+ * prose keeps its links, and only the disclosure around them is written as HTML.
58
+ *
59
+ * **A spoiler, not access control.** The section is in the published page, so a
60
+ * reader who opens the element or reads the source reads it. Foundry is the one
61
+ * surface that withholds anything.
62
+ *
63
+ * @param {string} source - A page's Markdown, headings intact.
64
+ * @returns {string} The same Markdown, each withheld section inside a
65
+ * `<details>`.
66
+ */
67
+ export function renderWithheldSections(source: string): string;
68
+ /** The blocks an author may open, with the heading each takes by default. */
69
+ export const BLOCK_NAMES: Readonly<{
70
+ info: "Info";
71
+ secret: "Secret";
72
+ warn: "Warn";
73
+ }>;
74
+ /**
75
+ * Blocks that may hold another named block.
76
+ *
77
+ * A box inside a GM-only section is a real thing to write — the section is a
78
+ * container for whatever the GM reads, boxes included — so a `secret` holds one
79
+ * and the box stays inside it. A box inside a box is noise, so `info` and `warn`
80
+ * hold no named block and a second opener inside one is a finding.
81
+ *
82
+ * Exported because a renderer has to know it: the body of a container is read
83
+ * for blocks of its own, and the body of a box is not.
84
+ *
85
+ * @type {readonly string[]}
86
+ */
87
+ export const BLOCK_CONTAINERS: readonly string[];
@@ -27,7 +27,20 @@ export function scanCaptions(source: string): {
27
27
  message: string;
28
28
  }>;
29
29
  };
30
- /** Render captioned blocks as HTML, leaving other Markdown untouched. */
30
+ /**
31
+ * Render captioned blocks as HTML, leaving other Markdown untouched.
32
+ *
33
+ * A code, table or prose caption's block is left as Markdown inside the
34
+ * wrapper, blank-line separated from its tags exactly as
35
+ * {@link module:engine/content-blocks.renderBlocks} leaves a named block's
36
+ * body — so the surrounding render sees it as part of its own document and a
37
+ * footnote reference inside it resolves against the note's own definitions.
38
+ *
39
+ * A figure's block is still rendered here, because its `<figcaption>` has to
40
+ * be stripped from HTML this pass controls directly; the surrounding render
41
+ * never sees it as Markdown, so a footnote reference inside a figure caption
42
+ * is not resolved, the same gap a block's own title carries.
43
+ */
31
44
  export function renderCaptionBlocks(source: any, renderMarkdown: ((src: string, env?: import("markdown-it").Env) => string) | undefined, numbers: any): {
32
45
  markdown: any;
33
46
  errors: {
@@ -8,3 +8,76 @@ export function separateFootnotes(source: string): {
8
8
  markdown: string;
9
9
  definitions: string;
10
10
  };
11
+ /**
12
+ * A footnote definition written inside a list item or a block quote, where
13
+ * the web and Foundry can resolve it but the book drops it silently.
14
+ *
15
+ * A definition inside a table cell is not reported here: a table cell is
16
+ * inline content, so `[^id]:` there is never recognised as a definition on
17
+ * any surface and simply prints as written — the reference it was meant to
18
+ * answer is what {@link unresolvedFootnoteReferences} reports.
19
+ *
20
+ * @param {string} source - A note's markdown body.
21
+ * @returns {Array<{line: number, column: number, label: string, message: string}>}
22
+ */
23
+ export function misplacedFootnoteDefinitions(source: string): Array<{
24
+ line: number;
25
+ column: number;
26
+ label: string;
27
+ message: string;
28
+ }>;
29
+ /**
30
+ * A footnote reference with no top-level definition to resolve against.
31
+ *
32
+ * Covers both commoner slips alike: a label never defined at all, and one
33
+ * "defined" only inside a table cell, which is inline content and never
34
+ * registers as a definition on any surface. A label misplaced in a list or a
35
+ * block quote is excluded — {@link misplacedFootnoteDefinitions} already
36
+ * reports that mistake at the definition's own line, and reporting it again
37
+ * at the reference would say the same thing twice.
38
+ *
39
+ * @param {string} source - A note's markdown body.
40
+ * @param {string} [definitions] - Its top-level definitions, when a caller
41
+ * already extracted them with {@link separateFootnotes}. Derived from
42
+ * `source` when omitted; a caller already holding both passes its own
43
+ * extraction instead, so the two cannot disagree about what is defined.
44
+ * @returns {Array<{line: number, column: number, message: string}>}
45
+ */
46
+ export function unresolvedFootnoteReferences(source: string, definitions?: string): Array<{
47
+ line: number;
48
+ column: number;
49
+ message: string;
50
+ }>;
51
+ /**
52
+ * A top-level footnote definition no reference anywhere in the note uses.
53
+ *
54
+ * `separateFootnotes` lifts it out correctly, nothing resolves against it,
55
+ * and the paragraph it held vanishes from every surface with the build
56
+ * exiting 0 — a dropped paragraph is not something a reader can notice is
57
+ * missing, so it is an error here rather than a warning, the same severity
58
+ * every other silent loss this pass catches already carries. A note mid-edit,
59
+ * with the reference already deleted and the definition not yet followed, is
60
+ * exactly the case this refuses — which is the point: the alternative is
61
+ * shipping it.
62
+ *
63
+ * @param {string} source - A note's markdown body.
64
+ * @returns {Array<{line: number, column: number, message: string}>}
65
+ */
66
+ export function unusedFootnoteDefinitions(source: string): Array<{
67
+ line: number;
68
+ column: number;
69
+ message: string;
70
+ }>;
71
+ /**
72
+ * Every footnote placement finding for one note body — the union
73
+ * {@link misplacedFootnoteDefinitions}, {@link unresolvedFootnoteReferences}
74
+ * and {@link unusedFootnoteDefinitions} report, in document order.
75
+ *
76
+ * @param {string} source - A note's markdown body.
77
+ * @returns {Array<{line: number, column: number, message: string}>}
78
+ */
79
+ export function footnoteFindings(source: string): Array<{
80
+ line: number;
81
+ column: number;
82
+ message: string;
83
+ }>;
@@ -180,12 +180,14 @@ export function checkDeclaredFields({ format, itemFields, system, severity }: {
180
180
  /**
181
181
  * Top-level keys the format names as the note's own, whatever its type.
182
182
  *
183
- * Top level is otherwise **open** — an unrecognised key there is a Hugo or
184
- * theme parameter this build has no standing to refuse — so this set exists
185
- * only to keep a note's own identity fields from being mistaken for a
186
- * misplaced `data` property where the two share a name. `type` and `subType`
187
- * are the pair that matters: several mapping tables name `subType` as a shared
188
- * source, and it is authored at top level by design.
183
+ * The top-level vocabulary itself, read rather than restated: this set exists
184
+ * to keep a note's own identity fields from being mistaken for a misplaced
185
+ * `data` property where the two share a name, and the keys that are a note's
186
+ * own are exactly the ones the region declares. `type` and `subType` are the
187
+ * pair that matters — several mapping tables name `subType` as a shared source,
188
+ * and it is authored at top level by design. A key the region does *not*
189
+ * declare, such as `id` beside `data.id`, is reported as the misplaced `data`
190
+ * property it is.
189
191
  *
190
192
  * @type {ReadonlySet<string>}
191
193
  */