@heroiclands/package-build 22.22.0 → 22.23.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 (91) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/content-commands.mjs +51 -14
  3. package/ci/content-format-reference.mjs +1 -1
  4. package/content-config.mjs +80 -2
  5. package/docs/api.md +85 -72
  6. package/docs/authoring/assets.md +14 -0
  7. package/docs/authoring/dates-and-calendars.md +37 -1
  8. package/docs/authoring/links-and-markup.md +112 -31
  9. package/docs/commands.md +12 -10
  10. package/docs/configuration.md +90 -13
  11. package/docs/content-format.md +1 -1
  12. package/docs/guides/book.md +5 -4
  13. package/docs/reference/format-details.md +323 -60
  14. package/docs/reference/note-types.md +9 -9
  15. package/engine/anchors.mjs +16 -10
  16. package/engine/art-fields.mjs +118 -0
  17. package/engine/art-slots.mjs +7 -1
  18. package/engine/asset-index.mjs +160 -10
  19. package/engine/asset-replacement.mjs +93 -0
  20. package/engine/asset-types.mjs +240 -0
  21. package/engine/base-compiler.mjs +54 -2
  22. package/engine/bundles.mjs +13 -1
  23. package/engine/content-blocks.mjs +4 -4
  24. package/engine/content-embeds.mjs +19 -0
  25. package/engine/content-figures.mjs +468 -0
  26. package/engine/content-footnotes.mjs +10 -2
  27. package/engine/content-format.yaml +1 -1
  28. package/engine/content-images.mjs +126 -25
  29. package/engine/content-index.mjs +36 -10
  30. package/engine/field-reference.mjs +31 -10
  31. package/engine/field-spec.mjs +10 -8
  32. package/engine/foreign-catalog.mjs +143 -18
  33. package/engine/frontmatter-lint.mjs +73 -1
  34. package/engine/heading-attributes.mjs +31 -18
  35. package/engine/helpers.mjs +97 -30
  36. package/engine/infobox-registry.mjs +4 -1
  37. package/engine/infobox.mjs +47 -8
  38. package/engine/itinerary-scenes.mjs +4 -10
  39. package/engine/journals.mjs +264 -39
  40. package/engine/lore-events.mjs +197 -0
  41. package/engine/markdown-expressions.mjs +257 -7
  42. package/engine/note-dates.mjs +305 -12
  43. package/engine/note-vocabulary.mjs +11 -5
  44. package/engine/pathnames.mjs +36 -0
  45. package/engine/pdf-build.mjs +357 -34
  46. package/engine/pdf-images.mjs +64 -0
  47. package/engine/pdf-render.mjs +160 -36
  48. package/engine/scenes.mjs +16 -4
  49. package/engine/site-build.mjs +127 -13
  50. package/engine/site-index.mjs +72 -1
  51. package/engine/site-preview.mjs +2 -0
  52. package/engine/system-block.mjs +44 -16
  53. package/hm3/actors.mjs +2 -2
  54. package/manifest.mjs +30 -24
  55. package/package.json +1 -1
  56. package/packagebuild-metadata.jsonl +13 -13
  57. package/sohl/item-fields.mjs +4 -3
  58. package/types/content-config.d.mts +48 -1
  59. package/types/engine/anchors.d.mts +9 -4
  60. package/types/engine/art-fields.d.mts +64 -0
  61. package/types/engine/asset-index.d.mts +43 -0
  62. package/types/engine/asset-replacement.d.mts +22 -0
  63. package/types/engine/asset-types.d.mts +18 -0
  64. package/types/engine/base-compiler.d.mts +0 -17
  65. package/types/engine/content-embeds.d.mts +16 -0
  66. package/types/engine/content-figures.d.mts +101 -0
  67. package/types/engine/content-footnotes.d.mts +7 -0
  68. package/types/engine/content-images.d.mts +78 -12
  69. package/types/engine/content-index.d.mts +8 -2
  70. package/types/engine/field-reference.d.mts +21 -0
  71. package/types/engine/field-spec.d.mts +20 -16
  72. package/types/engine/foreign-catalog.d.mts +39 -0
  73. package/types/engine/heading-attributes.d.mts +13 -8
  74. package/types/engine/helpers.d.mts +23 -7
  75. package/types/engine/infobox-registry.d.mts +4 -1
  76. package/types/engine/infobox.d.mts +6 -0
  77. package/types/engine/journals.d.mts +31 -9
  78. package/types/engine/lore-events.d.mts +14 -0
  79. package/types/engine/markdown-expressions.d.mts +23 -2
  80. package/types/engine/note-dates.d.mts +64 -1
  81. package/types/engine/pathnames.d.mts +26 -0
  82. package/types/engine/pdf-build.d.mts +28 -6
  83. package/types/engine/pdf-images.d.mts +52 -0
  84. package/types/engine/pdf-render.d.mts +12 -0
  85. package/types/engine/site-build.d.mts +14 -3
  86. package/types/engine/site-index.d.mts +54 -0
  87. package/types/engine/system-block.d.mts +26 -2
  88. package/types/hm3/actors.d.mts +18 -0
  89. package/types/manifest.d.mts +19 -23
  90. package/engine/content-captions.mjs +0 -230
  91. package/types/engine/content-captions.d.mts +0 -58
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The replacement record for one asset address, or `null`.
3
+ *
4
+ * @param {string} address - A canonical asset address.
5
+ * @param {object} opts
6
+ * @param {string} opts.localPackage - The compiling package's own segment.
7
+ * @param {readonly string[]} opts.replacements - The package segments of
8
+ * every relationship declaring `assetReplacement: true`, in declaration
9
+ * order.
10
+ * @param {Map<string, object>} opts.index - The merged content index, keyed
11
+ * by canonical address.
12
+ * @returns {{record: object, package: string}|null} The first replacement
13
+ * carrying the address, or `null` when none does.
14
+ */
15
+ export function resolveAssetReplacement(address: string, { localPackage, replacements, index }: {
16
+ localPackage: string;
17
+ replacements: readonly string[];
18
+ index: Map<string, object>;
19
+ }): {
20
+ record: object;
21
+ package: string;
22
+ } | null;
@@ -27,6 +27,24 @@ export function assetTypeOfRoot(root: unknown): AssetType | undefined;
27
27
  * @returns {boolean} Whether it matches {@link ADDRESS_SEGMENT_PATTERN}.
