@heroiclands/package-build 21.0.0 → 21.2.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 +228 -0
- package/CONTENT.md +109 -7
- package/assets/fonts/libertinus/LibertinusMono-Regular.otf +0 -0
- package/assets/fonts/libertinus/LibertinusSans-Bold.otf +0 -0
- package/assets/fonts/libertinus/LibertinusSans-Italic.otf +0 -0
- package/assets/fonts/libertinus/LibertinusSans-Regular.otf +0 -0
- package/assets/fonts/libertinus/OFL.txt +94 -0
- package/assets/fonts/libertinus/provenance.yaml +11 -0
- package/assets/images/banners/afflictionbnr.webp +0 -0
- package/assets/images/banners/armorclothingbnr.webp +0 -0
- package/assets/images/banners/attributebnr.webp +0 -0
- package/assets/images/banners/containerbnr.webp +0 -0
- package/assets/images/banners/devdocsbnr.webp +0 -0
- package/assets/images/banners/miscgearbnr.webp +0 -0
- package/assets/images/banners/mysticalabilitybnr.webp +0 -0
- package/assets/images/banners/projectilebnr.webp +0 -0
- package/assets/images/banners/rulesbnr.webp +0 -0
- package/assets/images/banners/skillbnr.webp +0 -0
- package/assets/images/banners/traumabnr.webp +0 -0
- package/assets/images/banners/userguidebnr.webp +0 -0
- package/assets/images/banners/weapongearbnr.webp +0 -0
- package/assets/images/provenance.yaml +4 -0
- package/bin/content-build.mjs +19 -5
- package/ci/emit-asset-index.mjs +32 -0
- package/content-config.mjs +30 -1
- package/docs/api.md +100 -26
- package/docs/configuration.md +16 -7
- package/docs/content-format.md +512 -109
- package/engine/actor-compiler.mjs +58 -9
- package/engine/art-fields.mjs +292 -0
- package/engine/asset-index.mjs +397 -0
- package/engine/asset-types.mjs +192 -0
- package/engine/base-compiler.mjs +69 -1
- package/engine/bundles.mjs +5 -5
- package/engine/compile-corpus.mjs +4 -0
- package/engine/content-address.mjs +14 -2
- package/engine/content-charset.mjs +5 -1
- package/engine/content-embeds.mjs +314 -0
- package/engine/content-html.mjs +5 -1
- package/engine/content-icons.mjs +5 -1
- package/engine/content-images.mjs +26 -5
- package/engine/content-index.mjs +68 -33
- package/engine/content-links.mjs +164 -24
- package/engine/frontmatter-lint.mjs +36 -23
- package/engine/helpers.mjs +83 -31
- package/engine/index-records.mjs +63 -4
- package/engine/index.mjs +15 -0
- package/engine/infobox-registry.mjs +50 -3
- package/engine/infobox-render.mjs +4 -3
- package/engine/infobox.mjs +12 -8
- package/engine/item-compiler.mjs +10 -10
- package/engine/journals.mjs +3 -0
- package/engine/macros.mjs +16 -13
- package/engine/map-notes.mjs +29 -22
- package/engine/metadata-index.mjs +44 -36
- package/engine/note-vocabulary.mjs +71 -29
- package/engine/pack-router.mjs +14 -0
- package/engine/packagebuild-index.mjs +124 -0
- package/engine/packages.mjs +83 -0
- package/engine/pathnames.mjs +74 -0
- package/engine/pdf-build.mjs +133 -10
- package/engine/pdf-render.mjs +41 -41
- package/engine/scenes.mjs +12 -9
- package/engine/site-build.mjs +43 -16
- package/engine/site-index.mjs +10 -1
- package/engine/subtype-registry.mjs +21 -0
- package/engine/web-wikilinks.mjs +46 -12
- package/engine/wikilink-syntax.mjs +20 -2
- package/engine/wikilinks.mjs +15 -2
- package/hm3/actors.mjs +11 -38
- package/hm3/infobox.mjs +1 -1
- package/package.json +5 -2
- package/packagebuild-metadata.jsonl +13 -0
- package/sohl/actors.mjs +12 -86
- package/sohl/infobox.mjs +1 -1
- package/sohl/note-schemas.mjs +4 -16
- package/types/content-config.d.mts +8 -0
- package/types/engine/actor-compiler.d.mts +29 -0
- package/types/engine/art-fields.d.mts +192 -0
- package/types/engine/asset-index.d.mts +92 -0
- package/types/engine/asset-types.d.mts +110 -0
- package/types/engine/base-compiler.d.mts +42 -1
- package/types/engine/compile-corpus.d.mts +4 -1
- package/types/engine/content-charset.d.mts +4 -0
- package/types/engine/content-embeds.d.mts +218 -0
- package/types/engine/content-html.d.mts +4 -0
- package/types/engine/content-icons.d.mts +4 -0
- package/types/engine/content-images.d.mts +15 -2
- package/types/engine/content-index.d.mts +27 -15
- package/types/engine/frontmatter-lint.d.mts +11 -14
- package/types/engine/helpers.d.mts +7 -2
- package/types/engine/index-records.d.mts +44 -3
- package/types/engine/index.d.mts +5 -0
- package/types/engine/infobox-registry.d.mts +36 -1
- package/types/engine/infobox.d.mts +6 -4
- package/types/engine/macros.d.mts +9 -5
- package/types/engine/map-notes.d.mts +10 -6
- package/types/engine/metadata-index.d.mts +2 -17
- package/types/engine/note-vocabulary.d.mts +30 -0
- package/types/engine/packagebuild-index.d.mts +56 -0
- package/types/engine/packages.d.mts +62 -0
- package/types/engine/pathnames.d.mts +41 -81
- package/types/engine/pdf-build.d.mts +58 -1
- package/types/engine/pdf-render.d.mts +3 -3
- package/types/engine/scenes.d.mts +2 -1
- package/types/engine/site-build.d.mts +5 -1
- package/types/engine/site-index.d.mts +4 -1
- package/types/engine/subtype-registry.d.mts +20 -0
- package/types/engine/web-wikilinks.d.mts +14 -4
- package/types/engine/wikilink-syntax.d.mts +13 -1
- package/types/engine/wikilinks.d.mts +7 -1
- package/types/sohl/actors.d.mts +0 -1
|
@@ -56,6 +56,7 @@ import { emitDiagnostic } from "./diagnostics.mjs";
|
|
|
56
56
|
// extraction is shared; which field a section lands in stays the system's.
|
|
57
57
|
export { extractAnchorSection, renderSection } from "./anchored-sections.mjs";
|
|
58
58
|
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
59
|
+
import { beingDefaultArt } from "./art-fields.mjs";
|
|
59
60
|
import { contentPackage } from "./content-package.mjs";
|
|
60
61
|
// Which Foundry Actor subtype a note's `type` compiles into, and which note
|
|
61
62
|
// types are actors at all. Looked up in the system's declared map, never
|
|
@@ -64,8 +65,10 @@ import { mapsNoteType, noteTypesFor, referencedSubtype } from "./document-subtyp
|
|
|
64
65
|
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
65
66
|
// An `items:` entry's `system:` overlay is merged verbatim, so it reaches the
|
|
66
67
|
// document by a path no field declaration sits on — including the
|
|
67
|
-
// fields the document is supposed to write for itself in play.
|
|
68
|
-
|
|
68
|
+
// fields the document is supposed to write for itself in play. `itemArt` is
|
|
69
|
+
// the same table the Item pass defaults from, so an entry that copies no
|
|
70
|
+
// template and names no art of its own carries what a note of its type would.
|
|
71
|
+
import { itemArt, itemFields } from "./item-registry.mjs";
|
|
69
72
|
import { runtimeOnlyIn, runtimeOnlyMessage } from "./runtime-only-fields.mjs";
|
|
70
73
|
// A `model:` is an address, read by the same grammar every wikilink is,
|
|
71
74
|
// so an author writes one form and meets one set of messages.
|
|
@@ -387,11 +390,10 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
387
390
|
static label = "actor";
|
|
388
391
|
|
|
389
392
|
/**
|
|
390
|
-
* **Both**, and they are two independent pictures
|
|
391
|
-
*
|
|
392
|
-
* — and `
|
|
393
|
-
*
|
|
394
|
-
* `system.bioImage` for HM3).
|
|
393
|
+
* **Both**, and they are two independent pictures. `icon` is the actor's
|
|
394
|
+
* profile art — what a directory listing shows beside the name and what the
|
|
395
|
+
* sheet header carries — and `tokenIcon` is what a token on the canvas
|
|
396
|
+
* wears; unset, the token follows the profile.
|
|
395
397
|
*
|
|
396
398
|
* Declared on the shared class because both subclasses emit both. A system
|
|
397
399
|
* whose actor genuinely carried only one would override it here rather than
|
|
@@ -399,7 +401,36 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
399
401
|
*
|
|
400
402
|
* @type {readonly string[]}
|
|
401
403
|
*/
|
|
402
|
-
static emitsArt = Object.freeze(["
|
|
404
|
+
static emitsArt = Object.freeze(["icon", "tokenIcon"]);
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* The actor's two pieces of art, resolved, with the being default beneath
|
|
408
|
+
* them.
|
|
409
|
+
*
|
|
410
|
+
* **The default is chosen from the note's tags**, which only a compiler can
|
|
411
|
+
* read: a `character` falls back to one file and a `creature` to another,
|
|
412
|
+
* and both are addresses in the package that ships them. A tree whose index
|
|
413
|
+
* cannot answer that address — a dependency not yet fetched — falls through
|
|
414
|
+
* to the subtype's own default, so the document is never left with no art.
|
|
415
|
+
*
|
|
416
|
+
* `tokenIcon` unset follows `icon`, and the fallback is applied after
|
|
417
|
+
* resolution rather than before: a note naming an icon and no token icon
|
|
418
|
+
* means "the token wears the profile art", not "the token wears the
|
|
419
|
+
* default".
|
|
420
|
+
*
|
|
421
|
+
* @param {object} fm - The note's frontmatter.
|
|
422
|
+
* @param {string} fallback - The subtype's own default art path.
|
|
423
|
+
* @returns {{img: string, token: string}} The two paths.
|
|
424
|
+
*/
|
|
425
|
+
actorArt(fm, fallback) {
|
|
426
|
+
const address = beingDefaultArt(fm);
|
|
427
|
+
const byTag = address ? this.artPathOf(address, "icon", "icon") : null;
|
|
428
|
+
const base = byTag ?? fallback;
|
|
429
|
+
// Nullish, not `||`: a note that names no art takes the default, one
|
|
430
|
+
// that writes `""` ships blank on purpose.
|
|
431
|
+
const img = this.artPath(fm, "icon") ?? base;
|
|
432
|
+
return { img, token: this.artPath(fm, "tokenIcon") ?? img };
|
|
433
|
+
}
|
|
403
434
|
|
|
404
435
|
/**
|
|
405
436
|
* Which `(actor, subType:identity)` each resolved entry claimed, and the
|
|
@@ -637,6 +668,12 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
637
668
|
* every embedded id exactly where it was.
|
|
638
669
|
* Returns null if the descriptor cannot be resolved.
|
|
639
670
|
*
|
|
671
|
+
* **An entry's `data:` is the authoring container, not a document field.**
|
|
672
|
+
* Its art slot is read here and the container itself goes no further, so an
|
|
673
|
+
* entry writing `data.icon` reaches the document as a resolved `img` and an
|
|
674
|
+
* entry writing none takes its type's default — the same two rules an item
|
|
675
|
+
* note compiles under, reached by the same functions.
|
|
676
|
+
*
|
|
640
677
|
* @param {Map<string, object>} itemsMap - The predefined items, by address.
|
|
641
678
|
* @param {string} actorId - The owning actor's id, seeding embedded ids.
|
|
642
679
|
* @param {string} type - The **note** type the reference names.
|
|
@@ -736,8 +773,20 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
736
773
|
this.errorCount++;
|
|
737
774
|
return null;
|
|
738
775
|
}
|
|
739
|
-
|
|
776
|
+
// `data:` is where a note names art, and it is an authoring container:
|
|
777
|
+
// the compiled document takes the resolved path and never the container.
|
|
778
|
+
// Held back from the merge rather than deleted after it, so there is no
|
|
779
|
+
// window in which a document carries one.
|
|
780
|
+
const { data: _authored, ...fields } = overlay ?? {};
|
|
781
|
+
const merged = overlay ? deepMerge(base, fields) : base;
|
|
740
782
|
merged.type = subType;
|
|
783
|
+
// The two art rules an item note compiles under, applied to an entry
|
|
784
|
+
// that is one in every respect but where it is written: the address it
|
|
785
|
+
// names wins, the template it copies answers next, and the type's own
|
|
786
|
+
// default answers last. Nullish coalescing throughout, so `icon: ""`
|
|
787
|
+
// ships blank on purpose rather than collecting a default.
|
|
788
|
+
merged.img =
|
|
789
|
+
this.artPath(overlay ?? {}, "icon") ?? merged.img ?? itemArt(type, this.system);
|
|
741
790
|
const identity = embeddedIdentity(merged);
|
|
742
791
|
const claim = `${actorId}\u0000${itemAddress(/** @type {string} */ (subType), identity)}`;
|
|
743
792
|
const first = this.#embeddedClaims.get(claim);
|
|
@@ -0,0 +1,292 @@
|
|
|
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 art slots a note declares, and turning one of them into a path.
|
|
16
|
+
*
|
|
17
|
+
* A note names the art a *document field* needs, and every other image in it is
|
|
18
|
+
* inline. Each slot is an ordinary `WikiLink` field declaring a default type,
|
|
19
|
+
* exactly as `seat` declares `place`: a bare shortcode takes its type from the
|
|
20
|
+
* declaration, and a value that qualifies itself climbs the same short-form
|
|
21
|
+
* ladder every other link uses.
|
|
22
|
+
*
|
|
23
|
+
* **A bare shortcode is told from a written address by the separator alone.** A
|
|
24
|
+
* shortcode is lowercase letters and digits (`ADDRESS_SEGMENT_PATTERN`), so a
|
|
25
|
+
* hyphen can only be a segment boundary — which makes `anvil` the bare form and
|
|
26
|
+
* `icon-anvil`, `none-icon-anvil` and `sohl-none-icon-anvil` the written ones,
|
|
27
|
+
* with nothing to guess and no vocabulary to match against.
|
|
28
|
+
*
|
|
29
|
+
* **Resolution is one step, because the record carries the path.** The address
|
|
30
|
+
* names a record, the record names the file's path inside its own package, and
|
|
31
|
+
* the pathname rule ({@link module:engine/pathnames}) turns the pair into the
|
|
32
|
+
* address each surface serves. So an art field and a body image go through the
|
|
33
|
+
* same ownership rule rather than through two that agree by inspection, and a
|
|
34
|
+
* package that ships no Foundry package — `packagebuild`, which is an npm
|
|
35
|
+
* package and installs nowhere — yields no Foundry address rather than a
|
|
36
|
+
* plausible broken one.
|
|
37
|
+
*
|
|
38
|
+
* @module
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { expandAddress } from "./content-address.mjs";
|
|
42
|
+
import { hasTag } from "./note-vocabulary.mjs";
|
|
43
|
+
import { ASSET_SYSTEM, isAssetType } from "./asset-types.mjs";
|
|
44
|
+
import { ASSETS_SEGMENT } from "./pathnames.mjs";
|
|
45
|
+
import { isAssetRecord } from "./index-records.mjs";
|
|
46
|
+
import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
|
|
47
|
+
import { readQualifier } from "./wikilinks.mjs";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* One art slot: the key a note authors, and the type a bare value takes.
|
|
51
|
+
*
|
|
52
|
+
* @typedef {object} ArtSlot
|
|
53
|
+
* @property {string} key - The key under `data:`.
|
|
54
|
+
* @property {string} type - The asset type a bare shortcode defaults to.
|
|
55
|
+
* @property {boolean} document - Whether the slot reaches a compiled document.
|
|
56
|
+
* @property {string} describe - One line, for the author-facing reference.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The four art slots, in the order the specification tabulates them.
|
|
61
|
+
*
|
|
62
|
+
* `banner` is the one that reaches no compiled document: it is the page's hero
|
|
63
|
+
* image, read by the site and by the book's section plates and by nothing else.
|
|
64
|
+
* That is what `document: false` states, and it is why the inert-art check
|
|
65
|
+
* skips it — a key that is *meant* to reach no document is not an inert key.
|
|
66
|
+
*
|
|
67
|
+
* @type {readonly ArtSlot[]}
|
|
68
|
+
*/
|
|
69
|
+
export const ART_SLOTS = Object.freeze([
|
|
70
|
+
Object.freeze({
|
|
71
|
+
key: "icon",
|
|
72
|
+
type: "icon",
|
|
73
|
+
document: true,
|
|
74
|
+
describe: "The document's profile art, resolved into `img`.",
|
|
75
|
+
}),
|
|
76
|
+
Object.freeze({
|
|
77
|
+
key: "tokenIcon",
|
|
78
|
+
type: "icon",
|
|
79
|
+
document: true,
|
|
80
|
+
describe: "What a token on the canvas wears; unset, it follows `icon`.",
|
|
81
|
+
}),
|
|
82
|
+
Object.freeze({
|
|
83
|
+
key: "bgImage",
|
|
84
|
+
type: "image",
|
|
85
|
+
document: true,
|
|
86
|
+
describe: "A map's background art, resolved into `background.src`.",
|
|
87
|
+
}),
|
|
88
|
+
Object.freeze({
|
|
89
|
+
key: "banner",
|
|
90
|
+
type: "image",
|
|
91
|
+
document: false,
|
|
92
|
+
describe: "The page's hero image. Reaches no compiled document.",
|
|
93
|
+
}),
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The art slot one key names, or `undefined`.
|
|
98
|
+
*
|
|
99
|
+
* @param {unknown} key - The key under `data:`.
|
|
100
|
+
* @returns {ArtSlot|undefined} The slot.
|
|
101
|
+
*/
|
|
102
|
+
export function artSlot(key) {
|
|
103
|
+
return ART_SLOTS.find((slot) => slot.key === key);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The address an authored art value names.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} value - The value as authored.
|
|
110
|
+
* @param {string} defaultType - The type the field declares.
|
|
111
|
+
* @returns {string} A written address, which may be partial.
|
|
112
|
+
*/
|
|
113
|
+
export function artTarget(value, defaultType) {
|
|
114
|
+
const written = String(value);
|
|
115
|
+
return written.includes("-") ? written : `${defaultType}-${written}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The address space an asset reference resolves against, from a corpus.
|
|
120
|
+
*
|
|
121
|
+
* Shaped exactly as {@link module:engine/wikilinks.buildWikilinkIndex}'s result
|
|
122
|
+
* is in the parts a resolver reads, so the site, the book and the pack compilers
|
|
123
|
+
* answer one authored address the same way.
|
|
124
|
+
*
|
|
125
|
+
* **The note types belong in `types` as well as the asset ones.** Without them
|
|
126
|
+
* `being-thorn` does not parse as an address at all, and an embed naming a note
|
|
127
|
+
* is reported as an unknown type on one surface and as the wrong kind of type on
|
|
128
|
+
* another — one mistake, two verdicts, which is what the shared vocabulary
|
|
129
|
+
* exists to prevent.
|
|
130
|
+
*
|
|
131
|
+
* @param {readonly object[]} records - The corpus, from
|
|
132
|
+
* {@link module:engine/content-index.indexRecordsFor}.
|
|
133
|
+
* @param {object} [opts]
|
|
134
|
+
* @param {object} [opts.config] - The resolved build configuration.
|
|
135
|
+
* @param {{index?: Map<string, object>, packages?: Iterable<string>}} [opts.foreign] -
|
|
136
|
+
* The vendored indexes a dependency published.
|
|
137
|
+
* @param {Iterable<string>} [opts.types] - The note types this tree knows.
|
|
138
|
+
* @returns {object} The index.
|
|
139
|
+
*/
|
|
140
|
+
export function assetAddressIndex(records = [], { config, foreign, types = [] } = {}) {
|
|
141
|
+
return {
|
|
142
|
+
types: new Set([...ASSET_TYPE_NAMES, ...types]),
|
|
143
|
+
packages: new Set([config?.contentPackage, ...(foreign?.packages ?? [])].filter(Boolean)),
|
|
144
|
+
contentPackage: config?.contentPackage,
|
|
145
|
+
assets: new Map(
|
|
146
|
+
records
|
|
147
|
+
.filter(isAssetRecord)
|
|
148
|
+
.map((record) => [record.address?.canonical, record])
|
|
149
|
+
.filter(([key]) => key),
|
|
150
|
+
),
|
|
151
|
+
foreign: foreign?.index ?? new Map(),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The asset one authored value names, or why it names none.
|
|
157
|
+
*
|
|
158
|
+
* The whole lookup in one place, because two callers need it and they need
|
|
159
|
+
* different halves of the answer: an art slot needs the record, and an embed
|
|
160
|
+
* needs to tell an address that resolves to nothing from one that reaches the
|
|
161
|
+
* wrong *kind* of type. Those are different mistakes with different fixes, and a
|
|
162
|
+
* single `null` would collapse them into one message.
|
|
163
|
+
*
|
|
164
|
+
* Local files answer first and foreign ones after, which is an ordering of maps
|
|
165
|
+
* rather than a precedence rule: an address carries its own package, so the two
|
|
166
|
+
* cannot both hold one.
|
|
167
|
+
*
|
|
168
|
+
* @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex},
|
|
169
|
+
* or the equivalent the site and the book build.
|
|
170
|
+
* @param {unknown} value - The value as authored.
|
|
171
|
+
* @param {string} defaultType - The type a bare value takes.
|
|
172
|
+
* @returns {{record: {package: string, asset: {path: string}}, pathname: string}
|
|
173
|
+
* |{record: null, reason: string, type?: string}} The asset and the pathname
|
|
174
|
+
* it is at, or a reason from
|
|
175
|
+
* {@link module:engine/wikilink-syntax.LINK_FINDING_REASONS}.
|
|
176
|
+
*/
|
|
177
|
+
export function readAssetAddress(index, value, defaultType) {
|
|
178
|
+
if (typeof value !== "string" || !value) {
|
|
179
|
+
return { record: null, reason: "not-an-address" };
|
|
180
|
+
}
|
|
181
|
+
const target = artTarget(value, defaultType);
|
|
182
|
+
const read = readQualifier(target, index?.types, index?.packages);
|
|
183
|
+
if (!read) return { record: null, reason: "not-an-address" };
|
|
184
|
+
if (read.reason) return { record: null, reason: read.reason };
|
|
185
|
+
// **Asset types only.** An address may name any type the vocabulary holds,
|
|
186
|
+
// and most of them name a note — which has no file to draw. Refused by its
|
|
187
|
+
// own reason rather than left to resolve to nothing, because the fix is a
|
|
188
|
+
// different one: an ordinary link, not a corrected shortcode.
|
|
189
|
+
if (!isAssetType(read.type)) {
|
|
190
|
+
return { record: null, reason: "not-an-asset", type: String(read.type) };
|
|
191
|
+
}
|
|
192
|
+
// The system segment is fixed at `none` for an asset type, so where the
|
|
193
|
+
// value was written does not enter into it — an embedded item's art is
|
|
194
|
+
// authored inside a system block and still names the same file.
|
|
195
|
+
const canonical = expandAddress(read, {
|
|
196
|
+
package: index?.contentPackage ?? index?.packageId,
|
|
197
|
+
system: ASSET_SYSTEM,
|
|
198
|
+
});
|
|
199
|
+
const hit = index?.assets?.get(canonical) ?? index?.foreign?.get(canonical) ?? null;
|
|
200
|
+
if (!hit?.asset?.path || !hit.package) return { record: null, reason: "unresolved" };
|
|
201
|
+
return { record: hit, pathname: `${hit.package}/${ASSETS_SEGMENT}/${hit.asset.path}` };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The index record an art value resolves to, or `null`.
|
|
206
|
+
*
|
|
207
|
+
* @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex}.
|
|
208
|
+
* @param {unknown} value - The value as authored.
|
|
209
|
+
* @param {string} defaultType - The type the field declares.
|
|
210
|
+
* @returns {{package: string, asset: {path: string}}|null} The record.
|
|
211
|
+
*/
|
|
212
|
+
export function resolveArtRecord(index, value, defaultType) {
|
|
213
|
+
return readAssetAddress(index, value, defaultType).record;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The pathname an art value names, in the form an authored one takes.
|
|
218
|
+
*
|
|
219
|
+
* Handing the result to {@link module:engine/helpers.resolveImg} is what puts an
|
|
220
|
+
* art address and a body image through one ownership rule. The two empties
|
|
221
|
+
* survive it unchanged: `null` and an absent key mean *no art named, apply the
|
|
222
|
+
* default*, and `""` means *ship blank on purpose*.
|
|
223
|
+
*
|
|
224
|
+
* @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex}.
|
|
225
|
+
* @param {unknown} value - The value as authored.
|
|
226
|
+
* @param {string} defaultType - The type the field declares.
|
|
227
|
+
* @returns {{pathname: string|null, resolved: boolean}} The pathname, and
|
|
228
|
+
* whether an address was actually answered — which tells a caller applying a
|
|
229
|
+
* default apart from one whose address named nothing.
|
|
230
|
+
*/
|
|
231
|
+
export function artPathname(index, value, defaultType) {
|
|
232
|
+
if (value == null) return { pathname: null, resolved: true };
|
|
233
|
+
if (value === "") return { pathname: "", resolved: true };
|
|
234
|
+
const read = readAssetAddress(index, value, defaultType);
|
|
235
|
+
if (!read.record) return { pathname: null, resolved: false };
|
|
236
|
+
return { pathname: read.pathname, resolved: true };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The art a being falls back to, by the kind it is tagged.
|
|
241
|
+
*
|
|
242
|
+
* Both files ship in `sohl`, under `assets/icons/other/`, and both are named
|
|
243
|
+
* here as addresses rather than as paths for the reason every art reference is:
|
|
244
|
+
* the address resolves to a record that carries the owning package, so a
|
|
245
|
+
* package borrowing the default gets the same file the system ships.
|
|
246
|
+
*
|
|
247
|
+
* **Only the compiler can choose between them**, because only the compiler
|
|
248
|
+
* reads the note's tags. A schema default is the last resort beneath this one,
|
|
249
|
+
* and covers a world document created by hand, which no note describes.
|
|
250
|
+
*
|
|
251
|
+
* @type {Readonly<Record<string, string>>}
|
|
252
|
+
*/
|
|
253
|
+
export const BEING_DEFAULT_ART = Object.freeze({
|
|
254
|
+
character: "sohl-none-icon-defaultcharhead",
|
|
255
|
+
creature: "sohl-none-icon-defaultcreathead",
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The default art address a being's own tags choose, or `null`.
|
|
260
|
+
*
|
|
261
|
+
* `character` means a **person**, not a human, and `creature` everything else;
|
|
262
|
+
* a being carries exactly one of the two, which is what makes the choice a
|
|
263
|
+
* lookup rather than a precedence rule.
|
|
264
|
+
*
|
|
265
|
+
* @param {object} fm - The note's frontmatter.
|
|
266
|
+
* @returns {string|null} The address, or `null` for a note carrying neither tag.
|
|
267
|
+
*/
|
|
268
|
+
export function beingDefaultArt(fm) {
|
|
269
|
+
for (const [tag, address] of Object.entries(BEING_DEFAULT_ART)) {
|
|
270
|
+
if (hasTag(fm, tag)) return address;
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* What an unresolved art address is reported as.
|
|
277
|
+
*
|
|
278
|
+
* One wording, so the four compilers that can meet the case do not each invent
|
|
279
|
+
* their own.
|
|
280
|
+
*
|
|
281
|
+
* @param {string} key - The key that was authored.
|
|
282
|
+
* @param {unknown} value - The value it carried.
|
|
283
|
+
* @param {string} defaultType - The type the field declares.
|
|
284
|
+
* @returns {string} The message.
|
|
285
|
+
*/
|
|
286
|
+
export function unresolvedArtMessage(key, value, defaultType) {
|
|
287
|
+
return (
|
|
288
|
+
`\`data.${key}\` names \`${artTarget(String(value), defaultType)}\`, and no ` +
|
|
289
|
+
`asset in this package or in a fetched index carries that address — the ` +
|
|
290
|
+
`document takes its default art instead`
|
|
291
|
+
);
|
|
292
|
+
}
|