@heroiclands/package-build 22.22.0 → 22.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/content-commands.mjs +51 -14
  3. package/ci/content-format-reference.mjs +1 -1
  4. package/content-config.mjs +80 -2
  5. package/docs/api.md +85 -72
  6. package/docs/authoring/assets.md +14 -0
  7. package/docs/authoring/dates-and-calendars.md +37 -1
  8. package/docs/authoring/links-and-markup.md +112 -31
  9. package/docs/commands.md +12 -10
  10. package/docs/configuration.md +90 -13
  11. package/docs/content-format.md +1 -1
  12. package/docs/guides/book.md +5 -4
  13. package/docs/reference/format-details.md +323 -60
  14. package/docs/reference/note-types.md +9 -9
  15. package/engine/anchors.mjs +16 -10
  16. package/engine/art-fields.mjs +118 -0
  17. package/engine/art-slots.mjs +7 -1
  18. package/engine/asset-index.mjs +160 -10
  19. package/engine/asset-replacement.mjs +93 -0
  20. package/engine/asset-types.mjs +240 -0
  21. package/engine/base-compiler.mjs +54 -2
  22. package/engine/bundles.mjs +13 -1
  23. package/engine/content-blocks.mjs +4 -4
  24. package/engine/content-embeds.mjs +19 -0
  25. package/engine/content-figures.mjs +468 -0
  26. package/engine/content-footnotes.mjs +10 -2
  27. package/engine/content-format.yaml +1 -1
  28. package/engine/content-images.mjs +126 -25
  29. package/engine/content-index.mjs +36 -10
  30. package/engine/field-reference.mjs +31 -10
  31. package/engine/field-spec.mjs +10 -8
  32. package/engine/foreign-catalog.mjs +143 -18
  33. package/engine/frontmatter-lint.mjs +73 -1
  34. package/engine/heading-attributes.mjs +31 -18
  35. package/engine/helpers.mjs +97 -30
  36. package/engine/infobox-registry.mjs +4 -1
  37. package/engine/infobox.mjs +47 -8
  38. package/engine/itinerary-scenes.mjs +4 -10
  39. package/engine/journals.mjs +264 -39
  40. package/engine/lore-events.mjs +197 -0
  41. package/engine/markdown-expressions.mjs +257 -7
  42. package/engine/note-dates.mjs +305 -12
  43. package/engine/note-vocabulary.mjs +11 -5
  44. package/engine/pathnames.mjs +36 -0
  45. package/engine/pdf-build.mjs +357 -34
  46. package/engine/pdf-images.mjs +64 -0
  47. package/engine/pdf-render.mjs +160 -36
  48. package/engine/scenes.mjs +16 -4
  49. package/engine/site-build.mjs +127 -13
  50. package/engine/site-index.mjs +72 -1
  51. package/engine/site-preview.mjs +2 -0
  52. package/engine/system-block.mjs +44 -16
  53. package/hm3/actors.mjs +2 -2
  54. package/manifest.mjs +30 -24
  55. package/package.json +1 -1
  56. package/packagebuild-metadata.jsonl +13 -13
  57. package/sohl/item-fields.mjs +4 -3
  58. package/types/content-config.d.mts +48 -1
  59. package/types/engine/anchors.d.mts +9 -4
  60. package/types/engine/art-fields.d.mts +64 -0
  61. package/types/engine/asset-index.d.mts +43 -0
  62. package/types/engine/asset-replacement.d.mts +22 -0
  63. package/types/engine/asset-types.d.mts +18 -0
  64. package/types/engine/base-compiler.d.mts +0 -17
  65. package/types/engine/content-embeds.d.mts +16 -0
  66. package/types/engine/content-figures.d.mts +101 -0
  67. package/types/engine/content-footnotes.d.mts +7 -0
  68. package/types/engine/content-images.d.mts +78 -12
  69. package/types/engine/content-index.d.mts +8 -2
  70. package/types/engine/field-reference.d.mts +21 -0
  71. package/types/engine/field-spec.d.mts +20 -16
  72. package/types/engine/foreign-catalog.d.mts +39 -0
  73. package/types/engine/heading-attributes.d.mts +13 -8
  74. package/types/engine/helpers.d.mts +23 -7
  75. package/types/engine/infobox-registry.d.mts +4 -1
  76. package/types/engine/infobox.d.mts +6 -0
  77. package/types/engine/journals.d.mts +31 -9
  78. package/types/engine/lore-events.d.mts +14 -0
  79. package/types/engine/markdown-expressions.d.mts +23 -2
  80. package/types/engine/note-dates.d.mts +64 -1
  81. package/types/engine/pathnames.d.mts +26 -0
  82. package/types/engine/pdf-build.d.mts +28 -6
  83. package/types/engine/pdf-images.d.mts +52 -0
  84. package/types/engine/pdf-render.d.mts +12 -0
  85. package/types/engine/site-build.d.mts +14 -3
  86. package/types/engine/site-index.d.mts +54 -0
  87. package/types/engine/system-block.d.mts +26 -2
  88. package/types/hm3/actors.d.mts +18 -0
  89. package/types/manifest.d.mts +19 -23
  90. package/engine/content-captions.mjs +0 -230
  91. package/types/engine/content-captions.d.mts +0 -58
@@ -352,7 +352,7 @@ order without changing the Markdown body.
352
352
 
353
353
  #### Dates and calendars
354
354
 
355
- A canonical date is `<year>[.<day>[:HHMMSS]]`. The day is one-based within the world's year. `720` covers the whole year; `720.136` covers that day; `720.136:143005` identifies one second. Negative years are valid. Prefix either form with `~` to express uncertainty beyond its written interval. `unknown` is allowed only in fields that accept an unknown occurrence.
355
+ A canonical date is `<year>[.<day>[:HHMMSS]]`. The day is one-based within the world's year, bound against `data.year.days` on the world's `place` note. A tree whose notes declare no world year bounds no day: the value is read as written, however large. `720` covers the whole year; `720.136` covers that day; `720.136:143005` identifies one second. Negative years are valid. Prefix either form with `~` to express uncertainty beyond its written interval. `unknown` is allowed only in fields that accept an unknown occurrence.
356
356
 
357
357
  **Quote a canonical date that states a day.** `born: "667.130"` is read as written; unquoted, YAML parses the scalar as a number and a trailing zero on the day is gone before any date reader sees it, so `667.130` and `667.13` arrive as one value. A number-valued date is an error wherever appending a zero to its day would still name a day inside the world's year — the message names the file, the line and both readings — and is read as written where no such day exists, since `675.2810` is no day of a 365-day year. A bare year loses nothing and needs no quotes.
358
358
 
@@ -701,12 +701,12 @@ exactly as `seat` declares `place` — a bare shortcode takes its type from the
701
701
  declaration, and qualification climbs the same short-form ladder every other
702
702
  link uses.
703
703
 
704
- | field | default type | accepts | resolves into | on |
705
- | ----------- | ------------ | --------------- | ---------------------------- | ----------------------------------------- |
706
- | `icon` | `icon` | `icon`, `image` | `document.img` | every Actor/Item type, and embedded items |
707
- | `tokenIcon` | `icon` | `icon`, `image` | `prototypeToken.texture.src` | Actor types |
708
- | `bgImage` | `image` | `icon`, `image` | `background.src` | map types |
709
- | `banner` | `image` | `icon`, `image` | the site hero image | any note; not a Foundry field |
704
+ | field | default type | accepts | resolves into | on |
705
+ | ----------- | ------------ | --------------- | ----------------------------- | ----------------------------------------- |
706
+ | `icon` | `icon` | `icon`, `image` | `document.img` | every Actor/Item type, and embedded items |
707
+ | `tokenIcon` | `icon` | `icon`, `image` | `prototypeToken.texture.src` | Actor types |
708
+ | `bgImage` | `image` | `icon`, `image` | `background.src` | map types |
709
+ | `banner` | `image` | `icon`, `image` | the site hero image, 1792×768 | any note; not a Foundry field |
710
710
 
711
711
  All four are authored under `data:`:
712
712
 
@@ -750,7 +750,7 @@ noticed:
750
750
  | `packFolder` | `Address` or pack map | The shared compendium folder Address. |
751
751
  | `harnworld` | `Map<string, unknown>` | HârnWorld source details. |
752
752
  | `icon` | `Address` | The document's profile art — an `icon` address, resolved into `img`. |
753
- | `banner` | `Address` | The page's hero image — an `image` address. |
753
+ | `banner` | `Address` | The page's hero image — an `image` address, cut to 1792×768. |
754
754
 
755
755
  `icon` is legal everywhere because most types compile into a document that
756
756
  carries one; where a type's passes emit none, the lint says so and the value is
@@ -853,12 +853,12 @@ ships as the Foundry module `harn-ensemble`) writing `images/map.webp`:
853
853
  And for the same note writing `sohl/assets/icons/noun/shield.svg`, a file the
854
854
  system ships and this repository does not hold:
855
855
 
856
- | Surface | Address |
857
- | ----------- | --------------------------------------------------------- |
858
- | **Foundry** | `systems/sohl/assets/icons/noun/shield.svg` |
859
- | **Local** | `assets/icons/noun/shield.svg`, in the `sohl` repository |
860
- | **Web** | `https://cdn.heroiclands.org/sohl/icons/noun/shield.svg` |
861
- | **Book** | not carried — a build stages only what this package ships |
856
+ | Surface | Address |
857
+ | ----------- | ------------------------------------------------------------- |
858
+ | **Foundry** | `systems/sohl/assets/icons/noun/shield.svg` |
859
+ | **Local** | `assets/icons/noun/shield.svg`, in the `sohl` repository |
860
+ | **Web** | `https://cdn.heroiclands.org/sohl/icons/noun/shield.svg` |
861
+ | **Book** | not carried, unless a declared replacement carries it instead |
862
862
 
863
863
  **The content package name and Foundry id are separate values.**
864
864
  `harnensemble` is what the content is called, what a note writes, and what the
@@ -884,6 +884,29 @@ index enabled, an asset with no matching note is a located build error. See
884
884
  [asset bindings](../configuration.md#packagebuildstagedir-and-packagebuildassets)
885
885
  for the configuration and cache requirements.
886
886
 
887
+ **A package declared as a replacement is carried into the book, from its
888
+ fetched archive, when it answers the address; every other foreign package
889
+ stays not carried.** A relationship declaring `assetReplacement: true` names a
890
+ dependency whose asset tree answers this package's own addresses first — see
891
+ [`relationships`](../configuration.md#relationships) — and the book is the one
892
+ surface that otherwise refuses a foreign file outright, so it is the one
893
+ surface this changes. A `thalorna` note embedding
894
+ `![[thalornaaltart-none-icon-anvil|An anvil]]` names `thalornaaltart`'s own
895
+ address, exactly as a note citing `sohl`'s shared bestiary art would; when
896
+ `thalornaaltart` is declared with `assetReplacement: true` and its fetched
897
+ archive carries `icon-anvil` under its own package, the book stages that file
898
+ from the archive instead of printing the caption with no picture. A note
899
+ citing a different package's own address this way — `sohl`'s bestiary art,
900
+ say — stays refused exactly as the row above states, because `sohl` was never
901
+ declared as a replacement: a package answers in place of another only when it
902
+ opted in, never because its archive happens to carry a file at the same type
903
+ and shortcode.
904
+
905
+ A relationship declaring `assetReplacement: true` whose archive was never
906
+ fetched is a configuration mistake, not an ordinary unresolved address: the
907
+ book reports a located error naming the relationship and instructing
908
+ `package-build deps fetch`, rather than printing a caption with no picture.
909
+
887
910
  **The host is configuration.** `site.assets` in `package-build.config.yaml` is
888
911
  the root the web form is joined onto, and a package-owned image on a page with
889
912
  none set is an error naming that key.
@@ -927,6 +950,18 @@ Foundry destination: searching a built `packs-json` tree for it turns up
927
950
  nothing. It reaches the generated page and the book's section plates, and
928
951
  nothing else.
929
952
 
953
+ **A hero image is 1792×768.** It fills one fixed strip wherever it is drawn, so
954
+ every banner is cut to that size and a picture of any other size is a finding
955
+ naming both sizes — resize the picture to match. The size is stated once, by the
956
+ slot itself, which is what gives an artist a figure to work to before any file
957
+ exists.
958
+
959
+ No other art slot states a size, and none of them asks anything of a picture's
960
+ dimensions. An `icon` is drawn at a nominal size per medium whatever its file
961
+ holds; a map's `bgImage` sets its own scene's dimensions, so it is whatever size
962
+ the map is; and a picture written in prose is fitted to the room it has. Those
963
+ three accept whatever they are given.
964
+
930
965
  It is an `Address` all the same, defaulting to type `image`, so a section note
931
966
  writes `banner: skillbnr` and a note borrowing another package's plate writes
932
967
  `banner: packagebuild-none-image-skillbnr`. The resolved record carries the
@@ -1685,14 +1720,17 @@ carries no frontmatter, no anchors and no `foundry` block: a file declares
1685
1720
  nothing about itself, compiles into no document, and publishes no page, so the
1686
1721
  `address` holds the canonical key and no page slug.
1687
1722
 
1688
- | `asset` field | Source | What it says |
1689
- | ------------- | ---------- | ------------------------------------------------------------------ |
1690
- | `path` | the walk | Where the file sits inside the emitting package's asset directory. |
1691
- | `attribution` | provenance | The person holding the rights, to whom attribution is legally due. |
1692
- | `source` | provenance | Where it came from — a URL, or a sentence. |
1693
- | `ai` | provenance | Whether the file is machine-generated — `true` or `false`. |
1694
- | `license` | provenance | The licence it is used under — an SPDX identifier, or terms. |
1695
- | `notes` | provenance | Anything else a person reading the attribution needs. |
1723
+ | `asset` field | Source | What it says |
1724
+ | ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1725
+ | `path` | the walk | Where the file sits inside the emitting package's asset directory. |
1726
+ | `attribution` | provenance | The person holding the rights, to whom attribution is legally due. |
1727
+ | `source` | provenance | Where it came from — a URL, or a sentence. |
1728
+ | `ai` | provenance | Whether the file is machine-generated — strictly the YAML boolean `true` or `false`; any other value is a finding, never a stringified record. Unstated resolves to `false`. |
1729
+ | `license` | provenance | The licence it is used under — an SPDX identifier, or terms. |
1730
+ | `notes` | provenance | Anything else a person reading the attribution needs. |
1731
+ | `role` | provenance | What the picture is for — `portrait`, `emblem`, `banner`, `plate` or `map`. Absent for an ordinary picture; a value outside that set is a finding. The `image` type only — a value on an `icon` address is a finding and is dropped. |
1732
+ | `width` | the walk | The file's pixel width, read from its own header. Blank for an SVG, which has no pixel dimensions. |
1733
+ | `height` | the walk | The file's pixel height, read from its own header. Blank for an SVG, which has no pixel dimensions. |
1696
1734
 
1697
1735
  Every field is present on every record, blank where nothing states one. A fixed
1698
1736
  shape is what lets a consumer read `asset.license` without first asking whether
@@ -1711,6 +1749,90 @@ joins its own root onto it:
1711
1749
  Foundry paths use the **Foundry package id**. The two values are both `thalorna`
1712
1750
  for this image; `harnensemble` content uses `harn-ensemble` in its Foundry paths.
1713
1751
 
1752
+ ##### What a role declares
1753
+
1754
+ `role` names what the picture is for, from a closed set:
1755
+
1756
+ | Role | What it names |
1757
+ | ---------- | -------------------------------------------------------- |
1758
+ | `portrait` | A picture of a subject — a being, a building, an object. |
1759
+ | `emblem` | A device, arms, a sigil — a mark rather than a scene. |
1760
+ | `banner` | A wide strip across the head of a page or a book plate. |
1761
+ | `plate` | A full-measure illustration meant to be looked at. |
1762
+ | `map` | A plan or chart, read by examining its detail. |
1763
+
1764
+ An asset declaring no role is an ordinary picture: there is no default role
1765
+ name, only the absence of one. A value outside this set is refused with a
1766
+ finding naming the file, the key and the accepted values, and the record
1767
+ carries no role rather than the one it was given. `role` belongs to the
1768
+ `image` type only — an `icon` address carries one nominal size per medium
1769
+ whatever the file holds, so a `role` declared there is a finding and is
1770
+ dropped rather than carried through.
1771
+
1772
+ A declared role makes two checks possible. **Shape**, an error, flags an
1773
+ asset whose aspect ratio departs from the median of every other asset
1774
+ sharing its role — the expectation is derived from the role-sharing group
1775
+ itself at build time, never from a second, hand-kept table. **Resolution**,
1776
+ a warning, flags an asset whose pixel width falls short of what its role's
1777
+ largest print slot needs. Both exempt a vector asset, which carries no pixel
1778
+ dimensions and no aspect that survives being drawn at a nominal size.
1779
+
1780
+ `role: map` also selects a `:::figure` fence's counter — see
1781
+ [figures and numbered references](../authoring/links-and-markup.md#figures-and-numbered-references).
1782
+ A fence whose picture draws an address with that role counts as `Map` rather
1783
+ than `Figure`, on every surface that numbers it, because the role is the one
1784
+ fact the fence's own markup cannot state.
1785
+
1786
+ ##### What a role draws at
1787
+
1788
+ A role names what a picture is for; it says nothing about how large one
1789
+ draws. Deciding that is a medium's own job — the book owns a table mapping
1790
+ each role to a maximum, and a website's or a Foundry system's stylesheet owns
1791
+ the equivalent table for its own surface, keyed off the role class and the
1792
+ `width`/`height` attributes the emitted markup carries (blank for a vector).
1793
+ Changing a role's slot is one line in that table, not an edit to every note
1794
+ drawing one.
1795
+
1796
+ **Every size is a maximum, never a target.** The drawn width is the smaller of
1797
+ the role's slot and what the file's own pixels support at the medium's print
1798
+ floor:
1799
+
1800
+ > drawn width = min(the role's slot, natural pixels ÷ the print floor)
1801
+
1802
+ A picture is never drawn larger than it was made: a 512px portrait stretched
1803
+ across the slot a `plate` fills is a soft rectangle that reads as broken. A
1804
+ vector carries no pixel count, so nothing narrows its role's slot; it always
1805
+ draws at the slot. An `icon` address takes the medium's nominal icon size
1806
+ whatever role it might otherwise have had — `role` never reaches one, since
1807
+ the asset record refuses it there.
1808
+
1809
+ The book's table, and the print floor it reads from the same constant its page
1810
+ geometry is set against (300 dpi):
1811
+
1812
+ | Role | The book's slot |
1813
+ | --------------------------- | --------------------------------- |
1814
+ | `portrait`, `emblem` | half the page's text measure |
1815
+ | `banner`, `plate`, `map` | the full page's text measure |
1816
+ | an `icon` address, any role | one inch, whatever the file holds |
1817
+
1818
+ On this book's US Letter page the text measure is about 7.00in, so a
1819
+ `portrait` or an `emblem` draws at up to about 3.50in and a `banner`, a
1820
+ `plate` or a `map` draws at up to about 7.00in — still the smaller of that
1821
+ and what the file's pixels support.
1822
+
1823
+ **A worked example.** A `portrait`-role file measuring 900×1350px, drawn with
1824
+ no named `size=`: at 300 dpi its pixels support 900 ÷ 300 = 3.00in, which is
1825
+ narrower than the role's 3.50in slot, so the book draws it at 3.00in. The same
1826
+ picture at 1200×1800px supports 4.00in, wider than the slot, so the book draws
1827
+ it at its 3.50in slot instead — the slot bounds it, exactly as a maximum
1828
+ should. Either way the emitted markup for the website and for a Foundry
1829
+ journal carries `note-image-role-portrait` as a class and `900`/`1350` (or
1830
+ `1200`/`1800`) as its `width`/`height` attributes, which is what a stylesheet
1831
+ on either surface sizes a `portrait` from.
1832
+
1833
+ A named `size=` written beside the embed overrides a role's slot outright, the
1834
+ same as it always has — a role decides the size nothing else states.
1835
+
1714
1836
  ##### Where provenance comes from
1715
1837
 
1716
1838
  A `provenance.yaml` states the provenance fields in the table above, and nothing
@@ -1738,9 +1860,40 @@ resolving a chain by hand.
1738
1860
  That is also why `attribution` and `license` are **required in any record that
1739
1861
  exists**: a key left out is not inherited from above, it is simply absent, so a
1740
1862
  record stating neither leaves every file it covers with no rights holder and no
1741
- terms. Omitting one is a finding. `source`, `ai` and `notes` stay optional,
1863
+ terms. Omitting one is a finding. `source`, `ai`, `notes` and `role` stay optional,
1742
1864
  because a blank is a truthful answer for each of them.
1743
1865
 
1866
+ ##### A declared replacement resolves first
1867
+
1868
+ A package may declare, on a `relationships.<kind>[]` entry, that a
1869
+ dependency's asset tree answers this package's own asset addresses ahead of
1870
+ its local record — `assetReplacement: true` in `package-build.config.yaml`.
1871
+ Once declared, an asset address resolves in this order:
1872
+
1873
+ 1. **Every declared replacement, in declaration order.** The package segment
1874
+ of each relationship declaring `assetReplacement: true` is tried in the
1875
+ order those relationships are written, and the first one carrying the
1876
+ address wins.
1877
+ 2. **Falling through to the package's own record** when no declared
1878
+ replacement carries the address — the resolution this package already
1879
+ performs with no `assetReplacement` declared at all.
1880
+
1881
+ **This reaches asset types only.** A being, an item or a lore note is never
1882
+ replaced this way, whatever a relationship declares: the rule tests the
1883
+ address's type, not the relationship. A replacement answers only `icon`,
1884
+ `image` and `audio` addresses whose package segment is this package's own; an
1885
+ address naming any other type, or naming a different package, is untouched.
1886
+
1887
+ **A replacement's own output address is never itself looked up against
1888
+ another replacement.** Resolving `thalornaaltart-none-image-thorn` never asks
1889
+ whether something replaces `thalornaaltart` — resolution is a single pass over
1890
+ the declared list, not a chain, which forecloses a cycle by construction.
1891
+
1892
+ This is distinct from a `foreign` lookup, which serves a declared dependency's
1893
+ own content for an address this package's local index does not carry. A
1894
+ replacement is consulted **before** the local address; a foreign lookup only
1895
+ **after** it misses locally.
1896
+
1744
1897
  #### A font is not an asset
1745
1898
 
1746
1899
  `assets/fonts` is not a root, and a font has no address.
@@ -2089,7 +2242,7 @@ See the [authoring examples](../authoring/links-and-markup.md#footnotes-and-defi
2089
2242
  ```
2090
2243
 
2091
2244
  Any header can end with a braced attribute block, in the same grammar a
2092
- `:::caption` and a named block take. Inside the braces:
2245
+ `:::figure` fence and a named block take. Inside the braces:
2093
2246
 
2094
2247
  - `#id` names the anchor a link resolves to (only one allowed)
2095
2248
  - `.class1` names a CSS class (any number of classes allowed)
@@ -2171,11 +2324,14 @@ either side of it, and every surface renders it as a figure. An image sharing a
2171
2324
  paragraph with prose is refused, because a width and a position mean nothing
2172
2325
  applied to a word in the middle of a sentence.
2173
2326
 
2174
- **The alt text is the caption.** Print has no `alt` attribute and has to put
2175
- those words somewhere a reader can see them, so every surface draws them under
2176
- the picture; the two HTML surfaces carry them as `alt` as well. A title —
2177
- `![alt](src "title")` — is refused rather than dropped in silence: there is one
2178
- place for those words and this is it.
2327
+ **Alt text renders as a caption on no surface.** On the website and in a
2328
+ Foundry journal it stays on the `img` element's `alt` attribute alone, and the
2329
+ book prints nothing beneath a picture that carries none. A visible label and
2330
+ caption are drawn only for a picture wrapped in [a `:::figure`
2331
+ fence](#the-figure-fence), and the text drawn is always the fence's own
2332
+ caption. A title — `![alt](src "title")` — is refused rather than dropped in
2333
+ silence: there is one place for those words, the alt text, and a title is not
2334
+ it.
2179
2335
 
2180
2336
  ##### Width is a class, and the ordinary width carries no marker
2181
2337
 
@@ -2465,34 +2621,110 @@ the tag and, where the directive restricts one, the type. An empty list under
2465
2621
  an authored heading is how a misspelled tag survives a build, so the
2466
2622
  directive's silence is what is refused rather than its output.
2467
2623
 
2468
- ##### Captioned blocks
2624
+ ##### The figure fence
2469
2625
 
2470
- `:::caption {#anchor}` labels the next Markdown block, whether it is prose,
2471
- code, a table, or an image. The closing `:::` ends the caption text; blank lines
2472
- between the fence and its block are allowed. The ID is unique within the note
2473
- and contains letters, digits, `_`, or `-`, beginning with a letter. An empty
2474
- caption, missing ID, missing closer, or missing following block is an error at
2475
- the opening line.
2626
+ `:::figure {#anchor .border}` declares what it numbers and, optionally,
2627
+ captions: the fence is the unit the counter counts, not the caption text it
2628
+ may or may not carry.
2476
2629
 
2477
2630
  ````markdown
2478
- See [[#trade|]] for the market routes.
2479
-
2480
- :::caption {#trade}
2481
- Regional trade routes
2482
- :::
2631
+ :::figure {#trade}
2483
2632
 
2484
2633
  ```sql
2485
2634
  SELECT name.full AS "Market" FROM notes WHERE type = 'place'
2486
2635
  ```
2636
+
2637
+ ///
2638
+ Regional trade routes
2639
+ :::
2640
+
2641
+ Refer to {{ref "#trade"}} for the market routes.
2487
2642
  ````
2488
2643
 
2489
- The block displays **Table 1: Regional trade routes** and `[[#trade|]]`
2490
- displays **Table 1** as a link. A nonempty label after the pipe takes precedence.
2491
- The category comes from the following block: a regular code fence is code,
2492
- a Markdown or expanded SQL table is a table, an image is a figure, and any other
2493
- block is prose. The website and Foundry number each category within a note;
2494
- the PDF numbers each category through the book. Foundry gives a captioned block
2495
- its own JournalEntryPage, so its anchor has a page UUID.
2644
+ The fence displays **Table 1: Regional trade routes** and `{{ref "#trade"}}`
2645
+ displays **Table 1** as a link.
2646
+
2647
+ **The grammar.** `:::figure` opens the fence, on a line of its own and
2648
+ optionally followed by a braced attribute block in the grammar a heading and
2649
+ a named block take too. A line of `:::` alone closes it. Between the
2650
+ two, the first `///` written alone on a line, read at the top level — outside
2651
+ any nested code fence — divides what the fence holds from its caption; a `///`
2652
+ written inside a nested fence is content, not a divider. A second top-level
2653
+ `///` is an error, naming the line: a figure holds one caption, so it carries
2654
+ one `///` line.
2655
+
2656
+ An opening line that is not `:::figure`, optionally followed by one braced
2657
+ attribute block and nothing else, is reported at its own line as
2658
+ `a figure's attributes are written as {#id .border}`.
2659
+
2660
+ **Every part but the fence itself is optional.**
2661
+
2662
+ - **No `{#id}`.** The fence is still numbered and drawn; it carries no anchor,
2663
+ so nothing can address it. An id, when written, must be a name unique within
2664
+ the note — a repeat is `duplicate figure id "<id>"` at the fence's opening
2665
+ line, and an id that collides with a heading anchor in the same note is
2666
+ `heading and figure declare the same anchor "<id>"` at the heading's line.
2667
+ - **No `///`.** The fence draws its label alone, with no caption beneath it. A
2668
+ `///` with nothing after it before the closing `:::` is an error at the
2669
+ `///` line: a figure's `///` section carries no caption — write no `///` to
2670
+ leave the figure uncaptioned.
2671
+ - **No `.border`.** The fence draws without one. `.border` is the only class
2672
+ the construct declares; any other class is reported at the opening line,
2673
+ naming it: a figure takes no such class, and the classes a figure takes
2674
+ are `.border`. A `key="value"` attribute is reported the same way, naming
2675
+ the key: a figure takes an id and classes, nothing else.
2676
+
2677
+ **What is never optional.** A fence with nothing between its opening line and
2678
+ its `///` or its closing `:::` is `a figure has no contents`, at the opening
2679
+ line. An opening `:::figure` with no matching `:::` runs to the end of the
2680
+ note and is `a figure needs a closing :::`, also at the opening line, and
2681
+ nothing after it is scanned as a second fence.
2682
+
2683
+ **The counter is derived, never authored.** The fence's contents decide which
2684
+ of four counters it draws from — `Code`, `Table`, `Figure`, `Prose` — read from
2685
+ the first top-level block inside the fence: a table is `Table`; a fenced `sql`
2686
+ block is `Table`, because its rendered output is one; any other fenced or
2687
+ indented code block is `Code`; one or more pictures and nothing else is
2688
+ `Figure`; everything else is `Prose`. A table or a picture written outside any
2689
+ `:::figure` fence draws no label and is counted nowhere. The website and a
2690
+ Foundry journal number each counter within the note; the book numbers each
2691
+ counter across the book in reading order.
2692
+
2693
+ **A heading that would start its own journal page cannot be written inside a
2694
+ fence.** An H1, or a heading at any level carrying an `{#anchor}`, is reported
2695
+ at its own line inside one: `a heading that starts a page cannot be written
2696
+ inside a figure — keep an H1 or an anchored heading at the top level, or drop
2697
+ the anchor and the level to stay inside it`.
2698
+
2699
+ **The `ref` expression renders a link to a figure's anchor.**
2700
+ `{{ref "#thorn"}}` renders "Figure 13"; `{{ref "#thorn" form="full"}}` renders
2701
+ the number and the caption; `{{ref "#thorn" form="title"}}` renders the
2702
+ caption alone. `form="number"` is the default and may be written explicitly;
2703
+ a `form` outside this closed set is reported naming the three. A link written
2704
+ inside the figure's own caption contributes only its label text to a `full`
2705
+ or `title` reference, because the reference is itself a link, and emphasis
2706
+ inside the caption is kept. `form="full"` or `form="title"` aimed at a figure
2707
+ with no caption is reported naming the anchor: that form needs a caption, and
2708
+ the figure has none. An address naming no figure in the note is reported
2709
+ naming the anchor; an address naming another note's figure —
2710
+ `{{ref "other-note#anchor"}}` — is reported as addressing a figure this build
2711
+ does not resolve outside the note carrying the reference. Only a same-note
2712
+ anchor resolves, on the website and in the book.
2713
+
2714
+ Foundry gives a `:::figure` fence its own JournalEntryPage. A fence holding
2715
+ exactly one picture, captioned in plain text with nothing else — or carrying
2716
+ no caption at all — becomes a page of type `image`: the picture is its `src`,
2717
+ a caption, when there is one, is `image.caption`, and the page is named for
2718
+ the figure's own number. Every other fence becomes a page of type `text`
2719
+ instead: a fence grouping more than one picture; an embedded sound, there
2720
+ being no audio page type; a fence whose caption carries inline markup —
2721
+ emphasis, a link, an embedded icon or image — because `image.caption` is a
2722
+ `StringField` with nowhere to render it; and a fence that shares its split
2723
+ with prose that follows it, because an `image` page has nowhere to carry that
2724
+ prose either. On a `text` page the render is the fence's own content, scanned
2725
+ and labelled as the first of its kind on that page alone and then corrected to
2726
+ the number the note-wide scan already assigned, so a second figure with no id
2727
+ is still named for its true, note-wide number rather than recounted from one.
2496
2728
 
2497
2729
  ##### Named blocks
2498
2730
 
@@ -3775,18 +4007,49 @@ the setting itself, rather than instructions or other apparatus for the GM.
3775
4007
  tournament is not a matter of time-reckoning at all — and from `culture`, which is a grouping
3776
4008
  of people rather than an occasion they attend.
3777
4009
 
3778
- | `data` property | Values | Description |
3779
- | --------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------ |
3780
- | `epoch` | canonical `<year>.<day>` | The canonical day that equals calendar year 1, day 1 |
3781
- | `months` | `{ name, abbreviation?, days }[]` | Ordered months; their days sum to the world's year |
3782
- | `weekdays` | `{ name, abbreviation? }[]` | Ordered week days, indexed from zero; weeks run continuously |
3783
- | `seasons` | `{ name, abbreviation?, start }[]` | Seasons starting on one-based days of year |
3784
- | `namedDays` | `{ name, abbreviation?, day }[]` | Names assigned to one-based days of year |
3785
- | `eras` | `{ shortcode, name, abbreviation?, marker?, proclaimedBy?, start, label? }[]` | Year counts; `start` is `null` or an in-calendar year |
3786
- | `formats` | map of names to Calendaria patterns | Named date formats; `std` is the preferred default |
3787
- | `event` | map | A dated occurrence and its chronology metadata |
3788
-
3789
- `data.event` is available on every `lore` subType. It holds structured chronology metadata, including an event's kind, date, sources, and affected places. The shared format checks that it is a map; a content package can check its details. It does not appear as an infobox row.
4010
+ | `data` property | Values | Description |
4011
+ | ----------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
4012
+ | `epoch` | canonical `<year>.<day>` | The canonical day that equals calendar year 1, day 1 |
4013
+ | `months` | `{ name, abbreviation?, days }[]` | Ordered months; their days sum to the world's year |
4014
+ | `weekdays` | `{ name, abbreviation? }[]` | Ordered week days, indexed from zero; weeks run continuously |
4015
+ | `seasons` | `{ name, abbreviation?, start }[]` | Seasons starting on one-based days of year |
4016
+ | `namedDays` | `{ name, abbreviation?, day }[]` | Names assigned to one-based days of year |
4017
+ | `eras` | `{ shortcode, name, abbreviation?, marker?, proclaimedBy?, start, label? }[]` | Year counts; `start` is `null` or an in-calendar year |
4018
+ | `formats` | map of names to Calendaria patterns | Named date formats; `std` is the preferred default |
4019
+ | `events` | `{ when, until?, recurs? }[]` | This note's dated occurrences, each with its own relationships and chronicle fields |
4020
+ | `events[].when` | a date, or `"0.<day>"` | The occurrence's anchor and first instance — required |
4021
+ | `events[].until` | a date | Where the occurrence ran to, or where a recurring series stopped |
4022
+ | `events[].recurs` | `{ every }` or `{ on }` | How further occurrences are found, absent for a one-time occurrence |
4023
+ | `events[].recurs.every` | whole number of years, 1 or more | A period counted on the canonical axis from `when` |
4024
+ | `events[].recurs.on` | list of dates, strictly increasing, each later than `when` | Recorded occurrences beyond the first, in place of a period |
4025
+
4026
+ `data.events` is available on every `lore` subType. Each entry's `kind`, `depth`, `summary`, `sources`, `standing`, `accounts`, `who`, `where`, `unresolved`, `follows` and `names` hold its chronicle detail; their shapes belong to whatever design governs chronicle records, and this format only asks that they sit inside an entry. The shared format checks `when`, `until` and `recurs`; a content package can check the rest. `events` itself does not appear as an infobox row, but a recurring entry's computed next occurrence does.
4027
+
4028
+ An entry reads by what it authors beside `when`:
4029
+
4030
+ | authored | reads as |
4031
+ | --------------------------------- | ----------------------------------------------- |
4032
+ | `when` | happened once |
4033
+ | `when` + `until` | ran continuously from then to then |
4034
+ | `when` + `recurs` | happened then and happens still |
4035
+ | `when` + `until` + `recurs.every` | happened then, again on the period, and stopped |
4036
+
4037
+ `recurs.every` and `recurs.on` are exclusive — a period with named exceptions is written as an enumeration instead. `recurs` is refused beside `born` or `died`, and beside a `when` of year `0`. `until` is refused beside `recurs.on`, whose own last entry already bounds it.
4038
+
4039
+ **The period advances the canonical year, never the era-relative year.** There is no year `0` in era-relative numbering — era year `-1` sits immediately before era year `1` — but canonical year `0` is an ordinary integer on the continuous axis `canonicalYear` builds, so a period of whole years steps on it with no special case at an epoch crossing. Precision and approximation carry through untouched: a year-precision anchor yields year-precision occurrences, an approximate anchor's occurrences are approximate too, and an anchor inside a short intercalary month recurs on that same day in every occurrence, with no special case for the month being short. Each occurrence selects its own era by testing its position against the anchor's calendar, so a later occurrence may print in a different era than its anchor, or print bare where no count was proclaimed for that year — neither is ever a finding, since nobody authored the occurrence.
4040
+
4041
+ **Year `0` means any year.** An entry whose `when` states year `0` recurs on that day of every year — `when: "0.5"` is the fifth day of the year, in every year. The value must be quoted, for the reason every canonical date stating a day must be: an unquoted `<year>.<day>` reaches the check as a float, and a day's trailing zero is lost before a date is read. Beside a year-`0` entry, `recurs` is refused (the entry is already annual), `until` is allowed and bounds the series, and the entry carries no `canonicalYear`, era or `sort` — it cannot date its own note. A note's date is its first entry stating a real year.
4042
+
4043
+ ```yaml
4044
+ data:
4045
+ events:
4046
+ - when: 412.1
4047
+ recurs: { every: 1 }
4048
+ - when: "0.286"
4049
+ until: 940.1
4050
+ ```
4051
+
4052
+ The first entry is an anniversary counted from canonical day `412.1`, recurring every year with no end. The second is an autumn rite — the two-hundred-eighty-sixth day of every year — that was kept until canonical year 940 and no longer is.
3790
4053
 
3791
4054
  #### Calendar note structure
3792
4055
 
@@ -32,14 +32,14 @@ These are the only keys a note writes at its top level, in the order the formatt
32
32
 
33
33
  These fields are accepted by every note type. A field's value can still be irrelevant to a particular output; the build reports that where it can.
34
34
 
35
- | Field | Shape | Meaning |
36
- | ----------------- | ----------------------------------------------- | ------------------------------------------------------------------------- |
37
- | `data.id` | string | A fixed document identity when the derived identity is unsuitable. |
38
- | `data.pack` | string | The shared compendium route, with a system block overriding it. |
39
- | `data.packFolder` | an Address, or a map of Addresses keyed by pack | The shared compendium folder Address, with a system block overriding it. |
40
- | `data.harnworld` | map | HârnWorld source details shared by every system. |
41
- | `data.icon` | an Address | The document's profile art — an `icon` address, resolved into `img`. |
42
- | `data.banner` | an Address | The page's hero image — an `image` address. Reaches no compiled document. |
35
+ | Field | Shape | Meaning |
36
+ | ----------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------ |
37
+ | `data.id` | string | A fixed document identity when the derived identity is unsuitable. |
38
+ | `data.pack` | string | The shared compendium route, with a system block overriding it. |
39
+ | `data.packFolder` | an Address, or a map of Addresses keyed by pack | The shared compendium folder Address, with a system block overriding it. |
40
+ | `data.harnworld` | map | HârnWorld source details shared by every system. |
41
+ | `data.icon` | an Address | The document's profile art — an `icon` address, resolved into `img`. |
42
+ | `data.banner` | an Address | The page's hero image — an `image` address, cut to 1792×768. Reaches no compiled document. |
43
43
 
44
44
  ## Type-specific fields
45
45
 
@@ -432,7 +432,7 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
432
432
  | `data.namedDays` | list of `{ name, abbreviation?, day }` | Names assigned to particular days of the year. |
433
433
  | `data.eras` | list of `{ shortcode, name, marker?, abbreviation?, proclaimedBy?, start, label? }` | The year-counts kept in this calendar. A marker names one era and uses these months. |
434
434
  | `data.formats` | map of named Calendaria format strings | Named patterns for reading and writing this calendar's dates. |
435
- | `data.event` | event metadata map | A dated occurrence and its relationships to other events and places. |
435
+ | `data.events` | list of `{ when, until?, recurs? }` | This note's dated occurrences — a founding once, an anniversary that recurs, or a list of recorded happenings. |
436
436
 
437
437
  ### map
438
438
 
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  /**
15
- * The anchors a note declares on headings and captioned blocks.
15
+ * The anchors a note declares on headings and figure fences.
16
16
  *
17
17
  * **A leaf, deliberately.** This is asked by the link checker, by the content
18
18
  * index, and by the builds that emit a link, and they cannot all import one
@@ -29,23 +29,28 @@
29
29
  * @module
30
30
  */
31
31
 
32
- import { scanCaptions } from "./content-captions.mjs";
32
+ import { scanFigures } from "./content-figures.mjs";
33
33
  import { HEADING_LINE, splitHeadingAttributes } from "./heading-attributes.mjs";
34
34
 
35
35
  /**
36
- * The `{#slug}` and `:::caption {#slug}` anchors a note declares.
36
+ * The `{#slug}` and `:::figure {#slug}` anchors a note declares.
37
37
  *
38
- * A bare `#` heading starts a journal page without declaring a slug. Captioned
39
- * blocks declare a slug and start an addressable journal page.
38
+ * A bare `#` heading starts a journal page without declaring a slug, and so
39
+ * does a figure fence that declares no id. A fence that declares one declares a
40
+ * slug and starts an addressable journal page.
40
41
  *
41
42
  * @param {string} body - The note's markdown body, frontmatter already removed.
42
43
  * @param {number} [bodyLine] - The 1-based file line the body starts on, from
43
44
  * `parseMarkdownFile`. Anchors are reported at their position in the **file**,
44
45
  * so an editor can jump straight to one; passing nothing numbers from the body.
46
+ * @param {(address: string) => string|undefined} [resolveRole] - From a
47
+ * picture's address to the role its asset declares, so a figure anchor's
48
+ * `name` reads `Map 1` rather than `Figure 1` where it is due — see
49
+ * {@link module:engine/content-figures.scanFigures}.
45
50
  * @returns {Array<{slug: string, name: string, level: number, line: number}>}
46
51
  * In document order.
47
52
  */
48
- export function collectAnchors(body, bodyLine = 1) {
53
+ export function collectAnchors(body, bodyLine = 1, resolveRole) {
49
54
  const anchors = [];
50
55
  let inCodeBlock = false;
51
56
  const lines = String(body ?? "").split("\n");
@@ -70,12 +75,13 @@ export function collectAnchors(body, bodyLine = 1) {
70
75
  line: bodyLine + i,
71
76
  });
72
77
  }
73
- for (const caption of scanCaptions(body).captions) {
78
+ for (const figure of scanFigures(body, { resolveRole }).figures) {
79
+ if (!figure.id) continue;
74
80
  anchors.push({
75
- slug: caption.id,
76
- name: caption.label,
81
+ slug: figure.id,
82
+ name: figure.label,
77
83
  level: 1,
78
- line: bodyLine + caption.line - 1,
84
+ line: bodyLine + figure.line - 1,
79
85
  });
80
86
  }
81
87
  return anchors.sort((a, b) => a.line - b.line);