@heroiclands/package-build 22.20.0 → 22.22.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 (118) hide show
  1. package/CHANGELOG-content-build.md +0 -8
  2. package/CHANGELOG.md +204 -0
  3. package/assets/styles/content-base.css +162 -0
  4. package/bin/content-commands.mjs +26 -3
  5. package/bin/package-build.mjs +15 -2
  6. package/ci/content-format-reference.mjs +68 -1
  7. package/config.mjs +26 -0
  8. package/content-config.mjs +73 -4
  9. package/docs/api.md +46 -28
  10. package/docs/authoring/dates-and-calendars.md +4 -2
  11. package/docs/authoring/frontmatter.md +2 -2
  12. package/docs/authoring/links-and-markup.md +237 -28
  13. package/docs/configuration.md +27 -1
  14. package/docs/content-format.md +3 -1
  15. package/docs/guides/reference-pages.md +15 -0
  16. package/docs/reference/format-details.md +265 -42
  17. package/docs/reference/note-types.md +45 -28
  18. package/engine/actor-compiler.mjs +23 -0
  19. package/engine/anchored-sections.mjs +35 -4
  20. package/engine/anchors.mjs +10 -20
  21. package/engine/base-compiler.mjs +86 -21
  22. package/engine/base-styles.mjs +149 -0
  23. package/engine/bundles.mjs +0 -1
  24. package/engine/compendiums.mjs +8 -1
  25. package/engine/content-blocks.mjs +410 -0
  26. package/engine/content-captions.mjs +57 -8
  27. package/engine/content-footnotes.mjs +189 -0
  28. package/engine/content-format-check.mjs +15 -12
  29. package/engine/content-index.mjs +2 -2
  30. package/engine/content-links.mjs +14 -6
  31. package/engine/content-tables.mjs +45 -4
  32. package/engine/content-tasklists.mjs +189 -0
  33. package/engine/docs-checks.mjs +4 -4
  34. package/engine/extension-attributes.mjs +48 -2
  35. package/engine/frontmatter-lint.mjs +62 -66
  36. package/engine/frontmatter.mjs +3 -3
  37. package/engine/heading-attributes.mjs +308 -0
  38. package/engine/helpers.mjs +19 -13
  39. package/engine/homepage.mjs +8 -8
  40. package/engine/index.mjs +3 -0
  41. package/engine/infobox-render.mjs +21 -4
  42. package/engine/infobox.mjs +167 -7
  43. package/engine/journals.mjs +36 -17
  44. package/engine/macros.mjs +1 -1
  45. package/engine/markdown-expressions.mjs +178 -53
  46. package/engine/note-addresses.mjs +17 -8
  47. package/engine/note-dates.mjs +49 -0
  48. package/engine/note-frontmatter.mjs +7 -16
  49. package/engine/note-vocabulary.mjs +216 -42
  50. package/engine/page-lists.mjs +358 -0
  51. package/engine/pdf-build.mjs +76 -42
  52. package/engine/pdf-render.mjs +422 -49
  53. package/engine/rank-ladder.mjs +198 -0
  54. package/engine/reckoning-markers.mjs +1 -1
  55. package/engine/reference-targets.mjs +13 -1
  56. package/engine/retired-fields.mjs +8 -13
  57. package/engine/scenes.mjs +10 -3
  58. package/engine/site-build.mjs +41 -12
  59. package/engine/site-config.mjs +10 -1
  60. package/engine/site-index.mjs +39 -4
  61. package/engine/site-preview.mjs +18 -7
  62. package/engine/social-ties.mjs +52 -14
  63. package/engine/sql-tables.mjs +66 -9
  64. package/engine/standing-terms.mjs +19 -0
  65. package/engine/standings.mjs +538 -0
  66. package/engine/svg-theme.mjs +15 -4
  67. package/engine/system-block.mjs +12 -14
  68. package/engine/web-wikilinks.mjs +48 -1
  69. package/engine/wikilinks.mjs +3 -1
  70. package/hm3/actors.mjs +6 -3
  71. package/manifest.mjs +13 -0
  72. package/package.json +1 -1
  73. package/sohl/actors.mjs +82 -103
  74. package/sohl/item-fields.mjs +4 -5
  75. package/sohl/items.mjs +1 -1
  76. package/sohl/note-schemas.mjs +11 -21
  77. package/types/config.d.mts +9 -0
  78. package/types/engine/actor-compiler.d.mts +11 -0
  79. package/types/engine/anchored-sections.d.mts +24 -0
  80. package/types/engine/base-compiler.d.mts +19 -0
  81. package/types/engine/base-styles.d.mts +93 -0
  82. package/types/engine/content-blocks.d.mts +87 -0
  83. package/types/engine/content-captions.d.mts +14 -1
  84. package/types/engine/content-footnotes.d.mts +73 -0
  85. package/types/engine/content-format-check.d.mts +8 -6
  86. package/types/engine/content-tables.d.mts +12 -4
  87. package/types/engine/content-tasklists.d.mts +72 -0
  88. package/types/engine/extension-attributes.d.mts +22 -1
  89. package/types/engine/frontmatter.d.mts +1 -1
  90. package/types/engine/heading-attributes.d.mts +174 -0
  91. package/types/engine/helpers.d.mts +7 -4
  92. package/types/engine/homepage.d.mts +7 -7
  93. package/types/engine/index.d.mts +1 -0
  94. package/types/engine/infobox-render.d.mts +6 -1
  95. package/types/engine/journals.d.mts +14 -7
  96. package/types/engine/markdown-expressions.d.mts +23 -0
  97. package/types/engine/note-frontmatter.d.mts +1 -4
  98. package/types/engine/note-vocabulary.d.mts +98 -8
  99. package/types/engine/page-lists.d.mts +126 -0
  100. package/types/engine/pdf-render.d.mts +72 -2
  101. package/types/engine/rank-ladder.d.mts +32 -0
  102. package/types/engine/retired-fields.d.mts +4 -6
  103. package/types/engine/site-build.d.mts +13 -1
  104. package/types/engine/site-config.d.mts +9 -0
  105. package/types/engine/site-index.d.mts +13 -1
  106. package/types/engine/site-preview.d.mts +1 -1
  107. package/types/engine/sql-tables.d.mts +31 -7
  108. package/types/engine/standing-terms.d.mts +16 -0
  109. package/types/engine/standings.d.mts +147 -0
  110. package/types/engine/system-block.d.mts +11 -12
  111. package/types/engine/web-wikilinks.d.mts +3 -1
  112. package/types/engine/wikilinks.d.mts +2 -1
  113. package/types/hm3/actors.d.mts +3 -3
  114. package/types/sohl/actors.d.mts +25 -7
  115. package/engine/content-admonitions.mjs +0 -114
  116. package/engine/content-secrets.mjs +0 -112
  117. package/types/engine/content-admonitions.d.mts +0 -24
  118. package/types/engine/content-secrets.d.mts +0 -17
