@heroiclands/package-build 8.1.0 → 10.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 (122) hide show
  1. package/CHANGELOG.md +748 -0
  2. package/CONTENT.md +273 -13
  3. package/bin/content-build.mjs +479 -123
  4. package/bin/package-build.mjs +27 -69
  5. package/bin/report.mjs +1 -2
  6. package/bundle.mjs +2 -10
  7. package/config.mjs +31 -106
  8. package/container.mjs +13 -57
  9. package/content-config.mjs +300 -188
  10. package/coverage.mjs +14 -55
  11. package/deploy.mjs +4 -13
  12. package/docs/content-format.md +1418 -0
  13. package/e2e.mjs +16 -55
  14. package/engine/address-charset.mjs +62 -0
  15. package/engine/address-diff.mjs +1 -4
  16. package/engine/alias-index.mjs +153 -0
  17. package/engine/base-compiler.mjs +203 -31
  18. package/engine/code-fences.mjs +4 -13
  19. package/engine/compendiums.mjs +13 -37
  20. package/engine/content-address.mjs +6 -10
  21. package/engine/content-format-check.mjs +570 -0
  22. package/engine/content-format.mjs +253 -0
  23. package/engine/content-links.mjs +144 -99
  24. package/engine/content-lint.mjs +12 -16
  25. package/engine/content-slug.mjs +2 -6
  26. package/engine/content-tables.mjs +26 -79
  27. package/engine/diagnostics.mjs +37 -15
  28. package/engine/document-subtypes.mjs +440 -0
  29. package/engine/field-reference.mjs +6 -20
  30. package/engine/field-spec.mjs +49 -45
  31. package/engine/foreign-catalog.mjs +7 -22
  32. package/engine/foreign-manifests.mjs +1 -4
  33. package/engine/frontmatter-lint.mjs +347 -43
  34. package/engine/frontmatter.mjs +3 -8
  35. package/engine/generate.mjs +36 -20
  36. package/engine/helpers.mjs +54 -81
  37. package/engine/homepage.mjs +4 -15
  38. package/engine/ids.mjs +21 -12
  39. package/engine/index.mjs +15 -0
  40. package/engine/item-registry.mjs +72 -9
  41. package/engine/journals.mjs +4 -14
  42. package/engine/kb-manifest.mjs +41 -24
  43. package/engine/macros.mjs +2 -10
  44. package/engine/manifest-emit.mjs +6 -17
  45. package/engine/map-notes.mjs +53 -87
  46. package/engine/note-claims.mjs +383 -0
  47. package/engine/note-package.mjs +1 -4
  48. package/engine/note-vocabulary.mjs +678 -0
  49. package/engine/pack-config.mjs +56 -60
  50. package/engine/pack-router.mjs +18 -8
  51. package/engine/prose-config.mjs +20 -4
  52. package/engine/prose-lint.mjs +61 -17
  53. package/engine/region-events.mjs +1 -3
  54. package/engine/retired-fields.mjs +117 -3
  55. package/engine/scene-levels.mjs +8 -22
  56. package/engine/scenes.mjs +31 -47
  57. package/engine/schema-check.mjs +348 -7
  58. package/engine/schema-extract.mjs +11 -39
  59. package/engine/site-build.mjs +13 -38
  60. package/engine/site-index.mjs +40 -35
  61. package/engine/system-block.mjs +513 -0
  62. package/engine/web-wikilinks.mjs +115 -92
  63. package/engine/wikilink-syntax.mjs +30 -0
  64. package/engine/wikilinks.mjs +147 -183
  65. package/index.mjs +1 -5
  66. package/lang.mjs +1 -3
  67. package/manifest.mjs +10 -37
  68. package/markdownlint-config.mjs +1 -5
  69. package/package.json +6 -2
  70. package/sohl/actors.mjs +251 -68
  71. package/sohl/being-info.mjs +3 -6
  72. package/sohl/document-subtypes.mjs +82 -0
  73. package/sohl/index.mjs +4 -6
  74. package/sohl/item-builders.mjs +1 -3
  75. package/sohl/item-fields.mjs +16 -34
  76. package/sohl/items.mjs +111 -17
  77. package/sohl/kb-passes.mjs +29 -39
  78. package/sohl/note-schemas.mjs +11 -7
  79. package/sohl/skill-base.mjs +7 -23
  80. package/stage.mjs +3 -13
  81. package/templates.mjs +4 -15
  82. package/types/bundle.d.mts +1 -1
  83. package/types/container.d.mts +2 -2
  84. package/types/content-config.d.mts +48 -4
  85. package/types/coverage.d.mts +1 -1
  86. package/types/e2e.d.mts +4 -4
  87. package/types/engine/address-charset.d.mts +45 -0
  88. package/types/engine/alias-index.d.mts +122 -0
  89. package/types/engine/base-compiler.d.mts +132 -4
  90. package/types/engine/content-address.d.mts +2 -2
  91. package/types/engine/content-format-check.d.mts +163 -0
  92. package/types/engine/content-format.d.mts +101 -0
  93. package/types/engine/content-links.d.mts +16 -1
  94. package/types/engine/content-lint.d.mts +6 -0
  95. package/types/engine/diagnostics.d.mts +29 -0
  96. package/types/engine/document-subtypes.d.mts +233 -0
  97. package/types/engine/field-spec.d.mts +76 -23
  98. package/types/engine/frontmatter-lint.d.mts +47 -2
  99. package/types/engine/generate.d.mts +15 -2
  100. package/types/engine/helpers.d.mts +22 -14
  101. package/types/engine/ids.d.mts +10 -0
  102. package/types/engine/index.d.mts +5 -0
  103. package/types/engine/item-registry.d.mts +21 -2
  104. package/types/engine/kb-manifest.d.mts +35 -8
  105. package/types/engine/map-notes.d.mts +21 -11
  106. package/types/engine/note-claims.d.mts +113 -0
  107. package/types/engine/note-vocabulary.d.mts +251 -0
  108. package/types/engine/pack-config.d.mts +4 -3
  109. package/types/engine/pack-router.d.mts +4 -4
  110. package/types/engine/prose-lint.d.mts +6 -2
  111. package/types/engine/retired-fields.d.mts +73 -2
  112. package/types/engine/schema-check.d.mts +182 -0
  113. package/types/engine/schema-extract.d.mts +1 -1
  114. package/types/engine/site-index.d.mts +1 -1
  115. package/types/engine/system-block.d.mts +281 -0
  116. package/types/engine/web-wikilinks.d.mts +23 -12
  117. package/types/engine/wikilink-syntax.d.mts +29 -0
  118. package/types/manifest.d.mts +1 -1
  119. package/types/sohl/actors.d.mts +62 -6
  120. package/types/sohl/document-subtypes.d.mts +14 -0
  121. package/types/sohl/index.d.mts +1 -0
  122. package/types/sohl/items.d.mts +21 -0
