@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
package/docs/api.md CHANGED
@@ -132,29 +132,30 @@ can write the result directly without introducing formatting drift.
132
132
 
133
133
  The per-system frontmatter block: how one note feeds more than one game system through properties named after that system (`<system>.system`, `<system>.type`, `<system>.img`, `<system>.items` on actors). Resolves a field's value through the block, the shared top level, and a retiring position in that order, and merges an authored `<system>.system` onto a compiler-built one without disturbing what the builder already wrote.
134
134
 
135
- | Export | Signature | Returns | Use it when |
136
- | --------------------------- | ------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------- |
137
- | `SYSTEM_DATA_KEY` | `const SYSTEM_DATA_KEY` | — | naming the key inside a system block whose contents are the system's own vocabulary |
138
- | `BLOCK_DOCUMENT_PROPERTIES` | `const BLOCK_DOCUMENT_PROPERTIES` | — | mapping a system-block key to the document property it becomes |
139
- | `BLOCK_DIRECTIVES` | `const BLOCK_DIRECTIVES` | — | recognizing a build directive (e.g. `pack`) that names no document property |
140
- | `SYSTEM_BLOCK_KEYS` | `const SYSTEM_BLOCK_KEYS` | — | enumerating every key any system block may carry |
141
- | `setPath` | `setPath(target, dotted, value)` | `object` — `target`, for chaining | writing a value at a dotted path in a document's `system` block |
142
- | `systemBlock` | `systemBlock(fm, block)` | `Record<string, unknown> \| undefined` | reading one system's whole block off a note, or nothing when it is absent or malformed |
143
- | `carriesSystemBlock` | `carriesSystemBlock(fm, block)` | `boolean` | deciding whether a pack that declares a system should compile a note at all |
144
- | `systemData` | `systemData(fm, block)` | `Record<string, unknown>` | reading a system block's `system` sub-block, `{}` when absent |
145
- | `blockField` | `blockField(fm, block, key, defaultValue)` | the value | reading a key from one system's block, falling back to the top level |
146
- | `sharedProperty` | `sharedProperty(fm, source, defaultValue)` | the value | reading a shared top-level property by a possibly-dotted path, blind to every system block |
147
- | `blockProperty` | `blockProperty(fm, block, key, defaultValue)` | the value | reading a property a system block may override, else the shared top-level one |
148
- | `blockDataProperty` | `blockDataProperty(fm, block, key, defaultValue)` | the value | reading a system override, then shared `data`, then a top-level value |
149
- | `legacyKeyOf` | `legacyKeyOf(field)` | `string \| undefined` | finding the key a field is authored at inside a system block |
150
- | `retiredTopLevelKey` | `retiredTopLevelKey(field)` | `string \| undefined` | finding the bare top-level key a `data:`-sourced field is being swept off |
151
- | `authoredKey` | `authoredKey(name)` | `string` | reading a shared source as the key an author writes, without the `data:` container holding it |
152
- | `resolveFieldValue` | `resolveFieldValue(field, fm, ...)` | `{value, from}` | resolving one declared field against a note, in the declared source order |
153
- | `systemDataPaths` | `systemDataPaths(data, prefix)` | `string[]` | listing every path a note authors under `<system>.system`, containers included |
154
- | `undeclaredPaths` | `undeclaredPaths(data, declared, prefix)` | `string[]` | finding authored paths a system's published schema does not declare |
155
- | `unknownBlockKeys` | `unknownBlockKeys(fm, block, ...)` | `string[]` | finding keys directly under a system block that neither this format nor the system recognizes |
156
- | `claimedPaths` | `claimedPaths(fields)` | `Set<string>` | listing the exact `system` paths a field declaration writes, for `mergeSystemData` |
157
- | `mergeSystemData` | `mergeSystemData(built, fm, ...)` | `object` — `built`, for chaining | merging a note's `<system>.system` verbatim onto a built `system` block |
135
+ | Export | Signature | Returns | Use it when |
136
+ | --------------------------- | --------------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
137
+ | `SYSTEM_DATA_KEY` | `const SYSTEM_DATA_KEY` | — | naming the key inside a system block whose contents are the system's own vocabulary |
138
+ | `BLOCK_DOCUMENT_PROPERTIES` | `const BLOCK_DOCUMENT_PROPERTIES` | — | mapping a system-block key to the document property it becomes |
139
+ | `BLOCK_DIRECTIVES` | `const BLOCK_DIRECTIVES` | — | recognizing a build directive (e.g. `pack`) that names no document property |
140
+ | `SYSTEM_BLOCK_KEYS` | `const SYSTEM_BLOCK_KEYS` | — | enumerating every key any system block may carry |
141
+ | `setPath` | `setPath(target, dotted, value)` | `object` — `target`, for chaining | writing a value at a dotted path in a document's `system` block |
142
+ | `systemBlock` | `systemBlock(fm, block)` | `Record<string, unknown> \| undefined` | reading one system's whole block off a note, or nothing when it is absent or malformed |
143
+ | `carriesSystemBlock` | `carriesSystemBlock(fm, block)` | `boolean` | deciding whether a pack that declares a system should compile a note at all |
144
+ | `systemData` | `systemData(fm, block)` | `Record<string, unknown>` | reading a system block's `system` sub-block, `{}` when absent |
145
+ | `blockField` | `blockField(fm, block, key, defaultValue)` | the value | reading a key from one system's block, falling back to the top level |
146
+ | `sharedProperty` | `sharedProperty(fm, source, defaultValue)` | the value | reading a shared top-level property by a possibly-dotted path, blind to every system block |
147
+ | `blockProperty` | `blockProperty(fm, block, key, defaultValue)` | the value | reading a property a system block may override, else the shared top-level one |
148
+ | `resolveDataProperty` | `resolveDataProperty(fm, block, key, defaultValue)` | `{value, from}` | resolving a shared `data:` key every type accepts — a system override, then shared `data`, then a top-level value — and reporting which one answered |
149
+ | `blockDataProperty` | `blockDataProperty(fm, block, key, defaultValue)` | the value | `resolveDataProperty`'s value alone, in the same order |
150
+ | `legacyKeyOf` | `legacyKeyOf(field)` | `string \| undefined` | finding the key a field is authored at inside a system block |
151
+ | `retiredTopLevelKey` | `retiredTopLevelKey(field)` | `string \| undefined` | finding the bare top-level key a `data:`-sourced field is being swept off |
152
+ | `authoredKey` | `authoredKey(name)` | `string` | reading a shared source as the key an author writes, without the `data:` container holding it |
153
+ | `resolveFieldValue` | `resolveFieldValue(field, fm, ...)` | `{value, from}` | resolving one declared field against a note, in the declared source order |
154
+ | `systemDataPaths` | `systemDataPaths(data, prefix)` | `string[]` | listing every path a note authors under `<system>.system`, containers included |
155
+ | `undeclaredPaths` | `undeclaredPaths(data, declared, prefix)` | `string[]` | finding authored paths a system's published schema does not declare |
156
+ | `unknownBlockKeys` | `unknownBlockKeys(fm, block, ...)` | `string[]` | finding keys directly under a system block that neither this format nor the system recognizes |
157
+ | `claimedPaths` | `claimedPaths(fields)` | `Set<string>` | listing the exact `system` paths a field declaration writes, for `mergeSystemData` |
158
+ | `mergeSystemData` | `mergeSystemData(built, fm, ...)` | `object` — `built`, for chaining | merging a note's `<system>.system` verbatim onto a built `system` block |
158
159
 
