@heroiclands/package-build 22.21.0 → 22.23.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 (152) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/assets/styles/content-base.css +162 -0
  3. package/bin/content-commands.mjs +77 -17
  4. package/bin/package-build.mjs +15 -2
  5. package/ci/content-format-reference.mjs +68 -1
  6. package/config.mjs +26 -0
  7. package/content-config.mjs +153 -6
  8. package/docs/api.md +120 -89
  9. package/docs/authoring/assets.md +14 -0
  10. package/docs/authoring/dates-and-calendars.md +41 -3
  11. package/docs/authoring/frontmatter.md +2 -2
  12. package/docs/authoring/links-and-markup.md +325 -35
  13. package/docs/commands.md +12 -10
  14. package/docs/configuration.md +117 -14
  15. package/docs/content-format.md +4 -2
  16. package/docs/guides/book.md +5 -4
  17. package/docs/guides/reference-pages.md +15 -0
  18. package/docs/reference/format-details.md +545 -104
  19. package/docs/reference/note-types.md +30 -13
  20. package/engine/actor-compiler.mjs +23 -0
  21. package/engine/anchored-sections.mjs +35 -4
  22. package/engine/anchors.mjs +26 -30
  23. package/engine/art-fields.mjs +118 -0
  24. package/engine/art-slots.mjs +7 -1
  25. package/engine/asset-index.mjs +160 -10
  26. package/engine/asset-replacement.mjs +93 -0
  27. package/engine/asset-types.mjs +240 -0
  28. package/engine/base-compiler.mjs +139 -22
  29. package/engine/base-styles.mjs +149 -0
  30. package/engine/bundles.mjs +13 -2
  31. package/engine/compendiums.mjs +8 -1
  32. package/engine/content-blocks.mjs +410 -0
  33. package/engine/content-embeds.mjs +19 -0
  34. package/engine/content-figures.mjs +468 -0
  35. package/engine/content-footnotes.mjs +197 -0
  36. package/engine/content-format-check.mjs +15 -12
  37. package/engine/content-format.yaml +1 -1
  38. package/engine/content-images.mjs +126 -25
  39. package/engine/content-index.mjs +38 -12
  40. package/engine/content-links.mjs +14 -6
  41. package/engine/content-tables.mjs +45 -4
  42. package/engine/content-tasklists.mjs +189 -0
  43. package/engine/docs-checks.mjs +4 -4
  44. package/engine/extension-attributes.mjs +48 -2
  45. package/engine/field-reference.mjs +31 -10
  46. package/engine/field-spec.mjs +10 -8
  47. package/engine/foreign-catalog.mjs +143 -18
  48. package/engine/frontmatter-lint.mjs +120 -65
  49. package/engine/frontmatter.mjs +3 -3
  50. package/engine/heading-attributes.mjs +321 -0
  51. package/engine/helpers.mjs +109 -41
  52. package/engine/homepage.mjs +8 -8
  53. package/engine/index.mjs +3 -0
  54. package/engine/infobox-registry.mjs +4 -1
  55. package/engine/infobox-render.mjs +15 -3
  56. package/engine/infobox.mjs +129 -15
  57. package/engine/itinerary-scenes.mjs +4 -10
  58. package/engine/journals.mjs +294 -50
  59. package/engine/lore-events.mjs +197 -0
  60. package/engine/macros.mjs +1 -1
  61. package/engine/markdown-expressions.mjs +434 -59
  62. package/engine/note-dates.mjs +354 -12
  63. package/engine/note-frontmatter.mjs +7 -16
  64. package/engine/note-vocabulary.mjs +183 -42
  65. package/engine/page-lists.mjs +358 -0
  66. package/engine/pathnames.mjs +36 -0
  67. package/engine/pdf-build.mjs +431 -74
  68. package/engine/pdf-images.mjs +64 -0
  69. package/engine/pdf-render.mjs +563 -66
  70. package/engine/rank-ladder.mjs +198 -0
  71. package/engine/reckoning-markers.mjs +1 -1
  72. package/engine/retired-fields.mjs +8 -13
  73. package/engine/scenes.mjs +26 -7
  74. package/engine/site-build.mjs +166 -23
  75. package/engine/site-config.mjs +10 -1
  76. package/engine/site-index.mjs +90 -3
  77. package/engine/site-preview.mjs +20 -7
  78. package/engine/social-ties.mjs +52 -14
  79. package/engine/sql-tables.mjs +66 -9
  80. package/engine/standings.mjs +89 -6
  81. package/engine/system-block.mjs +56 -30
  82. package/engine/web-wikilinks.mjs +48 -1
  83. package/hm3/actors.mjs +8 -5
  84. package/manifest.mjs +43 -24
  85. package/package.json +1 -1
  86. package/packagebuild-metadata.jsonl +13 -13
  87. package/sohl/actors.mjs +22 -99
  88. package/sohl/item-fields.mjs +5 -5
  89. package/sohl/items.mjs +1 -1
  90. package/sohl/note-schemas.mjs +11 -21
  91. package/types/config.d.mts +9 -0
  92. package/types/content-config.d.mts +48 -1
  93. package/types/engine/actor-compiler.d.mts +11 -0
  94. package/types/engine/anchored-sections.d.mts +24 -0
  95. package/types/engine/anchors.d.mts +9 -4
  96. package/types/engine/art-fields.d.mts +64 -0
  97. package/types/engine/asset-index.d.mts +43 -0
  98. package/types/engine/asset-replacement.d.mts +22 -0
  99. package/types/engine/asset-types.d.mts +18 -0
  100. package/types/engine/base-compiler.d.mts +19 -17
  101. package/types/engine/base-styles.d.mts +93 -0
  102. package/types/engine/content-blocks.d.mts +87 -0
  103. package/types/engine/content-embeds.d.mts +16 -0
  104. package/types/engine/content-figures.d.mts +101 -0
  105. package/types/engine/content-footnotes.d.mts +80 -0
  106. package/types/engine/content-format-check.d.mts +8 -6
  107. package/types/engine/content-images.d.mts +78 -12
  108. package/types/engine/content-index.d.mts +8 -2
  109. package/types/engine/content-tables.d.mts +12 -4
  110. package/types/engine/content-tasklists.d.mts +72 -0
  111. package/types/engine/extension-attributes.d.mts +22 -1
  112. package/types/engine/field-reference.d.mts +21 -0
  113. package/types/engine/field-spec.d.mts +20 -16
  114. package/types/engine/foreign-catalog.d.mts +39 -0
  115. package/types/engine/frontmatter.d.mts +1 -1
  116. package/types/engine/heading-attributes.d.mts +179 -0
  117. package/types/engine/helpers.d.mts +29 -10
  118. package/types/engine/homepage.d.mts +7 -7
  119. package/types/engine/index.d.mts +1 -0
  120. package/types/engine/infobox-registry.d.mts +4 -1
  121. package/types/engine/infobox-render.d.mts +6 -1
  122. package/types/engine/infobox.d.mts +6 -0
  123. package/types/engine/journals.d.mts +41 -12
  124. package/types/engine/lore-events.d.mts +14 -0
  125. package/types/engine/markdown-expressions.d.mts +46 -2
  126. package/types/engine/note-dates.d.mts +64 -1
  127. package/types/engine/note-frontmatter.d.mts +1 -4
  128. package/types/engine/note-vocabulary.d.mts +82 -6
  129. package/types/engine/page-lists.d.mts +126 -0
  130. package/types/engine/pathnames.d.mts +26 -0
  131. package/types/engine/pdf-build.d.mts +28 -6
  132. package/types/engine/pdf-images.d.mts +52 -0
  133. package/types/engine/pdf-render.d.mts +84 -2
  134. package/types/engine/rank-ladder.d.mts +32 -0
  135. package/types/engine/retired-fields.d.mts +4 -6
  136. package/types/engine/site-build.d.mts +27 -4
  137. package/types/engine/site-config.d.mts +9 -0
  138. package/types/engine/site-index.d.mts +67 -1
  139. package/types/engine/site-preview.d.mts +1 -1
  140. package/types/engine/sql-tables.d.mts +31 -7
  141. package/types/engine/standings.d.mts +21 -3
  142. package/types/engine/system-block.d.mts +37 -14
  143. package/types/engine/web-wikilinks.d.mts +3 -1
  144. package/types/hm3/actors.d.mts +21 -3
  145. package/types/manifest.d.mts +19 -23
  146. package/types/sohl/actors.d.mts +6 -7
  147. package/engine/content-admonitions.mjs +0 -114
  148. package/engine/content-captions.mjs +0 -181
  149. package/engine/content-secrets.mjs +0 -112
  150. package/types/engine/content-admonitions.d.mts +0 -24
  151. package/types/engine/content-captions.d.mts +0 -45
  152. package/types/engine/content-secrets.d.mts +0 -17
