@heroiclands/package-build 10.0.1 → 11.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 +279 -0
- package/CONTENT.md +218 -71
- package/MIGRATING.md +64 -0
- package/bin/content-build.mjs +59 -75
- package/docs/content-format.md +90 -67
- package/engine/base-compiler.mjs +7 -1
- package/engine/content-address.mjs +71 -18
- package/engine/content-format-check.mjs +1 -1
- package/engine/content-links.mjs +93 -112
- package/engine/content-lint.mjs +14 -10
- package/engine/content-slug.mjs +39 -105
- package/engine/diagnostics.mjs +16 -2
- package/engine/frontmatter-lint.mjs +26 -13
- package/engine/helpers.mjs +31 -68
- package/engine/homepage.mjs +131 -86
- package/engine/index.mjs +2 -5
- package/engine/manifest-emit.mjs +23 -4
- package/engine/note-vocabulary.mjs +58 -1
- package/engine/retired-fields.mjs +117 -6
- package/engine/site-build.mjs +182 -59
- package/engine/site-index.mjs +57 -102
- package/engine/web-wikilinks.mjs +183 -127
- package/engine/wikilink-syntax.mjs +174 -34
- package/engine/wikilinks.mjs +159 -117
- package/package.json +1 -1
- package/types/engine/base-compiler.d.mts +1 -1
- package/types/engine/content-address.d.mts +46 -14
- package/types/engine/content-links.d.mts +13 -17
- package/types/engine/content-slug.d.mts +11 -48
- package/types/engine/diagnostics.d.mts +14 -1
- package/types/engine/helpers.d.mts +4 -3
- package/types/engine/homepage.d.mts +96 -60
- package/types/engine/index.d.mts +0 -1
- package/types/engine/note-vocabulary.d.mts +43 -0
- package/types/engine/retired-fields.d.mts +78 -1
- package/types/engine/site-build.d.mts +70 -17
- package/types/engine/site-index.d.mts +19 -21
- package/types/engine/web-wikilinks.d.mts +29 -28
- package/types/engine/wikilink-syntax.d.mts +126 -40
- package/types/engine/wikilinks.d.mts +29 -24
- package/engine/abbreviations.mjs +0 -0
- package/engine/alias-index.mjs +0 -153
- package/types/engine/abbreviations.d.mts +0 -44
- package/types/engine/alias-index.d.mts +0 -122
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
export function resolveItemDocType(qualifier: string, types: Set<string>): string | null;
|
|
27
27
|
/**
|
|
28
28
|
* Read a link target as a **qualified** `type-shortcode` reference, or report
|
|
29
|
-
* that it
|
|
29
|
+
* that it does not parse as one.
|
|
30
30
|
*
|
|
31
31
|
* Two separators are accepted, and they are **not** interchangeable in how
|
|
32
32
|
* confidently they mark a target as qualified:
|
|
@@ -35,19 +35,20 @@ export function resolveItemDocType(qualifier: string, types: Set<string>): strin
|
|
|
35
35
|
* a wikilink as a *path* and resolves it against the vault's folders, so a
|
|
36
36
|
* slash-qualified link is a broken link in the editor where the content is now
|
|
37
37
|
* authored. A hyphen qualifies **only when what precedes it is a known type**:
|
|
38
|
-
* note names contain hyphens too (`Grukar-ahk`), and
|
|
39
|
-
* as
|
|
40
|
-
*
|
|
38
|
+
* note names contain hyphens too (`Grukar-ahk`), and a target that is one is
|
|
39
|
+
* reported as not an address rather than split at an arbitrary place. The
|
|
40
|
+
* split is at the **first** hyphen, so a shortcode may itself contain one
|
|
41
|
+
* (`trauma-self-pro` → `trauma` + `self-pro`).
|
|
41
42
|
* - **`type/shortcode`** — the legacy form, still resolved so that a link
|
|
42
43
|
* written before the vault migrated does not silently die. A slash is
|
|
43
44
|
* *unconditionally* a qualifier: nothing else uses one, so an unknown type
|
|
44
|
-
* before it is
|
|
45
|
-
*
|
|
45
|
+
* before it is reported rather than guessed at. The split is at the **last**
|
|
46
|
+
* slash, as it always was.
|
|
46
47
|
*
|
|
47
48
|
* A leading **package** segment is optional and outermost: `sohl-skill-lang` is
|
|
48
49
|
* `skill-lang` in the `sohl` package. It is read only when `packages` is given
|
|
49
50
|
* and names the segment, and only when the remainder is itself a valid address,
|
|
50
|
-
* so a note called "Grukar-ahk"
|
|
51
|
+
* so a note called "Grukar-ahk" is not mistaken for one (#1499).
|
|
51
52
|
*
|
|
52
53
|
* @param {string} target - The link target, anchor already removed.
|
|
53
54
|
* @param {Set<string>} types - Every type the content tree contains.
|
|
@@ -56,7 +57,7 @@ export function resolveItemDocType(qualifier: string, types: Set<string>): strin
|
|
|
56
57
|
* @returns {{type: string, shortcode: string, itemDoc: boolean,
|
|
57
58
|
* package?: string, reason?: undefined} | {reason: "unknown-type"} | null}
|
|
58
59
|
* The resolved qualifier; a `reason` when the target is definitely qualified
|
|
59
|
-
* but names no known type; or `null` when it is
|
|
60
|
+
* but names no known type; or `null` when it is not an address at all.
|
|
60
61
|
*/
|
|
61
62
|
export function readQualifier(target: string, types: Set<string>, packages?: Set<string>): {
|
|
62
63
|
type: string;
|
|
@@ -86,33 +87,32 @@ export function anchorPageId(noteId: string, anchorSlug: string): string;
|
|
|
86
87
|
* Builds the link-resolution tables for a content tree.
|
|
87
88
|
*
|
|
88
89
|
* @param {Array<{type: string, id: string, shortcode?: string|null,
|
|
89
|
-
*
|
|
90
|
+
* name?: string, pack?: string, docPack?: string,
|
|
91
|
+
* draft?: boolean}>} docs -
|
|
90
92
|
* One entry per content note. `pack` / `docPack` name the packs the note's
|
|
91
93
|
* document and its documentation entry landed in; omitted, the conventional
|
|
92
|
-
* one-pack-per-type names stand in.
|
|
94
|
+
* one-pack-per-type names stand in. `draft` says the note carries the `draft`
|
|
95
|
+
* tag, which marks links *into* it and changes nothing else (#183).
|
|
93
96
|
* @param {string} packageId - The Foundry package shipping the packs; the first
|
|
94
97
|
* segment of every emitted UUID.
|
|
95
98
|
* @param {Map<string, object>} [foreign] - Canonically keyed entries from
|
|
96
99
|
* vendored manifests of packages this build links into but does not publish.
|
|
97
100
|
* @param {string} [contentPackage] - This build's *content* package, which an
|
|
98
101
|
* authored address may name explicitly. Defaults to `packageId`.
|
|
99
|
-
* @returns {{byShortcode: Map<string, object>,
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* unusable for it. `types` is every type the tree actually contains, so a
|
|
103
|
-
* qualifier naming no real type can be told apart from a missing target.
|
|
102
|
+
* @returns {{byShortcode: Map<string, object>, types: Set<string>}} `types` is
|
|
103
|
+
* every type the tree actually contains, so a qualifier naming no real type
|
|
104
|
+
* can be told apart from a missing target.
|
|
104
105
|
*/
|
|
105
106
|
export function buildWikilinkIndex(docs: Array<{
|
|
106
107
|
type: string;
|
|
107
108
|
id: string;
|
|
108
109
|
shortcode?: string | null;
|
|
109
|
-
aliases?: string[];
|
|
110
110
|
name?: string;
|
|
111
111
|
pack?: string;
|
|
112
112
|
docPack?: string;
|
|
113
|
+
draft?: boolean;
|
|
113
114
|
}>, packageId: string, foreign?: Map<string, object>, contentPackage?: string): {
|
|
114
115
|
byShortcode: Map<string, object>;
|
|
115
|
-
byAlias: Map<string, object | null>;
|
|
116
116
|
types: Set<string>;
|
|
117
117
|
};
|
|
118
118
|
/** Matches a whole wikilink, capturing its inner text. */
|
|
@@ -133,19 +133,23 @@ export function buildWikilinkIndex(docs: Array<{
|
|
|
133
133
|
*
|
|
134
134
|
* @param {string} markdown - The note body (frontmatter already stripped).
|
|
135
135
|
* @param {object} ctx
|
|
136
|
-
* @param {string} ctx.type - The source note's `type`, which
|
|
136
|
+
* @param {string} ctx.type - The source note's `type`, which addresses a
|
|
137
|
+
* `[[#slug]]` self-link.
|
|
137
138
|
* @param {string} ctx.id - The source note's document id.
|
|
138
139
|
* @param {string} [ctx.pack] - The pack the source note's own document landed
|
|
139
140
|
* in, which addresses a `[[#slug]]` self-link — the one target with no index
|
|
140
141
|
* entry.
|
|
141
142
|
* @param {string} [ctx.docPack] - The pack the source note's documentation
|
|
142
143
|
* entry landed in.
|
|
143
|
-
* @param {{byShortcode: Map,
|
|
144
|
+
* @param {{byShortcode: Map, types: Set}} ctx.index - From
|
|
144
145
|
* {@link buildWikilinkIndex}.
|
|
145
146
|
* @returns {{markdown: string, unresolved: Array<{link: string, target: string,
|
|
146
|
-
* offset: number, reason:
|
|
147
|
-
*
|
|
148
|
-
*
|
|
147
|
+
* offset: number, reason: string, packages?: string[], anchor?: string}>}}
|
|
148
|
+
* Each `reason` is one of {@link LINK_FINDING_REASONS}, the vocabulary all
|
|
149
|
+
* three resolvers share (#184) — `ambiguous` carries the claiming `packages`
|
|
150
|
+
* and `unknown-anchor` the section it named. `offset` is the link's 0-based
|
|
151
|
+
* position in `markdown`, which is what lets a caller report the line and
|
|
152
|
+
* column it sits on (#17).
|
|
149
153
|
*/
|
|
150
154
|
export function convertWikilinks(markdown: string, { type, id, pack, docPack, index }: {
|
|
151
155
|
type: string;
|
|
@@ -154,7 +158,6 @@ export function convertWikilinks(markdown: string, { type, id, pack, docPack, in
|
|
|
154
158
|
docPack?: string | undefined;
|
|
155
159
|
index: {
|
|
156
160
|
byShortcode: Map<any, any>;
|
|
157
|
-
byAlias: Map<any, any>;
|
|
158
161
|
types: Set<any>;
|
|
159
162
|
};
|
|
160
163
|
}): {
|
|
@@ -163,7 +166,9 @@ export function convertWikilinks(markdown: string, { type, id, pack, docPack, in
|
|
|
163
166
|
link: string;
|
|
164
167
|
target: string;
|
|
165
168
|
offset: number;
|
|
166
|
-
reason:
|
|
169
|
+
reason: string;
|
|
170
|
+
packages?: string[];
|
|
171
|
+
anchor?: string;
|
|
167
172
|
}>;
|
|
168
173
|
};
|
|
169
174
|
import { ITEM_PACK } from "./ids.mjs";
|
package/engine/abbreviations.mjs
DELETED
|
Binary file
|
package/engine/alias-index.mjs
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Replace whole words with their abbreviations.
|
|
3
|
-
*
|
|
4
|
-
* Greedy and longest-first: at each position the longest run of tokens that
|
|
5
|
-
* names an entry wins, so `tribunus militum` never abbreviates as
|
|
6
|
-
* `trib militum`, and `countess` is never reached by `count`'s rule.
|
|
7
|
-
*
|
|
8
|
-
* @param {readonly string[]} tokens - Lowercased, alphanumeric-only tokens.
|
|
9
|
-
* @returns {string[]} The tokens, with each matched run replaced by one token.
|
|
10
|
-
*/
|
|
11
|
-
export function abbreviateTokens(tokens: readonly string[]): string[];
|
|
12
|
-
/**
|
|
13
|
-
* The abbreviations a slug may use to stay short.
|
|
14
|
-
*
|
|
15
|
-
* Names in this setting are long and formulaic — a rank, an office, a material,
|
|
16
|
-
* a unit — so a slug built from one runs to a mouthful that says little more
|
|
17
|
-
* than a short one would. These are the conventional shortenings for that
|
|
18
|
-
* vocabulary.
|
|
19
|
-
*
|
|
20
|
-
* **Whole words only.** `count` abbreviates, `countess` does not become
|
|
21
|
-
* `ctess`: it has its own entry. Matching is greedy and longest-first, so a
|
|
22
|
-
* multi-word phrase wins over its own first word — `tribunus militum` is
|
|
23
|
-
* `tribmil`, a bare `tribunus` is `trib` — and a longer word wins over a
|
|
24
|
-
* shorter one that prefixes it.
|
|
25
|
-
*
|
|
26
|
-
* **Abbreviations are not unique, and that is the caller's problem.** Several
|
|
27
|
-
* words share one: `abbess` and `abbot` are both `abb`, `monk` and `brother`
|
|
28
|
-
* both `br`, `emperor` and `empress` both `emp`. Two names that differ only in
|
|
29
|
-
* such a word therefore slug alike, which for a URL is a collision — the
|
|
30
|
-
* build's existing slug-collision guard is what catches it, and the fix is a
|
|
31
|
-
* more specific name.
|
|
32
|
-
*
|
|
33
|
-
* @module
|
|
34
|
-
*/
|
|
35
|
-
/**
|
|
36
|
-
* Word (or phrase) → abbreviation.
|
|
37
|
-
*
|
|
38
|
-
* Keys are lowercase and space-separated as they read; a hyphenated key like
|
|
39
|
-
* `shire-reeve` is stored with its hyphen because that is how the word is
|
|
40
|
-
* written, and the matcher tokenises the same way it tokenises a name.
|
|
41
|
-
*
|
|
42
|
-
* @type {Readonly<Record<string, string>>}
|
|
43
|
-
*/
|
|
44
|
-
export const ABBREVIATIONS: Readonly<Record<string, string>>;
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The **alias** namespace: what a note can be called, and who may claim a name.
|
|
3
|
-
*
|
|
4
|
-
* A wikilink resolves through one of two namespaces, and the pipe chooses
|
|
5
|
-
* which (#131): `[[x|…]]` is an *address*, parsed by the address grammar;
|
|
6
|
-
* `[[x]]` is an *alias*, looked up here. This module owns the second half —
|
|
7
|
-
* what goes into the index, how a key is spelled, and what happens when two
|
|
8
|
-
* notes claim one name.
|
|
9
|
-
*
|
|
10
|
-
* **An alias is scoped to the claiming note's own type.** The key is
|
|
11
|
-
* `(type, alias)`, so `Shock` may be a `skill` in one place and a `trauma` in
|
|
12
|
-
* another without the two ever meeting. A link resolves against the *source*
|
|
13
|
-
* note's type, which is why a bare name reaches a sibling and never a
|
|
14
|
-
* cross-type target — that one is written as an address.
|
|
15
|
-
*
|
|
16
|
-
* **Three sources, all authored.** `aliases`, `name.aliases`, and `name.full`.
|
|
17
|
-
* Each is something a person wrote down as a name for the note, which is
|
|
18
|
-
* exactly what a bare `[[…]]` cites.
|
|
19
|
-
*
|
|
20
|
-
* **The filename is deliberately not one of them**, and it used to be — every
|
|
21
|
-
* one of the three copies of this index added `basename(file, ".md")` with
|
|
22
|
-
* underscores turned to spaces. That admitted keys no author could ever cite
|
|
23
|
-
* and no author had ever written:
|
|
24
|
-
*
|
|
25
|
-
* - `_Introduction.md` yields the alias `" introduction"`, *with a leading
|
|
26
|
-
* space*. A wikilink target is trimmed, so nothing can ever match it. In one
|
|
27
|
-
* repository thirteen notes — one per documentation section — claimed that
|
|
28
|
-
* key, making it the largest alias collision in the corpus and every one of
|
|
29
|
-
* its claimants blameless.
|
|
30
|
-
* - `README.md` yields `readme`, claimed once per section for the same reason.
|
|
31
|
-
*
|
|
32
|
-
* Since a collision is now a build failure rather than a silent deletion, an
|
|
33
|
-
* index entry that cannot be cited can only ever *cause* one. Removing the
|
|
34
|
-
* source was measured first, across all five content trees: not one link that
|
|
35
|
-
* resolves today resolves through the filename alone, so nothing loses a
|
|
36
|
-
* target — while the collision count falls without a note being edited.
|
|
37
|
-
*
|
|
38
|
-
* @module
|
|
39
|
-
*/
|
|
40
|
-
/**
|
|
41
|
-
* Every alias a note claims, in the order the sources are consulted.
|
|
42
|
-
*
|
|
43
|
-
* @param {object} fm - Parsed frontmatter.
|
|
44
|
-
* @returns {string[]} The claimed aliases, each a non-empty string.
|
|
45
|
-
*/
|
|
46
|
-
export function aliasesOf(fm: object): string[];
|
|
47
|
-
/**
|
|
48
|
-
* The index key one note's claim on one alias is filed under.
|
|
49
|
-
*
|
|
50
|
-
* Stated here so the three indexes — the pack build's, the site build's and
|
|
51
|
-
* the link checker's — cannot spell it differently. All three already used
|
|
52
|
-
* `type|alias`, lowercased; the risk was never that they disagreed today.
|
|
53
|
-
*
|
|
54
|
-
* @param {string} type - The claiming note's content type.
|
|
55
|
-
* @param {string} alias - The alias, as authored.
|
|
56
|
-
* @returns {string} The key.
|
|
57
|
-
*/
|
|
58
|
-
export function aliasKey(type: string, alias: string): string;
|
|
59
|
-
/**
|
|
60
|
-
* One alias claimed by more than one note of a single type.
|
|
61
|
-
*
|
|
62
|
-
* @typedef {object} AliasCollision
|
|
63
|
-
* @property {string} key - The index key, `type|alias`.
|
|
64
|
-
* @property {string} type - The type both claimants share.
|
|
65
|
-
* @property {string} alias - The alias, as the first claimant wrote it.
|
|
66
|
-
* @property {unknown[]} claimants - Every note claiming it, in walk order.
|
|
67
|
-
*/
|
|
68
|
-
/**
|
|
69
|
-
* Build the type-scoped alias index, and report every collision in it.
|
|
70
|
-
*
|
|
71
|
-
* **A collision resolves to nothing, and is reported naming every claimant.**
|
|
72
|
-
* Both halves matter. Resolving to whichever note happened to be walked first
|
|
73
|
-
* makes a link silently point at the wrong document, and which one it is
|
|
74
|
-
* depends on directory order. Reporting it at the *citing* note blames a file
|
|
75
|
-
* whose author did nothing wrong — whoever added the second claimant broke
|
|
76
|
-
* every existing citation (#13) — so the claimants are kept rather than
|
|
77
|
-
* discarded along with the entry.
|
|
78
|
-
*
|
|
79
|
-
* @template T
|
|
80
|
-
* @param {Iterable<{type: string, aliases: Iterable<string>, value: T}>} entries
|
|
81
|
-
* One per note: the type that scopes its claims, the aliases it claims, and
|
|
82
|
-
* whatever the caller wants an alias to resolve to.
|
|
83
|
-
* @param {object} [opts]
|
|
84
|
-
* @param {(a: T, b: T) => boolean} [opts.same] - Whether two values are the
|
|
85
|
-
* same note. Defaults to identity; a caller whose values are freshly built
|
|
86
|
-
* records supplies its own.
|
|
87
|
-
* @returns {{byKey: Map<string, T>, claims: Map<string, T[]>,
|
|
88
|
-
* collisions: AliasCollision[]}} `byKey` omits every colliding key, so a
|
|
89
|
-
* lookup in it can never resolve an ambiguous alias.
|
|
90
|
-
*/
|
|
91
|
-
export function indexAliases<T>(entries: Iterable<{
|
|
92
|
-
type: string;
|
|
93
|
-
aliases: Iterable<string>;
|
|
94
|
-
value: T;
|
|
95
|
-
}>, { same }?: {
|
|
96
|
-
same?: ((a: T, b: T) => boolean) | undefined;
|
|
97
|
-
}): {
|
|
98
|
-
byKey: Map<string, T>;
|
|
99
|
-
claims: Map<string, T[]>;
|
|
100
|
-
collisions: AliasCollision[];
|
|
101
|
-
};
|
|
102
|
-
/**
|
|
103
|
-
* One alias claimed by more than one note of a single type.
|
|
104
|
-
*/
|
|
105
|
-
export type AliasCollision = {
|
|
106
|
-
/**
|
|
107
|
-
* - The index key, `type|alias`.
|
|
108
|
-
*/
|
|
109
|
-
key: string;
|
|
110
|
-
/**
|
|
111
|
-
* - The type both claimants share.
|
|
112
|
-
*/
|
|
113
|
-
type: string;
|
|
114
|
-
/**
|
|
115
|
-
* - The alias, as the first claimant wrote it.
|
|
116
|
-
*/
|
|
117
|
-
alias: string;
|
|
118
|
-
/**
|
|
119
|
-
* - Every note claiming it, in walk order.
|
|
120
|
-
*/
|
|
121
|
-
claimants: unknown[];
|
|
122
|
-
};
|