@heroiclands/package-build 23.12.8 → 23.13.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 +48 -0
  2. package/README.md +1 -1
  3. package/assets/preview/gitea.ini +2 -2
  4. package/assets/styles/content-base.css +143 -23
  5. package/bin/content-commands.mjs +15 -4
  6. package/bin/package-build-preview.mjs +2 -2
  7. package/docs/api.md +18 -18
  8. package/docs/authoring/links-and-markup.md +156 -22
  9. package/docs/commands.md +22 -7
  10. package/docs/configuration.md +1 -1
  11. package/docs/content-format.md +3 -3
  12. package/docs/diagnostics.md +42 -0
  13. package/docs/reference/format-details.md +99 -22
  14. package/docs/reference/note-types.md +8 -1
  15. package/docs/reference/theme-pages.md +51 -0
  16. package/docs/source-preview.md +1 -1
  17. package/engine/anchors.mjs +2 -2
  18. package/engine/code-fences.mjs +3 -3
  19. package/engine/content-blocks.mjs +3 -3
  20. package/engine/content-figures.mjs +5 -5
  21. package/engine/content-index.mjs +1 -1
  22. package/engine/content-scripts.mjs +55 -21
  23. package/engine/content-spans.mjs +2 -2
  24. package/engine/content-verse.mjs +750 -0
  25. package/engine/dictionary-rows.mjs +15 -4
  26. package/engine/field-reference.mjs +4 -4
  27. package/engine/first-use.mjs +17 -27
  28. package/engine/key-case-collisions.mjs +136 -0
  29. package/engine/language-data.mjs +203 -6
  30. package/engine/language-terms.mjs +74 -5
  31. package/engine/map-lexicon.mjs +42 -23
  32. package/engine/names.mjs +36 -9
  33. package/engine/note-matter.mjs +66 -0
  34. package/engine/pdf-build.mjs +1 -0
  35. package/engine/pdf-render.mjs +107 -18
  36. package/engine/read-stdin.mjs +29 -0
  37. package/engine/run-rows.mjs +2 -2
  38. package/engine/run-words.mjs +4 -111
  39. package/engine/site-build.mjs +4 -4
  40. package/engine/site-preview.mjs +14 -14
  41. package/engine/sql-tables.mjs +20 -3
  42. package/engine/web-wikilinks.mjs +1 -1
  43. package/engine/word-forms.mjs +349 -0
  44. package/engine/word-terms.mjs +9 -0
  45. package/hugo-theme/static/css/style.css +92 -14
  46. package/package.json +2 -2
  47. package/types/engine/code-fences.d.mts +3 -3
  48. package/types/engine/content-scripts.d.mts +8 -4
  49. package/types/engine/content-verse.d.mts +240 -0
  50. package/types/engine/dictionary-rows.d.mts +10 -0
  51. package/types/engine/field-reference.d.mts +1 -1
  52. package/types/engine/key-case-collisions.d.mts +28 -0
  53. package/types/engine/language-data.d.mts +0 -2
  54. package/types/engine/map-lexicon.d.mts +19 -6
  55. package/types/engine/names.d.mts +34 -3
  56. package/types/engine/note-matter.d.mts +48 -0
  57. package/types/engine/read-stdin.d.mts +11 -0
  58. package/types/engine/run-rows.d.mts +2 -2
  59. package/types/engine/run-words.d.mts +0 -15
  60. package/types/engine/sql-tables.d.mts +4 -1
  61. package/types/engine/web-wikilinks.d.mts +1 -1
  62. package/types/engine/word-forms.d.mts +82 -0
  63. package/engine/content-poetry.mjs +0 -240
  64. package/types/engine/content-poetry.d.mts +0 -58
