@heroiclands/package-build 22.22.0 → 22.24.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 (140) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -0
  3. package/bin/content-commands.mjs +51 -14
  4. package/ci/content-format-reference.mjs +1 -1
  5. package/content-config.mjs +80 -2
  6. package/docs/api.md +85 -72
  7. package/docs/authoring/assets.md +29 -0
  8. package/docs/authoring/dates-and-calendars.md +37 -1
  9. package/docs/authoring/links-and-markup.md +112 -31
  10. package/docs/commands.md +30 -29
  11. package/docs/configuration.md +92 -19
  12. package/docs/content-format.md +1 -1
  13. package/docs/guides/book.md +5 -4
  14. package/docs/guides/module-project.md +2 -2
  15. package/docs/guides/site.md +41 -0
  16. package/docs/project-setup.md +4 -4
  17. package/docs/reference/format-details.md +323 -60
  18. package/docs/reference/note-types.md +9 -9
  19. package/docs/reference/theme-pages.md +496 -0
  20. package/engine/anchors.mjs +16 -10
  21. package/engine/art-fields.mjs +118 -0
  22. package/engine/art-slots.mjs +9 -2
  23. package/engine/asset-index.mjs +160 -10
  24. package/engine/asset-replacement.mjs +93 -0
  25. package/engine/asset-types.mjs +240 -0
  26. package/engine/base-compiler.mjs +54 -2
  27. package/engine/bundles.mjs +13 -1
  28. package/engine/content-blocks.mjs +4 -4
  29. package/engine/content-embeds.mjs +19 -0
  30. package/engine/content-figures.mjs +468 -0
  31. package/engine/content-footnotes.mjs +10 -2
  32. package/engine/content-format.yaml +1 -1
  33. package/engine/content-images.mjs +126 -25
  34. package/engine/content-index.mjs +36 -10
  35. package/engine/field-reference.mjs +31 -10
  36. package/engine/field-spec.mjs +10 -8
  37. package/engine/foreign-catalog.mjs +145 -19
  38. package/engine/frontmatter-lint.mjs +73 -1
  39. package/engine/heading-attributes.mjs +31 -18
  40. package/engine/helpers.mjs +102 -33
  41. package/engine/infobox-registry.mjs +4 -1
  42. package/engine/infobox.mjs +47 -8
  43. package/engine/itinerary-scenes.mjs +4 -10
  44. package/engine/journals.mjs +264 -39
  45. package/engine/lore-events.mjs +197 -0
  46. package/engine/markdown-expressions.mjs +257 -7
  47. package/engine/note-dates.mjs +305 -12
  48. package/engine/note-vocabulary.mjs +11 -5
  49. package/engine/pathnames.mjs +36 -0
  50. package/engine/pdf-build.mjs +357 -34
  51. package/engine/pdf-images.mjs +64 -0
  52. package/engine/pdf-render.mjs +160 -36
  53. package/engine/scenes.mjs +16 -4
  54. package/engine/site-build.mjs +127 -13
  55. package/engine/site-config.mjs +19 -24
  56. package/engine/site-index.mjs +72 -1
  57. package/engine/site-preview.mjs +2 -0
  58. package/engine/system-block.mjs +44 -16
  59. package/hm3/actors.mjs +2 -2
  60. package/hugo-theme/data/banners.yaml +69 -0
  61. package/hugo-theme/layouts/404.html +85 -0
  62. package/hugo-theme/layouts/_default/baseof.html +64 -0
  63. package/hugo-theme/layouts/_default/list.html +139 -0
  64. package/hugo-theme/layouts/_default/single.html +4 -0
  65. package/hugo-theme/layouts/blog/list.html +54 -0
  66. package/hugo-theme/layouts/blog-post/single.html +36 -0
  67. package/hugo-theme/layouts/index.html +75 -0
  68. package/hugo-theme/layouts/partials/banner-url.html +117 -0
  69. package/hugo-theme/layouts/partials/cdn-url.html +36 -0
  70. package/hugo-theme/layouts/partials/draft-notice.html +41 -0
  71. package/hugo-theme/layouts/partials/draft-state.html +26 -0
  72. package/hugo-theme/layouts/partials/footer.html +29 -0
  73. package/hugo-theme/layouts/partials/header.html +35 -0
  74. package/hugo-theme/layouts/partials/hero-banner.html +86 -0
  75. package/hugo-theme/layouts/partials/holdings/entry.html +21 -0
  76. package/hugo-theme/layouts/partials/holdings/kind-groups.html +71 -0
  77. package/hugo-theme/layouts/partials/holdings.html +61 -0
  78. package/hugo-theme/layouts/partials/infobox/entry.html +15 -0
  79. package/hugo-theme/layouts/partials/infobox/items.html +26 -0
  80. package/hugo-theme/layouts/partials/infobox/section.html +56 -0
  81. package/hugo-theme/layouts/partials/infobox/value.html +43 -0
  82. package/hugo-theme/layouts/partials/infobox.html +57 -0
  83. package/hugo-theme/layouts/partials/page-body.html +154 -0
  84. package/hugo-theme/layouts/partials/place-map.html +9 -0
  85. package/hugo-theme/layouts/partials/related.html +121 -0
  86. package/hugo-theme/layouts/partials/search.html +84 -0
  87. package/hugo-theme/layouts/partials/site-url.html +32 -0
  88. package/hugo-theme/layouts/partials/tags.html +30 -0
  89. package/hugo-theme/layouts/partials/toc.html +28 -0
  90. package/hugo-theme/layouts/partials/warn-missing-banner.html +3 -0
  91. package/hugo-theme/layouts/shortcodes/cartouche.html +64 -0
  92. package/hugo-theme/layouts/shortcodes/exturl.html +12 -0
  93. package/hugo-theme/layouts/shortcodes/glyph.html +43 -0
  94. package/hugo-theme/layouts/shortcodes/pagelist.html +63 -0
  95. package/hugo-theme/static/css/sohl-icons.css +57 -0
  96. package/hugo-theme/static/css/style.css +2953 -0
  97. package/hugo-theme/static/fonts/game-icons-kb.woff2 +0 -0
  98. package/hugo-theme/static/img/banners/takheperu-hieroglyphs.svg +1 -0
  99. package/hugo-theme/static/img/cartouches/takheperu.svg +1 -0
  100. package/hugo-theme/theme.toml +11 -0
  101. package/manifest.mjs +30 -24
  102. package/package.json +2 -1
  103. package/packagebuild-metadata.jsonl +13 -13
  104. package/sohl/item-fields.mjs +4 -3
  105. package/types/content-config.d.mts +48 -1
  106. package/types/engine/anchors.d.mts +9 -4
  107. package/types/engine/art-fields.d.mts +64 -0
  108. package/types/engine/art-slots.d.mts +2 -1
  109. package/types/engine/asset-index.d.mts +43 -0
  110. package/types/engine/asset-replacement.d.mts +22 -0
  111. package/types/engine/asset-types.d.mts +18 -0
  112. package/types/engine/base-compiler.d.mts +0 -17
  113. package/types/engine/content-embeds.d.mts +16 -0
  114. package/types/engine/content-figures.d.mts +101 -0
  115. package/types/engine/content-footnotes.d.mts +7 -0
  116. package/types/engine/content-images.d.mts +78 -12
  117. package/types/engine/content-index.d.mts +8 -2
  118. package/types/engine/field-reference.d.mts +21 -0
  119. package/types/engine/field-spec.d.mts +20 -16
  120. package/types/engine/foreign-catalog.d.mts +39 -0
  121. package/types/engine/heading-attributes.d.mts +13 -8
  122. package/types/engine/helpers.d.mts +28 -10
  123. package/types/engine/infobox-registry.d.mts +4 -1
  124. package/types/engine/infobox.d.mts +6 -0
  125. package/types/engine/journals.d.mts +31 -9
  126. package/types/engine/lore-events.d.mts +14 -0
  127. package/types/engine/markdown-expressions.d.mts +23 -2
  128. package/types/engine/note-dates.d.mts +64 -1
  129. package/types/engine/pathnames.d.mts +26 -0
  130. package/types/engine/pdf-build.d.mts +28 -6
  131. package/types/engine/pdf-images.d.mts +52 -0
  132. package/types/engine/pdf-render.d.mts +12 -0
  133. package/types/engine/site-build.d.mts +14 -3
  134. package/types/engine/site-config.d.mts +9 -9
  135. package/types/engine/site-index.d.mts +54 -0
  136. package/types/engine/system-block.d.mts +26 -2
  137. package/types/hm3/actors.d.mts +18 -0
  138. package/types/manifest.d.mts +19 -23
  139. package/engine/content-captions.mjs +0 -230
  140. package/types/engine/content-captions.d.mts +0 -58
