@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
package/engine/web-wikilinks.mjs
CHANGED
|
@@ -64,6 +64,26 @@ export { slugify };
|
|
|
64
64
|
import { authoredLabel, WIKILINK, isSamePage, parseWikilink } from "./wikilink-syntax.mjs";
|
|
65
65
|
import { resolveEmbeds } from "./content-embeds.mjs";
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Whether a page's declared anchors include the slug a link names.
|
|
69
|
+
*
|
|
70
|
+
* A local page's entry carries a `Set` (built by {@link module:engine/site-index});
|
|
71
|
+
* a foreign one carries the `{slug: uuid}` map a fetched manifest publishes.
|
|
72
|
+
* `undefined` means neither build recorded an answer, which keeps a target
|
|
73
|
+
* this check cannot speak for exactly as unchecked as it always was.
|
|
74
|
+
*
|
|
75
|
+
* @param {Set<string>|Record<string, string>|undefined} anchors - The page's
|
|
76
|
+
* declared anchors, or `undefined` where none were recorded.
|
|
77
|
+
* @param {string} slug - The anchor a link names.
|
|
78
|
+
* @returns {boolean} Whether the anchor is declared, or `true` when `anchors`
|
|
79
|
+
* is `undefined` — which reads as "nothing to check against" rather than
|
|
80
|
+
* "declares none".
|
|
81
|
+
*/
|
|
82
|
+
function hasAnchor(anchors, slug) {
|
|
83
|
+
if (!anchors) return true;
|
|
84
|
+
return anchors instanceof Set ? anchors.has(slug) : Object.hasOwn(anchors, slug);
|
|
85
|
+
}
|
|
86
|
+
|
|
67
87
|
/**
|
|
68
88
|
* The index key a **piped** target resolves to, or `null` when it does not
|
|
69
89
|
* parse as an address at all.
|
|
@@ -315,7 +335,9 @@ function isPlainMap(value) {
|
|
|
315
335
|
* resolves against. `src` is the page's display
|
|
316
336
|
* path and `file` the source file a diagnostic should name — absent, `src`
|
|
317
337
|
* stands in. `resolved`, when supplied, is the array every resolved
|
|
318
|
-
* target's index entry is appended to.
|
|
338
|
+
* target's index entry is appended to. `anchors` is the `{#slug}` set
|
|
339
|
+
* *this* page declares, which a `[[#slug]]` self-link is checked against;
|
|
340
|
+
* absent, a self-link is not checked, exactly as before this existed.
|
|
319
341
|
* @returns {string} The body with embeds and wikilinks rewritten.
|
|
320
342
|
*/
|
|
321
343
|
export function resolveWebWikilinks(body, ctx) {
|
|
@@ -399,6 +421,16 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
399
421
|
|
|
400
422
|
// `[[#section-slug|Text]]` — a section of this same page.
|
|
401
423
|
if (isSamePage({ target, anchor })) {
|
|
424
|
+
// Checked against this page's own anchors, exactly as a link into
|
|
425
|
+
// another page is: a self-link naming a section the page does not
|
|
426
|
+
// declare is as dead as one naming a section nowhere does.
|
|
427
|
+
if (!hasAnchor(ctx.anchors, anchor)) {
|
|
428
|
+
return report(
|
|
429
|
+
all,
|
|
430
|
+
{ target, reason: "unknown-anchor", anchor, addressed: true },
|
|
431
|
+
label ?? anchor,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
402
434
|
return `[${label ?? ctx.captionLabels?.get(anchor) ?? anchor}](#${slugify(anchor)})`;
|
|
403
435
|
}
|
|
404
436
|
|
|
@@ -419,6 +451,21 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
419
451
|
lookupRead(ctx.foreign, read, ctx.contentPackage)
|
|
420
452
|
: undefined);
|
|
421
453
|
if (hit) {
|
|
454
|
+
// A `#section` the target declares no heading for. Checked only
|
|
455
|
+
// where there is a page to check against — a pack-only hit has no
|
|
456
|
+
// `url` and therefore nothing for the anchor to address, which is
|
|
457
|
+
// the asset-like case this must not touch. `anchorPageId` on the
|
|
458
|
+
// pack build and the KB manifest's own `{slug: uuid}` map both
|
|
459
|
+
// hash any slug into something that resolves, so an undeclared
|
|
460
|
+
// one otherwise joins onto the URL unchecked and dead-ends for
|
|
461
|
+
// the reader.
|
|
462
|
+
if (anchor && hit.url && !hasAnchor(hit.anchors, anchor)) {
|
|
463
|
+
return report(
|
|
464
|
+
all,
|
|
465
|
+
{ target, reason: "unknown-anchor", anchor, addressed: true },
|
|
466
|
+
label ?? hit.name ?? target,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
422
469
|
// The edge, for a caller reading the link graph off this pass.
|
|
423
470
|
ctx.resolved?.push(hit);
|
|
424
471
|
// An address with an *empty* label has no prose to show (a
|
package/engine/wikilinks.mjs
CHANGED
|
@@ -583,7 +583,7 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index, cap
|
|
|
583
583
|
* @param {object} index - From {@link buildWikilinkIndex}.
|
|
584
584
|
* @param {unknown} ref - The reference, as authored.
|
|
585
585
|
* @param {object} [hint] - `{type}`, where the caller knows what it expects.
|
|
586
|
-
* @returns {{name?: string, uuid?: string, address?: import("./address.mjs").AddressTuple, subType?: string}|undefined}
|
|
586
|
+
* @returns {{name?: string, uuid?: string, address?: import("./address.mjs").AddressTuple, subType?: string, standings?: object}|undefined}
|
|
587
587
|
* The target, or `undefined` where nothing answers.
|
|
588
588
|
*/
|
|
589
589
|
export function resolveReference(index, ref, hint) {
|
|
@@ -611,6 +611,7 @@ export function resolveReference(index, ref, hint) {
|
|
|
611
611
|
return {
|
|
612
612
|
name: published.name,
|
|
613
613
|
subType: published.subType,
|
|
614
|
+
...(published.standings ? { standings: published.standings } : {}),
|
|
614
615
|
...(published.uuid ? { uuid: published.uuid } : {}),
|
|
615
616
|
address: tuple,
|
|
616
617
|
};
|
|
@@ -638,6 +639,7 @@ export function resolveReference(index, ref, hint) {
|
|
|
638
639
|
return {
|
|
639
640
|
name: local.name,
|
|
640
641
|
subType: local.subType,
|
|
642
|
+
...(local.standings ? { standings: local.standings } : {}),
|
|
641
643
|
...(uuid ? { uuid } : {}),
|
|
642
644
|
address: tuple,
|
|
643
645
|
};
|
package/hm3/actors.mjs
CHANGED
|
@@ -227,9 +227,9 @@ export class Hm3Actors extends SystemActorCompiler {
|
|
|
227
227
|
/**
|
|
228
228
|
* Build every embedded item an HM3 actor carries, from `hm3.items`.
|
|
229
229
|
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
230
|
+
* HM3 keeps a character's abilities in `system.abilities` rather than as
|
|
231
|
+
* embedded documents, so an ability reaches the document through the
|
|
232
|
+
* `hm3.system` passthrough and never through this list.
|
|
233
233
|
*
|
|
234
234
|
* @param {Map<string, object>} itemsMap - The predefined items, by address.
|
|
235
235
|
* @param {string} actorId - The owning actor's id, seeding embedded ids.
|
|
@@ -333,6 +333,9 @@ export class Hm3Actors extends SystemActorCompiler {
|
|
|
333
333
|
isAddress: true,
|
|
334
334
|
});
|
|
335
335
|
|
|
336
|
+
this.checkAnchoredSection(body || "", "appearance", ctx);
|
|
337
|
+
this.checkAnchoredSection(body || "", "dossier", ctx);
|
|
338
|
+
|
|
336
339
|
const system = {
|
|
337
340
|
// `system.bioImage` is **not** written here, for the reason SoHL's
|
|
338
341
|
// `system.portrait` is not: a being's portrait is the lead image of
|
package/manifest.mjs
CHANGED
|
@@ -52,6 +52,7 @@ import fs from "node:fs/promises";
|
|
|
52
52
|
import fsSync from "node:fs";
|
|
53
53
|
import path from "node:path";
|
|
54
54
|
|
|
55
|
+
import { withBaseStyle } from "./engine/base-styles.mjs";
|
|
55
56
|
import { emitDiagnostic, formatDiagnostic, positionOfYamlPath } from "./engine/diagnostics.mjs";
|
|
56
57
|
import { metadataFileName } from "./engine/metadata-index.mjs";
|
|
57
58
|
|
|
@@ -548,6 +549,18 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
|
|
|
548
549
|
|
|
549
550
|
const merged = { ...declared, ...derived };
|
|
550
551
|
|
|
552
|
+
// The shared base stylesheet, named ahead of whatever the package declares.
|
|
553
|
+
// Derived rather than declared, because every package needs it and four of
|
|
554
|
+
// the five that do have no stylesheet of their own to name it from. See
|
|
555
|
+
// `engine/base-styles.mjs` for the layer it loads into and why that is what
|
|
556
|
+
// lets a package override it.
|
|
557
|
+
const styles = withBaseStyle({
|
|
558
|
+
declared: declared.styles,
|
|
559
|
+
artifact: /** @type {"system"|"module"} */ (artifact),
|
|
560
|
+
enabled: config.packageBuild?.baseStyles !== false,
|
|
561
|
+
});
|
|
562
|
+
if (styles !== undefined) merged.styles = styles;
|
|
563
|
+
|
|
551
564
|
// The index every consumer resolves this package's addresses through.
|
|
552
565
|
// Written unconditionally, because a package that publishes no
|
|
553
566
|
// index is one nothing can link into — and the failure of an absent one is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.22.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",
|
package/sohl/actors.mjs
CHANGED
|
@@ -18,10 +18,9 @@
|
|
|
18
18
|
* reference translation through the system's map, embedding and its stable ids,
|
|
19
19
|
* the anchored prose sections — lives in
|
|
20
20
|
* {@link module:engine/actor-compiler}, where a second system reaches it.
|
|
21
|
-
* What is left here is SoHL's data model: the
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* block itself.
|
|
21
|
+
* What is left here is SoHL's data model: the `sohl.items` frontmatter that
|
|
22
|
+
* becomes embedded documents, the opening mastery level a skill is baked with,
|
|
23
|
+
* and the `system` block itself.
|
|
25
24
|
*
|
|
26
25
|
* One content type today, and the actor subtype it produces is **declared**
|
|
27
26
|
* rather than assumed to be the same word: `sohl/document-subtypes.mjs` maps
|
|
@@ -29,15 +28,24 @@
|
|
|
29
28
|
* `ACTOR_VAULT_TYPE = "being"` here and emitted `type: "being"` several hundred
|
|
30
29
|
* lines below, which made the two vocabularies agree by coincidence.
|
|
31
30
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* `engine/ids.mjs`.
|
|
31
|
+
* Each entry in `sohl.items` is resolved by `(type, shortcode)` and deep-merged
|
|
32
|
+
* with the entry's other properties, so an attribute carries its `scoreBase`
|
|
33
|
+
* and a skill its `masteryLevelBase` as that entry's own `system` overlay.
|
|
36
34
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
35
|
+
* **A being's own document fields are authored under `sohl.system`**, which
|
|
36
|
+
* {@link module:engine/system-block.mergeSystemData} writes at the DataModel's
|
|
37
|
+
* own paths: a being's body is `sohl.system.body`, with
|
|
38
|
+
* `sohl.system.currentMoveMedium` and `sohl.system.movementProfiles` beside it.
|
|
39
|
+
* So this pass writes no body itself, and an incorporeal being authors none and
|
|
40
|
+
* keeps the schema's empty one.
|
|
41
|
+
*
|
|
42
|
+
* **A membership is derived, never authored here.** `data.affiliations` names
|
|
43
|
+
* the bodies a being belongs to and the standing it holds in each, and each
|
|
44
|
+
* entry becomes one embedded affiliation item: the entry's `rank` is that
|
|
45
|
+
* item's `system.level` and its `office` is `system.office`. A standing is a
|
|
46
|
+
* fact about the setting rather than about one system — it does not become true
|
|
47
|
+
* because SoHL is installed — so the system block states none of it, and a
|
|
48
|
+
* sheet and a page cannot come to disagree.
|
|
41
49
|
*
|
|
42
50
|
* Not a standalone script — exports the `Actors` compiler class, imported and
|
|
43
51
|
* driven by `engine/generate.mjs`. Must run after the items passes, since it
|
|
@@ -58,6 +66,10 @@ import { SOHL_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
|
|
|
58
66
|
// verbatim, and `sohl.img` / `sohl.effects` / `sohl.flags` overriding their
|
|
59
67
|
// shared top-level forms for this system alone.
|
|
60
68
|
import { blockProperty, mergeSystemData } from "../engine/system-block.mjs";
|
|
69
|
+
// A being's memberships and the standing it holds in each, read from the one
|
|
70
|
+
// place they are authored. The rank *is* the affiliation item's `level`, so
|
|
71
|
+
// there is nothing to map and nowhere for a second authoring to disagree.
|
|
72
|
+
import { readStandings } from "../engine/standings.mjs";
|
|
61
73
|
|
|
62
74
|
/**
|
|
63
75
|
* The system this pass compiles for — the block its notes write.
|
|
@@ -98,60 +110,12 @@ function defaultActorImg(subType) {
|
|
|
98
110
|
return img;
|
|
99
111
|
}
|
|
100
112
|
|
|
101
|
-
/**
|
|
102
|
-
* Normalize a being's persisted `system.body` from a `sohl.body` block. The
|
|
103
|
-
* authoring frontmatter mirrors the schema field-for-field: `sohl.body` nests
|
|
104
|
-
* `structure` / `weight` / `reachBase` / `bodyScaleBase` / `personalFatigue`,
|
|
105
|
-
* exactly like `system.body`.
|
|
106
|
-
*/
|
|
107
|
-
function normalizeBody(bodyObj) {
|
|
108
|
-
const b = bodyObj && typeof bodyObj === "object" ? bodyObj : {};
|
|
109
|
-
const weight = b.weight || {};
|
|
110
|
-
return {
|
|
111
|
-
structure: b.structure ?? { parts: [], adjacent: [] },
|
|
112
|
-
weight: {
|
|
113
|
-
base: weight.base == null ? null : Number(weight.base),
|
|
114
|
-
calc: String(weight.calc ?? "0"),
|
|
115
|
-
},
|
|
116
|
-
reachBase: Number(b.reachBase ?? 0) || 0,
|
|
117
|
-
bodyScaleBase: Number(b.bodyScaleBase ?? 1) || 1,
|
|
118
|
-
personalFatigue: String(b.personalFatigue ?? "enc"),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Normalize per-medium movement profiles from a `sohl.movementProfiles` list. */
|
|
123
|
-
function normalizeMovementProfiles(list) {
|
|
124
|
-
return (Array.isArray(list) ? list : []).map((p) => ({
|
|
125
|
-
medium: String(p.medium ?? "terrestrial"),
|
|
126
|
-
feetPerRound: Number(p.feetPerRound ?? 0) || 0,
|
|
127
|
-
leaguesPerWatch: Number(p.leaguesPerWatch ?? 0) || 0,
|
|
128
|
-
encumbrance: String(p.encumbrance ?? "0"),
|
|
129
|
-
strMod: String(p.strMod ?? "0"),
|
|
130
|
-
disabled: Boolean(p.disabled ?? false),
|
|
131
|
-
}));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Extract a being's body (+ its movement) from a `sohl` block that mirrors the
|
|
136
|
-
* schema: `sohl.body` (nested → `system.body`) and the flat
|
|
137
|
-
* `sohl.currentMoveMedium` / `sohl.movementProfiles` (→ the base-actor movement
|
|
138
|
-
* fields; movement is a universal actor capability, not part of the body).
|
|
139
|
-
*/
|
|
140
|
-
function extractBodyAndMovement(fm) {
|
|
141
|
-
return {
|
|
142
|
-
body: normalizeBody(sohlField(fm, "body", {})),
|
|
143
|
-
currentMoveMedium: String(sohlField(fm, "currentMoveMedium", "none")),
|
|
144
|
-
movementProfiles: normalizeMovementProfiles(sohlField(fm, "movementProfiles", [])),
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
113
|
/**
|
|
149
114
|
* SoHL's Actor compile pass.
|
|
150
115
|
*
|
|
151
116
|
* Declares SoHL's note-type → document-subtype map and builds a `being` note's
|
|
152
|
-
* document: the
|
|
153
|
-
*
|
|
154
|
-
* Everything else is {@link module:engine/actor-compiler}'s.
|
|
117
|
+
* document: the embedded item documents the frontmatter names, and the `system`
|
|
118
|
+
* block. Everything else is {@link module:engine/actor-compiler}'s.
|
|
155
119
|
*/
|
|
156
120
|
export class Actors extends SystemActorCompiler {
|
|
157
121
|
/**
|
|
@@ -177,30 +141,12 @@ export class Actors extends SystemActorCompiler {
|
|
|
177
141
|
}
|
|
178
142
|
|
|
179
143
|
/**
|
|
180
|
-
* Build all embedded items for an actor: one per `sohl.
|
|
181
|
-
*
|
|
144
|
+
* Build all embedded items for an actor: one per `sohl.items` entry, plus
|
|
145
|
+
* one per body the being belongs to.
|
|
182
146
|
*/
|
|
183
147
|
buildEmbeddedItems(itemsMap, actorId, fm, ctx) {
|
|
184
148
|
const items = [];
|
|
185
149
|
|
|
186
|
-
const attributes = sohlField(fm, "attributes", null);
|
|
187
|
-
if (attributes && typeof attributes === "object") {
|
|
188
|
-
for (const [shortcode, value] of Object.entries(attributes)) {
|
|
189
|
-
const overlay = { system: { scoreBase: Number(value) || 0 } };
|
|
190
|
-
const embedded = this.resolveEmbedded(
|
|
191
|
-
itemsMap,
|
|
192
|
-
actorId,
|
|
193
|
-
"attribute",
|
|
194
|
-
shortcode,
|
|
195
|
-
overlay,
|
|
196
|
-
`attr:${shortcode}`,
|
|
197
|
-
ctx,
|
|
198
|
-
{ fmKey: "attributes" },
|
|
199
|
-
);
|
|
200
|
-
if (embedded) items.push(embedded);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
150
|
const sohlItems = sohlField(fm, "items", null);
|
|
205
151
|
if (Array.isArray(sohlItems)) {
|
|
206
152
|
sohlItems.forEach((entry, index) => {
|
|
@@ -256,10 +202,59 @@ export class Actors extends SystemActorCompiler {
|
|
|
256
202
|
});
|
|
257
203
|
}
|
|
258
204
|
|
|
205
|
+
for (const embedded of this.buildMemberships(itemsMap, actorId, fm, ctx)) {
|
|
206
|
+
items.push(embedded);
|
|
207
|
+
}
|
|
208
|
+
|
|
259
209
|
this.openUnopenedSkills(items, ctx);
|
|
260
210
|
return items;
|
|
261
211
|
}
|
|
262
212
|
|
|
213
|
+
/**
|
|
214
|
+
* One embedded affiliation item per body the being belongs to.
|
|
215
|
+
*
|
|
216
|
+
* The body's own compiled item is the model, so a membership carries
|
|
217
|
+
* whatever that body declares — its subtype, its common skills, its
|
|
218
|
+
* standings toward others — and the being adds only what is true of *this*
|
|
219
|
+
* membership: the rung it stands on and the post it holds.
|
|
220
|
+
*
|
|
221
|
+
* A being naming a body that compiles to no item is a finding rather than a
|
|
222
|
+
* silently dropped membership: the note says the being belongs somewhere,
|
|
223
|
+
* and a sheet that shows nothing says it does not.
|
|
224
|
+
*
|
|
225
|
+
* @param {Map<string, object>} itemsMap - The predefined-item catalogue.
|
|
226
|
+
* @param {string} actorId - The owning actor's id.
|
|
227
|
+
* @param {object} fm - The note's frontmatter.
|
|
228
|
+
* @param {string} ctx - Diagnostic context.
|
|
229
|
+
* @returns {object[]} The embedded items, in the order the note names them.
|
|
230
|
+
*/
|
|
231
|
+
buildMemberships(itemsMap, actorId, fm, ctx) {
|
|
232
|
+
const { entries } = readStandings(fm?.data?.affiliations);
|
|
233
|
+
const items = [];
|
|
234
|
+
for (const { body, standing } of entries) {
|
|
235
|
+
const shortcode = body?.shortcode;
|
|
236
|
+
if (!shortcode) continue;
|
|
237
|
+
const level = Number(standing?.rank);
|
|
238
|
+
const office = typeof standing?.office === "string" ? standing.office.trim() : "";
|
|
239
|
+
const system = {
|
|
240
|
+
...(Number.isFinite(level) ? { level } : {}),
|
|
241
|
+
...(office ? { office } : {}),
|
|
242
|
+
};
|
|
243
|
+
const embedded = this.resolveEmbedded(
|
|
244
|
+
itemsMap,
|
|
245
|
+
actorId,
|
|
246
|
+
"affiliation",
|
|
247
|
+
shortcode,
|
|
248
|
+
Object.keys(system).length ? { system } : {},
|
|
249
|
+
`affiliations:${shortcode}`,
|
|
250
|
+
ctx,
|
|
251
|
+
{ fmKey: "affiliations", modelPackage: body.package ?? null },
|
|
252
|
+
);
|
|
253
|
+
if (embedded) items.push(embedded);
|
|
254
|
+
}
|
|
255
|
+
return items;
|
|
256
|
+
}
|
|
257
|
+
|
|
263
258
|
/**
|
|
264
259
|
* Bake each unopened skill's opening mastery level into the document.
|
|
265
260
|
*
|
|
@@ -277,8 +272,8 @@ export class Actors extends SystemActorCompiler {
|
|
|
277
272
|
*
|
|
278
273
|
* **The scores used are the ones just written.** `SkillLogic` resolves
|
|
279
274
|
* `attr.<code>` to an attribute's *effective* score, after active effects;
|
|
280
|
-
* all this pass has is the `scoreBase`
|
|
281
|
-
* compiled being carrying no attribute-altering effects the two agree,
|
|
275
|
+
* all this pass has is the `scoreBase` on each embedded attribute item. For
|
|
276
|
+
* a compiled being carrying no attribute-altering effects the two agree,
|
|
282
277
|
* which is every being in content today. One that did carry such an effect
|
|
283
278
|
* would bake a Skill Base its client then disagrees with — that is the
|
|
284
279
|
* limit of doing this at build time, and the point to revisit if it bites.
|
|
@@ -331,6 +326,9 @@ export class Actors extends SystemActorCompiler {
|
|
|
331
326
|
const { value: authoredFolder, isAddress } = folderField(fm);
|
|
332
327
|
const folder = this.folderResolver(authoredFolder, { isAddress });
|
|
333
328
|
|
|
329
|
+
this.checkAnchoredSection(body || "", "appearance", ctx);
|
|
330
|
+
this.checkAnchoredSection(body || "", "dossier", ctx);
|
|
331
|
+
|
|
334
332
|
const system = {
|
|
335
333
|
// The frontmatter shortcode is the actor's stable `(type, shortcode)`
|
|
336
334
|
// key — and, for a being that is an archetype, its archetype
|
|
@@ -355,25 +353,6 @@ export class Actors extends SystemActorCompiler {
|
|
|
355
353
|
dossier: withDraftNotice(fm, renderSection(body || "", "dossier")),
|
|
356
354
|
};
|
|
357
355
|
|
|
358
|
-
// Fill `system.body` (+ the base-actor movement fields) from the being's
|
|
359
|
-
// frontmatter, rather than embedding a corpus item. The `sohl`
|
|
360
|
-
// block mirrors `system` field-for-field: `sohl.body` nests the body
|
|
361
|
-
// (`structure` / `weight` / …), with `currentMoveMedium` /
|
|
362
|
-
// `movementProfiles` flat alongside it. An **incorporeal** being omits
|
|
363
|
-
// `sohl.body` and keeps the schema's empty body.
|
|
364
|
-
const bodyField = sohlField(fm, "body", null);
|
|
365
|
-
if (bodyField && typeof bodyField === "object") {
|
|
366
|
-
const bodyData = extractBodyAndMovement(fm);
|
|
367
|
-
system.body = bodyData.body;
|
|
368
|
-
system.currentMoveMedium = bodyData.currentMoveMedium;
|
|
369
|
-
system.movementProfiles = bodyData.movementProfiles;
|
|
370
|
-
} else if (bodyField != null) {
|
|
371
|
-
this.noteError(
|
|
372
|
-
`${ctx}: sohl.body must be an inline object (structure/weight/…), got ${typeof bodyField}`,
|
|
373
|
-
);
|
|
374
|
-
this.errorCount++;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
356
|
// Being-only combat grouping (mirrors `system.defaultCombatGroup`).
|
|
378
357
|
const defaultCombatGroup = sohlField(fm, "defaultCombatGroup", undefined);
|
|
379
358
|
if (defaultCombatGroup !== undefined) {
|
package/sohl/item-fields.mjs
CHANGED
|
@@ -391,11 +391,10 @@ export const ITEM_FIELDS = Object.freeze({
|
|
|
391
391
|
...STRING,
|
|
392
392
|
default: "",
|
|
393
393
|
topLevelMeans:
|
|
394
|
-
"
|
|
395
|
-
"
|
|
396
|
-
"
|
|
397
|
-
"
|
|
398
|
-
"`sohl.items`.",
|
|
394
|
+
"An office's style of address belongs to the membership that " +
|
|
395
|
+
"holds the office, so nothing outside the item supplies it. " +
|
|
396
|
+
"Author it at `sohl.system.title` on the item, or at the " +
|
|
397
|
+
"`system.title` of the entry in a being's `sohl.items`.",
|
|
399
398
|
describe: "The style of address the office carries.",
|
|
400
399
|
},
|
|
401
400
|
{
|
package/sohl/items.mjs
CHANGED
|
@@ -93,7 +93,7 @@ export class Items extends SystemItemCompiler {
|
|
|
93
93
|
// it — an undeclared `system` key is discarded at construction
|
|
94
94
|
// without a warning.
|
|
95
95
|
templatePriority: systemTemplatePriority(fm, label),
|
|
96
|
-
actionDefs:
|
|
96
|
+
actionDefs: [],
|
|
97
97
|
notes: "",
|
|
98
98
|
docHtml: description || "",
|
|
99
99
|
};
|
package/sohl/note-schemas.mjs
CHANGED
|
@@ -99,37 +99,27 @@ const MACRO_FIELDS = Object.freeze([
|
|
|
99
99
|
const VEHICLE_FIELDS = Object.freeze([]);
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* A `being` note — an actor, with its
|
|
102
|
+
* A `being` note — an actor, with its embedded items and its combat grouping.
|
|
103
|
+
*
|
|
104
|
+
* **A being's own document fields are authored under `sohl.system`**, which the
|
|
105
|
+
* passthrough writes at the DataModel's own paths — a body and the movement that
|
|
106
|
+
* goes with it are `sohl.system.body`, `sohl.system.currentMoveMedium` and
|
|
107
|
+
* `sohl.system.movementProfiles`, written together — so this declaration states
|
|
108
|
+
* none of them. **Attributes and skills are embedded items**, each named by the
|
|
109
|
+
* catalogue entry it copies, so both are `sohl.items` entries with the score or
|
|
110
|
+
* the mastery level as that entry's own `system` overlay.
|
|
111
|
+
*
|
|
112
|
+
* What is left is the two keys the pass reads off the block itself.
|
|
103
113
|
*
|
|
104
114
|
* @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
|
|
105
115
|
*/
|
|
106
116
|
const BEING_FIELDS = Object.freeze([
|
|
107
|
-
{
|
|
108
|
-
name: "body",
|
|
109
|
-
...MAP,
|
|
110
|
-
describe: "The being's body structure, by part shortcode.",
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
name: "attributes",
|
|
114
|
-
...MAP,
|
|
115
|
-
describe: "Attribute scores, by attribute shortcode.",
|
|
116
|
-
},
|
|
117
117
|
{
|
|
118
118
|
name: "items",
|
|
119
119
|
...LIST,
|
|
120
120
|
describe:
|
|
121
121
|
"Items embedded on the being, each addressed by (type, shortcode) or carrying enough fields to stand alone.",
|
|
122
122
|
},
|
|
123
|
-
{
|
|
124
|
-
name: "currentMoveMedium",
|
|
125
|
-
...STRING,
|
|
126
|
-
describe: "Which medium the being is currently moving through.",
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
name: "movementProfiles",
|
|
130
|
-
...LIST,
|
|
131
|
-
describe: "Movement rates, one profile per medium.",
|
|
132
|
-
},
|
|
133
123
|
{
|
|
134
124
|
name: "defaultCombatGroup",
|
|
135
125
|
...AS_AUTHORED,
|
package/types/config.d.mts
CHANGED
|
@@ -102,6 +102,9 @@ export const DERIVED_MANIFEST_KEYS: Readonly<Record<string, string>>;
|
|
|
102
102
|
* @property {string} stageDir The staged package root, relative to
|
|
103
103
|
* `rootDir`. Every asset `to:` lands under it.
|
|
104
104
|
* @property {readonly Readonly<AssetSpec>[]} assets
|
|
105
|
+
* @property {boolean} baseStyles Whether the package takes the shared base
|
|
106
|
+
* stylesheet, staged into `stageDir` and
|
|
107
|
+
* named first in the manifest's `styles`.
|
|
105
108
|
* @property {Readonly<Record<string, unknown>>} manifest The manifest
|
|
106
109
|
* specification, emitted as declared.
|
|
107
110
|
* @property {string|null} manifestFlags Module to load a `flags` function
|
|
@@ -235,6 +238,12 @@ export type PackageBuildConfig = {
|
|
|
235
238
|
*/
|
|
236
239
|
stageDir: string;
|
|
237
240
|
assets: readonly Readonly<AssetSpec>[];
|
|
241
|
+
/**
|
|
242
|
+
* Whether the package takes the shared base
|
|
243
|
+
* stylesheet, staged into `stageDir` and
|
|
244
|
+
* named first in the manifest's `styles`.
|
|
245
|
+
*/
|
|
246
|
+
baseStyles: boolean;
|
|
238
247
|
/**
|
|
239
248
|
* The manifest
|
|
240
249
|
* specification, emitted as declared.
|
|
@@ -248,6 +248,17 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
248
248
|
img: string;
|
|
249
249
|
token: string;
|
|
250
250
|
};
|
|
251
|
+
/**
|
|
252
|
+
* Report an `{#appearance}` or `{#dossier}` anchor on a heading
|
|
253
|
+
* {@link extractAnchorSection} cannot read, rather than letting the field
|
|
254
|
+
* it feeds compile blank with nothing to say why.
|
|
255
|
+
*
|
|
256
|
+
* @param {string} body - The note body.
|
|
257
|
+
* @param {string} anchorId - `"appearance"` or `"dossier"`.
|
|
258
|
+
* @param {string} ctx - The actor's label, for the message.
|
|
259
|
+
* @returns {void}
|
|
260
|
+
*/
|
|
261
|
+
checkAnchoredSection(body: string, anchorId: string, ctx: string): void;
|
|
251
262
|
/** @type {readonly string[]} */
|
|
252
263
|
itemsSourceDirs: readonly string[];
|
|
253
264
|
foreignSourceDirs: any;
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* heading exists. Fenced code blocks are respected so `# foo` inside
|
|
7
7
|
* ``` blocks does not trigger a match.
|
|
8
8
|
*
|
|
9
|
+
* An anchor declared on a lower heading is found by {@link collectAnchors}
|
|
10
|
+
* but not by the `#\s+` match below, so it extracts as absent rather than as
|
|
11
|
+
* wrong — {@link misplacedAnchorSection} is the check a caller runs first to
|
|
12
|
+
* tell the two apart.
|
|
13
|
+
*
|
|
9
14
|
* @param {string} body - The note body.
|
|
10
15
|
* @param {string} anchorId - The anchor to find.
|
|
11
16
|
* @returns {string} The section's markdown, or "".
|
|
@@ -19,3 +24,22 @@ export function extractAnchorSection(body: string, anchorId: string): string;
|
|
|
19
24
|
* @returns {string} The rendered HTML, or "".
|
|
20
25
|
*/
|
|
21
26
|
export function renderSection(body: string, anchorId: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Where an `{#appearance}` or `{#dossier}` anchor sits on a heading below the
|
|
29
|
+
* top level, which {@link extractAnchorSection} cannot read.
|
|
30
|
+
*
|
|
31
|
+
* {@link module:engine/anchors.collectAnchors} finds the anchor at any
|
|
32
|
+
* level, because a lower heading legitimately starts a journal page. It does
|
|
33
|
+
* not feed an actor field: an H1-only extractor reads nothing from one, and a
|
|
34
|
+
* caller that trusts the empty result as "no such section" rather than
|
|
35
|
+
* "found, but unreadable" compiles a blank field with nothing to say why.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} body - The note body.
|
|
38
|
+
* @param {string} anchorId - The anchor to check, case-insensitively.
|
|
39
|
+
* @param {number} [bodyLine=1] - The 1-based file line the body starts on.
|
|
40
|
+
* @returns {{line: number}|null} The misplaced anchor's line, or `null` when
|
|
41
|
+
* it is absent or already on an H1.
|
|
42
|
+
*/
|
|
43
|
+
export function misplacedAnchorSection(body: string, anchorId: string, bodyLine?: number): {
|
|
44
|
+
line: number;
|
|
45
|
+
} | null;
|
|
@@ -378,6 +378,25 @@ export class BasePackCompiler {
|
|
|
378
378
|
linkIndex: any;
|
|
379
379
|
sqlTables: any;
|
|
380
380
|
worldPresent: unknown;
|
|
381
|
+
/**
|
|
382
|
+
* Report every finding {@link BasePackCompiler#convertBody} collected
|
|
383
|
+
* across its passes, and decline the note if there were any.
|
|
384
|
+
*
|
|
385
|
+
* Reported directly, one `noteError` per finding, rather than thrown as a
|
|
386
|
+
* single wrapped message: the site build and the book report the same way,
|
|
387
|
+
* each finding at its own line, and a note with three problems meets all
|
|
388
|
+
* three in the run that found them instead of one per rebuild. The thrown
|
|
389
|
+
* sentinel carries `alreadyReported` so the compile loop's generic catch
|
|
390
|
+
* does not add a fourth, vaguer line on top of the three already said.
|
|
391
|
+
*
|
|
392
|
+
* @param {readonly {message: string, line?: number, column?: number}[]} findings
|
|
393
|
+
* @throws {Error} When `findings` is non-empty, after reporting all of them.
|
|
394
|
+
*/
|
|
395
|
+
reportConvertBodyFindings(findings: readonly {
|
|
396
|
+
message: string;
|
|
397
|
+
line?: number;
|
|
398
|
+
column?: number;
|
|
399
|
+
}[]): void;
|
|
381
400
|
/**
|
|
382
401
|
* The body {@link BasePackCompiler#buildEntry} receives.
|
|
383
402
|
*
|