@heroiclands/package-build 22.25.0 → 22.27.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 (67) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +4 -0
  3. package/assets/styles/content-base.css +46 -3
  4. package/docs/api.md +35 -12
  5. package/docs/authoring/frontmatter.md +21 -0
  6. package/docs/authoring/links-and-markup.md +96 -149
  7. package/docs/content-format.md +6 -2
  8. package/docs/guides/government-migration.md +52 -0
  9. package/docs/guides/site.md +5 -5
  10. package/docs/reference/format-details.md +152 -246
  11. package/docs/reference/note-types.md +27 -28
  12. package/docs/reference/theme-pages.md +33 -29
  13. package/engine/anchors.mjs +85 -13
  14. package/engine/art-fields.mjs +1 -1
  15. package/engine/base-compiler.mjs +9 -1
  16. package/engine/bundles.mjs +1 -1
  17. package/engine/calendar-notes.mjs +1 -1
  18. package/engine/code-fences.mjs +6 -2
  19. package/engine/content-alerts.mjs +195 -0
  20. package/engine/content-blocks.mjs +119 -241
  21. package/engine/content-embeds.mjs +1 -1
  22. package/engine/content-figures.mjs +166 -230
  23. package/engine/content-format-check.mjs +3 -2
  24. package/engine/content-format.yaml +1 -5
  25. package/engine/content-images.mjs +3 -3
  26. package/engine/content-poetry.mjs +141 -0
  27. package/engine/content-spans.mjs +177 -0
  28. package/engine/foundry-entries.mjs +2 -0
  29. package/engine/frontmatter-lint.mjs +2 -1
  30. package/engine/heading-attributes.mjs +1 -1
  31. package/engine/helpers.mjs +8 -3
  32. package/engine/holdings.mjs +197 -333
  33. package/engine/index.mjs +1 -1
  34. package/engine/infobox.mjs +11 -1
  35. package/engine/journals.mjs +8 -6
  36. package/engine/markdown-expressions.mjs +9 -3
  37. package/engine/metadata-index.mjs +5 -3
  38. package/engine/note-vocabulary.mjs +30 -13
  39. package/engine/pdf-build.mjs +1 -1
  40. package/engine/pdf-render.mjs +90 -31
  41. package/engine/populations.mjs +17 -83
  42. package/engine/site-build.mjs +44 -16
  43. package/engine/site-preview.mjs +22 -6
  44. package/engine/wikilinks.mjs +18 -4
  45. package/hugo-theme/layouts/partials/holdings/entry.html +2 -2
  46. package/hugo-theme/layouts/partials/holdings.html +14 -14
  47. package/hugo-theme/static/css/style.css +40 -2
  48. package/package.json +1 -1
  49. package/sohl/item-fields.mjs +0 -16
  50. package/types/engine/anchors.d.mts +13 -4
  51. package/types/engine/art-fields.d.mts +1 -1
  52. package/types/engine/calendar-notes.d.mts +1 -1
  53. package/types/engine/code-fences.d.mts +3 -1
  54. package/types/engine/content-alerts.d.mts +62 -0
  55. package/types/engine/content-blocks.d.mts +3 -10
  56. package/types/engine/content-embeds.d.mts +1 -1
  57. package/types/engine/content-figures.d.mts +13 -20
  58. package/types/engine/content-images.d.mts +1 -1
  59. package/types/engine/content-poetry.d.mts +32 -0
  60. package/types/engine/content-spans.d.mts +18 -0
  61. package/types/engine/holdings.d.mts +75 -118
  62. package/types/engine/infobox.d.mts +1 -0
  63. package/types/engine/markdown-expressions.d.mts +1 -1
  64. package/types/engine/note-vocabulary.d.mts +14 -2
  65. package/types/engine/populations.d.mts +4 -13
  66. package/types/engine/site-build.d.mts +3 -3
  67. package/types/engine/wikilinks.d.mts +5 -1
@@ -115,7 +115,6 @@ These fields are accepted by every note type. A field's value can still be irrel
115
115
  | `data.governance.ranks` | list | The ladder of ranks the body confers — level, title, description. |
116
116
  | `data.governance.offices` | as authored | Named offices, each with a description and optional dated holders. |