package/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 23.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ **Language registers**
8
+
9
+ - A register row tagged `other:<tongue>` or `exonym:<tongue>` that its tongue's own register does not list, or whose tongue no language note claims, is an error instead of a warning, so a package carrying one now fails its lint.
10
+
11
+ **Verse**
12
+
13
+ - A poem is written in a ` ```verse ` fence, with the same keys as before; a ` ```poetry ` fence is an ordinary code block, so rename each one to keep it a poem.
14
+ - On the site and in Foundry journals a poem carries the classes `verse` and `verse-line`; restyle any stylesheet that targets the old names.
15
+
16
+ ### Patch Changes
17
+
18
+ **Languages**
19
+
20
+ - An affix may name the word classes it attaches to in `attachesTo`, such as `[n, adj]`; mapped text then accepts its forms of those words alone.
21
+ - An affix that names no classes attaches to words of every class.
22
+
23
+ **Installing**
24
+
25
+ - `npm audit` in a package that installs this one reports no sprintf-js advisory.
26
+
27
+ **Names in prose**
28
+
29
+ - Bold text is emphasis only: a bold label such as `**Strength**:` draws no warning asking you to mark or link it as a name.
30
+ - A name marked with a `data-type` is compared only with notes of that type and subtype: a lineage that shares its name with a place draws no warning to link the place.
31
+
32
+ **Definition lists**
33
+
34
+ - A term with several descriptions sets each description on its own line on the site and in Foundry journals, instead of running them together.
35
+
36
+ **Language notes**
37
+
38
+ - A language may state `agreement` and `participles` in its grammar, and the word check then accepts its adjectives' and pronouns' agreeing forms and its participles on the verb root.
39
+ - `package-build lexicon --derived` lists every form a language's rules derive, each beside the word it comes from.
40
+
41
+ **Content index**
42
+
43
+ - Two notes that spell a key of the same map with different case, such as `carved` and `Carved`, are reported at both notes and their lines, instead of aborting with a message that names no file.
44
+
45
+ **Verse**
46
+
47
+ - A poem can number its lines with `numstep=`, and can be set in up to two columns with `col1=` and `col2=`, such as a verse in a script beside its English, line for line.
48
+ - Each column stacks entries that set one written line through a mapping and a font, so a line typed once in roman shows in its script and in roman.
49
+ - A line that does not pair across the columns is an error at that line.
50
+
3
51
  ## 23.12.8
4
52
 
5
53
  ### Patch Changes
