@heroiclands/package-build 22.20.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 (118) hide show
  1. package/CHANGELOG-content-build.md +0 -8
  2. package/CHANGELOG.md +204 -0
  3. package/assets/styles/content-base.css +162 -0
  4. package/bin/content-commands.mjs +26 -3
  5. package/bin/package-build.mjs +15 -2
  6. package/ci/content-format-reference.mjs +68 -1
  7. package/config.mjs +26 -0
  8. package/content-config.mjs +73 -4
  9. package/docs/api.md +46 -28
  10. package/docs/authoring/dates-and-calendars.md +4 -2
  11. package/docs/authoring/frontmatter.md +2 -2
  12. package/docs/authoring/links-and-markup.md +237 -28
  13. package/docs/configuration.md +27 -1
  14. package/docs/content-format.md +3 -1
  15. package/docs/guides/reference-pages.md +15 -0
  16. package/docs/reference/format-details.md +265 -42
  17. package/docs/reference/note-types.md +45 -28
  18. package/engine/actor-compiler.mjs +23 -0
  19. package/engine/anchored-sections.mjs +35 -4
  20. package/engine/anchors.mjs +10 -20
  21. package/engine/base-compiler.mjs +86 -21
  22. package/engine/base-styles.mjs +149 -0
  23. package/engine/bundles.mjs +0 -1
  24. package/engine/compendiums.mjs +8 -1
  25. package/engine/content-blocks.mjs +410 -0
  26. package/engine/content-captions.mjs +57 -8
  27. package/engine/content-footnotes.mjs +189 -0
  28. package/engine/content-format-check.mjs +15 -12
  29. package/engine/content-index.mjs +2 -2
  30. package/engine/content-links.mjs +14 -6
  31. package/engine/content-tables.mjs +45 -4
  32. package/engine/content-tasklists.mjs +189 -0
  33. package/engine/docs-checks.mjs +4 -4
  34. package/engine/extension-attributes.mjs +48 -2
  35. package/engine/frontmatter-lint.mjs +62 -66
  36. package/engine/frontmatter.mjs +3 -3
  37. package/engine/heading-attributes.mjs +308 -0
  38. package/engine/helpers.mjs +19 -13
  39. package/engine/homepage.mjs +8 -8
  40. package/engine/index.mjs +3 -0
  41. package/engine/infobox-render.mjs +21 -4
  42. package/engine/infobox.mjs +167 -7
  43. package/engine/journals.mjs +36 -17
  44. package/engine/macros.mjs +1 -1
  45. package/engine/markdown-expressions.mjs +178 -53
  46. package/engine/note-addresses.mjs +17 -8
  47. package/engine/note-dates.mjs +49 -0
  48. package/engine/note-frontmatter.mjs +7 -16
  49. package/engine/note-vocabulary.mjs +216 -42
  50. package/engine/page-lists.mjs +358 -0
  51. package/engine/pdf-build.mjs +76 -42
  52. package/engine/pdf-render.mjs +422 -49
  53. package/engine/rank-ladder.mjs +198 -0
  54. package/engine/reckoning-markers.mjs +1 -1
  55. package/engine/reference-targets.mjs +13 -1
  56. package/engine/retired-fields.mjs +8 -13
  57. package/engine/scenes.mjs +10 -3
  58. package/engine/site-build.mjs +41 -12
  59. package/engine/site-config.mjs +10 -1
  60. package/engine/site-index.mjs +39 -4
  61. package/engine/site-preview.mjs +18 -7
  62. package/engine/social-ties.mjs +52 -14
  63. package/engine/sql-tables.mjs +66 -9
  64. package/engine/standing-terms.mjs +19 -0
  65. package/engine/standings.mjs +538 -0
  66. package/engine/svg-theme.mjs +15 -4
  67. package/engine/system-block.mjs +12 -14
  68. package/engine/web-wikilinks.mjs +48 -1
  69. package/engine/wikilinks.mjs +3 -1
  70. package/hm3/actors.mjs +6 -3
  71. package/manifest.mjs +13 -0
  72. package/package.json +1 -1
  73. package/sohl/actors.mjs +82 -103
  74. package/sohl/item-fields.mjs +4 -5
  75. package/sohl/items.mjs +1 -1
  76. package/sohl/note-schemas.mjs +11 -21
  77. package/types/config.d.mts +9 -0
  78. package/types/engine/actor-compiler.d.mts +11 -0
  79. package/types/engine/anchored-sections.d.mts +24 -0
  80. package/types/engine/base-compiler.d.mts +19 -0
  81. package/types/engine/base-styles.d.mts +93 -0
  82. package/types/engine/content-blocks.d.mts +87 -0
  83. package/types/engine/content-captions.d.mts +14 -1
  84. package/types/engine/content-footnotes.d.mts +73 -0
  85. package/types/engine/content-format-check.d.mts +8 -6
  86. package/types/engine/content-tables.d.mts +12 -4
  87. package/types/engine/content-tasklists.d.mts +72 -0
  88. package/types/engine/extension-attributes.d.mts +22 -1
  89. package/types/engine/frontmatter.d.mts +1 -1
  90. package/types/engine/heading-attributes.d.mts +174 -0
  91. package/types/engine/helpers.d.mts +7 -4
  92. package/types/engine/homepage.d.mts +7 -7
  93. package/types/engine/index.d.mts +1 -0
  94. package/types/engine/infobox-render.d.mts +6 -1
  95. package/types/engine/journals.d.mts +14 -7
  96. package/types/engine/markdown-expressions.d.mts +23 -0
  97. package/types/engine/note-frontmatter.d.mts +1 -4
  98. package/types/engine/note-vocabulary.d.mts +98 -8
  99. package/types/engine/page-lists.d.mts +126 -0
  100. package/types/engine/pdf-render.d.mts +72 -2
  101. package/types/engine/rank-ladder.d.mts +32 -0
  102. package/types/engine/retired-fields.d.mts +4 -6
  103. package/types/engine/site-build.d.mts +13 -1
  104. package/types/engine/site-config.d.mts +9 -0
  105. package/types/engine/site-index.d.mts +13 -1
  106. package/types/engine/site-preview.d.mts +1 -1
  107. package/types/engine/sql-tables.d.mts +31 -7
  108. package/types/engine/standing-terms.d.mts +16 -0
  109. package/types/engine/standings.d.mts +147 -0
  110. package/types/engine/system-block.d.mts +11 -12
  111. package/types/engine/web-wikilinks.d.mts +3 -1
  112. package/types/engine/wikilinks.d.mts +2 -1
  113. package/types/hm3/actors.d.mts +3 -3
  114. package/types/sohl/actors.d.mts +25 -7
  115. package/engine/content-admonitions.mjs +0 -114
  116. package/engine/content-secrets.mjs +0 -112
  117. package/types/engine/content-admonitions.d.mts +0 -24
  118. package/types/engine/content-secrets.d.mts +0 -17
