@heroiclands/package-build 20.7.0 → 21.0.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 (63) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/CONTENT.md +134 -44
  3. package/bin/content-build.mjs +37 -5
  4. package/bin/package-build.mjs +77 -0
  5. package/content-config.mjs +59 -1
  6. package/docs/api.md +149 -19
  7. package/docs/commands.md +75 -0
  8. package/docs/configuration.md +37 -10
  9. package/docs/content-format.md +450 -49
  10. package/engine/content-format.mjs +52 -3
  11. package/engine/content-images.mjs +699 -0
  12. package/engine/dependency-bump.mjs +218 -0
  13. package/engine/frontmatter-lint.mjs +89 -2
  14. package/engine/helpers.mjs +81 -142
  15. package/engine/index.mjs +15 -0
  16. package/engine/infobox-registry.mjs +81 -0
  17. package/engine/infobox-render.mjs +381 -0
  18. package/engine/infobox.mjs +963 -0
  19. package/engine/item-registry.mjs +5 -5
  20. package/engine/journals.mjs +22 -1
  21. package/engine/map-notes.mjs +11 -5
  22. package/engine/metadata-index.mjs +5 -0
  23. package/engine/note-vocabulary.mjs +57 -2
  24. package/engine/pathnames.mjs +374 -0
  25. package/engine/pdf-build.mjs +208 -9
  26. package/engine/pdf-render.mjs +461 -21
  27. package/engine/pdf-toc.mjs +77 -5
  28. package/engine/scenes.mjs +2 -1
  29. package/engine/site-build.mjs +106 -7
  30. package/engine/site-index.mjs +93 -4
  31. package/engine/wikilinks.mjs +93 -0
  32. package/hm3/default-item-art.mjs +14 -15
  33. package/hm3/index.mjs +3 -0
  34. package/hm3/infobox.mjs +64 -0
  35. package/package.json +1 -1
  36. package/sohl/default-item-art.mjs +18 -16
  37. package/sohl/index.mjs +3 -0
  38. package/sohl/infobox.mjs +499 -0
  39. package/types/content-config.d.mts +7 -0
  40. package/types/engine/content-format.d.mts +36 -0
  41. package/types/engine/content-images.d.mts +281 -0
  42. package/types/engine/dependency-bump.d.mts +89 -0
  43. package/types/engine/frontmatter-lint.d.mts +23 -0
  44. package/types/engine/helpers.d.mts +30 -72
  45. package/types/engine/index.d.mts +5 -0
  46. package/types/engine/infobox-registry.d.mts +36 -0
  47. package/types/engine/infobox-render.d.mts +87 -0
  48. package/types/engine/infobox.d.mts +443 -0
  49. package/types/engine/item-registry.d.mts +5 -5
  50. package/types/engine/journals.d.mts +9 -1
  51. package/types/engine/note-vocabulary.d.mts +51 -0
  52. package/types/engine/pathnames.d.mts +189 -0
  53. package/types/engine/pdf-build.d.mts +46 -0
  54. package/types/engine/pdf-render.d.mts +99 -1
  55. package/types/engine/pdf-toc.d.mts +10 -5
  56. package/types/engine/site-build.d.mts +11 -3
  57. package/types/engine/site-index.d.mts +35 -3
  58. package/types/engine/wikilinks.d.mts +22 -0
  59. package/types/hm3/default-item-art.d.mts +5 -6
  60. package/types/hm3/index.d.mts +1 -0
  61. package/types/hm3/infobox.d.mts +22 -0
  62. package/types/sohl/index.d.mts +1 -0
  63. package/types/sohl/infobox.d.mts +145 -0
@@ -4,7 +4,10 @@
4
4
 
5
5
  All markdown will end up generating a JournalNote document: If anything appears before the first H1 header, that will be placed in an "Introduction" page, and then every H1 header will become a subsequent page. The link id of this document will be `<package>-none-<note_type>-<shortcode>`
6
6
 