@@ -16,11 +16,17 @@
16
16
  *
17
17
  * Content notes link to one another with wikilinks rather than file paths:
18
18
  *
19
- * `[[type/shortcode|Text]]` a document of that type
19
+ * `[[type-shortcode|Text]]` a document of that type
20
+ * `[[type-shortcode|]]` the same, showing the target's current name
20
21
  * `[[Text]]` an alias unique within the source's own type
21
- * `[[type/shortcode#slug|T]]` a section (see below)
22
+ * `[[type-shortcode#slug|T]]` a section (see below)
22
23
  * `[[#slug|Text]]` a section of the source note itself
23
- * `[[doctype/shortcode|T]]` an item's *documentation* (see below)
24
+ * `[[doctype-shortcode|T]]` an item's *documentation* (see below)
25
+ *
26
+ * **The pipe decides which of the two namespaces a target belongs to** (#131),
27
+ * and neither falls back to the other — see {@link resolvesAsAddress}, which
28
+ * states the rule for both builds. A piped target is parsed by the address
29
+ * grammar; an unpiped one is looked up in the alias index.
24
30
  *
25
31
  * The qualifier is the note's **type**, which with its shortcode is the system's
26
32
  * logical identity: `(type, shortcode)` is unique by rule (see the Shortcode
@@ -28,13 +34,13 @@
28
34
  * unique per type, not per directory, so a directory qualifier would add nothing
29
35
  * to the address while breaking every inbound link the moment a note is refiled.
30
36
  *
31
- * The bare form is the same address with the qualifier left implicit: it resolves
32
- * against the aliases of the source's **own type**, so a `doc` reaches any other
33
- * `doc` by name wherever it is filed. Nothing narrower is consulted — a note's
37
+ * The bare form is a **name**, not an abbreviated address: it resolves against
38
+ * the aliases of the source's **own type**, so a `doc` reaches any other `doc`
39
+ * by name wherever it is filed. Nothing narrower is consulted — a note's
34
40
  * directory and its `category` play no part in resolution. Where two notes of a
35
41
  * type legitimately share a name (a rules page and a user-guide page both called
36
42
  * "Gear"), the bare form is ambiguous and resolves to neither; the author writes
37
- * the full `[[type/shortcode|Text]]` form instead.
43
+ * the `[[type-shortcode|Text]]` address instead.
38
44
  *
39
45
  * At compile time each becomes a Foundry UUID enricher, routed to the pack that
40
46
  * the target's type compiles into (see {@link packForType}):
@@ -82,18 +88,15 @@
82
88
 
83
89
  import crypto from "crypto";
84
90
 
85
- import {
86
- compendiumUuid,
87
- ITEM_PACK,
88
- packForType,
89
- pageUuid,
90
- PACK_BY_TYPE,
91
- } from "./ids.mjs";
91
+ import { compendiumUuid, ITEM_PACK, packForType, pageUuid, PACK_BY_TYPE } from "./ids.mjs";
92
92
  import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
93
93
  import { replaceOutsideCode } from "./code-fences.mjs";
94
94
  // The syntax lives in `./wikilink-syntax.mjs`, so the web resolver and this
95
95
  // one cannot disagree about what counts as a link.
96
- import { authoredLabel, WIKILINK, parseWikilink } from "./wikilink-syntax.mjs";
96
+ import { authoredLabel, WIKILINK, parseWikilink, resolvesAsAddress } from "./wikilink-syntax.mjs";
97
+ // The alias half of the two namespaces: what may be claimed, and how a claim
98
+ // is keyed. Shared with the site build and the link checker (#131).
99
+ import { aliasKey } from "./alias-index.mjs";
97
100
 
98
101
  export { ITEM_PACK, PACK_BY_TYPE, packForType };
99
102
 
@@ -194,11 +197,7 @@ export function readQualifier(target, types, packages) {
194
197
 
195
198
  const slash = target.lastIndexOf("/");
196
199
  if (slash > 0) {
197
- const read = readTypeAndCode(
198
- target.slice(0, slash),
199
- target.slice(slash + 1),
200
- types,
201
- );
200
+ const read = readTypeAndCode(target.slice(0, slash), target.slice(slash + 1), types);
202
201
  // A slash means qualified whether or not the type is real.
203
202
  return read ?? { reason: "unknown-type" };
204
203
  }
@@ -206,11 +205,7 @@ export function readQualifier(target, types, packages) {
206
205
  const hyphen = target.indexOf("-");
207
206
  if (hyphen > 0) {
208
207
  // A hyphen qualifies only on a known type; otherwise it is part of a name.
209
- return readTypeAndCode(
210
- target.slice(0, hyphen),
211
- target.slice(hyphen + 1),
212
- types,
213
- );
208
+ return readTypeAndCode(target.slice(0, hyphen), target.slice(hyphen + 1), types);
214
209
  }
215
210
  return null;
216
211
  }
@@ -313,23 +308,14 @@ export function buildWikilinkIndex(docs, packageId, foreign, contentPackage) {
313
308
  // An item's prose compiles into a separate JournalEntry, addressed
314
309
  // by the virtual `doc<type>` qualifier. Its id is derived from the
315
310
  // item's, so its address is knowable here too.
316
- docUuid: compendiumUuid(
317
- packageId,
318
- "doc",
319
- itemDocEntryId(d.id),
320
- d.docPack,
321
- ),
311
+ docUuid: compendiumUuid(packageId, "doc", itemDocEntryId(d.id), d.docPack),
322
312
  });
323
313
 
324
- if (d.shortcode)
325
- byShortcode.set(`${norm(d.type)}/${norm(d.shortcode)}`, d);
314
+ if (d.shortcode) byShortcode.set(`${norm(d.type)}/${norm(d.shortcode)}`, d);
326
315
  for (const a of d.aliases ?? []) {
327
- const key = `${norm(d.type)}|${norm(a)}`;
316
+ const key = aliasKey(d.type, a);
328
317
  // Second claimant poisons the alias: it can no longer be resolved.
329
- byAlias.set(
330
- key,
331
- byAlias.has(key) && byAlias.get(key) !== d ? null : d,
332
- );
318
+ byAlias.set(key, byAlias.has(key) && byAlias.get(key) !== d ? null : d);
333
319
  // Every claimant is kept alongside, because poisoning the alias
334
320
  // discards exactly the information needed to report the problem.
335
321
  // The note that *cites* an ambiguous alias is innocent — whoever
@@ -411,11 +397,7 @@ function findForeign(index, read) {
411
397
  const wanted = norm(read.itemDoc ? `doc${read.type}` : read.type);
412
398
  const shortcode = norm(read.shortcode);
413
399
  if (read.package) {
414
- return (
415
- index.foreign.get(
416
- `${read.package}-${wanted}-${shortcode}`.toLowerCase(),
417
- ) ?? null
418
- );
400
+ return index.foreign.get(`${read.package}-${wanted}-${shortcode}`.toLowerCase()) ?? null;
419
401
  }
420
402
  const hits = [];
421
403
  for (const [key, v] of index.foreign) {
@@ -490,159 +472,141 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
490
472
  // `offset` is the third replacer argument because the pattern has exactly
491
473
  // one capture group. It is what makes two identical unresolved links on
492
474
  // one note tellable apart, and a position reportable at all (#17).
493
- const out = replaceOutsideCode(
494
- markdown,
495
- WIKILINK,
496
- (all, rawInner, offset) => {
497
- const parsed = parseWikilink(rawInner);
498
- const { labelled } = parsed;
499
- let target = parsed.target;
500
- // An unlabelled link shows its interior verbatim, anchor included;
501
- // a labelled one shows its label. An *empty* label is not a label
502
- // `[[x|]]` means "show the target's name" and that reading
503
- // comes from {@link authoredLabel} so the web resolver cannot draw
504
- // the line somewhere else (#113).
505
- let text = labelled ? (authoredLabel(parsed) ?? "") : parsed.inner;
506
- const slug = parsed.anchor || null;
475
+ const out = replaceOutsideCode(markdown, WIKILINK, (all, rawInner, offset) => {
476
+ const parsed = parseWikilink(rawInner);
477
+ const { labelled } = parsed;
478
+ let target = parsed.target;
479
+ // An unlabelled link shows its interior verbatim, anchor included;
480
+ // a labelled one shows its label. An *empty* label is not a label
481
+ // — `[[x|]]` means "show the target's name" — and that reading
482
+ // comes from {@link authoredLabel} so the web resolver cannot draw
483
+ // the line somewhere else (#113).
484
+ let text = labelled ? (authoredLabel(parsed) ?? "") : parsed.inner;
485
+ const slug = parsed.anchor || null;
507
486
 
508
- // Resolve the document: same-page (empty target), type-shortcode, or alias.
509
- let doc;
510
- // Set when the qualifier was the virtual `doc<type>` form, so the UUID
511
- // is built against the item doc entry rather than the item itself.
512
- let itemDoc = false;
513
- // Set when the target was read as `type-shortcode` an address rather
514
- // than prose, which decides what an unlabelled link shows (#1409).
515
- let addressed = false;
516
- // Kept for the foreign fallback below, which needs the parsed address.
517
- let qualifiedRead = null;
518
- if (target === "" && slug) {
519
- doc = { type, id, pack, docPack };
520
- } else {
521
- const qualified = readQualifier(
487
+ // Resolve the document: same-page (empty target), an address, or an
488
+ // alias. **The pipe chooses which**, with no fallback either way
489
+ // (#131) see {@link resolvesAsAddress}.
490
+ let doc;
491
+ // Set when the qualifier was the virtual `doc<type>` form, so the UUID
492
+ // is built against the item doc entry rather than the item itself.
493
+ let itemDoc = false;
494
+ // Set when the target was read as an address, which is what decides
495
+ // whether a foreign manifest is consulted for it below.
496
+ let addressed = false;
497
+ // Kept for the foreign fallback below, which needs the parsed address.
498
+ let qualifiedRead = null;
499
+ if (target === "" && slug) {
500
+ doc = { type, id, pack, docPack };
501
+ } else if (resolvesAsAddress(parsed)) {
502
+ const qualified = readQualifier(target, index.types, index.packages);
503
+ qualifiedRead = qualified;
504
+ // The author wrote a pipe, so they meant an address. A target that
505
+ // does not parse as one is therefore a defect and not, as it was
506
+ // under the old resolve-by-shape rule, an invitation to try the
507
+ // alias index — which is what let a note *name* resolve here.
508
+ if (!qualified || qualified.reason) {
509
+ unresolved.push({
510
+ link: all,
522
511
  target,
523
- index.types,
524
- index.packages,
525
- );
526
- qualifiedRead = qualified;
527
- if (qualified?.reason) {
528
- unresolved.push({
529
- link: all,
530
- target,
531
- offset,
532
- reason: qualified.reason,
533
- });
534
- return unresolvedLink(text || target, target);
535
- }
536
- if (qualified) {
537
- addressed = true;
538
- itemDoc = qualified.itemDoc;
539
- doc = index.byShortcode.get(
540
- `${qualified.type}/${qualified.shortcode}`,
541
- );
542
- } else {
543
- const aliasKey = `${norm(type)}|${norm(target)}`;
544
- const hit = index.byAlias.get(aliasKey);
545
- if (hit === null) {
546
- unresolved.push({
547
- link: all,
548
- target,
549
- offset,
550
- reason: "ambiguous",
551
- // Who claimed it, so the report can name the collision
552
- // rather than the note that merely cites it (#13).
553
- candidates: (
554
- index.aliasClaims?.get(aliasKey) ?? []
555
- ).map((d) => ({
556
- type: d.type,
557
- shortcode: d.shortcode,
558
- name: d.name,
559
- })),
560
- });
561
- return unresolvedLink(text || target, target);
562
- }
563
- doc = hit;
564
- }
512
+ offset,
513
+ reason: qualified?.reason ?? "not-an-address",
514
+ addressed: true,
515
+ });
516
+ return unresolvedLink(text || target, target);
565
517
  }
566
- if (!doc) {
567
- // Nothing local answers. A foreign package may publish this
568
- // address, in which case the manifest hands back a complete UUID —
569
- // including, for a section link, the anchor's own — so nothing is
570
- // derived here.
571
- const hit = findForeign(index, qualifiedRead);
572
- if (hit) {
573
- const uuid = slug ? hit.anchors?.[slug] : hit.uuid;
574
- if (uuid) {
575
- return `@UUID[${uuid}]{${text || hit.name || target}}`;
576
- }
577
- unresolved.push({
578
- link: all,
579
- target,
580
- offset,
581
- reason: "unknown-anchor",
582
- addressed: true,
583
- });
584
- return unresolvedLink(text || target, target);
518
+ addressed = true;
519
+ itemDoc = qualified.itemDoc;
520
+ doc = index.byShortcode.get(`${qualified.type}/${qualified.shortcode}`);
521
+ } else {
522
+ const key = aliasKey(type, target);
523
+ const hit = index.byAlias.get(key);
524
+ if (hit === null) {
525
+ unresolved.push({
526
+ link: all,
527
+ target,
528
+ offset,
529
+ reason: "ambiguous",
530
+ // Who claimed it, so the report can name the collision
531
+ // rather than the note that merely cites it (#13).
532
+ candidates: (index.aliasClaims?.get(key) ?? []).map((d) => ({
533
+ type: d.type,
534
+ shortcode: d.shortcode,
535
+ name: d.name,
536
+ })),
537
+ });
538
+ return unresolvedLink(text || target, target);
539
+ }
540
+ doc = hit;
541
+ }
542
+ if (!doc) {
543
+ // Nothing local answers. A foreign package may publish this
544
+ // address, in which case the manifest hands back a complete UUID —
545
+ // including, for a section link, the anchor's own — so nothing is
546
+ // derived here.
547
+ const hit = findForeign(index, qualifiedRead);
548
+ if (hit) {
549
+ const uuid = slug ? hit.anchors?.[slug] : hit.uuid;
550
+ if (uuid) {
551
+ return `@UUID[${uuid}]{${text || hit.name || target}}`;
585
552
  }
586
553
  unresolved.push({
587
554
  link: all,
588
555
  target,
589
556
  offset,
590
- reason: "unknown",
591
- // A *qualified* address that resolves nowhere is a typo: every
592
- // package it could name is either built here or vendored, so
593
- // there is no third possibility left. A bare alias is not — it
594
- // may simply be prose.
595
- addressed: !!qualifiedRead && !qualifiedRead.reason,
557
+ reason: "unknown-anchor",
558
+ addressed: true,
596
559
  });
597
560
  return unresolvedLink(text || target, target);
598
561
  }
562
+ unresolved.push({
563
+ link: all,
564
+ target,
565
+ offset,
566
+ reason: "unknown",
567
+ // An *address* that resolves nowhere is a typo: every package
568
+ // it could name is either built here or vendored, so there is
569
+ // no third possibility left. A bare alias is not — it may
570
+ // simply be prose, or a worldbuilding placeholder.
571
+ addressed,
572
+ });
573
+ return unresolvedLink(text || target, target);
574
+ }
599
575
 
600
- // With no explicit label, a *qualified* target has no prose to show — a
601
- // shortcode is an address, not display text so the document's own name
602
- // stands in (#1409). A bare `[[Text]]` is already the prose the author
603
- // wrote, and substituting the canonical name there would rewrite the
604
- // sentence ("worsens the [[Shock State]]" must not render as "Shock").
605
- // The knowledgebase build reads the same authored link the same way.
606
- if (!text || (!labelled && addressed)) text = doc.name ?? target;
576
+ // An address with no label `[[skill-clmb|]]` has no prose to show,
577
+ // a shortcode being an address rather than display text, so the
578
+ // document's **current** name stands in and a rename shows at every
579
+ // citation with no link edited (#1409, #131). A bare `[[Text]]` is
580
+ // already the prose the author wrote, and substituting the canonical
581
+ // name there would rewrite the sentence ("worsens the [[Shock State]]"
582
+ // must not render as "Shock"). The knowledgebase build reads the same
583
+ // authored link the same way.
584
+ if (!text) text = doc.name ?? target;
607
585
 
608
- // Both addresses were computed when the target was indexed. An item
609
- // doc lives in the journals pack under its own derived entry id, and
610
- // its pages hash against *that* id — not the item's.
611
- //
612
- // The one target with no index entry is the note itself: a `[[#slug]]`
613
- // self-link is resolved from the source's own type and id, which the
614
- // caller supplied, so it is addressed the same way here.
615
- const addresses = index.uuidByDoc.get(doc) ?? {
616
- uuid: compendiumUuid(
617
- index.packageId,
618
- doc.type,
619
- doc.id,
620
- doc.pack,
621
- ),
622
- docUuid: compendiumUuid(
623
- index.packageId,
624
- "doc",
625
- itemDocEntryId(doc.id),
626
- doc.docPack,
627
- ),
628
- };
629
- const entryUuid = itemDoc ? addresses.docUuid : addresses.uuid;
630
- const entryId = itemDoc ? itemDocEntryId(doc.id) : doc.id;
631
- const isJournal =
632
- itemDoc || packForType(doc.type).docType === "JournalEntry";
633
- // A JournalEntry link opens a journal — at its first page, or at the
634
- // page an anchor names. An Item or Actor link opens that document's
635
- // *sheet*, which has no sections, so the anchor has nothing to address
636
- // and is dropped. Forging a JournalEntryPage id onto a document that
637
- // can never hold one is what made such links dead-end (#1362); an
638
- // item's pages are addressed through its `doc<type>` counterpart.
639
- const uuid =
640
- slug && isJournal ?
641
- pageUuid(entryUuid, anchorPageId(entryId, slug))
642
- : entryUuid;
643
- return `@UUID[${uuid}]{${text}}`;
644
- },
645
- );
586
+ // Both addresses were computed when the target was indexed. An item
587
+ // doc lives in the journals pack under its own derived entry id, and
588
+ // its pages hash against *that* id — not the item's.
589
+ //
590
+ // The one target with no index entry is the note itself: a `[[#slug]]`
591
+ // self-link is resolved from the source's own type and id, which the
592
+ // caller supplied, so it is addressed the same way here.
593
+ const addresses = index.uuidByDoc.get(doc) ?? {
594
+ uuid: compendiumUuid(index.packageId, doc.type, doc.id, doc.pack),
595
+ docUuid: compendiumUuid(index.packageId, "doc", itemDocEntryId(doc.id), doc.docPack),
596
+ };
597
+ const entryUuid = itemDoc ? addresses.docUuid : addresses.uuid;
598
+ const entryId = itemDoc ? itemDocEntryId(doc.id) : doc.id;
599
+ const isJournal = itemDoc || packForType(doc.type).docType === "JournalEntry";
600
+ // A JournalEntry link opens a journal — at its first page, or at the
601
+ // page an anchor names. An Item or Actor link opens that document's
602
+ // *sheet*, which has no sections, so the anchor has nothing to address
603
+ // and is dropped. Forging a JournalEntryPage id onto a document that
604
+ // can never hold one is what made such links dead-end (#1362); an
605
+ // item's pages are addressed through its `doc<type>` counterpart.
606
+ const uuid =
607
+ slug && isJournal ? pageUuid(entryUuid, anchorPageId(entryId, slug)) : entryUuid;
608
+ return `@UUID[${uuid}]{${text}}`;
609
+ });
646
610
 
647
611
  return { markdown: out, unresolved };
648
612
  }
package/index.mjs CHANGED
@@ -42,11 +42,7 @@
42
42
  // ── Content: notes to compendium packs, site content, link manifests ────────
43
43
 
44
44
  /** The configuration contract a consuming repository declares its build with. */
45
- export {
46
- defineConfig,
47
- PACKAGE_KINDS,
48
- PACK_DOCUMENT_TYPES,
49
- } from "./content-config.mjs";
45
+ export { defineConfig, PACKAGE_KINDS, PACK_DOCUMENT_TYPES } from "./content-config.mjs";
50
46
 
51
47
  /** The content pipeline — walking, compiling, linking, emitting. */
52
48
  export * as engine from "./engine/index.mjs";
package/lang.mjs CHANGED
@@ -116,9 +116,7 @@ export function validateLangSource(raw) {
116
116
  } catch (err) {
117
117
  // Nothing further can be said about a file that does not parse, and
118
118
  // guessing at its intended shape would only bury this finding.
119
- return [
120
- { severity: "error", message: `not valid JSON: ${err.message}` },
121
- ];
119
+ return [{ severity: "error", message: `not valid JSON: ${err.message}` }];
122
120
  }
123
121
 
124
122
  if (!isRecord(json)) {
package/manifest.mjs CHANGED
@@ -81,8 +81,7 @@ export const ARTIFACTS = Object.freeze(["system", "module"]);
81
81
  * worse than a missing one: Foundry installs it and never offers an update.
82
82
  */
83
83
  export function normalizeRepoUrl(repository) {
84
- const raw =
85
- typeof repository === "string" ? repository : (repository?.url ?? "");
84
+ const raw = typeof repository === "string" ? repository : (repository?.url ?? "");
86
85
  const url = String(raw)
87
86
  .trim()
88
87
  .replace(/^git\+/, "")
@@ -177,10 +176,7 @@ const MANIFEST_KEY_ORDER = Object.freeze([
177
176
  * @returns {object[]} The manifest's `packs` array.
178
177
  */
179
178
  export function manifestPacks(config) {
180
- const flatten = (pack) => [
181
- pack,
182
- ...(pack.companions ?? []).flatMap(flatten),
183
- ];
179
+ const flatten = (pack) => [pack, ...(pack.companions ?? []).flatMap(flatten)];
184
180
  return config.packs.flatMap(flatten).map((pack) => {
185
181
  // Foundry requires `system` on ActiveEffect, Actor and Item packs and
186
182
  // on no others, so the value is per pack: its own declaration first,
@@ -205,11 +201,7 @@ export function manifestPacks(config) {
205
201
  *
206
202
  * @type {readonly string[]}
207
203
  */
208
- const PACK_FOLDERS_PATH = Object.freeze([
209
- "packageBuild",
210
- "manifest",
211
- "packFolders",
212
- ]);
204
+ const PACK_FOLDERS_PATH = Object.freeze(["packageBuild", "manifest", "packFolders"]);
213
205
 
214
206
  /**
215
207
  * Every pack name a folder tree names, with the folder that named it.
@@ -371,8 +363,7 @@ export const BUILD_ONLY_RELATIONSHIP_KEYS = Object.freeze(["itemCatalog"]);
371
363
  export function publishedRelationships(relationships) {
372
364
  const published = {};
373
365
  for (const [kind, entries] of Object.entries(relationships)) {
374
- published[kind] =
375
- Array.isArray(entries) ? entries.map(withoutBuildKeys) : entries;
366
+ published[kind] = Array.isArray(entries) ? entries.map(withoutBuildKeys) : entries;
376
367
  }
377
368
  return published;
378
369
  }
@@ -388,9 +379,7 @@ function withoutBuildKeys(entry) {
388
379
  return entry;
389
380
  }
390
381
  return Object.fromEntries(
391
- Object.entries(entry).filter(
392
- ([key]) => !BUILD_ONLY_RELATIONSHIP_KEYS.includes(key),
393
- ),
382
+ Object.entries(entry).filter(([key]) => !BUILD_ONLY_RELATIONSHIP_KEYS.includes(key)),
394
383
  );
395
384
  }
396
385
 
@@ -447,23 +436,18 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
447
436
  const entry = {
448
437
  id: config.requiresSystem,
449
438
  type: "system",
450
- ...(declaredSystem?.manifest ?
451
- { manifest: declaredSystem.manifest }
452
- : {}),
439
+ ...(declaredSystem?.manifest ? { manifest: declaredSystem.manifest } : {}),
453
440
  ...(declaredSystem?.compatibility ?
454
441
  {
455
442
  compatibility: Object.fromEntries(
456
- Object.entries(declaredSystem.compatibility).filter(
457
- ([, v]) => v != null,
458
- ),
443
+ Object.entries(declaredSystem.compatibility).filter(([, v]) => v != null),
459
444
  ),
460
445
  }
461
446
  : {}),
462
447
  };
463
448
  // An explicit `relationships.systems` still wins, so a repository
464
449
  // mid-migration is never told two different things about itself.
465
- relationships.systems =
466
- relationships.systems?.length ? relationships.systems : [entry];
450
+ relationships.systems = relationships.systems?.length ? relationships.systems : [entry];
467
451
  }
468
452
  if (Object.keys(relationships).length) {
469
453
  derived.relationships = publishedRelationships(relationships);
@@ -553,14 +537,7 @@ function reportPackFolders(findings, configFile) {
553
537
  * @throws {Error} When a `packFolders` entry names a pack the package does not
554
538
  * ship. Nothing is written in that case.
555
539
  */
556
- export async function writeManifest({
557
- config,
558
- packageJson,
559
- artifact,
560
- outDir,
561
- flags,
562
- configFile,
563
- }) {
540
+ export async function writeManifest({ config, packageJson, artifact, outDir, flags, configFile }) {
564
541
  const manifest = buildManifest({ config, packageJson, artifact, flags });
565
542
 
566
543
  const errors = reportPackFolders(
@@ -583,10 +560,6 @@ export async function writeManifest({
583
560
  const outPath = path.join(outDir, `${artifact}.json`);
584
561
  // Trailing newline: the file is committed to a release archive and read by
585
562
  // humans as often as by Foundry.
586
- await fs.writeFile(
587
- outPath,
588
- `${JSON.stringify(manifest, null, 2)}\n`,
589
- "utf8",
590
- );
563
+ await fs.writeFile(outPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
591
564
  return { path: outPath, manifest };
592
565
  }
@@ -31,11 +31,7 @@
31
31
  * @module
32
32
  */
33
33
 
34
- import {
35
- MARKDOWNLINT_CONFIG,
36
- MARKDOWN_GLOBS,
37
- MARKDOWN_IGNORES,
38
- } from "./engine/prose-config.mjs";
34
+ import { MARKDOWNLINT_CONFIG, MARKDOWN_GLOBS, MARKDOWN_IGNORES } from "./engine/prose-config.mjs";
39
35
 
40
36
  export default {
41
37
  config: MARKDOWNLINT_CONFIG,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "8.1.0",
3
+ "version": "10.0.0",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -101,6 +101,7 @@
101
101
  "container.mjs",
102
102
  "content-config.mjs",
103
103
  "coverage.mjs",
104
+ "docs",
104
105
  "deploy.mjs",
105
106
  "e2e.mjs",
106
107
  "engine",
@@ -148,9 +149,12 @@
148
149
  "build:types": "tsc -p tsconfig.dts.json",
149
150
  "format": "prettier --write .",
150
151
  "format:check": "prettier --check .",
151
- "lint": "npm run format:check && npm run lint:markdown",
152
+ "lint": "npm run format:check && npm run lint:markdown && npm run lint:content-format",
152
153
  "lint:markdown": "node bin/content-build.mjs markdown",
153
154
  "lint:markdown:fix": "node bin/content-build.mjs markdown --fix",
155
+ "lint:content-format": "npm run lint:content-format:schema && npm run lint:content-format:fields",
156
+ "lint:content-format:schema": "node bin/content-build.mjs content-format schema --schema sohl=tests/fixtures/content-format/schema-sohl.json",
157
+ "lint:content-format:fields": "node bin/content-build.mjs content-format fields --fields sohl",
154
158
  "changeset": "changeset",
155
159
  "changeset:check": "changeset status --since=origin/main",
156
160
  "changeset:version": "changeset version && npm install --package-lock-only",