117
117
  | `data.seat` | an Address | Where the affiliation's authority sits. |
118
- | `data.domains` | list of Addresses | Places over which it holds sway. |
119
118
  | `data.population` | number | How many people it counts. |
120
119
  | `data.economy` | list of Addresses | What its economic life runs on — currencies, banking bodies, goods. |
121
120
  | `data.lore` | list of Addresses | Lore concerning it — the peoples it draws on, the god a faith venerates, its law, its calendar. |
@@ -128,7 +127,6 @@ These fields are accepted by every note type. A field's value can still be irrel
128
127
  | ---------------- | ------ | ------------------ |
129
128
  | `subType` | sohl | `system.subType` |
130
129
  | `data.seat` | sohl | `system.seat` |
131
- | `data.domains` | sohl | `system.domain` |
132
130
  | `data.parents` | sohl | `system.parents` |
133
131
  | `data.relations` | sohl | `system.relations` |
134
132
 
@@ -463,32 +461,33 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
463
461
 
464
462
  **Subtypes:** `world`, `region`, `settlement`, `site`, `structure`, `feature`, `celestial`.
465
463
 
466
- | Field | Shape | Meaning |
467
- | -------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
468
- | `data.calendar` | an Address | Calendar used to display this place's dates. |
469
- | `data.demonym` | string | What a person from this place is called — a Vylarian. |
470
- | `data.purpose` | string | The reason this settlement, site, or structure exists, selected from its placeCharacter tags. |
471
- | `data.lore` | list of Addresses | Lore concerning this place — its peoples, its law, its calendar, its history. |
472
- | `data.parents` | list of Addresses | Enclosing places this one sits within. |
473
- | `data.population` | number | Approximate population, to two significant digits. |
474
- | `data.market` | number | What trade the settlement supports, on a scale of six: 1 hamlet, 2 village, 3 town, 4 market town, 5 city, 6 great city. |
475
- | `data.borders` | list of `{ to, bearing }` entries | Places sharing a frontier with this one — each the other's shortcode and where it lies from here. |
476
- | `data.routes` | list of `{ to, bearing, mode, days, terrain?, leagues? }` entries | Journeys from this place's centre — where the destination lies, how it is travelled, and about how many days it takes. |
477
- | `data.world.equatorialCircumferenceKm` | number | The distance round the body at its equator, in kilometres. |
478
- | `data.world.surfaceGravityG` | number | Surface gravity, as a multiple of Earth's. |
479
- | `data.world.axialTiltDegrees` | number | The tilt of the axis, in degrees — what makes the body have seasons. |
480
- | `data.year.days` | number | How many days the body's year holds. Every calendar's months sum to it. |
481
- | `data.year.hoursPerDay` | number | How many hours the day divides into. |
482
- | `data.year.minutesPerHour` | number | How many minutes the hour divides into. |
483
- | `data.year.secondsPerMinute` | number | How many seconds the minute divides into. |
484
- | `data.present` | a date | The day the setting stops and play begins — what an age is computed against. |
485
- | `data.body.diameterKm` | number | The body's diameter, in kilometres. |
486
- | `data.body.orbitalRadiusKm` | number | How far the body orbits from the one it circles, in kilometres. |
487
- | `data.body.orbit` | string | The orbit's shape — `circular` means the cycle never varies. |
488
- | `data.body.inclined` | boolean | Whether the orbit is inclined to the plane the world orbits in. |
489
- | `data.moon.cycle` | number | How many days the body takes to return to the same phase. |
490
- | `data.moon.newOn` | a day-precision date | A day the body was new, written in the reference calendar. |
491
- | `data.moon.eclipses` | string | How often the body eclipses or is eclipsed — `never`, `rare`, `occasional` or `frequent`. |
464
+ | Field | Shape | Meaning |
465
+ | -------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
466
+ | `data.calendar` | an Address | Calendar used to display this place's dates. |
467
+ | `data.demonym` | string | What a person from this place is called — a Vylarian. |
468
+ | `data.purpose` | string | The reason this settlement, site, or structure exists, selected from its placeCharacter tags. |
469
+ | `data.lore` | list of Addresses | Lore concerning this place — its peoples, its law, its calendar, its history. |
470
+ | `data.parents` | list of Addresses | Enclosing places this one sits within. |
471
+ | `data.population` | number | Approximate population, to two significant digits. |
472
+ | `data.government` | an Address or null | The governing affiliation Address; the default target type is affiliation. Explicit null means complete anarchy. A positive population with no government key produces an advisory. |
473
+ | `data.market` | number | What trade the settlement supports, on a scale of six: 1 hamlet, 2 village, 3 town, 4 market town, 5 city, 6 great city. |
474
+ | `data.borders` | list of `{ to, bearing }` entries | Places sharing a frontier with this one — each the other's shortcode and where it lies from here. |
475
+ | `data.routes` | list of `{ to, bearing, mode, days, terrain?, leagues? }` entries | Journeys from this place's centre — where the destination lies, how it is travelled, and about how many days it takes. |
476
+ | `data.world.equatorialCircumferenceKm` | number | The distance round the body at its equator, in kilometres. |
477
+ | `data.world.surfaceGravityG` | number | Surface gravity, as a multiple of Earth's. |
478
+ | `data.world.axialTiltDegrees` | number | The tilt of the axis, in degrees — what makes the body have seasons. |
479
+ | `data.year.days` | number | How many days the body's year holds. Every calendar's months sum to it. |
480
+ | `data.year.hoursPerDay` | number | How many hours the day divides into. |
481
+ | `data.year.minutesPerHour` | number | How many minutes the hour divides into. |
482
+ | `data.year.secondsPerMinute` | number | How many seconds the minute divides into. |
483
+ | `data.present` | a date | The day the setting stops and play begins — what an age is computed against. |
484
+ | `data.body.diameterKm` | number | The body's diameter, in kilometres. |
485
+ | `data.body.orbitalRadiusKm` | number | How far the body orbits from the one it circles, in kilometres. |
486
+ | `data.body.orbit` | string | The orbit's shape — `circular` means the cycle never varies. |
487
+ | `data.body.inclined` | boolean | Whether the orbit is inclined to the plane the world orbits in. |
488
+ | `data.moon.cycle` | number | How many days the body takes to return to the same phase. |
489
+ | `data.moon.newOn` | a day-precision date | A day the body was new, written in the reference calendar. |
490
+ | `data.moon.eclipses` | string | How often the body eclipses or is eclipsed — `never`, `rare`, `occasional` or `frequent`. |
492
491
 