@@ -62,7 +62,6 @@
62
62
  pass, which reads them exactly as it reads a local pack.
63
63
 
64
64
  Three things it does deliberately:
65
-
66
65
  - **Pins, rather than following `latest`.** A published
67
66
  `releases/latest/download/…` URL is rewritten to the declared
68
67
  `compatibility.verified` version, so a build names one particular dependency
@@ -177,7 +176,6 @@
177
176
 
178
177
  `content-build lint` now checks frontmatter as well as addresses. Five classes,
179
178
  each previously reported somewhere other than where it was made, or not at all:
180
-
181
179
  - **Unknown or retired type**, told what replaced it.
182
180
  - **Missing required property** — `dimensions` on a map, `subType` on a skill.
183
181
  - **Wrong value shape** — `weight: heavy` where a number belongs.
@@ -215,7 +213,6 @@
215
213
  configures (#69).
216
214
 
217
215
  Two new commands:
218
-
219
216
  - `content-build format [paths..] [--write]` — Prettier, with the shared
220
217
  configuration.
221
218
  - `content-build markdown [paths..] [--fix]` — markdownlint, with a narrow,
@@ -310,7 +307,6 @@ version` regenerates it in every repository here.
310
307
  entries) exactly as the scripts it replaces emit them, on the same toolchain.
311
308
 
312
309
  Also new:
313
-
314
310
  - `paths.manifestOut` (default `build/manifests`) — where the manifest is
315
311
  written. Deliberately not `paths.manifests`, which is the _inbound_ directory
316
312
  of vendored foreign manifests that `links` consumes.
@@ -377,7 +373,6 @@ compile`. Publishing the _same tree_ as a website was a script each consumer
377
373
  exotic, so neither is left to care.
378
374
 
379
375
  Also new:
380
-
381
376
  - `engine/site-build.mjs` exports each stage (`collectContentPages`,
382
377
  `collectTreePages`, `siteGates`, `renderPages`, `writeSectionLandings`) for a
383
378
  consumer that needs a step rather than the whole command.
@@ -422,7 +417,6 @@ compile`. Publishing the _same tree_ as a website was a script each consumer
422
417
  stale by `--check` on every clean checkout — the two undoing each other forever.
423
418
  Three things were making that happen, and all three are fixed at the source
424
419
  rather than by adding a formatting pass:
425
-
426
420
  - Table columns are padded to their widest cell, which is what Prettier's
427
421
  alignment comes to for this content.
428
422
  - The worked example's fence said `yaml`, but the block is a whole note —
@@ -632,7 +626,6 @@ compile`. Publishing the _same tree_ as a website was a script each consumer
632
626
  hide the next drift instead of surfacing it.
633
627
 
634
628
  Two deliberate differences from the code it replaces:
635
-
636
629
  - **The `corpus` derivation is dropped.** `corpus` is not a registered item
637
630
  type, so nothing can compile to one and the branch matched nothing — the same
638
631
  class of dead code as the gate that caused the bug. It was in SoHL's copy and
@@ -654,7 +647,6 @@ compile`. Publishing the _same tree_ as a website was a script each consumer
654
647
  `gh release create`, because cutting the Release is what published. Nothing
655
648
  enforced the last step, so on 2026-08-21 `main` carried 0.5.1 while npm served
656
649
  0.4.0: two versions merged and never published, with no check red.
657
-
658
650
  - Every pull request now declares its bump as a `.changeset/*.md` file, and CI's
659
651
  **Changeset declared** job fails one that does not. `npx changeset add --empty`
660
652
  is how a change says it needs no release — explicitly, rather than by omission.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,209 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ **Diagnostics**
8
+
9
+ - A dead `#anchor` now fails the website build, matching the compendium build
10
+ and the link checker — a link that used to publish a dead section quietly now
11
+ reports it.
12
+ - A note with more than one problem in its body now reports every one of them
13
+ in a single build, instead of only the first.
14
+
15
+ **The book**
16
+
17
+ - A page link in a book now goes where it says, and one into a package the book
18
+ depends on reaches that package's pages; the address they are built from is
19
+ `package.json`'s `homepage`, which a book now needs.
20
+ - A note written for the next author — a `markdownlint` pragma, an aside about a
21
+ quoted notice — no longer appears on the page.
22
+ - A box written inside a GM-only section prints as a box.
23
+ - A box's title prints as written, emphasis included, and one carrying a bracket,
24
+ a `#` or a `$` no longer stops the book being made.
25
+ - Two captions sharing one id each print their own block once, under their own
26
+ number.
27
+ - A link that resolves nowhere now fails the book build, as it already fails the
28
+ compendium and website builds, and the book is still written so the page
29
+ carrying the link can be found.
30
+ - Markup the book cannot set is reported at the file and line it is written on
31
+ rather than printed on the page, in a title page and a prose file as well as in
32
+ a note.
33
+ - A table query that selects no rows now says so, and names the line, where it
34
+ read `[object Object]`.
35
+ - A link the book cannot resolve names the line and column it is written on.
36
+
37
+ **Affiliations**
38
+
39
+ - A rank with no title or no description is now reported instead of shipping, so
40
+ a member's standing can no longer resolve to a blank on a sheet or a page.
41
+ - A rank level that is not a whole number, and a key a rank does not take, are
42
+ reported too, each naming the line the rank is written on.
43
+ - Every problem on one rank is reported at once, so a ladder is fixed in a single
44
+ pass.
45
+ - A rank needs only its level, title and description; the lore note is for a
46
+ standing that needs more said about it than a description can hold.
47
+ - An affiliation's rank ladder now appears on its page, alongside its offices,
48
+ ordered by level.
49
+ - A note's `lore`, `parents`, `domains` and `economy` lists now reach the page
50
+ wherever they are authored, instead of vanishing silently.
51
+ - A being's entry in a body states the rung it stands on — an ordinary member is
52
+ 1, someone cast out is 0 — and an entry that states none is reported.
53
+ - An office is not a rung, so an entry naming a post and no standing is reported
54
+ with the rest of them.
55
+ - A body that gives two rungs one level is reported on the second, so what a
56
+ standing is called does not depend on which rung is written first.
57
+
58
+ **Authoring**
59
+
60
+ - A note's frontmatter accepts only the documented keys; an unrecognised one is an error at its own line, naming every accepted key and the one it was probably meant to be.
61
+ - A page takes its heading from the note's name, so a homepage stating a title any other way publishes the name its package manifest carries.
62
+ - Active Effects and document flags are read from the game-system block that carries them, so a note stating either outside one ships a document without them.
63
+ - Info boxes, warning boxes and captions can be written again: any note using
64
+ one failed its build with an error about a secret block it did not contain.
65
+ - A note may now mix boxes, captions and GM-only sections freely, and a box
66
+ inside a GM-only section stays inside it.
67
+ - A `:::` line that closes nothing says so plainly instead of naming the wrong
68
+ construct.
69
+ - An image may be sized `full-width`, and a float takes a named corner — both
70
+ always worked and neither was written down.
71
+
72
+ **Beings**
73
+
74
+ - A being's complexion can now hold several values, because a face is weathered
75
+ and ruddy at once rather than only one of them.
76
+ - Several of them read as one phrase on a sheet or a page — "weathered, ruddy and
77
+ scarred complexion" — rather than saying the word once per value.
78
+ - A being may be recorded as `nonbinary`, or as having no gender at all, where
79
+ before only `male`, `female` and `other` were named.
80
+ - A frame is `scant`, `light`, `medium`, `heavy` or `massive`. The reference named
81
+ a `heavy` frame `large`, which nothing wrote.
82
+ - A being's body and movement are authored under `sohl.system`, and its
83
+ attributes and skills as `sohl.items` entries; a note writing either
84
+ elsewhere in the block is reported.
85
+
86
+ **Actors**
87
+
88
+ - An `{#appearance}` or `{#dossier}` anchor written on a heading below the top
89
+ level now fails the build naming the file and the line, instead of compiling
90
+ a blank appearance or biography with nothing to say why.
91
+
92
+ **Dates**
93
+
94
+ - A birth or death date written without quotes is reported where a day's
95
+ trailing zero could have been lost, naming both days the value could mean. A
96
+ calendar's epoch and a moon's new day read the same way.
97
+
98
+ **Definition lists**
99
+
100
+ - Every package now ships a definition-list style, so a term and its definition
101
+ line up in two columns with the terms sharing one right edge.
102
+ - A narrow journal window stacks each term above its own definition instead of
103
+ cramping both into columns, and widening the window restores them.
104
+ - A term is bold wherever it appears, so writing one needs no `**` around it.
105
+ - Infobox rows in the book sit against the same right edge, with space between
106
+ rows rather than run together.
107
+
108
+ **Packages**
109
+
110
+ - `packageBuild.baseStyles: false` declines the shared style for a package that
111
+ wants the surface entirely to itself.
112
+
113
+ **Named blocks**
114
+
115
+ - A `:::secret`, `:::info` or `:::warn` block now takes a title of its own, so a
116
+ GM passage can say "For the GM" or "If they ask about the harbour" instead of
117
+ one fixed word.
118
+ - A block also takes an id, classes and other attributes, so a note can link to
119
+ one and a package can style a particular kind its own way.
120
+ - Without a title a block heads itself **Secret**, **Info** or **Warn**. A
121
+ warning that used to head "Warning" now heads "Warn", and a secret that used to
122
+ head "GM note" now heads "Secret"; writing `title="GM note"` keeps the old
123
+ wording.
124
+ - A secret on a web page now opens with its own title rather than the word
125
+ "Spoiler", and carries its kind so a site can style it.
126
+ - A block's colours come from a stylesheet rather than from the block itself,
127
+ so a light panel no longer keeps a light background against dark page text.
128
+ - A misspelled block name is now reported instead of being left in the page as
129
+ the literal `:::name` an author typed.
130
+ - One malformed block no longer stops every other block in the same note from
131
+ rendering.
132
+ - A box written inside a GM-only section stays inside it, and a box inside
133
+ another box says so rather than rendering as a muddle.
134
+
135
+ **Configuration**
136
+
137
+ - A `compatibility.verified` build below the package's own `compatibility.minimum`
138
+ is now reported instead of shipping a manifest that claims a build was verified
139
+ against a floor it cannot install on.
140
+ - The same check applies to a declared system's and a related package's own
141
+ `compatibility`, naming the file and line of the offending `verified`.
142
+
143
+ ### Patch Changes
144
+
145
+ **Lists**
146
+
147
+ - A list item opened with `- [ ]` or `- [x]` reads as its own words everywhere
148
+ — on the website, in the compendium, and in the book — never as a checkbox.
149
+ - Writing one that way is reported at its own line, naming the ordinary list
150
+ it belongs in.
151
+
152
+ **Footnotes**
153
+
154
+ - A footnote referenced inside a box or a caption now prints and numbers with
155
+ the rest of the note instead of showing as literal text.
156
+ - A definition written below the top level of a note is reported, as is a
157
+ reference nothing defines.
158
+ - A definition no reference uses is reported rather than dropped from the page.
159
+
160
+ **Named blocks and captions**
161
+
162
+ - A heading that would open a page of its own is reported when it is written
163
+ inside a box or a caption.
164
+
165
+ **Headings**
166
+
167
+ - A heading carrying classes or attributes beside its anchor now anchors on the
168
+ anchor alone, so a link written to it resolves on every surface.
169
+ - A Foundry journal and a web page carry a heading's classes and attributes onto
170
+ the heading, and no surface prints the braces.
171
+ - A heading whose braces hold something other than an attribute block is
172
+ reported at its own line instead of being typeset.
173
+ - `.secret` on the heading that opens a section makes its Foundry page the
174
+ GM's alone.
175
+ - A withheld section reads as a spoiler on the web and a labelled block in the
176
+ book, which conceal it rather than withhold it.
177
+ - A heading refuses `id=`, `class=` and any key beginning `on`, as a named block
178
+ does, so no note writes an event handler onto a page.
179
+
180
+ **Page lists**
181
+
182
+ - A note can now write a page list: name a tag, and the pages carrying it come out as a list of links.
183
+ - The list reaches a reader in Foundry, on the website and in the book alike, from the one directive.
184
+ - A page joins the list by carrying its tag and leaves by losing it, so a landing page stays true as notes are written.
185
+ - A list can print each page's description beside its link, and can be restricted to one note type.
186
+ - A tag that no page carries is reported with the tag it looked for, rather than printing nothing.
187
+
188
+ **Beings**
189
+
190
+ - A being's `socialTies` map now builds without the two findings it always
191
+ produced, so a patron, friend, dependent, acquaintance, rival or nemesis
192
+ can be written.
193
+
194
+ ## 22.21.0
195
+
196
+ ### Minor Changes
197
+
198
+ **Standing in a body** — A being's `affiliations` is now a map keyed by the body, and each entry says what the being is there: `rank`, a level on that body's own ladder, and `office`, one of the posts it names. A page prints the standing closing on the body it belongs to — "War Chief, Hárár, of Vrystwald Tribes" — and an office links to the description the body already carries. In SoHL a being now arrives carrying one affiliation item per body it belongs to, standing included.
199
+
200
+ **Offices read the right way round** — An affiliation's offices each take their own row, with the post's name as the label and its description beside it, and each row can be linked to directly.
201
+
202
+ ### Patch Changes
203
+
204
+ - Icons drawn with outlines follow the colour scheme instead of keeping a black rim in dark mode.
205
+ - An explicitly black outline also moves to ink in light mode, matching the icon's filled shapes.
206
+
3
207
  ## 22.20.0
4
208
 
5
209
  ### Minor Changes
@@ -0,0 +1,162 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /*
15
+ * The base stylesheet every package ships, staged into the package root and
16
+ * named first in the manifest's `styles`.
17
+ *
18
+ * It carries only what every package needs for its compiled content to be
19
+ * legible, which today is the definition list: an infobox emits one, and so
20
+ * does the ordinary markdown `Term` / `: definition` syntax. A package keeps
21
+ * its own stylesheet for its own needs and overrides anything here from it.
22
+ *
23
+ * `packageBuild.baseStyles: false` declines it.
24
+ *
25
+ * **The scope is the content, not the interface.** Foundry styles `dl dt` and
26
+ * `dl dd` itself, application-wide, and a package stylesheet loads into a later
27
+ * cascade layer than those rules sit in — so an unscoped `dl` here would
28
+ * restyle every definition list in Foundry's own chrome and in every other
29
+ * package's sheets. The four roots are where compiled content renders: a
30
+ * journal page, a journal entry, an enriched field, and the editor.
31
+ */
32
+
33
+ /*
34
+ * Definition lists, in two states of one style.
35
+ *
36
+ * **Wide.** Two columns, the term right-aligned against a fixed gutter so every
37
+ * term in the list shares one right edge, the definition in the remaining
38
+ * space. The fixed track is what produces the shared edge; a `max-content`
39
+ * track cannot, because each row then sizes itself.
40
+ *
41
+ * **Narrow.** Stacked, the term above its own definition, with space between
42
+ * pairs and none between a term and the definition it belongs to.
43
+ *
44
+ * **The measurement is the list, not the screen.** A Foundry journal is a
45
+ * resizable window, usually far narrower than the viewport, so a viewport media
46
+ * query would hold a journal stacked while the screen said otherwise — or cramp
47
+ * it into two columns at 300px. `container-type: inline-size` on the `dl` and a
48
+ * `@container` query on its children measure the list itself, and give the same
49
+ * two states on a page, in a window and in a sidebar.
50
+ *
51
+ * The narrow state is reached by spanning both tracks rather than by changing
52
+ * the template, because an element cannot query the container it establishes:
53
+ * the `dl` holds the grid and the container, so only `dt` and `dd` can respond.
54
+ * Row spacing is the definition's bottom margin rather than the grid's
55
+ * `row-gap` for the same reason — `row-gap` sits on the `dl`, and the two
56
+ * states need different values.
57
+ *
58
+ * The three tunable values are custom properties, so a package adjusts the term
59
+ * column, the gutter or the stacking point without copying the rule.
60
+ */
61
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) dl {
62
+ display: grid;
63
+ /* `minmax(0, …)` keeps the track at its full width wherever there is room
64
+ * and lets it shrink rather than overflow where there is not. */
65
+ grid-template-columns: minmax(0, var(--pb-deflist-term-width, 11rem)) 1fr;
66
+ column-gap: var(--pb-deflist-gutter, 2rem);
67
+ container-type: inline-size;
68
+ container-name: pb-deflist;
69
+ }
70
+
71
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) dl > dt {
72
+ grid-column: 1;
73
+ /* The one rule that holds on every surface, so an author writes the term
74
+ * and nothing else rather than bolding it by hand. */
75
+ font-weight: 600;
76
+ text-align: right;
77
+ line-height: 1.6;
78
+ margin: 0;
79
+ }
80
+
81
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) dl > dd {
82
+ grid-column: 2;
83
+ margin: 0 0 var(--pb-deflist-row-gap, 1.5rem);
84
+ line-height: 1.6;
85
+ }
86
+
87
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) dl > dd:last-child {
88
+ margin-bottom: 0;
89
+ }
90
+
91
+ /*
92
+ * Stacked, below the width two columns need: the term column, the gutter, and
93
+ * enough left for a definition to read as prose rather than as a word per line.
94
+ *
95
+ * Named, so the query resolves against a list this sheet set up rather than
96
+ * against whichever ancestor container happens to be nearest.
97
+ */
98
+ @container pb-deflist (max-width: 32rem) {
99
+ dt {
100
+ grid-column: 1 / -1;
101
+ text-align: left;
102
+ }
103
+
104
+ dd {
105
+ grid-column: 1 / -1;
106
+ }
107
+ }
108
+
109
+ /*
110
+ * Named body blocks. `:::info` and `:::warn` emit a `<section>` carrying the
111
+ * block's name as its class, so the appearance belongs here rather than in an
112
+ * inline `style` a consumer cannot override.
113
+ *
114
+ * `secret` is deliberately absent: Foundry styles `section.secret` itself and
115
+ * owns the reveal control on it, and a rule here would fight both.
116
+ *
117
+ * Every colour is a custom property, so a package overrides one from its own
118
+ * sheet without restating the layout.
119
+ */
120
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
121
+ :is(section.info, section.warn) {
122
+ border-left: 3px solid var(--pb-block-edge);
123
+ background: var(--pb-block-fill);
124
+ color: var(--pb-block-text, inherit);
125
+ padding: 0.5em 0.8em;
126
+ margin: 0.75em 0;
127
+ }
128
+
129
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
130
+ :is(section.info, section.warn)
131
+ > strong:first-child {
132
+ color: var(--pb-block-edge);
133
+ }
134
+
135
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) section.info {
136
+ --pb-block-edge: #2f6f9f;
137
+ --pb-block-fill: #eef6fb;
138
+ }
139
+
140
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) section.warn {
141
+ --pb-block-edge: #9a6700;
142
+ --pb-block-fill: #fff5db;
143
+ }
144
+
145
+ /*
146
+ * A light panel keeps a dark foreground whatever the surrounding theme does to
147
+ * the inherited colour, because the fill does not follow it.
148
+ */
149
+ @media (prefers-color-scheme: dark) {
150
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
151
+ :is(section.info, section.warn) {
152
+ --pb-block-text: #1b1b1b;
153
+ }
154
+
155
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) section.info {
156
+ --pb-block-edge: #1d4f75;
157
+ }
158
+
159
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) section.warn {
160
+ --pb-block-edge: #7a5100;
161
+ }
162
+ }
@@ -70,6 +70,7 @@ import { lintContentCharset } from "../engine/content-charset.mjs";
70
70
  import { lintContentHtml } from "../engine/content-html.mjs";
