@heroiclands/package-build 22.15.0 → 22.16.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 (223) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +39 -678
  3. package/bin/{content-build.mjs → content-commands.mjs} +197 -118
  4. package/bin/package-build.mjs +303 -42
  5. package/bin/report.mjs +1 -1
  6. package/ci/ci-docker.mjs +6 -3
  7. package/ci/ci-steps.mjs +6 -4
  8. package/ci/content-format-reference.mjs +106 -0
  9. package/config.mjs +22 -2
  10. package/content-config.mjs +74 -134
  11. package/docs/api.md +294 -234
  12. package/docs/authoring/assets.md +31 -0
  13. package/docs/authoring/dates-and-calendars.md +33 -0
  14. package/docs/authoring/first-note.md +48 -0
  15. package/docs/authoring/frontmatter.md +40 -0
  16. package/docs/authoring/links-and-markup.md +137 -0
  17. package/docs/commands.md +631 -162
  18. package/docs/configuration.md +176 -140
  19. package/docs/content-format.md +50 -4229
  20. package/docs/design.md +32 -0
  21. package/docs/diagnostics.md +29 -22
  22. package/docs/getting-started.md +21 -877
  23. package/docs/guides/book.md +153 -0
  24. package/docs/guides/e2e.md +143 -0
  25. package/docs/guides/icons.md +126 -0
  26. package/docs/guides/localization.md +120 -0
  27. package/docs/guides/module-project.md +915 -0
  28. package/docs/guides/packs.md +167 -0
  29. package/docs/guides/reference-pages.md +148 -0
  30. package/docs/guides/release-and-deploy.md +114 -0
  31. package/docs/guides/site.md +152 -0
  32. package/docs/guides/system-project.md +45 -0
  33. package/docs/index.md +16 -0
  34. package/docs/project-setup.md +54 -35
  35. package/docs/reference/format-details.md +4420 -0
  36. package/docs/reference/note-types.md +531 -0
  37. package/engine/address-render.mjs +26 -0
  38. package/engine/address.mjs +3 -20
  39. package/engine/anchors.mjs +15 -6
  40. package/engine/asset-bindings.mjs +6 -2
  41. package/engine/base-compiler.mjs +47 -11
  42. package/engine/being-measurements.mjs +70 -0
  43. package/engine/calendar-artifacts.mjs +1 -1
  44. package/engine/calendar-choice.mjs +32 -0
  45. package/engine/calendar-human.mjs +71 -0
  46. package/engine/calendar-notes.mjs +41 -3
  47. package/engine/calendars.mjs +8 -1
  48. package/engine/changelog-lint.mjs +2 -2
  49. package/engine/code-fences.mjs +23 -97
  50. package/engine/compile-corpus.mjs +6 -7
  51. package/engine/content-admonitions.mjs +114 -0
  52. package/engine/content-captions.mjs +181 -0
  53. package/engine/content-embeds.mjs +2 -2
  54. package/engine/content-footnotes.mjs +34 -0
  55. package/engine/content-format-check.mjs +2 -2
  56. package/engine/content-format.mjs +127 -73
  57. package/engine/content-format.yaml +745 -0
  58. package/engine/content-html.mjs +1 -1
  59. package/engine/content-icons.mjs +45 -92
  60. package/engine/content-images.mjs +22 -74
  61. package/engine/content-index.mjs +12 -3
  62. package/engine/content-links.mjs +21 -26
  63. package/engine/content-lint.mjs +1 -5
  64. package/engine/content-secrets.mjs +3 -1
  65. package/engine/content-tables.mjs +47 -1257
  66. package/engine/culture-choice.mjs +32 -0
  67. package/engine/date-conversion.mjs +47 -0
  68. package/engine/declaration-check.mjs +87 -0
  69. package/engine/docs-checks.mjs +140 -0
  70. package/engine/documentation-links.mjs +31 -0
  71. package/engine/exported-scene.mjs +1 -1
  72. package/engine/extension-attributes.mjs +74 -0
  73. package/engine/field-reference.mjs +1 -1
  74. package/engine/field-spec.mjs +4 -0
  75. package/engine/foreign-catalog.mjs +5 -5
  76. package/engine/format-generated.mjs +15 -0
  77. package/engine/foundry-entries.mjs +3 -1
  78. package/engine/frontmatter-lint.mjs +5 -1
  79. package/engine/frontmatter.mjs +10 -0
  80. package/engine/generate.mjs +2 -2
  81. package/engine/helpers.mjs +71 -117
  82. package/engine/homepage.mjs +2 -3
  83. package/engine/index-records.mjs +1 -0
  84. package/engine/index.mjs +18 -3
  85. package/engine/infobox-registry.mjs +3 -1
  86. package/engine/infobox.mjs +38 -20
  87. package/engine/item-compiler.mjs +10 -8
  88. package/engine/item-docs.mjs +7 -6
  89. package/engine/itinerary-scenes.mjs +4 -2
  90. package/engine/journals.mjs +51 -10
  91. package/engine/macros.mjs +1 -1
  92. package/engine/map-build.mjs +1 -1
  93. package/engine/map-dot.mjs +1 -1
  94. package/engine/map-raster.mjs +4 -2
  95. package/engine/markdown-expressions.mjs +117 -0
  96. package/engine/metadata-index.mjs +7 -3
  97. package/engine/note-addresses.mjs +1 -0
  98. package/engine/note-dates.mjs +283 -61
  99. package/engine/note-package.mjs +5 -25
  100. package/engine/note-state.mjs +6 -2
  101. package/engine/note-vocabulary.mjs +74 -19
  102. package/engine/number-words.mjs +75 -0
  103. package/engine/office-holders.mjs +188 -0
  104. package/engine/pack-config.mjs +2 -15
  105. package/engine/pack-router.mjs +24 -5
  106. package/engine/packages.mjs +3 -2
  107. package/engine/pathnames.mjs +21 -24
  108. package/engine/pdf-build.mjs +217 -55
  109. package/engine/pdf-fonts.mjs +2 -2
  110. package/engine/pdf-images.mjs +82 -0
  111. package/engine/pdf-render.mjs +174 -22
  112. package/engine/populations.mjs +3 -2
  113. package/engine/project-init.mjs +499 -0
  114. package/engine/prose-config.mjs +1 -1
  115. package/engine/prose-lint.mjs +7 -6
  116. package/engine/readability-lint.mjs +86 -0
  117. package/engine/reckoning-markers.mjs +205 -93
  118. package/engine/scenes.mjs +5 -2
  119. package/engine/schema-check.mjs +3 -3
  120. package/engine/site-build.mjs +87 -37
  121. package/engine/site-config.mjs +8 -8
  122. package/engine/site-index.mjs +4 -0
  123. package/engine/site-maps.mjs +1 -1
  124. package/engine/site-preview.mjs +19 -8
  125. package/engine/social-ties.mjs +5 -1
  126. package/engine/sql-tables.mjs +92 -22
  127. package/engine/web-wikilinks.mjs +15 -31
  128. package/engine/wikilink-syntax.mjs +4 -1
  129. package/engine/wikilinks.mjs +12 -14
  130. package/hm3/item-fields.mjs +1 -1
  131. package/hm3/items.mjs +12 -45
  132. package/index.mjs +2 -14
  133. package/manifest.mjs +31 -4
  134. package/markdownlint-config.mjs +1 -1
  135. package/package.json +32 -11
  136. package/prettier-config.mjs +1 -1
  137. package/sohl/being-info.mjs +3 -145
  138. package/sohl/index.mjs +1 -1
  139. package/sohl/infobox.mjs +2 -1
  140. package/sohl/item-builders.mjs +3 -27
  141. package/sohl/item-fields.mjs +61 -22
  142. package/sohl/kb-passes.mjs +2 -2
  143. package/sohl/note-schemas.mjs +0 -8
  144. package/stage.mjs +19 -0
  145. package/types/config.d.mts +12 -3
  146. package/types/content-config.d.mts +15 -81
  147. package/types/engine/address-render.d.mts +13 -0
  148. package/types/engine/address.d.mts +2 -11
  149. package/types/engine/anchors.d.mts +3 -4
  150. package/types/engine/asset-bindings.d.mts +9 -2
  151. package/types/engine/base-compiler.d.mts +1 -2
  152. package/types/engine/being-measurements.d.mts +16 -0
  153. package/types/engine/calendar-choice.d.mts +14 -0
  154. package/types/engine/calendar-human.d.mts +4 -0
  155. package/types/engine/calendar-notes.d.mts +2 -0
  156. package/types/engine/calendars.d.mts +5 -3
  157. package/types/engine/code-fences.d.mts +6 -72
  158. package/types/engine/compile-corpus.d.mts +1 -3
  159. package/types/engine/content-admonitions.d.mts +24 -0
  160. package/types/engine/content-captions.d.mts +45 -0
  161. package/types/engine/content-footnotes.d.mts +10 -0
  162. package/types/engine/content-format-check.d.mts +17 -4
  163. package/types/engine/content-format.d.mts +24 -11
  164. package/types/engine/content-icons.d.mts +14 -23
  165. package/types/engine/content-images.d.mts +2 -2
  166. package/types/engine/content-index.d.mts +9 -2
  167. package/types/engine/content-links.d.mts +3 -5
  168. package/types/engine/content-tables.d.mts +11 -140
  169. package/types/engine/culture-choice.d.mts +14 -0
  170. package/types/engine/date-conversion.d.mts +4 -0
  171. package/types/engine/declaration-check.d.mts +24 -0
  172. package/types/engine/docs-checks.d.mts +28 -0
  173. package/types/engine/documentation-links.d.mts +7 -0
  174. package/types/engine/exported-scene.d.mts +6 -2
  175. package/types/engine/extension-attributes.d.mts +14 -0
  176. package/types/engine/field-spec.d.mts +9 -0
  177. package/types/engine/format-generated.d.mts +7 -0
  178. package/types/engine/frontmatter.d.mts +14 -4
  179. package/types/engine/generate.d.mts +1 -1
  180. package/types/engine/helpers.d.mts +37 -92
  181. package/types/engine/homepage.d.mts +2 -3
  182. package/types/engine/index.d.mts +5 -0
  183. package/types/engine/infobox-registry.d.mts +3 -1
  184. package/types/engine/infobox.d.mts +17 -6
  185. package/types/engine/item-compiler.d.mts +4 -4
  186. package/types/engine/item-docs.d.mts +4 -3
  187. package/types/engine/journals.d.mts +10 -7
  188. package/types/engine/map-graphviz.d.mts +1 -1
  189. package/types/engine/markdown-expressions.d.mts +17 -0
  190. package/types/engine/note-dates.d.mts +47 -1
  191. package/types/engine/note-package.d.mts +3 -19
  192. package/types/engine/note-state.d.mts +2 -1
  193. package/types/engine/note-vocabulary.d.mts +9 -0
  194. package/types/engine/number-words.d.mts +4 -0
  195. package/types/engine/office-holders.d.mts +14 -0
  196. package/types/engine/pack-config.d.mts +1 -1
  197. package/types/engine/packages.d.mts +3 -2
  198. package/types/engine/pdf-images.d.mts +38 -0
  199. package/types/engine/populations.d.mts +2 -2
  200. package/types/engine/project-init.d.mts +17 -0
  201. package/types/engine/prose-config.d.mts +1 -1
  202. package/types/engine/prose-lint.d.mts +3 -2
  203. package/types/engine/readability-lint.d.mts +22 -0
  204. package/types/engine/reckoning-markers.d.mts +4 -2
  205. package/types/engine/schema-check.d.mts +3 -3
  206. package/types/engine/site-build.d.mts +38 -28
  207. package/types/engine/site-config.d.mts +2 -2
  208. package/types/engine/site-index.d.mts +4 -0
  209. package/types/engine/site-preview.d.mts +1 -1
  210. package/types/engine/social-ties.d.mts +8 -2
  211. package/types/engine/sql-tables.d.mts +13 -2
  212. package/types/engine/web-wikilinks.d.mts +2 -1
  213. package/types/engine/wikilinks.d.mts +7 -1
  214. package/types/hm3/items.d.mts +5 -6
  215. package/types/labels.d.mts +40 -0
  216. package/types/manifest.d.mts +3 -3
  217. package/types/sohl/actors.d.mts +2 -2
  218. package/types/sohl/being-info.d.mts +3 -43
  219. package/types/sohl/index.d.mts +1 -1
  220. package/types/sohl/infobox.d.mts +5 -2
  221. package/types/sohl/item-fields.d.mts +4 -13
  222. package/types/stage.d.mts +9 -0
  223. package/CONTENT.md +0 -3239
