@heroiclands/package-build 22.20.0 → 22.22.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.
- package/CHANGELOG-content-build.md +0 -8
- package/CHANGELOG.md +204 -0
- package/assets/styles/content-base.css +162 -0
- package/bin/content-commands.mjs +26 -3
- package/bin/package-build.mjs +15 -2
- package/ci/content-format-reference.mjs +68 -1
- package/config.mjs +26 -0
- package/content-config.mjs +73 -4
- package/docs/api.md +46 -28
- package/docs/authoring/dates-and-calendars.md +4 -2
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +237 -28
- package/docs/configuration.md +27 -1
- package/docs/content-format.md +3 -1
- package/docs/guides/reference-pages.md +15 -0
- package/docs/reference/format-details.md +265 -42
- package/docs/reference/note-types.md +45 -28
- package/engine/actor-compiler.mjs +23 -0
- package/engine/anchored-sections.mjs +35 -4
- package/engine/anchors.mjs +10 -20
- package/engine/base-compiler.mjs +86 -21
- package/engine/base-styles.mjs +149 -0
- package/engine/bundles.mjs +0 -1
- package/engine/compendiums.mjs +8 -1
- package/engine/content-blocks.mjs +410 -0
- package/engine/content-captions.mjs +57 -8
- package/engine/content-footnotes.mjs +189 -0
- package/engine/content-format-check.mjs +15 -12
- package/engine/content-index.mjs +2 -2
- package/engine/content-links.mjs +14 -6
- package/engine/content-tables.mjs +45 -4
- package/engine/content-tasklists.mjs +189 -0
- package/engine/docs-checks.mjs +4 -4
- package/engine/extension-attributes.mjs +48 -2
- package/engine/frontmatter-lint.mjs +62 -66
- package/engine/frontmatter.mjs +3 -3
- package/engine/heading-attributes.mjs +308 -0
- package/engine/helpers.mjs +19 -13
- package/engine/homepage.mjs +8 -8
- package/engine/index.mjs +3 -0
- package/engine/infobox-render.mjs +21 -4
- package/engine/infobox.mjs +167 -7
- package/engine/journals.mjs +36 -17
- package/engine/macros.mjs +1 -1
- package/engine/markdown-expressions.mjs +178 -53
- package/engine/note-addresses.mjs +17 -8
- package/engine/note-dates.mjs +49 -0
- package/engine/note-frontmatter.mjs +7 -16
- package/engine/note-vocabulary.mjs +216 -42
- package/engine/page-lists.mjs +358 -0
- package/engine/pdf-build.mjs +76 -42
- package/engine/pdf-render.mjs +422 -49
- package/engine/rank-ladder.mjs +198 -0
- package/engine/reckoning-markers.mjs +1 -1
- package/engine/reference-targets.mjs +13 -1
- package/engine/retired-fields.mjs +8 -13
- package/engine/scenes.mjs +10 -3
- package/engine/site-build.mjs +41 -12
- package/engine/site-config.mjs +10 -1
- package/engine/site-index.mjs +39 -4
- package/engine/site-preview.mjs +18 -7
- package/engine/social-ties.mjs +52 -14
- package/engine/sql-tables.mjs +66 -9
- package/engine/standing-terms.mjs +19 -0
- package/engine/standings.mjs +538 -0
- package/engine/svg-theme.mjs +15 -4
- package/engine/system-block.mjs +12 -14
- package/engine/web-wikilinks.mjs +48 -1
- package/engine/wikilinks.mjs +3 -1
- package/hm3/actors.mjs +6 -3
- package/manifest.mjs +13 -0
- package/package.json +1 -1
- package/sohl/actors.mjs +82 -103
- package/sohl/item-fields.mjs +4 -5
- package/sohl/items.mjs +1 -1
- package/sohl/note-schemas.mjs +11 -21
- package/types/config.d.mts +9 -0
- package/types/engine/actor-compiler.d.mts +11 -0
- package/types/engine/anchored-sections.d.mts +24 -0
- package/types/engine/base-compiler.d.mts +19 -0
- package/types/engine/base-styles.d.mts +93 -0
- package/types/engine/content-blocks.d.mts +87 -0
- package/types/engine/content-captions.d.mts +14 -1
- package/types/engine/content-footnotes.d.mts +73 -0
- package/types/engine/content-format-check.d.mts +8 -6
- package/types/engine/content-tables.d.mts +12 -4
- package/types/engine/content-tasklists.d.mts +72 -0
- package/types/engine/extension-attributes.d.mts +22 -1
- package/types/engine/frontmatter.d.mts +1 -1
- package/types/engine/heading-attributes.d.mts +174 -0
- package/types/engine/helpers.d.mts +7 -4
- package/types/engine/homepage.d.mts +7 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox-render.d.mts +6 -1
- package/types/engine/journals.d.mts +14 -7
- package/types/engine/markdown-expressions.d.mts +23 -0
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-vocabulary.d.mts +98 -8
- package/types/engine/page-lists.d.mts +126 -0
- package/types/engine/pdf-render.d.mts +72 -2
- package/types/engine/rank-ladder.d.mts +32 -0
- package/types/engine/retired-fields.d.mts +4 -6
- package/types/engine/site-build.d.mts +13 -1
- package/types/engine/site-config.d.mts +9 -0
- package/types/engine/site-index.d.mts +13 -1
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -7
- package/types/engine/standing-terms.d.mts +16 -0
- package/types/engine/standings.d.mts +147 -0
- package/types/engine/system-block.d.mts +11 -12
- package/types/engine/web-wikilinks.d.mts +3 -1
- package/types/engine/wikilinks.d.mts +2 -1
- package/types/hm3/actors.d.mts +3 -3
- package/types/sohl/actors.d.mts +25 -7
- package/engine/content-admonitions.mjs +0 -114
- package/engine/content-secrets.mjs +0 -112
- package/types/engine/content-admonitions.d.mts +0 -24
- package/types/engine/content-secrets.d.mts +0 -17
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* **Anchored body sections** — the `# Heading {#anchor}` convention, and how a
|
|
16
16
|
* compiler pulls one section out of a note's prose.
|
|
17
17
|
*
|
|
18
|
-
* The content format gives
|
|
19
|
-
* `{#dossier}
|
|
18
|
+
* The content format gives two anchors a document meaning: `{#appearance}` and
|
|
19
|
+
* `{#dossier}`. *Which field* each lands in is a system's
|
|
20
20
|
* business — SoHL writes the first to an actor's `appearance`, HM3 to an
|
|
21
21
|
* actor's and an item's `description` — but *finding* it is not, so the
|
|
22
22
|
* extraction lives here where every compiler reaches it.
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import { renderFoundryMarkdown } from "./helpers.mjs";
|
|
33
|
+
import { collectAnchors } from "./anchors.mjs";
|
|
34
|
+
import { splitHeadingAttributes } from "./heading-attributes.mjs";
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* Extract the body of an H1 section whose heading carries the explicit
|
|
@@ -39,6 +41,11 @@ import { renderFoundryMarkdown } from "./helpers.mjs";
|
|
|
39
41
|
* heading exists. Fenced code blocks are respected so `# foo` inside
|
|
40
42
|
* ``` blocks does not trigger a match.
|
|
41
43
|
*
|
|
44
|
+
* An anchor declared on a lower heading is found by {@link collectAnchors}
|
|
45
|
+
* but not by the `#\s+` match below, so it extracts as absent rather than as
|
|
46
|
+
* wrong — {@link misplacedAnchorSection} is the check a caller runs first to
|
|
47
|
+
* tell the two apart.
|
|
48
|
+
*
|
|
42
49
|
* @param {string} body - The note body.
|
|
43
50
|
* @param {string} anchorId - The anchor to find.
|
|
44
51
|
* @returns {string} The section's markdown, or "".
|
|
@@ -57,8 +64,7 @@ export function extractAnchorSection(body, anchorId) {
|
|
|
57
64
|
}
|
|
58
65
|
const h1Match = !inCodeBlock ? line.match(/^\s*#\s+(.+?)\s*#*\s*$/) : null;
|
|
59
66
|
if (h1Match) {
|
|
60
|
-
const
|
|
61
|
-
const id = anchor?.[1]?.trim().toLowerCase() || null;
|
|
67
|
+
const id = splitHeadingAttributes(h1Match[1]).id.toLowerCase() || null;
|
|
62
68
|
if (capturing) break;
|
|
63
69
|
if (id === wanted) {
|
|
64
70
|
capturing = true;
|
|
@@ -81,3 +87,28 @@ export function renderSection(body, anchorId) {
|
|
|
81
87
|
const slice = extractAnchorSection(body, anchorId);
|
|
82
88
|
return slice ? renderFoundryMarkdown(slice) : "";
|
|
83
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Where an `{#appearance}` or `{#dossier}` anchor sits on a heading below the
|
|
93
|
+
* top level, which {@link extractAnchorSection} cannot read.
|
|
94
|
+
*
|
|
95
|
+
* {@link module:engine/anchors.collectAnchors} finds the anchor at any
|
|
96
|
+
* level, because a lower heading legitimately starts a journal page. It does
|
|
97
|
+
* not feed an actor field: an H1-only extractor reads nothing from one, and a
|
|
98
|
+
* caller that trusts the empty result as "no such section" rather than
|
|
99
|
+
* "found, but unreadable" compiles a blank field with nothing to say why.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} body - The note body.
|
|
102
|
+
* @param {string} anchorId - The anchor to check, case-insensitively.
|
|
103
|
+
* @param {number} [bodyLine=1] - The 1-based file line the body starts on.
|
|
104
|
+
* @returns {{line: number}|null} The misplaced anchor's line, or `null` when
|
|
105
|
+
* it is absent or already on an H1.
|
|
106
|
+
*/
|
|
107
|
+
export function misplacedAnchorSection(body, anchorId, bodyLine = 1) {
|
|
108
|
+
const wanted = String(anchorId).toLowerCase();
|
|
109
|
+
const found = collectAnchors(body, bodyLine).find(
|
|
110
|
+
(anchor) => anchor.slug.toLowerCase() === wanted,
|
|
111
|
+
);
|
|
112
|
+
if (!found || found.level === 1) return null;
|
|
113
|
+
return { line: found.line };
|
|
114
|
+
}
|
package/engine/anchors.mjs
CHANGED
|
@@ -22,23 +22,16 @@
|
|
|
22
22
|
* anchors does this note declare?" had two answers and they disagreed on
|
|
23
23
|
* `{#CalendarFormat}`.
|
|
24
24
|
*
|
|
25
|
+
* The suffix itself is read by {@link module:engine/heading-attributes}, which
|
|
26
|
+
* {@link module:engine/journals.splitPages} also reads it through — so the index
|
|
27
|
+
* cannot name an anchor the pages do not carry.
|
|
28
|
+
*
|
|
25
29
|
* @module
|
|
26
30
|
*/
|
|
27
31
|
|
|
28
32
|
import { scanCaptions } from "./content-captions.mjs";
|
|
33
|
+
import { HEADING_LINE, splitHeadingAttributes } from "./heading-attributes.mjs";
|
|
29
34
|
|
|
30
|
-
/**
|
|
31
|
-
* A heading, and the `{#slug}` anchor it declares.
|
|
32
|
-
*
|
|
33
|
-
* Kept identical to the pair {@link splitPages} matches, because the two must
|
|
34
|
-
* agree about what an anchor is: that pass decides which sections become
|
|
35
|
-
* addressable journal pages, and an index naming an anchor it does not produce
|
|
36
|
-
* would advertise a link that resolves nowhere. `tests/content-index.test.ts`
|
|
37
|
-
* asserts the two find the same anchors, so drift fails the suite rather than
|
|
38
|
-
* shipping.
|
|
39
|
-
*/
|
|
40
|
-
const HEADING = /^\s*(#{1,6})\s+(.+?)\s*#*\s*$/;
|
|
41
|
-
const ANCHOR = /^(.*?)\s*\{#([^}]+)\}\s*$/;
|
|
42
35
|
/**
|
|
43
36
|
* The `{#slug}` and `:::caption {#slug}` anchors a note declares.
|
|
44
37
|
*
|
|
@@ -66,16 +59,13 @@ export function collectAnchors(body, bodyLine = 1) {
|
|
|
66
59
|
}
|
|
67
60
|
if (inCodeBlock) continue;
|
|
68
61
|
|
|
69
|
-
const heading =
|
|
62
|
+
const heading = HEADING_LINE.exec(lines[i]);
|
|
70
63
|
if (!heading) continue;
|
|
71
|
-
const
|
|
72
|
-
if (!
|
|
73
|
-
|
|
74
|
-
const slug = anchor[2].trim();
|
|
75
|
-
if (!slug) continue;
|
|
64
|
+
const attributes = splitHeadingAttributes(heading[2]);
|
|
65
|
+
if (!attributes.id) continue;
|
|
76
66
|
anchors.push({
|
|
77
|
-
slug,
|
|
78
|
-
name:
|
|
67
|
+
slug: attributes.id,
|
|
68
|
+
name: attributes.text,
|
|
79
69
|
level: heading[1].length,
|
|
80
70
|
line: bodyLine + i,
|
|
81
71
|
});
|
package/engine/base-compiler.mjs
CHANGED
|
@@ -62,9 +62,10 @@
|
|
|
62
62
|
*/
|
|
63
63
|
|
|
64
64
|
import { authoredFrontmatter } from "./index-records.mjs";
|
|
65
|
-
import {
|
|
66
|
-
import {
|
|
65
|
+
import { scanBlocks } from "./content-blocks.mjs";
|
|
66
|
+
import { scanHeadingAttributes, withheldSections } from "./heading-attributes.mjs";
|
|
67
67
|
import { scanCaptions } from "./content-captions.mjs";
|
|
68
|
+
import { footnoteFindings } from "./content-footnotes.mjs";
|
|
68
69
|
import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
|
|
69
70
|
import { isGmNote } from "./note-vocabulary.mjs";
|
|
70
71
|
import { cloneAddressState } from "./address-values.mjs";
|
|
@@ -614,6 +615,34 @@ export class BasePackCompiler {
|
|
|
614
615
|
this.unresolvedLinks = 0;
|
|
615
616
|
}
|
|
616
617
|
|
|
618
|
+
/**
|
|
619
|
+
* Report every finding {@link BasePackCompiler#convertBody} collected
|
|
620
|
+
* across its passes, and decline the note if there were any.
|
|
621
|
+
*
|
|
622
|
+
* Reported directly, one `noteError` per finding, rather than thrown as a
|
|
623
|
+
* single wrapped message: the site build and the book report the same way,
|
|
624
|
+
* each finding at its own line, and a note with three problems meets all
|
|
625
|
+
* three in the run that found them instead of one per rebuild. The thrown
|
|
626
|
+
* sentinel carries `alreadyReported` so the compile loop's generic catch
|
|
627
|
+
* does not add a fourth, vaguer line on top of the three already said.
|
|
628
|
+
*
|
|
629
|
+
* @param {readonly {message: string, line?: number, column?: number}[]} findings
|
|
630
|
+
* @throws {Error} When `findings` is non-empty, after reporting all of them.
|
|
631
|
+
*/
|
|
632
|
+
reportConvertBodyFindings(findings) {
|
|
633
|
+
for (const finding of findings) {
|
|
634
|
+
this.errorCount++;
|
|
635
|
+
this.noteError(finding.message, { line: finding.line, column: finding.column });
|
|
636
|
+
}
|
|
637
|
+
if (findings.length) {
|
|
638
|
+
const error = new Error(
|
|
639
|
+
`${findings.length} finding(s) in the note body, each reported above`,
|
|
640
|
+
);
|
|
641
|
+
error.alreadyReported = true;
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
617
646
|
/**
|
|
618
647
|
* The body {@link BasePackCompiler#buildEntry} receives.
|
|
619
648
|
*
|
|
@@ -626,37 +655,70 @@ export class BasePackCompiler {
|
|
|
626
655
|
* that does not convert.
|
|
627
656
|
*/
|
|
628
657
|
convertBody(fm, body) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
658
|
+
// Every pass's own findings are collected before any of them is
|
|
659
|
+
// reported, so a note with a malformed block *and* a malformed
|
|
660
|
+
// caption *and* a bad expression surfaces all three in one run
|
|
661
|
+
// instead of one per rebuild. Each pass already returns every finding
|
|
662
|
+
// it made — `errors` or `findings`, never just the first — so nothing
|
|
663
|
+
// here re-scans; it only stops taking `[0]`.
|
|
664
|
+
const findings = [];
|
|
665
|
+
for (const secretError of scanBlocks(body).errors) {
|
|
666
|
+
findings.push({
|
|
667
|
+
message: secretError.message,
|
|
634
668
|
line: (this.currentNote?.bodyLine ?? 1) + secretError.line - 1,
|
|
635
669
|
column:
|
|
636
670
|
secretError.line === 1 ?
|
|
637
671
|
(this.currentNote?.bodyColumn ?? 1)
|
|
638
672
|
: secretError.column,
|
|
639
|
-
};
|
|
640
|
-
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
// A heading's attribute block, and a `.secret` written where no surface
|
|
676
|
+
// can honour it. Read off the body as authored, before the tables
|
|
677
|
+
// expand, because a heading is a heading whatever a pass does with the
|
|
678
|
+
// prose under it — so a pass that converts nothing reports these too.
|
|
679
|
+
for (const headingError of [
|
|
680
|
+
...scanHeadingAttributes(body, this.currentNote?.bodyLine ?? 1).errors,
|
|
681
|
+
...withheldSections(body, this.currentNote?.bodyLine ?? 1).errors,
|
|
682
|
+
]) {
|
|
683
|
+
findings.push({
|
|
684
|
+
message: headingError.message,
|
|
685
|
+
line: headingError.line,
|
|
686
|
+
column:
|
|
687
|
+
headingError.line === (this.currentNote?.bodyLine ?? 1) ?
|
|
688
|
+
(this.currentNote?.bodyColumn ?? 1) + headingError.column - 1
|
|
689
|
+
: headingError.column,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
for (const footnoteError of footnoteFindings(body)) {
|
|
693
|
+
findings.push({
|
|
694
|
+
message: footnoteError.message,
|
|
695
|
+
line: (this.currentNote?.bodyLine ?? 1) + footnoteError.line - 1,
|
|
696
|
+
column:
|
|
697
|
+
footnoteError.line === 1 ?
|
|
698
|
+
(this.currentNote?.bodyColumn ?? 1)
|
|
699
|
+
: footnoteError.column,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
if (!this.constructor.convertsWikilinks) {
|
|
703
|
+
this.reportConvertBodyFindings(findings);
|
|
704
|
+
return body;
|
|
641
705
|
}
|
|
642
|
-
if (!this.constructor.convertsWikilinks) return body;
|
|
643
706
|
const name = resolveName(fm);
|
|
644
707
|
const { absPath, bodyLine, bodyColumn } = this.currentNote ?? {};
|
|
645
708
|
const { markdown: tabulated, lineMap } = expandNoteTables(body, {
|
|
646
709
|
name,
|
|
647
710
|
bodyLine,
|
|
648
711
|
sqlTables: absPath ? this.sqlTables?.get(absPath) : undefined,
|
|
712
|
+
pageLists: absPath ? this.sqlTables?.pageLists?.get(absPath) : undefined,
|
|
649
713
|
});
|
|
650
|
-
const captionError
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
error.position = {
|
|
714
|
+
for (const captionError of scanCaptions(tabulated).errors) {
|
|
715
|
+
findings.push({
|
|
716
|
+
message: captionError.message,
|
|
654
717
|
line:
|
|
655
718
|
(bodyLine ?? 1) +
|
|
656
719
|
(lineMap[captionError.line - 1]?.line ?? captionError.line - 1),
|
|
657
720
|
column: captionError.column,
|
|
658
|
-
};
|
|
659
|
-
throw error;
|
|
721
|
+
});
|
|
660
722
|
}
|
|
661
723
|
const expressions = renderMarkdownExpressions(tabulated, {
|
|
662
724
|
fm,
|
|
@@ -665,12 +727,10 @@ export class BasePackCompiler {
|
|
|
665
727
|
file: absPath,
|
|
666
728
|
bodyLine,
|
|
667
729
|
});
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const error = new Error(finding.message);
|
|
671
|
-
error.position = { line: finding.line, column: finding.column };
|
|
672
|
-
throw error;
|
|
730
|
+
for (const finding of expressions.findings) {
|
|
731
|
+
findings.push({ message: finding.message, line: finding.line, column: finding.column });
|
|
673
732
|
}
|
|
733
|
+
this.reportConvertBodyFindings(findings);
|
|
674
734
|
const { markdown, unresolved } = convertNoteWikilinks(expressions.markdown, {
|
|
675
735
|
type: fm.type,
|
|
676
736
|
id: fm.id,
|
|
@@ -1237,6 +1297,11 @@ export class BasePackCompiler {
|
|
|
1237
1297
|
stats.compiled++;
|
|
1238
1298
|
this.onCompiled(fm, doc);
|
|
1239
1299
|
} catch (err) {
|
|
1300
|
+
// `convertBody` already reported every finding it collected,
|
|
1301
|
+
// one `noteError` each, before throwing this sentinel — so the
|
|
1302
|
+
// generic wrap below would only restate "failed to compile" on
|
|
1303
|
+
// top of findings an author has already been told about.
|
|
1304
|
+
if (err.alreadyReported) continue;
|
|
1240
1305
|
this.errorCount++;
|
|
1241
1306
|
const repeatedAnchor = err.message.match(
|
|
1242
1307
|
/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
|
+
}
|
package/engine/bundles.mjs
CHANGED
|
@@ -325,7 +325,6 @@ export class Bundles extends BasePackCompiler {
|
|
|
325
325
|
// it compiles into already has somewhere to put the prose.
|
|
326
326
|
description: markdown.trim() ? renderFoundryMarkdown(markdown) : "",
|
|
327
327
|
folder: this.folderResolver(authoredFolder, { isAddress: folderIsAddress }),
|
|
328
|
-
flags: fm.flags,
|
|
329
328
|
stats: this.stats,
|
|
330
329
|
contents,
|
|
331
330
|
});
|
package/engine/compendiums.mjs
CHANGED
|
@@ -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
|
-
|
|
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)
|