159
160
  ### `engine.codeFences`
160
161
 
@@ -651,7 +652,7 @@ The neutral day spelling is `<year>.<day>[:HHMMSS]`. The pure conversion functio
651
652
 
652
653
  ### `engine.noteDates`
653
654
 
654
- A date on a note is one authored scalar. `<year>[.<day>[:HHMMSS]]` names a calendar-neutral year, day, or second; `datefrom vrcal 23 Taranis 326 VR` names a calendar, a named month, and an era. Year and day values span their written interval; a clock time identifies a second. Either form may begin with `~` for uncertainty beyond that interval. The `approximate` field records that explicit marker, while `precision` and `seconds` describe the interval or time. A resolved date carries a canonical year, span, and sort key; a neutral year has no `canonicalDay`, while day-precision dates have one. Print `prose ?? text`, order on `sort`, and do arithmetic on `canonicalYear`. The literal `unknown` has no sort key or canonical year. `resolvedDateFields` supplies identical derived records to JSONL and generated page frontmatter.
655
+ A date on a note is one authored scalar. `<year>[.<day>[:HHMMSS]]` names a calendar-neutral year, day, or second; `datefrom vrcal 23 Taranis 326 VR` names a calendar, a named month, and an era. Year and day values span their written interval; a clock time identifies a second. Either form may begin with `~` for uncertainty beyond that interval. The `approximate` field records that explicit marker, while `precision` and `seconds` describe the interval or time. A resolved date carries a canonical year, span, and sort key; a neutral year has no `canonicalDay`, while day-precision dates have one. Print `prose ?? text`, order on `sort`, and do arithmetic on `canonicalYear`. The literal `unknown` has no sort key or canonical year. Year `0` is accepted only where a caller passes `allowZeroYear` — a `lore` event's `when` — and means "this day, every year": the value carries no `canonicalYear` and no `sort`. `resolvedDateFields` supplies identical derived records to JSONL and generated page frontmatter, including a `lore` note's `data.events`, each entry's `recurs` and computed `next`.
655
656
 
656
657
  | Export | Signature | Returns | Use it when |
657
658
  | ----------------------- | ------------------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------------------- |
@@ -664,6 +665,7 @@ A date on a note is one authored scalar. `<year>[.<day>[:HHMMSS]]` names a calen
664
665
  | `formatNoteDate` | `formatNoteDate(date, era, daysPerYear, formatName?)` | `{era, year, month, day, text, prose}` or `null` | printing a resolved date in a chosen era or its authored form |
665
666
  | `formatDateInCalendar` | `formatDateInCalendar(date, reference, context, formatName?)` | printable record or `null` | selecting a calendar's era and printing a resolved date in it |
666
667
  | `calendarEras` | `calendarEras(reference, context)` | ordered era records | resolving an addressed calendar or one of its eras |
668
+ | `occurrencesOf` | `occurrencesOf(date, recurs, options)` | occurrence records, in increasing order | finding a recurring event's next, last, or a membership probe |
667
669
  | `resolvedDateFields` | `resolvedDateFields(fm, context)` | map of normalized dates | deriving being and lore event date records for index and pages |
668
670
 
669
671
  ### `engine.dateConversion`
@@ -695,7 +697,7 @@ invokes the check during frontmatter lint.
695
697
 
696
698
  ### `engine.markdownExpressions`
697
699
 