71
71
  import { lintContentIcons } from "../engine/content-icons.mjs";
72
72
  import { lintContentImages } from "../engine/content-images.mjs";
73
+ import { lintContentTaskLists } from "../engine/content-tasklists.mjs";
73
74
  import {
74
75
  declaredSystems,
75
76
  lintFrontmatter,
@@ -1156,6 +1157,13 @@ function lintCommand() {
1156
1157
  config,
1157
1158
  });
1158
1159
 
1160
+ // A list item opened with `[ ]` or `[x]` — nothing here is
1161
+ // miscompiled, since every surface sets the marker as the
1162
+ // item's own text and all three agree, so this is advisory.
1163
+ const taskLists = lintContentTaskLists(root, {
1164
+ skipDirectories: config.skipDirectories,
1165
+ });
1166
+
1159
1167
  // Whether every empty body is a stub on purpose, and how
1160
1168
  // finished the tree is. The counts and the oldest drafts are
1161
1169
  // prose rather than findings: a note nobody has finished is not
@@ -1175,6 +1183,7 @@ function lintCommand() {
1175
1183
  ...icons.findings,
1176
1184
  ...html.findings,
1177
1185
  ...images.findings,
1186
+ ...taskLists.findings,
1178
1187
  ...states.findings,
1179
1188
  ];