@@ -51,7 +51,7 @@
51
51
  *
52
52
  * ## Icons
53
53
  *
54
- * `:icon-star-outline:` is parsed by the *same* {@link module:engine/content-icons.iconPlugin}
54
+ * `:icon star-outline:` is parsed by the *same* {@link module:engine/content-icons.iconPlugin}
55
55
  * the journals and the website use — one rule, three surfaces — and only the
56
56
  * output differs. The glyph is resolved from the font file the consumer named,
57
57
  * because the registry deliberately holds no codepoints; when no font is
@@ -62,9 +62,11 @@
62
62
  */
63
63
 
64
64
  import MarkdownIt from "markdown-it";
65
+ import footnotePlugin from "markdown-it-footnote";
66
+ import deflistPlugin from "markdown-it-deflist";
65
67
 
66
68
  import { bookDraftNoticePreamble } from "./draft-notice.mjs";
67
- import { iconPlugin, ICON_PATTERN } from "./content-icons.mjs";
69
+ import { iconPlugin, ICON_PATTERN, ICON_SIZES } from "./content-icons.mjs";
68
70
  import { IMAGE_CLASSES, IMAGE_FLOATS, imagePlugin } from "./content-images.mjs";
69
71
 
70
72
  /** Requested print width for every named image size. */
@@ -77,6 +79,9 @@ export const BOOK_IMAGE_WIDTHS = Object.freeze({
77
79
  "full-width": '"full-width"',
78
80
  });
79
81
  import { slugify } from "./content-slug.mjs";
82
+ import { scanCaptions } from "./content-captions.mjs";
83
+ import { scanAdmonitions } from "./content-admonitions.mjs";
84
+ import { separateFootnotes } from "./content-footnotes.mjs";
80
85
 
81
86
  /**
82
87
  * Characters that mean something to Typst's markup parser.
@@ -154,6 +159,8 @@ export function labelFor(anchor) {
154
159
  */
155
160
  export function createParser(registry) {
156
161
  const md = new MarkdownIt({ html: false, linkify: false, typographer: false });
162
+ md.use(footnotePlugin);
163
+ md.use(deflistPlugin);
157
164
  md.use(iconPlugin(registry));
158
165
  // The same plugin the HTML surfaces use, so one directive is read once and
159
166
  // three renderers read the same `meta` off the same token.
@@ -192,19 +199,110 @@ export function markdownToTypst(markdown, opts = {}) {
192
199
  headingOffset = 0,
193
200
  anchorPrefix = "",
194
201
  } = opts;
195
- const tokens = md.parse(String(markdown ?? ""), {});
202
+ const separated =
203
+ opts.footnoteDefinitions === undefined ? separateFootnotes(String(markdown ?? "")) : null;
204
+ const source = separated?.markdown ?? String(markdown ?? "");
205
+ const definitions = opts.footnoteDefinitions ?? separated?.definitions ?? "";
206
+ const footnoteState = opts.footnoteState ?? { seen: new Set() };
207
+ const sharedOptions = { ...opts, footnoteDefinitions: definitions, footnoteState };
208
+ if (!opts.insideAdmonition) {
209
+ const { blocks } = scanAdmonitions(source);
210
+ if (blocks.length) {
211
+ const lines = source.split("\n");
212
+ const output = [];
213
+ let cursor = 0;
214
+ for (const block of blocks) {
215
+ output.push(
216
+ markdownToTypst(lines.slice(cursor, block.start).join("\n"), {
217
+ ...sharedOptions,
218
+ insideAdmonition: true,
219
+ }),
220
+ );
221
+ const color = block.kind === "warn" ? "#9a6700" : "#2f6f9f";
222
+ const background = block.kind === "warn" ? "#fff5db" : "#eef6fb";
223
+ const label = block.kind === "warn" ? "Warning" : "Info";
224
+ const symbol = block.kind === "warn" ? "!" : "i";
225
+ const content = markdownToTypst(block.body, {
226
+ ...sharedOptions,
227
+ insideAdmonition: true,
228
+ });
229
+ output.push(
230
+ `\n#block(width: 100%, fill: rgb("${background}"), stroke: (left: 2pt + rgb("${color}")), inset: 8pt, above: 0.7em, below: 0.7em)[#text(fill: rgb("${color}"), weight: "bold")[${symbol} ${label}]\n\n${content}]\n`,
231
+ );
232
+ cursor = block.end + 1;
233
+ }
234
+ output.push(
235
+ markdownToTypst(lines.slice(cursor).join("\n"), {
236
+ ...sharedOptions,
237
+ insideAdmonition: true,
238
+ }),
239
+ );
240
+ return output.join("\n");
241
+ }
242
+ }
196
243
  // One map for the whole body, not one per block: a heading inside a
197
244
  // blockquote or a list item shares the entry's anchor namespace with every
198
245
  // other heading in the same body, because `sectionLabel` scopes by entry
199
246
  // rather than by container.
200
- return renderTokens(tokens, {
247
+ const ctx = {
248
+ md,
201
249
  links,
202
250
  glyphs,
203
251
  images,
204
252
  headingOffset,
205
253
  anchorPrefix,
206
254
  seen: new Map(),
207
- });
255
+ footnoteState,
256
+ footnotePrefix: opts.footnotePrefix ?? `footnote-${slugify(anchorPrefix || "body")}`,
257
+ };
258
+ const lines = source.split("\n");
259
+ const localCaptions = scanCaptions(source).captions;
260
+ const numbered = new Map(
261
+ (opts.captions ?? localCaptions).map((caption) => [caption.id, caption]),
262
+ );
263
+ const captions = localCaptions.map((caption) => numbered.get(caption.id) ?? caption);
264
+ if (!captions.length) return renderMarkdownSegment(source, md, ctx, definitions);
265
+ const out = [];
266
+ let cursor = 0;
267
+ for (const caption of captions) {
268
+ out.push(
269
+ renderMarkdownSegment(
270
+ lines.slice(cursor, caption.line - 1).join("\n"),
271
+ md,
272
+ ctx,
273
+ definitions,
274
+ ),
275
+ );
276
+ const block = lines.slice(caption.blockStart, caption.blockEnd).join("\n");
277
+ out.push(renderMarkdownSegment(block, md, { ...ctx, caption }, definitions));
278
+ if (caption.kind !== "table" && caption.kind !== "figure") {
279
+ out.push(
280
+ `\n#block(below: 0.6em)[#text(size: 7.6pt, style: "italic")[${captionMarkup(caption, ctx)}]] <${sectionLabel(anchorPrefix, slugify(caption.id))}>\n\n`,
281
+ );
282
+ }
283
+ cursor = caption.blockEnd;
284
+ }
285
+ out.push(renderMarkdownSegment(lines.slice(cursor).join("\n"), md, ctx, definitions));
286
+ return out.filter(Boolean).join("\n\n");
287
+ }
288
+
289
+ /** Render one fragment with the note's footnote definitions available. */
290
+ function renderMarkdownSegment(source, md, ctx, definitions) {
291
+ const input = definitions ? `${source}\n\n${definitions}` : source;
292
+ const tokens = md.parse(input, {});
293
+ const footnoteStart = tokens.findIndex((token) => token.type === "footnote_block_open");
294
+ const visible = footnoteStart < 0 ? tokens : tokens.slice(0, footnoteStart);
295
+ const footnotes = new Map();
296
+ if (footnoteStart >= 0) {
297
+ for (let at = footnoteStart + 1; at < tokens.length; at++) {
298
+ if (tokens[at].type !== "footnote_open") continue;
299
+ const id = tokens[at].meta.id;
300
+ const start = at + 1;
301
+ while (at < tokens.length && tokens[at].type !== "footnote_close") at++;
302
+ footnotes.set(id, tokens.slice(start, at));
303
+ }
304
+ }
305
+ return renderTokens(visible, { ...ctx, footnotes });
208
306
  }
209
307
 
210
308
  /**
@@ -293,6 +391,29 @@ function renderBlock(tokens, i, out, ctx) {
293
391
  out.push(`\n#${fn}(${items.map((it) => `[${it}]`).join(", ")})\n\n`);
294
392
  return end - i + 1;
295
393
  }
394
+ case "dl_open": {
395
+ const end = matching(tokens, i, "dl_open", "dl_close");
396
+ const entries = [];
397
+ let at = i + 1;
398
+ while (at < end) {
399
+ if (tokens[at].type !== "dt_open") {
400
+ at++;
401
+ continue;
402
+ }
403
+ const termEnd = matching(tokens, at, "dt_open", "dt_close");
404
+ const term = renderTokens(tokens.slice(at + 1, termEnd), ctx);
405
+ at = termEnd + 1;
406
+ const definitions = [];
407
+ while (at < end && tokens[at].type === "dd_open") {
408
+ const definitionEnd = matching(tokens, at, "dd_open", "dd_close");
409
+ definitions.push(renderTokens(tokens.slice(at + 1, definitionEnd), ctx));
410
+ at = definitionEnd + 1;
411
+ }
412
+ entries.push(`terms.item([${term}],[${definitions.join("\n\n")}])`);
413
+ }
414
+ out.push(`\n#terms(${entries.join(", ")})\n\n`);
415
+ return end - i + 1;
416
+ }
296
417
  case "table_open": {
297
418
  const end = matching(tokens, i, "table_open", "table_close");
298
419
  out.push(renderTable(tokens.slice(i, end + 1), ctx));
@@ -429,6 +550,12 @@ function listItems(tokens, start, end, ctx) {
429
550
  return items;
430
551
  }
431
552
 
553
+ /** Caption text with the same inline Markdown handling as the surrounding prose. */
554
+ function captionMarkup(caption, ctx) {
555
+ const inline = ctx.md.parseInline(caption.text, {})[0];
556
+ return `${escapeTypst(caption.label)}: ${renderInline(inline, ctx)}`;
557
+ }
558
+
432
559
  /**
433
560
  * A markdown table as a Typst `#table`.
434
561
  *
@@ -502,11 +629,17 @@ function renderTable(tokens, ctx) {
502
629
  )}),`
503
630
  : "";
504
631
  const drawn = `#table(\n columns: ${columns},${alignment}${header}\n${body}\n)`;
632
+ const caption = ctx.caption;
633
+ const labelled =
634
+ caption ?
635
+ `#text(size: 7.6pt, style: "italic")[${captionMarkup(caption, ctx)}] <${sectionLabel(ctx.anchorPrefix, slugify(caption.id))}>\n${drawn}`
636
+ : drawn;
505
637
  // Wide content is given an explicit span rather than left to overflow the
506
638
  // measure: past three columns a table is set across the page, and
507
639
  // `book-wide` decides between a float and pages of its own by measuring it.
508
- if (columns > WIDE_TABLE_COLUMNS) return `\n#book-wide[\n${drawn}\n]\n\n`;
509
- return `\n${drawn}\n\n`;
640
+ if (columns > WIDE_TABLE_COLUMNS)
641
+ return `${caption ? "" : "\n#pagebreak(weak: true)\n"}\n#book-wide[\n${labelled}\n]\n\n`;
642
+ return `\n${labelled}\n\n`;
510
643
  }
511
644
 
512
645
  /**
@@ -586,6 +719,21 @@ function renderInline(token, ctx) {
586
719
  case "heroiclands_icon":
587
720
  out.push(renderIcon(child, ctx));
588
721
  break;
722
+ case "footnote_ref": {
723
+ const label = child.meta?.label ?? String(child.meta?.id ?? "");
724
+ const key = `${ctx.footnotePrefix}-${slugify(label)}`;
725
+ const body = ctx.footnotes?.get(child.meta?.id);
726
+ if (!body) {
727
+ out.push(escapeTypst(`[^${label}]`));
728
+ break;
729
+ }
730
+ if (ctx.footnoteState.seen.has(key)) out.push(`#footnote(<${key}>)`);
731
+ else {
732
+ ctx.footnoteState.seen.add(key);
733
+ out.push(`#footnote[${renderTokens(body, ctx)}] <${key}>`);
734
+ }
735
+ break;
736
+ }
589
737
  case "link_open": {
590
738
  const close = childMatching(children, i, "link_open", "link_close");
591
739
  const inner = renderInline({ children: children.slice(i + 1, close) }, ctx);
@@ -632,10 +780,7 @@ function childMatching(children, i, open, close) {
632
780
  * @returns {string} Typst markup.
633
781
  */