698
- `renderMarkdownExpressions(body, { fm, dates, sqlResults, file, bodyLine })` expands scalar frontmatter references and registered Handlebars helpers outside Markdown code. The `dateformat` helper uses `formatDateInCalendar`; `gt`, `gte`, `lt`, `lte`, `eq`, `and`, `or`, and `not` evaluate explicit comparisons. The `sql` helper reads a prepared scalar result, which must contain one row and one column. `sqlQueriesInMarkdown` discovers SQL calls, including nested calls; `prepareInlineSqlExpressions` evaluates them through the same DuckDB connection as SQL tables. `EXPRESSION_HELPERS` is the one list of helpers: the engine registers from it and the authoring document is checked against it, so each name there carries the parameters it takes and a sentence on what it does. The renderer returns `{ markdown, findings }`, with source positions for invalid expressions. Site, Foundry and PDF passes invoke it after generated tables expand and before links resolve. Hugo shortcodes remain literal for the site renderer.
700
+ `renderMarkdownExpressions(body, { fm, dates, sqlResults, figures, file, bodyLine })` expands scalar frontmatter references and registered Handlebars helpers outside Markdown code. The `dateformat` helper uses `formatDateInCalendar`; `gt`, `gte`, `lt`, `lte`, `eq`, `and`, `or`, and `not` evaluate explicit comparisons. The `sql` helper reads a prepared scalar result, which must contain one row and one column. The `ref` helper links to a `:::figure` fence by its anchor — `"#thorn"` on the same note — in `number` (the default, and admitted explicitly), `full` or `title` form; `figures` is a `Map` of this note's own figures, by id, to `{label, caption, hasCaption}`, and an address naming another note is reported as unresolved rather than looked up. `sqlQueriesInMarkdown` discovers SQL calls, including nested calls; `prepareInlineSqlExpressions` evaluates them through the same DuckDB connection as SQL tables. `EXPRESSION_HELPERS` is the one list of helpers: the engine registers from it and the authoring document is checked against it, so each name there carries the parameters it takes and a sentence on what it does. The renderer returns `{ markdown, findings }`, with source positions for invalid expressions. Site, Foundry and PDF passes invoke it after generated tables expand and before links resolve. Hugo shortcodes remain literal for the site renderer.
699
701
 
700
702
  | Export | Signature | Returns | Use it when |
701
703
  | --------------------------- | ------------------------------------------ | ---------------------- | ------------------------------------------------ |
@@ -883,16 +885,17 @@ Emitting this package's content index. Every content build already walks the who
883
885
 
884
886
  The asset types — `icon`, `image` and `audio` — and the three roots they are walked from. An asset is addressed exactly as a note is; what differs is that a `.webp` carries no frontmatter and has nowhere to say what it is, so the root supplies the type and the list of roots is closed.
885
887
 
886
- | Export | Signature | Returns | Use it when |
887
- | ------------------ | ----------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
888
- | `ASSET_TYPES` | `const ASSET_TYPES` | `readonly AssetType[]` — type, root, extensions, description | walking the roots, or presenting the vocabulary |
889
- | `ASSET_TYPE_NAMES` | `const ASSET_TYPE_NAMES` | `ReadonlySet<string>` | admitting the asset types into a type vocabulary an address is parsed against |
890
- | `ASSET_SYSTEM` | `const ASSET_SYSTEM` | `string` — always `none` | writing an asset address, whose `<system>` segment is a property of the type rather than of the reference |
891
- | `IMAGE_EXTENSIONS` | `const IMAGE_EXTENSIONS` | `readonly string[]` — lowercase, dot-led | deciding whether a file under a picture root is an asset at all |
892
- | `AUDIO_EXTENSIONS` | `const AUDIO_EXTENSIONS` | `readonly string[]` — lowercase, dot-led | the same question for `audio/` |
893
- | `isAssetType` | `isAssetType(type)` | `boolean` | scoping a rewrite rule, which may substitute a file and nothing else |
894
- | `assetTypeOfRoot` | `assetTypeOfRoot(root)` | `AssetType \| undefined` | reading a root directory name back to the type it declares |
895
- | `isAssetShortcode` | `isAssetShortcode(shortcode)` | `boolean` | telling an addressable filename from one that carries a version string, a hyphen or a date stamp |
888
+ | Export | Signature | Returns | Use it when |
889
+ | ------------------ | ------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
890
+ | `ASSET_TYPES` | `const ASSET_TYPES` | `readonly AssetType[]` — type, root, extensions, description | walking the roots, or presenting the vocabulary |
891
+ | `ASSET_TYPE_NAMES` | `const ASSET_TYPE_NAMES` | `ReadonlySet<string>` | admitting the asset types into a type vocabulary an address is parsed against |
892
+ | `ASSET_SYSTEM` | `const ASSET_SYSTEM` | `string` — always `none` | writing an asset address, whose `<system>` segment is a property of the type rather than of the reference |
893
+ | `IMAGE_EXTENSIONS` | `const IMAGE_EXTENSIONS` | `readonly string[]` — lowercase, dot-led | deciding whether a file under a picture root is an asset at all |
894
+ | `AUDIO_EXTENSIONS` | `const AUDIO_EXTENSIONS` | `readonly string[]` — lowercase, dot-led | the same question for `audio/` |
895
+ | `isAssetType` | `isAssetType(type)` | `boolean` | scoping a rewrite rule, which may substitute a file and nothing else |
896
+ | `assetTypeOfRoot` | `assetTypeOfRoot(root)` | `AssetType \| undefined` | reading a root directory name back to the type it declares |
897
+ | `isAssetShortcode` | `isAssetShortcode(shortcode)` | `boolean` | telling an addressable filename from one that carries a version string, a hyphen or a date stamp |
898
+ | `imageDimensions` | `imageDimensions(absPath, ext)` | `{width, height} \| undefined` | reading a raster file's own pixel size from its header, which an SVG carries none of and a partial file fails closed on |
896
899
 
897
900
  ### `engine.assetIndex`