28
28
  */
29
29
  export function isAssetShortcode(shortcode: string): boolean;
30
+ /**
31
+ * The pixel dimensions a raster image's own header states.
32
+ *
33
+ * Read from the file's bytes directly rather than through a decoder, so a
34
+ * corrupt or partial file fails closed — the same `undefined` answer as a
35
+ * format this carries no reader for — instead of throwing out of the asset
36
+ * walk. An SVG has no pixel dimensions to read; stating one would be a
37
+ * fabricated viewBox reading rather than a fact about the file.
38
+ *
39
+ * @param {string} absPath - The file's absolute path.
40
+ * @param {string} ext - Its extension, lowercase and dot-led.
41
+ * @returns {{width: number, height: number}|undefined} The declared size, or
42
+ * `undefined` when the format carries none, or the header cannot be read.
43
+ */
44
+ export function imageDimensions(absPath: string, ext: string): {
45
+ width: number;
46
+ height: number;
47
+ } | undefined;
30
48
  /**
31
49
  * The `<system>` segment every asset address carries.
32
50
  *
@@ -1,20 +1,3 @@
1
- /**
2
- * The tallies one pass accumulates while walking the tree.
3
- *
4
- * `declined` and `skippedOther` are deliberately separate numbers. A declined
5
- * note is one this build **refused** — it declares a retired frontmatter field
6
- * — and it is an error; a skipped one legitimately belongs to another pass, and
7
- * there are thousands of those. Folding the first into the second is what let a
8
- * whole tree be filtered out in silence.
9
- *
10
- * @typedef {object} PassStats
11
- * @property {number} compiled - Notes that became a document.
12
- * @property {number} skippedNoId - Notes with no `id`, where that is tolerated.
13
- * @property {number} skippedOther - Notes this pass does not claim.
14
- * @property {number} declined - Notes this pack **refused** — one declaring a
15
- * retired frontmatter field, or one routed to a system pack whose system it
16
- * says nothing about. Counted as errors, never as skips.
17
- */
18
1
  /**
19
2
  * The shared walk → filter → expand → convert → build → write → count loop.
20
3
  *
@@ -45,6 +45,22 @@ export function resolveEmbed(index: object, embed: ParsedEmbed): {
45
45
  target: string;
46
46
  type?: string;
47
47
  };
48
+ /**
49
+ * The role an address's asset declares, read through the same resolution an
50
+ * embed's address takes.
51
+ *
52
+ * This is the lookup a `:::figure` fence's `map` counter reaches through —
53
+ * see {@link module:engine/content-figures.scanFigures}'s `resolveRole` — for
54
+ * an address still in its authored form, before {@link resolveEmbeds} has
55
+ * rewritten it into the ordinary image every surface renders.
56
+ *
57
+ * @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex},
58
+ * or the equivalent the site and the book build.
59
+ * @param {string} address - The address as authored, inside `![[...]]`.
60
+ * @returns {string|undefined} The role, or `undefined` for a note's address,
61
+ * one that resolves nowhere, or a picture that declares none.
62
+ */
63
+ export function embedRole(index: object, address: string): string | undefined;
48
64
  /**
49
65
  * What is wrong with one embed's directive or placement, if anything.
50
66
  *
@@ -0,0 +1,101 @@
1
+ /** Number a sequence of figures with counters shared across documents. */
2
+ export function numberFigures(figures: any, counts: any): any;
3
+ /**
4
+ * Read every `:::figure` fence in a body.
5
+ *
6
+ * SQL fences are expanded before this pass, so their output is a table.
7
+ *
8
+ * @param {string} source - A note body with expanded tables.
9
+ * @param {object} [options] - Options.
10
+ * @param {(address: string) => string|undefined} [options.resolveRole] - From
11
+ * a picture's address to the role its asset declares, so a picture whose
12
+ * asset declares `role: map` counts as `map` rather than `figure`. Omitted,
13
+ * every picture counts as `figure` — the same reading a role other than
14
+ * `map` gets.
15
+ * @returns {{figures: Array<{id: string, slug: string, caption: string,
16
+ * hasCaption: boolean, classes: string[], kind: string, number: number,
17
+ * label: string, line: number, bodyStart: number, bodyEnd: number,
18
+ * captionStart: number, captionEnd: number, close: number}>,
19
+ * errors: Array<{line: number, column: number, message: string}>}}
20
+ */
21
+ export function scanFigures(source: string, { resolveRole }?: {
22
+ resolveRole?: ((address: string) => string | undefined) | undefined;
23
+ }): {
24
+ figures: Array<{
25
+ id: string;
26
+ slug: string;
27
+ caption: string;
28
+ hasCaption: boolean;
29
+ classes: string[];
30
+ kind: string;
31
+ number: number;
32
+ label: string;
33
+ line: number;
34
+ bodyStart: number;
35
+ bodyEnd: number;
36
+ captionStart: number;
37
+ captionEnd: number;
38
+ close: number;
39
+ }>;
40
+ errors: Array<{
41
+ line: number;
42
+ column: number;
43
+ message: string;
44
+ }>;
45
+ };
46
+ /**
47
+ * Render figure fences as HTML, leaving other Markdown untouched.
48
+ *
49
+ * A code, table or prose figure's contents are left as Markdown inside the
50
+ * wrapper, blank-line separated from its tags exactly as
51
+ * {@link module:engine/content-blocks.renderBlocks} leaves a named block's
52
+ * body — so the surrounding render sees it as part of its own document and a
53
+ * footnote reference inside it resolves against the note's own definitions.
54
+ *
55
+ * A figure's contents are rendered here rather than left for a later pass,
56
+ * because a picture-only fence has to become the surface's own `<figure>`
57
+ * markup directly inside the wrapper this function writes; the surrounding
58
+ * render never sees it as Markdown, so a footnote reference inside one is not
59
+ * resolved, the same gap a block's own title carries.
60
+ *
61
+ * @param {string} source - A note body with expanded tables.
62
+ * @param {(markdown: string) => string} [renderMarkdown] - How to render a
63
+ * figure's own contents.
64
+ * @param {Array<{id: string, label: string}>} [numbers] - Labels assigned by a
65
+ * surface that counts across documents, matched by id.
66
+ * @param {object} [options] - Options.
67
+ * @param {(address: string) => string|undefined} [options.resolveRole] - See
68
+ * {@link scanFigures}.
69
+ * @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>}}
70
+ */
71
+ export function renderFigureBlocks(source: string, renderMarkdown?: (markdown: string) => string, numbers?: Array<{
72
+ id: string;
73
+ label: string;
74
+ }>, { resolveRole }?: {
75
+ resolveRole?: ((address: string) => string | undefined) | undefined;
76
+ }): {
77
+ markdown: string;
78
+ errors: Array<{
79
+ line: number;
80
+ column: number;
81
+ message: string;
82
+ }>;
83
+ };
84
+ /** A figure's automatically determined kind, and the word that labels it. */
85
+ export const FIGURE_NAMES: Readonly<{
86
+ code: "Code";
87
+ table: "Table";
88
+ figure: "Figure";
89
+ map: "Map";
90
+ prose: "Prose";
91
+ }>;
92
+ /**
93
+ * The classes a figure takes.
94
+ *
95
+ * `.border` states that the captioned thing is drawn inside a border, and each
96
+ * medium owns what that means. The vocabulary is closed: a class absent from it
97
+ * is a finding naming the class.
98
+ *
99
+ * @type {readonly string[]}
100
+ */
101
+ export const FIGURE_CLASSES: readonly string[];
@@ -81,3 +81,10 @@ export function footnoteFindings(source: string): Array<{
81
81
  column: number;
82
82
  message: string;
83
83
  }>;
84
+ /**
85
+ * A footnote reference's shape, wherever it is written — excluding a
86
+ * definition's own `[^id]:` opener, which this never matches.
87
+ *
88
+ * @type {RegExp}
89
+ */
90
+ export const FOOTNOTE_REFERENCE: RegExp;
@@ -30,16 +30,28 @@ export function parseImageDirective(raw?: string): {
30
30
  problems: string[];
31
31
  };
32
32
  /**
33
- * The classes a figure carries, from a parsed directive.
33
+ * The class a picture's role names, or `""` for no role or one outside the
34
+ * closed set — the same silent-degrade rule every other unrecognised value
35
+ * here gets, since a role this far downstream has already been validated or
36
+ * dropped by the asset record that carries it.
34
37
  *
35
- * @param {{classes?: string[], size?: string, float?: string}} [directive] - As parsed.
38
+ * @param {string} [role] - The role, from the resolved asset record.
39
+ * @returns {string} The class, or `""`.
40
+ */
41
+ export function roleClass(role?: string): string;
42
+ /**
43
+ * The classes a figure carries, from a parsed directive and its asset's role.
44
+ *
45
+ * @param {{classes?: string[], size?: string, float?: string, role?: string}} [directive] -
46
+ * As parsed, plus the role the resolved asset record carries, if any.
36
47
  * @returns {string} A space-separated class list, always naming
37
48
  * {@link IMAGE_FIGURE_CLASS} first.
38
49
  */
39
- export function figureClasses({ classes, size, float }?: {
50
+ export function figureClasses({ classes, size, float, role }?: {
40
51
  classes?: string[];
41
52
  size?: string;
42
53
  float?: string;
54
+ role?: string;
43
55
  }): string;
44
56
  /**
45
57
  * Text going inside an HTML attribute or between tags.
@@ -55,28 +67,57 @@ export function escapeHtml(text: string): string;
55
67
  * function, so the two cannot drift into styling the same directive through
56
68
  * different class names.
57
69
  *
70
+ * **No `<figcaption>` is emitted here.** A `:::figure` fence draws its own
71
+ * label and authored caption around whatever this returns; the alt text stays
72
+ * on the `img` element alone.
73
+ *
74
+ * **The role class and the `width`/`height` attributes are what a stylesheet
75
+ * elsewhere keys a role's size on.** This module states none — the measure a
76
+ * `portrait` fills on a page is a Hugo theme's or a Foundry system's own
77
+ * decision — and emits only what the resolved asset record states: a role
78
+ * class when the picture declares one, and the attributes whenever the
79
+ * picture resolved to an asset record at all, blank for a vector. The
80
+ * attributes reserve the picture's own layout space before it loads, which is
81
+ * what lets a page avoid reflowing as pictures load in.
82
+ *
58
83
  * @param {object} image - The image.
59
84
  * @param {string} image.src - The address, resolved for the surface.
60
- * @param {string} [image.alt] - The alt text, which is also the caption.
85
+ * @param {string} [image.alt] - The alt text, for a reader who cannot see the
86
+ * picture.
61
87
  * @param {string[]} [image.classes] - Width classes, from the directive.
62
88
  * @param {string} [image.size] - Named display size, from the directive.
63
89
  * @param {string} [image.float] - The float position, from the directive.
90
+ * @param {string} [image.role] - The picture's declared role, from the
91
+ * resolved asset record.
92
+ * @param {number|""} [image.width] - The file's own pixel width, from the
93
+ * resolved asset record; `""` for a vector. Omitted entirely when the
94
+ * address resolved to no asset record at all.
95
+ * @param {number|""} [image.height] - As `width`.
64
96
  * @returns {string} The figure, as one HTML block.
65
97
  */
66
- export function imageFigureHtml({ src, alt, classes, size, float }: {
98
+ export function imageFigureHtml({ src, alt, classes, size, float, role, width, height, }: {
67
99
  src: string;
68
100
  alt?: string | undefined;
69
101
  classes?: string[] | undefined;
70
102
  size?: string | undefined;
71
103
  float?: string | undefined;
104
+ role?: string | undefined;
105
+ width?: number | "" | undefined;
106
+ height?: number | "" | undefined;
72
107
  }): string;
73
108
  /**
74
109
  * Whether a match sits alone in its own paragraph.
75
110
  *
76
- * "Alone" is the whole of a block: nothing else on its line, and a blank line
77
- * or the end of the body either side of it. A leading `>` or list marker
78
- * disqualifies it for the same reason a word does — the paragraph it belongs to
79
- * holds something the figure would have to be lifted out of.
111
+ * "Alone" is the whole of a block: nothing else on its line, and a blank line,
112
+ * a body-extension delimiter or the end of the body either side of it. A
113
+ * leading `>` or list marker disqualifies it for the same reason a word does —
114
+ * the paragraph it belongs to holds something the figure would have to be
115
+ * lifted out of.
116
+ *
117
+ * **A footnote reference trailing the match is not "other text."** `[^note]`
118
+ * annotates the picture rather than sitting beside it in prose, the same
119
+ * standing its own directive already has, so `![A ranger](r.webp)[^note]` is
120
+ * still alone.
80
121
  *
81
122
  * @param {string} text - The body the match indexes into.
82
123
  * @param {number} start - Where the match begins.
@@ -127,6 +168,9 @@ export function imageSourcesIn(body: string): string[];
127
168
  * @param {object} [opts]
128
169
  * @param {number} [opts.bodyLine=1] - The 1-based file line the body starts on.
129
170
  * @param {number} [opts.bodyColumn=1] - The 1-based file column it starts at.
171
+ * @param {Array<{line: number, generated: boolean}>} [opts.lineMap] - From
172
+ * table expansion, mapping each line of `body` back to the line an author
173
+ * wrote, for a caller handing in a body whose tables already expanded.
130
174
  * @param {object} [opts.config] - The resolved build configuration. Supplied,
131
175
  * an address is also held to the one surface a pathname can be dead on
132
176
  * without any other pass noticing — see the Foundry address below. Omitted,
@@ -136,9 +180,13 @@ export function imageSourcesIn(body: string): string[];
136
180
  * severity: "error", message: string}>} One finding per defect, in source
137
181
  * order.
138
182
  */
139
- export function checkImages(body: string, file: string, { bodyLine, bodyColumn, config }?: {
183
+ export function checkImages(body: string, file: string, { bodyLine, bodyColumn, lineMap, config }?: {
140
184
  bodyLine?: number | undefined;
141
185
  bodyColumn?: number | undefined;
186
+ lineMap?: {
187
+ line: number;
188
+ generated: boolean;
189
+ }[] | undefined;
142
190
  config?: object | undefined;
143
191
  }): Array<{
144
192
  file: string;
@@ -201,9 +249,18 @@ export function lintContentImages(contentBase: string, { skipDirectories, config
201
249
  * @param {(src: string) => string} [resolveSrc] - Translates an authored
202
250
  * pathname into the address this surface serves. The default is the identity,
203
251
  * for a caller rendering the format rather than publishing it.
252
+ * @param {(src: string) => {type?: string, role?: string, width?: number|"", height?: number|""}|undefined}
253
+ * [lookupAsset] - What the resolved asset record says about the address, as
254
+ * authored — before `resolveSrc` translates it. The default answers nothing,
255
+ * which is what an address resolving to no asset record gets anyway.
204
256
  * @returns {string} The same body, with each block image as a `<figure>`.
205
257
  */
206
- export function renderImageFigures(body: string, resolveSrc?: (src: string) => string): string;
258
+ export function renderImageFigures(body: string, resolveSrc?: (src: string) => string, lookupAsset?: (src: string) => {
259
+ type?: string;
260
+ role?: string;
261
+ width?: number | "";
262
+ height?: number | "";
263
+ } | undefined): string;
207
264
  /**
208
265
  * A markdown-it plugin that reads an image's directive and renders its figure.
209
266
  *
@@ -226,9 +283,18 @@ export function renderImageFigures(body: string, resolveSrc?: (src: string) => s
226
283
  * address into the one this surface serves. Foundry is handed the path inside
227
284
  * the install; a renderer that resolves the address itself — the book stages
228
285
  * its own copy — passes nothing and gets the address as authored.
286
+ * @param {(src: string) => {type?: string, role?: string, width?: number|"", height?: number|""}|undefined}
287
+ * [lookupAsset] - What the resolved asset record says about the address, as
288
+ * authored — before `resolveSrc` runs. The default answers nothing, which is
289
+ * what an address resolving to no asset record gets anyway.
229
290
  * @returns {(md: object) => void} A markdown-it plugin.
230
291
  */
231
- export function imagePlugin(resolveSrc?: (src: string) => string): (md: object) => void;
292
+ export function imagePlugin(resolveSrc?: (src: string) => string, lookupAsset?: (src: string) => {
293
+ type?: string;
294
+ role?: string;
295
+ width?: number | "";
296
+ height?: number | "";
297
+ } | undefined): (md: object) => void;
232
298
  /**
233
299
  * The width classes an image may carry, and what each means to a renderer.
234
300
  *
@@ -103,6 +103,10 @@ export function asciiAliases(aliases: unknown): Array<string>;
103
103
  * entries are derived against.
104
104
  * @param {object} [options.addressContext] - Address resolution context.
105
105
  * @param {object} [options.dateContext] - Calendar conversion context.
106
+ * @param {(address: string) => string|undefined} [options.resolveRole] - From
107
+ * a picture's address to the role its asset declares, so a figure anchor's
108
+ * `name` reads `Map 1` rather than `Figure 1` where it is due — see
109
+ * {@link module:engine/content-figures.scanFigures}.
106
110
  * @returns {Record<string, any>} The record, keys sorted at every depth. A
107
111
  * **stub** — a note with an empty body, on a type an empty body suppresses —
108
112
  * carries `address` and `anchors` as `null`: it publishes no page, so it
@@ -114,7 +118,7 @@ export function asciiAliases(aliases: unknown): Array<string>;
114
118
  * otherwise be overwritten without a word. `file` and, where the file was
115
119
  * read, `position` ride on the error.
116
120
  */
117
- export function buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest, addressContext, dateContext, }: {
121
+ export function buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest, addressContext, dateContext, resolveRole, }: {
118
122
  frontmatter: Record<string, any>;
119
123
  relPath: string;
120
124
  absPath?: string | undefined;
@@ -124,9 +128,10 @@ export function buildIndexRecord({ frontmatter, relPath, absPath, contentPackage
124
128
  manifest?: object | undefined;
125
129
  addressContext?: object | undefined;
126
130
  dateContext?: object | undefined;
131
+ resolveRole?: ((address: string) => string | undefined) | undefined;
127
132
  }): Record<string, any>;
128
133
  /** Derive every index record belonging to one parsed note. */
129
- export function indexRecordsForNote({ frontmatter, body, bodyLine, relPath, absPath, contentPackage, manifest, addressContext, dateContext, }: {
134
+ export function indexRecordsForNote({ frontmatter, body, bodyLine, relPath, absPath, contentPackage, manifest, addressContext, dateContext, resolveRole, }: {
130
135
  frontmatter: any;
131
136
  body: any;
132
137
  bodyLine: any;
@@ -136,6 +141,7 @@ export function indexRecordsForNote({ frontmatter, body, bodyLine, relPath, absP
136
141
  manifest: any;
137
142
  addressContext: any;
138
143
  dateContext: any;
144
+ resolveRole: any;
139
145
  }): object[];
140
146
  /**
141
147
  * Read a content tree into index records, in the order they will be written.
@@ -1,3 +1,24 @@
1
+ /**
2
+ * The one line explaining why a single field's shared top-level fallback is
3
+ * off.
4
+ *
5
+ * **The field's reason stands on its own** — a complete sentence, or more
6
+ * than one — rather than completing a clause the frame starts. A field that
7
+ * collides with a top-level spelling meaning something else can say so
8
+ * ("`weight` is the item's own, independent of the note's."); a field whose
9
+ * top-level spelling is refused outright by the closed vocabulary can say
10
+ * that instead, honestly, which "there it means …" cannot: a refused key
11
+ * carries no meaning to report.
12
+ *
13
+ * Exported so a guard can walk every field declaring `topLevelMeans` across
14
+ * every registry and check that each one composes into a grammatical
15
+ * sentence, without reimplementing the frame it is checking.
16
+ *
17
+ * @param {import("./field-spec.mjs").FieldSpec} field - A field declaring
18
+ * `topLevelMeans`.
19
+ * @returns {string} The composed markdown line.
20
+ */
21
+ export function topLevelExemptionLine(field: import("./field-spec.mjs").FieldSpec): string;
1
22
  /**
2
23
  * Render the per-type item frontmatter reference.
3
24
  *
@@ -213,19 +213,21 @@ export function runtimeOnlyFields(fields: readonly FieldSpec[]): FieldSpec[];
213
213
  * retiring one — so a field can move into `data:` while the corpus catches
214
214
  * up, instead of on a flag day across four repositories. A declaration that
215
215
  * names one is mid-sweep by construction; see {@link readsLegacyKey}.
216
- * @property {string} [topLevelMeans] - **What the note's top-level key of this
217
- * name means instead** — declared only where it means something else, and
218
- * stating it removes the shared top-level position from this field's
219
- * resolution order.
216
+ * @property {string} [topLevelMeans] - **Why the note's top-level key of this
217
+ * name is not this field's source** — declared wherever that key's spelling
218
+ * means something else at the note level, or the closed top-level
219
+ * vocabulary refuses it outright, and stating either removes the shared
220
+ * top-level position from this field's resolution order.
220
221
  *
221
222
  * A field's `name` doubles as its identity and as the shared property it
222
223
  * draws from, which is right wherever the two levels state the same quantity
223
224
  * — `data.weight` is the weight, whoever reads it. It is wrong wherever a
224
- * spelling collides across the two vocabularies. An `affiliation` item's
225
+ * spelling collides across the two vocabularies, or names a key the closed
226
+ * top-level vocabulary does not admit at all. An `affiliation` item's
225
227
  * `system.title` is the style of address an office carries; a note's
226
- * top-level `title` is the note's own heading. Nothing relates them, and
227
- * before this key one silently fed the other, stringifying an authored
228
- * `title: null` into fifteen documents.
228
+ * top-level `title` is refused outright. Nothing relates them, and before
229
+ * this key one silently fed the other, stringifying an authored `title:
230
+ * null` into fifteen documents.
229
231
  *
230
232
  * **The value is the reason**, not a flag with a comment beside it. A boolean
231
233
  * would record the decision and lose the case for it, and the next person
@@ -466,19 +468,21 @@ export type FieldSpec = {
466
468
  */
467
469
  legacyKey?: string | undefined;
468
470
  /**
469
- * - **What the note's top-level key of this
470
- * name means instead** — declared only where it means something else, and
471
- * stating it removes the shared top-level position from this field's
472
- * resolution order.
471
+ * - **Why the note's top-level key of this
472
+ * name is not this field's source** — declared wherever that key's spelling
473
+ * means something else at the note level, or the closed top-level
474
+ * vocabulary refuses it outright, and stating either removes the shared
475
+ * top-level position from this field's resolution order.
473
476
  *
474
477
  * A field's `name` doubles as its identity and as the shared property it
475
478
  * draws from, which is right wherever the two levels state the same quantity
476
479
  * — `data.weight` is the weight, whoever reads it. It is wrong wherever a
477
- * spelling collides across the two vocabularies. An `affiliation` item's
480
+ * spelling collides across the two vocabularies, or names a key the closed
481
+ * top-level vocabulary does not admit at all. An `affiliation` item's
478
482
  * `system.title` is the style of address an office carries; a note's
479
- * top-level `title` is the note's own heading. Nothing relates them, and
480
- * before this key one silently fed the other, stringifying an authored
481
- * `title: null` into fifteen documents.
483
+ * top-level `title` is refused outright. Nothing relates them, and before
484
+ * this key one silently fed the other, stringifying an authored `title:
485
+ * null` into fifteen documents.
482
486
  *
483
487
  * **The value is the reason**, not a flag with a comment beside it. A boolean
484
488
  * would record the decision and lose the case for it, and the next person
@@ -11,6 +11,20 @@ export function itemCatalogRelationships(config: object): Array<{
11
11
  kind: string;
12
12
  verified: string | undefined;
13
13
  }>;
14
+ /**
15
+ * Every declared relationship that opted into supplying its release archive
16
+ * for asset bytes alone, with no item catalogue built from it.
17
+ *
18
+ * @param {object} config - The resolved build configuration.
19
+ * @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
20
+ * The opted-in relationships, in declaration order.
21
+ */
22
+ export function assetArchiveRelationships(config: object): Array<{
23
+ id: string;
24
+ manifest: string;
25
+ kind: string;
26
+ verified: string | undefined;
27
+ }>;
14
28
  /**
15
29
  * The cache directory for one dependency at one version.
16
30
  *
@@ -95,6 +109,26 @@ export function fetchCatalog(config: object, rel: {
95
109
  id: string;
96
110
  manifest: string;
97
111
  }): Promise<string>;
112
+ /**
113
+ * Fetch one dependency's release archive for its asset bytes alone, building
114
+ * no item catalogue from it.
115
+ *
116
+ * Idempotent: a complete cache for the resolved version is left alone. Where a
117
+ * relationship also declares `itemCatalog: true`, {@link fetchCatalog} already
118
+ * unpacked this same archive into this same cache directory, and
119
+ * {@link fetchAllCatalogs} does not call this function for it — the two never
120
+ * download the same bytes twice.
121
+ *
122
+ * @param {object} config - The resolved build configuration.
123
+ * @param {{id: string, manifest: string, verified?: string}} rel - The
124
+ * declared relationship.
125
+ * @returns {Promise<string>} The dependency's cache directory.
126
+ */
127
+ export function fetchAssetArchive(config: object, rel: {
128
+ id: string;
129
+ manifest: string;
130
+ verified?: string;
131
+ }): Promise<string>;
98
132
  /**
99
133
  * Fill the cache from a locally built artifact rather than a release.
100
134
  *
@@ -167,6 +201,11 @@ export function fetchAllMetadata(config: object): Promise<number>;
167
201
  /**
168
202
  * Fetch every opted-in dependency. The `deps fetch` command.
169
203
  *
204
+ * A relationship declaring both `itemCatalog: true` and `assetArchive: true`
205
+ * is fetched once: {@link fetchCatalog} already unpacks the archive on its
206
+ * way to extracting the item catalogue, so this skips the archive-only fetch
207
+ * for any id {@link fetchCatalog} already handled.
208
+ *
170
209
  * @param {object} config - The resolved build configuration.
171
210
  * @returns {Promise<number>} How many dependencies were fetched.
172
211
  */
@@ -31,9 +31,9 @@ export function splitHeadingAttributes(heading: string): {
31
31
  * A Foundry UUID addresses a page and nothing smaller, so a section a link can
32
32
  * reach has to be one: a top-level heading starts a page, and so does any
33
33
  * heading that names an anchor. {@link module:engine/journals.splitPages}
34
- * decides *where* a page begins — a captioned heading and a heading inside a
35
- * fence or a named block are its business — and asks this which headings are
36
- * eligible, so no caller re-decides it.
34
+ * decides *where* a page begins — a heading inside a fence or a named block is
35
+ * its business — and asks this which headings are eligible, so no caller
36
+ * re-decides it.
37
37
  *
38
38
  * @param {{level: number, id: string}} heading - From
39
39
  * {@link scanHeadingAttributes}, or any `{level, id}` pair.
@@ -86,7 +86,7 @@ export function headingAttributesPlugin(md: object): void;
86
86
  * Parse one line as a heading, and say whether it starts a journal page.
87
87
  *
88
88
  * The single reading of that rule. {@link module:engine/journals.splitPages}
89
- * builds its pages from it; `scanBlocks` and `scanCaptions` refuse a heading
89
+ * builds its pages from it; `scanBlocks` and `scanFigures` refuse a heading
90
90
  * that starts a page written where it cannot become one. All three ask this, so
91
91
  * none of them can disagree about what a heading line means or about what starts
92
92
  * a page — and the attribute block comes off here, so a class beside an anchor
@@ -115,15 +115,20 @@ export function parseHeadingLine(line: string): {
115
115
  * builds the pages from this, and the passes that honour a class on a
116
116
  * page-opening heading ask the same question of the same answer.
117
117
  *
118
- * A caption also opens a page, which is `splitPages`' business: a caption is
119
- * found by the caption pass, and a heading it captions is listed here and
120
- * dropped there.
118
+ * A figure fence also opens a page, which is `splitPages`' business: a fence is
119
+ * found by the figure pass, and the page it opens is named for its label.
121
120
  *
122
121
  * @param {string} source - A note's markdown body.
122
+ * @param {ReadonlySet<number>} [blockLines] - The 0-based lines sitting inside
123
+ * a named block, supplied by the caller. A named block is read by
124
+ * {@link module:engine/content-blocks.scanBlocks}, which knows every name in
125
+ * the registry, reads an opener's attribute block, and tracks nesting by
126
+ * depth — so the caller that already holds that answer passes it rather than
127
+ * a second reading of the same lines being kept here.
123
128
  * @returns {Map<number, {level: number, line: number, text: string, id: string,
124
129
  * classes: string[], values: Record<string, string>}>} Keyed by 0-based line.
125
130
  */
126
- export function pageOpenings(source: string): Map<number, {
131
+ export function pageOpenings(source: string, blockLines?: ReadonlySet<number>): Map<number, {
127
132
  level: number;
128
133
  line: number;
129
134
  text: string;
@@ -1,5 +1,21 @@
1
- /** Render a note body with Foundry's named-block markup. */
2
- export function renderFoundryMarkdown(body: any, captions: any, footnoteNumbers: any, docId: any): string;
1
+ /**
2
+ * Render a note body with Foundry's named-block markup.
3
+ *
4
+ * @param {string} body - The body, tables expanded and wikilinks resolved.
5
+ * @param {Array<{id: string, label: string}>} [figures] - Labels assigned by
6
+ * a note-wide scan, matched by id — see
7
+ * {@link module:engine/content-figures.renderFigureBlocks}.
8
+ * @param {Map<string, number>} [footnoteNumbers] - Shared across the note.
9
+ * @param {string} [docId] - Threaded into footnote anchors.
10
+ * @param {(address: string) => string|undefined} [resolveRole] - From a
11
+ * picture's address to the role its asset declares — see
12
+ * {@link module:engine/content-figures.scanFigures}.
13
+ * @returns {string} The rendered HTML.
14
+ */
15
+ export function renderFoundryMarkdown(body: string, figures?: Array<{
16
+ id: string;
17
+ label: string;
18
+ }>, footnoteNumbers?: Map<string, number>, docId?: string, resolveRole?: (address: string) => string | undefined): string;
3
19
  /**
4
20
  * Parses a markdown file with YAML frontmatter.
5
21
  *
@@ -206,11 +222,11 @@ export function makeFilename(name: string, id: string): string;
206
222
  * the other.
207
223
  *
208
224
  * **`title` does not follow this rule**, and must not be made to. On a
209
- * `type: affiliation` note `title` is *also* a declared item field whose default
210
- * is `""` (`sohl/item-fields.mjs`), resolved from the very same shared top-level
211
- * key the site emitter reads as the page title — so `title: null` stringifies
212
- * into the compiled document as the literal `"null"`. One key, two destinations
213
- * that disagree about what empty means.
225
+ * `type: affiliation` note `title` is a declared item field with its own
226
+ * default (`""`, in `sohl/item-fields.mjs`), authored only inside the item's
227
+ * own `sohl` block — the closed top-level vocabulary has no `title` entry, so
228
+ * there is no shared key for this function's null-versus-blank distinction to
229
+ * apply to.
214
230
  *
215
231
  * **`banner:` does not follow it either, deliberately.** It is not a file
216
232
  * inside a Foundry install: it reaches no compiled document and no book, its