@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
@@ -267,29 +267,25 @@ export const ARTIFACTS: readonly string[];
267
267
  */
268
268
  export const HOMEPAGE_ORIGIN: string;
269
269
  /**
270
- * Relationship keys that direct the **build**, rather than describe the
271
- * package.
272
- *
273
- * `relationships` is the one manifest block with a second reader.
274
- * `@heroiclands/package-build` consumes it too, and v1.8.0 added
275
- * `itemCatalog: true` as an opt-in on a declared dependency: it selects that
276
- * package's Item packs as a resolution
277
- * source for the actors pass. That is an instruction to the build, not a fact
278
- * about the shipped package — Foundry's relationship schema does not define
279
- * it, and someone reading a published manifest cannot tell a build directive
280
- * from a declaration about what the package needs.
281
- *
282
- * So the block is filtered rather than copied whole. The rule is the
283
- * distinction, not the name: a key listed here answers *how is this built?*,
284
- * and every key that survives answers *what does this package depend on?*.
285
- * `itemCatalog` is the first build-time key to land on a relationship and is
286
- * unlikely to be the last.
287
- *
288
- * A list is enough, and needs no prefix agreed between the two packages,
289
- * because the input is already closed: package-build normalises a relationship
290
- * to `id`, `type`, `manifest`, `compatibility` and its own build keys, and
291
- * rejects anything else at configuration time. A key that reaches here is one
292
- * the toolchain itself put there.
270
+ * The relationship keys Foundry's own schema names.
271
+ *
272
+ * `RelatedPackage` declares exactly these (`common/packages/base-package.mjs`),
273
+ * so a key outside them is one the build put there for itself.
274
+ *
275
+ * @type {readonly string[]}
276
+ */
277
+ export const FOUNDRY_RELATIONSHIP_KEYS: readonly string[];
278
+ /**
279
+ * The relationship keys the published manifest drops.
280
+ *
281
+ * Derived rather than listed: every key this package accepts on a relationship
282
+ * that Foundry's schema does not name is one the build reads for itself, so a
283
+ * key added to the configuration is dropped from the manifest without a second
284
+ * list being kept in step.
285
+ *
286
+ * The filter stays a denylist for the reason the pass-through below gives — a
287
+ * key Foundry adds later reaches the manifest unread rather than being stripped
288
+ * by an allowlist that has not heard of it.
293
289
  *
294
290
  * @type {readonly string[]}
295
291
  */