898
901
 
@@ -900,6 +903,7 @@ The asset record: one line of the content index per addressable file. The record
900
903
 
901
904
  | Export | Signature | Returns | Use it when |
902
905
  | --------------------------- | --------------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
906
+ | `ASSET_ROLES` | `const ASSET_ROLES` | `readonly string[]` | naming the closed set of roles a picture may declare |
903
907
  | `ASSET_RECORD_FIELDS` | `const ASSET_RECORD_FIELDS` | `readonly AssetRecordField[]` | reading what an `asset` block carries, and where each field comes from |
904
908
  | `PROVENANCE_KEYS` | `const PROVENANCE_KEYS` | `ReadonlySet<string>` | validating a provenance file, whose keys are derived from the record's own fields |
905
909
  | `REQUIRED_PROVENANCE_KEYS` | `const REQUIRED_PROVENANCE_KEYS` | `ReadonlySet<string>` | the keys a provenance file must state — a record resolves wholesale, so one omitting them leaves every file it covers with no rights holder or no terms |
@@ -1047,6 +1051,7 @@ Raw HTML in a note's prose, reported. **A note is markdown.** What markdown cann
1047
1051
  | `EMBED_PATTERN` | `const EMBED_PATTERN` | — | An embed, as authored, with the directive it may carry. |
1048
1052
  | `embedsIn` | `function embedsIn(body)` | {ParsedEmbed[]} One entry per embed. | Every embed in one body, in source order. |
1049
1053
  | `resolveEmbed` | `function resolveEmbed(index, embed)` | `{{pathname: string}\|{reason: string, target: string, type?: string}}` The authored pathname, or the finding. | What one embed resolves to, or why it does not. |
1054
+ | `embedRole` | `function embedRole(index, address)` | {string\|undefined} The role, or `undefined` for a note's address, one that resolves nowhere, or a picture that declares none. | The role an address's asset declares, by the same resolution. |
1050
1055
  | `embedProblems` | `function embedProblems(embed)` | {Array<{link: string, offset: number, message: string}>} One entry per defect. | What is wrong with one embed's directive or placement. |
1051
1056
  | `resolveEmbeds` | `function resolveEmbeds(body, ctx)` | `{{markdown: string, unresolved: Array<object>, problems: Array<object>}}` The body, the embeds that named nothing, and the directives that could not be honoured. | Rewrite every embed into the image each surface renders. |
1052
1057
  | `checkEmbeds` | `function checkEmbeds(body, file, opts)` | {Array<{file: string, line: number, column: number\|undefined, severity: "error", message: string}>} One finding per defect, in source order. | Every defect in one note's embeds, located. |
@@ -1055,25 +1060,26 @@ Raw HTML in a note's prose, reported. **A note is markdown.** What markdown cann
1055
1060
 
1056
1061
  An image saying how wide it is and where it sits. A directive in the curly-attribute convention Pandoc and Kramdown use supplies a width class, a named `size`, and a `float` position. The named size sets a maximum display width for each surface.
1057
1062
 
1058
- | Export | Signature | Returns | Use it when |
1059
- | --------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
1060
- | `IMAGE_CLASSES` | `const IMAGE_CLASSES` | — | The width classes an image may carry, and what each means to a renderer. |
1061
- | `IMAGE_SIZES` | `const IMAGE_SIZES` | — | The named sizes an image directive accepts. |
1062
- | `IMAGE_FLOATS` | `const IMAGE_FLOATS` | — | The `float` positions an image may take, and where each puts it. |
1063
- | `IMAGE_FIGURE_CLASS` | `const IMAGE_FIGURE_CLASS` | — | The class every figure carries, whatever its width or position. |
1064
- | `IMAGE_PATTERN` | `const IMAGE_PATTERN` | — | A markdown image, with the directive it may carry. |
1065
- | `imageSourceProblem` | `function imageSourceProblem(src)` | {string} The problem, as a finding's sentence, or `""`. | What is wrong with an image's address, or `""` when nothing is. |
1066
- | `parseImageDirective` | `function parseImageDirective(raw)` | `{{classes: string[], size: string, float: string, problems: string[]}}` What was written, and what cannot be honoured. | Read the directive on an image. |
1067
- | `figureClasses` | `function figureClasses(directive)` | {string} A space-separated class list. | The classes a figure carries, from a parsed directive. |
1068
- | `escapeHtml` | `function escapeHtml(text)` | {string} The same value, safe in markup. | Text going inside an HTML attribute or between tags. |
1069
- | `imageFigureHtml` | `function imageFigureHtml(image)` | {string} The figure, as one HTML block. | One image as the `<figure>` both HTML surfaces render. |
1070
- | `standsAlone` | `function standsAlone(text, start, end)` | {boolean} Whether the match is a block of its own. | Whether a match sits alone in its own paragraph. |
1071
- | `imagesIn` | `function imagesIn(body)` | {Array<{alt: string, src: string, title: string, directive: string, index: number, length: number, block: boolean}>} One entry per image. | Every image in one body, with its directive and its position. |
1072
- | `imageSourcesIn` | `function imageSourcesIn(body)` | {string[]} The addresses, with repeats. | Every image address one body names, in order of appearance. |
1073
- | `checkImages` | `function checkImages(body, file,` | {Array<{file: string, line: number, column: number\|undefined, severity: "error", message: string}>} One finding per defect, in source order. | Every defect in one note's images. |
1074
- | `lintContentImages` | `function lintContentImages(contentBase,` | `{{findings: Array<{file: string, line: number, column: number\|undefined, severity: "error", message: string}>, files: number}}` What it found. | Walk a content tree and report every image it cannot render as authored. |
1075
- | `renderImageFigures` | `function renderImageFigures(body, resolveSrc)` | {string} The same body, with each block image as a `<figure>`. | Rewrite every block image in a body into the figure the website publishes. |
1076
- | `imagePlugin` | `function imagePlugin()` | {(md: object) => void} A markdown-it plugin. | A markdown-it plugin that reads an image's directive and renders its figure. |
1063
+ | Export | Signature | Returns | Use it when |
1064
+ | --------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
1065
+ | `IMAGE_CLASSES` | `const IMAGE_CLASSES` | — | The width classes an image may carry, and what each means to a renderer. |
1066
+ | `IMAGE_SIZES` | `const IMAGE_SIZES` | — | The named sizes an image directive accepts. |
1067
+ | `IMAGE_FLOATS` | `const IMAGE_FLOATS` | — | The `float` positions an image may take, and where each puts it. |
1068
+ | `IMAGE_FIGURE_CLASS` | `const IMAGE_FIGURE_CLASS` | — | The class every figure carries, whatever its width or position. |
1069
+ | `IMAGE_PATTERN` | `const IMAGE_PATTERN` | — | A markdown image, with the directive it may carry. |
1070
+ | `imageSourceProblem` | `function imageSourceProblem(src)` | {string} The problem, as a finding's sentence, or `""`. | What is wrong with an image's address, or `""` when nothing is. |
1071
+ | `parseImageDirective` | `function parseImageDirective(raw)` | `{{classes: string[], size: string, float: string, problems: string[]}}` What was written, and what cannot be honoured. | Read the directive on an image. |
1072
+ | `roleClass` | `function roleClass(role)` | {string} The class, or `""`. | The class a picture's role names, or `""` for no role or one outside the closed set. |
1073
+ | `figureClasses` | `function figureClasses(directive)` | {string} A space-separated class list. | The classes a figure carries, from a parsed directive and its asset's role. |
1074
+ | `escapeHtml` | `function escapeHtml(text)` | {string} The same value, safe in markup. | Text going inside an HTML attribute or between tags. |
1075
+ | `imageFigureHtml` | `function imageFigureHtml(image)` | {string} The figure, as one HTML block. | One image as the `<figure>` both HTML surfaces render. |
1076
+ | `standsAlone` | `function standsAlone(text, start, end)` | {boolean} Whether the match is a block of its own. | Whether a match sits alone in its own paragraph. |
1077
+ | `imagesIn` | `function imagesIn(body)` | {Array<{alt: string, src: string, title: string, directive: string, index: number, length: number, block: boolean}>} One entry per image. | Every image in one body, with its directive and its position. |
1078
+ | `imageSourcesIn` | `function imageSourcesIn(body)` | {string[]} The addresses, with repeats. | Every image address one body names, in order of appearance. |
1079
+ | `checkImages` | `function checkImages(body, file,` | {Array<{file: string, line: number, column: number\|undefined, severity: "error", message: string}>} One finding per defect, in source order. | Every defect in one note's images. |
1080
+ | `lintContentImages` | `function lintContentImages(contentBase,` | `{{findings: Array<{file: string, line: number, column: number\|undefined, severity: "error", message: string}>, files: number}}` What it found. | Walk a content tree and report every image it cannot render as authored. |
1081
+ | `renderImageFigures` | `function renderImageFigures(body, resolveSrc)` | {string} The same body, with each block image as a `<figure>`. | Rewrite every block image in a body into the figure the website publishes. |
1082
+ | `imagePlugin` | `function imagePlugin()` | {(md: object) => void} A markdown-it plugin. | A markdown-it plugin that reads an image's directive and renders its figure. |
1077
1083
 
1078
1084
  ### `engine.contentTaskLists`
1079
1085
 
@@ -1090,15 +1096,16 @@ A list item opened with `[ ]` or `[x]`, reported. The format has no checkbox: a
1090
1096
 
1091
1097
  One authored pathname, and the four addresses it resolves to. A note names a file once — in `img:`, in `data.portrait:`, in the body of a markdown image — and the first segment says which package owns it when an `assets/` follows. Every surface derives its own address from that one statement: the path inside a Foundry install, the file in the owning repository's tree, the address the website serves, and where the book stages its copy.
1092
1098
 
1093
- | Export | Signature | Returns | Use it when |
1094
- | ----------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
1095
- | `ASSETS_SEGMENT` | `const ASSETS_SEGMENT` | — | The directory a package ships its files in. |
1096
- | `PATHNAME_SURFACES` | `const PATHNAME_SURFACES` | — | The surfaces one authored pathname resolves for. |
1097
- | `pathnameProblem` | `function pathnameProblem(raw)` | {string} The problem, as a finding's sentence, or `""`. | What is wrong with an authored pathname, or `""` when nothing is. |
1098
- | `packageAddresses` | `function packageAddresses(config)` | {Map<string, {root: string\|null, id: string\|null, own: boolean}>} The packages, by package name. | Every content package this build can resolve a pathname against. |
1099
- | `resolvePathname` | `function resolvePathname(raw, config)` | {PathnameForms\|null} The four forms, or `null` when the note names no file. | Resolve one authored pathname into the address each surface serves. |
1100
- | `servesFoundry` | `function servesFoundry(config)` | {boolean} Whether the package being built has a Foundry root. | Whether this build installs anything into a Foundry data directory. |
1101
- | `foundryAddressProblem` | `function foundryAddressProblem(raw, config)` | {string} The problem, as a finding's sentence, or `""`. | Why a pathname has no Foundry address, or `""` when it has one. |
1099
+ | Export | Signature | Returns | Use it when |
1100
+ | ----------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
1101
+ | `ASSETS_SEGMENT` | `const ASSETS_SEGMENT` | — | The directory a package ships its files in. |
1102
+ | `PATHNAME_SURFACES` | `const PATHNAME_SURFACES` | — | The surfaces one authored pathname resolves for. |
1103
+ | `pathnameProblem` | `function pathnameProblem(raw)` | {string} The problem, as a finding's sentence, or `""`. | What is wrong with an authored pathname, or `""` when nothing is. |
1104
+ | `packageAddresses` | `function packageAddresses(config)` | {Map<string, {root: string\|null, id: string\|null, own: boolean}>} The packages, by package name. | Every content package this build can resolve a pathname against. |
1105
+ | `resolvePathname` | `function resolvePathname(raw, config)` | {PathnameForms\|null} The four forms, or `null` when the note names no file. | Resolve one authored pathname into the address each surface serves. |
1106
+ | `assetPathnameKey` | `function assetPathnameKey(src, config)` | {string\|null} The pathname, or `null` for an address naming no asset record to look up. | The pathname one authored image address keys an asset's role and pixel size under, whichever form it was written in. |
1107
+ | `servesFoundry` | `function servesFoundry(config)` | {boolean} Whether the package being built has a Foundry root. | Whether this build installs anything into a Foundry data directory. |
1108
+ | `foundryAddressProblem` | `function foundryAddressProblem(raw, config)` | {string} The problem, as a finding's sentence, or `""`. | Why a pathname has no Foundry address, or `""` when it has one. |
1102
1109
 
1103
1110
  ### `engine.contentLinks`
1104
1111
 
@@ -1233,11 +1240,13 @@ What a `[[…]]` **is**, before anything decides where it points. One authored l
1233
1240
  `buildSiteIndex` gathers addressable pages and ambiguity findings. `wikiContext`
1234
1241
  provides the per-page context used when resolving a link such as `[[Something]]`.
1235
1242
 
1236
- | Export | Signature | Returns | Use it when |
1237
- | ------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
1238
- | `buildSiteIndex` | `function buildSiteIndex(entries,` | {SiteIndex} The index, and what could not be addressed unambiguously. | Build the address index a site's wikilink resolver reads. |
1239
- | `wikiContext` | `function wikiContext(built,` | {object} The resolver context. | The per-page context a wikilink resolver takes. |
1240
- | `resolveInfoboxRef` | `function resolveInfoboxRef(siteIndex, ref, hint)` | {{name?: string, url?: string, address?: string, subType?: string}\|undefined} The page, or `undefined` where nothing answers. | Resolve one infobox reference — a bare shortcode, a short address or a canonical one — against a site index. |
1243
+ | Export | Signature | Returns | Use it when |
1244
+ | ------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
1245
+ | `buildSiteIndex` | `function buildSiteIndex(entries,` | {SiteIndex} The index, and what could not be addressed unambiguously. | Build the address index a site's wikilink resolver reads. |
1246
+ | `figureIndexKeys` | `function figureIndexKeys(pkg, type, shortcode)` | {Set<string>} The short `type/shortcode` key and the canonical `package-system-type-shortcode` one. | Get the address keys one page's figures are reachable by, matching `buildSiteIndex`'s own. |
1247
+ | `resolveCrossNoteFigures` | `function resolveCrossNoteFigures(target, options)` | {{url: string, figures: Map<string, object>}\|undefined} The target's own address and figures, or `undefined` where no note resolves. | Resolve a `ref` call's cross-note address to the target's own figures, the way a wikilink would. |
1248
+ | `wikiContext` | `function wikiContext(built,` | {object} The resolver context. | The per-page context a wikilink resolver takes. |
1249
+ | `resolveInfoboxRef` | `function resolveInfoboxRef(siteIndex, ref, hint)` | {{name?: string, url?: string, address?: string, subType?: string}\|undefined} The page, or `undefined` where nothing answers. | Resolve one infobox reference — a bare shortcode, a short address or a canonical one — against a site index. |
1241
1250
 
1242
1251
  ### `engine.pdfToc`
1243
1252
 
@@ -1589,9 +1598,11 @@ HM3's Item pass — the parts of compiling a note into an HM3 Item that are fact
1589
1598
 
1590
1599
  HM3's Actor pass — what an HM3 `character` or `creature` holds. A being note authors which HM3 subtype it becomes via `hm3.type` (never defaulted), and only four of the content format's `being` mapping-table rows have an HM3 destination (`portrait`, `species`, `gender`, `occupation`, `templatePriority`) plus the two anchored prose sections (`{#appearance}` → `description`, `{#dossier}` → `biography`); `gender` and `occupation` are written only on a `character`.
1591
1600
 
1592
- | Export | Signature | Returns | Use it when |
1593
- | ----------- | ----------------- | ------- | ------------------------------------------------------------------ |
1594
- | `Hm3Actors` | `class Hm3Actors` | — | not called directly — imported and driven by `engine/generate.mjs` |
1601
+ | Export | Signature | Returns | Use it when |
1602
+ | ------------------ | ------------------------ | ------- | ----------------------------------------------------------------------------- |
1603
+ | `Hm3Actors` | `class Hm3Actors` | — | not called directly — imported and driven by `engine/generate.mjs` |
1604
+ | `ACTOR_FIELDS` | `const ACTOR_FIELDS` | — | reading the fields native to every HM3 actor subtype |
1605
+ | `CHARACTER_FIELDS` | `const CHARACTER_FIELDS` | — | reading the fields `hm3.actors` declares on `character` and not on `creature` |
1595
1606
 
1596
1607
  ### `hm3.templatePriority`
1597
1608
 
@@ -1631,6 +1642,7 @@ const config = defineConfig({
1631
1642
  | `defineConfig` | `defineConfig(config)` | `ContentBuildConfig` — the frozen, defaulted configuration | validating and normalizing a content configuration; every configuration reaches this function whether authored as YAML or as `.mjs` |
1632
1643
  | `PACKAGE_KINDS` | `const PACKAGE_KINDS` | — | reading the three kinds of Foundry package a content module can be built into — `systems`, `modules`, and `documentation`, the kind that compiles no Foundry document at all (also the Foundry install directory name for the first two) |
1633
1644
  | `DOCUMENTATION_KIND` | `const DOCUMENTATION_KIND` | — | naming the `packageKind` value that compiles no Foundry documents, spelled once so the validator, the CLI and the compile passes cannot disagree about what it means |
1645
+ | `RELATIONSHIP_KEYS` | `const RELATIONSHIP_KEYS` | `readonly string[]` | reading every key a declared relationship may carry — the configuration check refuses anything else, and the manifest writer drops the ones Foundry's own schema does not name |
1634
1646
  | `compilesFoundryDocuments` | `compilesFoundryDocuments(config)` | `boolean` | checking whether a resolved configuration compiles Foundry documents at all — false only for a `documentation` package |
1635
1647
  | `PACK_DOCUMENT_TYPES` | `const PACK_DOCUMENT_TYPES` | — | reading the Foundry document types this toolchain is able to compile a compendium pack of |
1636
1648
  | `DEFAULT_PATHS` | `const DEFAULT_PATHS` | — | reading the conventional directory layout a build reads from and writes to, relative to `rootDir` |
@@ -1868,7 +1880,8 @@ console.log(normalizeRepoUrl("git@github.com:HeroicLands/sohl.git"));
1868
1880
  | `metadataUrl` | `metadataUrl({ repoUrl, version, contentPackage })` | `string` — the version-pinned asset URL | computing where this release publishes its content index, pinned to this version like `download` so a dependency's manifest and its fetched index always describe the same release |
1869
1881
  | `manifestPacks` | `manifestPacks(config)` | `object[]` | deriving the manifest's `packs` array from the one pack list the build already has, in build order, companions flattened in |
1870
1882
  | `packFolderFindings` | `packFolderFindings({ packFolders, packs = [] })` | `Array<{severity: "error"\|"warning", message: string, pack: string, folder?: string, keyPath: Array<string\|number>}>` | checking a declared `packFolders` against the derived pack list — a folder naming a pack that does not exist is an error, a pack no folder names is a warning |
1871
- | `BUILD_ONLY_RELATIONSHIP_KEYS` | `const BUILD_ONLY_RELATIONSHIP_KEYS` | — | reading which `relationships` keys direct the build (e.g. `itemCatalog`) rather than describe the published package, so they can be filtered out of what ships |
1883
+ | `FOUNDRY_RELATIONSHIP_KEYS` | `const FOUNDRY_RELATIONSHIP_KEYS` | `readonly string[]` — `id`, `type`, `manifest`, `compatibility`, `reason` | reading which relationship keys Foundry's own schema names, which is what makes every other declared key the build's own |
1884
+ | `BUILD_ONLY_RELATIONSHIP_KEYS` | `const BUILD_ONLY_RELATIONSHIP_KEYS` | `readonly string[]` — derived | reading which `relationships` keys direct the build rather than describe the published package, so they can be filtered out of what ships |
1872
1885
  | `publishedRelationships` | `publishedRelationships(relationships)` | `Record<string, unknown>` | producing the `relationships` block as it should be published, with `BUILD_ONLY_RELATIONSHIP_KEYS` dropped and everything else preserved in order |
1873
1886
  | `buildManifest` | `buildManifest({ config, packageJson, artifact, flags })` | `object` — the manifest, ready to serialise | assembling the full Foundry package manifest object from the resolved configuration, without writing it anywhere |
1874
1887
  | `writeManifest` | `async writeManifest({ config, packageJson, artifact, outDir, flags, configFile })` | `Promise<{path: string, manifest: object}>` | building the manifest and writing it into the staged package; refuses to write when `packFolders` names a pack the build does not produce |
@@ -28,4 +28,33 @@ The embed's label is alternative text. `![[icon-harbor|]]` is decorative. `size=
28
28
 
29
29
  Record provenance for the assets you ship. Put `provenance.yaml` in an asset directory to describe files there and below, or put a YAML sidecar beside one asset to give that file its own record. A sidecar takes precedence over a directory record for that asset. An asset with no record still enters the index with blank provenance fields. Keep attribution with the authored asset so movement through the tree does not lose its source or license. See the [asset record reference](../reference/format-details.md#the-asset-record) for the accepted keys, inheritance, and examples.
30
30
 
31
+ A directory of portraits also states what the pictures in it are for:
32
+
33
+ ```yaml
34
+ attribution: Tom Rodriguez
35
+ license: CC-BY-SA-4.0
36
+ role: portrait
37
+ ```
38
+
39
+ **A hero image is 1792×768.** `data.banner` fills one fixed strip wherever it is drawn, so every banner is cut to that size and a picture of any other size is a finding naming both sizes — resize the picture to match. That is the one size any art field requires. A map's `bgImage` sets its own scene's dimensions, so that asks nothing of a picture's dimensions either.
40
+
41
+ `data.banner` reads in one of three states:
42
+
43
+ | A note writes | The page draws |
44
+ | ------------------ | -------------------------------------------------- |
45
+ | an asset Address | that picture |
46
+ | a blank | no picture, on purpose, and no default replaces it |
47
+ | nothing, or `null` | a default picture chosen by the note's `type` |
48
+
49
+ Omitting the key does not mean "no hero image" — it means artwork chosen for
50
+ the author, which inverts the ordinary reading of an absent key. **A note
51
+ declines with a blank, not with `none`.** `none` parses as an ordinary asset
52
+ Address — a shortcode, not a keyword — and since nothing in the corpus
53
+ answers to it, the build drops the key outright and the page falls through to
54
+ its type's default: the opposite of a decline.
55
+
56
+ `role` is one of `portrait`, `emblem`, `banner`, `plate` or `map`, and belongs to the `image` type only — declaring it in a `provenance.yaml` under `assets/icons` is a finding, since an icon carries one nominal size per medium whatever the file holds. Leave the key out for an ordinary picture; there is no default role name, only its absence.
57
+
58
+ **A role decides how large a picture written in prose draws, when nothing else states a size.** Each medium maps a role to a maximum, and the drawn size is the smaller of that slot and what the file's own pixels support — see [what a role draws at](../reference/format-details.md#what-a-role-draws-at). A named `size=` beside the embed still overrides it outright; an icon draws at the medium's nominal icon size whatever role it might otherwise have had, because `role` never reaches an icon address at all.
59
+
31
60
  The build writes transformed or optimized copies under `build/`; the authored file remains the input for the next build. Output paths are derived from the owning package, asset type, and build target. The [icon guide](../guides/icons.md) covers generated icon variants and placement in Foundry.
@@ -7,7 +7,7 @@ subType: howto
7
7
 
8
8
  # Dates, calendars, and eras
9
9
 
10
- A canonical date uses the world's year and day count without choosing a culture's calendar. Write `<year>[.<day>[:HHMMSS]]`: `720` covers year 720, `720.136` covers its day 136, and `720.136:143005` identifies 14:30:05 on that day. The dot separates the year from the **day of year**, not a month. Negative years are valid, and the day must fit the world's declared year length. Prefix a date with `~` when its uncertainty extends outside the stated year, day, or second. `unknown` records an occurrence whose date is not known, in fields that permit it.
10
+ A canonical date uses the world's year and day count without choosing a culture's calendar. Write `<year>[.<day>[:HHMMSS]]`: `720` covers year 720, `720.136` covers its day 136, and `720.136:143005` identifies 14:30:05 on that day. The dot separates the year from the **day of year**, not a month. Negative years are valid, and the day must fit the world's declared year length — `data.year.days` on its `place` note. A tree whose notes declare no world year bounds no day at all: every value is read as written, however large, because there is no year length to check it against. Prefix a date with `~` when its uncertainty extends outside the stated year, day, or second. `unknown` records an occurrence whose date is not known, in fields that permit it.
11
11
 
12
12
  **Put a date that states a day in quotes.** Write `born: "667.130"`, not `born: 667.130`. Unquoted, YAML reads the scalar as a number, and a number keeps no trailing zero — `667.130` becomes `667.13`, which is day 13 rather than day 130, and the authored digits are gone before any reader of the date can object. A date whose lost zero would still name a day inside the world's year is therefore refused, naming the line and both readings, so quoting it is the fix. A bare year such as `born: 667` loses nothing and needs no quotes.
13
13
 
@@ -85,3 +85,39 @@ package-build dateto commoncal 326.114 --format long
85
85
  The CLI `datefrom` command requires a day and reads the default (`std`, or first) pattern. `dateto` prints the era covering the canonical day. A clock time makes a date exact; a year or day without one spans the stated interval. `data.epoch` and `data.moon.newOn` require a day. The [detailed date rules](../reference/format-details.md#dates-and-calendars) describe precision and bounds.
86
86
 
87
87
  In prose, `{{dateformat "commoncal" data.born}}` prints a date in the chosen calendar. `{{dateformat data.calendar data.born}}` uses the note's calendar Address. A third argument chooses another named pattern. A being or place can set `data.calendar`; its infobox uses that calendar for dates.
88
+
89
+ ## Write a recurring date
90
+
91
+ A `lore` note states its dated occurrences under `data.events`, a list — a note may record a founding once and an annual festival beside it. Each entry is `{ when, until?, recurs? }`, with `when` the occurrence's anchor and first instance:
92
+
93
+ ```yaml
94
+ data:
95
+ events:
96
+ - when: 412.1
97
+ recurs: { every: 1 }
98
+ - when: 689.5
99
+ until: 1203.9
100
+ recurs: { every: 514 }
101
+ ```
102
+
103
+ The first entry is annual, with no end. The second recurs every five hundred fourteen years and stopped after canonical year 1203; `recurs.every` always counts whole years on the canonical axis. In place of a period, `recurs.on` lists the dates somebody actually recorded, strictly increasing and each later than `when`:
104
+
105
+ ```yaml
106
+ data:
107
+ events:
108
+ - when: 326.1
109
+ recurs:
110
+ on: ["412.1", "689.5"]
111
+ ```
112
+
113
+ A build computes the next occurrence against the world's declared present the way it computes a being's age against it, and publishes it on the web page, in the content index, and in the infobox.
114
+
115
+ **A day that recurs every year, with no founding date of its own, writes year `0`** — quoted, since every canonical date stating a day must be:
116
+
117
+ ```yaml
118
+ data:
119
+ events:
120
+ - when: "0.286"
121
+ ```
122
+
123
+ `when: "0.286"` is the two-hundred-eighty-sixth day of the year, every year — a harvest rite nobody dates to a founding. `recurs` is refused beside it, since the entry is already annual; `until` is allowed, and bounds how long the rite was kept.