@heroiclands/package-build 22.26.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 (64) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +3 -1
  3. package/assets/styles/content-base.css +46 -3
  4. package/docs/api.md +33 -13
  5. package/docs/authoring/frontmatter.md +2 -2
  6. package/docs/authoring/links-and-markup.md +96 -149
  7. package/docs/content-format.md +5 -3
  8. package/docs/guides/government-migration.md +52 -0
  9. package/docs/guides/site.md +5 -5
  10. package/docs/reference/format-details.md +110 -219
  11. package/docs/reference/note-types.md +27 -29
  12. package/docs/reference/theme-pages.md +31 -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/code-fences.mjs +6 -2
  18. package/engine/content-alerts.mjs +195 -0
  19. package/engine/content-blocks.mjs +119 -241
  20. package/engine/content-embeds.mjs +1 -1
  21. package/engine/content-figures.mjs +166 -230
  22. package/engine/content-format-check.mjs +3 -2
  23. package/engine/content-format.yaml +0 -5
  24. package/engine/content-images.mjs +3 -3
  25. package/engine/content-poetry.mjs +141 -0
  26. package/engine/content-spans.mjs +177 -0
  27. package/engine/foundry-entries.mjs +2 -0
  28. package/engine/frontmatter-lint.mjs +2 -1
  29. package/engine/heading-attributes.mjs +1 -1
  30. package/engine/helpers.mjs +8 -3
  31. package/engine/holdings.mjs +165 -273
  32. package/engine/index.mjs +1 -1
  33. package/engine/infobox.mjs +1 -1
  34. package/engine/journals.mjs +8 -6
  35. package/engine/markdown-expressions.mjs +9 -3
  36. package/engine/metadata-index.mjs +2 -3
  37. package/engine/note-vocabulary.mjs +16 -11
  38. package/engine/pdf-build.mjs +1 -1
  39. package/engine/pdf-render.mjs +90 -31
  40. package/engine/populations.mjs +17 -83
  41. package/engine/site-build.mjs +44 -16
  42. package/engine/site-preview.mjs +22 -6
  43. package/engine/wikilinks.mjs +18 -4
  44. package/hugo-theme/layouts/partials/holdings/entry.html +2 -2
  45. package/hugo-theme/layouts/partials/holdings.html +14 -14
  46. package/hugo-theme/static/css/style.css +40 -2
  47. package/package.json +1 -1
  48. package/sohl/item-fields.mjs +0 -16
  49. package/types/engine/anchors.d.mts +13 -4
  50. package/types/engine/art-fields.d.mts +1 -1
  51. package/types/engine/code-fences.d.mts +3 -1
  52. package/types/engine/content-alerts.d.mts +62 -0
  53. package/types/engine/content-blocks.d.mts +3 -10
  54. package/types/engine/content-embeds.d.mts +1 -1
  55. package/types/engine/content-figures.d.mts +13 -20
  56. package/types/engine/content-images.d.mts +1 -1
  57. package/types/engine/content-poetry.d.mts +32 -0
  58. package/types/engine/content-spans.d.mts +18 -0
  59. package/types/engine/holdings.d.mts +59 -105
  60. package/types/engine/markdown-expressions.d.mts +1 -1
  61. package/types/engine/note-vocabulary.d.mts +10 -2
  62. package/types/engine/populations.d.mts +4 -13
  63. package/types/engine/site-build.d.mts +3 -3
  64. package/types/engine/wikilinks.d.mts +5 -1
@@ -1,21 +1,18 @@
1
1
  /* SPDX-License-Identifier: GPL-3.0-or-later */
2
2
 
3
3
  /**
4
- * Named body blocks — `:::secret`, `:::info` and `:::warn` — as one construct.
4
+ * General divs and the `:::secret` disclosure fence.
5
5
  *
6
6
  * ```
7
- * :::info {#route title="The road north" .wide}
7
+ * :::secret {#route title="For the GM" .wide}
8
8
  * The ford floods in Azlet.
9
9
  * :::
10
10
  * ```
11
11
  *
12
- * The block's name becomes its first class, the author's classes follow it, and
13
- * every other attribute is written onto the element. `title` is reserved: it
14
- * supplies the heading, and a block that states none takes the capitalised name
15
- * — `Secret`, `Info`, `Warn`.
16
- *
17
- * The three names differ in the class they carry and in nothing else, so a
18
- * stylesheet owns their appearance. None is emitted with an inline `style`.
12
+ * The secret fence carries the `secret` class and a heading, defaulting to
13
+ * `Secret`. General divs carry only authored classes and ordinary attributes.
14
+ * The author supplies secret headings through the reserved `title` attribute.
15
+ * Neither construct adds inline styles.
19
16
  *
20
17
  * Each block stands on its own: a malformed one is a finding at its own line
21
18
  * and the blocks around it still render.
@@ -25,11 +22,7 @@
25
22
  * page in two and publish the rest with no wrapper around it at all.
26
23
  *
27
24
 
28
- * **One pass reads all three names.** Reading them in two passes made the first
29
- * one meet the second's closers with nothing open, and report a block the author
30
- * had not written. A construct this pass does not own — `:::figure` — is
31
- * counted rather than claimed, so its closer is attributed to it and the pass
32
- * that owns it still finds it in the markdown this one passes through.
25
+ * **One pass reads named blocks and general divs.**
33
26
  *
34
27
  * **A block's body is left as Markdown, not pre-rendered.** The wrapper is
35
28
  * written with a blank line after the opening tag and before the closing one,
@@ -46,15 +39,12 @@
46
39
  */
47
40
 
48
41
  import crypto from "node:crypto";
42
+ import { slugify } from "./content-slug.mjs";
49
43
  import MarkdownIt from "markdown-it";
50
- import footnotePlugin from "markdown-it-footnote";
51
- import deflistPlugin from "markdown-it-deflist";
52
44
  import { parseExtensionAttributes, refusedAttributes } from "./extension-attributes.mjs";
53
- import { poetryMarkdown, verseLines } from "./content-poetry.mjs";
45
+ import { scanPoetry, renderPoetry } from "./content-poetry.mjs";
54
46
  import { WITHHELD_CLASS, parseHeadingLine, withheldSections } from "./heading-attributes.mjs";
55
47
 
56
- const parser = new MarkdownIt({ html: true }).use(footnotePlugin).use(deflistPlugin);
57
-
58
48
  /**
59
49
  * Titles carry emphasis and nothing else. `html: false` escapes any tag an
60
50
  * author writes there, because a title is written into an element's first child
@@ -62,46 +52,22 @@ const parser = new MarkdownIt({ html: true }).use(footnotePlugin).use(deflistPlu
62
52
  */
63
53
  const titleParser = new MarkdownIt({ html: false });
64
54
 
65
- const OPEN = /^:::([A-Za-z][A-Za-z0-9-]*)(?:[ \t]+(.*?))?[ \t]*$/;
66
- const CLOSE = /^:::[ \t]*$/;
67
- const INDENTED_POETRY_OPEN = /^ +:::poetry(?:[ \t]+(.*?))?[ \t]*$/;
68
- const INDENTED_CLOSE = /^ *:::[ \t]*$/;
69
-
70
55
  /** The blocks an author may open, with the heading each takes by default. */
71
56
  export const BLOCK_NAMES = Object.freeze({
72
- info: "Info",
73
57
  secret: "Secret",
74
- warn: "Warn",
75
58
  });
76
59
 
77
60
  /**
78
- * Blocks that may hold another named block.
79
- *
80
- * A box inside a GM-only section is a real thing to write — the section is a
81
- * container for whatever the GM reads, boxes included — so a `secret` holds one
82
- * and the box stays inside it. A box inside a box is noise, so `info` and `warn`
83
- * hold no named block and a second opener inside one is a finding.
61
+ * Container fences whose bodies are scanned and rendered recursively.
84
62
  *
85
- * Exported because a renderer has to know it: the body of a container is read
86
- * for blocks of its own, and the body of a box is not.
63
+ * A secret may hold general divs; a general div may hold another div or a
64
+ * secret. A secret nested directly within another secret is refused.
87
65
  *
88
66
  * @type {readonly string[]}
89
67
  */
90
- export const BLOCK_CONTAINERS = Object.freeze(["secret"]);
91
-
92
- /**
93
- * Constructs another pass owns. A `:::figure` is read by the figure pass, so
94
- * this one **counts it and does not claim it**: the opener and its closer pass
95
- * through untouched, and the count is what lets a closer be attributed to the
96
- * innermost block actually open. Claiming one would report a block that does not
97
- * exist and take the figure out of the note.
98
- */
99
- const FOREIGN = Object.freeze(["figure"]);
100
-
101
- /** `title` is the heading. Everything else an author writes becomes an attribute. */
102
- const TITLE = "title";
68
+ export const BLOCK_CONTAINERS = Object.freeze(["secret", "div"]);
103
69
 
104
- const names = () => [...Object.keys(BLOCK_NAMES), "poetry"].join(", ");
70
+ const names = () => [...Object.keys(BLOCK_NAMES), "div"].join(", ");
105
71
 
106
72
  /** Text safe inside a double-quoted attribute. */
107
73
  function escapeAttribute(value) {
@@ -122,197 +88,126 @@ function escapeAttribute(value) {
122
88
  * @returns {{blocks: Array<{name: string, title: string, id: string, classes: string[], attributes: Record<string, string>, start: number, end: number, body: string}>, errors: Array<{line: number, column: number, message: string}>}}
123
89
  */
124
90
  export function scanBlocks(source) {
125
- const lines = String(source ?? "").split("\n");
126
- const blocks = [];
127
- const errors = [];
128
- let opening = null;
129
- let codeFence = null;
130
- // Blocks open inside this pass's own block, and blocks of another
131
- // construct open outside it, counted so a `:::` closes the innermost thing
132
- // rather than whatever this pass happens to have open.
133
- let innerOpen = 0;
134
- let foreignOutside = 0;
135
-
91
+ const lines = String(source ?? "").split("\n"),
92
+ blocks = [],
93
+ errors = [];
94
+ const stack = [];
95
+ let fence = null;
136
96
  for (let i = 0; i < lines.length; i++) {
137
97
  const line = lines[i];
138
- const fence = opening?.name === "poetry" ? null : /^ {0,3}(`{3,}|~{3,})/.exec(line);
139
- if (codeFence) {
140
- if (fence && fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length) {
141
- codeFence = null;
142
- }
98
+ const code = /^ *(`{3,}|~{3,})(.*)$/.exec(line);
99
+ if (fence) {
100
+ if (
101
+ code &&
102
+ code[1][0] === fence[0] &&
103
+ code[1].length >= fence.length &&
104
+ !code[2].trim()
105
+ )
106
+ fence = null;
143
107
  continue;
144
108
  }
145
- if (fence) {
146
- codeFence = fence[1];
109
+ if (code) {
110
+ fence = code[1];
147
111
  continue;
148
112
  }
149
-
150
- if (CLOSE.test(line) || (opening?.name === "poetry" && INDENTED_CLOSE.test(line))) {
151
- // A closer belongs to the innermost block still open, whichever
152
- // pass owns it.
153
- if (opening && innerOpen > 0) {
154
- innerOpen -= 1;
155
- continue;
156
- }
157
- if (!opening) {
158
- if (foreignOutside > 0) {
159
- foreignOutside -= 1;
160
- continue;
161
- }
162
- errors.push({ line: i + 1, column: 1, message: "a ::: line closes no block" });
163
- continue;
164
- }
165
- if (opening.rejected) {
166
- opening = null;
167
- continue;
168
- }
169
- const bodyLines = lines.slice(opening.start + 1, i);
170
- const rawBody = bodyLines.join("\n");
171
- const body = opening.name === "poetry" ? rawBody : rawBody.trim();
172
- if (opening.name === "poetry") {
173
- for (let offset = 0; offset < bodyLines.length; offset++) {
174
- const column = bodyLines[offset].indexOf("\t");
175
- if (column >= 0)
176
- errors.push({
177
- line: opening.start + offset + 2,
178
- column: column + 1,
179
- message: "tabs are not allowed in poetry",
180
- });
181
- }
182
- }
183
- if (!body.trim()) {
113
+ const close = /^ *:::\s*$/.test(line);
114
+ const named = /^ *:::([A-Za-z][A-Za-z0-9-]*)(?:\s+(.*))?$/.exec(line);
115
+ const div = /^ *:::\s+(\{.*)$/.exec(line);
116
+ if (close && stack.length) {
117
+ const block = stack.pop();
118
+ if (block.rejected) continue;
119
+ block.end = i;
120
+ block.body = lines
121
+ .slice(block.start + 1, i)
122
+ .join("\n")
123
+ .trim();
124
+ if (!block.body)
184
125
  errors.push({
185
- line: opening.start + 1,
126
+ line: block.start + 1,
186
127
  column: 1,
187
- message: `${opening.name} block is empty`,
128
+ message: `${block.name} block is empty`,
188
129
  });
189
- opening = null;
190
- continue;
191
- }
192
- if (opening.name === "poetry" && opening.attributes.syllables) {
193
- const expected = opening.attributes.syllables.split(",").length;
194
- const actual = verseLines(body).length;
195
- if (expected !== actual)
196
- errors.push({
197
- line: opening.start + 1,
198
- column: 1,
199
- message: `poetry syllables= gives ${expected} counts for ${actual} verse lines`,
200
- });
201
- }
202
- // An H1, or an anchored heading at any level, starts a Foundry
203
- // journal page — splitting the block's own page in two and
204
- // publishing the rest with no wrapper around it, a GM-only
205
- // section included. Refused rather than split: a lower heading
206
- // with no anchor is still the ordinary way to structure a box.
207
- // Poetry treats those same characters as verse text.
208
- for (let at = opening.start + 1; opening.name !== "poetry" && at < i; at++) {
209
- if (!parseHeadingLine(lines[at])?.startsPage) continue;
210
- const article = /^[aeiou]/i.test(opening.name) ? "an" : "a";
211
- errors.push({
212
- line: at + 1,
213
- column: 1,
214
- message:
215
- `a heading that starts a page cannot be written inside ${article} ` +
216
- `${opening.name} block — keep an H1 or an anchored heading ` +
217
- "at the top level, or drop the anchor and the level to stay inside it",
218
- });
219
- }
220
- blocks.push({ ...opening, end: i, body });
221
- opening = null;
130
+ if (!stack.length) blocks.push(block);
222
131
  continue;
223
132
  }
224
-
225
- const indentedPoetry = INDENTED_POETRY_OPEN.exec(line);
226
- const open = OPEN.exec(line) ?? (indentedPoetry && [line, "poetry", indentedPoetry[1]]);
227
- if (!open) continue;
228
- const [, name, raw = ""] = open;
133
+ if (!close && !named && !div) continue;
134
+ const name = named?.[1] ?? "div",
135
+ raw = named?.[2] ?? div?.[1] ?? "";
229
136
  const at = { line: i + 1, column: 1 };
230
- if (FOREIGN.includes(name)) {
231
- if (opening) innerOpen += 1;
232
- else foreignOutside += 1;
233
- continue;
234
- }
235
- if (!Object.hasOwn(BLOCK_NAMES, name) && name !== "poetry") {
137
+ if (name !== "div" && !Object.hasOwn(BLOCK_NAMES, name)) {
236
138
  errors.push({ ...at, message: `there is no ${name} block; the blocks are ${names()}` });
237
- opening = { start: i, rejected: true };
139
+ stack.push({ start: i, rejected: true });
238
140
  continue;
239
141
  }
240
- if (opening) {
241
- if (BLOCK_CONTAINERS.includes(opening.name) && !BLOCK_CONTAINERS.includes(name)) {
242
- // Held by the block it is written in, and counted so the
243
- // closers are attributed in the order they were opened. The
244
- // body is rendered for its own blocks before it is rendered as
245
- // markdown, so the inner block comes out as a block.
246
- innerOpen += 1;
247
- continue;
248
- }
249
- // Not counted, unlike a block the outer one holds: the nesting is
250
- // already reported, and counting it would leave the outer block
251
- // looking unclosed as well — two findings for one mistake.
142
+ if (stack.length && name !== "div" && !["secret", "div"].includes(stack.at(-1).name)) {
252
143
  errors.push({ ...at, message: `nested ${name} blocks are not supported` });
144
+ stack.push({ start: i, rejected: true });
253
145
  continue;
254
146
  }
255
-
256
- let id = "";
257
- let classes = [];
258
- const attributes = {};
259
- let title = BLOCK_NAMES[name];
260
- const text = raw.trim();
261
- if (text) {
262
- if (!/^\{[^}\n]*\}$/.test(text)) {
263
- errors.push({ ...at, message: "block attributes need {…} braces" });
264
- opening = { start: i, rejected: true };
265
- continue;
266
- }
267
- const parsed = parseExtensionAttributes(text.slice(1, -1));
268
- if (parsed.problems.length) {
269
- for (const message of parsed.problems) errors.push({ ...at, message });
270
- opening = { start: i, rejected: true };
271
- continue;
272
- }
273
- id = parsed.id;
274
- classes = parsed.classes;
275
- const refused = refusedAttributes(parsed.values);
276
- for (const message of refused) errors.push({ ...at, message });
277
- if (refused.length) {
278
- opening = { start: i, rejected: true };
279
- continue;
280
- }
281
- for (const [key, value] of Object.entries(parsed.values)) {
282
- if (name === "poetry") {
283
- if (!["form", "meter", "rhyme", "syllables", "lang"].includes(key))
284
- errors.push({ ...at, message: `${key}= is not a poetry attribute` });
285
- else if (key === "syllables" && !/^[1-9]\d*(?:,[1-9]\d*)*$/.test(value))
286
- errors.push({
287
- ...at,
288
- message: "poetry syllables= needs positive comma-separated counts",
289
- });
290
- else if (key === "lang" && !/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/.test(value))
291
- errors.push({ ...at, message: "poetry lang= needs a language tag" });
292
- }
293
- if (key === TITLE) title = value;
294
- else attributes[key] = value;
295
- }
147
+ if (stack.length && name === "secret" && stack.at(-1).name === "secret") {
148
+ errors.push({ ...at, message: "nested secret blocks are not supported" });
149
+ stack.push({ start: i, rejected: true });
150
+ continue;
296
151
  }
297
- opening = {
152
+ let parsed = { id: "", classes: [], values: {}, problems: [] };
153
+ if (raw.trim()) {
154
+ if (!/^\{[^}\n]*\}$/.test(raw.trim()))
155
+ parsed.problems.push("block attributes need {…} braces");
156
+ else parsed = parseExtensionAttributes(raw.trim().slice(1, -1));
157
+ }
158
+ for (const message of [...parsed.problems, ...refusedAttributes(parsed.values)])
159
+ errors.push({ ...at, message });
160
+ const attributes = { ...parsed.values };
161
+ let title = BLOCK_NAMES[name] ?? "";
162
+ if (name !== "div" && Object.hasOwn(attributes, "title")) {
163
+ title = attributes.title;
164
+ delete attributes.title;
165
+ }
166
+ stack.push({
298
167
  start: i,
299
168
  name,
300
169
  title,
301
- id,
302
- classes,
170
+ id: parsed.id,
171
+ classes: parsed.classes,
303
172
  attributes,
304
- indent: name === "poetry" ? /^ */.exec(line)[0] : "",
305
- };
306
- }
307
-
308
- if (opening && !opening.rejected) {
309
- errors.push({
310
- line: opening.start + 1,
311
- column: 1,
312
- message: `${opening.name} block needs a closing ::: line`,
173
+ rejected: parsed.problems.length > 0 || refusedAttributes(parsed.values).length > 0,
174
+ indent: /^ */.exec(line)[0],
313
175
  });
314
176
  }
315
- return { blocks, errors };
177
+ for (const block of stack)
178
+ if (!block.rejected)
179
+ errors.push({
180
+ line: block.start + 1,
181
+ column: 1,
182
+ message: `${block.name} block needs a closing ::: line`,
183
+ });
184
+ for (const block of blocks) {
185
+ let literal = null;
186
+ for (let i = block.start + 1; i < block.end; i++) {
187
+ const marker = /^ *(`{3,}|~{3,})(.*)$/.exec(lines[i]);
188
+ if (literal) {
189
+ if (
190
+ marker &&
191
+ marker[1][0] === literal[0] &&
192
+ marker[1].length >= literal.length &&
193
+ !marker[2].trim()
194
+ )
195
+ literal = null;
196
+ continue;
197
+ }
198
+ if (marker) {
199
+ literal = marker[1];
200
+ continue;
201
+ }
202
+ if (parseHeadingLine(lines[i])?.startsPage)
203
+ errors.push({
204
+ line: i + 1,
205
+ column: 1,
206
+ message: `a heading that starts a page cannot be written inside a ${block.name} block — keep an H1 or an anchored heading at the top level, or drop the anchor and the level to stay inside it`,
207
+ });
208
+ }
209
+ }
210
+ return { blocks, errors: [...errors, ...scanPoetry(source).errors] };
316
211
  }
317
212
 
318
213
  /**
@@ -323,7 +218,7 @@ export function scanBlocks(source) {
323
218
  * remembered.
324
219
  */
325
220
  function elementId(block, target) {
326
- if (block.id) return block.id;
221
+ if (block.id) return slugify(block.id);
327
222
  if (target !== "foundry" || block.name !== "secret") return "";
328
223
  const digest = crypto
329
224
  .createHash("sha256")
@@ -339,26 +234,12 @@ function attributeText(block, target, classes) {
339
234
  const parts = [`class="${escapeAttribute(classes.join(" "))}"`];
340
235
  if (id) parts.push(`id="${escapeAttribute(id)}"`);
341
236
  for (const [key, value] of Object.entries(block.attributes)) {
342
- const attribute = block.name === "poetry" && key !== "lang" ? `data-${key}` : key;
237
+ const attribute = key;
343
238
  parts.push(`${attribute}="${escapeAttribute(value)}"`);
344
239
  }
345
240
  return parts.join(" ");
346
241
  }
347
242
 
348
- /** Keep a poem in its list, without turning standalone readable indentation into code. */
349
- function poetryWrapperIndent(lines, block) {
350
- const indent = block.indent;
351
- if (indent.length < 4) return indent;
352
- for (let at = block.start - 1; at >= 0; at--) {
353
- const line = lines[at];
354
- if (!line.trim()) continue;
355
- const leading = /^ */.exec(line)[0].length;
356
- if (/^ *(?:[-+*]|\d+[.)]) +/.test(line) && leading < indent.length) return indent;
357
- if (leading < indent.length) break;
358
- }
359
- return "";
360
- }
361
-
362
243
  /**
363
244
  * Render named blocks for one publishing surface.
364
245
  *
@@ -377,14 +258,16 @@ function poetryWrapperIndent(lines, block) {
377
258
  * @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>}}
378
259
  */
379
260
  export function renderBlocks(source, target) {
380
- const { blocks, errors } = scanBlocks(source);
381
- if (!blocks.length) return { markdown: String(source ?? ""), errors };
261
+ const { errors } = scanBlocks(source);
262
+ source = renderPoetry(source).markdown;
263
+ const renderedBlocks = scanBlocks(source).blocks;
264
+ if (!renderedBlocks.length) return { markdown: String(source ?? ""), errors };
382
265
  const lines = String(source ?? "").split("\n");
383
266
  const output = [];
384
267
  let cursor = 0;
385
- for (const block of blocks) {
268
+ for (const block of renderedBlocks) {
386
269
  output.push(...lines.slice(cursor, block.start));
387
- const classes = [block.name, ...block.classes];
270
+ const classes = [...(block.name === "div" ? [] : [block.name]), ...block.classes];
388
271
  const attributes = attributeText(block, target, classes);
389
272
  // A container's body may hold a block of its own, rendered first so
390
273
  // its own wrapper is already written when the outer one wraps it in
@@ -394,13 +277,8 @@ export function renderBlocks(source, target) {
394
277
  BLOCK_CONTAINERS.includes(block.name) ?
395
278
  renderBlocks(block.body, target).markdown
396
279
  : block.body;
397
- if (block.name === "poetry") {
398
- const indent = poetryWrapperIndent(lines, block);
399
- const verses = poetryMarkdown(held)
400
- .split("\n")
401
- .map((line) => (line ? `${indent}${line}` : line))
402
- .join("\n");
403
- output.push("", `${indent}<div ${attributes}>`, "", verses, "", `${indent}</div>`, "");
280
+ if (block.name === "div") {
281
+ output.push("", `<div ${attributes}>`, "", held, "", "</div>", "");
404
282
  cursor = block.end + 1;
405
283
  continue;
406
284
  }
@@ -177,7 +177,7 @@ export function resolveEmbed(index, embed) {
177
177
  * The role an address's asset declares, read through the same resolution an
178
178
  * embed's address takes.
179
179
  *
180
- * This is the lookup a `:::figure` fence's `map` counter reaches through —
180
+ * This is the lookup a captioned item's `map` counter reaches through —
181
181
  * see {@link module:engine/content-figures.scanFigures}'s `resolveRole` — for
182
182
  * an address still in its authored form, before {@link resolveEmbeds} has
183
183
  * rewritten it into the ordinary image every surface renders.