@@ -37,7 +37,9 @@ The harbor faces west.
37
37
  Travelers present their papers here. See [[#harbor|the harbor]].
38
38
  ```
39
39
 
40
- A `:::caption` block starts a JournalEntryPage too, named for the caption's own label, in Foundry alone — the web and the book keep the caption in the flow of the page around it. The prose introducing a captioned block and the block itself land on two different Foundry pages, so a reference such as `[[#trade|]]` written just above a caption sends a Foundry reader to a different page than the one carrying the sentence. Give the introducing prose its own heading, or accept that following the reference leaves the page, before relying on a caption to sit inside a longer page's flow.
40
+ A `:::figure` fence starts a JournalEntryPage too, in Foundry alone — the web and the book keep the fence in the flow of the page around it. A fence holding exactly one picture, captioned with plain text and nothing else, becomes a page of type `image`: the picture is its `src`, the caption is `image.caption`, and the page is named for the figure's own number, such as "Figure 3". A fence carrying no caption still takes the `image` type when it holds one picture, named the same way. Every other fence takes a page of type `text` instead — a grouped fence holding more than one picture, an audio embed, a fence whose caption carries emphasis, an icon, or a link, and a fence that shares its page with trailing prose before the next heading or fence.
41
+
42
+ The prose introducing a fence and the fence itself land on two different Foundry pages, so a reference such as `{{ref "#trade"}}` written just above a fence sends a Foundry reader to a different page than the one carrying the sentence. Give the introducing prose its own heading, or accept that following the reference leaves the page, before relying on a fence to sit inside a longer page's flow.
41
43
 
42
44
  A heading's braces hold an attribute block, not an anchor alone: `#slug` names the anchor, `.class` adds a CSS class, and `key="value"` writes an attribute. The website and a Foundry journal carry all three onto the heading; a book keeps the anchor and drops the rest, because Typst has no stylesheet to name a class in. Braces holding anything but an attribute block — `# The set {a, b}` — are reported at the heading's line and set as written.
43
45
 
@@ -61,23 +63,98 @@ A `:::secret` block inside the section is legal and renders as it does anywhere
61
63
 
62
64
  The special Actor anchors `{#appearance}` and `{#dossier}` feed the same authored sections to each system's Actor fields, and take an H1 only — not the lower-level heading a journal page may otherwise start from. The section runs to the next H1, nested headings included. An anchor declared on a lower heading is still found, and is a build error rather than a quietly empty field: move the heading to the top level, or drop the anchor if the section is meant to stay unaddressed prose.
63
65
 
64
- ## Captions and numbered references
66
+ ## Figures and numbered references
65
67
 
66
- A caption fence labels the next Markdown block. Give it an ID so prose can refer to the block without hard-coding its number:
68
+ A `:::figure` fence numbers whatever it holds and, written with an id, makes it a target `{{ref}}` can point to:
67
69
 
68
70
  ````markdown
69
- Refer to [[#trade|]] for the market routes.
70
-
71
- :::caption {#trade}
72
- Regional trade routes
73
- :::
71
+ :::figure {#trade}
74
72
 
75
73
  ```sql
76
74
  SELECT name.full AS "Market" FROM notes WHERE type = 'place'
77
75
  ```
76
+
77
+ ///
78
+ Regional trade routes
79
+ :::
80
+
81
+ Refer to {{ref "#trade"}} for the market routes.
78
82
  ````
79
83
 
80
- The table displays **Table 1: Regional trade routes**, and the reference displays **Table 1**. The number is assigned by the output. An explicit link label, such as `[[#trade|the trade table]]`, displays the written label. A caption can precede a code fence, table, image, or any prose block. An ordinary code fence is code; an SQL result is a table; an image is a figure; every other block is prose. Web pages and Foundry journals number each kind within the note. Books number each kind across the book in reading order. A caption needs a unique ID, nonempty text, a closing `:::`, and a following block.
84
+ The fence displays **Table 1: Regional trade routes**, and `{{ref "#trade"}}` displays **Table 1** as a link. **A figure is numbered because it is a fence, not because it carries caption text** — an ordinary table or picture written outside any fence is numbered not at all.
85
+
86
+ **The construct is `:::figure`**, with an optional `{#id .border}` attribute block, closed by `:::`. Everything between the opening line and the closing `:::` is the captioned thing.
87
+
88
+ **The first `///` at the top level of the fence separates the captioned thing from its caption.** A second `///` at the top level is an error — a figure holds one caption. A `///` written inside a nested code fence is content, not a separator, so a captioned code listing can carry `///` as a comment of its own.
89
+
90
+ **The caption is optional.** A fence with no `///` draws its label alone, with nothing under it. So the counter and the label come from the fence itself, whether or not an author writes a caption.
91
+
92
+ **`{#id}` is optional too.** A fence that declares none is still numbered and drawn; it carries no anchor, so nothing can refer to it. Give a figure an id only when prose needs to point at it.
93
+
94
+ **`.border` draws the figure and its label inside a border.** It is the only class the construct accepts — any other class, such as `.wide`, is an error naming the class. `key="value"` is refused outright: a figure takes an id and classes, nothing else.
95
+
96
+ **The counter is derived from the fence's contents, never authored.** A table is a table; an `sql` fence is a table, because its rendered output is one; any other fence or code block is code; pictures alone, one or several, are a figure; everything else is prose. A fence around prose with `.border` is a numbered, referable boxed aside — the `Prose` counter's whole purpose. The counters are `Code`, `Table`, `Figure`, `Map`, and `Prose`. The website and a Foundry journal number each counter within the note; a book numbers each counter across the book in reading order.
97
+
98
+ **A picture counts as `Map` instead of `Figure` when the asset it draws declares `role: map`** in its provenance — see [the asset provenance reference](../reference/format-details.md). The fence's own markup cannot say so; a picture of a map looks like any other picture, so the fact rides entirely on the asset's own record. A fence grouping several pictures counts as `Map` only when every one of them declares the role; one picture without it keeps the whole fence a `Figure`.
99
+
100
+ **A reference is written `{{ref "#thorn"}}`**, rendering as a link reading "Figure 13". `form="full"` renders the number and the caption; `form="title"` renders the caption alone; `form="number"` is the default, and may be written explicitly. A link written inside the caption contributes only its label text to a `full` or `title` reference, because the reference is itself a link and an anchor cannot nest inside another.
101
+
102
+ **A reference may name another note's figure**, writing the note's address before the anchor: `{{ref "place-thornford#trade"}}` points at the figure `#trade` in `place-thornford`. It renders the number that figure carries **on the surface doing the rendering** — its own count within `place-thornford` on the website and in a Foundry journal, and its place in the book's reading order in the book, which may differ from the number it carries on its own page. An anchor matching no figure in the addressed note is refused, and so is an address naming no note at all.
103
+
104
+ In a Foundry journal, a reference resolves to the figure's own JournalEntryPage — the `image` page a solitary, plainly captioned picture becomes, or the `text` page any other fence becomes — addressed exactly as an authored `[[#anchor|Text]]` link to the same anchor would be. A reference naming another note's figure is not resolved there: a journal addresses a page by UUID, and has none for a note outside the one compiling.
105
+
106
+ **A figure needs contents and a closing `:::`.** Its id, when one is given, must be unique in the note; its caption, when one is given, is the one `///` section the fence carries.
107
+
108
+ A few worked examples. A captioned picture:
109
+
110
+ ```markdown
111
+ :::figure {#thorn}
112
+ ![[being-foobar|The Great Beast]]
113
+ ///
114
+ The great beast, as drawn by [[person-havard|Havard]].
115
+ :::
116
+ ```
117
+
118
+ This draws **Figure 1**, labelled "Figure 1: The great beast, as drawn by Havard." beneath the picture.
119
+
120
+ A grouped fence holding two pictures as one figure — a plate rather than two separate plates:
121
+
122
+ ```markdown
123
+ :::figure {#passes}
124
+ ![[image-northpass|Northern approach]]
125
+ ![[image-southpass|Southern approach]]
126
+ ///
127
+ The two approaches to the pass.
128
+ :::
129
+ ```
130
+
131
+ Both pictures draw together under one label, **Figure 2**, because the fence is the unit the counter numbers — not the pictures inside it.
132
+
133
+ A fence with no caption:
134
+
135
+ ```markdown
136
+ :::figure {#anvil}
137
+ ![[icon-anvil|A worn anvil]]
138
+ :::
139
+ ```
140
+
141
+ This draws **Figure 3**, with no caption line beneath it, and still carries the anchor `#anvil`.
142
+
143
+ A reference in running prose:
144
+
145
+ ```markdown
146
+ See {{ref "#thorn"}} for the plate, or {{ref "#thorn" form="full"}} for the full description.
147
+ ```
148
+
149
+ This displays as "See [Figure 1](#thorn) for the plate, or [Figure 1: The great beast, as drawn by Havard.](#thorn) for the full description," each bracketed phrase a link to the figure.
150
+
151
+ A reference to a figure in another note:
152
+
153
+ ```markdown
154
+ The market routes are drawn in {{ref "place-thornford#trade"}}.
155
+ ```
156
+
157
+ On the website this links into `place-thornford`'s own page, reading "Table 1" — the number that fence carries there, whatever number it carries in the book or in a note of its own.
81
158
 
82
159
  ## Images and protected content
83
160
 
@@ -100,8 +177,10 @@ a closing `:::`. A name that is not one of the three is reported.
100
177
  **A GM-only section holds a box.** A `:::secret` is a container for whatever the
101
178
  GM reads, boxes included, and a box written inside one stays inside it. A box
102
179
  holds no named block of its own, so a second box inside a box is reported. A
103
- `:::caption` belongs to the caption construct and may be written inside any of
104
- them.
180
+ `:::figure` fence may be written inside any of them. The two draw a box for
181
+ different reasons: a named block says what kind of aside a passage is, while a
182
+ `:::figure` fence makes a passage numbered and referable — nesting one inside
183
+ the other gives a passage both.
105
184
 
106
185
  **An attribute block follows the name.** It is the same braced grammar the other
107
186
  body extensions use, so it carries an id, classes, and any attribute the element
@@ -147,11 +226,11 @@ not parse is reported at its own line and left as written; every well-formed
147
226
  block in the same note still renders.
148
227
 
149
228
  **A heading that would start its own journal page cannot be written inside a
150
- block, or inside a caption.** An H1, or a heading of any level carrying an
151
- `{#anchor}`, is reported at its own line there — starting a page would tear
152
- the block in two, and a reader could open the second half with none of the
153
- box around it. A lower heading with no anchor is unaffected and is the
154
- ordinary way to structure a long box.
229
+ block, or inside a `:::figure` fence.** An H1, or a heading of any level
230
+ carrying an `{#anchor}`, is reported at its own line there — starting a page
231
+ would tear the block or the figure in two, and a reader could open the second
232
+ half with none of the box around it. A lower heading with no anchor is
233
+ unaffected and is the ordinary way to structure a long box.
155
234
 
156
235
  ## Footnotes and definition lists
157
236
 
@@ -170,7 +249,8 @@ The road is passable in summer.[^season] The bridge has a toll.[^2]
170
249
  ```
171
250
 
172
251
  A reference may be written anywhere prose can be written — a word in a
173
- quotation, a bullet, a table cell, a heading, a named block, a caption. A
252
+ quotation, a bullet, a table cell, a heading, a named block, a figure's
253
+ caption. A
174
254
  definition belongs at the top level of the note: indent a following paragraph
175
255
  or code block by four spaces to include it in the same footnote, but never
176
256
  inside a list, a block quote, or a table. A definition written there, or a
@@ -324,20 +404,21 @@ There are {{words (sql "SELECT COUNT(*) FROM notes WHERE type = 'being'")}} bein
324
404
 
325
405
  <!-- expression-helpers:start -->
326
406
 
327
- | Helper | Parameters | What it gives you |
328
- | ------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
329
- | `eq` | `a b` | True when both are the same value. Compares exactly, so 1 and "1" differ. |
330
- | `gt` | `a b` | True when a is greater than b. Both must be finite numbers. |
331
- | `gte` | `a b` | True when a is greater than or equal to b. Both must be finite numbers. |
332
- | `lt` | `a b` | True when a is less than b. Both must be finite numbers. |
333
- | `lte` | `a b` | True when a is less than or equal to b. Both must be finite numbers. |
334
- | `not` | `value` | True when the value is false, zero, empty or absent. |
335
- | `and` | `value …` | True when every value given is true. Takes two or more. |
336
- | `or` | `value …` | True when any value given is true. Takes two or more. |
337
- | `words` | `number` | The number spelled out for running prose — `1200` becomes one thousand two hundred. Whole numbers only; a decimal is a finding, and `digits` takes one. |
338
- | `digits` | `number` | The number as a grouped numeral — `1200` becomes 1,200, and a fractional part is kept. |
339
- | `sql` | `"query"` | The single value a query returns, for use inside a sentence. The query is a quoted string, the same SQL a table fence takes. |
340
- | `dateformat` | `calendar date ["format"]` | A date written in a calendar. The calendar is an Address or its shortcode, the date is the value a note carries, and the optional third argument names an output pattern. |
407
+ | Helper | Parameters | What it gives you |
408
+ | ------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
409
+ | `eq` | `a b` | True when both are the same value. Compares exactly, so 1 and "1" differ. |
410
+ | `gt` | `a b` | True when a is greater than b. Both must be finite numbers. |
411
+ | `gte` | `a b` | True when a is greater than or equal to b. Both must be finite numbers. |
412
+ | `lt` | `a b` | True when a is less than b. Both must be finite numbers. |
413
+ | `lte` | `a b` | True when a is less than or equal to b. Both must be finite numbers. |
414
+ | `not` | `value` | True when the value is false, zero, empty or absent. |
415
+ | `and` | `value …` | True when every value given is true. Takes two or more. |
416
+ | `or` | `value …` | True when any value given is true. Takes two or more. |
417
+ | `words` | `number` | The number spelled out for running prose — `1200` becomes one thousand two hundred. Whole numbers only; a decimal is a finding, and `digits` takes one. |
418
+ | `digits` | `number` | The number as a grouped numeral — `1200` becomes 1,200, and a fractional part is kept. |
419
+ | `sql` | `"query"` | The single value a query returns, for use inside a sentence. The query is a quoted string, the same SQL a table fence takes. |
420
+ | `dateformat` | `calendar date ["format"]` | A date written in a calendar. The calendar is an Address or its shortcode, the date is the value a note carries, and the optional third argument names an output pattern. |
421
+ | `ref` | `address [form="number"\|"full"\|"title"]` | A link to a `:::figure` fence, by its anchor — `"#thorn"` on this note, `"note-address#thorn"` on another. `form` is `number` (the default, and admitted explicitly), rendering "Figure 13"; `full`, the number and the caption; or `title`, the caption alone. Always renders as a link to the figure, and a link inside the caption contributes only its label text. The number rendered is the one the figure carries on the surface doing the rendering, whichever note names it. |
341
422
 
342
423
  <!-- expression-helpers:end -->
343
424
 
package/docs/commands.md CHANGED
@@ -885,8 +885,8 @@ Install it with `npm ci`, which resolves from the lockfile this just moved.
885
885
  ```
886
886
 
887
887
  ```
888
- $ package-build bump @heroiclands/hugo-theme --check
889
- @heroiclands/hugo-theme 0.5.0 → 0.6.0
888
+ $ package-build bump @heroiclands/package-build --check
889
+ @heroiclands/package-build 22.6.0 → 22.7.0
890
890
 
891
891
  Run without --check to take it.
892
892
  ```
@@ -1455,15 +1455,17 @@ package-build deps fetch [--from <zip|dir>] [--id <id>]
1455
1455
  **DESCRIPTION**
1456
1456
 
1457
1457
  The only action is `fetch`, which fills three caches under `build/cache/`:
1458
- the **content index** of every declared dependency, the **item catalogue**
1459
- of those that additionally declare `itemCatalog: true`, and the **site
1460
- navigation** — `https://www.heroiclands.org/nav.json`, the header menu
1461
- every package site renders, fetched for every package because every package
1462
- publishes a site. Its own command rather than a step of `package compile`
1463
- or `site`, so neither reaches the network — a build that downloads silently
1464
- is not reproducible and hides a dependency's version change behind a
1465
- passing run. Each cache is stamped complete only once its fetch finishes,
1466
- so a half-finished one reads as cold.
1458
+ the **content index** of every declared dependency, the **item catalogue or
1459
+ asset archive** of those that additionally declare `itemCatalog: true` or
1460
+ `assetArchive: true`, and the **site navigation** —
1461
+ `https://www.heroiclands.org/nav.json`, the header menu every package site
1462
+ renders, fetched for every package because every package publishes a site.
1463
+ A relationship declaring both `itemCatalog: true` and `assetArchive: true`
1464
+ is fetched once. Its own command rather than a step of `package compile` or
1465
+ `site`, so neither reaches the network — a build that downloads silently is
1466
+ not reproducible and hides a dependency's version change behind a passing
1467
+ run. Each cache is stamped complete only once its fetch finishes, so a
1468
+ half-finished one reads as cold.
1467
1469
 
1468
1470
  `lint`, `links`, `site`, `pdf`, `map`, `reachability`, and `package compile`
1469
1471
  check the declared dependencies' content-index cache before reading notes or
@@ -1501,7 +1503,7 @@ including when the repository declares no dependencies at all.
1501
1503
  ```
1502
1504
  $ package-build deps fetch
1503
1505
  […] Fetched the site navigation to build/cache/navigation/nav.json.
1504
- […] No relationship declares `itemCatalog: true`; nothing to fetch.
1506
+ […] No relationship declares `itemCatalog: true` or `assetArchive: true`; nothing to fetch.
1505
1507
  […] This package declares no dependencies.
1506
1508
 
1507
1509
  $ package-build deps fetch --from build/dist/module.zip
@@ -2308,9 +2310,9 @@ Three things are written, and nothing outside `build/`:
2308
2310
  - `build/hugo/hugo.toml`, generated on every run from `package.json`
2309
2311
  (`homepage`, `description`, `author`), `package-build.config.yaml`
2310
2312
  (`packageBuild.manifest.title`, `site.assets`, `site.notfound`,
2311
- `site.hugo`), the organisation's constants, the installed
2312
- `@heroiclands/hugo-theme`'s location, and the navigation `deps fetch`
2313
- cached. Every value's source is listed under
2313
+ `site.hugo`), the organisation's constants, the shared theme's own
2314
+ location, and the navigation `deps fetch` cached. Every value's source is
2315
+ listed under
2314
2316
  [the generated Hugo configuration](configuration.md#the-generated-hugo-configuration).
2315
2317
  - `build/hugo/content/`, the content mount — the homepage as its `_index.md`,
2316
2318
  and the content tree's pages flat below `publish.address.prefix`. Wiped on
@@ -2319,13 +2321,13 @@ Three things are written, and nothing outside `build/`:
2319
2321
  deployment root `package-build site-root` writes beside. Nothing Hugo
2320
2322
  reads lands in what is published.
2321
2323
 
2322
- The configuration's sources are read before the output tree is touched, so
2323
- a missing `homepage`, a cold navigation cache or an uninstalled theme fails
2324
- with the previous site intact. Every gate is then checked and reported, and
2325
- the run stops at the first that fires, ordered so the report names the cause
2326
- rather than its symptoms — an unusable dependency manifest, reported after
2327
- the links that failed because of it, would otherwise read as a pile of
2328
- broken notes. Reads the content tree named by `paths.content`.
2324
+ The configuration's sources are read before the output tree is touched, so a
2325
+ missing `homepage` or a cold navigation cache fails with the previous site
2326
+ intact. Every gate is then checked and reported, and the run stops at the
2327
+ first that fires, ordered so the report names the cause rather than its
2328
+ symptoms — an unusable dependency manifest, reported after the links that
2329
+ failed because of it, would otherwise read as a pile of broken notes. Reads
2330
+ the content tree named by `paths.content`.
2329
2331
 
2330
2332
  **OPTIONS**
2331
2333
 
@@ -2334,13 +2336,12 @@ None.
2334
2336
  **EXIT STATUS**
2335
2337
 
2336
2338
  1 if `package.json` declares no `homepage`, or one that does not end
2337
- `/<contentPackage>/`; if `packageBuild.manifest.title` is undeclared; if the
2338
- navigation has not been fetched (`package-build deps fetch` fills the cache
2339
- and is named in the message); or if `@heroiclands/hugo-theme` is not
2340
- installed. 1 if any gate fires — no homepage or two competing for it, a
2341
- frontmatter wikilink, an address that cannot be derived, a stale or
2342
- unaddressable dependency manifest, an address published twice, a table that
2343
- failed to expand, or a dead wikilink. Otherwise 0.
2339
+ `/<contentPackage>/`; if `packageBuild.manifest.title` is undeclared; or if
2340
+ the navigation has not been fetched (`package-build deps fetch` fills the
2341
+ cache and is named in the message). 1 if any gate fires — no homepage or two
2342
+ competing for it, a frontmatter wikilink, an address that cannot be derived,
2343
+ a stale or unaddressable dependency manifest, an address published twice, a
2344
+ table that failed to expand, or a dead wikilink. Otherwise 0.
2344
2345
 
2345
2346
  **EXAMPLES**
2346
2347
 
@@ -55,7 +55,7 @@ around its own evaluation.
55
55
 
56
56
  ### Quick reference
57
57
 
58
- 22 top-level keys. `rootDir` is not one of them — a data configuration never
58
+ 23 top-level keys. `rootDir` is not one of them — a data configuration never
59
59
  writes it — and is documented under [Derived values](#derived-values) instead,
60
60
  alongside `foundryPackage`, `homepage`, `author` and `itemBuilders`, whose
61
61
  data-configuration behaviour is also derivation rather than ordinary
@@ -72,6 +72,7 @@ authoring.
72
72
  | [`itemBuilders`](#itembuilders) | object, or list of `{system, builders}` (or a name/list of names, in YAML) | no; refused in a `documentation` package | `{}` |
73
73
  | [`paths`](#paths) | object | no | see [`paths`](#paths) |
74
74
  | [`skipDirectories`](#skipdirectories) | string[] | no | `[]` |
75
+ | [`forbidGeneratedArt`](#forbidgeneratedart) | boolean | no | `false` |
75
76
  | [`icons`](#icons) | object, or a path to a file holding one | no | empty registry |
76
77
  | [`packs`](#packs) | array | yes, at least one entry, in a `systems` or `modules` package; refused in a `documentation` package | — |
77
78
  | [`docs`](#docs) | object | no; refused in a `documentation` package | `{}` |
@@ -87,7 +88,7 @@ authoring.
87
88
 
88
89
  Any key outside this list is refused:
89
90
 
90
- > `` `<key>` is not a recognized option (expected one of: rootDir, contentPackage, foundryPackage, homepage, author, packageKind, stats, itemBuilders, paths, skipDirectories, icons, packs, docs, site, pdf, compatibility, relationships, systems, requiresSystem, packageBuild, publish, changelog). ``
91
+ > `` `<key>` is not a recognized option (expected one of: rootDir, contentPackage, foundryPackage, homepage, author, packageKind, stats, itemBuilders, paths, skipDirectories, forbidGeneratedArt, icons, packs, docs, site, pdf, compatibility, relationships, systems, requiresSystem, packageBuild, publish, changelog). ``
91
92
 
92
93
  (`rootDir` appears in that list because it is a key `defineConfig` itself
93
94
  accepts — an `.mjs` configuration authors it directly. A YAML configuration
@@ -515,7 +516,7 @@ HeroicLands layout, resolved against `rootDir`:
515
516
  | `paths.packJson` | `build/packs-json` | Build-only per-entry JSON intermediate. |
516
517
  | `paths.stage` | `build/stage/packs` | Compiled LevelDB packs. |
517
518
  | `paths.unpack` | `build/tmp/packs` | Where `unpack` extracts JSON back to. |
518
- | `paths.foreignCache` | `build/cache/foreign` | Where a dependency declaring `itemCatalog: true` is unpacked. |
519
+ | `paths.foreignCache` | `build/cache/foreign` | Where a dependency declaring `itemCatalog: true` or `assetArchive: true` is unpacked. |
519
520
  | `paths.metadataCache` | `build/cache/metadata` | Where a dependency's published content index is fetched to, for every declared dependency. |
520
521
  | `paths.navigationCache` | `build/cache/navigation` | Where the site navigation heroiclands.org publishes is fetched to, for the generated Hugo menu. |
521
522
 
@@ -547,6 +548,26 @@ skipDirectories: [Templates]
547
548
 
548
549
  > ``package-build config: `skipDirectories[<index>]` must be a non-empty string.``
549
550
 
551
+ ### `forbidGeneratedArt`
552
+
553
+ **Type:** boolean · **Optional** · default `false`.
554
+
555
+ With it `true`, `package-build lint` refuses every asset record of this
556
+ package whose `ai` field states `true` — a licensing stance, not a build
557
+ detail: this package carries no machine-generated art. With it absent or
558
+ `false`, `lint` reports nothing extra about an `ai: true` record.
559
+
560
+ ```yaml
561
+ forbidGeneratedArt: true
562
+ ```
563
+
564
+ `lint` reports a located error naming the record and its file for each one it
565
+ finds:
566
+
567
+ > ``<file>: error: `<address>` is machine-generated art (`ai: true`), and `forbidGeneratedArt` refuses it in this package``
568
+
569
+ > ``package-build config: `forbidGeneratedArt` must be a boolean.``
570
+
550
571
  ### `icons`
551
572
 
552
573
  **Type:** object (`{families, icons, defaultFamily?}`), or a string naming a
@@ -1010,8 +1031,8 @@ in it has one source, and that source is where it is edited:
1010
1031
  | `locale` | the organisation's locale, `en-us`, in `engine/site-config.mjs` |
1011
1032
  | `publishDir` | `contentPackage`, under the deployment root `build/site` — written relative to `build/hugo/`, so `../site/<contentPackage>` |
1012
1033
  | `contentDir` | the fixed content mount, `build/hugo/content` — written as `content` |
1013
- | `themesDir` | where `@heroiclands/hugo-theme` is installed, resolved the way Node resolves a package and written relative to `build/hugo/` |
1014
- | `theme` | the installed `@heroiclands/hugo-theme`, so `hugo-theme` |
1034
+ | `themesDir` | this package's own root, which holds the theme it ships, written relative to `build/hugo/` |
1035
+ | `theme` | the theme's directory inside this package, `hugo-theme` |
1015
1036
  | `disableKinds` | the toolchain, which renders a site as its homepage and its pages: `["section", "taxonomy", "term", "RSS"]` on every site, whatever its notes carry |
1016
1037
  | `taxonomies` | the toolchain — never written, because `taxonomy` and `term` are disabled kinds |
1017
1038
  | `outputs` | the toolchain — never written, because every listing kind is disabled |
@@ -1028,10 +1049,6 @@ error naming the command that fills it:
1028
1049
 
1029
1050
  > `the site navigation has not been fetched. Run `package-build deps fetch` first.`
1030
1051
 
1031
- A missing theme names the package to install:
1032
-
1033
- > `@heroiclands/hugo-theme is not installed anywhere above <rootDir> — add it to `devDependencies`and run`npm ci``
1034
-
1035
1052
  The site needs a title from `site.title` or the package manifest:
1036
1053
 
1037
1054
  > ``package-build config: declare `site.title` or `packageBuild.manifest.title` for the site's title.``
@@ -1205,15 +1222,17 @@ system relationship it declares — see [`stats.systemVersion`](#statssystemvers
1205
1222
 
1206
1223
  Each entry, in any of the four lists:
1207
1224
 
1208
- | Key (under `relationships.<kind>[]`) | Type | Required | Default |
1209
- | ---------------------------------------- | ------------------------------- | -------- | ------- |
1210
- | `relationships.systems[].id` | string | yes | — |
1211
- | `relationships.systems[].contentPackage` | string | no | the id |
1212
- | `relationships.systems[].type` | string | no | none |
1213
- | `relationships.systems[].manifest` | string | no | none |
1214
- | `relationships.systems[].compatibility` | object, `{minimum?, verified?}` | no | none |
1215
- | `relationships.systems[].itemCatalog` | boolean | no | `false` |
1216
- | `relationships.systems[].contentIndex` | boolean | no | `true` |
1225
+ | Key (under `relationships.<kind>[]`) | Type | Required | Default |
1226
+ | ------------------------------------------ | ------------------------------- | -------- | ------- |
1227
+ | `relationships.systems[].id` | string | yes | — |
1228
+ | `relationships.systems[].contentPackage` | string | no | the id |
1229
+ | `relationships.systems[].type` | string | no | none |
1230
+ | `relationships.systems[].manifest` | string | no | none |
1231
+ | `relationships.systems[].compatibility` | object, `{minimum?, verified?}` | no | none |
1232
+ | `relationships.systems[].itemCatalog` | boolean | no | `false` |
1233
+ | `relationships.systems[].assetArchive` | boolean | no | `false` |
1234
+ | `relationships.systems[].contentIndex` | boolean | no | `true` |
1235
+ | `relationships.systems[].assetReplacement` | boolean | no | `false` |
1217
1236
 
1218
1237
  (the same keys apply under `requires[]`, `recommends[]` and
1219
1238
  `conflicts[]`.)
@@ -1222,7 +1241,7 @@ Each entry, in any of the four lists:
1222
1241
 
1223
1242
  > ``package-build config: `relationships.<kind>[<index>].id` must be a non-empty string.``
1224
1243
 
1225
- > ``package-build config: `relationships.<kind>[<index>].<key>` is not a recognized option (expected one of: id, contentPackage, type, manifest, compatibility, itemCatalog, contentIndex).``
1244
+ > ``package-build config: `relationships.<kind>[<index>].<key>` is not a recognized option (expected one of: id, contentPackage, type, manifest, compatibility, itemCatalog, assetArchive, contentIndex, assetReplacement).``
1226
1245
 
1227
1246
  `contentPackage` names what the other package's _content_ is called, where
1228
1247
  that differs from its Foundry id. A note addresses a file by the content
@@ -1241,6 +1260,31 @@ item catalogue at build time. It requires a `manifest`:
1241
1260
 
1242
1261
  > ``package-build config: `relationships.<kind>[<index>].itemCatalog` needs a `manifest` naming the package to fetch.``
1243
1262
 
1263
+ `assetArchive` opts into unpacking the named package's release archive for
1264
+ its asset bytes alone — the sibling of `itemCatalog`, for a dependency whose
1265
+ bytes are wanted and whose items are not. It builds no item catalogue from
1266
+ the archive, which is what distinguishes it from `itemCatalog: true`. It
1267
+ requires a `manifest`:
1268
+
1269
+ > ``package-build config: `relationships.<kind>[<index>].assetArchive` must be true or false.``
1270
+
1271
+ > ``package-build config: `relationships.<kind>[<index>].assetArchive` needs a `manifest` naming the package to fetch.``
1272
+
1273
+ A relationship with no items of its own to extract, such as an asset
1274
+ replacement module, declares only `assetArchive: true`:
1275
+
1276
+ ```yaml
1277
+ relationships:
1278
+ requires:
1279
+ - id: thalornaaltart
1280
+ manifest: https://…/releases/latest/download/module.json
1281
+ compatibility: { verified: "1.0.0" }
1282
+ assetArchive: true
1283
+ ```
1284
+
1285
+ A relationship declaring both `itemCatalog: true` and `assetArchive: true` is
1286
+ fetched once: `deps fetch` does not download the archive twice for one entry.
1287
+
1244
1288
  `contentIndex` and `itemCatalog` are the two edges a relationship may declare,
1245
1289
  and a package may have either without the other. `itemCatalog` says a
1246
1290
  dependency supplies _items_; `contentIndex`, `true` by default, says
@@ -1260,6 +1304,35 @@ demand.
1260
1304
 
1261
1305
  > ``package-build config: `relationships.<kind>[<index>].contentIndex` cannot be false together with `itemCatalog: true` — a catalogue is fetched from the same index.``
1262
1306
 
1307
+ `assetReplacement` opts a relationship into serving this package's own asset
1308
+ addresses from the named dependency's tree instead of its local one —
1309
+ `engine/asset-replacement.mjs`'s `resolveAssetReplacement` is what reads the
1310
+ declaration. Off by default: declaring a dependency is not the same as
1311
+ wanting its pictures in place of the ones this package ships itself. It
1312
+ requires a fetched index to check the replacement against, so it cannot pair
1313
+ with `contentIndex: false`:
1314
+
1315
+ ```yaml
1316
+ relationships:
1317
+ requires:
1318
+ - id: thalornaaltart
1319
+ manifest: https://…/releases/latest/download/module.json
1320
+ assetReplacement: true
1321
+ contentIndex: true
1322
+ ```
1323
+
1324
+ With that declared, an address `thalorna` would otherwise answer itself —
1325
+ `thalorna-none-image-thorn` — resolves against
1326
+ `thalornaaltart-none-image-thorn` first, falling back to `thalorna`'s own
1327
+ record when `thalornaaltart` carries no file at that address. A package
1328
+ declaring more than one `assetReplacement: true` relationship is tried in
1329
+ the order the relationships are declared, and the first one carrying the
1330
+ address wins.
1331
+
1332
+ > ``package-build config: `relationships.<kind>[<index>].assetReplacement` must be true or false.``
1333
+
1334
+ > ``package-build config: `relationships.<kind>[<index>].assetReplacement` cannot be true together with `contentIndex: false` — a replacement with no fetched index to check against resolves nothing.``
1335
+
1263
1336
  ### `systems`
1264
1337
 
1265
1338
  **Type:** object (`{id: spec}`) · **Optional** · default `{}`. Refused in a
@@ -25,7 +25,7 @@ Start with [your first note](authoring/first-note.md). It explains the smallest
25
25
 
26
26
  A nonempty note body creates one system-agnostic Foundry JournalEntry. Content before the first H1 becomes an introduction page; every H1 starts another JournalEntryPage. A lower-level heading with an explicit `{#anchor}` starts a page too, so a link can address it. The same prose becomes a web page and can be selected for a book. The content index records the note and its frontmatter for queries and navigation.
27
27
 
28
- A `:::caption {#anchor}` fence labels the next prose, code, table, or image block. Its anchor makes the block addressable; `[[#anchor|]]` displays the generated kind and number, such as **Figure 12**. Foundry gives the captioned block a JournalEntryPage. See [captions and numbered references](authoring/links-and-markup.md#captions-and-numbered-references) for the syntax.
28
+ A `:::figure {#anchor}` fence numbers whatever it holds — prose, code, a table, or an image. Its anchor makes the fence a target `{{ref "#anchor"}}` can point to, displaying the generated kind and number, such as **Figure 12**. Foundry gives the fence its own JournalEntryPage. See [figures and numbered references](authoring/links-and-markup.md#figures-and-numbered-references) for the syntax.
29
29
 
30
30
  Body extensions use braces with space-separated `key=value` attributes:
31
31
  `![[icon-harbor|Harbor]]{float=top-left size=medium}` and an SQL fence with
@@ -88,10 +88,11 @@ finding.
88
88
  Each section and each selected entry starts on its own page. The PDF has a
89
89
  contents page and an outline for section and entry navigation. A note's
90
90
  `description` appears as an epigraph when present. Wider tables span both
91
- columns; an uncaptioned wide table starts on a new page so it stays after its
92
- introduction. A captioned wide table may float to the page top, and its numbered
93
- caption stays with it. A table too tall for one page takes pages of its own.
94
- Caption numbers run through the whole book in reading order. Maps attached
91
+ columns; a wide table outside a figure fence starts on a new page so it stays
92
+ after its introduction. A wide table inside one may float to the page top, and
93
+ its label stays with it. A table too tall for one page takes pages of its own.
94
+ A figure's number runs through the whole book in reading order, counted within
95
+ its own kind. Maps attached
95
96
  to selected places use landscape pages after those entries. Regional, theatre
96
97
  of the mind, battle, and local map backgrounds also print on landscape pages.
97
98
  Battle and local maps print each Scene level's background on its own page.
@@ -863,8 +863,8 @@ None of them is needed to build a package.
863
863
  **A website.** Every package publishes one — at the least, the homepage note
864
864
  from step 5 — at `https://www.heroiclands.org/<contentPackage>/`. Add that
865
865
  address to `package.json` as `homepage`, with a `description` and an `author`
866
- beside it; add `@heroiclands/hugo-theme` under `devDependencies`; and add a
867
- `packageBuild.manifest.title`. Then `package-build deps fetch` caches the
866
+ beside it, and add a `packageBuild.manifest.title`. The theme the site renders
867
+ through ships with this package, so nothing else is installed. Then `package-build deps fetch` caches the
868
868
  organisation's navigation, and `package-build site` writes the whole Hugo
869
869
  source tree under `build/hugo/` — the configuration generated from those
870
870
  values, and the content mount — for `hugo --source build/hugo` to render
@@ -132,6 +132,47 @@ fenced example remains literal. `site.passOptions` supplies that named pass's
132
132
  options. This is a package-specific extension point; keep the general note
133
133
  format and link behavior in the [content format](../content-format.md).
134
134
 
135
+ ## What an emitted page carries
136
+
137
+ A theme reads the front matter `package-build site` writes: the note's own
138
+ front matter, plus the values the build derives. Which is which matters,
139
+ because an authored value under a derived key is discarded.
140
+
141
+ The build always writes `title`, `slug`, `url`, `kbfolder`, `package` and
142
+ `infoboxes`. `title` is the note's name, `url` is the page's address, and
143
+ `slug` is that address's last segment. `package` names the package shipping the
144
+ note. `infoboxes` is assembled from the note's own fields, so no note authors
145
+ it and nothing in the note format accepts it.
146
+
147
+ The build writes `resolvedDates`, `related`, `contains`, `held_by`, `holdings`
148
+ and `map` only when it has something to write. A page with no links either way
149
+ carries no `related`, and a place with no drawing carries no `map`.
150
+
151
+ An authored `aliases`, `related`, `contains`, `held_by`, `holdings` or `map` is
152
+ dropped. Each states a fact about the whole tree rather than about one note, so
153
+ the build replaces whatever a note wrote.
154
+
155
+ Artwork addresses are rewritten in place under `data`. A page's `data.icon`,
156
+ `data.bgImage` and `data.banner` carry the URLs the site serves rather than the
157
+ addresses the note authored, and there are four cases to tell apart:
158
+
159
+ | The note writes | The page carries |
160
+ | ---------------------------- | ---------------- |
161
+ | an address something answers | the resolved URL |
162
+ | an address nothing answers | no key at all |
163
+ | `null` | `null` |
164
+ | `""` | `""` |
165
+
166
+ An address nothing answers is deleted rather than written through, so an absent
167
+ key is the ordinary case for a theme to render nothing against, and no page
168
+ reaches a reader carrying a broken image source. The two empties are distinct
169
+ and both survive: `null` is a note naming no art, where a default may apply,
170
+ and `""` is a note refusing art, where no default may replace it.
171
+
172
+ The build writes these keys; [what the theme draws](../reference/theme-pages.md)
173
+ describes the panel each one feeds — the infobox, the hero band, the holdings
174
+ and related cards, the place map, and search.
175
+
135
176
  ## Build and serve
136
177
 
137
178
  ```bash
@@ -309,10 +309,10 @@ the rendered pages for search into `build/site/<contentPackage>/pagefind/`;
309
309
  and `serve:site` does the first and then `hugo server` for a live preview.
310
310
  The repository carries no Hugo configuration of its own: `hugo.toml` is
311
311
  generated on every run from `package.json`, `package-build.config.yaml`, the
312
- installed `@heroiclands/hugo-theme` and the navigation `deps fetch` cached,
313
- and the only file to add is `@heroiclands/hugo-theme` under
314
- `devDependencies`. Hugo itself is a separate install — the extended edition,
315
- on the developer's `PATH` and the runner's.
312
+ shared theme that ships with `@heroiclands/package-build`, and the navigation
313
+ `deps fetch` cached — a site declares this one package and needs nothing
314
+ further added for its theme. Hugo itself is a separate install — the
315
+ extended edition, on the developer's `PATH` and the runner's.
316
316
 
317
317
  The site build reads the cached navigation, so `build:site` in a package with
318
318
  no other dependency still runs `deps fetch` first: