@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.
- package/CHANGELOG.md +223 -0
- package/assets/styles/content-base.css +162 -0
- package/bin/content-commands.mjs +77 -17
- package/bin/package-build.mjs +15 -2
- package/ci/content-format-reference.mjs +68 -1
- package/config.mjs +26 -0
- package/content-config.mjs +153 -6
- package/docs/api.md +120 -89
- package/docs/authoring/assets.md +14 -0
- package/docs/authoring/dates-and-calendars.md +41 -3
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +325 -35
- package/docs/commands.md +12 -10
- package/docs/configuration.md +117 -14
- package/docs/content-format.md +4 -2
- package/docs/guides/book.md +5 -4
- package/docs/guides/reference-pages.md +15 -0
- package/docs/reference/format-details.md +545 -104
- package/docs/reference/note-types.md +30 -13
- package/engine/actor-compiler.mjs +23 -0
- package/engine/anchored-sections.mjs +35 -4
- package/engine/anchors.mjs +26 -30
- package/engine/art-fields.mjs +118 -0
- package/engine/art-slots.mjs +7 -1
- package/engine/asset-index.mjs +160 -10
- package/engine/asset-replacement.mjs +93 -0
- package/engine/asset-types.mjs +240 -0
- package/engine/base-compiler.mjs +139 -22
- package/engine/base-styles.mjs +149 -0
- package/engine/bundles.mjs +13 -2
- package/engine/compendiums.mjs +8 -1
- package/engine/content-blocks.mjs +410 -0
- package/engine/content-embeds.mjs +19 -0
- package/engine/content-figures.mjs +468 -0
- package/engine/content-footnotes.mjs +197 -0
- package/engine/content-format-check.mjs +15 -12
- package/engine/content-format.yaml +1 -1
- package/engine/content-images.mjs +126 -25
- package/engine/content-index.mjs +38 -12
- 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/field-reference.mjs +31 -10
- package/engine/field-spec.mjs +10 -8
- package/engine/foreign-catalog.mjs +143 -18
- package/engine/frontmatter-lint.mjs +120 -65
- package/engine/frontmatter.mjs +3 -3
- package/engine/heading-attributes.mjs +321 -0
- package/engine/helpers.mjs +109 -41
- package/engine/homepage.mjs +8 -8
- package/engine/index.mjs +3 -0
- package/engine/infobox-registry.mjs +4 -1
- package/engine/infobox-render.mjs +15 -3
- package/engine/infobox.mjs +129 -15
- package/engine/itinerary-scenes.mjs +4 -10
- package/engine/journals.mjs +294 -50
- package/engine/lore-events.mjs +197 -0
- package/engine/macros.mjs +1 -1
- package/engine/markdown-expressions.mjs +434 -59
- package/engine/note-dates.mjs +354 -12
- package/engine/note-frontmatter.mjs +7 -16
- package/engine/note-vocabulary.mjs +183 -42
- package/engine/page-lists.mjs +358 -0
- package/engine/pathnames.mjs +36 -0
- package/engine/pdf-build.mjs +431 -74
- package/engine/pdf-images.mjs +64 -0
- package/engine/pdf-render.mjs +563 -66
- package/engine/rank-ladder.mjs +198 -0
- package/engine/reckoning-markers.mjs +1 -1
- package/engine/retired-fields.mjs +8 -13
- package/engine/scenes.mjs +26 -7
- package/engine/site-build.mjs +166 -23
- package/engine/site-config.mjs +10 -1
- package/engine/site-index.mjs +90 -3
- package/engine/site-preview.mjs +20 -7
- package/engine/social-ties.mjs +52 -14
- package/engine/sql-tables.mjs +66 -9
- package/engine/standings.mjs +89 -6
- package/engine/system-block.mjs +56 -30
- package/engine/web-wikilinks.mjs +48 -1
- package/hm3/actors.mjs +8 -5
- package/manifest.mjs +43 -24
- package/package.json +1 -1
- package/packagebuild-metadata.jsonl +13 -13
- package/sohl/actors.mjs +22 -99
- package/sohl/item-fields.mjs +5 -5
- package/sohl/items.mjs +1 -1
- package/sohl/note-schemas.mjs +11 -21
- package/types/config.d.mts +9 -0
- package/types/content-config.d.mts +48 -1
- package/types/engine/actor-compiler.d.mts +11 -0
- package/types/engine/anchored-sections.d.mts +24 -0
- package/types/engine/anchors.d.mts +9 -4
- package/types/engine/art-fields.d.mts +64 -0
- package/types/engine/asset-index.d.mts +43 -0
- package/types/engine/asset-replacement.d.mts +22 -0
- package/types/engine/asset-types.d.mts +18 -0
- package/types/engine/base-compiler.d.mts +19 -17
- package/types/engine/base-styles.d.mts +93 -0
- package/types/engine/content-blocks.d.mts +87 -0
- package/types/engine/content-embeds.d.mts +16 -0
- package/types/engine/content-figures.d.mts +101 -0
- package/types/engine/content-footnotes.d.mts +80 -0
- package/types/engine/content-format-check.d.mts +8 -6
- package/types/engine/content-images.d.mts +78 -12
- package/types/engine/content-index.d.mts +8 -2
- 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/field-reference.d.mts +21 -0
- package/types/engine/field-spec.d.mts +20 -16
- package/types/engine/foreign-catalog.d.mts +39 -0
- package/types/engine/frontmatter.d.mts +1 -1
- package/types/engine/heading-attributes.d.mts +179 -0
- package/types/engine/helpers.d.mts +29 -10
- package/types/engine/homepage.d.mts +7 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox-registry.d.mts +4 -1
- package/types/engine/infobox-render.d.mts +6 -1
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/journals.d.mts +41 -12
- package/types/engine/lore-events.d.mts +14 -0
- package/types/engine/markdown-expressions.d.mts +46 -2
- package/types/engine/note-dates.d.mts +64 -1
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-vocabulary.d.mts +82 -6
- package/types/engine/page-lists.d.mts +126 -0
- package/types/engine/pathnames.d.mts +26 -0
- package/types/engine/pdf-build.d.mts +28 -6
- package/types/engine/pdf-images.d.mts +52 -0
- package/types/engine/pdf-render.d.mts +84 -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 +27 -4
- package/types/engine/site-config.d.mts +9 -0
- package/types/engine/site-index.d.mts +67 -1
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -7
- package/types/engine/standings.d.mts +21 -3
- package/types/engine/system-block.d.mts +37 -14
- package/types/engine/web-wikilinks.d.mts +3 -1
- package/types/hm3/actors.d.mts +21 -3
- package/types/manifest.d.mts +19 -23
- package/types/sohl/actors.d.mts +6 -7
- package/engine/content-admonitions.mjs +0 -114
- package/engine/content-captions.mjs +0 -181
- package/engine/content-secrets.mjs +0 -112
- package/types/engine/content-admonitions.d.mts +0 -24
- package/types/engine/content-captions.d.mts +0 -45
- package/types/engine/content-secrets.d.mts +0 -17
package/hm3/actors.mjs
CHANGED
|
@@ -78,7 +78,7 @@ import {
|
|
|
78
78
|
*
|
|
79
79
|
* @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
|
|
80
80
|
*/
|
|
81
|
-
const ACTOR_FIELDS = Object.freeze([
|
|
81
|
+
export const ACTOR_FIELDS = Object.freeze([
|
|
82
82
|
{
|
|
83
83
|
name: "species",
|
|
84
84
|
to: "species",
|
|
@@ -97,7 +97,7 @@ const ACTOR_FIELDS = Object.freeze([
|
|
|
97
97
|
*
|
|
98
98
|
* @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
|
|
99
99
|
*/
|
|
100
|
-
const CHARACTER_FIELDS = Object.freeze([
|
|
100
|
+
export const CHARACTER_FIELDS = Object.freeze([
|
|
101
101
|
{
|
|
102
102
|
name: "data.gender",
|
|
103
103
|
legacyKey: "gender",
|
|
@@ -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
|
@@ -48,10 +48,12 @@
|
|
|
48
48
|
* @module
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
|
+
import { RELATIONSHIP_KEYS } from "./content-config.mjs";
|
|
51
52
|
import fs from "node:fs/promises";
|
|
52
53
|
import fsSync from "node:fs";
|
|
53
54
|
import path from "node:path";
|
|
54
55
|
|
|
56
|
+
import { withBaseStyle } from "./engine/base-styles.mjs";
|
|
55
57
|
import { emitDiagnostic, formatDiagnostic, positionOfYamlPath } from "./engine/diagnostics.mjs";
|
|
56
58
|
import { metadataFileName } from "./engine/metadata-index.mjs";
|
|
57
59
|
|
|
@@ -389,33 +391,38 @@ export function packFolderFindings({ packFolders, packs = [] }) {
|
|
|
389
391
|
}
|
|
390
392
|
|
|
391
393
|
/**
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
* to
|
|
413
|
-
*
|
|
414
|
-
*
|
|
394
|
+
* The relationship keys Foundry's own schema names.
|
|
395
|
+
*
|
|
396
|
+
* `RelatedPackage` declares exactly these (`common/packages/base-package.mjs`),
|
|
397
|
+
* so a key outside them is one the build put there for itself.
|
|
398
|
+
*
|
|
399
|
+
* @type {readonly string[]}
|
|
400
|
+
*/
|
|
401
|
+
export const FOUNDRY_RELATIONSHIP_KEYS = Object.freeze([
|
|
402
|
+
"id",
|
|
403
|
+
"type",
|
|
404
|
+
"manifest",
|
|
405
|
+
"compatibility",
|
|
406
|
+
"reason",
|
|
407
|
+
]);
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* The relationship keys the published manifest drops.
|
|
411
|
+
*
|
|
412
|
+
* Derived rather than listed: every key this package accepts on a relationship
|
|
413
|
+
* that Foundry's schema does not name is one the build reads for itself, so a
|
|
414
|
+
* key added to the configuration is dropped from the manifest without a second
|
|
415
|
+
* list being kept in step.
|
|
416
|
+
*
|
|
417
|
+
* The filter stays a denylist for the reason the pass-through below gives — a
|
|
418
|
+
* key Foundry adds later reaches the manifest unread rather than being stripped
|
|
419
|
+
* by an allowlist that has not heard of it.
|
|
415
420
|
*
|
|
416
421
|
* @type {readonly string[]}
|
|
417
422
|
*/
|
|
418
|
-
export const BUILD_ONLY_RELATIONSHIP_KEYS = Object.freeze(
|
|
423
|
+
export const BUILD_ONLY_RELATIONSHIP_KEYS = Object.freeze(
|
|
424
|
+
RELATIONSHIP_KEYS.filter((key) => !FOUNDRY_RELATIONSHIP_KEYS.includes(key)),
|
|
425
|
+
);
|
|
419
426
|
|
|
420
427
|
/**
|
|
421
428
|
* The `relationships` block as published — every declared dependency, with the
|
|
@@ -548,6 +555,18 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
|
|
|
548
555
|
|
|
549
556
|
const merged = { ...declared, ...derived };
|
|
550
557
|
|
|
558
|
+
// The shared base stylesheet, named ahead of whatever the package declares.
|
|
559
|
+
// Derived rather than declared, because every package needs it and four of
|
|
560
|
+
// the five that do have no stylesheet of their own to name it from. See
|
|
561
|
+
// `engine/base-styles.mjs` for the layer it loads into and why that is what
|
|
562
|
+
// lets a package override it.
|
|
563
|
+
const styles = withBaseStyle({
|
|
564
|
+
declared: declared.styles,
|
|
565
|
+
artifact: /** @type {"system"|"module"} */ (artifact),
|
|
566
|
+
enabled: config.packageBuild?.baseStyles !== false,
|
|
567
|
+
});
|
|
568
|
+
if (styles !== undefined) merged.styles = styles;
|
|
569
|
+
|
|
551
570
|
// The index every consumer resolves this package's addresses through.
|
|
552
571
|
// Written unconditionally, because a package that publishes no
|
|
553
572
|
// 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.23.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",
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
{"address":{"canonical":"packagebuild-none-image-afflictionbnr"},"asset":{"ai":
|
|
2
|
-
{"address":{"canonical":"packagebuild-none-image-armorclothingbnr"},"asset":{"ai":
|
|
3
|
-
{"address":{"canonical":"packagebuild-none-image-attributebnr"},"asset":{"ai":
|
|
4
|
-
{"address":{"canonical":"packagebuild-none-image-containerbnr"},"asset":{"ai":
|
|
5
|
-
{"address":{"canonical":"packagebuild-none-image-devdocsbnr"},"asset":{"ai":
|
|
6
|
-
{"address":{"canonical":"packagebuild-none-image-miscgearbnr"},"asset":{"ai":
|
|
7
|
-
{"address":{"canonical":"packagebuild-none-image-mysticalabilitybnr"},"asset":{"ai":
|
|
8
|
-
{"address":{"canonical":"packagebuild-none-image-projectilebnr"},"asset":{"ai":
|
|
9
|
-
{"address":{"canonical":"packagebuild-none-image-rulesbnr"},"asset":{"ai":
|
|
10
|
-
{"address":{"canonical":"packagebuild-none-image-skillbnr"},"asset":{"ai":
|
|
11
|
-
{"address":{"canonical":"packagebuild-none-image-traumabnr"},"asset":{"ai":
|
|
12
|
-
{"address":{"canonical":"packagebuild-none-image-userguidebnr"},"asset":{"ai":
|
|
13
|
-
{"address":{"canonical":"packagebuild-none-image-weapongearbnr"},"asset":{"ai":
|
|
1
|
+
{"address":{"canonical":"packagebuild-none-image-afflictionbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/afflictionbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"afflictionbnr","type":"image"}
|
|
2
|
+
{"address":{"canonical":"packagebuild-none-image-armorclothingbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/armorclothingbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"armorclothingbnr","type":"image"}
|
|
3
|
+
{"address":{"canonical":"packagebuild-none-image-attributebnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/attributebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"attributebnr","type":"image"}
|
|
4
|
+
{"address":{"canonical":"packagebuild-none-image-containerbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/containerbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"containerbnr","type":"image"}
|
|
5
|
+
{"address":{"canonical":"packagebuild-none-image-devdocsbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/devdocsbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"devdocsbnr","type":"image"}
|
|
6
|
+
{"address":{"canonical":"packagebuild-none-image-miscgearbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/miscgearbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"miscgearbnr","type":"image"}
|
|
7
|
+
{"address":{"canonical":"packagebuild-none-image-mysticalabilitybnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/mysticalabilitybnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"mysticalabilitybnr","type":"image"}
|
|
8
|
+
{"address":{"canonical":"packagebuild-none-image-projectilebnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/projectilebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"projectilebnr","type":"image"}
|
|
9
|
+
{"address":{"canonical":"packagebuild-none-image-rulesbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/rulesbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"rulesbnr","type":"image"}
|
|
10
|
+
{"address":{"canonical":"packagebuild-none-image-skillbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/skillbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"skillbnr","type":"image"}
|
|
11
|
+
{"address":{"canonical":"packagebuild-none-image-traumabnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/traumabnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"traumabnr","type":"image"}
|
|
12
|
+
{"address":{"canonical":"packagebuild-none-image-userguidebnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/userguidebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"userguidebnr","type":"image"}
|
|
13
|
+
{"address":{"canonical":"packagebuild-none-image-weapongearbnr"},"asset":{"ai":false,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/weapongearbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"weapongearbnr","type":"image"}
|
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,10 +28,16 @@
|
|
|
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
|
-
*
|
|
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.
|
|
34
|
+
*
|
|
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.
|
|
36
41
|
*
|
|
37
42
|
* **A membership is derived, never authored here.** `data.affiliations` names
|
|
38
43
|
* the bodies a being belongs to and the standing it holds in each, and each
|
|
@@ -105,60 +110,12 @@ function defaultActorImg(subType) {
|
|
|
105
110
|
return img;
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
/**
|
|
109
|
-
* Normalize a being's persisted `system.body` from a `sohl.body` block. The
|
|
110
|
-
* authoring frontmatter mirrors the schema field-for-field: `sohl.body` nests
|
|
111
|
-
* `structure` / `weight` / `reachBase` / `bodyScaleBase` / `personalFatigue`,
|
|
112
|
-
* exactly like `system.body`.
|
|
113
|
-
*/
|
|
114
|
-
function normalizeBody(bodyObj) {
|
|
115
|
-
const b = bodyObj && typeof bodyObj === "object" ? bodyObj : {};
|
|
116
|
-
const weight = b.weight || {};
|
|
117
|
-
return {
|
|
118
|
-
structure: b.structure ?? { parts: [], adjacent: [] },
|
|
119
|
-
weight: {
|
|
120
|
-
base: weight.base == null ? null : Number(weight.base),
|
|
121
|
-
calc: String(weight.calc ?? "0"),
|
|
122
|
-
},
|
|
123
|
-
reachBase: Number(b.reachBase ?? 0) || 0,
|
|
124
|
-
bodyScaleBase: Number(b.bodyScaleBase ?? 1) || 1,
|
|
125
|
-
personalFatigue: String(b.personalFatigue ?? "enc"),
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Normalize per-medium movement profiles from a `sohl.movementProfiles` list. */
|
|
130
|
-
function normalizeMovementProfiles(list) {
|
|
131
|
-
return (Array.isArray(list) ? list : []).map((p) => ({
|
|
132
|
-
medium: String(p.medium ?? "terrestrial"),
|
|
133
|
-
feetPerRound: Number(p.feetPerRound ?? 0) || 0,
|
|
134
|
-
leaguesPerWatch: Number(p.leaguesPerWatch ?? 0) || 0,
|
|
135
|
-
encumbrance: String(p.encumbrance ?? "0"),
|
|
136
|
-
strMod: String(p.strMod ?? "0"),
|
|
137
|
-
disabled: Boolean(p.disabled ?? false),
|
|
138
|
-
}));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Extract a being's body (+ its movement) from a `sohl` block that mirrors the
|
|
143
|
-
* schema: `sohl.body` (nested → `system.body`) and the flat
|
|
144
|
-
* `sohl.currentMoveMedium` / `sohl.movementProfiles` (→ the base-actor movement
|
|
145
|
-
* fields; movement is a universal actor capability, not part of the body).
|
|
146
|
-
*/
|
|
147
|
-
function extractBodyAndMovement(fm) {
|
|
148
|
-
return {
|
|
149
|
-
body: normalizeBody(sohlField(fm, "body", {})),
|
|
150
|
-
currentMoveMedium: String(sohlField(fm, "currentMoveMedium", "none")),
|
|
151
|
-
movementProfiles: normalizeMovementProfiles(sohlField(fm, "movementProfiles", [])),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
113
|
/**
|
|
156
114
|
* SoHL's Actor compile pass.
|
|
157
115
|
*
|
|
158
116
|
* Declares SoHL's note-type → document-subtype map and builds a `being` note's
|
|
159
|
-
* document: the
|
|
160
|
-
*
|
|
161
|
-
* 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.
|
|
162
119
|
*/
|
|
163
120
|
export class Actors extends SystemActorCompiler {
|
|
164
121
|
/**
|
|
@@ -184,30 +141,12 @@ export class Actors extends SystemActorCompiler {
|
|
|
184
141
|
}
|
|
185
142
|
|
|
186
143
|
/**
|
|
187
|
-
* Build all embedded items for an actor: one per `sohl.
|
|
188
|
-
*
|
|
144
|
+
* Build all embedded items for an actor: one per `sohl.items` entry, plus
|
|
145
|
+
* one per body the being belongs to.
|
|
189
146
|
*/
|
|
190
147
|
buildEmbeddedItems(itemsMap, actorId, fm, ctx) {
|
|
191
148
|
const items = [];
|
|
192
149
|
|
|
193
|
-
const attributes = sohlField(fm, "attributes", null);
|
|
194
|
-
if (attributes && typeof attributes === "object") {
|
|
195
|
-
for (const [shortcode, value] of Object.entries(attributes)) {
|
|
196
|
-
const overlay = { system: { scoreBase: Number(value) || 0 } };
|
|
197
|
-
const embedded = this.resolveEmbedded(
|
|
198
|
-
itemsMap,
|
|
199
|
-
actorId,
|
|
200
|
-
"attribute",
|
|
201
|
-
shortcode,
|
|
202
|
-
overlay,
|
|
203
|
-
`attr:${shortcode}`,
|
|
204
|
-
ctx,
|
|
205
|
-
{ fmKey: "attributes" },
|
|
206
|
-
);
|
|
207
|
-
if (embedded) items.push(embedded);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
150
|
const sohlItems = sohlField(fm, "items", null);
|
|
212
151
|
if (Array.isArray(sohlItems)) {
|
|
213
152
|
sohlItems.forEach((entry, index) => {
|
|
@@ -333,8 +272,8 @@ export class Actors extends SystemActorCompiler {
|
|
|
333
272
|
*
|
|
334
273
|
* **The scores used are the ones just written.** `SkillLogic` resolves
|
|
335
274
|
* `attr.<code>` to an attribute's *effective* score, after active effects;
|
|
336
|
-
* all this pass has is the `scoreBase`
|
|
337
|
-
* 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,
|
|
338
277
|
* which is every being in content today. One that did carry such an effect
|
|
339
278
|
* would bake a Skill Base its client then disagrees with — that is the
|
|
340
279
|
* limit of doing this at build time, and the point to revisit if it bites.
|
|
@@ -387,6 +326,9 @@ export class Actors extends SystemActorCompiler {
|
|
|
387
326
|
const { value: authoredFolder, isAddress } = folderField(fm);
|
|
388
327
|
const folder = this.folderResolver(authoredFolder, { isAddress });
|
|
389
328
|
|
|
329
|
+
this.checkAnchoredSection(body || "", "appearance", ctx);
|
|
330
|
+
this.checkAnchoredSection(body || "", "dossier", ctx);
|
|
331
|
+
|
|
390
332
|
const system = {
|
|
391
333
|
// The frontmatter shortcode is the actor's stable `(type, shortcode)`
|
|
392
334
|
// key — and, for a being that is an archetype, its archetype
|
|
@@ -411,25 +353,6 @@ export class Actors extends SystemActorCompiler {
|
|
|
411
353
|
dossier: withDraftNotice(fm, renderSection(body || "", "dossier")),
|
|
412
354
|
};
|
|
413
355
|
|
|
414
|
-
// Fill `system.body` (+ the base-actor movement fields) from the being's
|
|
415
|
-
// frontmatter, rather than embedding a corpus item. The `sohl`
|
|
416
|
-
// block mirrors `system` field-for-field: `sohl.body` nests the body
|
|
417
|
-
// (`structure` / `weight` / …), with `currentMoveMedium` /
|
|
418
|
-
// `movementProfiles` flat alongside it. An **incorporeal** being omits
|
|
419
|
-
// `sohl.body` and keeps the schema's empty body.
|
|
420
|
-
const bodyField = sohlField(fm, "body", null);
|
|
421
|
-
if (bodyField && typeof bodyField === "object") {
|
|
422
|
-
const bodyData = extractBodyAndMovement(fm);
|
|
423
|
-
system.body = bodyData.body;
|
|
424
|
-
system.currentMoveMedium = bodyData.currentMoveMedium;
|
|
425
|
-
system.movementProfiles = bodyData.movementProfiles;
|
|
426
|
-
} else if (bodyField != null) {
|
|
427
|
-
this.noteError(
|
|
428
|
-
`${ctx}: sohl.body must be an inline object (structure/weight/…), got ${typeof bodyField}`,
|
|
429
|
-
);
|
|
430
|
-
this.errorCount++;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
356
|
// Being-only combat grouping (mirrors `system.defaultCombatGroup`).
|
|
434
357
|
const defaultCombatGroup = sohlField(fm, "defaultCombatGroup", undefined);
|
|
435
358
|
if (defaultCombatGroup !== undefined) {
|
package/sohl/item-fields.mjs
CHANGED
|
@@ -391,11 +391,11 @@ export const ITEM_FIELDS = Object.freeze({
|
|
|
391
391
|
...STRING,
|
|
392
392
|
default: "",
|
|
393
393
|
topLevelMeans:
|
|
394
|
-
"
|
|
395
|
-
"
|
|
396
|
-
"
|
|
397
|
-
"
|
|
398
|
-
"`sohl.items`.",
|
|
394
|
+
"A top-level `title` is refused outright, since an office's " +
|
|
395
|
+
"style of address belongs to the membership that holds the " +
|
|
396
|
+
"office and nothing outside the item supplies it. Author it " +
|
|
397
|
+
"at `sohl.system.title` on the item, or at the " +
|
|
398
|
+
"`system.title` of the entry in a being's `sohl.items`.",
|
|
399
399
|
describe: "The style of address the office carries.",
|
|
400
400
|
},
|
|
401
401
|
{
|
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.
|
|
@@ -126,6 +126,16 @@ export const RETIRED_ADDRESS_KEYS: Readonly<Record<string, string>>;
|
|
|
126
126
|
export const DEFAULT_ADDRESS_SCHEME: Readonly<{
|
|
127
127
|
prefix: "";
|
|
128
128
|
}>;
|
|
129
|
+
/**
|
|
130
|
+
* Every key a declared relationship may carry.
|
|
131
|
+
*
|
|
132
|
+
* Read by the manifest writer as well as by the configuration check: the keys
|
|
133
|
+
* Foundry's own relationship schema does not name are the build's own, and the
|
|
134
|
+
* published manifest carries none of them.
|
|
135
|
+
*
|
|
136
|
+
* @type {readonly string[]}
|
|
137
|
+
*/
|
|
138
|
+
export const RELATIONSHIP_KEYS: readonly string[];
|
|
129
139
|
/**
|
|
130
140
|
* How the loader hands {@link defineConfig} the system version it resolved.
|
|
131
141
|
*
|
|
@@ -274,7 +284,8 @@ export type PathsInput = {
|
|
|
274
284
|
unpack?: string | undefined;
|
|
275
285
|
/**
|
|
276
286
|
* Where a dependency declaring
|
|
277
|
-
* `itemCatalog: true`
|
|
287
|
+
* `itemCatalog: true` or
|
|
288
|
+
* `assetArchive: true` is unpacked.
|
|
278
289
|
* Inbound, and fetched rather than
|
|
279
290
|
* committed.
|
|
280
291
|
*/
|
|
@@ -438,6 +449,21 @@ export type RelationshipSpec = {
|
|
|
438
449
|
* `_stats.systemVersion` is stamped from.
|
|
439
450
|
*/
|
|
440
451
|
compatibility?: CompatibilitySpec | undefined;
|
|
452
|
+
/**
|
|
453
|
+
* Whether `deps fetch` unpacks this
|
|
454
|
+
* dependency's release archive and extracts
|
|
455
|
+
* its Item packs, so the actors pass can
|
|
456
|
+
* resolve embedded items this repository
|
|
457
|
+
* does not hold. Needs a `manifest`.
|
|
458
|
+
*/
|
|
459
|
+
itemCatalog?: boolean | undefined;
|
|
460
|
+
/**
|
|
461
|
+
* Whether `deps fetch` unpacks this
|
|
462
|
+
* dependency's release archive for its
|
|
463
|
+
* asset bytes alone, building no item
|
|
464
|
+
* catalogue from it. Needs a `manifest`.
|
|
465
|
+
*/
|
|
466
|
+
assetArchive?: boolean | undefined;
|
|
441
467
|
/**
|
|
442
468
|
* Whether `deps fetch` fetches this
|
|
443
469
|
* dependency's content index. Default
|
|
@@ -449,6 +475,16 @@ export type RelationshipSpec = {
|
|
|
449
475
|
* index.
|
|
450
476
|
*/
|
|
451
477
|
contentIndex?: boolean | undefined;
|
|
478
|
+
/**
|
|
479
|
+
* Whether this dependency's asset
|
|
480
|
+
* tree answers this package's own asset
|
|
481
|
+
* addresses ahead of its local record.
|
|
482
|
+
* Default `false`. Refuses `contentIndex:
|
|
483
|
+
* false` on the same entry, since a
|
|
484
|
+
* replacement with no fetched index to
|
|
485
|
+
* check against resolves nothing.
|
|
486
|
+
*/
|
|
487
|
+
assetReplacement?: boolean | undefined;
|
|
452
488
|
};
|
|
453
489
|
/**
|
|
454
490
|
* How a generated documentation page is framed in the repository publishing it.
|
|
@@ -628,6 +664,12 @@ export type ContentBuildConfigInput = {
|
|
|
628
664
|
* `Templates`). Default `[]`.
|
|
629
665
|
*/
|
|
630
666
|
skipDirectories?: string[] | undefined;
|
|
667
|
+
/**
|
|
668
|
+
* Whether `lint` refuses an asset
|
|
669
|
+
* record of this package carrying
|
|
670
|
+
* `ai: true`. Default `false`.
|
|
671
|
+
*/
|
|
672
|
+
forbidGeneratedArt?: boolean | undefined;
|
|
631
673
|
/**
|
|
632
674
|
* Reserved for
|
|
633
675
|
* `@heroiclands/package-build`, which
|
|
@@ -764,6 +806,11 @@ export type ContentBuildConfig = {
|
|
|
764
806
|
*/
|
|
765
807
|
docEntryTypes: ReadonlySet<string>;
|
|
766
808
|
skipDirectories: readonly string[];
|
|
809
|
+
/**
|
|
810
|
+
* Whether `lint` refuses an asset
|
|
811
|
+
* record of this package carrying `ai: true`.
|
|
812
|
+
*/
|
|
813
|
+
forbidGeneratedArt: boolean;
|
|
767
814
|
/**
|
|
768
815
|
* The
|
|
769
816
|
* fonts this package ships and the names it
|
|
@@ -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;
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `{#slug}` and `:::
|
|
2
|
+
* The `{#slug}` and `:::figure {#slug}` anchors a note declares.
|
|
3
3
|
*
|
|
4
|
-
* A bare `#` heading starts a journal page without declaring a slug
|
|
5
|
-
*
|
|
4
|
+
* A bare `#` heading starts a journal page without declaring a slug, and so
|
|
5
|
+
* does a figure fence that declares no id. A fence that declares one declares a
|
|
6
|
+
* slug and starts an addressable journal page.
|
|
6
7
|
*
|
|
7
8
|
* @param {string} body - The note's markdown body, frontmatter already removed.
|
|
8
9
|
* @param {number} [bodyLine] - The 1-based file line the body starts on, from
|
|
9
10
|
* `parseMarkdownFile`. Anchors are reported at their position in the **file**,
|
|
10
11
|
* so an editor can jump straight to one; passing nothing numbers from the body.
|
|
12
|
+
* @param {(address: string) => string|undefined} [resolveRole] - From a
|
|
13
|
+
* picture's address to the role its asset declares, so a figure anchor's
|
|
14
|
+
* `name` reads `Map 1` rather than `Figure 1` where it is due — see
|
|
15
|
+
* {@link module:engine/content-figures.scanFigures}.
|
|
11
16
|
* @returns {Array<{slug: string, name: string, level: number, line: number}>}
|
|
12
17
|
* In document order.
|
|
13
18
|
*/
|
|
14
|
-
export function collectAnchors(body: string, bodyLine?: number): Array<{
|
|
19
|
+
export function collectAnchors(body: string, bodyLine?: number, resolveRole?: (address: string) => string | undefined): Array<{
|
|
15
20
|
slug: string;
|
|
16
21
|
name: string;
|
|
17
22
|
level: number;
|