1180
1189
  // Only an **error** fails the run. Every finding was an error
@@ -1461,14 +1470,20 @@ function linksCommand() {
1461
1470
  }),
1462
1471
  );
1463
1472
 
1473
+ // Worded by the shared table, like every finding below it: a
1474
+ // sentence written here is free to describe the same defect
1475
+ // differently from the build that also refuses it, which is the
1476
+ // drift the table exists to prevent.
1464
1477
  for (const d of deadAnchors) {
1465
1478
  emitDiagnostic({
1466
1479
  file: d.note.file,
1467
1480
  ...positionOfLiteral(d.note.raw, d.text, d.occurrence),
1468
1481
  severity: "error",
1469
- message:
1470
- `link [[${d.link}]] points at an anchor no ` +
1471
- `heading in ${d.dest.rel} declares`,
1482
+ message: linkFindingMessage({
1483
+ reason: "unknown-anchor",
1484
+ target: d.target,
1485
+ anchor: d.anchor,
1486
+ }),
1472
1487
  });
1473
1488
  }
1474
1489
  // Every link is an address and every address must
@@ -1799,6 +1814,12 @@ function siteCommand() {
1799
1814
  for (const e of result.captionErrors) {
1800
1815
  emitDiagnostic({ ...e, severity: "error" });
1801
1816
  }
1817
+ for (const e of result.headingErrors) {
1818
+ emitDiagnostic({ ...e, severity: "error" });
1819
+ }
1820
+ for (const e of result.footnoteErrors) {
1821
+ emitDiagnostic({ ...e, severity: "error" });
1822
+ }
1802
1823
  // Reported the way the pack build reports the very same
1803
1824
  // finding: `file:line:column: error: message`, path first, and
1804
1825
  // the message from the shared table, not a
@@ -1838,6 +1859,8 @@ function siteCommand() {
1838
1859
  result.expressionErrors.length ||
1839
1860
  result.secretErrors.length ||
1840
1861
  result.captionErrors.length ||
1862
+ result.headingErrors.length ||
1863
+ result.footnoteErrors.length ||
1841
1864
  result.wikiErrors.length ||
1842
1865
  result.imageErrors.length
1843
1866
  ) {
@@ -93,6 +93,7 @@ import { DEPLOY_ROOT } from "../engine/site-config.mjs";
93
93
  import { compilesFoundryDocuments } from "../content-config.mjs";
94
94
  import { loadPackConfig, packConfigPath, resolveConfigFile } from "../engine/pack-config.mjs";
95
95
  import { cleanBuildArtifacts, resetStage, stageAssets } from "../stage.mjs";
96
+ import { baseStyleStageEntry } from "../engine/base-styles.mjs";
96
97
  import { buildSchemaArtifact } from "../engine/schema-extract.mjs";
97
98
  import { emitCalendarArtifacts } from "../engine/calendar-artifacts.mjs";
98
99
  import { dateFromCalendar, dateToCalendar } from "../engine/date-conversion.mjs";
@@ -424,7 +425,7 @@ function assetsCommand() {
424
425
  builder: (y) => y,
425
426
  handler: handler(async () => {
426
427
  const config = loadPackageBuildConfig();
427
- if (!config.assets.length) {
428
+ if (!config.assets.length && !config.baseStyles) {
428
429
  console.log("package-build: no `packageBuild.assets` declared; nothing to stage.");
429
430
  return;
430
431
  }
@@ -462,10 +463,22 @@ function assetsCommand() {
462
463
  from,
463
464
  path.join(config.stageDir, to),
464
465
  ]);
465
- const { entries: count, files } = stageAssets(entries, {
466
+ let { entries: count, files } = stageAssets(entries, {
466
467
  cwd: config.rootDir,
467
468
  transform,
468
469
  });
470
+
471
+ // Staged in its own pass, without the repository's transform: the
472
+ // sheet is this toolchain's file rather than one of the
473
+ // repository's assets, and a `transform` written to retheme a
474
+ // package's own SVGs has no business being handed it.
475
+ if (config.baseStyles) {
476
+ const staged = stageAssets([baseStyleStageEntry(config.stageDir)], {
477
+ cwd: config.rootDir,
478
+ });
479
+ count += staged.entries;
480
+ files += staged.files;
481
+ }
469
482
  console.log(`✅ Static assets staged (${count} entries, ${files} files).`);
470
483
  }),
471
484
  };
@@ -9,9 +9,12 @@ import { formatGenerated } from "../engine/format-generated.mjs";
9
9
  import { loadContentFormat } from "../engine/content-format.mjs";
10
10
  import {
11
11
  BEING_ARCHETYPES,
12
+ NOTE_TOP_LEVEL_FIELDS,
12
13
  NOTE_VOCABULARY,
13
14
  SHARED_DATA_FIELDS,
14
15
  } from "../engine/note-vocabulary.mjs";
16
+ import { EXPRESSION_HELPERS } from "../engine/markdown-expressions.mjs";
17
+ import { PAGE_LIST_ATTRIBUTES } from "../engine/page-lists.mjs";
15
18
 
16
19
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
17
20
  const output = path.join(root, "docs/reference/note-types.md");
@@ -39,6 +42,16 @@ const rows = [
39
42
  "",
40
43
  "The [first-note guide](../authoring/first-note.md) teaches the shape of a note. [Frontmatter](../authoring/frontmatter.md) explains shared and system-specific values. The [detailed reference](format-details.md) gives the full behavior and examples.",
41
44
  "",
45
+ "## Top-level keys",
46
+ "",
47
+ "These are the only keys a note writes at its top level, in the order the formatter puts them. A key absent from this table is a lint error at its own line: the region is closed, so the key reaches no document and no page. Facts about the subject belong under `data`, and values for one game system belong inside that system's block.",
48
+ "",
49
+ "| Key | Meaning |",
50
+ "| --- | --- |",
51
+ ...NOTE_TOP_LEVEL_FIELDS.map(
52
+ (field) => `| \`${cell(field.name)}\` | ${cell(field.describe)} |`,
53
+ ),
54
+ "",
42
55
  "## Shared `data` fields",
43
56
  "",
44
57
  "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.",
@@ -106,7 +119,61 @@ if (process.argv.includes("--check")) {
106
119
  process.exitCode = 1;
107
120
  }
108
121
  } else {
109
- process.stdout.write(result);
122
+ fs.writeFileSync(output, result);
123
+ }
124
+
125
+ // The two tables `docs/authoring/links-and-markup.md` generates: every helper
126
+ // an expression may call, and every attribute a page-list fence takes. Both are
127
+ // written from the one list the engine reads, so neither document can fall
128
+ // behind a new entry. Replaced in one pass and written once, so a `--check` run
129
+ // names the file rather than one of the tables in it.
130
+ const markupPath = path.join(root, "docs/authoring/links-and-markup.md");
131
+ const markup = fs.readFileSync(markupPath, "utf8");
132
+ const GENERATED_TABLES = [
133
+ {
134
+ marker: "expression-helpers",
135
+ rows: [
136
+ "| Helper | Parameters | What it gives you |",
137
+ "| ------ | ---------- | ----------------- |",
138
+ ...Object.entries(EXPRESSION_HELPERS).map(
139
+ ([name, { params, summary }]) =>
140
+ `| \`${name}\` | \`${params}\` | ${cell(summary)} |`,
141
+ ),
142
+ ],
143
+ },
144
+ {
145
+ marker: "page-list-attributes",
146
+ rows: [
147
+ "| Attribute | Value | Default | What it does |",
148
+ "| --------- | ----- | ------- | ------------ |",
149
+ ...Object.entries(PAGE_LIST_ATTRIBUTES).map(
150
+ ([name, { value, default: fallback, summary }]) =>
151
+ `| \`${name}\` | ${cell(value)} | ${cell(fallback)} | ${cell(summary)} |`,
152
+ ),
153
+ ],
154
+ },
155
+ ];
156
+ let markupBody = markup;
157
+ for (const { marker, rows } of GENERATED_TABLES) {
158
+ const region = new RegExp(`<!-- ${marker}:start -->[\\s\\S]*?<!-- ${marker}:end -->`);
159
+ if (!region.test(markupBody)) {
160
+ throw new Error(`The ${marker} reference requires its generation markers`);
161
+ }
162
+ markupBody = markupBody.replace(
163
+ region,
164
+ `<!-- ${marker}:start -->\n\n${rows.join("\n")}\n\n<!-- ${marker}:end -->`,
165
+ );
166
+ }
167
+ const updatedMarkup = await formatGenerated(markupBody, markupPath);
168
+ if (process.argv.includes("--check")) {
169
+ if (markup !== updatedMarkup) {
170
+ console.error(
171
+ "docs/authoring/links-and-markup.md: error: run node ci/content-format-reference.mjs to refresh its generated tables",
172
+ );
173
+ process.exitCode = 1;
174
+ }
175
+ } else {
176
+ fs.writeFileSync(markupPath, updatedMarkup);
110
177
  }
111
178
 
112
179
  const detailsPath = path.join(root, "docs/reference/format-details.md");