7
- If there are `hm3` or `sohl` sections in the frontmatter, those will be used to generate InfoBoxes, which will be displayed as a separate page (named "SoHL InfoBox" and "HM3 InfoBox", with anchors sohlinfobox and hm3infobox)
7
+ An **infobox** is prepended to the entry's first page, before the prose. It is
8
+ not a page of its own: a page is what a Foundry UUID addresses, and a summary a
9
+ reader has to navigate to is a summary they do not see. What the box holds, and
10
+ where it sits in every other medium, is [the infobox](#the-infobox) below.
8
11
 
9
12
  **There are two kinds of infobox.**
10
13
 
@@ -50,6 +53,221 @@ rather than something nobody notices.
50
53
 
51
54
  For Web Pages, the entire markdown content will be converted into an HTML page, with appropriate infoboxes.
52
55
 
56
+ ### The infobox
57
+
58
+ **The infobox is generated content at a known position — prepended, before the
59
+ prose.** It is not a floating sidebar and it is not defined in a rendering
60
+ template. The toolchain settles what each box holds; each medium lays that out
61
+ its own way.
62
+
63
+ | medium | placement | collapsing |
64
+ | ------- | ------------------------------------------------------ | ------------------------- |
65
+ | book | flows in the column measure, breaking between sections | none, and none needed |
66
+ | website | side rail on wide screens, inline on narrow | `<details>`, default open |
67
+ | Foundry | inlined, the page being narrow | `<details>`, default open |
68
+
69
+ Two columns are print-only. A scrolling page has no fixed viewport, so columns
70
+ make a reader travel down and back up, and a narrow screen collapses to one
71
+ column regardless.
72
+
73
+ **Six rules hold in every medium.**
74
+
75
+ 1. **It is content in document order**, prepended before the prose. An image
76
+ authored before it appears before it.
77
+ 2. **It contains no image.** A picture is authored in the text with its own
78
+ directive, and its position governs.
79
+ 3. **A section is the unit that flows.** Sections are whole and unbreakable and
80
+ the panel breaks _between_ them, which is what lets a long box cross a
81
+ column or page boundary without splitting a stat grid down the middle.
82
+ 4. **An absent field is absent, not empty.** A row with no value is not
83
+ emitted, and neither is a section with no rows: a creature carrying no
84
+ equipment has no `EQUIPMENT` heading rather than an empty one. A placeholder
85
+ asserts a fact that is not there, and an empty heading asserts that something
86
+ was expected here and is missing. A corpus writes an absence three ways and
87
+ all three count: the key is omitted, it holds the field's own default, or it
88
+ holds a **sentinel** — `na`, `none`, `not applicable` — which is the same
89
+ absence written as a word. `Potency: Na` is not a fact about a potion.
90
+ 5. **A system box is never an empty panel; it says which silence it is.** Rule
91
+ 4 governs rows, and a box is not a row — a heading over nothing asserts that
92
+ something should have been there. See
93
+ [the three states of a system box](#the-three-states-of-a-system-box).
94
+ 6. **Order is the toolchain's.** A medium renders boxes, sections and rows in
95
+ the order given.
96
+
97
+ #### What the note box holds
98
+
99
+ **The note box's fields are the type's own `data:` vocabulary, in its declared
100
+ order** — the very table each `### type:` section below states. There is no
101
+ second list, so a key added to a type appears in the book, on the website and
102
+ in a compendium journal with no further edit. A `Name` row stands first on
103
+ every note, because every note has one.
104
+
105
+ A few keys carry no row, and the reason in each case is one of exactly two —
106
+ the key is **machinery**, steering a build or an interface rather than
107
+ describing the subject, or it is an **image**, which rule 2 keeps out of the
108
+ box:
109
+
110
+ | key | why it carries no row |
111
+ | ---------------------------- | ------------------------------------------------ |
112
+ | `templatePriority` | template machinery, not a fact about the subject |
113
+ | `color` | sidebar machinery, not a fact about the subject |
114
+ | `portrait`, `img`, `overlay` | an image, which rule 2 keeps out of the box |
115
+
116
+ A field whose value is a **mapping** carries no row either — a governance
117
+ ladder or a wall layout has no summary shape — and neither does one the note
118
+ left empty.
119
+
120
+ A being's `age`, `height`, `weight` and `appearance.*` compose into a single
121
+ **Appearance** clause rather than taking six rows of their own: _Age 34, 6′ 1″,
122
+ 181 lbs, medium frame, brown eyes_.
123
+
124
+ #### What a system box holds
125
+
126
+ **A system box's fields are that system's own field declaration**, in its
127
+ order. There is no second list here either, so a field added to a type reaches
128
+ the box with no further edit.
129
+
130
+ Two things keep a fact off it:
131
+
132
+ - **A value the declaration would have supplied anyway.** A field answered by
133
+ its own default is a fact about the compiler, not about the note — and that is
134
+ true of the value, wherever it was written. A corpus writes its defaults out:
135
+ `improveFlag: false` and `combatCategory: none` are typed into hundreds of
136
+ notes that mean nothing by them.
137
+ - **A fact the note box already put on the page.** A gear item's weight is
138
+ system-agnostic and belongs to the note. What counts is what the note box
139
+ _shows_, not what its vocabulary declares — the same fact is written under
140
+ `data:` on one note and at its destination path on another, and a system box
141
+ that stood down on the declaration alone would leave it on no surface at all.
142
+
143
+ **A measured quantity carries its unit on its value** — `Price 160d`,
144
+ `Weight 1.1 lbs`. The unit belongs to the quantity rather than to the name of
145
+ the quantity, so it is not folded into the label: `Price (d)` beside `160` makes
146
+ a reader reassemble one fact from two cells, and reads worst in the book, whose
147
+ label column is a narrow small-caps rule. A medium cannot supply it either —
148
+ appending `d` to a price means knowing which row is the price, which is the one
149
+ thing a generic renderer must never know. A row carrying a unit is `text`,
150
+ because a number with a unit on it is no longer a number.
151
+
152
+ **What a field is called** is a **presentation overlay** on that same
153
+ declaration, per system. It carries two things a compiler's field list cannot,
154
+ because they are about a page rather than about a document: a reader's word
155
+ where the declaration's key is the compiler's — `assocSkillCode` is exactly
156
+ right in a DataModel and wrong in a panel somebody reads — and the handful of
157
+ keys that belong on no page at all, either because the box shows the same fact
158
+ whole somewhere else or because they steer a character sheet rather than
159
+ describe the subject. It is an overlay and not a list: a field it does not
160
+ mention still gets a row, under its own humanised name.
161
+
162
+ Four types are read differently, because their box is derived rather than read
163
+ field by field — a **being**, whose attributes, skills, mystical abilities and
164
+ carried gear are one flat `sohl.items` list; **armour**, whose protection is
165
+ shown for all four aspects with an unstated one rendered `0`; a **weapon**,
166
+ whose strike modes are shown one per line with an unstated value rendered `—`;
167
+ and a **projectile**, whose impact is three declared fields composing into the
168
+ one quantity a reader wants. Both of those placeholders are decided by the
169
+ toolchain and travel as data, so no renderer has to know which field it is
170
+ looking at.
171
+
172
+ A weapon's strike modes are authored either as a **list**, each mode carrying
173
+ its own `shortcode`, or as a **mapping** keyed by the mode's name. Both are
174
+ read, and both yield the same section.
175
+
176
+ #### The three states of a system box
177
+
178
+ A system box says something on every page that carries one. Which of the three
179
+ it says is decided here and travels as the box's `statement`, so a medium draws
180
+ one thing and decides none of it.
181
+
182
+ | state | when | what the box says |
183
+ | -------------- | ---------------------------------------------------------- | --------------------------------------- |
184
+ | content | the system holds something the note box has not shown | its sections, and no `statement` |
185
+ | not available | the system maps this note's type but produced no document | `statement: Not available` |
186
+ | nothing to add | the system produced a document holding nothing new to show | `statement: Nothing beyond the profile` |
187
+
188
+ The third state is not the second. _Not available_ is a statement about this
189
+ note — the system has no document for it, and a reader can act on that. A
190
+ system that _does_ compile the note and holds only facts the note box has
191
+ already given says so instead, because a reader told "not available" would go
192
+ looking for a document that exists, and an empty panel tells them nothing at
193
+ all.
194
+
195
+ A system that does not map the note's type gets **no box**. A box reading
196
+ _Not available_ on every affiliation page would suggest a gap in the note when
197
+ the truth is about the system's scope.
198
+
199
+ #### Four section layouts
200
+
201
+ A section says how it is arranged, and a renderer switches on that and on a
202
+ value's `kind` — never on a field name, which is what keeps a field list from
203
+ leaking back into a template.
204
+
205
+ | `layout` value | shape | what carries it | where it is used |
206
+ | -------------- | ------------------------------- | --------------- | -------------------------- |
207
+ | `rows` | label/value pairs, one per line | `rows` | a profile |
208
+ | `grid` | short label/value cells | `cells` | attributes, armour aspects |
209
+ | `runin` | groups of comma-joined entries | `groups` | skills, equipment |
210
+ | `list` | one entry per line | `entries` | mystical abilities |
211
+
212
+ | `kind` value | the value is |
213
+ | ------------ | ------------------------------- |
214
+ | `text` | a string |
215
+ | `number` | a number |
216
+ | `link` | `{text, url?, uuid?, address?}` |
217
+ | `links` | a list of those |
218
+ | `list` | a list of strings |
219
+
220
+ A website renders a link by its `url`, a compendium journal by its `uuid`, and
221
+ the book by the `address`, which is the entry's own label in the volume. A
222
+ reference the index could not reach keeps its words rather than being dropped.
223
+
224
+ **Emitted shape**, as a page's front matter carries it:
225
+
226
+ ```yaml
227
+ infoboxes:
228
+ - id: note
229
+ kind: note
230
+ title: Profile
231
+ sections:
232
+ - id: profile
233
+ layout: rows
234
+ rows:
235
+ - label: Name
236
+ kind: text
237
+ value: Brànwâal Dôrgaar
238
+ - label: Affiliations
239
+ kind: links
240
+ value:
241
+ - text: The Silent Talon Company
242
+ url: /thalorna/affiliation-slntlncmpny/
243
+ - id: sohl
244
+ kind: system
245
+ system: sohl
246
+ title: SoHL
247
+ available: true
248
+ sections:
249
+ - id: attributes
250
+ label: Attributes
251
+ layout: grid
252
+ cells:
253
+ - label: STR
254
+ value: 14
255
+ - id: skills
256
+ label: Skills
257
+ layout: runin
258
+ groups:
259
+ - label: Combat
260
+ entries:
261
+ - text: Melee 75
262
+ - id: hm3
263
+ kind: system
264
+ system: hm3
265
+ title: HM3
266
+ available: false
267
+ sections: []
268
+ statement: Not available
269
+ ```
270
+
53
271
  ### Frontmatter has three regions
54
272
 
55
273
  A note's frontmatter divides into three parts, and the difference matters because
@@ -89,6 +307,7 @@ is declared, so a near miss is a finding that names what you probably meant.
89
307
  | **place character** | `place` | `fortified`, `temple`, `market`, `trading`, `merchant`, `mining`, `fishing`, `naval`, `military`, `imperial`, `provincial`, `coastal`, `river`, `lakeside`, `hill`, `mountain`, `valley`, `forest`, `woodland`, `inland`, `island`, `frontier`, `border`, `craft`, `caravan`, `pilgrimage`, `holy`, `sacred`, `free` |
90
308
  | **place scale** | `place` / `region` | `continent` |
91
309
  | **being station** | `being` | `tradesfolk`, `common-folk`, `soldiery`, `administration`, `clergy`, `mages`, `underworld`, `dependents`, `guilded`, `unguilded` |
310
+ | **being kind** | `being` | `character`, `creature` |
92
311
  | **state** | any | `draft` |
93
312
 
94
313
  **`draft` is the one tag either build reads.** A note tagged `draft` exists so a
@@ -124,6 +343,50 @@ first because a person may be several at once and because nothing ranks
124
343
  **A continent is a region carrying a tag, not a subtype**, because structurally it
125
344
  is a region: the same fields, the same parent chain, everything but scale.
126
345
 
346
+ #### A being's kind is the one tag group that is a slot
347
+
348
+ A person and a beast carry very different amounts of data, and a being note says
349
+ which it is with a tag rather than leaving it to be guessed from how much the
350
+ note holds.
351
+
352
+ | `beingKind` value | the subject is |
353
+ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
354
+ | `character` | a person — someone with a name, a station, a history, and the attributes, skills and carried gear that go with living a life |
355
+ | `creature` | a beast, a monster or a made thing — statted for what it does rather than for who it is, and usually carrying no equipment and no affiliation |
356
+
357
+ **The slot is filled once, or not at all.** The two are alternative answers to
358
+ one question, so a note carrying both has answered it twice and every reader of
359
+ the tag gets to pick; that is refused as an error naming the note. A being
360
+ carrying **neither** is not a finding. The kind is authored deliberately, and
361
+ nothing can tell a being nobody has classified from one whose author means to
362
+ leave it unclassified — a tree part-way through tagging is a tree with untagged
363
+ beings in it, and failing its build would be a rule about the schedule rather
364
+ than about the content.
365
+
366
+ **Nothing else fills the slot, and nothing else is refused for failing to.** A
367
+ being tagged `undead` or `beast-of-burden` is describing the subject in the
368
+ author's own words, in a region that is open, and this build has no standing to
369
+ take that back. What is refused is a near miss of one of the two — `charcter`
370
+ drops the note out of every query for characters while the list still renders —
371
+ and the two of them together. Those are the only two refusals a closed tag
372
+ vocabulary can make here, and both are errors, so a build carrying one fails.
373
+
374
+ **The same two words name two other things, and neither is this.** `hm3.type`
375
+ takes `character` or `creature` as the HM3 _document_ a being compiles to, in a
376
+ system block rather than in `tags:`. Both are also **retired note types**, so a
377
+ note writing `type: character` is refused by name and told to write `being`.
378
+ Three namespaces, one pair of words; the tag is the system-agnostic one, and it
379
+ is the only one that describes the subject rather than a document.
380
+
381
+ **Neither the book nor the website branches on the kind.** One flow serves both
382
+ — the authored image, then the infobox, then the prose — and a creature's box is
383
+ simply shorter, because it has less in it. A section with nothing to put in it is
384
+ not emitted at all, so a creature with no equipment has no `EQUIPMENT` heading
385
+ rather than an empty one, exactly as rule 4 of [the infobox](#the-infobox) drops
386
+ a row with no value. The tag classifies the subject, which is what makes a list
387
+ of the creatures in a setting a thing a query can ask for; it is not an
388
+ instruction to a renderer.
389
+
127
390
  The mapping tables below describe the **document** destinations. A key that
128
391
  appears in no table still reaches the web page; it simply reaches no Foundry
129
392
  document.
@@ -331,47 +594,91 @@ item compiled from a template note loses the fact that it is one
331
594
  The row states the mapping the format makes;
332
595
  the gap is in the pass, not in the table.
333
596
 
334
- #### An asset path's first segment says which package owns it
335
-
336
- `img` and `portrait` are paths, and a path has to say **which package holds the
337
- file** because a module's content routinely cites the system's art, while the
338
- system's content never cites the module's. The first segment answers that, and
339
- there are exactly three answers:
340
-
341
- | Authored path starts with | Owner | Emitted |
342
- | ------------------------- | --------------------- | -------------------- |
343
- | `systems/` | a separate **system** | unchanged |
344
- | `modules/` | a separate **module** | unchanged |
345
- | anything else | **this package** | `<assetRoot>/<path>` |
346
-
347
- `<assetRoot>` is `<packageKind>/<foundryPackage>/assets`, derived from the
348
- configuration — `systems/sohl/assets` for the system,
349
- `modules/sohl-thalorna/assets` for that module. A `documentation` package has
350
- no asset root at all, because Foundry installs no such package and serves no
351
- files for it: there, the third row is refused, and a note names the owning
352
- package (`systems/…`, `modules/…`) or a URL. So one authored
353
- `icons/relic.svg` means "my own `assets/icons/relic.svg`" in whichever package
354
- writes it, while an authored `systems/sohl/assets/icons/noun/shield.svg` names
355
- the system's file and is left exactly as written wherever it appears. That
356
- second case is not hypothetical: every default this toolchain pairs with an item
357
- type is a `systems/sohl/…` path, so a module's compiled documents carry it
358
- verbatim.
359
-
360
- **The third row is "anything else", not a list of directories.** It is a rule
361
- about ownership: a package owns its whole `assets/` tree, and the directory
362
- names inside it are that package's business. `sohl-kethira-basic` keeps art
363
- under `assets/artwork/`, and `artwork/deity.webp` is rooted under its assets by
364
- the same rule that roots `icons/…` and `images/…` there.
365
-
366
- An address naming **no** package passes through untouched, which is that same
367
- rule rather than an exception — an absolute URL, a `data:` URI and a `/`-rooted
368
- path each already address something no package owns, so prefixing any of them
369
- would break an address that was already correct.
370
-
371
- `worlds/` is deliberately **not** exempt. A package may not ship art out of a
372
- world, so a note writing one has made a mistake; prefixing it yields a plainly
373
- broken path rather than a plausible one that 404s in Foundry with nothing
374
- reporting it.
597
+ #### A pathname names the package that owns the file
598
+
599
+ A note names a file once, and four surfaces have to serve it: a Foundry
600
+ install, this repository's own working tree, the website, and the book. Each
601
+ addresses the same file differently, so an authored pathname is a **statement
602
+ of ownership** and every surface derives its own address from it.
603
+
604
+ **The first segment names the package, when an `assets/` follows it.**
605
+ Everything after that `assets/` is the _suffix_ — the path inside the package's
606
+ shipped tree, and the one piece every derived form is built from. A pathname
607
+ that does not open `<package>/assets/` belongs to the package being compiled,
608
+ and the whole of it is the suffix.
609
+
610
+ ```yaml
611
+ img: images/beings/athlwvthrnd-portrait.webp # this package's
612
+ img: sohl/assets/icons/noun/shield.svg # the sohl package's
613
+ ```
614
+
615
+ The four forms, for a `thalorna` note (the `thalorna` package ships as the
616
+ Foundry module `sohl-thalorna`) writing `images/map.webp`:
617
+
618
+ | Surface | Address |
619
+ | ----------- | -------------------------------------------------------- |
620
+ | **Foundry** | `modules/sohl-thalorna/assets/images/map.webp` |
621
+ | **Local** | `assets/images/map.webp` |
622
+ | **Web** | `https://cdn.heroiclands.org/thalorna/images/map.webp` |
623
+ | **Book** | `assets/images/map.webp`, staged beside the Typst source |
624
+
625
+ And for the same note writing `sohl/assets/icons/noun/shield.svg`, a file the
626
+ system ships and this repository does not hold:
627
+
628
+ | Surface | Address |
629
+ | ----------- | --------------------------------------------------------- |
630
+ | **Foundry** | `systems/sohl/assets/icons/noun/shield.svg` |
631
+ | **Local** | `assets/icons/noun/shield.svg`, in the `sohl` repository |
632
+ | **Web** | `https://cdn.heroiclands.org/sohl/icons/noun/shield.svg` |
633
+ | **Book** | not carried — a build stages only what this package ships |
634
+
635
+ **The package's name is not its Foundry id.** `thalorna` is what the content is
636
+ called, what a note writes, and what the website serves it under.
637
+ `sohl-thalorna` is what Foundry installs the module as, and it appears in the
638
+ Foundry form alone. The two words are the same for `sohl` and for `hm3`, which
639
+ is exactly why they are kept apart here.
640
+
641
+ Which packages a build can resolve is derived from its configuration: its own
642
+ `contentPackage`, every game system it compiles content for (`systems:`,
643
+ `packs[].system`, `requiresSystem`, `relationships.systems`), and every package
644
+ it declares under `relationships`, whose `contentPackage` names the content
645
+ where that differs from the Foundry id. A pathname naming a package the build
646
+ does not know still resolves on the website and in the book — those need only
647
+ the name and the suffix — and has no Foundry address, which is refused rather
648
+ than guessed.
649
+
650
+ **The host is configuration.** `site.assets` in `package-build.config.yaml` is
651
+ the root the web form is joined onto, and a package-owned image on a page with
652
+ none set is an error naming that key.
653
+
654
+ **A pathname naming no package at all passes through on every surface** — an
655
+ absolute URL, a `data:` URI, a protocol-relative `//host/…`, or a `/`-rooted
656
+ path, which Foundry serves from its data root. That is how a note addresses
657
+ core Foundry art (`/icons/svg/mystery-man.svg`) or a package this build knows
658
+ nothing of (`/systems/dnd5e/icons/spell.webp`).
659
+
660
+ **A `systems/…` or `modules/…` pathname is refused**, with a finding naming the
661
+ replacement. It is a Foundry address written where an ownership statement
662
+ belongs: it resolves for Foundry and for nothing else, because neither the
663
+ website nor the book has any such directory. `systems/sohl/assets/ui/logo.webp`
664
+ is written `sohl/assets/ui/logo.webp`, and `systems/hm3/images/svg/sword.svg` —
665
+ where the package serves its pictures from its own root — is written
666
+ `hm3/assets/images/svg/sword.svg`, because `assets/` is where a package's files
667
+ sit in every derived form.
668
+
669
+ `worlds/` is not a Foundry root for this purpose. A package may not ship files
670
+ out of a world, so a note writing one has made a different mistake and gets the
671
+ ordinary "this package owns it" reading — a plainly broken path rather than a
672
+ plausible one that 404s with nothing reporting it.
673
+
674
+ **The rule is about ownership, not a list of directories.** A package owns its
675
+ whole `assets/` tree and the directory names inside it are its own business:
676
+ `sohl-kethira-basic` keeps art under `assets/artwork/`, and `artwork/deity.webp`
677
+ is that package's by the same rule that claims `icons/…` and `images/…`.
678
+
679
+ **Every pathname a note carries follows it.** `img:` and `data.portrait:`; a
680
+ map note's background, overlay, tile textures and ambient sounds; and the
681
+ address of every image in a note's body.
375
682
 
376
683
  #### `banner:` addresses the CDN, not the Foundry install
377
684
 
@@ -388,19 +695,20 @@ and **anything else is prefixed with `images/`** and joined onto
388
695
  to a doubled path:
389
696
 
390
697
  ```text
391
- banner: systems/sohl/assets/images/banners/lore.webp
392
- → <cdnBaseURL>/images/systems/sohl/assets/images/banners/lore.webp
698
+ banner: sohl/assets/images/banners/lore.webp
699
+ → <cdnBaseURL>/images/sohl/assets/images/banners/lore.webp
393
700
  ```
394
701
 
395
702
  That can be made to work by mirroring the path on the CDN, and one consumer
396
703
  does exactly that — but it is not what the author meant.
397
704
 
398
705
  **The two are not reconciled, because they are not two spellings of one thing.**
399
- `img:` addresses a file inside a Foundry install, where the package that holds
400
- it is the question worth asking. `banner:` addresses a file on a CDN, where
401
- there are no packages at all. Write a `banner:` relative to the CDN's `images/`
402
- root — `banners/lore.webp`, not `images/banners/lore.webp` and not a
403
- package-rooted path.
706
+ A pathname names a file a package ships, and four surfaces derive an address
707
+ from it. `banner:` names a hero image on the site's own asset host, reaches no
708
+ compiled document and no book, and is resolved by the theme rather than by this
709
+ toolchain — `banner: none` is not even a target. Write a `banner:` relative to
710
+ the CDN's `images/` root — `banners/lore.webp`, not `images/banners/lore.webp`
711
+ and not a package-qualified pathname.
404
712
 
405
713
  #### The pack a note compiles into
406
714
 
@@ -1086,6 +1394,99 @@ Any header can include curly braces. Inside the curly braces:
1086
1394
  - `.class1` represents a CSS class named `class1` (any number of classes allowed)
1087
1395
  - `attr="value"` represents an HTML attribute named `attr` whose value is `value` (any number of attr/value pairs allowed)
1088
1396
 
1397
+ #### Images
1398
+
1399
+ An image is authored in the body, in the place it belongs, and every surface
1400
+ renders it there — the book, the website and a Foundry Journal Page alike. One
1401
+ authored statement, three renderers, which is the single-sourcing every other
1402
+ part of this format follows.
1403
+
1404
+ ```markdown
1405
+ ![Brànwâal Dôrgaar](images/beings/branwldrgr-portrait.webp){float: top-left}
1406
+
1407
+ ![Map of Thalorna](images/maps/thalorna.webp){.full-width}
1408
+ ```
1409
+
1410
+ **An image is a block.** It stands alone in its paragraph, with a blank line
1411
+ either side of it, and every surface renders it as a figure. An image sharing a
1412
+ paragraph with prose is refused, because a width and a position mean nothing
1413
+ applied to a word in the middle of a sentence.
1414
+
1415
+ **The alt text is the caption.** Print has no `alt` attribute and has to put
1416
+ those words somewhere a reader can see them, so every surface draws them under
1417
+ the picture; the two HTML surfaces carry them as `alt` as well. A title —
1418
+ `![alt](src "title")` — is refused rather than dropped in silence: there is one
1419
+ place for those words and this is it.
1420
+
1421
+ ##### Width is a class, and the ordinary width carries no marker
1422
+
1423
+ | `class` value | book | website | Foundry journal |
1424
+ | ------------- | ------------- | ------------------ | ---------------- |
1425
+ | _(none)_ | one column | the text measure | the page measure |
1426
+ | `.full-width` | the full page | full content width | full page width |
1427
+
1428
+ The simple case needs no spelling, which is why the ordinary width has no name.
1429
+ A third width joins this table as a name.
1430
+
1431
+ **No pixel values, ever.** A number means something in a browser and nothing
1432
+ coherent in print, and a directive carrying a class and a dimension at once
1433
+ gives one question two answers with no rule for which wins. `{width=800}` is
1434
+ refused.
1435
+
1436
+ ##### Position is `float:`
1437
+
1438
+ | `float` value | book | website and Foundry journal |
1439
+ | -------------- | -------------------- | ------------------------------- |
1440
+ | `top-left` | top of the column | floated left, text wraps |
1441
+ | `bottom-left` | bottom of the column | floated left, text wraps |
1442
+ | `top-right` | top of the column | floated right, text wraps |
1443
+ | `bottom-right` | bottom of the column | floated right, text wraps |
1444
+ | `center` | top of the column | centred, with no text beside it |
1445
+
1446
+ **Print cannot wrap text around an arbitrary shape.** A Typst float occupies
1447
+ the column measure, so in the book only the vertical half of a position has an
1448
+ effect — top of the column or bottom of it — while the horizontal half does
1449
+ not. The website and a Foundry journal get true CSS wrap from the same
1450
+ directive. Expect the same statement, not the same page.
1451
+
1452
+ An image with no `float:` is an ordinary block in the flow, where it was
1453
+ written. A `.full-width` image is placed at the top of the page it falls on,
1454
+ since a block cannot leave the column it is set in.
1455
+
1456
+ ##### Both vocabularies are closed
1457
+
1458
+ `{.fullwidth}`, `{.full_width}`, `{width=800}` and `{float: middle}` are
1459
+ refused, located by file, line and column, and the build fails. An unrecognised
1460
+ value rendering as the ordinary width is the failure worth preventing, because
1461
+ it looks exactly like a directive that worked.
1462
+
1463
+ A directive holding anything this section does not name is not honoured at all,
1464
+ even in the part that parsed: a half-honoured directive is the same silent
1465
+ failure in a smaller costume. The image keeps its braces and renders them as
1466
+ literal text, so the mistake is visible on the page as well as in the log.
1467
+
1468
+ **Nothing else reaches emitted markup.** The two vocabularies and the address
1469
+ are all that leave a note; there is no `style`, no `id` and no arbitrary
1470
+ attribute, because data is never compiled into markup here. An address carries
1471
+ a scheme of `http:` or `https:`, or none at all.
1472
+
1473
+ ##### Where an address resolves
1474
+
1475
+ An image's address follows the rule [`img:` follows](#an-asset-paths-first-segment-says-which-package-owns-it)
1476
+ — its first segment says which package owns the file — and each surface
1477
+ resolves it to what that surface serves:
1478
+
1479
+ - **Foundry** is handed the path inside the install, so `images/map.webp`
1480
+ reaches a journal page as `modules/<package>/assets/images/map.webp`.
1481
+ - **The book** is handed a copy of the file, staged out of this package's own
1482
+ asset tree into the build directory before the compiler runs. An address
1483
+ naming a file this package does not ship — another package's, or a URL —
1484
+ cannot be staged, and the book prints the caption alone and says so.
1485
+ - **The website** passes the address through exactly as authored, because a
1486
+ site serves its imagery from its own asset host and this package is not told
1487
+ what that host is. An address that has to resolve there is written as a full
1488
+ URL.
1489
+
1089
1490
  #### Content tables
1090
1491
 
1091
1492
  A fenced `dataview` block is replaced by the table its query selects: