@heroiclands/package-build 20.7.0 → 21.0.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 (63) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/CONTENT.md +134 -44
  3. package/bin/content-build.mjs +37 -5
  4. package/bin/package-build.mjs +77 -0
  5. package/content-config.mjs +59 -1
  6. package/docs/api.md +149 -19
  7. package/docs/commands.md +75 -0
  8. package/docs/configuration.md +37 -10
  9. package/docs/content-format.md +450 -49
  10. package/engine/content-format.mjs +52 -3
  11. package/engine/content-images.mjs +699 -0
  12. package/engine/dependency-bump.mjs +218 -0
  13. package/engine/frontmatter-lint.mjs +89 -2
  14. package/engine/helpers.mjs +81 -142
  15. package/engine/index.mjs +15 -0
  16. package/engine/infobox-registry.mjs +81 -0
  17. package/engine/infobox-render.mjs +381 -0
  18. package/engine/infobox.mjs +963 -0
  19. package/engine/item-registry.mjs +5 -5
  20. package/engine/journals.mjs +22 -1
  21. package/engine/map-notes.mjs +11 -5
  22. package/engine/metadata-index.mjs +5 -0
  23. package/engine/note-vocabulary.mjs +57 -2
  24. package/engine/pathnames.mjs +374 -0
  25. package/engine/pdf-build.mjs +208 -9
  26. package/engine/pdf-render.mjs +461 -21
  27. package/engine/pdf-toc.mjs +77 -5
  28. package/engine/scenes.mjs +2 -1
  29. package/engine/site-build.mjs +106 -7
  30. package/engine/site-index.mjs +93 -4
  31. package/engine/wikilinks.mjs +93 -0
  32. package/hm3/default-item-art.mjs +14 -15
  33. package/hm3/index.mjs +3 -0
  34. package/hm3/infobox.mjs +64 -0
  35. package/package.json +1 -1
  36. package/sohl/default-item-art.mjs +18 -16
  37. package/sohl/index.mjs +3 -0
  38. package/sohl/infobox.mjs +499 -0
  39. package/types/content-config.d.mts +7 -0
  40. package/types/engine/content-format.d.mts +36 -0
  41. package/types/engine/content-images.d.mts +281 -0
  42. package/types/engine/dependency-bump.d.mts +89 -0
  43. package/types/engine/frontmatter-lint.d.mts +23 -0
  44. package/types/engine/helpers.d.mts +30 -72
  45. package/types/engine/index.d.mts +5 -0
  46. package/types/engine/infobox-registry.d.mts +36 -0
  47. package/types/engine/infobox-render.d.mts +87 -0
  48. package/types/engine/infobox.d.mts +443 -0
  49. package/types/engine/item-registry.d.mts +5 -5
  50. package/types/engine/journals.d.mts +9 -1
  51. package/types/engine/note-vocabulary.d.mts +51 -0
  52. package/types/engine/pathnames.d.mts +189 -0
  53. package/types/engine/pdf-build.d.mts +46 -0
  54. package/types/engine/pdf-render.d.mts +99 -1
  55. package/types/engine/pdf-toc.d.mts +10 -5
  56. package/types/engine/site-build.d.mts +11 -3
  57. package/types/engine/site-index.d.mts +35 -3
  58. package/types/engine/wikilinks.d.mts +22 -0
  59. package/types/hm3/default-item-art.d.mts +5 -6
  60. package/types/hm3/index.d.mts +1 -0
  61. package/types/hm3/infobox.d.mts +22 -0
  62. package/types/sohl/index.d.mts +1 -0
  63. package/types/sohl/infobox.d.mts +145 -0
