@heroiclands/package-build 17.1.0 → 18.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 +1773 -0
- package/CONTENT.md +288 -72
- package/README.md +1 -1
- package/bin/content-build.mjs +218 -137
- package/bin/package-build.mjs +43 -0
- package/content-config.mjs +62 -67
- package/docs/content-format.md +768 -58
- package/engine/actor-compiler.mjs +586 -0
- package/engine/address-charset.mjs +11 -8
- package/engine/address-diff.mjs +266 -22
- package/engine/anchored-sections.mjs +83 -0
- package/engine/anchors.mjs +83 -0
- package/engine/base-compiler.mjs +148 -18
- package/engine/bundle-notes.mjs +276 -0
- package/engine/bundles.mjs +307 -0
- package/engine/code-fences.mjs +103 -0
- package/engine/compile-corpus.mjs +89 -0
- package/engine/content-address.mjs +267 -5
- package/engine/content-format-check.mjs +54 -11
- package/engine/content-format.mjs +37 -5
- package/engine/content-index.mjs +394 -111
- package/engine/content-links.mjs +185 -77
- package/engine/content-lint.mjs +232 -14
- package/engine/content-package.mjs +2 -1
- package/engine/content-tables.mjs +99 -3
- package/engine/document-subtypes.mjs +49 -6
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +56 -6
- package/engine/folder-notes.mjs +470 -0
- package/engine/foreign-catalog.mjs +195 -4
- package/engine/{manifest-emit.mjs → foundry-entries.mjs} +78 -81
- package/engine/frontmatter-lint.mjs +245 -19
- package/engine/frontmatter.mjs +88 -8
- package/engine/generate.mjs +304 -13
- package/engine/helpers.mjs +293 -64
- package/engine/ids.mjs +137 -2
- package/engine/index-records.mjs +126 -0
- package/engine/index.mjs +23 -5
- package/engine/item-compiler.mjs +349 -0
- package/engine/item-docs.mjs +7 -1
- package/engine/item-registry.mjs +6 -0
- package/engine/journals.mjs +93 -27
- package/engine/macros.mjs +4 -2
- package/engine/metadata-index.mjs +495 -0
- package/engine/note-claims.mjs +146 -25
- package/engine/note-ids.mjs +112 -0
- package/engine/note-renames.mjs +134 -0
- package/engine/note-schemas.mjs +25 -0
- package/engine/note-vocabulary.mjs +110 -27
- package/engine/pack-config.mjs +4 -0
- package/engine/pack-router.mjs +23 -0
- package/engine/retired-fields.mjs +137 -3
- package/engine/scenes.mjs +66 -34
- package/engine/schema-check.mjs +58 -17
- package/engine/site-build.mjs +71 -12
- package/engine/site-index.mjs +20 -7
- package/engine/sql-tables.mjs +485 -0
- package/engine/subtype-registry.mjs +102 -0
- package/engine/system-block.mjs +96 -12
- package/engine/systems.mjs +201 -0
- package/engine/web-wikilinks.mjs +50 -20
- package/engine/wikilink-syntax.mjs +17 -8
- package/engine/wikilinks.mjs +124 -51
- package/engine/yaml-lint.mjs +282 -0
- package/hm3/actors.mjs +352 -0
- package/hm3/default-item-art.mjs +75 -0
- package/hm3/document-subtypes.mjs +134 -0
- package/hm3/index.mjs +56 -0
- package/hm3/item-builders.mjs +84 -0
- package/hm3/item-fields.mjs +180 -0
- package/hm3/items.mjs +96 -0
- package/hm3/template-priority.mjs +85 -0
- package/labels.mjs +191 -0
- package/manifest.mjs +43 -2
- package/package.json +19 -4
- package/release.mjs +62 -7
- package/sohl/actors.mjs +33 -487
- package/sohl/being-info.mjs +16 -7
- package/sohl/default-item-art.mjs +14 -3
- package/sohl/document-subtypes.mjs +16 -10
- package/sohl/item-builders.mjs +14 -5
- package/sohl/item-fields.mjs +68 -7
- package/sohl/items.mjs +44 -258
- package/sohl/note-schemas.mjs +2 -2
- package/types/content-config.d.mts +21 -42
- package/types/engine/actor-compiler.d.mts +204 -0
- package/types/engine/address-charset.d.mts +11 -8
- package/types/engine/address-diff.d.mts +53 -5
- package/types/engine/anchored-sections.d.mts +21 -0
- package/types/engine/anchors.d.mts +20 -0
- package/types/engine/base-compiler.d.mts +17 -17
- package/types/engine/bundle-notes.d.mts +173 -0
- package/types/engine/bundles.d.mts +60 -0
- package/types/engine/code-fences.d.mts +43 -0
- package/types/engine/compile-corpus.d.mts +32 -0
- package/types/engine/content-address.d.mts +205 -5
- package/types/engine/content-format-check.d.mts +6 -2
- package/types/engine/content-format.d.mts +57 -1
- package/types/engine/content-index.d.mts +64 -80
- package/types/engine/content-links.d.mts +52 -7
- package/types/engine/content-lint.d.mts +10 -1
- package/types/engine/content-package.d.mts +2 -1
- package/types/engine/content-tables.d.mts +21 -39
- package/types/engine/document-subtypes.d.mts +37 -3
- package/types/engine/field-spec.d.mts +76 -5
- package/types/engine/folder-notes.d.mts +159 -0
- package/types/engine/foreign-catalog.d.mts +53 -0
- package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +16 -38
- package/types/engine/frontmatter-lint.d.mts +10 -2
- package/types/engine/frontmatter.d.mts +64 -0
- package/types/engine/generate.d.mts +38 -0
- package/types/engine/helpers.d.mts +94 -30
- package/types/engine/ids.d.mts +96 -0
- package/types/engine/index-records.d.mts +68 -0
- package/types/engine/index.d.mts +9 -3
- package/types/engine/item-compiler.d.mts +131 -0
- package/types/engine/journals.d.mts +47 -9
- package/types/engine/metadata-index.d.mts +226 -0
- package/types/engine/note-claims.d.mts +57 -11
- package/types/engine/note-ids.d.mts +38 -0
- package/types/engine/note-renames.d.mts +102 -0
- package/types/engine/note-vocabulary.d.mts +44 -8
- package/types/engine/retired-fields.d.mts +75 -0
- package/types/engine/scenes.d.mts +3 -2
- package/types/engine/schema-check.d.mts +25 -4
- package/types/engine/site-build.d.mts +4 -4
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +185 -0
- package/types/engine/subtype-registry.d.mts +49 -0
- package/types/engine/system-block.d.mts +40 -1
- package/types/engine/systems.d.mts +106 -0
- package/types/engine/web-wikilinks.d.mts +4 -2
- package/types/engine/wikilink-syntax.d.mts +10 -3
- package/types/engine/wikilinks.d.mts +41 -13
- package/types/engine/yaml-lint.d.mts +107 -0
- package/types/hm3/actors.d.mts +48 -0
- package/types/hm3/default-item-art.d.mts +42 -0
- package/types/hm3/document-subtypes.d.mts +24 -0
- package/types/hm3/index.d.mts +7 -0
- package/types/hm3/item-builders.d.mts +11 -0
- package/types/hm3/item-fields.d.mts +12 -0
- package/types/hm3/items.d.mts +23 -0
- package/types/hm3/template-priority.d.mts +21 -0
- package/types/manifest.d.mts +22 -1
- package/types/release.d.mts +9 -4
- package/types/sohl/actors.d.mts +7 -74
- package/types/sohl/being-info.d.mts +8 -27
- package/types/sohl/default-item-art.d.mts +5 -3
- package/types/sohl/items.d.mts +17 -32
- package/engine/foreign-manifests.mjs +0 -126
- package/engine/kb-manifest.mjs +0 -490
- package/types/engine/foreign-manifests.d.mts +0 -43
- package/types/engine/kb-manifest.d.mts +0 -241
package/engine/content-links.mjs
CHANGED
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
* link to an anchor nobody declares compiles cleanly, emits an enricher, and
|
|
24
24
|
* dead-ends for the reader.
|
|
25
25
|
* 2. **A dead address.** Every link is an address, and one resolving to no note
|
|
26
|
-
* is a typo. So is a target that does not parse as an address at all.
|
|
26
|
+
* is a typo. So is a target that does not parse as an address at all. A
|
|
27
|
+
* written target is a *partial* address — the segments it omits are
|
|
28
|
+
* wildcards, and the package it omits is this one — so a target resolving to
|
|
29
|
+
* *several* notes is an ambiguity rather than a first match, and is reported
|
|
30
|
+
* naming every candidate (#59).
|
|
27
31
|
* 3. **An unlabelled link.** `[[x]]` addresses nothing: the alias namespace it
|
|
28
32
|
* used to name is retired (#180), and a shortcode is an address rather than
|
|
29
33
|
* prose, so the link has neither a resolvable target nor text to show. The
|
|
@@ -55,17 +59,19 @@ import path from "node:path";
|
|
|
55
59
|
|
|
56
60
|
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
57
61
|
import { expandContentTables } from "./content-tables.mjs";
|
|
58
|
-
import {
|
|
62
|
+
import { collectAnchors } from "./anchors.mjs";
|
|
63
|
+
// The corpus, and everything derived from it, read from the one place that
|
|
64
|
+
// derives it (#243). Nothing in the index's own import graph reaches this
|
|
65
|
+
// module, so this is a plain static import rather than the deferred one
|
|
66
|
+
// `sql-tables` needs to keep out of the compilers' cycle.
|
|
67
|
+
import { authoredFrontmatter, indexRecordsFor, isNoteRecord, noteFile } from "./content-index.mjs";
|
|
59
68
|
import { hasDocEntry } from "./item-docs.mjs";
|
|
60
|
-
import {
|
|
69
|
+
import { NO_SYSTEM, systemOf } from "./document-subtypes.mjs";
|
|
70
|
+
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./note-claims.mjs";
|
|
71
|
+
import { loadPackConfig } from "./pack-config.mjs";
|
|
61
72
|
import { searchableFrontmatter } from "./note-package.mjs";
|
|
62
|
-
import {
|
|
63
|
-
|
|
64
|
-
loadForeignManifests,
|
|
65
|
-
manifestsComplete,
|
|
66
|
-
PACKAGE_BASE,
|
|
67
|
-
readCanonicalKey,
|
|
68
|
-
} from "./kb-manifest.mjs";
|
|
73
|
+
import { canonicalKey, PACKAGE_BASE, readCanonicalKey } from "./content-address.mjs";
|
|
74
|
+
import { loadForeignIndexes } from "./metadata-index.mjs";
|
|
69
75
|
import { frontmatterWikilinks, slugify } from "./web-wikilinks.mjs";
|
|
70
76
|
import { homepageAddresses, isHomepage } from "./homepage.mjs";
|
|
71
77
|
import { RETIRED_TYPES } from "./ids.mjs";
|
|
@@ -75,20 +81,46 @@ import { readQualifier } from "./wikilinks.mjs";
|
|
|
75
81
|
/**
|
|
76
82
|
* Every `{#anchor}` a note declares on a heading.
|
|
77
83
|
*
|
|
84
|
+
* **Read from the content index's reader, not a second one.** This module kept
|
|
85
|
+
* its own until #243, and the two disagreed: it matched `{#([a-z0-9-]+)}` while
|
|
86
|
+
* {@link module:engine/content-index.collectAnchors} matches `{#([^}]+)}`, so
|
|
87
|
+
* an anchor with a capital in it — `{#CalendarFormat}` — existed for the index
|
|
88
|
+
* and for the compiler and did not exist for the link checker. Nothing links to
|
|
89
|
+
* one today, so the disagreement was latent; the first link to one would have
|
|
90
|
+
* been reported dead against a heading plainly present in the file.
|
|
91
|
+
*
|
|
92
|
+
* The specification puts no charset on the id: "`#id` represents an id anchor
|
|
93
|
+
* named `id`". The narrower pattern was this module's invention, which is the
|
|
94
|
+
* argument for there being one reader rather than a well-chosen one.
|
|
95
|
+
*
|
|
78
96
|
* @param {string} body - The note's markdown body.
|
|
79
97
|
* @returns {Set<string>} The declared anchor slugs.
|
|
80
98
|
*/
|
|
81
99
|
export function anchorsOf(body) {
|
|
82
|
-
|
|
83
|
-
for (const line of String(body ?? "").split("\n")) {
|
|
84
|
-
const m = /^#{1,6}\s+.*\{#([a-z0-9-]+)\}\s*$/.exec(line.trim());
|
|
85
|
-
if (m) found.add(m[1]);
|
|
86
|
-
}
|
|
87
|
-
return found;
|
|
100
|
+
return new Set(collectAnchors(body).map((anchor) => anchor.slug));
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
/**
|
|
91
|
-
*
|
|
104
|
+
* Read a content tree into the index a link resolves against.
|
|
105
|
+
*
|
|
106
|
+
* **The corpus comes from the content index, not from a walk of this module's
|
|
107
|
+
* own** (#243). Every pass used to answer "which files are the content?" for
|
|
108
|
+
* itself and throw the answer away; this one now reads
|
|
109
|
+
* {@link module:engine/content-index.indexRecordsFor}, which is the same
|
|
110
|
+
* derivation the published artifact and the compilers are driven from. So a
|
|
111
|
+
* note the index records is a note the link check sees, and the addresses and
|
|
112
|
+
* anchors it resolves against are the ones every other pass will emit — rather
|
|
113
|
+
* than a second derivation that agrees with them only by inspection. That was
|
|
114
|
+
* not hypothetical: this module carried its own anchor reader until the anchor
|
|
115
|
+
* half of #243, and the two disagreed about which anchors existed.
|
|
116
|
+
*
|
|
117
|
+
* **The file is opened for its bytes and nothing else.** The index deliberately
|
|
118
|
+
* carries no note *body*, and a link lives in the body — so each note is read
|
|
119
|
+
* once, here, for the prose. Everything *about* the note — its frontmatter, its
|
|
120
|
+
* addresses, its anchors — is already in the record, and none of it is derived
|
|
121
|
+
* a second time. That is one read per note rather than the two this module did
|
|
122
|
+
* before, since the walk read the file and it then read it again for the raw
|
|
123
|
+
* text.
|
|
92
124
|
*
|
|
93
125
|
* The index mirrors what both builds construct, including the two addresses a
|
|
94
126
|
* doc-carrying note answers to: `type/shortcode` for the document, and
|
|
@@ -99,57 +131,93 @@ export function anchorsOf(body) {
|
|
|
99
131
|
*
|
|
100
132
|
* @param {string} contentBase - Root of the content tree.
|
|
101
133
|
* @param {object} [opts]
|
|
102
|
-
* @param {
|
|
103
|
-
*
|
|
104
|
-
*
|
|
134
|
+
* @param {object} [opts.config] - The resolved build configuration, whose
|
|
135
|
+
* fetched dependency indexes foreign addresses resolve through (#239), and
|
|
136
|
+
* whose `contentPackage` every local address is built from. Omitted, the
|
|
137
|
+
* ambient configuration is resolved and no cross-package address resolves.
|
|
138
|
+
* @param {readonly string[]} [opts.skipDirectories] - The walk's scope, passed
|
|
139
|
+
* on to the index rather than defaulted away.
|
|
140
|
+
* @param {Map<string, object[]>} [opts.sqlTables] - Prepared `sql` results, by
|
|
141
|
+
* note path.
|
|
142
|
+
* @param {object[]} [opts.records] - Index records the caller already derived,
|
|
143
|
+
* so a command that also needs them — every one of them does, to answer its
|
|
144
|
+
* `sql` tables — enumerates the corpus once rather than twice.
|
|
145
|
+
* @param {object[]} [opts.problems] - Collects the notes the index cannot
|
|
146
|
+
* record, as diagnostics, instead of letting one of them abort the check
|
|
147
|
+
* before it has reported anything else.
|
|
105
148
|
* @returns {object} The notes, the index, and the resolvers built over it.
|
|
106
149
|
*/
|
|
107
|
-
export function buildLinkIndex(
|
|
150
|
+
export function buildLinkIndex(
|
|
151
|
+
contentBase,
|
|
152
|
+
{ config, skipDirectories, sqlTables, records, problems } = {},
|
|
153
|
+
) {
|
|
108
154
|
const notes = [];
|
|
109
155
|
const frontmatterLinks = [];
|
|
110
|
-
const walkOpts = skipDirectories ? { skipDirectories } : undefined;
|
|
111
156
|
|
|
112
|
-
|
|
113
|
-
|
|
157
|
+
// The one package every note in this tree belongs to. Taken from the
|
|
158
|
+
// configuration this build resolved — never from a note (`package:` is
|
|
159
|
+
// retired, so there is no second source an address could disagree with,
|
|
160
|
+
// #56) and never from the ambient one, which is a different configuration
|
|
161
|
+
// whenever a test injects one, `PACKAGE_BUILD_CONFIG` names one, or the
|
|
162
|
+
// command runs from a worktree (#243).
|
|
163
|
+
const resolved = config ?? loadPackConfig();
|
|
164
|
+
const pkg = resolved.contentPackage;
|
|
165
|
+
|
|
166
|
+
const indexRecords =
|
|
167
|
+
records ?? indexRecordsFor({ contentBase, config: resolved, skipDirectories, problems });
|
|
168
|
+
|
|
169
|
+
const byKey = new Map();
|
|
170
|
+
const anchors = new Map();
|
|
171
|
+
|
|
172
|
+
for (const record of indexRecords) {
|
|
173
|
+
// A documentation journal has a record of its own but no file and no
|
|
174
|
+
// authored frontmatter — it is a document this tree emits, not a note
|
|
175
|
+
// in it. Its addresses are keyed below, from the note it documents.
|
|
176
|
+
if (!isNoteRecord(record) || typeof record.type !== "string") continue;
|
|
177
|
+
|
|
178
|
+
const fm = authoredFrontmatter(record);
|
|
179
|
+
const rel = record.file.path;
|
|
180
|
+
const absPath = noteFile(contentBase, record);
|
|
114
181
|
// The raw text is kept beside the parsed body: a consumer's own checks
|
|
115
182
|
// may need what frontmatter carried, which the body has dropped.
|
|
116
183
|
const raw = fs.readFileSync(absPath, "utf8");
|
|
117
184
|
const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, "");
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
fm,
|
|
122
|
-
body,
|
|
123
|
-
raw,
|
|
124
|
-
type: fm.type.toLowerCase(),
|
|
125
|
-
};
|
|
185
|
+
const type = record.type.toLowerCase();
|
|
186
|
+
const note = { file: absPath, rel, fm, body, raw, type };
|
|
187
|
+
|
|
126
188
|
for (const hit of frontmatterWikilinks(fm)) {
|
|
127
189
|
frontmatterLinks.push({ note, ...hit });
|
|
128
190
|
}
|
|
129
191
|
notes.push(note);
|
|
130
|
-
}
|
|
131
192
|
|
|
132
|
-
|
|
193
|
+
// The anchors the index recorded, rather than a second reading of the
|
|
194
|
+
// same headings — the disagreement #243's anchor half removed.
|
|
195
|
+
anchors.set(note, new Set((record.anchors ?? []).map((a) => a.slug)));
|
|
133
196
|
|
|
134
|
-
// The one package every note in this tree belongs to. Taken from the
|
|
135
|
-
// configuration, never from a note: `package:` is retired, so there is no
|
|
136
|
-
// second source an address could disagree with (#56).
|
|
137
|
-
const pkg = contentPackage();
|
|
138
|
-
|
|
139
|
-
for (const note of notes) {
|
|
140
|
-
const { fm, type } = note;
|
|
141
197
|
if (typeof fm.shortcode === "string" && fm.shortcode) {
|
|
142
198
|
byKey.set(`${type}/${fm.shortcode}`.toLowerCase(), note);
|
|
143
199
|
// The canonical, fully qualified address alongside the short one,
|
|
144
200
|
// so a package-qualified link checks the same way a bare one does.
|
|
145
|
-
|
|
201
|
+
// Taken from the record, which is where the address rule is applied
|
|
202
|
+
// once for the whole build.
|
|
203
|
+
const canonical =
|
|
204
|
+
record.address?.canonical ??
|
|
205
|
+
canonicalKey(pkg, systemOf(type, KNOWN_DOCUMENT_SUBTYPE_MAPS), type, fm.shortcode);
|
|
206
|
+
byKey.set(canonical, note);
|
|
146
207
|
if (hasDocEntry(type)) {
|
|
147
208
|
byKey.set(`doc${type}/${fm.shortcode}`.toLowerCase(), note);
|
|
148
|
-
|
|
209
|
+
// A documentation journal is `none`: no game system defines a
|
|
210
|
+
// JournalEntry, and one note has one of them however many
|
|
211
|
+
// system blocks it carries.
|
|
212
|
+
byKey.set(canonicalKey(pkg, NO_SYSTEM, `doc${type}`, fm.shortcode), note);
|
|
149
213
|
}
|
|
150
214
|
}
|
|
151
215
|
}
|
|
152
216
|
|
|
217
|
+
// The types a link may name, which are the ones notes declare. A
|
|
218
|
+
// documentation journal's `doc<type>` is deliberately not among them: it is
|
|
219
|
+
// virtual, and `readQualifier` resolves it from the base type rather than
|
|
220
|
+
// from a type any tree declares.
|
|
153
221
|
const types = new Set(notes.map((n) => n.type));
|
|
154
222
|
|
|
155
223
|
// A foreign package may use a type this tree has never seen, so its types
|
|
@@ -157,8 +225,8 @@ export function buildLinkIndex(contentBase, { manifestDir, skipDirectories } = {
|
|
|
157
225
|
// is never checked at all.
|
|
158
226
|
const localPackages = new Set([pkg]);
|
|
159
227
|
const foreign =
|
|
160
|
-
|
|
161
|
-
|
|
228
|
+
config ?
|
|
229
|
+
loadForeignIndexes(config, localPackages)
|
|
162
230
|
: { index: new Map(), packages: new Set(), stale: [] };
|
|
163
231
|
for (const v of foreign.index.values()) if (v.type) types.add(v.type);
|
|
164
232
|
|
|
@@ -174,8 +242,6 @@ export function buildLinkIndex(contentBase, { manifestDir, skipDirectories } = {
|
|
|
174
242
|
folder: path.dirname(n.rel).split("/").pop(),
|
|
175
243
|
}));
|
|
176
244
|
|
|
177
|
-
const anchors = new Map(notes.map((n) => [n, anchorsOf(n.body)]));
|
|
178
|
-
|
|
179
245
|
/**
|
|
180
246
|
* Every wikilink in a note body, with its `dataview` tables expanded.
|
|
181
247
|
*
|
|
@@ -187,13 +253,17 @@ export function buildLinkIndex(contentBase, { manifestDir, skipDirectories } = {
|
|
|
187
253
|
*/
|
|
188
254
|
function linksOf(note) {
|
|
189
255
|
let body = note.body;
|
|
190
|
-
if (/^[ \t]*(?:`{3,}|~{3,})[ \t]*dataview\b/im.test(body)) {
|
|
256
|
+
if (/^[ \t]*(?:`{3,}|~{3,})[ \t]*(?:dataview|sql)\b/im.test(body)) {
|
|
191
257
|
body = expandContentTables(body, {
|
|
192
258
|
// Unfiltered: every note in the tree is this package's, so
|
|
193
259
|
// there is no other package's note to exclude (#56).
|
|
194
260
|
docs: tableDocs,
|
|
195
261
|
linkable: (d) => Boolean(d.fm.shortcode),
|
|
196
262
|
source: note.file,
|
|
263
|
+
// A `sql` table's links are checked like an authored one's, so
|
|
264
|
+
// its rows are prepared ahead of this walk — see
|
|
265
|
+
// {@link module:engine/sql-tables.prepareTreeSqlTables}.
|
|
266
|
+
sqlTables: sqlTables?.get(note.file),
|
|
197
267
|
}).markdown;
|
|
198
268
|
}
|
|
199
269
|
const out = [];
|
|
@@ -232,25 +302,75 @@ export function buildLinkIndex(contentBase, { manifestDir, skipDirectories } = {
|
|
|
232
302
|
* @param {string} target - The link target, anchor already removed.
|
|
233
303
|
* @returns {object|undefined} The note it addresses.
|
|
234
304
|
*/
|
|
305
|
+
/**
|
|
306
|
+
* Every indexed entry an address names, matching only the segments it
|
|
307
|
+
* supplies.
|
|
308
|
+
*
|
|
309
|
+
* This is the whole of #59's resolution rule in one place: a written
|
|
310
|
+
* address is a *partial* one, unsupplied segments are wildcards, and the
|
|
311
|
+
* caller requires exactly one hit. Nothing here decides an ambiguity — zero
|
|
312
|
+
* and many are different findings with different fixes, so the count is
|
|
313
|
+
* returned rather than collapsed.
|
|
314
|
+
*
|
|
315
|
+
* The **system** is wildcarded unless stated. Defaulting it to `none` would
|
|
316
|
+
* exclude every link to an item, which is most of them.
|
|
317
|
+
*
|
|
318
|
+
* @param {Array<[string, any]>} pairs - Indexed `[canonicalKey, value]`.
|
|
319
|
+
* @param {object} q - The parsed qualifier.
|
|
320
|
+
* @returns {Array<[string, any]>} The matching pairs.
|
|
321
|
+
*/
|
|
322
|
+
function matchAddress(pairs, q) {
|
|
323
|
+
const type = String(q.type).toLowerCase();
|
|
324
|
+
const shortcode = String(q.shortcode).toLowerCase();
|
|
325
|
+
return pairs.filter(([k]) => {
|
|
326
|
+
const parts = readCanonicalKey(k);
|
|
327
|
+
if (!parts) return false;
|
|
328
|
+
if (q.package && parts.package !== String(q.package).toLowerCase()) return false;
|
|
329
|
+
if (q.system && parts.system !== String(q.system).toLowerCase()) return false;
|
|
330
|
+
return parts.type === type && parts.shortcode === shortcode;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The local notes an address names, by the same rule.
|
|
336
|
+
*
|
|
337
|
+
* @param {object} q - The parsed qualifier.
|
|
338
|
+
* @returns {object[]} The notes.
|
|
339
|
+
*/
|
|
340
|
+
function matchLocal(q) {
|
|
341
|
+
return matchAddress([...byKey], q).map(([, v]) => v);
|
|
342
|
+
}
|
|
343
|
+
|
|
235
344
|
function resolveAddress(target) {
|
|
236
345
|
const qualified = readQualifier(target, types, packages);
|
|
237
346
|
if (!qualified || qualified.reason) return undefined;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
347
|
+
// A target naming neither package nor system stays the system-blind
|
|
348
|
+
// short key, which is already the wildcard an author writing
|
|
349
|
+
// `[[skill-melee]]` means.
|
|
350
|
+
//
|
|
351
|
+
// Anything that *does* state one is matched by the segments it supplied
|
|
352
|
+
// rather than by an exact key (#59), and exactly one hit is required —
|
|
353
|
+
// two systems' documents legitimately share a
|
|
354
|
+
// `(package, type, shortcode)`, so a target that names no system may
|
|
355
|
+
// name two notes, and naming two is not resolving.
|
|
356
|
+
if (!qualified.package && !qualified.system) {
|
|
357
|
+
return byKey.get(`${qualified.type}/${qualified.shortcode}`.toLowerCase());
|
|
358
|
+
}
|
|
359
|
+
const hits = matchLocal(qualified);
|
|
360
|
+
return hits.length === 1 ? hits[0] : undefined;
|
|
243
361
|
}
|
|
244
362
|
|
|
245
363
|
/**
|
|
246
364
|
* Every foreign manifest entry an address names, in package order.
|
|
247
365
|
*
|
|
248
|
-
* A **
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
366
|
+
* A written target is a **partial** address, so this matches on the
|
|
367
|
+
* segments it supplies and wildcards the rest (#59). A target naming a
|
|
368
|
+
* package necessarily names its system too — omission runs left to right —
|
|
369
|
+
* so the fully qualified form matches at most one entry; a shorter one
|
|
370
|
+
* names no package, and resolves against any foreign package that
|
|
371
|
+
* publishes it. Either way only exactly one hit resolves. Two claimants make it ambiguous, which is a different finding
|
|
372
|
+
* from resolving nowhere and has a different fix, so the count is returned
|
|
373
|
+
* rather than collapsed here (#184).
|
|
254
374
|
*
|
|
255
375
|
* @param {string} target - The link target.
|
|
256
376
|
* @returns {object[]} The foreign entries, each carrying its `package`.
|
|
@@ -258,18 +378,7 @@ export function buildLinkIndex(contentBase, { manifestDir, skipDirectories } = {
|
|
|
258
378
|
function foreignHits(target) {
|
|
259
379
|
const q = readQualifier(target, types, packages);
|
|
260
380
|
if (!q || q.reason) return [];
|
|
261
|
-
|
|
262
|
-
const one = foreign.index.get(canonicalKey(q.package, q.type, q.shortcode));
|
|
263
|
-
return one ? [one] : [];
|
|
264
|
-
}
|
|
265
|
-
const type = String(q.type).toLowerCase();
|
|
266
|
-
const shortcode = String(q.shortcode).toLowerCase();
|
|
267
|
-
return [...foreign.index]
|
|
268
|
-
.filter(([k]) => {
|
|
269
|
-
const parts = readCanonicalKey(k);
|
|
270
|
-
return parts?.type === type && parts.shortcode === shortcode;
|
|
271
|
-
})
|
|
272
|
-
.map(([, v]) => v);
|
|
381
|
+
return matchAddress([...foreign.index], q).map(([, v]) => v);
|
|
273
382
|
}
|
|
274
383
|
|
|
275
384
|
/**
|
|
@@ -299,7 +408,6 @@ export function buildLinkIndex(contentBase, { manifestDir, skipDirectories } = {
|
|
|
299
408
|
*/
|
|
300
409
|
contentPackage: pkg,
|
|
301
410
|
foreign,
|
|
302
|
-
manifests: manifestsComplete(localPackages, foreign.packages),
|
|
303
411
|
linksOf,
|
|
304
412
|
/**
|
|
305
413
|
* Resolve a link target the way both builds do, or `undefined`. Every
|
|
@@ -338,17 +446,17 @@ const SITE_HOST = /^(?:[a-z0-9-]+\.)*heroiclands\.org$/i;
|
|
|
338
446
|
* is that a landing therefore cannot be addressed. It does not follow: a
|
|
339
447
|
* landing's address is not a *note's* address but the **package's**, and
|
|
340
448
|
* {@link PACKAGE_BASE} already records where each package is served. That is a
|
|
341
|
-
* frozen constant
|
|
449
|
+
* frozen constant compiled into every build, so consulting it walks no
|
|
342
450
|
* tree, reads no manifest and builds no index — which is precisely why the
|
|
343
451
|
* mechanism survives `homepage` mode, where the licensing fence means none of
|
|
344
452
|
* those exist.
|
|
345
453
|
*
|
|
346
454
|
* The roster is consulted **for landings only**. Widening the package set the
|
|
347
455
|
* other rules read would make them offer manifest-based advice about packages
|
|
348
|
-
* no
|
|
456
|
+
* no index has been fetched for.
|
|
349
457
|
*
|
|
350
458
|
* @param {string} ownPackage - The package this build publishes.
|
|
351
|
-
* @param {Iterable<string>} manifestPackages - Packages a
|
|
459
|
+
* @param {Iterable<string>} manifestPackages - Packages a fetched index
|
|
352
460
|
* names, which are addressable whether or not the roster lists them.
|
|
353
461
|
* @returns {Map<string, string>} Package to base, each base slash-terminated.
|
|
354
462
|
*/
|
|
@@ -458,7 +566,7 @@ function readAddress(url, packages) {
|
|
|
458
566
|
* and what replaced it, so this is a fact rather than a guess — and it is
|
|
459
567
|
* exactly the SoHL defect.
|
|
460
568
|
* - A **hardcoded absolute URL** into this package's own prefix, or into one a
|
|
461
|
-
*
|
|
569
|
+
* a fetched index names. Every one of them has a better form to write, which
|
|
462
570
|
* is why every one is reported — including a bare `/<package>/`, which names
|
|
463
571
|
* another package's landing (#87).
|
|
464
572
|
*
|
|
@@ -533,7 +641,7 @@ export function auditHomepageLinks(index) {
|
|
|
533
641
|
|
|
534
642
|
// Landings first, and by the roster rather than by the manifest
|
|
535
643
|
// package set: a landing is addressable in a repository that
|
|
536
|
-
//
|
|
644
|
+
// has fetched no index at all, which is the case the fence creates
|
|
537
645
|
// and the case this rule exists for (#87).
|
|
538
646
|
const landing = landingTarget(url, bases);
|
|
539
647
|
if (landing) {
|