@heroiclands/package-build 21.1.0 → 22.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 (115) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/CONTENT.md +107 -3
  3. package/assets/fonts/libertinus/LibertinusMono-Regular.otf +0 -0
  4. package/assets/fonts/libertinus/LibertinusSans-Bold.otf +0 -0
  5. package/assets/fonts/libertinus/LibertinusSans-Italic.otf +0 -0
  6. package/assets/fonts/libertinus/LibertinusSans-Regular.otf +0 -0
  7. package/assets/fonts/libertinus/OFL.txt +94 -0
  8. package/assets/fonts/libertinus/provenance.yaml +11 -0
  9. package/assets/images/banners/afflictionbnr.webp +0 -0
  10. package/assets/images/banners/armorclothingbnr.webp +0 -0
  11. package/assets/images/banners/attributebnr.webp +0 -0
  12. package/assets/images/banners/containerbnr.webp +0 -0
  13. package/assets/images/banners/devdocsbnr.webp +0 -0
  14. package/assets/images/banners/miscgearbnr.webp +0 -0
  15. package/assets/images/banners/mysticalabilitybnr.webp +0 -0
  16. package/assets/images/banners/projectilebnr.webp +0 -0
  17. package/assets/images/banners/rulesbnr.webp +0 -0
  18. package/assets/images/banners/skillbnr.webp +0 -0
  19. package/assets/images/banners/traumabnr.webp +0 -0
  20. package/assets/images/banners/userguidebnr.webp +0 -0
  21. package/assets/images/banners/weapongearbnr.webp +0 -0
  22. package/assets/images/provenance.yaml +4 -0
  23. package/bin/content-build.mjs +19 -5
  24. package/bin/package-build.mjs +6 -5
  25. package/ci/emit-asset-index.mjs +32 -0
  26. package/content-config.mjs +30 -1
  27. package/docs/api.md +103 -27
  28. package/docs/commands.md +13 -11
  29. package/docs/configuration.md +16 -7
  30. package/docs/content-format.md +523 -111
  31. package/engine/actor-compiler.mjs +58 -9
  32. package/engine/art-fields.mjs +292 -0
  33. package/engine/asset-index.mjs +397 -0
  34. package/engine/asset-types.mjs +192 -0
  35. package/engine/base-compiler.mjs +69 -1
  36. package/engine/bundles.mjs +5 -5
  37. package/engine/compile-corpus.mjs +4 -0
  38. package/engine/content-address.mjs +14 -2
  39. package/engine/content-charset.mjs +5 -1
  40. package/engine/content-embeds.mjs +314 -0
  41. package/engine/content-html.mjs +5 -1
  42. package/engine/content-icons.mjs +5 -1
  43. package/engine/content-images.mjs +26 -5
  44. package/engine/content-index.mjs +68 -33
  45. package/engine/content-links.mjs +164 -24
  46. package/engine/dependency-bump.mjs +14 -16
  47. package/engine/frontmatter-lint.mjs +36 -23
  48. package/engine/helpers.mjs +83 -31
  49. package/engine/index-records.mjs +63 -4
  50. package/engine/index.mjs +15 -0
  51. package/engine/infobox-registry.mjs +50 -3
  52. package/engine/infobox.mjs +12 -8
  53. package/engine/item-compiler.mjs +10 -10
  54. package/engine/journals.mjs +3 -0
  55. package/engine/macros.mjs +16 -13
  56. package/engine/map-notes.mjs +29 -22
  57. package/engine/metadata-index.mjs +44 -36
  58. package/engine/note-vocabulary.mjs +71 -29
  59. package/engine/pack-router.mjs +14 -0
  60. package/engine/packagebuild-index.mjs +124 -0
  61. package/engine/packages.mjs +83 -0
  62. package/engine/pathnames.mjs +74 -0
  63. package/engine/pdf-build.mjs +133 -8
  64. package/engine/pdf-render.mjs +49 -12
  65. package/engine/scenes.mjs +12 -9
  66. package/engine/site-build.mjs +43 -16
  67. package/engine/site-index.mjs +10 -1
  68. package/engine/subtype-registry.mjs +21 -0
  69. package/engine/web-wikilinks.mjs +46 -12
  70. package/engine/wikilink-syntax.mjs +20 -2
  71. package/engine/wikilinks.mjs +15 -2
  72. package/hm3/actors.mjs +11 -38
  73. package/manifest.mjs +67 -13
  74. package/package.json +5 -2
  75. package/packagebuild-metadata.jsonl +13 -0
  76. package/sohl/actors.mjs +12 -86
  77. package/sohl/note-schemas.mjs +4 -16
  78. package/types/content-config.d.mts +8 -0
  79. package/types/engine/actor-compiler.d.mts +29 -0
  80. package/types/engine/art-fields.d.mts +192 -0
  81. package/types/engine/asset-index.d.mts +92 -0
  82. package/types/engine/asset-types.d.mts +110 -0
  83. package/types/engine/base-compiler.d.mts +42 -1
  84. package/types/engine/compile-corpus.d.mts +4 -1
  85. package/types/engine/content-charset.d.mts +4 -0
  86. package/types/engine/content-embeds.d.mts +218 -0
  87. package/types/engine/content-html.d.mts +4 -0
  88. package/types/engine/content-icons.d.mts +4 -0
  89. package/types/engine/content-images.d.mts +15 -2
  90. package/types/engine/content-index.d.mts +27 -15
  91. package/types/engine/dependency-bump.d.mts +2 -2
  92. package/types/engine/frontmatter-lint.d.mts +11 -14
  93. package/types/engine/helpers.d.mts +7 -2
  94. package/types/engine/index-records.d.mts +44 -3
  95. package/types/engine/index.d.mts +5 -0
  96. package/types/engine/infobox-registry.d.mts +36 -1
  97. package/types/engine/infobox.d.mts +6 -4
  98. package/types/engine/macros.d.mts +9 -5
  99. package/types/engine/map-notes.d.mts +10 -6
  100. package/types/engine/metadata-index.d.mts +2 -17
  101. package/types/engine/note-vocabulary.d.mts +30 -0
  102. package/types/engine/packagebuild-index.d.mts +56 -0
  103. package/types/engine/packages.d.mts +62 -0
  104. package/types/engine/pathnames.d.mts +41 -81
  105. package/types/engine/pdf-build.d.mts +58 -1
  106. package/types/engine/pdf-render.d.mts +3 -1
  107. package/types/engine/scenes.d.mts +2 -1
  108. package/types/engine/site-build.d.mts +5 -1
  109. package/types/engine/site-index.d.mts +4 -1
  110. package/types/engine/subtype-registry.d.mts +20 -0
  111. package/types/engine/web-wikilinks.d.mts +14 -4
  112. package/types/engine/wikilink-syntax.d.mts +13 -1
  113. package/types/engine/wikilinks.d.mts +7 -1
  114. package/types/manifest.d.mts +42 -1
  115. package/types/sohl/actors.d.mts +0 -1
