@heroiclands/package-build 22.21.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 (110) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/assets/styles/content-base.css +162 -0
  3. package/bin/content-commands.mjs +26 -3
  4. package/bin/package-build.mjs +15 -2
  5. package/ci/content-format-reference.mjs +68 -1
  6. package/config.mjs +26 -0
  7. package/content-config.mjs +73 -4
  8. package/docs/api.md +36 -18
  9. package/docs/authoring/dates-and-calendars.md +4 -2
  10. package/docs/authoring/frontmatter.md +2 -2
  11. package/docs/authoring/links-and-markup.md +237 -28
  12. package/docs/configuration.md +27 -1
  13. package/docs/content-format.md +3 -1
  14. package/docs/guides/reference-pages.md +15 -0
  15. package/docs/reference/format-details.md +224 -46
  16. package/docs/reference/note-types.md +21 -4
  17. package/engine/actor-compiler.mjs +23 -0
  18. package/engine/anchored-sections.mjs +35 -4
  19. package/engine/anchors.mjs +10 -20
  20. package/engine/base-compiler.mjs +86 -21
  21. package/engine/base-styles.mjs +149 -0
  22. package/engine/bundles.mjs +0 -1
  23. package/engine/compendiums.mjs +8 -1
  24. package/engine/content-blocks.mjs +410 -0
  25. package/engine/content-captions.mjs +57 -8
  26. package/engine/content-footnotes.mjs +189 -0
  27. package/engine/content-format-check.mjs +15 -12
  28. package/engine/content-index.mjs +2 -2
  29. package/engine/content-links.mjs +14 -6
  30. package/engine/content-tables.mjs +45 -4
  31. package/engine/content-tasklists.mjs +189 -0
  32. package/engine/docs-checks.mjs +4 -4
  33. package/engine/extension-attributes.mjs +48 -2
  34. package/engine/frontmatter-lint.mjs +47 -64
  35. package/engine/frontmatter.mjs +3 -3
  36. package/engine/heading-attributes.mjs +308 -0
  37. package/engine/helpers.mjs +14 -13
  38. package/engine/homepage.mjs +8 -8
  39. package/engine/index.mjs +3 -0
  40. package/engine/infobox-render.mjs +15 -3
  41. package/engine/infobox.mjs +83 -8
  42. package/engine/journals.mjs +36 -17
  43. package/engine/macros.mjs +1 -1
  44. package/engine/markdown-expressions.mjs +178 -53
  45. package/engine/note-dates.mjs +49 -0
  46. package/engine/note-frontmatter.mjs +7 -16
  47. package/engine/note-vocabulary.mjs +172 -37
  48. package/engine/page-lists.mjs +358 -0
  49. package/engine/pdf-build.mjs +76 -42
  50. package/engine/pdf-render.mjs +422 -49
  51. package/engine/rank-ladder.mjs +198 -0
  52. package/engine/reckoning-markers.mjs +1 -1
  53. package/engine/retired-fields.mjs +8 -13
  54. package/engine/scenes.mjs +10 -3
  55. package/engine/site-build.mjs +41 -12
  56. package/engine/site-config.mjs +10 -1
  57. package/engine/site-index.mjs +18 -2
  58. package/engine/site-preview.mjs +18 -7
  59. package/engine/social-ties.mjs +52 -14
  60. package/engine/sql-tables.mjs +66 -9
  61. package/engine/standings.mjs +89 -6
  62. package/engine/system-block.mjs +12 -14
  63. package/engine/web-wikilinks.mjs +48 -1
  64. package/hm3/actors.mjs +6 -3
  65. package/manifest.mjs +13 -0
  66. package/package.json +1 -1
  67. package/sohl/actors.mjs +22 -99
  68. package/sohl/item-fields.mjs +4 -5
  69. package/sohl/items.mjs +1 -1
  70. package/sohl/note-schemas.mjs +11 -21
  71. package/types/config.d.mts +9 -0
  72. package/types/engine/actor-compiler.d.mts +11 -0
  73. package/types/engine/anchored-sections.d.mts +24 -0
  74. package/types/engine/base-compiler.d.mts +19 -0
  75. package/types/engine/base-styles.d.mts +93 -0
  76. package/types/engine/content-blocks.d.mts +87 -0
  77. package/types/engine/content-captions.d.mts +14 -1
  78. package/types/engine/content-footnotes.d.mts +73 -0
  79. package/types/engine/content-format-check.d.mts +8 -6
  80. package/types/engine/content-tables.d.mts +12 -4
  81. package/types/engine/content-tasklists.d.mts +72 -0
  82. package/types/engine/extension-attributes.d.mts +22 -1
  83. package/types/engine/frontmatter.d.mts +1 -1
  84. package/types/engine/heading-attributes.d.mts +174 -0
  85. package/types/engine/helpers.d.mts +7 -4
  86. package/types/engine/homepage.d.mts +7 -7
  87. package/types/engine/index.d.mts +1 -0
  88. package/types/engine/infobox-render.d.mts +6 -1
  89. package/types/engine/journals.d.mts +14 -7
  90. package/types/engine/markdown-expressions.d.mts +23 -0
  91. package/types/engine/note-frontmatter.d.mts +1 -4
  92. package/types/engine/note-vocabulary.d.mts +82 -6
  93. package/types/engine/page-lists.d.mts +126 -0
  94. package/types/engine/pdf-render.d.mts +72 -2
  95. package/types/engine/rank-ladder.d.mts +32 -0
  96. package/types/engine/retired-fields.d.mts +4 -6
  97. package/types/engine/site-build.d.mts +13 -1
  98. package/types/engine/site-config.d.mts +9 -0
  99. package/types/engine/site-index.d.mts +13 -1
  100. package/types/engine/site-preview.d.mts +1 -1
  101. package/types/engine/sql-tables.d.mts +31 -7
  102. package/types/engine/standings.d.mts +21 -3
  103. package/types/engine/system-block.d.mts +11 -12
  104. package/types/engine/web-wikilinks.d.mts +3 -1
  105. package/types/hm3/actors.d.mts +3 -3
  106. package/types/sohl/actors.d.mts +6 -7
  107. package/engine/content-admonitions.mjs +0 -114
  108. package/engine/content-secrets.mjs +0 -112
  109. package/types/engine/content-admonitions.d.mts +0 -24
  110. package/types/engine/content-secrets.d.mts +0 -17