634
782
  function inlineRaw(content) {
635
- const text = String(content ?? "");
636
- const longest = (text.match(/`+/g) ?? []).reduce((n, run) => Math.max(n, run.length), 0);
637
- const ticks = "`".repeat(Math.max(1, longest + 1));
638
- return `${ticks}${text}${ticks}`;
783
+ return `#raw("${escapeTypstString(content)}", block: false)`;
639
784
  }
640
785
 
641
786
  /**
@@ -655,6 +800,8 @@ function inlineRaw(content) {
655
800
  function renderLink(href, inner, ctx) {
656
801
  const url = String(href ?? "");
657
802
  const fragment = /#([^?]*)/.exec(url)?.[1] ?? "";
803
+ if (url.startsWith("#") && fragment)
804
+ return `#link(<${sectionLabel(ctx.anchorPrefix, fragment)}>)[${inner}]`;
658
805
  const slug =
659
806
  url
660
807
  .replace(/[#?].*$/, "")
@@ -692,7 +839,7 @@ function renderLink(href, inner, ctx) {
692
839
  * the figure at the top of the fresh one, where nothing is above it to displace
693
840
  * it and nothing that follows it can print first.
694
841
  *
695
- * A `.full-width` image that **also states a `float:`** is asking for a float,
842
+ * A `.full-width` image that **also states a `float`** is asking for a float,
696
843
  * and keeps one — deferral is the honest consequence of the request.
697
844
  *
698
845
  * ## A float occupies the measure
@@ -716,15 +863,18 @@ function renderLink(href, inner, ctx) {
716
863
  */
717
864
  function renderImage(token, ctx) {
718
865
  const alt = token.content || token.attrGet?.("alt") || "";
719
- const caption = alt ? `[${escapeTypst(alt)}]` : "none";
866
+ const captionText = ctx.caption ? captionMarkup(ctx.caption, ctx) : escapeTypst(alt);
867
+ const caption = captionText ? `[${captionText}]` : "none";
868
+ const anchor =
869
+ ctx.caption ? ` <${sectionLabel(ctx.anchorPrefix, slugify(ctx.caption.id))}>` : "";
720
870
  const staged = ctx.images.get(token.attrGet?.("src") ?? "");
721
871
  if (!staged)
722
- return alt ?
723
- `\n#block(below: 0.6em)[#text(size: 7.6pt, style: "italic", fill: luma(45%))${caption}]\n\n`
872
+ return captionText ?
873
+ `\n#block(below: 0.6em)[#text(size: 7.6pt, style: "italic", fill: luma(45%))${caption}]${anchor}\n\n`
724
874
  : "";
725
875
 
726
876
  const size = BOOK_IMAGE_WIDTHS[token.meta?.size] ?? BOOK_IMAGE_WIDTHS.auto;
727
- const figure = `#book-image("${escapeTypstString(staged)}", requested: ${size}, caption: ${caption})`;
877
+ const figure = `#book-image("${escapeTypstString(staged)}", requested: ${size}, caption: ${caption})${anchor}`;
728
878
 
729
879
  const width = token.meta?.classes?.[0];
730
880
  const scope =
@@ -749,8 +899,10 @@ function renderImage(token, ctx) {
749
899
  function renderIcon(token, ctx) {
750
900
  const name = token?.meta?.name ?? "";
751
901
  const glyph = ctx.glyphs.get(name);
752
- if (!glyph) return escapeTypst(`:icon-${name}:`);
753
- return `#text(font: "${escapeTypstString(glyph.font)}")[\\u{${glyph.codepoint.toString(16)}}]`;
902
+ if (!glyph) return escapeTypst(`:icon ${name}:`);
903
+ const scale = ICON_SIZES[token?.meta?.attrs?.size]?.scale;
904
+ const size = scale ? `, size: ${scale}em` : "";
905
+ return `#text(font: "${escapeTypstString(glyph.font)}"${size})[\\u{${glyph.codepoint.toString(16)}}]`;
754
906
  }
755
907
 
756
908
  /**
@@ -964,11 +1116,11 @@ export function bookTypstPreamble() {
964
1116
  " }\n" +
965
1117
  "}",
966
1118
  "#let book-place-map(title, path) = {\n" +
967
- " page(columns: 1)[\n" +
1119
+ " page(columns: 1, flipped: true)[\n" +
968
1120
  " #set par(justify: false, first-line-indent: 0em)\n" +
969
1121
  ' #text(size: 18pt, weight: "bold", fill: book-head)[#title]\n' +
970
1122
  " #v(0.4cm)\n" +
971
- ' #image(path, width: 100%, height: book-text-height - 2cm, fit: "contain")\n' +
1123
+ ' #image(path, width: 100%, height: book-text-width - 2cm, fit: "contain")\n' +
972
1124
  " ]\n" +
973
1125
  "}",
974
1126
  // Wide content spans the page, and how it spans depends on how tall it
@@ -1122,6 +1274,7 @@ export function renderBook({
1122
1274
  out.push(
1123
1275
  `#set text(font: "${escapeTypstString(serif)}", size: 9.6pt, fill: book-ink, lang: "en")`,
1124
1276
  );
1277
+ out.push("#show footnote.entry: set text(size: 7.8pt)");
1125
1278
  out.push("#set par(justify: true, leading: 0.55em, first-line-indent: 1.2em)");
1126
1279
  // The first-line indent is what separates one paragraph of running prose
1127
1280
  // from the next, and inside a list item the marker already does that. An
@@ -1247,8 +1400,7 @@ export function renderBook({
1247
1400
  out.push(body);
1248
1401
  out.push("");
1249
1402
  }
1250
- const map = maps.get(entry.anchor);
1251
- if (map) {
1403
+ for (const map of maps.get(entry.anchor) ?? []) {
1252
1404
  out.push(
1253
1405
  `#book-place-map([${escapeTypst(map.title)}], "${escapeTypstString(map.path)}")`,
1254
1406
  );
@@ -217,8 +217,8 @@ function localNode(note) {
217
217
  /**
218
218
  * Read a fetched index entry into a node, or `null` where it takes no part.
219
219
  *
220
- * A fetched entry carries the structure its record stated and no figure, so
221
- * a dependency's place sits in the geography and counts nobody.
220
+ * A fetched entry carries its author's figure when the published index has
221
+ * one. An older index without a figure contributes no population.
222
222
  *
223
223
  * @param {string} canonical - The entry's address.
224
224
  * @param {object} entry - The entry.
@@ -234,6 +234,7 @@ export function foreignNode(canonical, entry) {
234
234
  type,
235
235
  subType: String(entry.subType ?? ""),
236
236
  title: String(entry.name ?? shortcode),
237
+ population: figureOf(entry.population),
237
238
  parents: shortcodesOf(entry.parents),
238
239
  domains: type === "affiliation" ? shortcodesOf(entry.domains) : [],
239
240
  };