package/README.md CHANGED
@@ -82,7 +82,7 @@ Every icon and image is covered by a provenance record stating `attribution`, `l
82
82
 
83
83
  `package-build cdn publish --set <file> --name <display name>` uploads every icon and image the package has, shipped or not, to `<site.assets>/<contentPackage>/` in that flat layout, and writes the named set listing the pictures the package leaves out. It reads the bucket from `CDN_ENDPOINT`, `CDN_BUCKET`, `CDN_ACCESS_KEY_ID` and `CDN_SECRET_ACCESS_KEY`, in the environment or `.env.local`. See [publish pictures to the CDN](docs/guides/release-and-deploy.md#publish-pictures-to-the-cdn).
84
84
 
85
- Leading captions, generic fenced divs, inline spans, and poetry code fences are described in [links and markup](docs/authoring/links-and-markup.md).
85
+ Leading captions, generic fenced divs, inline spans, and verse code fences are described in [links and markup](docs/authoring/links-and-markup.md).
86
86
 
87
87
  Cypress end-to-end suites use pinned Chrome for Testing 155.0.8059.84 by default
88
88
  on macOS arm64 and Linux x64, downloaded once into a reusable cache. Select an
@@ -9,12 +9,12 @@ RENDER_CONTENT_MODE = sanitized
9
9
  [markup.sanitizer.markdown.previewDiv]
10
10
  ELEMENT = div
11
11
  ALLOW_ATTR = class
12
- REGEXP = ^(hl-preview(?:-[a-z-]+)?|alert(?:-(?:note|tip|important|warning))?|content-figure(?:-[a-z-]+)?|poetry)( (hl-preview(?:-[a-z-]+)?|alert(?:-(?:note|tip|important|warning))?|content-figure(?:-[a-z-]+)?|poetry))*$
12
+ REGEXP = ^(hl-preview(?:-[a-z-]+)?|alert(?:-(?:note|tip|important|warning))?|content-figure(?:-[a-z-]+)?|verse)( (hl-preview(?:-[a-z-]+)?|alert(?:-(?:note|tip|important|warning))?|content-figure(?:-[a-z-]+)?|verse))*$
13
13
 
14
14
  [markup.sanitizer.markdown.previewSpan]
15
15
  ELEMENT = span
16
16
  ALLOW_ATTR = class
17
- REGEXP = ^(hl-preview-[a-z-]+|alert-(?:title|icon)|poetry-[a-z-]+|i[0-8])( (hl-preview-[a-z-]+|alert-(?:title|icon)|poetry-[a-z-]+|i[0-8]))*$
17
+ REGEXP = ^(hl-preview-[a-z-]+|alert-(?:title|icon)|verse-[a-z-]+|i[0-8])( (hl-preview-[a-z-]+|alert-(?:title|icon)|verse-[a-z-]+|i[0-8]))*$
18
18
 
19
19
  [markup.sanitizer.markdown.previewPre]
20
20
  ELEMENT = pre
@@ -16,7 +16,7 @@
16
16
  * named first in the manifest's `styles`.
17
17
  *
18
18
  * It carries what every package's compiled content needs to read in Foundry as
19
- * it does in the preview and on the site: definition lists, poetry, alerts,
19
+ * it does in the preview and on the site: definition lists, verse, alerts,
20
20
  * inline spoilers, marked names, unresolved links and captioned blocks. Every
21
21
  * class the Foundry renderer emits is styled here or exempted by name in
22
22
  * `tests/base-styles.test.ts`. A package keeps its own stylesheet for its own
@@ -120,48 +120,152 @@
120
120
  }
121
121
 
122
122
  /* Each verse line breaks in the markup; each paragraph is a stanza. */
123
- :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) .poetry {
123
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) .verse {
124
124
  margin: 1.5rem 0;
125
125
  line-height: 1.7;
126
126
  }
127
127
 
128
- :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) .poetry p {
128
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) .verse p {
129
129
  margin: 0;
130
130
  }
131
131
 
132
- :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) .poetry p + p {
132
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) .verse p + p {
133
133
  margin-top: 1em;
134
134
  }
135
135
 
136
- .poetry .poetry-line {
136
+ .verse .verse-line {
137
137
  display: inline-block;
138
- margin-inline-start: var(--poetry-indent, 0em);
138
+ margin-inline-start: var(--verse-indent, 0em);
139
139
  white-space: pre-wrap;
140
140
  }
141
141
 
142
- .poetry .poetry-line.i1 {
143
- --poetry-indent: 1.25em;
142
+ .verse .verse-line.i1 {
143
+ --verse-indent: 1.25em;
144
144
  }
145
- .poetry .poetry-line.i2 {
146
- --poetry-indent: 2.5em;
145
+ .verse .verse-line.i2 {
146
+ --verse-indent: 2.5em;
147
147
  }
148
- .poetry .poetry-line.i3 {
149
- --poetry-indent: 3.75em;
148
+ .verse .verse-line.i3 {
149
+ --verse-indent: 3.75em;
150
150
  }
151
- .poetry .poetry-line.i4 {
152
- --poetry-indent: 5em;
151
+ .verse .verse-line.i4 {
152
+ --verse-indent: 5em;
153
153
  }
154
- .poetry .poetry-line.i5 {
155
- --poetry-indent: 6.25em;
154
+ .verse .verse-line.i5 {
155
+ --verse-indent: 6.25em;
156
156
  }
157
- .poetry .poetry-line.i6 {
158
- --poetry-indent: 7.5em;
157
+ .verse .verse-line.i6 {
158
+ --verse-indent: 7.5em;
159
159
  }
160
- .poetry .poetry-line.i7 {
161
- --poetry-indent: 8.75em;
160
+ .verse .verse-line.i7 {
161
+ --verse-indent: 8.75em;
162
162
  }
163
- .poetry .poetry-line.i8 {
164
- --poetry-indent: 10em;
163
+ .verse .verse-line.i8 {
164
+ --verse-indent: 10em;
165
+ }
166
+
167
+ /* Numbered verse carries its numbers in a margin. A verse in columns is one
168
+ * grid per verse line: its number, then each column's parts stacked, the
169
+ * first in the normal tone and the rest muted, every part in the serif unless
170
+ * its face sets it. A narrow journal stacks the columns of each line. */
171
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
172
+ .verse.verse-numbered {
173
+ position: relative;
174
+ padding-inline-start: 3em;
175
+ }
176
+
177
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
178
+ .verse
179
+ .verse-number {
180
+ color: color-mix(in srgb, currentColor 60%, transparent);
181
+ font-size: 0.8em;
182
+ }
183
+
184
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
185
+ .verse-numbered
186
+ .verse-line
187
+ .verse-number {
188
+ position: absolute;
189
+ inset-inline-start: 0;
190
+ width: 2.2em;
191
+ text-align: end;
192
+ }
193
+
194
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
195
+ .verse.verse-columns {
196
+ container-type: inline-size;
197
+ }
198
+
199
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
200
+ .verse-numbered.verse-columns {
201
+ padding-inline-start: 0;
202
+ }
203
+
204
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
205
+ .verse-columns
206
+ .verse-row {
207
+ display: grid;
208
+ grid-auto-flow: column;
209
+ grid-auto-columns: 1fr;
210
+ column-gap: 1.5rem;
211
+ margin-bottom: 0.5em;
212
+ }
213
+
214
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
215
+ .verse-numbered.verse-columns
216
+ .verse-row {
217
+ grid-template-columns: 2.5em;
218
+ }
219
+
220
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
221
+ .verse-columns
222
+ .verse-number {
223
+ text-align: end;
224
+ padding-top: 0.2em;
225
+ }
226
+
227
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
228
+ .verse-columns
229
+ :is(.verse-column, .verse-part) {
230
+ display: block;
231
+ white-space: pre-wrap;
232
+ }
233
+
234
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
235
+ .verse-columns
236
+ .verse-part {
237
+ font-family: serif;
238
+ }
239
+
240
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
241
+ .verse-columns
242
+ .verse-secondary {
243
+ color: color-mix(in srgb, currentColor 60%, transparent);
244
+ }
245
+
246
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
247
+ .verse-columns
248
+ .verse-part[dir="rtl"] {
249
+ text-align: start;
250
+ }
251
+
252
+ @container (max-width: 32rem) {
253
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
254
+ .verse-columns
255
+ .verse-row {
256
+ grid-auto-flow: row;
257
+ grid-template-columns: 1fr;
258
+ }
259
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
260
+ .verse-numbered.verse-columns
261
+ .verse-row {
262
+ grid-template-columns: 2.5em 1fr;
263
+ }
264
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
265
+ .verse-numbered.verse-columns
266
+ .verse-column {
267
+ grid-column: 2;
268
+ }
165
269
  }
166
270
 
167
271
  /* Text set in one of the faces. The font family and features come from the
@@ -172,7 +276,7 @@
172
276
  }
173
277
 
174
278
  .script-block[dir="rtl"],
175
- .poetry[dir="rtl"] {
279
+ .verse[dir="rtl"] {
176
280
  text-align: start;
177
281
  }
178
282
 
@@ -343,6 +447,22 @@
343
447
  > p:first-child {
344
448
  display: inline;
345
449
  }
450
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
451
+ .dictionary
452
+ > div
453
+ > dd
454
+ + dd {
455
+ display: block;
456
+ text-indent: 0;
457
+ }
458
+ :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror)
459
+ .dictionary
460
+ > div
461
+ > dd
462
+ + dd
463
+ > p:first-child {
464
+ display: block;
465
+ }
346
466
  @media (max-width: 600px) {
347
467
  :is(.journal-entry-page, .journal-entry-content, .editor-content, .prosemirror) .dictionary {
348
468
  --indent: 100px;
@@ -31,7 +31,7 @@
31
31
  * npx package-build map [--tree] [--from <shortcode>] [--chart <shortcode>] [--travel] [--out <dir>]
32
32
  * npx package-build reachability <dir> [file] [--index <shortcode>]
33
33
  * npx package-build transliterate <map> [--lang <language>] [--font <slug>] [--hex] [text..]
34
- * npx package-build lexicon <language> --map <map> [--font <slug>] [--json]
34
+ * npx package-build lexicon <language> --map <map> [--font <slug>] [--derived] [--json]
35
35
  * npx package-build addresses diff --from <zip|dir> [--strict]
36
36
  * npx package-build npc new (--spec <json> | --specfile <path>) [--seed <text>] [--out <path>]
37
37
  *
@@ -45,6 +45,7 @@ import fs from "fs";
45
45
  import path from "node:path";
46
46
  import log from "loglevel";
47
47
  import prefix from "loglevel-plugin-prefix";
48
+ import { readStdin } from "../engine/read-stdin.mjs";
48
49
  import { compilePacks, cleanPacks, unpackPacks } from "../engine/compendiums.mjs";
49
50
  import { compilesFoundryDocuments, publishesContentPages } from "../content-config.mjs";
50
51
  import { loadPackConfig, packConfigPath } from "../engine/pack-config.mjs";
@@ -2397,7 +2398,7 @@ function transliterateCommand() {
2397
2398
  default: false,
2398
2399
  describe: "Print the code points, as `U+XXXX` separated by spaces",
2399
2400
  }),
2400
- handler: (argv) => {
2401
+ handler: async (argv) => {
2401
2402
  try {
2402
2403
  const read = mappingContext();
2403
2404
  if (!read) {
@@ -2407,7 +2408,7 @@ function transliterateCommand() {
2407
2408
  const words = (argv.text ?? []).map(String);
2408
2409
  const text =
2409
2410
  !words.length || (words.length === 1 && words[0] === "-") ?
2410
- fs.readFileSync(0, "utf8").replace(/\r?\n$/, "")
2411
+ (await readStdin()).replace(/\r?\n$/, "")
2411
2412
  : words.join(" ");
2412
2413
  const mapped = transliterate(
2413
2414
  read.ctx,
@@ -2437,7 +2438,9 @@ function transliterateCommand() {
2437
2438
 
2438
2439
  /**
2439
2440
  * `package-build lexicon <language> --map <map>` — every word of a language's
2440
- * lexicon with its native form under a map, as the build writes it.
2441
+ * lexicon with its native form under a map, as the build writes it; with
2442
+ * `--derived`, the forms its rules derive follow, each naming its word in a
2443
+ * sixth column.
2441
2444
  *
2442
2445
  * @returns {object} The yargs command module.
2443
2446
  */
@@ -2461,6 +2464,12 @@ function lexiconCommand() {
2461
2464
  type: "string",
2462
2465
  describe: "The face; required when the map lists more than one",
2463
2466
  })
2467
+ .option("derived", {
2468
+ type: "boolean",
2469
+ default: false,
2470
+ describe:
2471
+ "Also list the forms the language's rules derive, each beside its lexicon word",
2472
+ })
2464
2473
  .option("json", {
2465
2474
  type: "boolean",
2466
2475
  default: false,
@@ -2477,6 +2486,7 @@ function lexiconCommand() {
2477
2486
  lang: String(argv.language),
2478
2487
  map: String(argv.map),
2479
2488
  font: argv.font,
2489
+ derived: Boolean(argv.derived),
2480
2490
  });
2481
2491
  if ("problem" in listed) {
2482
2492
  reportMapping(read.config, listed);
@@ -2492,6 +2502,7 @@ function lexiconCommand() {
2492
2502
  entry.exception ? "exception" : "rows",
2493
2503
  entry.class ?? "",
2494
2504
  entry.gloss ?? "",
2505
+ ...(argv.derived ? [entry.from ?? ""] : []),
2495
2506
  ].join("\t"),
2496
2507
  );
2497
2508
  } catch (err) {
@@ -13,7 +13,6 @@
13
13
  */
14
14
 
15
15
  /** Render stdin with optional date-only configuration and no content index. */
16
- import { readFileSync } from "node:fs";
17
16
  import { formatDiagnostic } from "../engine/diagnostics.mjs";
18
17
 
19
18
  const HELP = `Usage: package-build-preview [--dark] [--current-date DATE] [--css]