@@ -0,0 +1,32 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * Publish this package's own content index, for the readers that are not this
16
+ * process.
17
+ *
18
+ * Run by `prepack`, beside the declaration build and for the same reason: the
19
+ * artifact is derived from what the tree already holds, so it belongs in the
20
+ * tarball and not in the repository. A build inside this process never reads
21
+ * it — {@link module:engine/packagebuild-index.packageBuildRecords} walks the
22
+ * shipped tree — so an absent file makes nothing fail, and anything reading the
23
+ * published form gets the same records the walk produces.
24
+ *
25
+ * Not a `package-build` subcommand: no consuming repository has a reason to run
26
+ * it, and a CLI that offers one would be surface nobody asked for.
27
+ */
28
+
29
+ import { emitPackageBuildIndex } from "../engine/packagebuild-index.mjs";
30
+
31
+ const { file, assets, bytes } = emitPackageBuildIndex();
32
+ console.log(`package-build: ${file} (${assets} asset(s), ${bytes} bytes)`);
@@ -70,6 +70,8 @@ import YAML from "yaml";
70
70
  // Leaves with no local imports of their own, so naming them here cannot close
71
71
  // a cycle around a consumer's config file (see `engine/pack-config.mjs`).
72
72
  import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./engine/address-charset.mjs";
73
+ import { ASSET_TYPE_NAMES } from "./engine/asset-types.mjs";
74
+ import { isReservedPackage } from "./engine/packages.mjs";
73
75
  import { EMPTY_ICON_REGISTRY, checkIconRegistry } from "./engine/content-icons.mjs";
74
76
  import { MAP_TYPES, PACK_BY_TYPE } from "./engine/ids.mjs";
75
77
  import { ACTOR_TYPES } from "./engine/subtype-registry.mjs";