@@ -1,230 +0,0 @@
1
- /*
2
- * SPDX-License-Identifier: GPL-3.0-or-later
3
- */
4
-
5
- import MarkdownIt from "markdown-it";
6
- import footnotePlugin from "markdown-it-footnote";
7
- import deflistPlugin from "markdown-it-deflist";
8
-
9
- import { slugify } from "./content-slug.mjs";
10
- import { parseExtensionAttributes } from "./extension-attributes.mjs";
11
- import { HEADING_LINE, parseHeadingLine, splitHeadingAttributes } from "./heading-attributes.mjs";
12
- import { imagesIn } from "./content-images.mjs";
13
-
14
- const parser = new MarkdownIt({ html: true }).use(footnotePlugin).use(deflistPlugin);
15
- const OPEN = /^:::caption\s+(\{[^}\n]*\})\s*$/;
16
- const CAPTION_LINE = /^:::caption\b/;
17
- const CLOSE = /^:::\s*$/;
18
-
19
- /** A caption's automatically determined kind. */
20
- export const CAPTION_NAMES = Object.freeze({
21
- code: "Code",
22
- table: "Table",
23
- figure: "Figure",
24
- prose: "Prose",
25
- });
26
-
27
- /** Number a sequence of captions with counters shared across documents. */
28
- export function numberCaptions(captions, counts) {
29
- return captions.map((caption) => {
30
- const number = (counts[caption.kind] ?? 0) + 1;
31
- counts[caption.kind] = number;
32
- return { ...caption, number, label: `${CAPTION_NAMES[caption.kind]} ${number}` };
33
- });
34
- }
35
-
36
- /**
37
- * Read caption directives and the Markdown block immediately after each one.
38
- * SQL fences are expanded before this pass, so their output is a table.
39
- *
40
- * @param {string} source - A note body with expanded tables.
41
- * @returns {{captions: Array<{id: string, text: string, kind: string, number: number,
42
- * label: string, line: number, blockStart: number, blockEnd: number,
43
- * directiveEnd: number}>, errors: Array<{line: number, column: number, message: string}>}}
44
- */
45
- export function scanCaptions(source) {
46
- const lines = String(source ?? "").split("\n");
47
- const captions = [];
48
- const errors = [];
49
- const counts = { code: 0, table: 0, figure: 0, prose: 0 };
50
- const ids = new Set();
51
- const headingIds = [];
52
- let codeFence = null;
53
-
54
- for (let i = 0; i < lines.length; i++) {
55
- const line = lines[i];
56
- const fence = /^ {0,3}(`{3,}|~{3,})/.exec(line);
57
- if (codeFence) {
58
- if (fence && fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length)
59
- codeFence = null;
60
- continue;
61
- }
62
- if (fence) {
63
- codeFence = fence[1];
64
- continue;
65
- }
66
- const heading = HEADING_LINE.exec(line);
67
- const headingId = heading ? splitHeadingAttributes(heading[2]).id : "";
68
- if (headingId) headingIds.push({ id: slugify(headingId), line: i + 1 });
69
- if (!CAPTION_LINE.test(line)) continue;
70
- const open = OPEN.exec(line);
71
- if (!open) {
72
- errors.push({ line: i + 1, column: 1, message: "caption needs {#anchor} attributes" });
73
- continue;
74
- }
75
- const attributes = parseExtensionAttributes(open[1].slice(1, -1));
76
- if (
77
- attributes.problems.length ||
78
- !attributes.id ||
79
- attributes.classes.length ||
80
- Object.keys(attributes.values).length
81
- ) {
82
- errors.push({
83
- line: i + 1,
84
- column: 1,
85
- message: attributes.problems[0] ?? "caption accepts only {#anchor}",
86
- });
87
- continue;
88
- }
89
- const id = attributes.id;
90
- const start = i;
91
- let close = i + 1;
92
- while (close < lines.length && !CLOSE.test(lines[close])) close++;
93
- if (close === lines.length) {
94
- errors.push({ line: start + 1, column: 1, message: "caption needs a closing :::" });
95
- continue;
96
- }
97
- const caption = lines
98
- .slice(i + 1, close)
99
- .join("\n")
100
- .trim();
101
- if (!caption) errors.push({ line: start + 1, column: 1, message: "caption text is empty" });
102
- // An H1, or an anchored heading at any level, starts a Foundry
103
- // journal page — see `engine/content-blocks.mjs`'s identical refusal
104
- // for a named block.
105
- for (let at = i + 1; at < close; at++) {
106
- if (!parseHeadingLine(lines[at])?.startsPage) continue;
107
- errors.push({
108
- line: at + 1,
109
- column: 1,
110
- message:
111
- "a heading that starts a page cannot be written inside a caption — " +
112
- "keep an H1 or an anchored heading at the top level, or drop the " +
113
- "anchor and the level to stay inside it",
114
- });
115
- }
116
- if (ids.has(slugify(id)))
117
- errors.push({ line: start + 1, column: 1, message: `duplicate caption id "${id}"` });
118
- ids.add(slugify(id));
119
-
120
- let next = close + 1;
121
- while (next < lines.length && !lines[next].trim()) next++;
122
- if (next === lines.length || CAPTION_LINE.test(lines[next])) {
123
- errors.push({ line: start + 1, column: 1, message: "caption needs a following block" });
124
- i = close;
125
- continue;
126
- }
127
- const tokens = parser.parse(lines.slice(next).join("\n"), {});
128
- const first = tokens.find((token) => token.level === 0 && token.map);
129
- if (!first) {
130
- errors.push({ line: start + 1, column: 1, message: "caption needs a following block" });
131
- i = close;
132
- continue;
133
- }
134
- const blockEnd = next + first.map[1];
135
- const block = lines.slice(next, blockEnd).join("\n");
136
- const trimmedBlock = block.trim();
137
- // A markdown image is only a figure when it stands alone — nothing
138
- // else in the block, which is also what decides whether the image
139
- // plugin wraps it in a `<figure>` at all. A trailing footnote
140
- // reference, or any other trailing text, disqualifies it exactly as
141
- // it disqualifies `standsAlone`; such a block is prose with an inline
142
- // image in it, left as Markdown so a reference inside resolves
143
- // rather than classified as a figure whose own render strips nothing.
144
- const aloneImage =
145
- /^(?:!\[|!\[\[)/.test(trimmedBlock) &&
146
- imagesIn(trimmedBlock).some((image) => image.block);
147
- const kind =
148
- first.type === "table_open" ? "table"
149
- : first.type === "fence" && /^\s*sql\b/i.test(first.info ?? "") ? "table"
150
- : first.type === "fence" || first.type === "code_block" ? "code"
151
- : aloneImage || /^(?:<figure\b|<img\b)/.test(trimmedBlock) ? "figure"
152
- : "prose";
153
- const number = ++counts[kind];
154
- captions.push({
155
- id,
156
- text: caption,
157
- kind,
158
- number,
159
- label: `${CAPTION_NAMES[kind]} ${number}`,
160
- line: start + 1,
161
- blockStart: next,
162
- blockEnd,
163
- directiveEnd: close + 1,
164
- });
165
- i = close;
166
- }
167
- for (const heading of headingIds) {
168
- if (ids.has(heading.id))
169
- errors.push({
170
- line: heading.line,
171
- column: 1,
172
- message: `heading and caption declare the same anchor "${heading.id}"`,
173
- });
174
- }
175
- return { captions, errors };
176
- }
177
-
178
- /**
179
- * Render captioned blocks as HTML, leaving other Markdown untouched.
180
- *
181
- * A code, table or prose caption's block is left as Markdown inside the
182
- * wrapper, blank-line separated from its tags exactly as
183
- * {@link module:engine/content-blocks.renderBlocks} leaves a named block's
184
- * body — so the surrounding render sees it as part of its own document and a
185
- * footnote reference inside it resolves against the note's own definitions.
186
- *
187
- * A figure's block is still rendered here, because its `<figcaption>` has to
188
- * be stripped from HTML this pass controls directly; the surrounding render
189
- * never sees it as Markdown, so a footnote reference inside a figure caption
190
- * is not resolved, the same gap a block's own title carries.
191
- */
192
- export function renderCaptionBlocks(source, renderMarkdown = parser.render.bind(parser), numbers) {
193
- const { captions, errors } = scanCaptions(source);
194
- if (errors.length) return { markdown: source, errors };
195
- const byId = numbers ? new Map(numbers.map((entry) => [entry.id, entry])) : new Map();
196
- const lines = String(source ?? "").split("\n");
197
- const output = [];
198
- let cursor = 0;
199
- const escape = (value) =>
200
- String(value)
201
- .replace(/&/g, "&amp;")
202
- .replace(/</g, "&lt;")
203
- .replace(/>/g, "&gt;")
204
- .replace(/"/g, "&quot;");
205
- for (const caption of captions) {
206
- output.push(...lines.slice(cursor, caption.line - 1));
207
- const numbered = byId.get(caption.id) ?? caption;
208
- const block = lines.slice(caption.blockStart, caption.blockEnd).join("\n");
209
- const captionHtml = parser.renderInline(caption.text);
210
- output.push(
211
- `<div id="${escape(slugify(caption.id))}" class="content-caption content-caption-${caption.kind}">`,
212
- );
213
- if (caption.kind === "figure") {
214
- const html = renderMarkdown(block).replace(
215
- /<figcaption\b[^>]*>[\s\S]*?<\/figcaption>/i,
216
- "",
217
- );
218
- output.push(html.trim());
219
- } else {
220
- output.push("", block, "");
221
- }
222
- output.push(
223
- `<p class="content-caption-label">${escape(numbered.label)}: ${captionHtml}</p>`,
224
- "</div>",
225
- );
226
- cursor = caption.blockEnd;
227
- }
228
- output.push(...lines.slice(cursor));
229
- return { markdown: output.join("\n"), errors: [] };
230
- }
@@ -1,58 +0,0 @@
1
- /** Number a sequence of captions with counters shared across documents. */
2
- export function numberCaptions(captions: any, counts: any): any;
3
- /**
4
- * Read caption directives and the Markdown block immediately after each one.
5
- * SQL fences are expanded before this pass, so their output is a table.
6
- *
7
- * @param {string} source - A note body with expanded tables.
8
- * @returns {{captions: Array<{id: string, text: string, kind: string, number: number,
9
- * label: string, line: number, blockStart: number, blockEnd: number,
10
- * directiveEnd: number}>, errors: Array<{line: number, column: number, message: string}>}}
11
- */
12
- export function scanCaptions(source: string): {
13
- captions: Array<{
14
- id: string;
15
- text: string;
16
- kind: string;
17
- number: number;
18
- label: string;
19
- line: number;
20
- blockStart: number;
21
- blockEnd: number;
22
- directiveEnd: number;
23
- }>;
24
- errors: Array<{
25
- line: number;
26
- column: number;
27
- message: string;
28
- }>;
29
- };
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
- */
44
- export function renderCaptionBlocks(source: any, renderMarkdown: ((src: string, env?: import("markdown-it").Env) => string) | undefined, numbers: any): {
45
- markdown: any;
46
- errors: {
47
- line: number;
48
- column: number;
49
- message: string;
50
- }[];
51
- };
52
- /** A caption's automatically determined kind. */
53
- export const CAPTION_NAMES: Readonly<{
54
- code: "Code";
55
- table: "Table";
56
- figure: "Figure";
57
- prose: "Prose";
58
- }>;