package/CHANGELOG.md CHANGED
@@ -1,5 +1,196 @@
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
+
3
194
  ## 22.21.0
4
195
 
5
196
  ### 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");
package/config.mjs CHANGED
@@ -65,6 +65,7 @@ const SECTION_KEYS = [
65
65
  "stageDir",
66
66
  "assets",
67
67
  "assetTransform",
68
+ "baseStyles",
68
69
  "manifest",
69
70
  "manifestFlags",
70
71
  "schema",
@@ -205,6 +206,23 @@ function requireNonEmptyString(value, where) {
205
206
  return /** @type {string} */ (value);
206
207
  }
207
208
 
209
+ /**
210
+ * A declared switch, with the behaviour that applies when it is not declared.
211
+ *
212
+ * A truthy string is refused rather than coerced: `baseStyles: "false"` reads
213
+ * as off and would be on, which is the one wrong answer available here.
214
+ *
215
+ * @param {unknown} value - What was declared, or `undefined`.
216
+ * @param {boolean} fallback - The undeclared behaviour.
217
+ * @param {string} where - Dotted path, for the error.
218
+ * @returns {boolean} The setting.
219
+ */
220
+ function requireBoolean(value, fallback, where) {
221
+ if (value === undefined) return fallback;
222
+ if (typeof value !== "boolean") fail(where, "must be true or false");
223
+ return value;
224
+ }
225
+
208
226
  /**
209
227
  * One staging copy: a source path in the repository, and where it lands under
210
228
  * the staged package root.
@@ -537,6 +555,9 @@ function normalizeExceptions(value, field, where) {
537
555
  * @property {string} stageDir The staged package root, relative to
538
556
  * `rootDir`. Every asset `to:` lands under it.
539
557
  * @property {readonly Readonly<AssetSpec>[]} assets
558
+ * @property {boolean} baseStyles Whether the package takes the shared base
559
+ * stylesheet, staged into `stageDir` and
560
+ * named first in the manifest's `styles`.
540
561
  * @property {Readonly<Record<string, unknown>>} manifest The manifest
541
562
  * specification, emitted as declared.
542
563
  * @property {string|null} manifestFlags Module to load a `flags` function
@@ -877,6 +898,11 @@ export function resolvePackageBuildConfig(shared) {
877
898
  ARTIFACT_OF_KIND[/** @type {"systems"|"modules"} */ (shared.packageKind)]
878
899
  : requireNonEmptyString(releaseInput.artifact, "packageBuild.release.artifact"),
879
900
  assets: Object.freeze(assets),
901
+ // On by default, because the rules it carries are what compiled content
902
+ // needs to be legible and a package that declined them by omission
903
+ // would ship the content unstyled without anything saying so. A package
904
+ // that wants the surface entirely to itself says so.
905
+ baseStyles: requireBoolean(section.baseStyles, true, "packageBuild.baseStyles"),
880
906
  schema: normalizeSchema(section.schema),
881
907
  assetTransform:
882
908
  section.assetTransform === undefined ?