493
492
  ### scenario
494
493
 
@@ -9,12 +9,12 @@ subType: reference
9
9
 
10
10
  `package-build site` writes a page's front matter; the shared Hugo theme reads
11
11
  it and draws the page around it. This reference describes that second half —
12
- the infobox rail, the hero band, the holdings and related cards, the place
12
+ the infobox rail, the hero band, the government and related cards, the place
13
13
  map, search, and the markup an image or a figure renders as — and names the
14
14
  classes a consumer styles against.
15
15
 
16
16
  **None of the sections below is an authoring instruction.** `infoboxes`,
17
- `related`, `contains`, `held_by`, `holdings`, `map`, `package`, `slug`, `url`
17
+ `related`, `contains`, `governed_by`, `governed_places`, `map`, `package`, `slug`, `url`
18
18
  and `title` cannot be written by a note at all: a note's top-level region is
19
19
  closed and does not admit them, so each arrives already resolved by the time
20
20
  the theme sees it. Read this page when a panel is missing or a class needs
@@ -25,6 +25,8 @@ describes the keys the build writes from it.
25
25
 
26
26
  ## The infobox
27
27
 
28
+ A place’s Government row links to its authored governing affiliation. Explicit `data.government: null` produces a text row saying “Complete anarchy”; an omitted government produces no row. The same distinction is retained in the page’s `data` metadata.
29
+
28
30
  The toolchain settles what each box holds and in what order — see