@@ -119,7 +119,7 @@ export function checkHomepageCount(found: ReadonlyArray<{
119
119
  * `packageBuild.manifest.title` — the name Foundry already shows for the
120
120
  * package — so a homepage that adds nothing to it need not restate it. An
121
121
  * authored `name.full` wins, because a front page may greet a reader
122
- * differently from a package browser. A top-level `title` is also readable.
122
+ * differently from a package browser.
123
123
  *
124
124
  * Falls back to `contentPackage` last, so a package that has no manifest of its
125
125
  * own still yields a titled page rather than a blank heading.
@@ -244,12 +244,12 @@ export const HOMEPAGE_DESTINATION: string;
244
244
  * `name` and `shortcode` are permitted: the shortcode is what a link is
245
245
  * written with, and `name` titles the page like every other note's.
246
246
  *
247
- * **A named class, not an allow-list, and that boundary is the decision.** A
248
- * homepage's frontmatter is *emitted into the published page*
249
- * ({@link homepageFrontmatter}), so an unrecognised key is a Hugo or theme
250
- * parameter this build has never heard of and has no standing to refuse.
251
- * Rejecting unknown keys would make every new theme parameter wait on a
252
- * package-build release.
247
+ * **A named class, not an allow-list, and that boundary is the decision.**
248
+ * What this class answers is a key the top-level vocabulary *accepts* and a
249
+ * homepage still cannot use, which is a different question from an unknown
250
+ * key: the frontmatter lint refuses those for every note alike, naming the
251
+ * closed vocabulary, so repeating that judgement per type would be a second
252
+ * answer to one question.
253
253
  *
254
254
  * `aliases` is deliberately not in the class: it is a **retired** field, refused
255
255
  * on every note whatever its type, so it is answered there rather than
@@ -61,6 +61,7 @@ export * as contentCharset from "./content-charset.mjs";
61
61
  export * as contentIcons from "./content-icons.mjs";
62
62
  export * as contentHtml from "./content-html.mjs";
63
63
  export * as contentImages from "./content-images.mjs";
64
+ export * as contentTaskLists from "./content-tasklists.mjs";
64
65
  export * as contentEmbeds from "./content-embeds.mjs";
65
66
  export * as pathnames from "./pathnames.mjs";
66
67
  export * as contentLinks from "./content-links.mjs";
@@ -54,12 +54,17 @@ export function sectionHasContent(section: object): boolean;
54
54
  * reference, so the link works on paper as a cross-reference and in a PDF
55
55
  * viewer as a jump. Everything else is set as its own words.
56
56
  *
57
+ * A panel's rows address pages the same way prose does, so a page the book does
58
+ * not print is set as the absolute URL a reader can follow — see
59
+ * {@link module:engine/pdf-render.absolutePageUrl}.
60
+ *
57
61
  * @param {object} value - A `link` value.
58
62
  * @param {Map<string, string>} links - Address slug → the book's anchor.
59
63
  * @param {(anchor: string) => string} labelFor - The anchor's Typst label.
64
+ * @param {string} [site] - The absolute address this book's pages are served at.
60
65
  * @returns {string} Typst markup.
61
66
  */
62
- export function linkToTypst(value: object, links: Map<string, string>, labelFor: (anchor: string) => string): string;
67
+ export function linkToTypst(value: object, links: Map<string, string>, labelFor: (anchor: string) => string, site?: string): string;
63
68
  /**
64
69
  * Every box as one Typst panel each, breaking between sections.
65
70
  *
@@ -4,7 +4,8 @@
4
4
  * a split. Content before the first H1 (if non-empty) becomes a leading
5
5
  * page. Each H1 yields a page whose name is the heading
6
6
  * text (with any `{#anchor-id}` suffix stripped out and surfaced as
7
- * `anchorSlug`).
7
+ * `anchorSlug`). The classes the heading declares come along as `classes`, so
8
+ * a surface that honours one has it without reading the suffix again.
8
9
  *
9
10
  * `leadName` names that leading page. A journal note's is "Introduction",
10
11
  * because it introduces the pages that follow. An item doc's is the item — a
@@ -15,12 +16,13 @@
15
16
  * @param {string} body - Markdown body to split.
16
17
  * @param {string} [leadName] - Name of the page before the first heading.
17
18
  * @returns {Array<{name: string, anchorSlug: string|null, level: number,
18
- * markdown: string}>} Pages in document order.
19
+ * classes: string[], markdown: string}>} Pages in document order.
19
20
  */
20
21
  export function splitPages(body: string, leadName?: string): Array<{
21
22
  name: string;
22
23
  anchorSlug: string | null;
23
24
  level: number;
25
+ classes: string[];
24
26
  markdown: string;
25
27
  }>;
26
28
  /**
@@ -97,10 +99,14 @@ export function journalPageId(entryId: string, page: {
97
99
  * @param {Array<object>} rawPages - From {@link splitPages}.
98
100
  * @param {string} entryId - The owning JournalEntry's `_id`.
99
101
  * @param {string} noteName - The note, for error messages.
102
+ * A page whose heading carries `.secret` is given to the GM alone, by the one
103
+ * ownership value Foundry reads as "nobody but a GM". Every other page states
104
+ * no ownership and inherits the journal's.
105
+ *
100
106
  * @returns {Array<{_id: string, name: string, type: string,
101
107
  * title: {show: boolean, level: number},
102
- * text: {format: number, content: string}, _key: string}>} The page
103
- * documents, in order.
108
+ * text: {format: number, content: string}, _key: string,
109
+ * ownership?: {default: number}}>} The page documents, in order.
104
110
  * @throws {Error} When the note has no content at all, or repeats an anchor.
105
111
  */
106
112
  export function buildPages(rawPages: Array<object>, entryId: string, noteName: string, captions: any): Array<{
@@ -116,6 +122,9 @@ export function buildPages(rawPages: Array<object>, entryId: string, noteName: s
116
122
  content: string;
117
123
  };
118
124
  _key: string;
125
+ ownership?: {
126
+ default: number;
127
+ };
119
128
  }>;
120
129
  /**
121
130
  * Assemble one JournalEntry document from a note's converted markdown.
@@ -133,7 +142,6 @@ export function buildPages(rawPages: Array<object>, entryId: string, noteName: s
133
142
  * @param {string} [params.leadName] - Name for the page before the first
134
143
  * heading; see {@link splitPages}.
135
144
  * @param {string|null} [params.folder] - The folder id, or `null`.
136
- * @param {object} [params.flags] - Document flags.
137
145
  * @param {object} [params.stats] - The `_stats` block to stamp. Passed by the
138
146
  * caller because it is a property of the *pack* being written, not of the
139
147
  * entry: a module may ship the same content for two systems, and each pack's
@@ -150,13 +158,12 @@ export function buildPages(rawPages: Array<object>, entryId: string, noteName: s
150
158
  * before they read it rather than after.
151
159
  * @returns {object} The JournalEntry document, keyed for the pack.
152
160
  */
153
- export function buildJournalEntry({ id, name, markdown, leadName, folder, flags, stats, infoboxes, notice, }: {
161
+ export function buildJournalEntry({ id, name, markdown, leadName, folder, stats, infoboxes, notice, }: {
154
162
  id: string;
155
163
  name: string;
156
164
  markdown: string;
157
165
  leadName?: string | undefined;
158
166
  folder?: string | null | undefined;
159
- flags?: object | undefined;
160
167
  stats?: object | undefined;
161
168
  infoboxes?: {
162
169
  id: string;
@@ -15,3 +15,26 @@ export function renderMarkdownExpressions(body: string, { fm, dates, sqlResults,
15
15
  markdown: string;
16
16
  findings: any[];
17
17
  };
18
+ /**
19
+ * An inline expression, as an author writes one.
20
+ *
21
+ * Hugo shortcodes begin `{{<` or `{{%` and stay in the source for Hugo.
22
+ *
23
+ * Exported because a renderer that meets one has met a body this pass never ran
24
+ * over, and has to be able to say so.
25
+ *
26
+ * @type {RegExp}
27
+ */
28
+ export const EXPRESSION: RegExp;
29
+ /**
30
+ * Each helper's parameters and what it does, keyed by name.
31
+ *
32
+ * The authoring document describes this surface, and a test holds the two
33
+ * together. A consumer reads it to list what an expression may call.
34
+ *
35
+ * @type {Readonly<Record<string, Readonly<{params: string, summary: string}>>>}
36
+ */
37
+ export const EXPRESSION_HELPERS: Readonly<Record<string, Readonly<{
38
+ params: string;
39
+ summary: string;
40
+ }>>>;
@@ -1,10 +1,7 @@
1
1
  /** Return authored keys from an opening YAML frontmatter map. */
2
2
  export function authoredNoteKeys(markdown: any): string[];
3
- /** The complete ordered top-level vocabulary for addressed content notes. */
4
- export const NOTE_TOP_LEVEL_KEYS: readonly string[];
5
- /** Membership test for the top-level vocabulary. */
6
- export const NOTE_TOP_LEVEL_KEY_SET: Set<string>;
7
3
  /** The name properties every note accepts, in display order. */
8
4
  export const NOTE_NAME_KEYS: readonly string[];
9
5
  /** Additional name components accepted by an individual character. */
10
6
  export const CHARACTER_NAME_KEYS: readonly string[];
7
+ export { NOTE_TOP_LEVEL_FIELDS, NOTE_TOP_LEVEL_KEYS, NOTE_TOP_LEVEL_KEY_SET } from "./note-vocabulary.mjs";
@@ -42,8 +42,6 @@ export function exclusiveTagGroups(type: string, groups?: object): {
42
42
  * The spelling of the tag *itself* still is — a near miss is a near miss, and
43
43
  * the frontmatter lint is what reports it; nothing here guesses.
44
44
  *
45
- * Reads `tags` and the singular `tag` spelling.
46
- *
47
45
  * @param {object|null|undefined} fm - Parsed frontmatter.
48
46
  * @param {string} tag - The tag to look for, in its declared spelling.
49
47
  * @returns {boolean} Whether the note carries it.
@@ -135,6 +133,59 @@ export function dataFields(type: string, vocabulary?: Readonly<Record<string, Ty
135
133
  * it has no `subType` at all — see {@link TypeVocabulary}.
136
134
  */
137
135
  export function subTypes(type: string, vocabulary?: Readonly<Record<string, TypeVocabulary>>): readonly string[] | null | undefined;
136
+ /**
137
+ * One top-level frontmatter key.
138
+ *
139
+ * Narrower than a {@link DataFieldSpec}: the top level is a fixed list rather
140
+ * than a per-type vocabulary, nothing there is keyed by pack or resolved as an
141
+ * Address, and the value's shape is checked by whatever owns the region the key
142
+ * opens — `name`, `tags` and each system block each have their own check.
143
+ *
144
+ * @typedef {object} TopLevelFieldSpec
145
+ * @property {string} name - The key, as a note writes it.
146
+ * @property {boolean} [system] - The key opens a game system's block, so it is
147
+ * one of {@link module:engine/systems.SYSTEM_IDS} rather than a fixed key.
148
+ * @property {string} describe - One line, for the author-facing reference.
149
+ */
150
+ /**
151
+ * **The top-level vocabulary**, in the order a formatted note writes it.
152
+ *
153
+ * The region describes the note as a published artefact: what it is called,
154
+ * what kind of thing it is about, how it is classified, and which of the other
155
+ * two regions it opens. Everything else a note states is a fact about the
156
+ * subject, which belongs under `data:`, or a fact about one system's document,
157
+ * which belongs inside that system's block.
158
+ *
159
+ * **Closed, like the other two regions.** A key this list does not name is a
160
+ * finding at its own line, because a region that passed one through would turn
161
+ * a misspelling into a value nothing reads — `title` beside `name`, or `pack`
162
+ * where `data.pack` was meant.
163
+ *
164
+ * **The system blocks are derived from the system registry**, so recognising a
165
+ * further system is a change to {@link module:engine/systems.SYSTEM_IDS} and to
166
+ * nothing else. They sort by name after the fixed keys: a block is the largest
167
+ * thing a note writes, every one of them belongs at the end, and a rule a
168
+ * reader can restate in one sentence is worth more than a hand-kept order.
169
+ *
170
+ * Five readers derive from this one list — the frontmatter lint's accepted set,
171
+ * the message it prints, the formatter's key order, the content-format check,
172
+ * and the author-facing reference — so none of them can fall behind the others.
173
+ *
174
+ * @type {readonly TopLevelFieldSpec[]}
175
+ */
176
+ export const NOTE_TOP_LEVEL_FIELDS: readonly TopLevelFieldSpec[];
177
+ /**
178
+ * The top-level keys, in formatted order.
179
+ *
180
+ * @type {readonly string[]}
181
+ */
182
+ export const NOTE_TOP_LEVEL_KEYS: readonly string[];
183
+ /**
184
+ * Membership test for the top-level vocabulary.
185
+ *
186
+ * @type {ReadonlySet<string>}
187
+ */
188
+ export const NOTE_TOP_LEVEL_KEY_SET: ReadonlySet<string>;
138
189
  /**
139
190
  * The `data:` keys **every** note type accepts, whatever it is.
140
191
  *
@@ -174,15 +225,17 @@ export const GM_TAG: "gm";
174
225
  /**
175
226
  * The tags that **classify** a note, grouped by what they classify.
176
227
  *
177
- * `tags:` lives at the open top level and most tags belong there: a theme, a
178
- * region, a working state is the author's own and this build has no opinion
179
- * about it. A classifying tag is different, because something queries it — a
228
+ * `tags:` holds an open vocabulary of *values*, and most tags belong there: a
229
+ * theme, a region, a working state is the author's own and this build has no
230
+ * opinion about it. A classifying tag is different, because something queries
231
+ * it — a
180
232
  * settlement tagged `village` appears in the list of villages and an untagged
181
233
  * one does not, so `vilage` does not merely look wrong, it removes the note from
182
234
  * an index while the index still renders a table that looks complete.
183
235
  *
184
236
  * **This list is not a closed set.** An unrecognised tag is legal, because the
185
- * region is open; what is reported is a **near miss** — a tag close enough to a
237
+ * values are the author's; what is reported is a **near miss** — a tag close
238
+ * enough to a
186
239
  * declared one to be a typo of it.
187
240
  *
188
241
  * **Each group names the types it applies to**, and that scope is what makes the
@@ -296,15 +349,29 @@ export type DataFieldSpec = {
296
349
  * value — which is the honest answer wherever the specification's stated
297
350
  * shape and the shape notes are authored in today disagree.
298
351
  */
299
- kind?: "string" | "number" | "boolean" | "map" | "address" | "list" | "shortcode" | "scalar-or-map" | undefined;
352
+ kind?: "string" | "number" | "boolean" | "map" | "address" | "list" | "shortcode" | "list-or-map" | "scalar-or-map" | undefined;
300
353
  /**
301
354
  * - Type of each list or pack-map value.
302
355
  */
303
356
  entryKind?: "address" | "shortcode" | undefined;
304
357
  /**
305
- * - Type of each map key.
358
+ * - Type of each map key. On a
359
+ * `list-or-map` field it describes the map form alone, and `entryKind` the
360
+ * list form alone: reading both of one value would check a map's entries as
361
+ * if they were its keys.
306
362
  */
307
363
  keyKind?: "address" | "shortcode" | undefined;
364
+ /**
365
+ * - The map's entries are standings held in the
366
+ * body each is keyed by, so a row names the two together — see
367
+ * {@link module :engine/standings}.
368
+ */
369
+ standings?: boolean | undefined;
370
+ /**
371
+ * - The map names posts, each with a description,
372
+ * so each takes a row labelled and anchored by the post's own name.
373
+ */
374
+ roster?: boolean | undefined;
308
375
  /**
309
376
  * - An alternative `subType:<skill-subtype>` key.
310
377
  */
@@ -404,3 +471,26 @@ export type TypeVocabulary = {
404
471
  index?: object;
405
472
  }) => object[]) | undefined;
406
473
  };
474
+ /**
475
+ * One top-level frontmatter key.
476
+ *
477
+ * Narrower than a {@link DataFieldSpec}: the top level is a fixed list rather
478
+ * than a per-type vocabulary, nothing there is keyed by pack or resolved as an
479
+ * Address, and the value's shape is checked by whatever owns the region the key
480
+ * opens — `name`, `tags` and each system block each have their own check.
481
+ */
482
+ export type TopLevelFieldSpec = {
483
+ /**
484
+ * - The key, as a note writes it.
485
+ */
486
+ name: string;
487
+ /**
488
+ * - The key opens a game system's block, so it is
489
+ * one of {@link module :engine/systems.SYSTEM_IDS} rather than a fixed key.
490
+ */
491
+ system?: boolean | undefined;
492
+ /**
493
+ * - One line, for the author-facing reference.
494
+ */
495
+ describe: string;
496
+ };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Every `pagelist` fence in a markdown body, with the position each occupies.
3
+ *
4
+ * Positions are 0-based lines into the body as given, which is what a
5
+ * diagnostic about a directive needs and what the expander uses to splice
6
+ * results back in.
7
+ *
8
+ * **A malformed directive is still a directive.** It is returned carrying its
9
+ * problems rather than skipped, so the expander finds a prepared answer at its
10
+ * ordinal and reports the fault where the fence sits. That includes a fence
11
+ * nobody closed, which the author meant as a directive and which would
12
+ * otherwise swallow the rest of the note into a code block.
13
+ *
14
+ * @param {string} markdown - The note body, frontmatter already stripped.
15
+ * @returns {Array<{line: number, close: number, indent: string, tag: string,
16
+ * type: string, sort: string, descriptions: boolean, allowEmpty: boolean,
17
+ * problems: string[], block: string}>} One entry per fence, in document order.
18
+ */
19
+ export function findPageListBlocks(markdown: string): Array<{
20
+ line: number;
21
+ close: number;
22
+ indent: string;
23
+ tag: string;
24
+ type: string;
25
+ sort: string;
26
+ descriptions: boolean;
27
+ allowEmpty: boolean;
28
+ problems: string[];
29
+ block: string;
30
+ }>;
31
+ /**
32
+ * The pages a directive selects, as index records.
33
+ *
34
+ * A **stub** is left out: an empty body publishes no page, which the index says
35
+ * by holding no address, so there is nothing for the list to link to. A
36
+ * documentation journal and an asset are not pages an author tagged, which
37
+ * {@link module:engine/index-records.isNoteRecord} already answers.
38
+ *
39
+ * @param {object[]} records - Content-index records.
40
+ * @param {{tag: string, type: string}} directive - From {@link findPageListBlocks}.
41
+ * @returns {object[]} The selected records, unordered.
42
+ */
43
+ export function pagesForList(records: object[], { tag, type }: {
44
+ tag: string;
45
+ type: string;
46
+ }): object[];
47
+ /**
48
+ * Render one directive's answer as markdown.
49
+ *
50
+ * **The emitted link is the page's `address.slug`** — the `type-shortcode`
51
+ * form a wikilink resolves — which is exactly what a table's `_ref` cell
52
+ * writes. A short form names the package the link is written in, so it is
53
+ * unambiguous for a list of this package's own pages, and it resolves to the
54
+ * note's **page** on each surface: a compendium journal in Foundry, a URL on
55
+ * the web and in the book. An item note's page is its documentation journal,
56
+ * and the short form reaches it the way prose does.
57
+ *
58
+ * @param {object[]} records - Content-index records.
59
+ * @param {object} directive - From {@link findPageListBlocks}.
60
+ * @returns {{markdown: string, pages: number}} The list, and how many pages
61
+ * are in it.
62
+ */
63
+ export function renderPageList(records: object[], directive: object): {
64
+ markdown: string;
65
+ pages: number;
66
+ };
67
+ /**
68
+ * Answer every `pagelist` directive in a set of note bodies, ahead of expansion.
69
+ *
70
+ * **Why a separate pass.** The same reason the SQL tables have one:
71
+ * {@link module:engine/content-tables.expandContentTables} is synchronous and
72
+ * exported, and the corpus is enumerated once by whoever already holds it
73
+ * rather than derived again inside a renderer. A page list needs no database —
74
+ * it is a filter over the records — so preparing one costs a walk of what the
75
+ * caller already has.
76
+ *
77
+ * **Keyed by note, then by the directive's ordinal within it**, not by its
78
+ * line: the passes do not agree on what a body is, and trim it differently,
79
+ * but a directive's position in the sequence of fences is the same in each.
80
+ *
81
+ * A malformed directive is recorded rather than thrown, so one bad fence costs
82
+ * its own list and not the whole build's report.
83
+ *
84
+ * @param {object[]} records - Content-index records, already narrowed to the
85
+ * audience the surface publishes to.
86
+ * @param {Array<{source: string, markdown: string}>} sources - The bodies to scan.
87
+ * @returns {Map<string, Array<{markdown?: string, pages?: number, empty?: string,
88
+ * reason?: string, allowEmpty: boolean}>>} Note to results, in document order.
89
+ */
90
+ export function preparePageLists(records: object[], sources: Array<{
91
+ source: string;
92
+ markdown: string;
93
+ }>): Map<string, Array<{
94
+ markdown?: string;
95
+ pages?: number;
96
+ empty?: string;
97
+ reason?: string;
98
+ allowEmpty: boolean;
99
+ }>>;
100
+ /** The fence language that opens a page list. */
101
+ export const PAGE_LIST_LANGUAGE: "pagelist";
102
+ /**
103
+ * The orders a page list may come out in.
104
+ *
105
+ * Ordering is about output rather than about the pages, so it is an attribute
106
+ * rather than something read off a note. `name` is alphabetical, case and
107
+ * accents folded; `type` groups the note types and orders by name inside each.
108
+ *
109
+ * @type {readonly string[]}
110
+ */
111
+ export const PAGE_LIST_SORTS: readonly string[];
112
+ /**
113
+ * The attributes a page-list fence takes, with what each one is for.
114
+ *
115
+ * **This is the one list.** The fence is validated against it, the authoring
116
+ * document's table is generated from it, and the reference is checked against
117
+ * it — so an attribute added here is one both documents describe, and anything
118
+ * else an author writes is reported by name.
119
+ *
120
+ * @type {Readonly<Record<string, Readonly<{value: string, default: string, summary: string}>>>}
121
+ */
122
+ export const PAGE_LIST_ATTRIBUTES: Readonly<Record<string, Readonly<{
123
+ value: string;
124
+ default: string;
125
+ summary: string;
126
+ }>>>;
@@ -37,6 +37,24 @@ export function labelFor(anchor: string): string;
37
37
  * @returns {object} A markdown-it instance.
38
38
  */
39
39
  export function createParser(registry?: object): object;
40
+ /**
41
+ * A markdown body with its HTML comments taken out.
42
+ *
43
+ * A comment is an author's aside to the next author — a `markdownlint` pragma, a
44
+ * note recording why a notice is quoted verbatim — and no surface shows one: the
45
+ * packs and the website emit it into HTML, where it is a comment still. Typst
46
+ * has nowhere to put it, so markdown-it hands the delimiters over as prose and
47
+ * the aside is typeset. It comes out here instead, and a comment inside a fence
48
+ * or a code span stays, because there it is an example of a comment.
49
+ *
50
+ * **The line count is preserved.** Every finding this module reports is located
51
+ * in the markdown it was handed, so a pass that removed a line would move every
52
+ * position after it.
53
+ *
54
+ * @param {string} markdown - The body, as authored.
55
+ * @returns {string} The same body, its comments replaced by their own newlines.
56
+ */
57
+ export function stripHtmlComments(markdown: string): string;
40
58
  /**
41
59
  * Render markdown as Typst content.
42
60
  *
@@ -55,8 +73,26 @@ export function createParser(registry?: object): object;
55
73
  * note's own `##` nests beneath the entry heading the book gave it.
56
74
  * @param {string} [opts.anchorPrefix] - The entry's anchor, which namespaces
57
75
  * every `{#slug}` the body declares.
58
- * capital. Set for an entry, which begins a page; not for front matter or a
59
- * prose file, which carry headings of their own.
76
+ * @param {object[]} [opts.findings] - Collected here rather than thrown. Every
77
+ * construct this pass cannot render is reported into it — see
78
+ * {@link reportUnrenderable} — and a caller that passes none renders the same
79
+ * Typst and is told nothing.
80
+ * @param {string} [opts.file] - The file the markdown came from, named in every
81
+ * finding.
82
+ * @param {number} [opts.bodyLine] - The 1-based file line the body starts on, so
83
+ * a finding's line is the file's rather than the body's.
84
+ * @param {number} [opts.bodyColumn] - The 1-based column the body starts at.
85
+ * @param {Array<{line: number, generated: boolean}>} [opts.lineMap] - From
86
+ * table expansion, mapping each line handed in back to the line an author
87
+ * wrote.
88
+ * @param {boolean} [opts.prepared] - Whether this body has been through the
89
+ * passes that resolve a wikilink, an embed and an inline expression. A note's
90
+ * has; a front-matter file's and a prose file's have not, and markup of theirs
91
+ * left in one is reported rather than typeset in silence.
92
+ * @param {string} [opts.url] - The absolute address this book's pages are served
93
+ * at. Every link the book sets as a URL is resolved against it, because a
94
+ * reader holding a PDF has no page to resolve a path against — see
95
+ * {@link renderLink}.
60
96
  * @returns {string} Typst markup.
61
97
  */
62
98
  export function markdownToTypst(markdown: string, opts?: {
@@ -67,7 +103,41 @@ export function markdownToTypst(markdown: string, opts?: {
67
103
  images?: Map<string, string> | undefined;
68
104
  headingOffset?: number | undefined;
69
105
  anchorPrefix?: string | undefined;
106
+ findings?: object[] | undefined;
107
+ file?: string | undefined;
108
+ bodyLine?: number | undefined;
109
+ bodyColumn?: number | undefined;
110
+ lineMap?: {
111
+ line: number;
112
+ generated: boolean;
113
+ }[] | undefined;
114
+ prepared?: boolean | undefined;
115
+ url?: string | undefined;
70
116
  }): string;
117
+ /**
118
+ * A page address as a reader of the book can follow it.
119
+ *
120
+ * **Every link the book sets as a URL is absolute.** A page's address is written
121
+ * from the root of the site that serves it — `/sohl/skill-guil/` — because on the
122
+ * website that is the shortest form that is right wherever the page is read
123
+ * from. A PDF is read from nowhere: a viewer handed a path has no document to
124
+ * resolve it against, so it either does nothing or looks for a file on the
125
+ * reader's own disk, and a book of a thousand pages carries a thousand links
126
+ * that do neither.
127
+ *
128
+ * So a root-relative address is resolved against the site's own, which is the
129
+ * one absolute address a build already holds. A **dependency's** page keeps its
130
+ * own package prefix, from the registry in
131
+ * {@link module:engine/content-address.PACKAGE_BASE}, so it resolves to the host
132
+ * serving that package — and where the registry names a host of its own the
133
+ * address is already absolute and is left exactly as it is.
134
+ *
135
+ * @param {string} url - The address, as the link pass resolved it.
136
+ * @param {string} [site] - The absolute address this book's pages are served at.
137
+ * @returns {string} An absolute URL, or the address unchanged when there is no
138
+ * site to resolve it against or it is already absolute.
139
+ */
140
+ export function absolutePageUrl(url: string, site?: string): string;
71
141
  /**
72
142
  * The Typst definitions the book's page furniture is drawn with.
73
143
  *
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Check one affiliation's `data.governance.ranks` against the rung contract.
3
+ *
4
+ * Each problem is its own finding, so a rung missing all three of its required
5
+ * keys reports three rather than the first: a reader fixing a ladder wants the
6
+ * whole of it in one pass.
7
+ *
8
+ * **A finding is located by the node it is about**, through the frontmatter's
9
+ * own YAML tree — a present-but-wrong value points at that value, and an absent
10
+ * key points at the rung that should have carried it, since a key that was
11
+ * never written has no position of its own. Where even the rung cannot be
12
+ * located the position is dropped rather than guessed, and the finding names
13
+ * the file alone.
14
+ *
15
+ * @param {object} note - The note being checked.
16
+ * @param {string} note.file - Its path, as a diagnostic spells it.
17
+ * @param {string} [note.raw] - Its source, for the position.
18
+ * @param {object} [note.fm] - Its parsed frontmatter.
19
+ * @returns {Array<{file: string, line?: number, column?: number,
20
+ * severity: string, message: string}>} The findings, in authored order.
21
+ */
22
+ export function checkRankLadder(note: {
23
+ file: string;
24
+ raw?: string | undefined;
25
+ fm?: object | undefined;
26
+ }): Array<{
27
+ file: string;
28
+ line?: number;
29
+ column?: number;
30
+ severity: string;
31
+ message: string;
32
+ }>;
@@ -256,12 +256,10 @@ export function legacyKeyMessage(block: string, field: {
256
256
  * What a note writing a field at the **top-level key `data:` gathered it off**
257
257
  * is told.
258
258
  *
259
- * {@link legacyKeyMessage}'s counterpart for the other retiring position. The
260
- * sweep
261
- * did not invent the facts `data:` holds — it collected them out of the note's
262
- * open top level — so `portrait:` beside `img:` is the *pre-`data:`* spelling
263
- * of `data.portrait`, and both are read for the same reason both in-block
264
- * spellings are: a package moves its corpus when it is ready, not on a flag day.
259
+ * {@link legacyKeyMessage}'s counterpart for the other retiring position.
260
+ * `portrait:` beside `img:` is the bare spelling of `data.portrait`, and both
261
+ * are read for the same reason both in-block spellings are: a package moves its
262
+ * corpus when it is ready, not on a flag day.
265
263
  *
266
264
  * It says nothing about which value is emitted, because that is not what an
267
265
  * author needs from it. The value is the same either way; what the finding
@@ -249,7 +249,7 @@ export function renderSitePage(page: any, { index, foreign, universe, pass, deco
249
249
  artIndex: any;
250
250
  }): {
251
251
  page: any;
252
- body: any;
252
+ body: string;
253
253
  data: object;
254
254
  resolved: any[];
255
255
  tableErrors: object[];
@@ -266,6 +266,18 @@ export function renderSitePage(page: any, { index, foreign, universe, pass, deco
266
266
  column: number;
267
267
  message: string;
268
268
  }[];
269
+ headingErrors: {
270
+ line: number;
271
+ column: number;
272
+ message: string;
273
+ file: any;
274
+ }[];
275
+ footnoteErrors: {
276
+ file: any;
277
+ line: number;
278
+ column: number;
279
+ message: string;
280
+ }[];
269
281
  wikiErrors: any[];
270
282
  imageErrors: any[];
271
283
  };
@@ -178,12 +178,21 @@ export const DISABLE_KINDS: readonly string[];
178
178
  * `<span>` marking an unresolved link — and Goldmark drops raw HTML unless
179
179
  * told otherwise. A theme cannot supply this: Hugo does not merge a theme's
180
180
  * `markup` block.
181
+ *
182
+ * `extensions.taskList` is off because the format has no checkbox: Goldmark
183
+ * enables it by default, which is the only reason a list item opened with
184
+ * `[ ]` renders a `<input type="checkbox">` here and the same two characters as
185
+ * plain text in the pack and the book. Off, a list item's text begins right
186
+ * after its marker on every surface.
181
187
  */
182
188
  export const MARKUP: Readonly<{
183
189
  goldmark: Readonly<{
184
190
  renderer: Readonly<{
185
191
  unsafe: true;
186
192
  }>;
193
+ extensions: Readonly<{
194
+ taskList: false;
195
+ }>;
187
196
  }>;
188
197
  }>;
189
198
  /** Where the navigation is published. */