@@ -474,6 +474,10 @@ export function publishesContentPages(config) {
474
474
  *
475
475
  * @typedef {object} RelationshipSpec
476
476
  * @property {string} id The other package's id.
477
+ * @property {string} [contentPackage] What the other package's content is
478
+ * called, where that differs from its Foundry
479
+ * id. It is the name a note writes when it
480
+ * addresses a file that package ships.
477
481
  * @property {string} [type] `system`, `module`, or `world`.
478
482
  * @property {string} [manifest] Where its manifest is published.
479
483
  * @property {CompatibilitySpec} [compatibility] The version range of *that*
@@ -712,6 +716,7 @@ const DOCS_KEYS = ["itemFields"];
712
716
  const SITE_KEYS = [
713
717
  "out",
714
718
  "base",
719
+ "assets",
715
720
  "packages",
716
721
  "sections",
717
722
  "readmeSections",
@@ -728,7 +733,14 @@ const EMPTY_PDF_FONTS = Object.freeze({ serif: "", sans: "", mono: "", path: ""
728
733
  const SECTION_META_KEYS = ["title", "banner", "description", "listType", "listSubType"];
729
734
  const DOC_PAGE_KEYS = ["title", "out", "preamble"];
730
735
  const RELATIONSHIP_KINDS = ["systems", "requires", "recommends", "conflicts"];
731
- const RELATIONSHIP_KEYS = ["id", "type", "manifest", "compatibility", "itemCatalog"];
736
+ const RELATIONSHIP_KEYS = [
737
+ "id",
738
+ "contentPackage",
739
+ "type",
740
+ "manifest",
741
+ "compatibility",
742
+ "itemCatalog",
743
+ ];
732
744
  const ITEM_BUILDER_KEYS = ["system", "img", "fields"];
733
745
  const ITEM_REGISTRY_KEYS = ["system", "builders"];
734
746
  const PACK_KEYS = [
@@ -1414,6 +1426,38 @@ function normalizeSectionMeta(value, where) {
1414
1426
  return Object.freeze(out);
1415
1427
  }
1416
1428
 
1429
+ /**
1430
+ * The asset host the website resolves a pathname against.
1431
+ *
1432
+ * The one address in this configuration that is not this repository's own. A
1433
+ * note names a file by the package that owns it and the path inside that
1434
+ * package's `assets/`, and the website serves every package's files from one
1435
+ * host — so the host is the missing half of a web address, and it is written
1436
+ * here because a build has no way to find it out.
1437
+ *
1438
+ * **Absolute, and with no trailing slash.** The forms are joined with a single
1439
+ * `/`, so a trailing one would double it; it is trimmed rather than refused,
1440
+ * because a doubled slash is the sort of thing a reader's eye slides past. A
1441
+ * relative value is refused outright: it would resolve against each page's own
1442
+ * URL, which is the failure the key exists to remove.
1443
+ *
1444
+ * @param {unknown} value - The configured value, or `undefined`.
1445
+ * @returns {string} The host, without its trailing slash; `""` when unset.
1446
+ */
1447
+ function normalizeSiteAssets(value) {
1448
+ if (value === undefined) return "";
1449
+ const assets = requireNonEmptyString(value, "site.assets");
1450
+ if (!/^https?:\/\/[^/]+/.test(assets)) {
1451
+ fail(
1452
+ "site.assets",
1453
+ "must be an absolute `http://` or `https://` address — it is the host " +
1454
+ "every package's imagery is served from, and a relative value " +
1455
+ "resolves against whichever page happens to carry the image",
1456
+ );
1457
+ }
1458
+ return assets.replace(/\/+$/, "");
1459
+ }
1460
+
1417
1461
  /**
1418
1462
  * A map of section name → landing metadata.
1419
1463
  *
@@ -1447,6 +1491,7 @@ function normalizeSite(value) {
1447
1491
  const empty = Object.freeze({
1448
1492
  out: "",
1449
1493
  base: "",
1494
+ assets: "",
1450
1495
  packages: Object.freeze([]),
1451
1496
  sections: Object.freeze({}),
1452
1497
  readmeSections: Object.freeze({}),
@@ -1502,6 +1547,7 @@ function normalizeSite(value) {
1502
1547
  return Object.freeze({
1503
1548
  out: input.out === undefined ? "" : requireNonEmptyString(input.out, "site.out"),
1504
1549
  base: input.base === undefined ? "" : requireNonEmptyString(input.base, "site.base"),
1550
+ assets: normalizeSiteAssets(input.assets),
1505
1551
  packages: Object.freeze(packages),
1506
1552
  sections: normalizeSectionMap(input.sections, "site.sections"),
1507
1553
  readmeSections: normalizeSectionMap(input.readmeSections, "site.readmeSections"),
@@ -1814,6 +1860,18 @@ function normalizeRelationships(value) {
1814
1860
  const spec = {
1815
1861
  id: requireNonEmptyString(rel.id, `${at}.id`),
1816
1862
  };
1863
+ // What the other package's *content* is called, where that
1864
+ // differs from its Foundry id. A note addresses a file by the
1865
+ // content package that owns it — `thalorna/assets/…` — and the
1866
+ // Foundry id (`sohl-thalorna`) appears only in the install
1867
+ // path this derives. Omitted where the two are the same word,
1868
+ // which they are for every system.
1869
+ if (rel.contentPackage !== undefined) {
1870
+ spec.contentPackage = requireNonEmptyString(
1871
+ rel.contentPackage,
1872
+ `${at}.contentPackage`,
1873
+ );
1874
+ }
1817
1875
  for (const key of ["type", "manifest"]) {
1818
1876
  if (rel[key] !== undefined) {
1819
1877
  spec[key] = requireNonEmptyString(rel[key], `${at}.${key}`);
package/docs/api.md CHANGED
@@ -338,6 +338,8 @@ The closed half of a note's frontmatter: the `data:` container and each type's `
338
338
  | `NOTE_VOCABULARY` | `const NOTE_VOCABULARY` | — | looking up, per content type, its `data:` field list and its closed `subType` values |
339
339
  | `DECLARED_TAGS` | `const DECLARED_TAGS` | — | looking up the tags a note type may declare, grouped |
340
340
  | `declaredTags` | `declaredTags(type, groups)` | `readonly string[]` | reading the declared tags a note of a type may carry, flattened |
341
+ | `applicableTagGroups` | `applicableTagGroups(type, groups)` | `object[]` | reading the declared tag groups that apply to a note type |
342
+ | `exclusiveTagGroups` | `exclusiveTagGroups(type, groups)` | `Array<{slot, tags}>` | reading the single-valued tag slots a note type has, such as a being's kind |
341
343
  | `hasTag` | `hasTag(fm, tag)` | `boolean` | checking whether a note carries a given tag, whatever scalar-or-list form it was authored in |
342
344
  | `isDraftNote` | `isDraftNote(fm)` | `boolean` | checking whether a note is tagged as an unfinished draft |
343
345
  | `subTypeCharsetMessage` | `subTypeCharsetMessage(value)` | `string` | building the message for a `subType` outside the address charset |
@@ -346,6 +348,66 @@ The closed half of a note's frontmatter: the `data:` container and each type's `
346
348
  | `dataFields` | `dataFields(type, vocabulary)` | `readonly DataFieldSpec[] \| undefined` | looking up the `data:` keys a note type may carry |
347
349
  | `subTypes` | `subTypes(type, vocabulary)` | `readonly string[] \| null \| undefined` | looking up the closed `subType` values a note type declares |
348
350
 
351
+ ### `engine.infobox`
352
+
353
+ The declared infobox: what a note's summary panel holds, decided once and rendered by each medium. The note box's fields are the type's own `data:` vocabulary, in its declared order, so a key added to a type appears everywhere with no second edit; the overlay declares only the label and the handful of keys that carry no row.
354
+
355
+ | Export | Signature | Returns | Use it when |
356
+ | ------------------------- | -------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------- |
357
+ | `INFOBOX_LAYOUTS` | `const INFOBOX_LAYOUTS` | — | looking up the four section layouts and the property each carries its content in |
358
+ | `INFOBOX_VALUE_KINDS` | `const INFOBOX_VALUE_KINDS` | — | enumerating what a row's value may be |
359
+ | `DURATION_LABELS` | `const DURATION_LABELS` | — | naming a duration pair — the roll and the flat number of seconds — in either overlay |
360
+ | `GEAR_UNITS` | `const GEAR_UNITS` | — | naming a gear item's price and weight, and the unit each carries, in either overlay |
361
+ | `UNSET_VALUES` | `const UNSET_VALUES` | — | enumerating the words a corpus writes when it means "there is nothing here" |
362
+ | `applyUnit` | `applyUnit(kind, value, unit)` | `object` | putting a declared unit on a row's value, where the quantity is |
363
+ | `NOT_AVAILABLE` | `const NOT_AVAILABLE` | — | naming what a mapped system that produced no document says |
364
+ | `NOTHING_BEYOND_PROFILE` | `const NOTHING_BEYOND_PROFILE` | — | naming what a system holding nothing the note box has not shown says |
365
+ | `NOTE_BOX_ID` | `const NOTE_BOX_ID` | — | naming the note infobox, which is not a system id |
366
+ | `NOTE_BOX_TITLE` | `const NOTE_BOX_TITLE` | — | naming the note infobox's heading |
367
+ | `NOTE_SECTION_ID` | `const NOTE_SECTION_ID` | — | naming the note infobox's single section |
368
+ | `NOTE_FIELD_PRESENTATION` | `const NOTE_FIELD_PRESENTATION` | — | looking up a `data:` key's label, the group it composes into, or why it carries no row |
369
+ | `assertInfoboxSet` | `assertInfoboxSet(boxes, fm, options)` | `readonly object[]`, throws | refusing a page that carries anything but the boxes its type maps to |
370
+ | `buildInfoboxes` | `buildInfoboxes(fm, options)` | `object[]` | building every box a note carries, against a given set of maps and declarations |
371
+ | `defineInfobox` | `defineInfobox(declaration)` | `object` | declaring one system's half of the infobox |
372
+ | `hasRenderableValue` | `hasRenderableValue(kind, value)` | `boolean` | deciding whether a built value is worth a row |
373
+ | `hasValue` | `hasValue(value)` | `boolean` | deciding whether an authored value is worth a row |
374
+ | `humanizeFieldName` | `humanizeFieldName(name)` | `string` | turning a declared key into the label a reader sees |
375
+ | `humanizeValue` | `humanizeValue(value)` | `string` | turning an authored value into readable text |
376
+ | `isDeclaredDefault` | `isDeclaredDefault(field, raw)` | `boolean` | deciding whether a value is the one the field's own declaration would have supplied |
377
+ | `isUnsetSentinel` | `isUnsetSentinel(value)` | `boolean` | deciding whether a value is a word meaning "nothing here" rather than a value |
378
+ | `linkValue` | `linkValue(ref, resolve, hint)` | `object` | resolving one reference into a `link` value |
379
+ | `noteInfobox` | `noteInfobox(fm, options)` | `object` | building the note box alone, from the type's `data:` vocabulary |
380
+ | `overlayFor` | `overlayFor(presentation, type, name)` | `object` | reading a field's overlay entry, preferring the `<type>.<field>` key over the bare one |
381
+ | `presentValue` | `presentValue(value)` | `string` | showing a value in a row, capitalising an enumerated one and leaving prose as written |
382
+ | `requiredInfoboxIds` | `requiredInfoboxIds(fm, options)` | `string[]` | asking which boxes a note's type maps to |
383
+ | `sectionHolds` | `sectionHolds(section)` | `boolean` | deciding whether a section holds anything a medium would draw |
384
+ | `systemRowsSection` | `systemRowsSection(fm, fields, ctx)` | `object[]` | building the rows a type's own field declaration yields |
385
+ | `valueKindOf` | `valueKindOf(field, value)` | `string` | reading the value kind a field declaration implies |
386
+
387
+ ### `engine.infoboxRegistry`
388
+
389
+ The infobox declarations this toolchain ships, one per system, and the single call each medium makes to build a note's boxes.
390
+
391
+ | Export | Signature | Returns | Use it when |
392
+ | ----------------- | ---------------------------- | --------------------- | ------------------------------------------------------------------------------ |
393
+ | `KNOWN_INFOBOXES` | `const KNOWN_INFOBOXES` | — | enumerating every system's infobox declaration, in the order a page shows them |
394
+ | `infoboxFor` | `infoboxFor(system)` | `object \| undefined` | looking up one system's declaration by its id |
395
+ | `noteInfoboxes` | `noteInfoboxes(fm, options)` | `object[]`, throws | building every box one note carries, wired to the shipped registries |
396
+
397
+ ### `engine.infoboxRender`
398
+
399
+ Drawing a declared infobox: HTML for a Foundry Journal Page, Typst for the book. The website's boxes travel in front matter and the site theme draws them, so there is no renderer for it here.
400
+
401
+ | Export | Signature | Returns | Use it when |
402
+ | ---------------------- | ------------------------------------- | --------- | ------------------------------------------------------------------------ |
403
+ | `infoboxTypstPreamble` | `infoboxTypstPreamble()` | `string` | emitting the panel definitions the book's bodies call, once per document |
404
+ | `infoboxesToHtml` | `infoboxesToHtml(boxes, options)` | `string` | drawing the boxes as `<details>` disclosures, open by default |
405
+ | `infoboxesToTypst` | `infoboxesToTypst(boxes, options)` | `string` | drawing the boxes as panels that break between their sections |
406
+ | `linkToHtml` | `linkToHtml(value)` | `string` | drawing a link value as an anchor on its URL |
407
+ | `linkToTypst` | `linkToTypst(value, links, labelFor)` | `string` | drawing a link value as a cross-reference into the book |
408
+ | `linkToUuid` | `linkToUuid(value)` | `string` | drawing a link value as a Foundry document reference |
409
+ | `sectionHasContent` | `sectionHasContent(section)` | `boolean` | deciding whether a section holds anything worth drawing |
410
+
349
411
  ### `engine.systems`
350
412
 
351
413
  The closed registry of system ids, and the `none` that stands for no system at all. An unknown system value is an error; adding a system is a data change to this registry rather than a hardcoded set scattered through the pipeline.
@@ -563,6 +625,41 @@ Raw HTML in a note's prose, reported. **A note is markdown.** What markdown cann
563
625
  | `checkHtml` | `function checkHtml(body, file,` | {Array<{file: string, line: number, column: number, severity: "warning", message: string}>} One finding per tag, in source order. | Every raw HTML tag in one note's body. |
564
626
  | `lintContentHtml` | `function lintContentHtml(contentBase,` | {{findings: Array<{file: string, line: number, column: number, severity: "warning", message: string}>, files: number}} The findings, and how many files were read. | Walk a content tree and report raw HTML in every note's prose. |
565
627
 
628
+ ### `engine.contentImages`
629
+
630
+ An image saying how wide it is and where it sits. A markdown image carries no indication of either, so each of the three surfaces decides for itself and the author — who is the one who knows — has no way to say. A directive in the curly-attribute convention Pandoc and Kramdown use closes that, in two closed vocabularies: a width class, and a `float:` position.
631
+
632
+ | Export | Signature | Returns | Use it when |
633
+ | --------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
634
+ | `IMAGE_CLASSES` | `const IMAGE_CLASSES` | — | The width classes an image may carry, and what each means to a renderer. |
635
+ | `IMAGE_FLOATS` | `const IMAGE_FLOATS` | — | The `float:` positions an image may take, and where each puts it. |
636
+ | `IMAGE_FIGURE_CLASS` | `const IMAGE_FIGURE_CLASS` | — | The class every figure carries, whatever its width or position. |
637
+ | `IMAGE_PATTERN` | `const IMAGE_PATTERN` | — | A markdown image, with the directive it may carry. |
638
+ | `imageSourceProblem` | `function imageSourceProblem(src)` | {string} The problem, as a finding's sentence, or `""`. | What is wrong with an image's address, or `""` when nothing is. |
639
+ | `parseImageDirective` | `function parseImageDirective(raw)` | {{classes: string[], float: string, problems: string[]}} What was written, and what cannot be honoured. | Read the directive on an image. |
640
+ | `figureClasses` | `function figureClasses(directive)` | {string} A space-separated class list. | The classes a figure carries, from a parsed directive. |
641
+ | `escapeHtml` | `function escapeHtml(text)` | {string} The same value, safe in markup. | Text going inside an HTML attribute or between tags. |
642
+ | `imageFigureHtml` | `function imageFigureHtml(image)` | {string} The figure, as one HTML block. | One image as the `<figure>` both HTML surfaces render. |
643
+ | `standsAlone` | `function standsAlone(text, start, end)` | {boolean} Whether the match is a block of its own. | Whether a match sits alone in its own paragraph. |
644
+ | `imagesIn` | `function imagesIn(body)` | {Array<{alt: string, src: string, title: string, directive: string, index: number, length: number, block: boolean}>} One entry per image. | Every image in one body, with its directive and its position. |
645
+ | `imageSourcesIn` | `function imageSourcesIn(body)` | {string[]} The addresses, with repeats. | Every image address one body names, in order of appearance. |
646
+ | `checkImages` | `function checkImages(body, file,` | {Array<{file: string, line: number, column: number\|undefined, severity: "error", message: string}>} One finding per defect, in source order. | Every defect in one note's images. |
647
+ | `lintContentImages` | `function lintContentImages(contentBase,` | {{findings: Array<{file: string, line: number, column: number\|undefined, severity: "error", message: string}>, files: number}} What it found. | Walk a content tree and report every image it cannot render as authored. |
648
+ | `renderImageFigures` | `function renderImageFigures(body, resolveSrc)` | {string} The same body, with each block image as a `<figure>`. | Rewrite every block image in a body into the figure the website publishes. |
649
+ | `imagePlugin` | `function imagePlugin()` | {(md: object) => void} A markdown-it plugin. | A markdown-it plugin that reads an image's directive and renders its figure. |
650
+
651
+ ### `engine.pathnames`
652
+
653
+ One authored pathname, and the four addresses it resolves to. A note names a file once — in `img:`, in `data.portrait:`, in the body of a markdown image — and the first segment says which package owns it when an `assets/` follows. Every surface derives its own address from that one statement: the path inside a Foundry install, the file in the owning repository's tree, the address the website serves, and where the book stages its copy.
654
+
655
+ | Export | Signature | Returns | Use it when |
656
+ | ------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
657
+ | `ASSETS_SEGMENT` | `const ASSETS_SEGMENT` | — | The directory a package ships its files in. |
658
+ | `PATHNAME_SURFACES` | `const PATHNAME_SURFACES` | — | The surfaces one authored pathname resolves for. |
659
+ | `pathnameProblem` | `function pathnameProblem(raw)` | {string} The problem, as a finding's sentence, or `""`. | What is wrong with an authored pathname, or `""` when nothing is. |
660
+ | `packageAddresses` | `function packageAddresses(config)` | {Map<string, {root: string\|null, id: string\|null, own: boolean}>} The packages, by package name. | Every content package this build can resolve a pathname against. |
661
+ | `resolvePathname` | `function resolvePathname(raw, config)` | {PathnameForms\|null} The four forms, or `null` when the note names no file. | Resolve one authored pathname into the address each surface serves. |
662
+
566
663
  ### `engine.contentLinks`
567
664
 
568
665
  Resolving every link in a content tree, and reporting the ones that land nowhere. Three link defects survive both content builds silently, so neither the pack compilers nor a site build catches them:
@@ -672,6 +769,7 @@ Wikilink resolution for the pack compilers. Content notes link to one another wi
672
769
  | `readQualifier` | `function readQualifier(target, types, packages)` | {{type: string, shortcode: string, itemDoc: boolean, package?: string, system?: string, reason?: undefined} \| {reason: "unknown-type"} \| null} The resolved qualifier; a `reason` when the target is definitely qualified but names no known type; or `null` when it is not an address at all. | Read a link target as a **qualified** `type-shortcode` reference, or report that it does not parse as one. |
673
770
  | `anchorPageId` | `function anchorPageId(noteId, anchorSlug)` | {string} A 16-character alphanumeric id. | The deterministic JournalEntryPage id for one anchor: SHA-256 of `"<noteId>-<anchorSlug>"`, base64-encoded, reduced to the 16 alphanumeric characters a Foundry id allows. |
674
771
  | `buildWikilinkIndex` | `function buildWikilinkIndex(docs, packageId, foreign, contentPackage)` | {{byShortcode: Map<string, object>, types: Set<string>}} `types` is every type the tree actually contains, so a qualifier naming no real type can be told apart from a missing target. | Builds the link-resolution tables for a content tree. |
772
+ | `resolveReference` | `function resolveReference(index, ref, hint)` | {{name?: string, uuid?: string, address?: string, subType?: string}\|undefined} The target, or `undefined` where nothing answers. | Resolve one reference — a bare shortcode, a short address or a canonical one — to what a compendium can use. |
675
773
  | `convertWikilinks` | `function convertWikilinks(markdown,` | {{markdown: string, unresolved: Array<{link: string, target: string, offset: number, reason: string, packages?: string[], anchor?: string}>}} Each `reason` is one of {@link LINK_FINDING_REASONS}, the vocabulary all three resolvers share — `ambiguous` carries the claiming `packages` and `unknown-anchor` the section it named. | Rewrites every wikilink in a markdown body as a Foundry UUID enricher. |
676
774
 
677
775
  ### `engine.wikilinkSyntax`
@@ -694,10 +792,11 @@ What a `[[…]]` **is**, before anything decides where it points. One authored l
694
792
 
695
793
  **The address index a site build resolves its wikilinks against.** Every consumer that publishes a content tree as a website has to answer the same question — given `[[Something]]`, which page? — and every one of them answered it with its own copy of the same 150 lines. `sohl`'s and `sohl-thalorna`'s site builds still share 147 identical lines of it, comments and indentation aside. This is that shared half, lifted out whole.
696
794
 
697
- | Export | Signature | Returns | Use it when |
698
- | ---------------- | ---------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------- |
699
- | `buildSiteIndex` | `function buildSiteIndex(entries,` | {SiteIndex} The index, and what could not be addressed unambiguously. | Build the address index a site's wikilink resolver reads. |
700
- | `wikiContext` | `function wikiContext(built,` | {object} The resolver context. | The per-page context a wikilink resolver takes. |
795
+ | Export | Signature | Returns | Use it when |
796
+ | ------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
797
+ | `buildSiteIndex` | `function buildSiteIndex(entries,` | {SiteIndex} The index, and what could not be addressed unambiguously. | Build the address index a site's wikilink resolver reads. |
798
+ | `wikiContext` | `function wikiContext(built,` | {object} The resolver context. | The per-page context a wikilink resolver takes. |
799
+ | `resolveInfoboxRef` | `function resolveInfoboxRef(siteIndex, ref, hint)` | {{name?: string, url?: string, address?: string, subType?: string}\|undefined} The page, or `undefined` where nothing answers. | Resolve one infobox reference — a bare shortcode, a short address or a canonical one — against a site index. |
701
800
 
702
801
  ### `engine.pdfToc`
703
802
 
@@ -714,16 +813,17 @@ The document tree a PDF is built from, and the plan it resolves to (#316). The p
714
813
 
715
814
  A note's markdown, and a document plan, rendered as Typst source. **This module emits text and reads nothing.** It takes markdown and a plan and returns a `.typ` document; the filesystem, the note bodies and the compiler that turns the result into a PDF all live in {@link module:engine/pdf-build}. That split is what lets the outline, the table of contents, every anchor and every link destination be asserted in a unit test with no renderer installed — which is most of what a book has to get right, and all of what a test can check without eyes.
716
815
 
717
- | Export | Signature | Returns | Use it when |
718
- | ----------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------ |
719
- | `escapeTypst` | `function escapeTypst(text)` | {string} The same text, inert. | Escape literal text for Typst markup. |
720
- | `escapeTypstString` | `function escapeTypstString(text)` | {string} The same value, quotable. | Escape a string going inside Typst string quotes, as a `#link` URL does. |
721
- | `labelFor` | `function labelFor(anchor)` | {string} A Typst label name. | A Typst label, from a plan anchor. |
722
- | `createParser` | `function createParser(registry)` | {object} A markdown-it instance. | A markdown-it configured to parse, not to render. |
723
- | `markdownToTypst` | `function markdownToTypst(markdown, opts` | {string} Typst markup. | Render markdown as Typst content. |
724
- | `renderBook` | `renderBook({ plan, bodies, title, subtitle, front, fonts, version })` | {string} A complete `.typ` document. | The whole book, as one Typst document. |
725
- | `resolveDanglingLabels` | `function resolveDanglingLabels(source, findings` | {string} The same document, with no reference left dangling. | Point every internal link at a label the document actually declares. |
726
- | `iconNamesIn` | `function iconNamesIn(markdown)` | {string[]} The names, in order of appearance, with repeats. | Every icon name a body uses, so a build can resolve them once. |
816
+ | Export | Signature | Returns | Use it when |
817
+ | ----------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------ |
818
+ | `escapeTypst` | `function escapeTypst(text)` | {string} The same text, inert. | Escape literal text for Typst markup. |
819
+ | `escapeTypstString` | `function escapeTypstString(text)` | {string} The same value, quotable. | Escape a string going inside Typst string quotes, as a `#link` URL does. |
820
+ | `labelFor` | `function labelFor(anchor)` | {string} A Typst label name. | A Typst label, from a plan anchor. |
821
+ | `createParser` | `function createParser(registry)` | {object} A markdown-it instance. | A markdown-it configured to parse, not to render. |
822
+ | `markdownToTypst` | `function markdownToTypst(markdown, opts` | {string} Typst markup. | Render markdown as Typst content. |
823
+ | `renderBook` | `renderBook({ plan, bodies, title, subtitle, front, fonts, version, preamble, banners })` | {string} A complete `.typ` document. | The whole book, as one Typst document. |
824
+ | `resolveDanglingLabels` | `function resolveDanglingLabels(source, findings` | {string} The same document, with no reference left dangling. | Point every internal link at a label the document actually declares. |
825
+ | `iconNamesIn` | `function iconNamesIn(markdown)` | {string[]} The names, in order of appearance, with repeats. | Every icon name a body uses, so a build can resolve them once. |
826
+ | `bookTypstPreamble` | `function bookTypstPreamble()` | {string} Typst markup. | The Typst definitions the book's page furniture is drawn with. |
727
827
 
728
828
  ### `engine.pdfFonts`
729
829
 
@@ -739,11 +839,13 @@ Which glyph an icon name resolves to, read from the font that carries it. {@link
739
839
 
740
840
  The content tree, built into a book. The I/O half of the PDF surface: it reads the configuration, the document tree and the notes, drives the passes the site build already owns, hands the result to {@link module:engine/pdf-render} and runs Typst over what comes back. Everything about _what the book says_ is decided in the pure half; this module is where the filesystem and the compiler live.
741
841
 
742
- | Export | Signature | Returns | Use it when |
743
- | -------------- | --------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------- |
744
- | `pdfFileName` | `function pdfFileName(artifact, version)` | {string} `<artifact>-<version>.pdf`, or `<artifact>.pdf` unversioned. | The file name a downloaded book identifies itself by. |
745
- | `buildPdf` | `async buildPdf({ config, out, version, compile })` | {Promise<object>} `{ built, reason, findings, typ, pdf, stats }`. | Build the book. |
746
- | `compileTypst` | `function compileTypst(typPath, pdfPath, pdf` | {{ok: boolean, message: string}} What happened. | Run Typst over the emitted source. |
842
+ | Export | Signature | Returns | Use it when |
843
+ | ----------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
844
+ | `pdfFileName` | `function pdfFileName(artifact, version)` | {string} `<artifact>-<version>.pdf`, or `<artifact>.pdf` unversioned. | The file name a downloaded book identifies itself by. |
845
+ | `buildPdf` | `async buildPdf({ config, out, version, compile })` | {Promise<object>} `{ built, reason, findings, typ, pdf, stats }`. | Build the book. |
846
+ | `stagedImagePath` | `function stagedImagePath(src, config)` | {{from: string, to: string}\|null} The file, and where under the output directory it is staged. | The file on disk an authored image address names, or `null`. |
847
+ | `stageBanners` | `function stageBanners(entries, config, outDir, findings)` | {Map<string, string>} Declared path → the staged file's path, relative to the `.typ`. | Copy every banner the document tree names into the output directory. |
848
+ | `compileTypst` | `function compileTypst(typPath, pdfPath, pdf` | {{ok: boolean, message: string}} What happened. | Run Typst over the emitted source. |
747
849
 
748
850
  ### `engine.baseCompiler`
749
851
 
@@ -940,6 +1042,24 @@ SoHL's Actor pass — what a SoHL `being` document holds and nothing else: the b
940
1042
  | -------- | -------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
941
1043
  | `Actors` | `class Actors` | — | not called directly — imported and driven by `engine/generate.mjs`; must run after the items passes, since it reads their generated JSON trees |
942
1044
 
1045
+ ### `sohl.infobox`
1046
+
1047
+ Which of SoHL's facts a note's summary panel carries, and how they group. Read off the same field declaration the compiler obeys, with a builder of its own only where a box is derived rather than read field by field.
1048
+
1049
+ | Export | Signature | Returns | Use it when |
1050
+ | ------------------------- | ------------------------------- | --------------------- | ----------------------------------------------------------------------------------- |
1051
+ | `SOHL_INFOBOX` | `const SOHL_INFOBOX` | — | reading SoHL's infobox declaration |
1052
+ | `SOHL_INFOBOX_TITLE` | `const SOHL_INFOBOX_TITLE` | — | naming SoHL's box |
1053
+ | `SOHL_FIELD_PRESENTATION` | `const SOHL_FIELD_PRESENTATION` | — | looking up what one of SoHL's fields is called, or why it carries no row |
1054
+ | `PROTECTION_FIELDS` | `const PROTECTION_FIELDS` | — | reading the declarations of the aspects armour is rated against, in the order shown |
1055
+ | `UNSTATED` | `const UNSTATED` | — | naming what a strike mode shows where a value was not stated |
1056
+ | `armorSections` | `armorSections(fm, ctx)` | `object[]` | building armour's box, protection included |
1057
+ | `beingSections` | `beingSections(fm, ctx)` | `object[]` | building a being's attributes, skills, mystical abilities and equipment |
1058
+ | `decodeItem` | `decodeItem(entry)` | `object \| undefined` | reading what one `sohl.items` entry names, whichever form it was written in |
1059
+ | `projectileSections` | `projectileSections(fm, ctx)` | `object[]` | building a projectile's box, its impact composed into one row |
1060
+ | `strikeModes` | `strikeModes(declared)` | `[string, object][]` | reading a weapon's strike modes, whichever of the two shapes were authored |
1061
+ | `weaponSections` | `weaponSections(fm, ctx)` | `object[]` | building a weapon's box, strike modes included |
1062
+
943
1063
  ### `sohl.kbPasses`
944
1064
 
945
1065
  The `sohl` knowledgebase's own body passes: two rewrites driven by a TypeDoc symbol map and a repository layout only this package has, named from `site.passOptions` the same way an asset transform is named from configuration. Neither rewrite ever fails a build — an unresolved `{@link}` degrades to a code span, and a relative link outside the documentation tree becomes a GitHub blob URL — but building the bundle from a misconfigured `symbolMap` fails loudly before any page renders.
@@ -1002,6 +1122,16 @@ HM3's note-type → document-subtype map. Unlike SoHL's near-identity map, HM3's
1002
1122
  | `HM3_TYPE_KEY` | `const HM3_TYPE_KEY` | — | reading the frontmatter key (`hm3.type`) inside the `hm3:` block that resolves a one-to-many content type to its HM3 document subtype |
1003
1123
  | `HM3_DOCUMENT_SUBTYPES` | `const HM3_DOCUMENT_SUBTYPES` | — | looking up which Foundry document and subtype a content type compiles into under HM3; a type absent from this map compiles into no HM3 document at all |
1004
1124
 
1125
+ ### `hm3.infobox`
1126
+
1127
+ Which of HM3's facts a note's summary panel carries, read off the same field list the item builders obey.
1128
+
1129
+ | Export | Signature | Returns | Use it when |
1130
+ | ------------------------ | ------------------------------ | ------- | --------------------------------------------- |
1131
+ | `HM3_INFOBOX` | `const HM3_INFOBOX` | — | reading HM3's infobox declaration |
1132
+ | `HM3_INFOBOX_TITLE` | `const HM3_INFOBOX_TITLE` | — | naming HM3's box |
1133
+ | `HM3_FIELD_PRESENTATION` | `const HM3_FIELD_PRESENTATION` | — | looking up what one of HM3's fields is called |
1134
+
1005
1135
  ### `hm3.items`
1006
1136
 
1007
1137
  HM3's Item pass — the parts of compiling a note into an HM3 Item that are facts about HM3: the `description` key rendered from a note's `{#appearance}` section (the one HM3 Item field with nowhere else to put it), and the `flags.hm3.templatePriority` flag HM3's data model has no field for. There is no HM3 equivalent of SoHL's `docHtml` — the prose still compiles into its JournalEntry, it just isn't addressed from the item.
package/docs/commands.md CHANGED
@@ -437,6 +437,81 @@ package-build: registry and §3 agree (11 labels).
437
437
 
438
438
  None.
439
439
 
440
+ ### `package-build bump [packages..]`
441
+
442
+ **NAME**
443
+
444
+ Take a newer version of a dependency, keeping the lockfile's formatting.
445
+
446
+ **SYNOPSIS**
447
+
448
+ ```
449
+ package-build bump [packages..] [--tag <tag>] [--check]
450
+ ```
451
+
452
+ **DESCRIPTION**
453
+
454
+ Takes the newest published version of one or more declared dependencies,
455
+ updating `package-lock.json` — and `package.json` when the declared range has to
456
+ move — then restoring both files to the indentation they already used.
457
+
458
+ npm performs the resolution, so a version whose dependency set differs from the
459
+ one it replaces is handled as correctly as one that moves three lines. Editing
460
+ those three lines by hand is only right while the two dependency sets are
461
+ identical, and nothing tells the author when they are not.
462
+
463
+ The indentation is the reason this exists rather than `npm install` being run
464
+ directly. Every repository consuming this toolchain writes `package-lock.json`
465
+ with four spaces and prettier-ignores it; npm rewrites it with two, turning a
466
+ three-line version change into a whole-file reformat no reviewer can read past.
467
+
468
+ Named no packages, it takes every `@heroiclands/*` dependency the manifest
469
+ declares. That scope is the one a person bumps by hand — a first-party release
470
+ is taken the moment it publishes, usually to unblock the change that prompted
471
+ it, while third-party updates arrive from Dependabot on their own schedule.
472
+
473
+ The version reported is the one the **lockfile** resolves, not the range the
474
+ manifest declares, because `npm ci` installs from the lockfile. Above 1.0 a
475
+ caret range usually already admits the new version, so the manifest does not
476
+ move and the lockfile is the whole change; below 1.0 a caret is locked to the
477
+ minor, so it does.
478
+
479
+ Writes no `node_modules`: install the result with `npm ci`.
480
+
481
+ **OPTIONS**
482
+
483
+ | Option | Type | Default | Description |
484
+ | --------- | ------- | -------- | ------------------------------------------- |
485
+ | `--tag` | string | `latest` | The dist-tag to take. |
486
+ | `--check` | boolean | `false` | Report what would change and write nothing. |
487
+
488
+ **EXIT STATUS**
489
+
490
+ 1 if the working directory holds no `package.json` or no `package-lock.json`. 1
491
+ if a named package is not a declared dependency. Otherwise 0, including when
492
+ every package is already current.
493
+
494
+ **EXAMPLES**
495
+
496
+ ```
497
+ $ package-build bump
498
+ @heroiclands/package-build 20.6.0 → 20.7.0
499
+ kept the existing indentation of package-lock.json
500
+
501
+ Install it with `npm ci`, which resolves from the lockfile this just moved.
502
+ ```
503
+
504
+ ```
505
+ $ package-build bump @heroiclands/hugo-theme --check
506
+ @heroiclands/hugo-theme 0.5.0 → 0.6.0
507
+
508
+ Run without --check to take it.
509
+ ```
510
+
511
+ **SEE ALSO**
512
+
513
+ `package-build clean`.
514
+
440
515
  ### `package-build yaml [paths..]`
441
516
 
442
517
  **NAME**
@@ -689,6 +689,7 @@ Any other key under `docs.itemFields` is refused:
689
689
  | ----------------------- | -------- | ------- |
690
690
  | `site.out` | string | `""` |
691
691
  | `site.base` | string | `""` |
692
+ | `site.assets` | string | `""` |
692
693
  | `site.packages` | string[] | `[]` |
693
694
  | `site.sections` | object | `{}` |
694
695
  | `site.readmeSections` | object | `{}` |
@@ -706,7 +707,7 @@ rewrites.
706
707
 
707
708
  > ``package-build config: `site` must be a mapping.``
708
709
 
709
- > ``package-build config: `site.<key>` is not a recognized option (expected one of: out, base, packages, sections, readmeSections, landing, trees, pass, passOptions, backfillSections).``
710
+ > ``package-build config: `site.<key>` is not a recognized option (expected one of: out, base, assets, packages, sections, readmeSections, landing, trees, pass, passOptions, backfillSections).``
710
711
 
711
712
  `site.out` is the output root, resolved by `engine/site-build.mjs`; unset,
712
713
  it is refused **at build time** rather than by `defineConfig` (an unset
@@ -715,6 +716,23 @@ wipes on every run would be the working tree):
715
716
 
716
717
  > `site.out is not set, so there is nowhere to write the site. Refusing to continue: the output directory is wiped on every run, and an unset one resolves to the repository root.`
717
718
 
719
+ `site.assets` is the host every package's imagery is served from, and it is
720
+ the one address in this file that is not this repository's own. A note names
721
+ a file by the package that owns it and the path inside that package's
722
+ `assets/` (see `docs/content-format.md`), and the website joins the two onto
723
+ this host — `https://cdn.heroiclands.org` + `/thalorna` +
724
+ `/images/map.webp`. A build has no way to find the host out, so a page
725
+ carrying a package-owned image with none set is an error naming this key.
726
+ Absolute, and the trailing slash is trimmed:
727
+
728
+ > ``package-build config: `site.assets` must be an absolute `http://` or `https://` address — it is the host every package's imagery is served from, and a relative value resolves against whichever page happens to carry the image.``
729
+
730
+ The consuming Hugo site spells the same host as `params.cdnBaseURL`, which
731
+ its theme resolves a relative asset path against. The two are the same
732
+ address written for two readers: this one is what the toolchain emits into a
733
+ page, that one is what the theme joins onto anything the toolchain left
734
+ relative.
735
+
718
736
  `site.packages` names which content packages' notes the site walks, beyond
719
737
  this one's own; `site.pass` names a repository's own body-rewrite bundle
720
738
  (the one part of the site contract that is code, exactly as `itemBuilders`
@@ -925,22 +943,31 @@ system relationship it declares — see [`stats.systemVersion`](#statssystemvers
925
943
 
926
944
  Each entry, in any of the four lists:
927
945
 
928
- | Key (under `relationships.<kind>[]`) | Type | Required | Default |
929
- | --------------------------------------- | ------------------------------- | -------- | ------- |
930
- | `relationships.systems[].id` | string | yes | — |
931
- | `relationships.systems[].type` | string | no | none |
932
- | `relationships.systems[].manifest` | string | no | none |
933
- | `relationships.systems[].compatibility` | object, `{minimum?, verified?}` | no | none |
934
- | `relationships.systems[].itemCatalog` | boolean | no | `false` |
946
+ | Key (under `relationships.<kind>[]`) | Type | Required | Default |
947
+ | ---------------------------------------- | ------------------------------- | -------- | ------- |
948
+ | `relationships.systems[].id` | string | yes | — |
949
+ | `relationships.systems[].contentPackage` | string | no | the id |
950
+ | `relationships.systems[].type` | string | no | none |
951
+ | `relationships.systems[].manifest` | string | no | none |
952
+ | `relationships.systems[].compatibility` | object, `{minimum?, verified?}` | no | none |
953
+ | `relationships.systems[].itemCatalog` | boolean | no | `false` |
935
954
 
936
- (the same four keys apply under `requires[]`, `recommends[]` and
955
+ (the same keys apply under `requires[]`, `recommends[]` and
937
956
  `conflicts[]`.)
938
957
 
939
958
  > ``package-build config: `relationships.<kind>[<index>]` must be a mapping.``
940
959
 
941
960
  > ``package-build config: `relationships.<kind>[<index>].id` must be a non-empty string.``
942
961
 
943
- > ``package-build config: `relationships.<kind>[<index>].<key>` is not a recognized option (expected one of: id, type, manifest, compatibility, itemCatalog).``
962
+ > ``package-build config: `relationships.<kind>[<index>].<key>` is not a recognized option (expected one of: id, contentPackage, type, manifest, compatibility, itemCatalog).``
963
+
964
+ `contentPackage` names what the other package's _content_ is called, where
965
+ that differs from its Foundry id. A note addresses a file by the content
966
+ package that owns it — `thalorna/assets/images/map.webp` — and the Foundry id
967
+ (`sohl-thalorna`) appears only in the install path that pathname resolves to.
968
+ Omit it where the two are the same word, which they are for every system:
969
+
970
+ > ``package-build config: `relationships.<kind>[<index>].contentPackage` must be a non-empty string.``
944
971
 
945
972
  `itemCatalog` opts into extracting the named package's Item packs so the
946
973
  actors pass can resolve embedded items this repository does not hold — off