29
31
  [the infobox](format-details.md#the-infobox) for the field semantics, the
30
32
  three states a system box can say, and the four section layouts a box is
@@ -164,7 +166,7 @@ artwork where the considered answer was "none". See
164
166
  declines one; the spelling differs from the hand-authored page's, and the two
165
167
  are not interchangeable.
166
168
 
167
- ## The holdings card
169
+ ## The containment and government card
168
170
 
169
171
  `partials/holdings.html` renders a bordered card below the body and above the
170
172
  Related card, holding up to three small tables. The build derives this front
@@ -174,13 +176,13 @@ note and dropped if one carries them:
174
176
  ```yaml
175
177
  ---
176
178
  contains: [{ title: …, url: …, type: place, subType: … }] # places within this one
177
- held_by: [{ title: …, url: …, type: affiliation, subType: … }] # affiliations holding this place
178
- holdings: [{ title: …, url: …, type: place, subType: … }] # places this affiliation holds
179
+ governed_by: [{ title: …, url: …, type: affiliation, subType: … }] # the governing affiliation of this place
180
+ governed_places: [{ title: …, url: …, type: place, subType: … }] # places explicitly governed by this affiliation
179
181
  ---
180
182
  ```
181
183
 
182
- A place page carries `contains` and/or `held_by`; an affiliation page carries
183
- `holdings`. Each key is tested for independently — a page carrying none of the
184
+ A place page carries `contains` and/or `governed_by`; an affiliation page carries
185
+ `governed_places`. Each key is tested for independently — a page carrying none of the
184
186
  three renders no card, and a page carrying one renders one table.
185
187
 
186
188
  **`url` is absent for an entry with no page of its own** — a stub note with an
@@ -189,29 +191,29 @@ an entry's title renders as plain text; every other entry renders as a link.
189
191
  `partials/holdings/entry.html` carries the guard, the same one
190
192
  `partials/infobox/entry.html` already applies to its own `{text, url}` shape.
191
193
 
192
- **Within** (`contains`) and **Holdings** (`holdings`) list places, and each is
194
+ **Within** (`contains`) and **Governed places** (`governed_places`) list places, and each is
193
195
  broken into groups by `subType`, in the closed order region, settlement, site,
194
196
  structure, feature — a `subType` outside that order, or missing entirely,
195
- groups last, alphabetically by its rendered label. **Held by** (`held_by`)
197
+ groups last, alphabetically by its rendered label. **Governed by** (`governed_by`)
196
198
  lists affiliations, whose kinds have no fixed hierarchy to group by, so it
197
199
  stays one flat grid with the kind printed beside each name.
198
200
 
199
201
  Every grid is `repeat(auto-fill, minmax(14em, 1fr))`, the one the Related card
200
202
  uses, so a region with forty settlements stays scannable.
201
203
 
202
- | class | drawn as |
203
- | ----------------------- | -------------------------------------------------------------------- |
204
- | `.holdings` | the card |
205
- | `.holdings-table` | one of the three tables, label and body together |
206
- | `.holdings-table-label` | "Within" / "Held by" / "Holdings" |
207
- | `.holdings-table-grid` | the grid of entries (Held by; the innermost grid of Within/Holdings) |
208
- | `.holdings-kind-group` | one kind's block within Within or Holdings |
209
- | `.holdings-kind-label` | the kind label heading a group |
210
- | `.holdings-entry-name` | an entry's title, linked or plain |
211
- | `.holdings-entry-kind` | the kind printed beside a Held by entry |
204
+ | class | drawn as |
205
+ | ----------------------- | ------------------------------------------------------------------------------- |
206
+ | `.holdings` | the card |
207
+ | `.holdings-table` | one of the three tables, label and body together |
208
+ | `.holdings-table-label` | "Within" / "Governed by" / "Governed places" |
209
+ | `.holdings-table-grid` | the grid of entries (Governed by; the innermost grid of Within/Governed places) |
210
+ | `.holdings-kind-group` | one kind's block within Within or Governed places |
211
+ | `.holdings-kind-label` | the kind label heading a group |
212
+ | `.holdings-entry-name` | an entry's title, linked or plain |
213
+ | `.holdings-entry-kind` | the kind printed beside a Governed by entry |
212
214
 
213
- A page whose front matter declares none of `contains`, `held_by` or
214
- `holdings` renders no card at all.
215
+ A page whose front matter declares none of `contains`, `governed_by` or
216
+ `governed_places` renders no card at all.
215
217
 
216
218
  ## The place map
217
219
 
@@ -289,9 +291,8 @@ for how an author writes `size`, `float` and a role.
289
291
 
290
292
  ## A numbered figure
291
293
 
292
- A `:::figure` fence wraps a picture, a table, a listing, a map or a passage of
293
- prose in a numbered, referable block. The content build decides which from
294
- the fence's contents, counts each kind separately, and emits the block with
294
+ A leading caption wraps the next supported item in a referable block; `:@` also numbers it. The content build decides which from
295
+ the next item, counts each kind separately, and emits the block with
295
296
  its label beneath whatever it holds:
296
297
 
297
298
  ```html
@@ -301,15 +302,14 @@ its label beneath whatever it holds:
301
302
  </div>
302
303
  ```
303
304
 
304
- The kind is `code`, `table`, `figure`, `map`, `poem` or `prose`, labelled **Code**,
305
- **Table**, **Figure**, **Map**, **Poem** and **Prose**. Every kind is drawn alike — the
305
+ The kind is `code`, `table`, `figure`, `map`, `poem`, `prose` or `example`, labelled **Code**,
306
+ **Table**, **Figure**, **Map**, **Poem**, **Prose** and **Example**. Every kind is drawn alike — the
306
307
  block carries the spacing, its contents add none of their own at the edges,
307
308
  and the label reads as a caption rather than as another paragraph of the body
308
- — so a kind added to the vocabulary arrives styled. A fence carrying an `id`
309
+ — so a kind added to the vocabulary arrives styled. A caption carrying an `id`
309
310
  is a cross-reference target and takes the same scroll offset a heading does.
310
311
 
311
- `border` is the one class an author may write on a fence, and it draws the
312
- block as a boxed aside.
312
+ Caption classes are unrestricted. The built-in `border` class draws the block as a boxed aside. Unnumbered captions use the same wrapper and show caption text without a numbered label.
313
313
 
314
314
  | class | drawn as |
315
315
  | ------------------------ | ------------------------------------------ |
@@ -494,3 +494,7 @@ Each section below has its own landing page.
494
494
  ```
495
495
 
496
496
  A row is a linked title and, when the page carries one, its description.
497
+
498
+ ## Alerts
499
+
500
+ The content renderer emits an `aside` with `alert` and `alert-note`, `alert-tip`, `alert-important`, `alert-warning` classes. The `alert-title` header contains a Font Awesome icon and the type's label. Shared styles supply a colored left rule, colored header and ordinary readable body; alerts do not use `details` and never start collapsed. Authored identifiers, additional classes and named attributes belong to the entire aside. Secret blocks retain their separate expandable presentation on the website.
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  /**
15
- * The anchors a note declares on headings and figure fences.
15
+ * The anchors a note declares on headings, captions, divs, poems and spans.
16
16
  *
17
17
  * **A leaf, deliberately.** This is asked by the link checker, by the content
18
18
  * index, and by the builds that emit a link, and they cannot all import one
@@ -29,15 +29,20 @@
29
29
  * @module
30
30
  */
31
31
 
32
+ import { scanAlerts } from "./content-alerts.mjs";
33
+ import { scanBlocks } from "./content-blocks.mjs";
34
+ import { scanPoetry } from "./content-poetry.mjs";
35
+ import { scanSpans } from "./content-spans.mjs";
36
+ import { codeRegions } from "./code-fences.mjs";
37
+ import { slugify } from "./content-slug.mjs";
32
38
  import { scanFigures } from "./content-figures.mjs";
33
39
  import { HEADING_LINE, splitHeadingAttributes } from "./heading-attributes.mjs";
34
40
 
35
41
  /**
36
- * The `{#slug}` and `:::figure {#slug}` anchors a note declares.
42
+ * The identifiers a note declares on headings, captions, divs, poems and spans.
37
43
  *
38
- * A bare `#` heading starts a journal page without declaring a slug, and so
39
- * does a figure fence that declares no id. A fence that declares one declares a
40
- * slug and starts an addressable journal page.
44
+ * Headings and captions may start journal pages. Div, poem and inline-span
45
+ * anchors address locations within their containing pages.
41
46
  *
42
47
  * @param {string} body - The note's markdown body, frontmatter already removed.
43
48
  * @param {number} [bodyLine] - The 1-based file line the body starts on, from
@@ -52,17 +57,15 @@ import { HEADING_LINE, splitHeadingAttributes } from "./heading-attributes.mjs";
52
57
  */
53
58
  export function collectAnchors(body, bodyLine = 1, resolveRole) {
54
59
  const anchors = [];
55
- let inCodeBlock = false;
60
+ const literal = codeRegions(body, { spans: false, poetryAsProse: false });
61
+ let offset = 0;
56
62
  const lines = String(body ?? "").split("\n");
57
63
 
58
64
  for (let i = 0; i < lines.length; i++) {
59
- // A fenced block's contents are not headings, and `#` is a comment in
60
- // most of what gets fenced.
61
- if (lines[i].trim().startsWith("```")) {
62
- inCodeBlock = !inCodeBlock;
65
+ const lineOffset = offset;
66
+ offset += lines[i].length + 1;
67
+ if (literal.some((region) => lineOffset >= region.start && lineOffset < region.end))
63
68
  continue;
64
- }
65
- if (inCodeBlock) continue;
66
69
 
67
70
  const heading = HEADING_LINE.exec(lines[i]);
68
71
  if (!heading) continue;
@@ -79,10 +82,79 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
79
82
  if (!figure.id) continue;
80
83
  anchors.push({
81
84
  slug: figure.id,
82
- name: figure.label,
85
+ name: figure.label || figure.caption,
83
86
  level: 1,
84
87
  line: bodyLine + figure.line - 1,
85
88
  });
86
89
  }
90
+ const blockAnchors = (source, firstLine) => {
91
+ const sourceLines = source.split("\n");
92
+ for (const block of scanBlocks(source).blocks) {
93
+ if (block.id)
94
+ anchors.push({
95
+ slug: slugify(block.id),
96
+ name: block.title || block.id,
97
+ level: 0,
98
+ line: firstLine + block.start,
99
+ });
100
+ blockAnchors(
101
+ sourceLines.slice(block.start + 1, block.end).join("\n"),
102
+ firstLine + block.start + 1,
103
+ );
104
+ }
105
+ };
106
+ blockAnchors(String(body ?? ""), bodyLine);
107
+ for (const alert of scanAlerts(body).blocks) {
108
+ if (alert.id)
109
+ anchors.push({
110
+ slug: slugify(alert.id),
111
+ name: alert.title,
112
+ level: 0,
113
+ line: bodyLine + alert.start,
114
+ });
115
+ }
116
+ for (const poem of scanPoetry(body).blocks) {
117
+ if (poem.id)
118
+ anchors.push({
119
+ slug: slugify(poem.id),
120
+ name: poem.id,
121
+ level: 0,
122
+ line: bodyLine + poem.start,
123
+ });
124
+ }
125
+ for (const span of scanSpans(body).spans) {
126
+ if (span.id)
127
+ anchors.push({
128
+ slug: slugify(span.id),
129
+ name: span.text,
130
+ level: 0,
131
+ line: bodyLine + span.line - 1,
132
+ });
133
+ }
87
134
  return anchors.sort((a, b) => a.line - b.line);
88
135
  }
136
+
137
+ /** Collisions involving a div, verse or inline-span anchor.
138
+ * Heading and caption collisions are diagnosed by their existing scanners.
139
+ * @param {string} body - Authored Markdown.
140
+ * @returns {Array<{line: number, column: number, message: string}>} Located collisions.
141
+ */
142
+ export function markupAnchorFindings(body) {
143
+ const groups = new Map();
144
+ for (const anchor of collectAnchors(body)) {
145
+ const slug = slugify(anchor.slug);
146
+ if (!groups.has(slug)) groups.set(slug, []);
147
+ groups.get(slug).push(anchor);
148
+ }
149
+ const errors = [];
150
+ for (const [slug, anchors] of groups) {
151
+ if (anchors.length < 2 || !anchors.some((anchor) => anchor.level === 0)) continue;
152
+ for (const anchor of anchors.slice(1))
153
+ errors.push({
154
+ line: anchor.line,
155
+ column: 1,
156
+ message: `duplicate markup anchor "${slug}"`,
157
+ });
158
+ }
159
+ return errors;
160
+ }
@@ -247,7 +247,7 @@ export function resolveArtRecord(index, value, defaultType, accepts) {
247
247
  * carries once an embed's address has been rewritten into the ordinary image
248
248
  * every surface renders ({@link module:engine/content-embeds.resolveEmbeds}).
249
249
  *
250
- * This is the lookup a `:::figure` fence's `map` counter reaches through —
250
+ * This is the lookup a captioned item's `map` counter reaches through —
251
251
  * see {@link module:engine/content-figures.scanFigures}'s `resolveRole` —
252
252
  * for a caller that reads a body after that rewrite: a Foundry journal, an
253
253
  * item or actor's documentation, and the book.
@@ -61,7 +61,10 @@
61
61
  * @module
62
62
  */
63
63
 
64
+ import { scanAlerts } from "./content-alerts.mjs";
64
65
  import { authoredFrontmatter } from "./index-records.mjs";
66
+ import { markupAnchorFindings } from "./anchors.mjs";
67
+ import { scanSpans } from "./content-spans.mjs";
65
68
  import { scanBlocks } from "./content-blocks.mjs";
66
69
  import { scanHeadingAttributes, withheldSections } from "./heading-attributes.mjs";
67
70
  import { scanFigures } from "./content-figures.mjs";
@@ -682,7 +685,12 @@ export class BasePackCompiler {
682
685
  // it made — `errors` or `findings`, never just the first — so nothing
683
686
  // here re-scans; it only stops taking `[0]`.
684
687
  const findings = [];
685
- for (const secretError of scanBlocks(body).errors) {
688
+ for (const secretError of [
689
+ ...scanBlocks(body).errors,
690
+ ...scanAlerts(body).errors,
691
+ ...scanSpans(body).errors,
692
+ ...markupAnchorFindings(body),
693
+ ]) {
686
694
  findings.push({
687
695
  message: secretError.message,
688
696
  line: (this.currentNote?.bodyLine ?? 1) + secretError.line - 1,
@@ -314,7 +314,7 @@ export class Bundles extends BasePackCompiler {
314
314
 
315
315
  // The markdown this pass reads has already had its embeds rewritten
316
316
  // into ordinary images, each `src` the pathname `pathnameRoles` keys
317
- // its roles by — so a `:::figure` of a map names itself `Map 1` here
317
+ // its roles by — so a captioned map names itself `Map 1` here
318
318
  // exactly as the website and the book name the same picture.
319
319
  const roles = pathnameRoles(this.linkIndex);
320
320
 
@@ -511,7 +511,7 @@ function checkCalendarDayNames(note, daysPerYear) {
511
511
  /**
512
512
  * Check a `place` note's world facts.
513
513
  *
514
- * Declared on `place` beside its tenure check, so the lint runs both with the
514
+ * Declared on `place` beside its government advisory, so the lint runs both with the
515
515
  * same index. Four rules, and each is a fact about the setting rather than
516
516
  * about the file:
517
517
  *
@@ -104,10 +104,11 @@ function column(space) {
104
104
  * @param {object} [options]
105
105
  * @param {boolean} [options.spans=true] - Include inline code spans. Set false
106
106
  * to consider only block-level code.
107
+ * @param {boolean} [options.poetryAsProse=true] - Treat verse bodies as inline prose.
107
108
  * @returns {Array<{start: number, end: number}>} Non-overlapping regions, in
108
109
  * source order. `end` is exclusive.
109
110
  */
110
- export function codeRegions(markdown, { spans = true } = {}) {
111
+ export function codeRegions(markdown, { spans = true, poetryAsProse = true } = {}) {
111
112
  const src = String(markdown ?? "");
112
113
  if (!src) return [];
113
114
  const lines = src.split("\n");
@@ -144,7 +145,10 @@ export function codeRegions(markdown, { spans = true } = {}) {
144
145
  let close = i + 1;
145
146
  while (close < lines.length && !closer.test(lines[close])) close++;
146
147
  // An unclosed fence runs to the end of the document.
147
- regions.push(through(i, Math.min(close, lines.length - 1)));
148
+ if (poetryAsProse && /^poetry(?:\s|$)/.test(fence[3])) {
149
+ regions.push(through(i, i));
150
+ if (close < lines.length) regions.push(through(close, close));
151
+ } else regions.push(through(i, Math.min(close, lines.length - 1)));
148
152
  i = close;
149
153
  afterBlank = false;
150
154
  continue;