@@ -62,9 +62,11 @@
62
62
  */
63
63
 
64
64
  import { authoredFrontmatter } from "./index-records.mjs";
65
- import { renderSecretBlocks } from "./content-secrets.mjs";
66
- import { scanAdmonitions } from "./content-admonitions.mjs";
67
- import { scanCaptions } from "./content-captions.mjs";
65
+ import { scanBlocks } from "./content-blocks.mjs";
66
+ import { scanHeadingAttributes, withheldSections } from "./heading-attributes.mjs";
67
+ import { scanFigures } from "./content-figures.mjs";
68
+ import { footnoteFindings } from "./content-footnotes.mjs";
69
+ import { checkImages } from "./content-images.mjs";
68
70
  import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
69
71
  import { isGmNote } from "./note-vocabulary.mjs";
70
72
  import { cloneAddressState } from "./address-values.mjs";
@@ -124,6 +126,25 @@ import { reckoningContext } from "./reckoning-markers.mjs";
124
126
  * says nothing about. Counted as errors, never as skips.
125
127
  */
126
128
 
129
+ /**
130
+ * A `ref` call's Foundry rendering: a same-page wikilink rather than a
131
+ * Markdown link, so the wikilink pass that runs after expressions render
132
+ * addresses it exactly as an authored `[[#anchor|Text]]` is — by the
133
+ * JournalEntryPage the figure's own fence became, whether that page holds
134
+ * only the figure or carries it alongside other prose. A Markdown link's
135
+ * `#anchor` fragment resolves nowhere in Foundry, which addresses a page by
136
+ * UUID and an anchor within a page by that page's own anchor; this is why the
137
+ * two surfaces do not share a renderer.
138
+ *
139
+ * @param {{anchor: string}} target - The figure's own anchor.
140
+ * @param {string} label - The link's text, which may itself carry Markdown.
141
+ * @returns {string} `[[#anchor|label]]`, with a literal `|` in the label
142
+ * escaped so it cannot be read as the wikilink's own separator.
143
+ */
144
+ function foundryRefLink({ anchor }, label) {
145
+ return `[[#${anchor}|${label.replace(/\|/g, "\\|")}]]`;
146
+ }
147
+
127
148
  /**
128
149
  * The shared walk → filter → expand → convert → build → write → count loop.
129
150
  *
@@ -614,6 +635,34 @@ export class BasePackCompiler {
614
635
  this.unresolvedLinks = 0;
615
636
  }
616
637
 
638
+ /**
639
+ * Report every finding {@link BasePackCompiler#convertBody} collected
640
+ * across its passes, and decline the note if there were any.
641
+ *
642
+ * Reported directly, one `noteError` per finding, rather than thrown as a
643
+ * single wrapped message: the site build and the book report the same way,
644
+ * each finding at its own line, and a note with three problems meets all
645
+ * three in the run that found them instead of one per rebuild. The thrown
646
+ * sentinel carries `alreadyReported` so the compile loop's generic catch
647
+ * does not add a fourth, vaguer line on top of the three already said.
648
+ *
649
+ * @param {readonly {message: string, line?: number, column?: number}[]} findings
650
+ * @throws {Error} When `findings` is non-empty, after reporting all of them.
651
+ */
652
+ reportConvertBodyFindings(findings) {
653
+ for (const finding of findings) {
654
+ this.errorCount++;
655
+ this.noteError(finding.message, { line: finding.line, column: finding.column });
656
+ }
657
+ if (findings.length) {
658
+ const error = new Error(
659
+ `${findings.length} finding(s) in the note body, each reported above`,
660
+ );
661
+ error.alreadyReported = true;
662
+ throw error;
663
+ }
664
+ }
665
+
617
666
  /**
618
667
  * The body {@link BasePackCompiler#buildEntry} receives.
619
668
  *
@@ -626,51 +675,114 @@ export class BasePackCompiler {
626
675
  * that does not convert.
627
676
  */
628
677
  convertBody(fm, body) {
629
- const secretError =
630
- renderSecretBlocks(body, "book").errors[0] ?? scanAdmonitions(body).errors[0];
631
- if (secretError) {
632
- const error = new Error(secretError.message);
633
- error.position = {
678
+ // Every pass's own findings are collected before any of them is
679
+ // reported, so a note with a malformed block *and* a malformed
680
+ // caption *and* a bad expression surfaces all three in one run
681
+ // instead of one per rebuild. Each pass already returns every finding
682
+ // it made — `errors` or `findings`, never just the first — so nothing
683
+ // here re-scans; it only stops taking `[0]`.
684
+ const findings = [];
685
+ for (const secretError of scanBlocks(body).errors) {
686
+ findings.push({
687
+ message: secretError.message,
634
688
  line: (this.currentNote?.bodyLine ?? 1) + secretError.line - 1,
635
689
  column:
636
690
  secretError.line === 1 ?
637
691
  (this.currentNote?.bodyColumn ?? 1)
638
692
  : secretError.column,
639
- };
640
- throw error;
693
+ });
694
+ }
695
+ // A heading's attribute block, and a `.secret` written where no surface
696
+ // can honour it. Read off the body as authored, before the tables
697
+ // expand, because a heading is a heading whatever a pass does with the
698
+ // prose under it — so a pass that converts nothing reports these too.
699
+ for (const headingError of [
700
+ ...scanHeadingAttributes(body, this.currentNote?.bodyLine ?? 1).errors,
701
+ ...withheldSections(body, this.currentNote?.bodyLine ?? 1).errors,
702
+ ]) {
703
+ findings.push({
704
+ message: headingError.message,
705
+ line: headingError.line,
706
+ column:
707
+ headingError.line === (this.currentNote?.bodyLine ?? 1) ?
708
+ (this.currentNote?.bodyColumn ?? 1) + headingError.column - 1
709
+ : headingError.column,
710
+ });
711
+ }
712
+ for (const footnoteError of footnoteFindings(body)) {
713
+ findings.push({
714
+ message: footnoteError.message,
715
+ line: (this.currentNote?.bodyLine ?? 1) + footnoteError.line - 1,
716
+ column:
717
+ footnoteError.line === 1 ?
718
+ (this.currentNote?.bodyColumn ?? 1)
719
+ : footnoteError.column,
720
+ });
721
+ }
722
+ // An image sharing its paragraph with other text, a bad address or a
723
+ // title with nowhere to draw it — the same findings `lint` reports,
724
+ // asked here too so a build run on its own refuses what the lint
725
+ // already names rather than rendering the directive as though it were
726
+ // absent. `checkImages` already resolves `bodyLine` and `bodyColumn`
727
+ // internally, so its findings need no further adjustment.
728
+ for (const imageError of checkImages(body, "", {
729
+ bodyLine: this.currentNote?.bodyLine ?? 1,
730
+ bodyColumn: this.currentNote?.bodyColumn ?? 1,
731
+ })) {
732
+ findings.push({
733
+ message: imageError.message,
734
+ line: imageError.line,
735
+ column: imageError.column,
736
+ });
737
+ }
738
+ if (!this.constructor.convertsWikilinks) {
739
+ this.reportConvertBodyFindings(findings);
740
+ return body;
641
741
  }
642
- if (!this.constructor.convertsWikilinks) return body;
643
742
  const name = resolveName(fm);
644
743
  const { absPath, bodyLine, bodyColumn } = this.currentNote ?? {};
645
744
  const { markdown: tabulated, lineMap } = expandNoteTables(body, {
646
745
  name,
647
746
  bodyLine,
648
747
  sqlTables: absPath ? this.sqlTables?.get(absPath) : undefined,
748
+ pageLists: absPath ? this.sqlTables?.pageLists?.get(absPath) : undefined,
649
749
  });
650
- const captionError = scanCaptions(tabulated).errors[0];
651
- if (captionError) {
652
- const error = new Error(captionError.message);
653
- error.position = {
750
+ const figureScan = scanFigures(tabulated);
751
+ for (const captionError of figureScan.errors) {
752
+ findings.push({
753
+ message: captionError.message,
654
754
  line:
655
755
  (bodyLine ?? 1) +
656
756
  (lineMap[captionError.line - 1]?.line ?? captionError.line - 1),
657
757
  column: captionError.column,
658
- };
659
- throw error;
758
+ });
660
759
  }
760
+ // What the `ref` expression helper needs: this note's own figures, by
761
+ // id, and how a reference renders — a same-page wikilink rather than
762
+ // a Markdown link, so the wikilink pass below addresses it exactly as
763
+ // an authored `[[#anchor|Text]]` is, by the page the figure's own
764
+ // fence became. Cross-note resolution is omitted: a journal addresses
765
+ // a page by UUID, which this pass has none of for another note.
766
+ const figuresById = new Map(
767
+ figureScan.figures
768
+ .filter((figure) => figure.id)
769
+ .map((figure) => [
770
+ figure.id,
771
+ { label: figure.label, caption: figure.caption, hasCaption: figure.hasCaption },
772
+ ]),
773
+ );
661
774
  const expressions = renderMarkdownExpressions(tabulated, {
662
775
  fm,
663
776
  dates: reckoningContext(this.linkIndex),
664
777
  sqlResults: absPath ? this.sqlTables?.inline?.get(absPath) : undefined,
665
778
  file: absPath,
666
779
  bodyLine,
780
+ figures: { get: (id) => figuresById.get(id), link: foundryRefLink },
667
781
  });
668
- if (expressions.findings.length) {
669
- const finding = expressions.findings[0];
670
- const error = new Error(finding.message);
671
- error.position = { line: finding.line, column: finding.column };
672
- throw error;
782
+ for (const finding of expressions.findings) {
783
+ findings.push({ message: finding.message, line: finding.line, column: finding.column });
673
784
  }
785
+ this.reportConvertBodyFindings(findings);
674
786
  const { markdown, unresolved } = convertNoteWikilinks(expressions.markdown, {
675
787
  type: fm.type,
676
788
  id: fm.id,
@@ -1237,6 +1349,11 @@ export class BasePackCompiler {
1237
1349
  stats.compiled++;
1238
1350
  this.onCompiled(fm, doc);
1239
1351
  } catch (err) {
1352
+ // `convertBody` already reported every finding it collected,
1353
+ // one `noteError` each, before throwing this sentinel — so the
1354
+ // generic wrap below would only restate "failed to compile" on
1355
+ // top of findings an author has already been told about.
1356
+ if (err.alreadyReported) continue;
1240
1357
  this.errorCount++;
1241
1358
  const repeatedAnchor = err.message.match(
1242
1359
  /declares the anchor \{#([^}]+)\} on more than one heading/,
@@ -0,0 +1,149 @@
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
+ * The **base stylesheet** every package ships, and where it lands.
16
+ *
17
+ * Compiled content relies on a handful of rules to be legible — a definition
18
+ * list is the whole of it today — and five packages need them. Four of those
19
+ * have no stylesheet at all, so writing the rules by hand means one edit and
20
+ * four new files, each with a configuration entry, each free to diverge from
21
+ * the first time one of them is touched. This repository already ships
22
+ * configuration to consumers and already generates the manifest that declares
23
+ * a package's `styles`, so it can ship the sheet the same way: one file,
24
+ * staged into every package, corrected in one place.
25
+ *
26
+ * A package keeps its own stylesheet for its own needs. The base sheet carries
27
+ * only what all of them share.
28
+ *
29
+ * Everything here is pure but for {@link baseStylesheetSource}, which resolves
30
+ * a path against this module's own location.
31
+ *
32
+ * @module
33
+ */
34
+
35
+ import path from "node:path";
36
+ import { fileURLToPath } from "node:url";
37
+
38
+ /**
39
+ * Where the shipped sheet lands inside the staged package.
40
+ *
41
+ * Under `styles/` beside a package's own sheet, and named for the toolchain so
42
+ * nothing a repository writes can collide with it. The path is relative to the
43
+ * staged package root, which is what both the stage entry and the manifest's
44
+ * `styles` are addressed in.
45
+ *
46
+ * @type {string}
47
+ */
48
+ export const BASE_STYLESHEET_DEST = "styles/package-build-base.css";
49
+
50
+ /**
51
+ * The cascade layer the sheet loads into, per package kind.
52
+ *
53
+ * Foundry declares its layer order in `css/foundry2.css`, ending
54
+ * `… layouts, system, modules, exceptions` — the eighth and ninth being, by its
55
+ * own comments, default game system styles and default module styles. That is
56
+ * what this sheet is, so it takes the layer its kind is given and is therefore
57
+ * the weakest thing a package can load:
58
+ *
59
+ * - **An unlayered package sheet beats it**, because an unlayered rule beats
60
+ * every layered one whatever its specificity.
61
+ * - **A package sheet naming any other layer beats it too**, because a layer
62
+ * Foundry's order does not mention is appended after the ones it does.
63
+ *
64
+ * So a package overrides the base sheet from its own, whichever way it declares
65
+ * one, and the override needs no `!important` and no specificity contest.
66
+ *
67
+ * @param {"system"|"module"} artifact - Which artifact is shipped.
68
+ * @returns {string} The layer name.
69
+ */
70
+ export function baseStyleLayer(artifact) {
71
+ return artifact === "system" ? "system" : "modules";
72
+ }
73
+
74
+ /**
75
+ * The manifest `styles` entry for the base sheet.
76
+ *
77
+ * Foundry's `styles` is an array of `{layer?, src}`, and a bare string is
78
+ * migrated into that shape on read, so the object form is written directly.
79
+ *
80
+ * @param {"system"|"module"} artifact - Which artifact is shipped.
81
+ * @returns {{src: string, layer: string}} The entry.
82
+ */
83
+ export function baseStyleEntry(artifact) {
84
+ return { src: BASE_STYLESHEET_DEST, layer: baseStyleLayer(artifact) };
85
+ }
86
+
87
+ /**
88
+ * The absolute path of the sheet as installed, so the stage can copy it.
89
+ *
90
+ * Resolved against this module rather than against the consumer's working
91
+ * directory, because the file belongs to this package and arrives through
92
+ * `node_modules`.
93
+ *
94
+ * @returns {string} Absolute path of the shipped stylesheet.
95
+ */
96
+ export function baseStylesheetSource() {
97
+ return path.resolve(
98
+ fileURLToPath(new URL("../assets/styles", import.meta.url)),
99
+ "content-base.css",
100
+ );
101
+ }
102
+
103
+ /**
104
+ * The `[source, destination]` pair that stages the sheet.
105
+ *
106
+ * The destination is relative, in the same spelling the repository's own asset
107
+ * table uses, so the caller resolves both against the repository root exactly
108
+ * as it resolves every other entry.
109
+ *
110
+ * @param {string} stageDir - The configured stage directory, relative to the
111
+ * repository root.
112
+ * @returns {[string, string]} The pair, for `stageAssets`.
113
+ */
114
+ export function baseStyleStageEntry(stageDir) {
115
+ return [baseStylesheetSource(), path.join(stageDir, BASE_STYLESHEET_DEST)];
116
+ }
117
+
118
+ /**
119
+ * The manifest's `styles`, with the base sheet named first.
120
+ *
121
+ * **First rather than last**, because `styles` is load order: a package's own
122
+ * sheet has to be read after the base one for an override written at equal
123
+ * specificity in an equal layer to take. The cascade layer makes the override
124
+ * work anyway, and the order makes it work for the right reason.
125
+ *
126
+ * A declared entry addressing the same file is not duplicated — a repository
127
+ * that named the staged path itself gets one entry, keeping whatever layer it
128
+ * chose, so declaring it is a way of placing it rather than an error.
129
+ *
130
+ * @param {object} options
131
+ * @param {unknown} [options.declared] - The declared `styles`, whatever shape
132
+ * it was written in; anything that is not a list is returned untouched.
133
+ * @param {"system"|"module"} options.artifact - Which artifact is shipped.
134
+ * @param {boolean} [options.enabled] - Whether the package takes the base
135
+ * sheet. `false` returns the declared list unchanged.
136
+ * @returns {unknown} The `styles` to emit, or `undefined` when there is
137
+ * nothing to emit at all.
138
+ */
139
+ export function withBaseStyle({ declared, artifact, enabled = true }) {
140
+ if (!enabled) return declared;
141
+ const list =
142
+ Array.isArray(declared) ? declared
143
+ : declared === undefined ? []
144
+ : declared;
145
+ if (!Array.isArray(list)) return list;
146
+ const names = (entry) => (typeof entry === "string" ? entry : entry?.src);
147
+ if (list.some((entry) => names(entry) === BASE_STYLESHEET_DEST)) return list;
148
+ return [baseStyleEntry(artifact), ...list];
149
+ }
@@ -75,6 +75,7 @@ import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
75
75
  import { folderField, renderFoundryMarkdown, resolveName } from "./helpers.mjs";
76
76
  import { packForType } from "./ids.mjs";
77
77
  import { readQualifier } from "./wikilinks.mjs";
78
+ import { pathnameRoles } from "./art-fields.mjs";
78
79
 
79
80
  /**
80
81
  * Load every compiled document a bundle may hold, keyed `<docType>/<id>`.
@@ -311,6 +312,12 @@ export class Bundles extends BasePackCompiler {
311
312
  contents.push({ docType: member.docType, document });
312
313
  }
313
314
 
315
+ // The markdown this pass reads has already had its embeds rewritten
316
+ // into ordinary images, each `src` the pathname `pathnameRoles` keys
317
+ // its roles by — so a `:::figure` of a map names itself `Map 1` here
318
+ // exactly as the website and the book name the same picture.
319
+ const roles = pathnameRoles(this.linkIndex);
320
+
314
321
  return buildAdventure({
315
322
  id: fm.id,
316
323
  name,
@@ -323,9 +330,13 @@ export class Bundles extends BasePackCompiler {
323
330
  // Foundry renders on the import card. That is why a bundle earns no
324
331
  // separate documentation journal the way an item does: the document
325
332
  // it compiles into already has somewhere to put the prose.
326
- description: markdown.trim() ? renderFoundryMarkdown(markdown) : "",
333
+ description:
334
+ markdown.trim() ?
335
+ renderFoundryMarkdown(markdown, undefined, undefined, undefined, (pathname) =>
336
+ roles.get(pathname),
337
+ )
338
+ : "",
327
339
  folder: this.folderResolver(authoredFolder, { isAddress: folderIsAddress }),
328
- flags: fm.flags,
329
340
  stats: this.stats,
330
341
  contents,
331
342
  });
@@ -191,7 +191,14 @@ function cleanPackEntry(
191
191
 
192
192
  if (data.effects) data.effects.forEach((i) => cleanPackEntry(i, { clearSourceId: false }));
193
193
  if (data.items) data.items.forEach((i) => cleanPackEntry(i, { clearSourceId: false }));
194
- if (data.pages) data.pages.forEach((i) => cleanPackEntry(i, { ownership: -1 }));
194
+ // A page's own stated default survives, because a page compiled as GM-only
195
+ // says so here and nowhere else; the per-user grants beside it do not, which
196
+ // is what the reset is for. `-1` is `INHERIT`, the value a page that says
197
+ // nothing takes.
198
+ if (data.pages)
199
+ data.pages.forEach((page) =>
200
+ cleanPackEntry(page, { ownership: page.ownership?.default ?? -1 }),
201
+ );
195
202
  if (data.system?.description) data.system.description = cleanString(data.system.description);
196
203
  if (data.system?.biography) data.system.biography = cleanString(data.system.biography);
197
204
  if (data.system?.textReference)