@@ -54,10 +53,11 @@ try {
54
53
  else {
55
54
  const { renderPreview, previewStyles, readPreviewCurrentDate } =
56
55
  await import("../engine/site-preview.mjs");
56
+ const { readStdin } = await import("../engine/read-stdin.mjs");
57
57
  if (css) process.stdout.write(`${previewStyles()}\n`);
58
58
  else {
59
59
  currentDate ??= await readPreviewCurrentDate(process.cwd());
60
- const result = await renderPreview(readFileSync(0, "utf8"), {
60
+ const result = await renderPreview(await readStdin(), {
61
61
  dark,
62
62
  file: "<stdin>",
63
63
  currentDate,
package/docs/api.md CHANGED
@@ -1001,7 +1001,6 @@ into one warning for each occurrence; the message names the full name or
1001
1001
  explicit label that needs attention. Only `.name` spans and authored link
1002
1002
  labels enter name tracking; plain text is not searched. Indexed full names and
1003
1003
  declared aliases share an identity, while other labels use their whole text.
1004
- A whole bold indexed name receives an advisory without entering name tracking.
1005
1004
  Local and captured dependency names and aliases share the same corpus
1006
1005
  context. The command's lint target constructs
1007
1006
  the checker once, uses exact authored `raw` strings and keeps these findings
@@ -1340,7 +1339,7 @@ const result = renderPreview(currentBufferText, { dark: true });
1340
1339
  showDiagnostics(result.findings);
1341
1340
  ```
1342
1341
 
1343
- Ordinary Markdown, definition lists, footnotes, alert boxes, poetry, captions, local figures and container blocks render locally. Alerts reuse the four shared icons and titles, accept custom titles and nested alerts, and have opaque tinted backgrounds in both modes. Definition terms are bold; definitions retain ordinary Markdown styling. Wikilinks and image embeds retain their entire authored source: blue opening markers (including all of `![[`), brackets and pipes, purple addresses and anchors, bold foreground labels, and grey attributes. Spans retain their entire source in bold, with blue brackets and grey attributes. Code examples stay literal. Incomplete syntax remains visible while editing.
1342
+ Ordinary Markdown, definition lists, footnotes, alert boxes, verse, captions, local figures and container blocks render locally. Alerts reuse the four shared icons and titles, accept custom titles and nested alerts, and have opaque tinted backgrounds in both modes. Definition terms are bold; definitions retain ordinary Markdown styling. Wikilinks and image embeds retain their entire authored source: blue opening markers (including all of `![[`), brackets and pipes, purple addresses and anchors, bold foreground labels, and grey attributes. Spans retain their entire source in bold, with blue brackets and grey attributes. Code examples stay literal. Incomplete syntax remains visible while editing.
1344
1343
 
1345
1344
  SQL and pagelist fences remain grey source, including delimiters. Calendar/date expressions, cross-note references and maps remain green source. A dependent call nested inside a pure helper preserves the entire outer expression. Pure helpers and scalar frontmatter expressions evaluate locally; same-note figure references and non-date event facts can resolve from this buffer. Infobox fields show authored values and green reference source; image and portrait wikilinks use the image syntax colors. Missing ages are calculated locally from neutral birth dates and `currentDate`, defaulting to `"720.1"`; authored ages, including `0`, take precedence. Unavailable ages remain `null` internally, have no infobox row, and render as empty text in `{{data.age}}`. Named-calendar birth dates remain unavailable without calendar context. Set `infobox: false` to omit the profile.
1346
1345
 
@@ -2439,12 +2438,12 @@ Not a JavaScript module — this subpath entry exists so tooling (bundlers, `imp
2439
2438
 
2440
2439
  The engine wildcard subpath also exposes the body parsers below. Their figure exports operate on leading captions.
2441
2440
 
2442
- | Module | Exports | Contract |
2443
- | ---------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2444
- | `engine/content-figures.mjs` | `FIGURE_NAMES`, `FIGURE_CLASSES`, `scanFigures`, `numberFigures`, `renderFigureBlocks` | Scan leading captions and their next supported blocks; `numbered` distinguishes `:` and `:@`. `type` may override inference; `example` is supported. `FIGURE_CLASSES` retains built-in styled classes, not a closed author vocabulary. |
2445
- | `engine/content-blocks.mjs` | `BLOCK_NAMES`, `BLOCK_CONTAINERS`, `scanBlocks`, `renderBlocks`, `renderWithheldSections` | Read div and secret fences; generic div records have `name: "div"` and no generated title. Scanning includes poetry errors. |
2446
- | `engine/content-poetry.mjs` | `verseLines`, `poetryStanzas`, `inlineVerse`, `poetryMarkdown`, `scanPoetry`, `renderPoetry` | Read `poetry` code fences, validate authored metadata and tabs, and preserve relative indent levels and stanzas. |
2447
- | `engine/content-spans.mjs` | `spanMarkdownPlugin`, `scanSpans`, `renderSpans` | Parse bracket spans after recognized links. The scanner returns spans, the exact `.name` inventory and located errors, retaining visible text and attribute positions; rendering preserves inline Markdown and permitted attributes. |
2441
+ | Module | Exports | Contract |
2442
+ | ---------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2443
+ | `engine/content-figures.mjs` | `FIGURE_NAMES`, `FIGURE_CLASSES`, `scanFigures`, `numberFigures`, `renderFigureBlocks` | Scan leading captions and their next supported blocks; `numbered` distinguishes `:` and `:@`. `type` may override inference; `example` is supported. `FIGURE_CLASSES` retains built-in styled classes, not a closed author vocabulary. |
2444
+ | `engine/content-blocks.mjs` | `BLOCK_NAMES`, `BLOCK_CONTAINERS`, `scanBlocks`, `renderBlocks`, `renderWithheldSections` | Read div and secret fences; generic div records have `name: "div"` and no generated title. Scanning includes verse errors. |
2445
+ | `engine/content-verse.mjs` | `verseLines`, `verseStanzas`, `inlineVerse`, `verseMarkdown`, `scanVerse`, `renderVerse` | Read `verse` code fences, validate authored metadata and tabs, and preserve relative indent levels and stanzas. |
2446
+ | `engine/content-spans.mjs` | `spanMarkdownPlugin`, `scanSpans`, `renderSpans` | Parse bracket spans after recognized links. The scanner returns spans, the exact `.name` inventory and located errors, retaining visible text and attribute positions; rendering preserves inline Markdown and permitted attributes. |
2448
2447
 
2449
2448
  Inline `.secret` spans preserve their inline content, name classes and
2450
2449
  attributes. Site rendering uses a separate show/hide button and content span,
@@ -2535,16 +2534,17 @@ The wildcard engine subpath exposes
2535
2534
  `@heroiclands/package-build/engine/dictionary-rows` for deriving the
2536
2535
  `dictionary` and `terms` SQL relations from content-index records:
2537
2536
 
2538
- | Export | Signature | Returns | Use it when |
2539
- | ----------------------- | ----------------------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------- |
2540
- | `DICTIONARY_COLUMNS` | `const DICTIONARY_COLUMNS` | A readonly list of column names | Declaring the schema of a language's dictionary. |
2541
- | `TERM_COLUMNS` | `const TERM_COLUMNS` | A readonly list of column names | Declaring the schema of owner-specific glossary terms. |
2542
- | `ETYMOLOGY_KEYS` | `const ETYMOLOGY_KEYS` | A readonly list of keys | Reading the structure of a dictionary word's etymology. |
2543
- | `dictionaryLetter` | `dictionaryLetter(text)` | `string` | Deriving a heading letter from a sort key. |
2544
- | `dictionaryEntryAnchor` | `dictionaryEntryAnchor(language, name)` | `string` | Deriving the stable entry anchor for a word in a language's dictionary. |
2545
- | `dictionaryAndTermRows` | `dictionaryAndTermRows(records, all = records)` | `{ dictionary: object[], terms: object[] }` | Deriving dictionary words and glossary terms for the selected records. |
2546
- | `serializeRows` | `serializeRows(rows)` | `string` | Writing deterministic JSON Lines with keys sorted at every depth. |
2547
- | `pluralOf` | `pluralOf(word, grammar)` | `string \| null` | Deriving a noun's plural: its own `plural`, else the first `grammar.plural` row whose ending fits. |
2537
+ | Export | Signature | Returns | Use it when |
2538
+ | ----------------------- | ----------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
2539
+ | `DICTIONARY_COLUMNS` | `const DICTIONARY_COLUMNS` | A readonly list of column names | Declaring the schema of a language's dictionary. |
2540
+ | `TERM_COLUMNS` | `const TERM_COLUMNS` | A readonly list of column names | Declaring the schema of owner-specific glossary terms. |
2541
+ | `ETYMOLOGY_KEYS` | `const ETYMOLOGY_KEYS` | A readonly list of keys | Reading the structure of a dictionary word's etymology. |
2542
+ | `dictionaryLetter` | `dictionaryLetter(text)` | `string` | Deriving a heading letter from a sort key. |
2543
+ | `dictionaryEntryAnchor` | `dictionaryEntryAnchor(language, name)` | `string` | Deriving the stable entry anchor for a word in a language's dictionary. |
2544
+ | `dictionaryAndTermRows` | `dictionaryAndTermRows(records, all = records)` | `{ dictionary: object[], terms: object[] }` | Deriving dictionary words and glossary terms for the selected records. |
2545
+ | `serializeRows` | `serializeRows(rows)` | `string` | Writing deterministic JSON Lines with keys sorted at every depth. |
2546
+ | `pluralOf` | `pluralOf(word, grammar)` | `string \| null` | Deriving a noun's plural: its own `plural`, else the first `grammar.plural` row whose ending fits. |
2547
+ | `pluralByRule` | `pluralByRule(form, grammar)` | `string \| null` | Deriving the plural the `grammar.plural` rule gives any form, whatever its class: the first row whose ending fits. |
2548
2548
 
2549
2549
  `dictionaryEntryAnchor` takes the language's canonical Address and the
2550
2550
  word's form. Its `word-` anchor is derived from the full SHA-256 digest of