@heroiclands/package-build 22.9.1 → 22.10.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 +54 -0
- package/CONTENT.md +33 -0
- package/bin/content-build.mjs +3 -1
- package/docs/api.md +10 -6
- package/docs/content-format.md +384 -200
- package/docs/project-setup.md +23 -0
- package/engine/actor-compiler.mjs +20 -7
- package/engine/address-fields.mjs +404 -0
- package/engine/address-values.mjs +80 -0
- package/engine/address.mjs +769 -0
- package/engine/art-fields.mjs +112 -97
- package/engine/art-slots.mjs +81 -0
- package/engine/base-compiler.mjs +59 -7
- package/engine/bundle-notes.mjs +5 -2
- package/engine/bundles.mjs +17 -10
- package/engine/content-address.mjs +23 -186
- package/engine/content-embeds.mjs +18 -4
- package/engine/content-format-check.mjs +50 -22
- package/engine/content-index.mjs +85 -43
- package/engine/content-links.mjs +105 -104
- package/engine/content-lint.mjs +11 -1
- package/engine/content-tables.mjs +9 -1
- package/engine/field-spec.mjs +20 -4
- package/engine/folder-notes.mjs +44 -15
- package/engine/foundry-entries.mjs +3 -0
- package/engine/frontmatter-lint.mjs +144 -43
- package/engine/frontmatter.mjs +55 -10
- package/engine/generate.mjs +7 -1
- package/engine/helpers.mjs +37 -9
- package/engine/holdings.mjs +41 -12
- package/engine/index-records.mjs +2 -0
- package/engine/infobox-render.mjs +2 -1
- package/engine/infobox.mjs +26 -6
- package/engine/item-compiler.mjs +5 -0
- package/engine/item-docs.mjs +7 -44
- package/engine/map-dot.mjs +2 -0
- package/engine/map-notes.mjs +7 -2
- package/engine/map-places.mjs +42 -10
- package/engine/metadata-index.mjs +60 -5
- package/engine/note-addresses.mjs +310 -0
- package/engine/note-vocabulary.mjs +145 -26
- package/engine/pdf-build.mjs +1 -1
- package/engine/pdf-toc.mjs +17 -2
- package/engine/place-relations.mjs +160 -50
- package/engine/populations.mjs +17 -9
- package/engine/reference-targets.mjs +54 -0
- package/engine/scenes.mjs +5 -2
- package/engine/shortcode-references.mjs +68 -0
- package/engine/site-build.mjs +39 -15
- package/engine/site-index.mjs +57 -75
- package/engine/sql-tables.mjs +43 -9
- package/engine/system-block.mjs +21 -0
- package/engine/system-vocabulary.mjs +46 -0
- package/engine/web-wikilinks.mjs +11 -9
- package/engine/wikilink-syntax.mjs +21 -0
- package/engine/wikilinks.mjs +92 -306
- package/githooks/attribution.mjs +82 -0
- package/githooks/commit-message.mjs +26 -0
- package/githooks/commit-msg +3 -35
- package/hm3/actors.mjs +8 -24
- package/hm3/item-fields.mjs +15 -5
- package/package.json +1 -1
- package/sohl/infobox.mjs +80 -17
- package/sohl/item-fields.mjs +114 -41
- package/types/engine/address-fields.d.mts +90 -0
- package/types/engine/address-values.d.mts +34 -0
- package/types/engine/address.d.mts +452 -0
- package/types/engine/art-fields.d.mts +59 -53
- package/types/engine/art-slots.d.mts +40 -0
- package/types/engine/base-compiler.d.mts +24 -2
- package/types/engine/bundle-notes.d.mts +2 -2
- package/types/engine/content-address.d.mts +3 -139
- package/types/engine/content-embeds.d.mts +1 -1
- package/types/engine/content-format-check.d.mts +25 -0
- package/types/engine/content-index.d.mts +2 -2
- package/types/engine/content-links.d.mts +6 -5
- package/types/engine/field-spec.d.mts +44 -9
- package/types/engine/folder-notes.d.mts +11 -10
- package/types/engine/frontmatter-lint.d.mts +7 -97
- package/types/engine/frontmatter.d.mts +16 -1
- package/types/engine/helpers.d.mts +2 -2
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/item-docs.d.mts +1 -35
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/note-addresses.d.mts +41 -0
- package/types/engine/note-vocabulary.d.mts +20 -3
- package/types/engine/place-relations.d.mts +12 -1
- package/types/engine/populations.d.mts +17 -2
- package/types/engine/reference-targets.d.mts +9 -0
- package/types/engine/shortcode-references.d.mts +14 -0
- package/types/engine/site-build.d.mts +3 -3
- package/types/engine/site-index.d.mts +9 -30
- package/types/engine/sql-tables.d.mts +2 -2
- package/types/engine/system-block.d.mts +17 -0
- package/types/engine/system-vocabulary.d.mts +16 -0
- package/types/engine/wikilink-syntax.d.mts +7 -0
- package/types/engine/wikilinks.d.mts +6 -109
- package/types/hm3/item-fields.d.mts +5 -0
- package/types/sohl/infobox.d.mts +4 -5
- package/types/sohl/item-fields.d.mts +8 -0
package/engine/holdings.mjs
CHANGED
|
@@ -43,7 +43,19 @@
|
|
|
43
43
|
* @module
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
|
+
import { isAddressTuple } from "./address.mjs";
|
|
46
47
|
import { positionInFrontmatter } from "./diagnostics.mjs";
|
|
48
|
+
import { parseAddress } from "./address.mjs";
|
|
49
|
+
import { readCanonicalKey } from "./content-address.mjs";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The one type a `parents` or `domains` entry may name — the default
|
|
53
|
+
* {@link module:engine/address.parseAddress} fills in when the segment is
|
|
54
|
+
* omitted, and the whole of what this position accepts.
|
|
55
|
+
*
|
|
56
|
+
* @type {ReadonlySet<string>}
|
|
57
|
+
*/
|
|
58
|
+
const PLACE_TYPES = Object.freeze(new Set(["place"]));
|
|
47
59
|
|
|
48
60
|
/**
|
|
49
61
|
* The keys this module writes, and which a note cannot author: `contains`
|
|
@@ -103,22 +115,39 @@ export const HELD_SUBTYPES = Object.freeze(["settlement", "site", "structure"]);
|
|
|
103
115
|
/**
|
|
104
116
|
* The shortcode a `parents` or `domains` entry names, lower case.
|
|
105
117
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
118
|
+
* **A `parents` or `domains` entry is an Address** — the specification types
|
|
119
|
+
* both `Address[]`, targeting `place` — read at whatever length says what it
|
|
120
|
+
* means, `place` the default and the whole of the accepted set. Frontmatter
|
|
121
|
+
* never holds a Wikilink, so no bracket, `|` or `#` is stripped here; a
|
|
122
|
+
* qualified form is read structurally by {@link readCanonicalKey} for its
|
|
123
|
+
* literal four segments, since this position asks neither its package nor its
|
|
124
|
+
* system, only the shortcode {@link module:engine/address} would resolve it
|
|
125
|
+
* to anyway.
|
|
126
|
+
*
|
|
127
|
+
* **The match this shortcode feeds is deliberately package-blind.** `places`
|
|
128
|
+
* and `affiliations` merge this package's own notes with every fetched
|
|
129
|
+
* dependency's into one shortcode space, first declaration wins — the way a
|
|
130
|
+
* dependency's generic geography attaches to whatever a consumer calls the
|
|
131
|
+
* same shortcode, which is the point of _A dependency's places and
|
|
132
|
+
* affiliations take part_ above. Comparing the full Address instead would
|
|
133
|
+
* ask an entry's package to agree with the citing note's, and a fetched
|
|
134
|
+
* entry's own `parents` or `domains` is written in *its* package while the
|
|
135
|
+
* blend it takes part in is *this* one's — see
|
|
136
|
+
* `tests/site-holdings.test.ts`'s `empire.domains: ["mill", "abroad"]`, where
|
|
137
|
+
* `mill` is local and `abroad` is the dependency's own, both bare, both
|
|
138
|
+
* correct only because neither package is asked.
|
|
109
139
|
*
|
|
110
140
|
* @param {unknown} value - One entry.
|
|
111
141
|
* @returns {string} Its shortcode, or `""` for an entry that names nothing.
|
|
112
142
|
*/
|
|
113
143
|
function namedShortcode(value) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return text.split("-").pop()?.toLowerCase() ?? "";
|
|
144
|
+
if (isAddressTuple(value)) return value.shortcode.toLowerCase();
|
|
145
|
+
const text = String(value ?? "").trim();
|
|
146
|
+
if (!text) return "";
|
|
147
|
+
const qualified = readCanonicalKey(text);
|
|
148
|
+
if (qualified) return qualified.shortcode.toLowerCase();
|
|
149
|
+
const read = parseAddress(text, { type: "place", types: PLACE_TYPES });
|
|
150
|
+
return read.reason ? "" : read.shortcode.toLowerCase();
|
|
122
151
|
}
|
|
123
152
|
|
|
124
153
|
/**
|
|
@@ -244,7 +273,7 @@ export function foreignHoldingsNodes(foreignIndex) {
|
|
|
244
273
|
for (const [canonical, entry] of foreignIndex) {
|
|
245
274
|
const type = String(entry?.type ?? "");
|
|
246
275
|
if (type !== "place" && type !== "affiliation") continue;
|
|
247
|
-
const shortcode = canonical
|
|
276
|
+
const shortcode = readCanonicalKey(canonical)?.shortcode?.toLowerCase() ?? "";
|
|
248
277
|
if (!shortcode) continue;
|
|
249
278
|
out.push({
|
|
250
279
|
shortcode,
|
package/engine/index-records.mjs
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
import path from "node:path";
|
|
35
|
+
import { isAddressTuple } from "./address.mjs";
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* The keys the content index adds to a record, which a note therefore may not
|
|
@@ -72,6 +73,7 @@ export const DERIVED_KEYS = Object.freeze([
|
|
|
72
73
|
* @returns {unknown} The value with every plain object's keys in sorted order.
|
|
73
74
|
*/
|
|
74
75
|
export function sortKeysDeep(value) {
|
|
76
|
+
if (isAddressTuple(value)) return value;
|
|
75
77
|
if (Array.isArray(value)) return value.map(sortKeysDeep);
|
|
76
78
|
if (value === null || typeof value !== "object") return value;
|
|
77
79
|
// A Date or any other exotic object would lose itself in a rebuild from
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* @module
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
+
import { encodeAddresses } from "./address-values.mjs";
|
|
37
38
|
import { sectionHolds } from "./infobox.mjs";
|
|
38
39
|
import { escapeTypst, escapeTypstString } from "./pdf-render.mjs";
|
|
39
40
|
|
|
@@ -227,7 +228,7 @@ export function sectionHasContent(section) {
|
|
|
227
228
|
*/
|
|
228
229
|
export function linkToTypst(value, links, labelFor) {
|
|
229
230
|
const text = escapeTypst(value?.text ?? "");
|
|
230
|
-
const anchor = value?.address ? links?.get(value.address) : undefined;
|
|
231
|
+
const anchor = value?.address ? links?.get(encodeAddresses(value.address)) : undefined;
|
|
231
232
|
if (anchor) return `#link(<${labelFor(anchor)}>)[${text}]`;
|
|
232
233
|
if (value?.url) return `#link("${escapeTypstString(value.url)}")[${text}]`;
|
|
233
234
|
return text;
|
package/engine/infobox.mjs
CHANGED
|
@@ -112,11 +112,13 @@
|
|
|
112
112
|
* @module
|
|
113
113
|
*/
|
|
114
114
|
|
|
115
|
+
import { isAddressTuple, renderAddress } from "./address.mjs";
|
|
115
116
|
import { getFrontmatter } from "./frontmatter.mjs";
|
|
116
117
|
import { currentType } from "./ids.mjs";
|
|
117
118
|
import { NOTE_VOCABULARY, dataFields } from "./note-vocabulary.mjs";
|
|
118
119
|
import { readAliasedField } from "./retired-fields.mjs";
|
|
119
120
|
import { subtypeRow } from "./document-subtypes.mjs";
|
|
121
|
+
import { authoredKey } from "./system-block.mjs";
|
|
120
122
|
|
|
121
123
|
/**
|
|
122
124
|
* How a section arranges what it holds.
|
|
@@ -326,13 +328,20 @@ export const NOTE_FIELD_PRESENTATION = Object.freeze({
|
|
|
326
328
|
* `<type>.<field>` wins over the bare name, so a spelling two types use for two
|
|
327
329
|
* quantities can be said differently on each without splitting the overlay.
|
|
328
330
|
*
|
|
331
|
+
* **Keyed by the key an author writes, not by the path a declaration names.**
|
|
332
|
+
* One fact has two declarations — the note vocabulary's `weight` and an item
|
|
333
|
+
* field's `data.weight` — and the overlay says what a *reader* is shown, which
|
|
334
|
+
* is one word either way. So the container is dropped before the lookup, and
|
|
335
|
+
* `GEAR_UNITS` answers for both sides with one entry.
|
|
336
|
+
*
|
|
329
337
|
* @param {Readonly<Record<string, object>>} presentation - The overlay.
|
|
330
338
|
* @param {string|undefined} type - The note's type.
|
|
331
339
|
* @param {string} name - The field's declared name.
|
|
332
340
|
* @returns {object} The entry, or an empty one.
|
|
333
341
|
*/
|
|
334
342
|
export function overlayFor(presentation, type, name) {
|
|
335
|
-
|
|
343
|
+
const key = authoredKey(name);
|
|
344
|
+
return presentation?.[`${currentType(type)}.${key}`] ?? presentation?.[key] ?? {};
|
|
336
345
|
}
|
|
337
346
|
|
|
338
347
|
/** What a composed group's row is called. @type {Readonly<Record<string, string>>} */
|
|
@@ -418,6 +427,7 @@ export function isUnsetSentinel(value) {
|
|
|
418
427
|
* @returns {boolean} Whether to emit it.
|
|
419
428
|
*/
|
|
420
429
|
export function hasValue(value) {
|
|
430
|
+
if (isAddressTuple(value)) return true;
|
|
421
431
|
if (value == null) return false;
|
|
422
432
|
if (typeof value === "string") return value.trim() !== "" && !isUnsetSentinel(value);
|
|
423
433
|
if (Array.isArray(value)) return value.some((entry) => hasValue(entry));
|
|
@@ -454,6 +464,7 @@ export function humanizeFieldName(name) {
|
|
|
454
464
|
* @returns {string} The text.
|
|
455
465
|
*/
|
|
456
466
|
export function humanizeValue(value) {
|
|
467
|
+
if (isAddressTuple(value)) value = value.shortcode;
|
|
457
468
|
return String(value ?? "")
|
|
458
469
|
.replace(/[_-]+/g, " ")
|
|
459
470
|
.trim();
|
|
@@ -472,6 +483,7 @@ export function humanizeValue(value) {
|
|
|
472
483
|
* @returns {string} The text.
|
|
473
484
|
*/
|
|
474
485
|
export function presentValue(value) {
|
|
486
|
+
if (isAddressTuple(value)) value = value.shortcode;
|
|
475
487
|
const text = humanizeValue(value);
|
|
476
488
|
if (!/^[a-z][a-z0-9_-]*$/.test(String(value ?? ""))) return text;
|
|
477
489
|
return text.replace(/\b[a-z]/g, (letter) => letter.toUpperCase());
|
|
@@ -507,8 +519,12 @@ export function hasRenderableValue(kind, value) {
|
|
|
507
519
|
* @returns {string} One of {@link INFOBOX_VALUE_KINDS}.
|
|
508
520
|
*/
|
|
509
521
|
export function valueKindOf(field, value) {
|
|
510
|
-
if (field?.shape === "a wikilink") return "link";
|
|
511
|
-
if (
|
|
522
|
+
if (field?.kind === "address" || field?.shape === "a wikilink") return "link";
|
|
523
|
+
if (
|
|
524
|
+
(field?.kind === "list" && field?.entryKind === "address") ||
|
|
525
|
+
field?.shape === "list of wikilinks"
|
|
526
|
+
)
|
|
527
|
+
return "links";
|
|
512
528
|
if (field?.kind === "number") return "number";
|
|
513
529
|
if (field?.kind === "list") return "list";
|
|
514
530
|
if (field?.kind === "string") return "text";
|
|
@@ -814,19 +830,23 @@ export function systemRowsSection(
|
|
|
814
830
|
) {
|
|
815
831
|
const rows = [];
|
|
816
832
|
for (const field of fields ?? []) {
|
|
817
|
-
|
|
833
|
+
// Asked on the authored key: the note box records what it showed by the
|
|
834
|
+
// key a note writes, and a field whose shared source is a path into
|
|
835
|
+
// `data:` names the container as well — so comparing the declared names
|
|
836
|
+
// would put one fact on two surfaces.
|
|
837
|
+
if (!field?.name || taken.has(authoredKey(field.name))) continue;
|
|
818
838
|
const overlay = overlayFor(presentation, fm?.type, field.name);
|
|
819
839
|
if (overlay.withheld) continue;
|
|
820
840
|
const { value: raw, from } = resolveField(field, fm, { block });
|
|
821
841
|
if (from === "default" || from === "value") continue;
|
|
822
842
|
if (isDeclaredDefault(field, raw)) continue;
|
|
823
843
|
if (!hasValue(raw)) continue;
|
|
824
|
-
const declaredKind = field.
|
|
844
|
+
const declaredKind = field.code ? "link" : valueKindOf(field, raw);
|
|
825
845
|
const built = rowValue(
|
|
826
846
|
declaredKind,
|
|
827
847
|
raw,
|
|
828
848
|
resolve,
|
|
829
|
-
field.
|
|
849
|
+
field.code ? { kind: "shortcode", type: field.code, system: block } : undefined,
|
|
830
850
|
);
|
|
831
851
|
if (!hasRenderableValue(declaredKind, built)) continue;
|
|
832
852
|
const { kind, value } = applyUnit(declaredKind, built, overlay.unit);
|
package/engine/item-compiler.mjs
CHANGED
|
@@ -355,6 +355,11 @@ export class SystemItemCompiler extends BasePackCompiler {
|
|
|
355
355
|
block: system,
|
|
356
356
|
claimed: claimedPaths(itemFields(type, system)),
|
|
357
357
|
});
|
|
358
|
+
// Every field whose authored value is an Address and whose emitted value
|
|
359
|
+
// is a Shortcode, reduced once the block is whole — so a value the field
|
|
360
|
+
// read and one authored straight at the destination go through the same
|
|
361
|
+
// step.
|
|
362
|
+
this.reduceEmittedAddresses(built, itemFields(type, system));
|
|
358
363
|
this.reportUndeclaredSystemData(fm, system, "Item", subType);
|
|
359
364
|
// And what *this* pass wrote on its own initiative, which no field
|
|
360
365
|
// declaration states and so no other check can see. Read off the
|
package/engine/item-docs.mjs
CHANGED
|
@@ -43,8 +43,6 @@
|
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
45
|
import { compendiumUuid, makeId, pageUuid } from "./ids.mjs";
|
|
46
|
-
import { loadPackConfig } from "./pack-config.mjs";
|
|
47
|
-
import { currentType } from "./ids.mjs";
|
|
48
46
|
import { itemTypes } from "./item-registry.mjs";
|
|
49
47
|
import { packRouter } from "./pack-router.mjs";
|
|
50
48
|
|
|
@@ -61,50 +59,15 @@ import { packRouter } from "./pack-router.mjs";
|
|
|
61
59
|
export { itemTypes };
|
|
62
60
|
|
|
63
61
|
/**
|
|
64
|
-
*
|
|
65
|
-
* addressed by the virtual `doc<type>` qualifier.
|
|
62
|
+
* The doc-carrying types, under the names the compilers hold them by.
|
|
66
63
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* the
|
|
71
|
-
*
|
|
72
|
-
* **One set, read by the compiler and the emitter alike.** The journals pass
|
|
73
|
-
* decides what to compile from it, and the link manifest decides what to
|
|
74
|
-
* publish a `doc<type>` entry for. Held apart, the two drift into a manifest
|
|
75
|
-
* that asserts documentation nothing compiled — or a compiled entry no
|
|
76
|
-
* consumer can address. It is composed exactly once, in `defineConfig`, and
|
|
77
|
-
* read from there — never recomposed at a call site.
|
|
78
|
-
*
|
|
79
|
-
* `doc` notes are absent, for the reason that applies to them alone: a `doc`
|
|
80
|
-
* note's single document *is* its prose, so there is no second document for a
|
|
81
|
-
* `doc<type>` address to name.
|
|
82
|
-
*
|
|
83
|
-
* An accessor rather than a hoisted constant, so that importing this module
|
|
84
|
-
* needs no configuration (#2).
|
|
85
|
-
*
|
|
86
|
-
* @returns {ReadonlySet<string>} The configured doc-carrying types.
|
|
87
|
-
*/
|
|
88
|
-
export function docEntryTypes() {
|
|
89
|
-
return loadPackConfig().docEntryTypes;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Whether a content note's type is one whose prose becomes a JournalEntry of
|
|
94
|
-
* its own.
|
|
95
|
-
*
|
|
96
|
-
* @param {string} type - The note's `type` frontmatter.
|
|
97
|
-
* @returns {boolean} True for an item type, an actor type, a map type and
|
|
98
|
-
* `macro`; false for `doc`.
|
|
64
|
+
* The set is what decides whether `doc<type>` is a legal type segment, so it is
|
|
65
|
+
* stated in `engine/address.mjs` beside the grammar that reads one and read from
|
|
66
|
+
* there by the journals pass and the link manifest — one set, so an authored
|
|
67
|
+
* `doc<type>` address and the entry it names cannot disagree about which types
|
|
68
|
+
* have one.
|
|
99
69
|
*/
|
|
100
|
-
export
|
|
101
|
-
// Through {@link currentType}, because the set is derived from the item
|
|
102
|
-
// registry's keys and those are the *current* spelling of a note type. A
|
|
103
|
-
// note still on a renamed one carries its documentation journal exactly as
|
|
104
|
-
// before — this is the one lookup between an item compiling and its prose
|
|
105
|
-
// silently compiling into nothing.
|
|
106
|
-
return docEntryTypes().has(String(currentType(type)));
|
|
107
|
-
}
|
|
70
|
+
export { docEntryTypes, hasDocEntry } from "./address.mjs";
|
|
108
71
|
|
|
109
72
|
/**
|
|
110
73
|
* The id of the JournalEntry a note's prose compiles into — an item's, or a
|
package/engine/map-dot.mjs
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* @module
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
+
import { isAddressTuple, renderAddress } from "./address.mjs";
|
|
34
35
|
import { subtreeOf } from "./map-places.mjs";
|
|
35
36
|
|
|
36
37
|
/* --------------------------------------------------------------------- */
|
|
@@ -200,6 +201,7 @@ const LEGEND = Object.freeze([
|
|
|
200
201
|
* @returns {string} Quoted and escaped, newlines folded to spaces.
|
|
201
202
|
*/
|
|
202
203
|
export function dotString(value) {
|
|
204
|
+
if (isAddressTuple(value)) value = renderAddress(value);
|
|
203
205
|
return (
|
|
204
206
|
'"' +
|
|
205
207
|
String(value ?? "")
|
package/engine/map-notes.mjs
CHANGED
|
@@ -59,6 +59,11 @@ import { CURATED_REGION_EVENTS, EXCLUDED_REGION_EVENTS } from "./region-events.m
|
|
|
59
59
|
// A map's background art is `img`, as every other note type's art is. `image`,
|
|
60
60
|
// the spelling a map alone once used, is retired and gone.
|
|
61
61
|
import { sohlField } from "./frontmatter.mjs";
|
|
62
|
+
// `bgImage` is one of the four declared art slots, so its accepted set comes
|
|
63
|
+
// from there rather than being restated: the overlay, a tile's texture and an
|
|
64
|
+
// ambient sound's `audio` are map-specific address fields, not slots, and take
|
|
65
|
+
// no set beyond their own type.
|
|
66
|
+
import { artSlot } from "./art-fields.mjs";
|
|
62
67
|
|
|
63
68
|
/* -------------------------------------------------------------------- */
|
|
64
69
|
/* Note types and their canvas profiles */
|
|
@@ -908,7 +913,7 @@ export function buildScene(fm, ctx) {
|
|
|
908
913
|
// A Scene has no `img`, so a map's background is its own slot, authored
|
|
909
914
|
// under `data:` with the other art. It is an `image` address like every
|
|
910
915
|
// other art reference, and the record it resolves to carries the path.
|
|
911
|
-
const img = ctx.art(fm.data?.bgImage, "bgImage", "image");
|
|
916
|
+
const img = ctx.art(fm.data?.bgImage, "bgImage", "image", artSlot("bgImage")?.accepts);
|
|
912
917
|
if (!img) throw new Error("a map note needs a `bgImage`");
|
|
913
918
|
|
|
914
919
|
const warn = (message) => {
|
|
@@ -984,7 +989,7 @@ export function buildScene(fm, ctx) {
|
|
|
984
989
|
* `bgImage` address by {@link buildScene}. Passed rather than resolved here,
|
|
985
990
|
* because an address is answered by the compile's index and this function
|
|
986
991
|
* takes none.
|
|
987
|
-
* @param {(value: unknown, key: string, type: string) => string|null} [art] -
|
|
992
|
+
* @param {(value: unknown, key: string, type: string, accepts?: Iterable<string>) => string|null} [art] -
|
|
988
993
|
* The art resolver, for the foreground overlay. Omitted, a note naming one
|
|
989
994
|
* gets no overlay rather than a path nothing serves.
|
|
990
995
|
* @returns {object} The Level document, keyed for the pack.
|
package/engine/map-places.mjs
CHANGED
|
@@ -39,7 +39,17 @@ import { indexRecordsFor, isNoteRecord } from "./content-index.mjs";
|
|
|
39
39
|
import { noteFile } from "./index-records.mjs";
|
|
40
40
|
import { cachedIndexPath, loadForeignIndexes } from "./metadata-index.mjs";
|
|
41
41
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
42
|
-
import {
|
|
42
|
+
import { parseAddress, isAddressTuple } from "./address.mjs";
|
|
43
|
+
import { readCanonicalKey, resolvePackageUrl } from "./content-address.mjs";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The one type a `parents` entry may name — the default
|
|
47
|
+
* {@link module:engine/address.parseAddress} fills in when the segment is
|
|
48
|
+
* omitted, and the whole of what this position accepts.
|
|
49
|
+
*
|
|
50
|
+
* @type {ReadonlySet<string>}
|
|
51
|
+
*/
|
|
52
|
+
const PLACE_TYPES = Object.freeze(new Set(["place"]));
|
|
43
53
|
|
|
44
54
|
/**
|
|
45
55
|
* A place as the map reads it, from either source.
|
|
@@ -89,17 +99,36 @@ import { resolvePackageUrl } from "./content-address.mjs";
|
|
|
89
99
|
*/
|
|
90
100
|
|
|
91
101
|
/**
|
|
92
|
-
* The shortcode a `parents` entry names, lower case
|
|
93
|
-
*
|
|
102
|
+
* The shortcode a `parents` entry names, lower case.
|
|
103
|
+
*
|
|
104
|
+
* **A `parents` entry is an Address** — the specification types it
|
|
105
|
+
* `Address[]`, and the description ("enclosing places") is what makes `place`
|
|
106
|
+
* its default and its only accepted type. A qualified form is read
|
|
107
|
+
* structurally by {@link readCanonicalKey} for its literal four segments,
|
|
108
|
+
* since this position asks neither its package nor its system, only the
|
|
109
|
+
* shortcode {@link module:engine/address} would resolve it to anyway.
|
|
110
|
+
*
|
|
111
|
+
* **The match this shortcode feeds is deliberately package-blind.**
|
|
112
|
+
* `world.places` merges this package's own places with every fetched
|
|
113
|
+
* dependency's into one shortcode space, the way a dependency's place
|
|
114
|
+
* attaches to the local containment tree it is drawn beside — see
|
|
115
|
+
* `tests/map.test.ts`'s _loads a dependency's places beside the package's
|
|
116
|
+
* own_, where a fetched place's bare `parents: ["world"]` reaches this
|
|
117
|
+
* package's own root. Comparing the full Address instead would ask the
|
|
118
|
+
* dependency's own package to agree with this one's, which the feature is
|
|
119
|
+
* built to cross.
|
|
94
120
|
*
|
|
95
121
|
* @param {unknown} parent - One `parents` entry.
|
|
96
|
-
* @returns {string} Its shortcode.
|
|
122
|
+
* @returns {string} Its shortcode, or `""` for an entry that names nothing.
|
|
97
123
|
*/
|
|
98
124
|
function parentShortcode(parent) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
125
|
+
if (isAddressTuple(parent)) return parent.shortcode.toLowerCase();
|
|
126
|
+
const text = String(parent ?? "").trim();
|
|
127
|
+
if (!text) return "";
|
|
128
|
+
const qualified = readCanonicalKey(text);
|
|
129
|
+
if (qualified) return qualified.shortcode.toLowerCase();
|
|
130
|
+
const read = parseAddress(text, { type: "place", types: PLACE_TYPES });
|
|
131
|
+
return read.reason ? "" : read.shortcode.toLowerCase();
|
|
103
132
|
}
|
|
104
133
|
|
|
105
134
|
/**
|
|
@@ -127,7 +156,10 @@ function relationList(value) {
|
|
|
127
156
|
if (!Array.isArray(value)) return [];
|
|
128
157
|
return value
|
|
129
158
|
.filter((e) => e && typeof e === "object" && !Array.isArray(e))
|
|
130
|
-
.map((e) => ({
|
|
159
|
+
.map((e) => ({
|
|
160
|
+
...e,
|
|
161
|
+
to: isAddressTuple(e.to) ? e.to.shortcode : String(e.to ?? "").toLowerCase(),
|
|
162
|
+
}));
|
|
131
163
|
}
|
|
132
164
|
|
|
133
165
|
/**
|
|
@@ -209,7 +241,7 @@ export function placesFromRecords(records, { contentBase, base }) {
|
|
|
209
241
|
function addForeignPlaces(world, foreignIndex) {
|
|
210
242
|
for (const [canonical, entry] of foreignIndex) {
|
|
211
243
|
if (entry?.type !== "place") continue;
|
|
212
|
-
const shortcode = canonical
|
|
244
|
+
const shortcode = readCanonicalKey(canonical)?.shortcode?.toLowerCase() ?? "";
|
|
213
245
|
if (!shortcode || world.places.has(shortcode)) continue;
|
|
214
246
|
world.places.set(shortcode, {
|
|
215
247
|
shortcode,
|
|
@@ -39,6 +39,11 @@
|
|
|
39
39
|
* @module
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
+
import { buildReferenceTargets } from "./reference-targets.mjs";
|
|
43
|
+
|
|
44
|
+
import { completeAddress, renderAddress } from "./address.mjs";
|
|
45
|
+
|
|
46
|
+
import { decodeIndexAddresses, noteAddressContext, encodeAddresses } from "./note-addresses.mjs";
|
|
42
47
|
import fs from "node:fs";
|
|
43
48
|
import path from "node:path";
|
|
44
49
|
|
|
@@ -330,6 +335,7 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
330
335
|
const index = new Map();
|
|
331
336
|
const packages = new Set();
|
|
332
337
|
const stale = [];
|
|
338
|
+
const referenceRecords = [];
|
|
333
339
|
|
|
334
340
|
/**
|
|
335
341
|
* Fold one package's records into the index.
|
|
@@ -338,7 +344,7 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
338
344
|
* @param {Array<Record<string, any>>} records - Its index records.
|
|
339
345
|
* @returns {void}
|
|
340
346
|
*/
|
|
341
|
-
const ingest = (pkg, records) => {
|
|
347
|
+
const ingest = (pkg, records, sourceFile) => {
|
|
342
348
|
// A base is only needed to resolve a page *URL*, so a pack-only
|
|
343
349
|
// dependency — Foundry addresses and no site, which `kethira` is by
|
|
344
350
|
// licensing rather than by accident — needs none. Demanding one would
|
|
@@ -351,8 +357,20 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
351
357
|
const base = bases?.[pkg];
|
|
352
358
|
const web = typeof base === "string" && base.length > 0;
|
|
353
359
|
|
|
354
|
-
|
|
355
|
-
|
|
360
|
+
const pageAliases = [];
|
|
361
|
+
for (const [recordIndex, record] of records.entries()) {
|
|
362
|
+
try {
|
|
363
|
+
decodeIndexAddresses(record, noteAddressContext(config));
|
|
364
|
+
} catch (error) {
|
|
365
|
+
error.file = sourceFile;
|
|
366
|
+
const lines = sourceFile ? fs.readFileSync(sourceFile, "utf8").split("\n") : [];
|
|
367
|
+
let nonempty = -1;
|
|
368
|
+
const line = lines.findIndex((text) => text.trim() && ++nonempty === recordIndex);
|
|
369
|
+
error.position = line >= 0 ? { line: line + 1 } : {};
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
referenceRecords.push(record);
|
|
373
|
+
const key = encodeAddresses(record?.address?.canonical);
|
|
356
374
|
if (!key) continue;
|
|
357
375
|
const parts = readCanonicalKey(key);
|
|
358
376
|
if (!parts) continue;
|
|
@@ -398,7 +416,44 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
398
416
|
domains: record.data?.domains ?? undefined,
|
|
399
417
|
package: pkg,
|
|
400
418
|
});
|
|
419
|
+
const entry = index.get(key);
|
|
420
|
+
for (const [system, published] of Object.entries(record.foundry ?? {})) {
|
|
421
|
+
const identity = completeAddress({
|
|
422
|
+
package: record.package,
|
|
423
|
+
system,
|
|
424
|
+
type: record.type,
|
|
425
|
+
shortcode: record.shortcode,
|
|
426
|
+
});
|
|
427
|
+
const publishedKey = renderAddress(identity);
|
|
428
|
+
if (!index.has(publishedKey))
|
|
429
|
+
index.set(publishedKey, {
|
|
430
|
+
...entry,
|
|
431
|
+
uuid: published.uuid,
|
|
432
|
+
anchors: published.anchors,
|
|
433
|
+
type: identity.type,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
if (entry.url) {
|
|
437
|
+
const identity = completeAddress({
|
|
438
|
+
package: record.package,
|
|
439
|
+
system: "none",
|
|
440
|
+
type: record.type,
|
|
441
|
+
shortcode: record.shortcode,
|
|
442
|
+
});
|
|
443
|
+
if (identity.type !== record.type)
|
|
444
|
+
pageAliases.push([
|
|
445
|
+
renderAddress(identity),
|
|
446
|
+
{
|
|
447
|
+
...entry,
|
|
448
|
+
uuid: undefined,
|
|
449
|
+
anchors: undefined,
|
|
450
|
+
doc: undefined,
|
|
451
|
+
type: identity.type,
|
|
452
|
+
},
|
|
453
|
+
]);
|
|
454
|
+
}
|
|
401
455
|
}
|
|
456
|
+
for (const [key, entry] of pageAliases) if (!index.has(key)) index.set(key, entry);
|
|
402
457
|
packages.add(pkg);
|
|
403
458
|
};
|
|
404
459
|
|
|
@@ -424,10 +479,10 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
424
479
|
}
|
|
425
480
|
const pkg = records[0]?.package ?? packageOfCache(file);
|
|
426
481
|
if (local.has(pkg)) continue;
|
|
427
|
-
ingest(pkg, records);
|
|
482
|
+
ingest(pkg, records, file);
|
|
428
483
|
}
|
|
429
484
|
|
|
430
|
-
return { index, packages, stale };
|
|
485
|
+
return { index, references: buildReferenceTargets(referenceRecords, index), packages, stale };
|
|
431
486
|
}
|
|
432
487
|
|
|
433
488
|
/**
|