@heroiclands/package-build 8.1.0 → 10.0.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 +748 -0
- package/CONTENT.md +273 -13
- package/bin/content-build.mjs +479 -123
- package/bin/package-build.mjs +27 -69
- package/bin/report.mjs +1 -2
- package/bundle.mjs +2 -10
- package/config.mjs +31 -106
- package/container.mjs +13 -57
- package/content-config.mjs +300 -188
- package/coverage.mjs +14 -55
- package/deploy.mjs +4 -13
- package/docs/content-format.md +1418 -0
- package/e2e.mjs +16 -55
- package/engine/address-charset.mjs +62 -0
- package/engine/address-diff.mjs +1 -4
- package/engine/alias-index.mjs +153 -0
- package/engine/base-compiler.mjs +203 -31
- package/engine/code-fences.mjs +4 -13
- package/engine/compendiums.mjs +13 -37
- package/engine/content-address.mjs +6 -10
- package/engine/content-format-check.mjs +570 -0
- package/engine/content-format.mjs +253 -0
- package/engine/content-links.mjs +144 -99
- package/engine/content-lint.mjs +12 -16
- package/engine/content-slug.mjs +2 -6
- package/engine/content-tables.mjs +26 -79
- package/engine/diagnostics.mjs +37 -15
- package/engine/document-subtypes.mjs +440 -0
- package/engine/field-reference.mjs +6 -20
- package/engine/field-spec.mjs +49 -45
- package/engine/foreign-catalog.mjs +7 -22
- package/engine/foreign-manifests.mjs +1 -4
- package/engine/frontmatter-lint.mjs +347 -43
- package/engine/frontmatter.mjs +3 -8
- package/engine/generate.mjs +36 -20
- package/engine/helpers.mjs +54 -81
- package/engine/homepage.mjs +4 -15
- package/engine/ids.mjs +21 -12
- package/engine/index.mjs +15 -0
- package/engine/item-registry.mjs +72 -9
- package/engine/journals.mjs +4 -14
- package/engine/kb-manifest.mjs +41 -24
- package/engine/macros.mjs +2 -10
- package/engine/manifest-emit.mjs +6 -17
- package/engine/map-notes.mjs +53 -87
- package/engine/note-claims.mjs +383 -0
- package/engine/note-package.mjs +1 -4
- package/engine/note-vocabulary.mjs +678 -0
- package/engine/pack-config.mjs +56 -60
- package/engine/pack-router.mjs +18 -8
- package/engine/prose-config.mjs +20 -4
- package/engine/prose-lint.mjs +61 -17
- package/engine/region-events.mjs +1 -3
- package/engine/retired-fields.mjs +117 -3
- package/engine/scene-levels.mjs +8 -22
- package/engine/scenes.mjs +31 -47
- package/engine/schema-check.mjs +348 -7
- package/engine/schema-extract.mjs +11 -39
- package/engine/site-build.mjs +13 -38
- package/engine/site-index.mjs +40 -35
- package/engine/system-block.mjs +513 -0
- package/engine/web-wikilinks.mjs +115 -92
- package/engine/wikilink-syntax.mjs +30 -0
- package/engine/wikilinks.mjs +147 -183
- package/index.mjs +1 -5
- package/lang.mjs +1 -3
- package/manifest.mjs +10 -37
- package/markdownlint-config.mjs +1 -5
- package/package.json +6 -2
- package/sohl/actors.mjs +251 -68
- package/sohl/being-info.mjs +3 -6
- package/sohl/document-subtypes.mjs +82 -0
- package/sohl/index.mjs +4 -6
- package/sohl/item-builders.mjs +1 -3
- package/sohl/item-fields.mjs +16 -34
- package/sohl/items.mjs +111 -17
- package/sohl/kb-passes.mjs +29 -39
- package/sohl/note-schemas.mjs +11 -7
- package/sohl/skill-base.mjs +7 -23
- package/stage.mjs +3 -13
- package/templates.mjs +4 -15
- package/types/bundle.d.mts +1 -1
- package/types/container.d.mts +2 -2
- package/types/content-config.d.mts +48 -4
- package/types/coverage.d.mts +1 -1
- package/types/e2e.d.mts +4 -4
- package/types/engine/address-charset.d.mts +45 -0
- package/types/engine/alias-index.d.mts +122 -0
- package/types/engine/base-compiler.d.mts +132 -4
- package/types/engine/content-address.d.mts +2 -2
- package/types/engine/content-format-check.d.mts +163 -0
- package/types/engine/content-format.d.mts +101 -0
- package/types/engine/content-links.d.mts +16 -1
- package/types/engine/content-lint.d.mts +6 -0
- package/types/engine/diagnostics.d.mts +29 -0
- package/types/engine/document-subtypes.d.mts +233 -0
- package/types/engine/field-spec.d.mts +76 -23
- package/types/engine/frontmatter-lint.d.mts +47 -2
- package/types/engine/generate.d.mts +15 -2
- package/types/engine/helpers.d.mts +22 -14
- package/types/engine/ids.d.mts +10 -0
- package/types/engine/index.d.mts +5 -0
- package/types/engine/item-registry.d.mts +21 -2
- package/types/engine/kb-manifest.d.mts +35 -8
- package/types/engine/map-notes.d.mts +21 -11
- package/types/engine/note-claims.d.mts +113 -0
- package/types/engine/note-vocabulary.d.mts +251 -0
- package/types/engine/pack-config.d.mts +4 -3
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/prose-lint.d.mts +6 -2
- package/types/engine/retired-fields.d.mts +73 -2
- package/types/engine/schema-check.d.mts +182 -0
- package/types/engine/schema-extract.d.mts +1 -1
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/system-block.d.mts +281 -0
- package/types/engine/web-wikilinks.d.mts +23 -12
- package/types/engine/wikilink-syntax.d.mts +29 -0
- package/types/manifest.d.mts +1 -1
- package/types/sohl/actors.d.mts +62 -6
- package/types/sohl/document-subtypes.d.mts +14 -0
- package/types/sohl/index.d.mts +1 -0
- package/types/sohl/items.d.mts +21 -0
package/e2e.mjs
CHANGED
|
@@ -93,9 +93,7 @@ const EXACT_BUILD = /^\d+\.\d+$/;
|
|
|
93
93
|
* @returns {string} The hex hash.
|
|
94
94
|
*/
|
|
95
95
|
export function hashPassword(password, salt) {
|
|
96
|
-
return crypto
|
|
97
|
-
.pbkdf2Sync(password, salt, 1000, 64, "sha512")
|
|
98
|
-
.toString("hex");
|
|
96
|
+
return crypto.pbkdf2Sync(password, salt, 1000, 64, "sha512").toString("hex");
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
/**
|
|
@@ -127,23 +125,13 @@ export function resolveE2EWorld(config, env = process.env) {
|
|
|
127
125
|
const world = config.e2eWorld ?? {};
|
|
128
126
|
const gm = config.e2eGm ?? {};
|
|
129
127
|
return {
|
|
130
|
-
worldId:
|
|
131
|
-
|
|
132
|
-
world.id ||
|
|
133
|
-
`${config.packageId}-e2e`,
|
|
134
|
-
worldTitle:
|
|
135
|
-
env[`${prefix}WORLD_TITLE`]?.trim() ||
|
|
136
|
-
world.title ||
|
|
137
|
-
`${config.packageId} E2E`,
|
|
128
|
+
worldId: env[`${prefix}WORLD_ID`]?.trim() || world.id || `${config.packageId}-e2e`,
|
|
129
|
+
worldTitle: env[`${prefix}WORLD_TITLE`]?.trim() || world.title || `${config.packageId} E2E`,
|
|
138
130
|
worldDescription:
|
|
139
|
-
world.description ||
|
|
140
|
-
`Disposable world for ${config.packageId} end-to-end tests.`,
|
|
131
|
+
world.description || `Disposable world for ${config.packageId} end-to-end tests.`,
|
|
141
132
|
gmId: E2E_GM_ID,
|
|
142
133
|
gmName: env[`${prefix}GM_NAME`]?.trim() || gm.name || "Gamemaster",
|
|
143
|
-
gmPassword:
|
|
144
|
-
env[`${prefix}GM_PASSWORD`]?.trim() ||
|
|
145
|
-
gm.password ||
|
|
146
|
-
`${config.packageId}-e2e`,
|
|
134
|
+
gmPassword: env[`${prefix}GM_PASSWORD`]?.trim() || gm.password || `${config.packageId}-e2e`,
|
|
147
135
|
};
|
|
148
136
|
}
|
|
149
137
|
|
|
@@ -330,8 +318,7 @@ export function parseFastArgs(argv, build) {
|
|
|
330
318
|
|
|
331
319
|
for (let i = 0; i < own.length; i += 1) {
|
|
332
320
|
const arg = /** @type {string} */ (own[i]);
|
|
333
|
-
if (arg.startsWith("--build="))
|
|
334
|
-
requested = arg.slice("--build=".length);
|
|
321
|
+
if (arg.startsWith("--build=")) requested = arg.slice("--build=".length);
|
|
335
322
|
else if (arg === "--recreate") recreate = true;
|
|
336
323
|
else if (arg === "--no-run") runSuite = false;
|
|
337
324
|
else if (arg === "--spec") {
|
|
@@ -387,12 +374,7 @@ export function parseFastArgs(argv, build) {
|
|
|
387
374
|
* @param {(message: string) => void} [opts.log] - Progress reporting.
|
|
388
375
|
* @returns {Promise<{worldDir: string, world: E2EWorld}>} Where it landed.
|
|
389
376
|
*/
|
|
390
|
-
export async function seedTestWorld({
|
|
391
|
-
config,
|
|
392
|
-
packageJson,
|
|
393
|
-
env = process.env,
|
|
394
|
-
log = () => {},
|
|
395
|
-
}) {
|
|
377
|
+
export async function seedTestWorld({ config, packageJson, env = process.env, log = () => {} }) {
|
|
396
378
|
const stage = config.e2eStage;
|
|
397
379
|
const dataRoot = requireIsolatedDataRoot(stage, env);
|
|
398
380
|
const world = resolveE2EWorld(config, env);
|
|
@@ -530,13 +512,7 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
530
512
|
* @returns {Promise<void>} Resolves once the world is active.
|
|
531
513
|
* @throws {Error} On a licence failure or a timeout.
|
|
532
514
|
*/
|
|
533
|
-
export async function waitForWorld({
|
|
534
|
-
url,
|
|
535
|
-
container,
|
|
536
|
-
stage,
|
|
537
|
-
timeoutMs = 180_000,
|
|
538
|
-
log = () => {},
|
|
539
|
-
}) {
|
|
515
|
+
export async function waitForWorld({ url, container, stage, timeoutMs = 180_000, log = () => {} }) {
|
|
540
516
|
const join = `${url}/join`;
|
|
541
517
|
const deadline = Date.now() + timeoutMs;
|
|
542
518
|
log(`Waiting for the world at ${join} …`);
|
|
@@ -597,27 +573,17 @@ function captureContainerLog(container) {
|
|
|
597
573
|
* @param {(message: string) => void} [opts.log] - Progress reporting.
|
|
598
574
|
* @returns {number} The suite's exit status.
|
|
599
575
|
*/
|
|
600
|
-
export function runSuite({
|
|
601
|
-
command,
|
|
602
|
-
args = [],
|
|
603
|
-
cwd,
|
|
604
|
-
env = process.env,
|
|
605
|
-
log = () => {},
|
|
606
|
-
}) {
|
|
576
|
+
export function runSuite({ command, args = [], cwd, env = process.env, log = () => {} }) {
|
|
607
577
|
const [program, ...rest] = command;
|
|
608
578
|
const childEnv = { ...env };
|
|
609
579
|
delete childEnv.ELECTRON_RUN_AS_NODE;
|
|
610
580
|
log(`▸ ${[...command, ...args].join(" ")}`);
|
|
611
|
-
const result = spawnSync(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
env: childEnv,
|
|
618
|
-
shell: process.platform === "win32",
|
|
619
|
-
},
|
|
620
|
-
);
|
|
581
|
+
const result = spawnSync(/** @type {string} */ (program), [...rest, ...args], {
|
|
582
|
+
stdio: "inherit",
|
|
583
|
+
cwd,
|
|
584
|
+
env: childEnv,
|
|
585
|
+
shell: process.platform === "win32",
|
|
586
|
+
});
|
|
621
587
|
if (result.error) throw result.error;
|
|
622
588
|
return result.status ?? 1;
|
|
623
589
|
}
|
|
@@ -756,12 +722,7 @@ export async function e2eRun({
|
|
|
756
722
|
* @param {(message: string) => void} [opts.log] - Progress reporting.
|
|
757
723
|
* @returns {Promise<number>} The suite's exit status.
|
|
758
724
|
*/
|
|
759
|
-
export async function e2eFast({
|
|
760
|
-
config,
|
|
761
|
-
argv = [],
|
|
762
|
-
env = process.env,
|
|
763
|
-
log = () => {},
|
|
764
|
-
}) {
|
|
725
|
+
export async function e2eFast({ config, argv = [], env = process.env, log = () => {} }) {
|
|
765
726
|
const {
|
|
766
727
|
targets,
|
|
767
728
|
recreate,
|
|
@@ -0,0 +1,62 @@
|
|
|
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 one charset every segment of a canonical address is held to (#59).
|
|
16
|
+
*
|
|
17
|
+
* An address is a hyphen-joined tuple — `sohl-skill-clmb` — and it is read back
|
|
18
|
+
* by **counting segments**, with a fixed meaning per position. That is sound
|
|
19
|
+
* for exactly one reason: the hyphen is *purely* a separator, because no
|
|
20
|
+
* segment may contain one. Take that away and reading an address needs a
|
|
21
|
+
* vocabulary to match against, a longest-match rule, and an answer for every
|
|
22
|
+
* name that is a prefix of another — none of which exist.
|
|
23
|
+
*
|
|
24
|
+
* So the charset is not a tidiness rule. It is the premise the address grammar
|
|
25
|
+
* rests on, and the issue's word for how it should be held is **enforced rather
|
|
26
|
+
* than assumed**: a value that breaks it is refused where it is written, not
|
|
27
|
+
* discovered later as addresses that fail to parse and report nothing about
|
|
28
|
+
* why. `harn-adventures` was that case — its keys read as four segments and
|
|
29
|
+
* failed as a `null` return.
|
|
30
|
+
*
|
|
31
|
+
* This module is a **leaf with no local imports**, so the validator a
|
|
32
|
+
* consumer's `package-build.config.mjs` reaches (`content-config.mjs`) can name
|
|
33
|
+
* it without closing a cycle around that file.
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The shape every address segment must match: ASCII letters and digits only.
|
|
40
|
+
*
|
|
41
|
+
* Case is deliberately **not** constrained. Hundreds of authored shortcodes are
|
|
42
|
+
* mixed-case and collide with nothing, and case has no bearing on the
|
|
43
|
+
* separator, which is the whole of what this pattern is protecting.
|
|
44
|
+
*
|
|
45
|
+
* @type {RegExp}
|
|
46
|
+
*/
|
|
47
|
+
export const ADDRESS_SEGMENT_PATTERN = /^[A-Za-z0-9]+$/;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether a value is a well-formed address segment.
|
|
51
|
+
*
|
|
52
|
+
* A blank value is **not** valid: this answers "is this an acceptable segment",
|
|
53
|
+
* never "is a segment present". Presence is a separate question, asked
|
|
54
|
+
* wherever the value is required, and conflating the two would report a missing
|
|
55
|
+
* key as a charset violation.
|
|
56
|
+
*
|
|
57
|
+
* @param {unknown} value - The candidate segment.
|
|
58
|
+
* @returns {boolean} `true` when it matches {@link ADDRESS_SEGMENT_PATTERN}.
|
|
59
|
+
*/
|
|
60
|
+
export function isAddressSegment(value) {
|
|
61
|
+
return typeof value === "string" && ADDRESS_SEGMENT_PATTERN.test(value);
|
|
62
|
+
}
|
package/engine/address-diff.mjs
CHANGED
|
@@ -198,10 +198,7 @@ export function diffItemAddresses(baseline, current, { baseline: label }) {
|
|
|
198
198
|
export function noteFilesById(contentBase, { skipDirectories } = {}) {
|
|
199
199
|
const byId = new Map();
|
|
200
200
|
const walkOpts = skipDirectories ? { skipDirectories } : undefined;
|
|
201
|
-
for (const { frontmatter: fm, absPath } of walkMarkdownTree(
|
|
202
|
-
contentBase,
|
|
203
|
-
walkOpts,
|
|
204
|
-
)) {
|
|
201
|
+
for (const { frontmatter: fm, absPath } of walkMarkdownTree(contentBase, walkOpts)) {
|
|
205
202
|
if (fm?.id && !byId.has(fm.id)) byId.set(fm.id, absPath);
|
|
206
203
|
}
|
|
207
204
|
return byId;
|
|
@@ -0,0 +1,153 @@
|
|
|
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 **alias** namespace: what a note can be called, and who may claim a name.
|
|
16
|
+
*
|
|
17
|
+
* A wikilink resolves through one of two namespaces, and the pipe chooses
|
|
18
|
+
* which (#131): `[[x|…]]` is an *address*, parsed by the address grammar;
|
|
19
|
+
* `[[x]]` is an *alias*, looked up here. This module owns the second half —
|
|
20
|
+
* what goes into the index, how a key is spelled, and what happens when two
|
|
21
|
+
* notes claim one name.
|
|
22
|
+
*
|
|
23
|
+
* **An alias is scoped to the claiming note's own type.** The key is
|
|
24
|
+
* `(type, alias)`, so `Shock` may be a `skill` in one place and a `trauma` in
|
|
25
|
+
* another without the two ever meeting. A link resolves against the *source*
|
|
26
|
+
* note's type, which is why a bare name reaches a sibling and never a
|
|
27
|
+
* cross-type target — that one is written as an address.
|
|
28
|
+
*
|
|
29
|
+
* **Three sources, all authored.** `aliases`, `name.aliases`, and `name.full`.
|
|
30
|
+
* Each is something a person wrote down as a name for the note, which is
|
|
31
|
+
* exactly what a bare `[[…]]` cites.
|
|
32
|
+
*
|
|
33
|
+
* **The filename is deliberately not one of them**, and it used to be — every
|
|
34
|
+
* one of the three copies of this index added `basename(file, ".md")` with
|
|
35
|
+
* underscores turned to spaces. That admitted keys no author could ever cite
|
|
36
|
+
* and no author had ever written:
|
|
37
|
+
*
|
|
38
|
+
* - `_Introduction.md` yields the alias `" introduction"`, *with a leading
|
|
39
|
+
* space*. A wikilink target is trimmed, so nothing can ever match it. In one
|
|
40
|
+
* repository thirteen notes — one per documentation section — claimed that
|
|
41
|
+
* key, making it the largest alias collision in the corpus and every one of
|
|
42
|
+
* its claimants blameless.
|
|
43
|
+
* - `README.md` yields `readme`, claimed once per section for the same reason.
|
|
44
|
+
*
|
|
45
|
+
* Since a collision is now a build failure rather than a silent deletion, an
|
|
46
|
+
* index entry that cannot be cited can only ever *cause* one. Removing the
|
|
47
|
+
* source was measured first, across all five content trees: not one link that
|
|
48
|
+
* resolves today resolves through the filename alone, so nothing loses a
|
|
49
|
+
* target — while the collision count falls without a note being edited.
|
|
50
|
+
*
|
|
51
|
+
* @module
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Every alias a note claims, in the order the sources are consulted.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} fm - Parsed frontmatter.
|
|
58
|
+
* @returns {string[]} The claimed aliases, each a non-empty string.
|
|
59
|
+
*/
|
|
60
|
+
export function aliasesOf(fm) {
|
|
61
|
+
return [
|
|
62
|
+
...(Array.isArray(fm?.aliases) ? fm.aliases : []),
|
|
63
|
+
...(Array.isArray(fm?.name?.aliases) ? fm.name.aliases : []),
|
|
64
|
+
fm?.name?.full,
|
|
65
|
+
].filter((a) => typeof a === "string" && a);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The index key one note's claim on one alias is filed under.
|
|
70
|
+
*
|
|
71
|
+
* Stated here so the three indexes — the pack build's, the site build's and
|
|
72
|
+
* the link checker's — cannot spell it differently. All three already used
|
|
73
|
+
* `type|alias`, lowercased; the risk was never that they disagreed today.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} type - The claiming note's content type.
|
|
76
|
+
* @param {string} alias - The alias, as authored.
|
|
77
|
+
* @returns {string} The key.
|
|
78
|
+
*/
|
|
79
|
+
export function aliasKey(type, alias) {
|
|
80
|
+
return `${String(type).trim()}|${String(alias).trim()}`.toLowerCase();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* One alias claimed by more than one note of a single type.
|
|
85
|
+
*
|
|
86
|
+
* @typedef {object} AliasCollision
|
|
87
|
+
* @property {string} key - The index key, `type|alias`.
|
|
88
|
+
* @property {string} type - The type both claimants share.
|
|
89
|
+
* @property {string} alias - The alias, as the first claimant wrote it.
|
|
90
|
+
* @property {unknown[]} claimants - Every note claiming it, in walk order.
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build the type-scoped alias index, and report every collision in it.
|
|
95
|
+
*
|
|
96
|
+
* **A collision resolves to nothing, and is reported naming every claimant.**
|
|
97
|
+
* Both halves matter. Resolving to whichever note happened to be walked first
|
|
98
|
+
* makes a link silently point at the wrong document, and which one it is
|
|
99
|
+
* depends on directory order. Reporting it at the *citing* note blames a file
|
|
100
|
+
* whose author did nothing wrong — whoever added the second claimant broke
|
|
101
|
+
* every existing citation (#13) — so the claimants are kept rather than
|
|
102
|
+
* discarded along with the entry.
|
|
103
|
+
*
|
|
104
|
+
* @template T
|
|
105
|
+
* @param {Iterable<{type: string, aliases: Iterable<string>, value: T}>} entries
|
|
106
|
+
* One per note: the type that scopes its claims, the aliases it claims, and
|
|
107
|
+
* whatever the caller wants an alias to resolve to.
|
|
108
|
+
* @param {object} [opts]
|
|
109
|
+
* @param {(a: T, b: T) => boolean} [opts.same] - Whether two values are the
|
|
110
|
+
* same note. Defaults to identity; a caller whose values are freshly built
|
|
111
|
+
* records supplies its own.
|
|
112
|
+
* @returns {{byKey: Map<string, T>, claims: Map<string, T[]>,
|
|
113
|
+
* collisions: AliasCollision[]}} `byKey` omits every colliding key, so a
|
|
114
|
+
* lookup in it can never resolve an ambiguous alias.
|
|
115
|
+
*/
|
|
116
|
+
export function indexAliases(entries, { same = Object.is } = {}) {
|
|
117
|
+
const claims = new Map();
|
|
118
|
+
/** The alias as first written, per key, for a message that reads. */
|
|
119
|
+
const written = new Map();
|
|
120
|
+
const typeOf = new Map();
|
|
121
|
+
|
|
122
|
+
for (const { type, aliases, value } of entries) {
|
|
123
|
+
for (const alias of aliases ?? []) {
|
|
124
|
+
if (typeof alias !== "string" || !alias) continue;
|
|
125
|
+
const key = aliasKey(type, alias);
|
|
126
|
+
const claimants = claims.get(key);
|
|
127
|
+
if (!claimants) {
|
|
128
|
+
claims.set(key, [value]);
|
|
129
|
+
written.set(key, alias);
|
|
130
|
+
typeOf.set(key, String(type).toLowerCase());
|
|
131
|
+
} else if (!claimants.some((c) => same(c, value))) {
|
|
132
|
+
claimants.push(value);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const byKey = new Map();
|
|
138
|
+
const collisions = [];
|
|
139
|
+
for (const [key, claimants] of claims) {
|
|
140
|
+
if (claimants.length === 1) {
|
|
141
|
+
byKey.set(key, claimants[0]);
|
|
142
|
+
} else {
|
|
143
|
+
collisions.push({
|
|
144
|
+
key,
|
|
145
|
+
type: typeOf.get(key),
|
|
146
|
+
alias: written.get(key),
|
|
147
|
+
claimants,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { byKey, claims, collisions };
|
|
153
|
+
}
|