@@ -149,6 +151,12 @@ const DOCUMENTATION_REFUSES = Object.freeze({
149
151
  */
150
152
  export const DEFAULT_PATHS = /** @type {const} */ ({
151
153
  content: "assets/content",
154
+ // The asset roots' parent — `icons/`, `images/` and `audio/` sit directly
155
+ // under it, and the content tree beside them. Named separately from
156
+ // `content` rather than derived from its parent, because the two are free to
157
+ // move independently and deriving one from the other would make relocating
158
+ // either a surprise for the other.
159
+ assets: "assets",
152
160
  // Where `content-index` writes this package's note index. Under `build/`
153
161
  // because it is derived and disposable — regenerating it costs a
154
162
  // frontmatter parse — and emphatically not under `stage`, which is mirrored
@@ -361,6 +369,9 @@ export function publishesContentPages(config) {
361
369
  *
362
370
  * @typedef {object} PathsInput
363
371
  * @property {string} [content] Content tree root.
372
+ * @property {string} [assets] The asset roots' parent — the directory
373
+ * holding `icons/`, `images/` and
374
+ * `audio/`.
364
375
  * @property {string} [contentIndex] Where `content-index` writes this
365
376
  * package's note index. Outbound, and a
366
377
  * derived artifact — never a source, and
@@ -383,6 +394,7 @@ export function publishesContentPages(config) {
383
394
  *
384
395
  * @typedef {object} ResolvedPaths
385
396
  * @property {string} content
397
+ * @property {string} assets
386
398
  * @property {string} contentIndex
387
399
  * @property {string} packJson
388
400
  * @property {string} stage
@@ -827,7 +839,7 @@ function requireNonEmptyString(value, field) {
827
839
  }
828
840
 
829
841
  /**
830
- * The `contentPackage`, checked against the two rules an address puts on it.
842
+ * The `contentPackage`, checked against the three rules an address puts on it.
831
843
  *
832
844
  * It is the first segment of every canonical address this repository publishes
833
845
  * (`package-system-type-shortcode`, so `sohl-none-doc-gear`), and an address is
@@ -856,6 +868,13 @@ function requireNonEmptyString(value, field) {
856
868
  * package's id to *be* its system id, and `sohl-sohl-skill-clmb` is the
857
869
  * honest address that results — which is the reason to prevent the ones that
858
870
  * are avoidable.
871
+ * 3. _Not reserved_. `packagebuild` addresses the files this toolchain ships
872
+ * itself, so a repository claiming the name would publish addresses that
873
+ * collide with them — see {@link module:engine/packages}.
874
+ *
875
+ * The type vocabulary rule reaches the **asset** types too: `icon`, `image` and
876
+ * `audio` are types an address names exactly as it names a being, so a package
877
+ * called `image` would make `image-thorn` readable two ways.
859
878
  *
860
879
  * @param {unknown} value - The configured `contentPackage`.
861
880
  * @param {ReadonlySet<string>} docEntryTypes - Every type whose prose compiles
@@ -878,6 +897,15 @@ function requireContentPackage(value, docEntryTypes) {
878
897
  "than merely ugly. `harn-adventures` became `harnadventures`",
879
898
  );
880
899
  }
900
+ if (isReservedPackage(pkg)) {
901
+ fail(
902
+ "contentPackage",
903
+ `is \`${pkg}\`, which is a reserved package name. ` +
904
+ `\`${pkg}-none-image-<shortcode>\` already addresses a file the ` +
905
+ "toolchain itself ships, so a package claiming the name would " +
906
+ "publish addresses that collide with it. Rename the package",
907
+ );
908
+ }
881
909
  // The closed vocabulary is read alongside the configured registries, not
882
910
  // instead of them, because neither is a superset of the other. The format's
883
911
  // vocabulary holds every type a note may declare *however this repository
@@ -891,6 +919,7 @@ function requireContentPackage(value, docEntryTypes) {
891
919
  const typeNames = new Set([
892
920
  ...Object.keys(PACK_BY_TYPE),
893
921
  ...Object.keys(NOTE_VOCABULARY),
922
+ ...ASSET_TYPE_NAMES,
894
923
  ...docEntryTypes,
895
924
  ...[...docEntryTypes].map((type) => `doc${type}`),
896
925
  ]);
package/docs/api.md CHANGED
@@ -336,6 +336,7 @@ The closed half of a note's frontmatter: the `data:` container and each type's `
336
336
  | ------------------------- | -------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------- |
337
337
  | `DRAFT_TAG` | `const DRAFT_TAG` | — | naming the declared tag that marks a note as unfinished |
338
338
  | `NOTE_VOCABULARY` | `const NOTE_VOCABULARY` | — | looking up, per content type, its `data:` field list and its closed `subType` values |
339
+ | `SHARED_DATA_FIELDS` | `const SHARED_DATA_FIELDS` | `readonly DataFieldSpec[]` | reading the `data:` keys every type accepts, whatever it is — the art slots legal everywhere |
339
340
  | `DECLARED_TAGS` | `const DECLARED_TAGS` | — | looking up the tags a note type may declare, grouped |
340
341
  | `declaredTags` | `declaredTags(type, groups)` | `readonly string[]` | reading the declared tags a note of a type may carry, flattened |
341
342
  | `applicableTagGroups` | `applicableTagGroups(type, groups)` | `object[]` | reading the declared tag groups that apply to a note type |
@@ -388,11 +389,12 @@ The declared infobox: what a note's summary panel holds, decided once and render
388
389
 
389
390
  The infobox declarations this toolchain ships, one per system, and the single call each medium makes to build a note's boxes.
390
391
 
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 |
392
+ | Export | Signature | Returns | Use it when |
393
+ | ------------------------ | ----------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------ |
394
+ | `KNOWN_INFOBOXES` | `const KNOWN_INFOBOXES` | — | enumerating every system's infobox declaration, in the order a page shows them |
395
+ | `infoboxFor` | `infoboxFor(system)` | `object \| undefined` | looking up one system's declaration by its id |
396
+ | `noteInfoboxes` | `noteInfoboxes(fm, options)` | `object[]`, throws | building every box one note carries, wired to the shipped registries |
397
+ | `compilesSystemDocument` | `compilesSystemDocument(fm, map, router)` | `boolean` | asking whether one system compiles a document for one note, which is what a system box's _available_ asserts |
396
398
 
397
399
  ### `engine.infoboxRender`
398
400
 
@@ -462,6 +464,7 @@ Which note-type → document-subtype maps this toolchain ships, and the two ques
462
464
  | ----------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
463
465
  | `KNOWN_DOCUMENT_SUBTYPE_MAPS` | `const KNOWN_DOCUMENT_SUBTYPE_MAPS` | — | The note-type → document-subtype maps this toolchain ships. |
464
466
  | `ACTOR_TYPES` | `const ACTOR_TYPES` | — | Every note type any shipped map compiles into an **Actor**. |
467
+ | `DEFAULT_DOCUMENT_SUBTYPES` | `const DEFAULT_DOCUMENT_SUBTYPES` | — | The map a pack declaring no `system:` compiles its documents against. |
465
468
  | `subtypeMapFor` | `function subtypeMapFor(system)` | {import("./document-subtypes.mjs").DocumentSubtypeMap\|undefined} Its map, or `undefined` where this toolchain ships none for it. | The map one system ships, by its id. |
466
469
  | `schemaSubtypeOf` | `function schemaSubtypeOf(system, type)` | {string} The document subtype to look up. | The document subtype a note type compiles into for one system — the translation the _schema_ check needs, and the reason it needs one. |
467
470
 
@@ -521,19 +524,19 @@ Emitting this package's cross-package link manifest. `engine/content-address.mjs
521
524
 
522
525
  Emitting this package's content index. Every content build already walks the whole note tree and parses every note's frontmatter — the pack compilers, the site build, and the content-table expander each do it — and every one of them throws the result away when it finishes. So nothing outside a build can ask a question about the content: "which beings carry no `kbcat`?", "what does this table actually select?", "did that type rename leave anything behind?" have no answer short of writing a throwaway script that re-walks the tree. Eight dead Bestiary tables shipped for weeks behind exactly that gap.
523
526
 
524
- | Export | Signature | Returns | Use it when |
525
- | ----------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
526
- | `noteAddress` | `function noteAddress(frontmatter, contentPackage)` | {{slug: string, canonical: string}\|null} `slug` is what goes inside `[[…]]` within this package; `canonical` is the fully qualified key the manifest files the note under, carrying the package and the system as well. | The address a wikilink writes to reach a note, or `null` when it has none. |
527
- | `sortKeysDeep` | `function sortKeysDeep(value)` | {unknown} The value with every plain object's keys in sorted order. | Recursively sort an object's keys, so serialization is order-independent. |
528
- | `asciiName` | `function asciiName(name)` | {string\|null} The ASCII form, or `null` when there is no name, or nothing printable survives. | A note's display name reduced to printable 7-bit ASCII. |
529
- | `asciiAliases` | `function asciiAliases(aliases)` | {Array<string>} Possibly empty, never null: a note with no aliases has an empty set of them, which is a fact rather than a missing value, and a consumer iterating it should not have to check first. | A note's `name.aliases` reduced to printable 7-bit ASCII, in order. |
530
- | `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated. | Serialize records as JSON Lines. |
531
- | `indexRecordsFor` | `indexRecordsFor({ contentBase, config, skipDirectories, problems })` | {object[]} One record per note, plus one per documentation entry. | The index records for a content tree, without writing anything. |
532
- | `emitContentIndex` | `emitContentIndex({ contentBase, outDir, config })` | {{file: string, notes: number, bytes: number}} Where it was written, how many notes it holds, and its size. | Emit this package's content index. |
533
- | `buildIndexRecord` | `buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest })` | `Record<string, any>` — the record, with derived fields sorted deep | building one note's content-index record; refuses a note that authors a key the index derives itself (`package` among them) before building anything |
534
- | `collectContentIndex` | `collectContentIndex(contentBase, { contentPackage, skipDirectories, manifest, problems })` | `Array<Record<string, any>>` | reading a whole content tree into index records, in a total order independent of directory-read order; an item note yields two records (its own and its documentation journal's) |
527
+ | Export | Signature | Returns | Use it when |
528
+ | ----------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
529
+ | `noteAddress` | `function noteAddress(frontmatter, contentPackage)` | {{slug: string, canonical: string}\|null} `slug` is what goes inside `[[…]]` within this package; `canonical` is the fully qualified key the manifest files the note under, carrying the package and the system as well. | The address a wikilink writes to reach a note, or `null` when it has none. |
530
+ | `sortKeysDeep` | `function sortKeysDeep(value)` | {unknown} The value with every plain object's keys in sorted order. | Recursively sort an object's keys, so serialization is order-independent. |
531
+ | `asciiName` | `function asciiName(name)` | {string\|null} The ASCII form, or `null` when there is no name, or nothing printable survives. | A note's display name reduced to printable 7-bit ASCII. |
532
+ | `asciiAliases` | `function asciiAliases(aliases)` | {Array<string>} Possibly empty, never null: a note with no aliases has an empty set of them, which is a fact rather than a missing value, and a consumer iterating it should not have to check first. | A note's `name.aliases` reduced to printable 7-bit ASCII, in order. |
533
+ | `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated. | Serialize records as JSON Lines. |
534
+ | `indexRecordsFor` | `indexRecordsFor({ contentBase, config, skipDirectories, problems })` | {object[]} One record per note, plus one per documentation entry. | The index records for a content tree, without writing anything. |
535
+ | `emitContentIndex` | `emitContentIndex({ contentBase, outDir, config })` | {{file: string, notes: number, assets: number, records: number, bytes: number}} Where it was written, how many notes and how many assets it holds, how many records that is in all, and its size. | Emit this package's content index. |
536
+ | `buildIndexRecord` | `buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest })` | `Record<string, any>` — the record, with derived fields sorted deep | building one note's content-index record; refuses a note that authors a key the index derives itself (`package` among them) before building anything |
537
+ | `collectContentIndex` | `collectContentIndex(contentBase, { contentPackage, skipDirectories, assetsBase, manifest, problems })` | `Array<Record<string, any>>` | reading a whole content tree into index records, in a total order independent of directory-read order; an item note yields two records (its own and its documentation journal's), and each asset root yields one per file |
535
538
 
536
- `engine.contentIndex` also re-exports five names from neighbouring leaf modules, at this same import path:
539
+ `engine.contentIndex` also re-exports seven names from neighbouring leaf modules, at this same import path:
537
540
 
538
541
  | Export | Signature | Returns | Use it when |
539
542
  | --------------------- | -------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -541,7 +544,59 @@ Emitting this package's content index. Every content build already walks the who
541
544
  | `DERIVED_KEYS` | `const DERIVED_KEYS` | — | reading which keys the content index adds to a record, and which a note therefore may not author itself |
542
545
  | `noteFile` | `noteFile(contentBase, record)` | `string` — the note's absolute path | composing an index record's `file.path` (recorded relative, for a byte-stable artifact) back into an openable absolute path |
543
546
  | `authoredFrontmatter` | `authoredFrontmatter(record)` | `Record<string, any>` — the frontmatter without `DERIVED_KEYS` | reading back exactly what a note authored from its index record, so a pass can lint or compile from the index without reasoning about derived fields as if the author wrote them |
544
- | `isNoteRecord` | `isNoteRecord(record)` | `boolean` | telling a note's own record apart from its documentation journal's, when enumerating the corpus |
547
+ | `isNoteRecord` | `isNoteRecord(record)` | `boolean` | telling a note's own record apart from its documentation journal's and from an asset's, when enumerating the corpus |
548
+ | `isAssetRecord` | `isAssetRecord(record)` | `boolean` | telling a record that addresses a file apart from one that addresses a note; the `asset` block is what marks it |
549
+ | `recordPath` | `recordPath(record)` | `string` — the path the record names inside its package | ordering or reporting across both record shapes: a note names the `.md` it was parsed from, an asset the file it is |
550
+
551
+ ### `engine.assetTypes`
552
+
553
+ The asset types — `icon`, `image` and `audio` — and the three roots they are walked from. An asset is addressed exactly as a note is; what differs is that a `.webp` carries no frontmatter and has nowhere to say what it is, so the root supplies the type and the list of roots is closed.
554
+
555
+ | Export | Signature | Returns | Use it when |
556
+ | ------------------ | ----------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
557
+ | `ASSET_TYPES` | `const ASSET_TYPES` | `readonly AssetType[]` — type, root, extensions, description | walking the roots, or presenting the vocabulary |
558
+ | `ASSET_TYPE_NAMES` | `const ASSET_TYPE_NAMES` | `ReadonlySet<string>` | admitting the asset types into a type vocabulary an address is parsed against |
559
+ | `ASSET_SYSTEM` | `const ASSET_SYSTEM` | `string` — always `none` | writing an asset address, whose `<system>` segment is a property of the type rather than of the reference |
560
+ | `IMAGE_EXTENSIONS` | `const IMAGE_EXTENSIONS` | `readonly string[]` — lowercase, dot-led | deciding whether a file under a picture root is an asset at all |
561
+ | `AUDIO_EXTENSIONS` | `const AUDIO_EXTENSIONS` | `readonly string[]` — lowercase, dot-led | the same question for `audio/` |
562
+ | `isAssetType` | `isAssetType(type)` | `boolean` | scoping a rewrite rule, which may substitute a file and nothing else |
563
+ | `assetTypeOfRoot` | `assetTypeOfRoot(root)` | `AssetType \| undefined` | reading a root directory name back to the type it declares |
564
+ | `isAssetShortcode` | `isAssetShortcode(shortcode)` | `boolean` | telling an addressable filename from one that carries a version string, a hyphen or a date stamp |
565
+
566
+ ### `engine.assetIndex`
567
+
568
+ The asset record: one line of the content index per addressable file. The record is emitted by the package holding the bytes, so its `path` is that package's path and each consumer joins its own root onto it — Foundry its data directory, the website its CDN prefix, the book its asset base.
569
+
570
+ | Export | Signature | Returns | Use it when |
571
+ | --------------------------- | --------------------------------------------------------------- | ----------------------------- | --------------------------------------------------------------------------------- |
572
+ | `ASSET_RECORD_FIELDS` | `const ASSET_RECORD_FIELDS` | `readonly AssetRecordField[]` | reading what an `asset` block carries, and where each field comes from |
573
+ | `PROVENANCE_KEYS` | `const PROVENANCE_KEYS` | `ReadonlySet<string>` | validating a provenance file, whose keys are derived from the record's own fields |
574
+ | `PROVENANCE_FILE` | `const PROVENANCE_FILE` | `string` — `provenance.yaml` | naming the file a directory records provenance for its subtree in |
575
+ | `PROVENANCE_SIDECAR_SUFFIX` | `const PROVENANCE_SIDECAR_SUFFIX` | `string` — `.yaml` | naming a per-file record, which replaces an inherited one wholesale |
576
+ | `collectAssetRecords` | `collectAssetRecords(assetsBase, { contentPackage, problems })` | `Array<Record<string, any>>` | reading a package's asset roots into index records, without walking its notes |
577
+
578
+ ### `engine.packages`
579
+
580
+ The `<package>` segment's own registry: the names no repository may claim. A content package names itself, and exactly one name is held back — `packagebuild`, which addresses the files the toolchain ships itself.
581
+
582
+ | Export | Signature | Returns | Use it when |
583
+ | ---------------------- | ---------------------------- | ------------------------------------- | ------------------------------------------------------------------------------ |
584
+ | `PACKAGEBUILD_PACKAGE` | `const PACKAGEBUILD_PACKAGE` | `string` — `packagebuild` | naming the address namespace package-build's own assets publish under |
585
+ | `metadataFileName` | `metadataFileName(pkg)` | `string` — `<package>-metadata.jsonl` | naming a package's content index, wherever it is written, published or fetched |
586
+ | `RESERVED_PACKAGES` | `const RESERVED_PACKAGES` | `ReadonlySet<string>` | listing the names a configuration is refused for claiming |
587
+ | `isReservedPackage` | `isReservedPackage(pkg)` | `boolean` | checking one candidate `contentPackage` against the reservation |
588
+
589
+ ### `engine.packagebuildIndex`
590
+
591
+ The toolchain's own content index — the files it ships, addressed. Every other package's index is fetched; this one is read from disk, because package-build is an npm dependency of every consumer rather than a Foundry package. The records join `foreign.index` like any other package's, so every lookup stays one path, and the special case is entirely about acquisition.
592
+
593
+ | Export | Signature | Returns | Use it when |
594
+ | ------------------------- | --------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------- |
595
+ | `PACKAGEBUILD_ROOT` | `const PACKAGEBUILD_ROOT` | `string` | naming this package's own root, wherever it is installed |
596
+ | `PACKAGEBUILD_ASSETS` | `const PACKAGEBUILD_ASSETS` | `string` | naming the asset directory it ships |
597
+ | `PACKAGEBUILD_INDEX_FILE` | `const PACKAGEBUILD_INDEX_FILE` | `string` | naming where the published index sits, beside the files it describes |
598
+ | `packageBuildRecords` | `packageBuildRecords(assetsBase)` | `Array<Record<string, any>>` | reading the records for the files it ships, walked rather than read back |
599
+ | `emitPackageBuildIndex` | `emitPackageBuildIndex({ assetsBase, file })` | `{file: string, assets: number, bytes: number}` | publishing that walk as a file, for the readers that are not this process |
545
600
 
546
601
  ### `engine.siteBuild`
547
602
 
@@ -625,6 +680,20 @@ Raw HTML in a note's prose, reported. **A note is markdown.** What markdown cann
625
680
  | `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. |
626
681
  | `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. |
627
682
 
683
+ ### `engine.contentEmbeds`
684
+
685
+ `![[address|label]]` — the wikilink that renders a picture where it stands. An embed is the wikilink the syntax module defines, with `!` meaning _render it here_, and the syntax supplying the default type `image`. It resolves to an ordinary markdown image before any surface renders it, so the figure, the staging list and the directive all come from the image machinery that already exists. An embed reaches asset types only.
686
+
687
+ | Export | Signature | Returns | Use it when |
688
+ | -------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
689
+ | `EMBED_DEFAULT_TYPE` | `const EMBED_DEFAULT_TYPE` | — | The type a bare shortcode takes, supplied by the syntax itself. |
690
+ | `EMBED_PATTERN` | `const EMBED_PATTERN` | — | An embed, as authored, with the directive it may carry. |
691
+ | `embedsIn` | `function embedsIn(body)` | {ParsedEmbed[]} One entry per embed. | Every embed in one body, in source order. |
692
+ | `resolveEmbed` | `function resolveEmbed(index, embed)` | {{pathname: string}\|{reason: string, target: string, type?: string}} The authored pathname, or the finding. | What one embed resolves to, or why it does not. |
693
+ | `embedProblems` | `function embedProblems(embed)` | {Array<{link: string, offset: number, message: string}>} One entry per defect. | What is wrong with one embed's directive or placement. |
694
+ | `resolveEmbeds` | `function resolveEmbeds(body, ctx)` | {{markdown: string, unresolved: Array<object>, problems: Array<object>}} The body, the embeds that named nothing, and the directives that could not be honoured. | Rewrite every embed into the image each surface renders. |
695
+ | `checkEmbeds` | `function checkEmbeds(body, file, opts)` | {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 embeds, located. |
696
+
628
697
  ### `engine.contentImages`
629
698
 
630
699
  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.
@@ -652,13 +721,15 @@ An image saying how wide it is and where it sits. A markdown image carries no in
652
721
 
653
722
  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
723
 
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. |
724
+ | Export | Signature | Returns | Use it when |
725
+ | ----------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
726
+ | `ASSETS_SEGMENT` | `const ASSETS_SEGMENT` | — | The directory a package ships its files in. |
727
+ | `PATHNAME_SURFACES` | `const PATHNAME_SURFACES` | — | The surfaces one authored pathname resolves for. |
728
+ | `pathnameProblem` | `function pathnameProblem(raw)` | {string} The problem, as a finding's sentence, or `""`. | What is wrong with an authored pathname, or `""` when nothing is. |
729
+ | `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. |
730
+ | `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. |
731
+ | `servesFoundry` | `function servesFoundry(config)` | {boolean} Whether the package being built has a Foundry root. | Whether this build installs anything into a Foundry data directory. |
732
+ | `foundryAddressProblem` | `function foundryAddressProblem(raw, config)` | {string} The problem, as a finding's sentence, or `""`. | Why a pathname has no Foundry address, or `""` when it has one. |
662
733
 
663
734
  ### `engine.contentLinks`
664
735
 
@@ -845,7 +916,10 @@ The content tree, built into a book. The I/O half of the PDF surface: it reads t
845
916
  | `buildPdf` | `async buildPdf({ config, out, version, compile })` | {Promise<object>} `{ built, reason, findings, typ, pdf, stats }`. | Build the book. |
846
917
  | `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
918
  | `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. |
919
+ | `compileTypst` | `function compileTypst(typPath, pdfPath, pdf` | {{ok: boolean, message: string, findings: object[]}} What happened. | Run Typst over the emitted source. |
920
+ | `typstArgs` | `function typstArgs(typPath, pdfPath, pdf)` | {string[]} The arguments, in order. | The command line the compile runs, as data. |
921
+ | `typstWarnings` | `function typstWarnings(output)` | {object[]} One finding per warning the compiler wrote. | The compiler's own warnings, as findings. |
922
+ | `BOOK_FONTS_PATH` | `const BOOK_FONTS_PATH` | {string} The directory holding them. | The faces the book is set in, shipped with this package. |
849
923
 
850
924
  ### `engine.baseCompiler`
851
925
 
@@ -1413,7 +1487,9 @@ console.log(normalizeRepoUrl("git@github.com:HeroicLands/sohl.git"));
1413
1487
  | ------------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1414
1488
  | `ARTIFACTS` | `const ARTIFACTS` | — | reading the two Foundry package kinds (`"system"`, `"module"`) as the artifact name their manifest and release archive are called |
1415
1489
  | `normalizeRepoUrl` | `normalizeRepoUrl(repository)` | `string` — normalised `https://` URL, no trailing slash | normalising `package.json`'s `repository` field (object or shorthand string, `git+…git` or plain) to the exact URL Foundry fetches release assets from |
1416
- | `releaseUrls` | `releaseUrls({ repoUrl, version, artifact })` | `{url: string, bugs: string, manifest: string, download: string}` | building the manifest's release addresses `manifest` points at `releases/latest` so an installed package can discover updates, `download` is pinned to this version |
1490
+ | `HOMEPAGE_ORIGIN` | `HOMEPAGE_ORIGIN` | `string` the origin every package's homepage is served from | stating the site origin once, so the address a manifest advertises and the address the configuration documents cannot disagree |
1491
+ | `packageHomepage` | `packageHomepage(contentPackage)` | `string` — `<origin>/<contentPackage>/` | deriving the homepage a manifest's `url` points at; throws when given no name rather than advertising `<origin>/undefined/` |
1492
+ | `releaseUrls` | `releaseUrls({ repoUrl, homeUrl, version, artifact })` | `{url: string, bugs: string, manifest: string, download: string}` | building the manifest's addresses — `url` is the homepage a reader follows before installing, while `bugs`, `manifest` and `download` stay on the repository holding the artefacts |
1417
1493
  | `metadataUrl` | `metadataUrl({ repoUrl, version, contentPackage })` | `string` — the version-pinned asset URL | computing where this release publishes its content index, pinned to this version like `download` so a dependency's manifest and its fetched index always describe the same release |
1418
1494
  | `manifestPacks` | `manifestPacks(config)` | `object[]` | deriving the manifest's `packs` array from the one pack list the build already has, in build order, companions flattened in |
1419
1495
  | `packFolderFindings` | `packFolderFindings({ packFolders, packs = [] })` | `Array<{severity: "error"\|"warning", message: string, pack: string, folder?: string, keyPath: Array<string\|number>}>` | checking a declared `packFolders` against the derived pack list — a folder naming a pack that does not exist is an error, a pack no folder names is a warning |
package/docs/commands.md CHANGED
@@ -441,7 +441,7 @@ None.
441
441
 
442
442
  **NAME**
443
443
 
444
- Take a newer version of a dependency, keeping the lockfile's formatting.
444
+ Take a newer published version of a declared dependency.
445
445
 
446
446
  **SYNOPSIS**
447
447
 
@@ -453,17 +453,20 @@ package-build bump [packages..] [--tag <tag>] [--check]
453
453
 
454
454
  Takes the newest published version of one or more declared dependencies,
455
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.
456
+ move — and holding both files to the indentation they already carry.
457
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.
458
+ npm performs the resolution, which is what this exists for. A version whose
459
+ dependency set differs from the one it replaces is handled as correctly as one
460
+ that moves three lines, while editing the `version` / `resolved` / `integrity`
461
+ lines by hand is only right while the two dependency sets are identical — and
462
+ nothing tells the author when they are not.
462
463
 
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.
464
+ npm writes `package-lock.json` with the indentation `package.json` uses, so a
465
+ consumer whose manifest is formatted gets a formatted lockfile back and the
466
+ diff is the version change alone. The indentation step covers the case where
467
+ the two files disagree: each is written back with the indent it already
468
+ carried, rather than the lockfile taking the manifest's. A run that had to put
469
+ an indent back names the file it rewrote.
467
470
 
468
471
  Named no packages, it takes every `@heroiclands/*` dependency the manifest
469
472
  declares. That scope is the one a person bumps by hand — a first-party release
@@ -496,7 +499,6 @@ every package is already current.
496
499
  ```
497
500
  $ package-build bump
498
501
  @heroiclands/package-build 20.6.0 → 20.7.0
499
- kept the existing indentation of package-lock.json
500
502
 
501
503
  Install it with `npm ci`, which resolves from the lockfile this just moved.
502
504
  ```
@@ -439,6 +439,7 @@ HeroicLands layout, resolved against `rootDir`:
439
439
  | Key | Default | What it is |
440
440
  | --------------------- | ---------------------- | ------------------------------------------------------------------------------------------ |
441
441
  | `paths.content` | `assets/content` | The content tree root. |
442
+ | `paths.assets` | `assets` | The asset roots' parent, holding `icons/`, `images/` and `audio/`. |
442
443
  | `paths.contentIndex` | `build/content-index` | Where `content-index` writes this package's note index. Derived and disposable. |
443
444
  | `paths.packJson` | `build/packs-json` | Build-only per-entry JSON intermediate. |
444
445
  | `paths.stage` | `build/stage/packs` | Compiled LevelDB packs. |
@@ -457,7 +458,7 @@ An empty value is refused generically:
457
458
 
458
459
  Any other key is refused:
459
460
 
460
- > ``package-build config: `paths.<key>` is not a recognized option (expected one of: content, contentIndex, packJson, stage, unpack, foreignCache, metadataCache).``
461
+ > ``package-build config: `paths.<key>` is not a recognized option (expected one of: content, assets, contentIndex, packJson, stage, unpack, foreignCache, metadataCache).``
461
462
 
462
463
  ### `skipDirectories`
463
464
 
@@ -850,12 +851,20 @@ reader downloaded, and a title with no document has nothing to print:
850
851
  `pdf.fonts` names font **families**, not files — the renderer asks the font
851
852
  stack for a family by name:
852
853
 
853
- | Key (under `pdf.fonts`) | Type | Required | Default |
854
- | ----------------------- | ------------- | -------- | ---------------------------------------------------------------- |
855
- | `pdf.fonts.serif` | string | no | `""` |
856
- | `pdf.fonts.sans` | string | no | `""` |
857
- | `pdf.fonts.mono` | string | no | `""` |
858
- | `pdf.fonts.path` | string (path) | no | `""` — where the renderer may look beyond the system's own fonts |
854
+ | Key (under `pdf.fonts`) | Type | Required | Default |
855
+ | ----------------------- | ------------- | -------- | --------------------------------------------------------- |
856
+ | `pdf.fonts.serif` | string | no | `""` — the book is set in `Libertinus Serif` |
857
+ | `pdf.fonts.sans` | string | no | `""` — its headings in `Libertinus Sans` |
858
+ | `pdf.fonts.mono` | string | no | `""` — its raw and code spans in `DejaVu Sans Mono` |
859
+ | `pdf.fonts.path` | string (path) | no | `""` — faces of your own, searched as well as the shipped |
860
+
861
+ Each role falls back to a face that resolves on a machine carrying none of them:
862
+ the toolchain ships the sans and the compiler embeds the other two. The compile
863
+ searches the shipped directory, and `pdf.fonts.path` when one is given, and
864
+ nothing the machine has installed — so a face named here has to come from one of
865
+ those two places, and a name nothing resolves is reported rather than set in the
866
+ fallback. Shipped alongside the sans, and available to a package that names it,
867
+ is `Libertinus Mono`.
859
868
 
860
869
  > ``package-build config: `pdf.fonts` must be a mapping.``
861
870