@heroiclands/package-build 10.0.1 → 11.1.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 +375 -0
- package/CONTENT.md +234 -71
- package/MIGRATING.md +64 -0
- package/bin/content-build.mjs +79 -75
- package/content-config.mjs +28 -0
- 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 +161 -18
- 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/content-config.d.mts +24 -0
- 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/frontmatter-lint.d.mts +27 -2
- 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
package/bin/content-build.mjs
CHANGED
|
@@ -72,6 +72,10 @@ import {
|
|
|
72
72
|
import { renderItemFieldReference } from "../engine/field-reference.mjs";
|
|
73
73
|
import { lintContentTree } from "../engine/content-lint.mjs";
|
|
74
74
|
import { lintFrontmatter } from "../engine/frontmatter-lint.mjs";
|
|
75
|
+
// The sections this repository declares — the open set a `README` landing's
|
|
76
|
+
// `subType` names (#197). Read from the resolved configuration the site build
|
|
77
|
+
// renders those landings from, so neither can name a section the other does not.
|
|
78
|
+
import { declaredSections } from "../content-config.mjs";
|
|
75
79
|
import { loadContentFormat } from "../engine/content-format.mjs";
|
|
76
80
|
import {
|
|
77
81
|
checkDeclaredFields,
|
|
@@ -103,6 +107,8 @@ import {
|
|
|
103
107
|
formatUnaddressableFinding as formatUnaddressable,
|
|
104
108
|
} from "../engine/site-build.mjs";
|
|
105
109
|
import { auditLinks, buildLinkIndex, walkReachability } from "../engine/content-links.mjs";
|
|
110
|
+
// The one place a link finding is worded, shared with both builds (#184).
|
|
111
|
+
import { linkFindingMessage } from "../engine/wikilink-syntax.mjs";
|
|
106
112
|
import {
|
|
107
113
|
emitDiagnostic,
|
|
108
114
|
positionInFrontmatter,
|
|
@@ -189,6 +195,26 @@ function reportFailure(err) {
|
|
|
189
195
|
else log.error(message);
|
|
190
196
|
}
|
|
191
197
|
|
|
198
|
+
/**
|
|
199
|
+
* A note's source text, or `""` when it cannot be read.
|
|
200
|
+
*
|
|
201
|
+
* Used only to turn a link finding into a position. A path that no longer
|
|
202
|
+
* resolves — a page a build generated, a tree walked from somewhere else —
|
|
203
|
+
* yields `""`, and {@link positionOfLiteral} then reports nothing, so the
|
|
204
|
+
* diagnostic drops the line and column rather than guessing them.
|
|
205
|
+
*
|
|
206
|
+
* @param {string|undefined} file - Absolute path to the note.
|
|
207
|
+
* @returns {string} The file's contents, or `""`.
|
|
208
|
+
*/
|
|
209
|
+
function readRawNote(file) {
|
|
210
|
+
if (!file) return "";
|
|
211
|
+
try {
|
|
212
|
+
return fs.readFileSync(file, "utf8");
|
|
213
|
+
} catch {
|
|
214
|
+
return "";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
192
218
|
/**
|
|
193
219
|
* The declaration sets this package ships, addressable by system id.
|
|
194
220
|
*
|
|
@@ -714,6 +740,22 @@ function lintCommand() {
|
|
|
714
740
|
// decides which vocabulary a tree is held to.
|
|
715
741
|
vocabulary: NOTE_VOCABULARY,
|
|
716
742
|
references: argv.references,
|
|
743
|
+
// How this repository addresses a section landing, and the
|
|
744
|
+
// sections that can exist — the open vocabulary a
|
|
745
|
+
// `README`'s `subType` is read against (#197, #200).
|
|
746
|
+
// Passed in for the same reason the schemas are: the
|
|
747
|
+
// linter checks a note against what it is handed.
|
|
748
|
+
//
|
|
749
|
+
// The types come from the **specification**, not from
|
|
750
|
+
// `schemas`: a landing's `subType` is an address, and a
|
|
751
|
+
// type the format declares is a section whether or not
|
|
752
|
+
// this build carries a schema to check its notes' fields
|
|
753
|
+
// with. Reading `schemas` here would refuse `Lore/README.md`
|
|
754
|
+
// for a reason that is not about addresses, and report one
|
|
755
|
+
// gap twice in two vocabularies.
|
|
756
|
+
landing: config.publish.address.landing,
|
|
757
|
+
types: [...loadContentFormat().types.keys()],
|
|
758
|
+
sections: declaredSections(config),
|
|
717
759
|
});
|
|
718
760
|
|
|
719
761
|
// What the builders emit, against what the receiving system
|
|
@@ -1022,8 +1064,7 @@ function linksCommand() {
|
|
|
1022
1064
|
const {
|
|
1023
1065
|
deadAnchors,
|
|
1024
1066
|
deadAddresses,
|
|
1025
|
-
|
|
1026
|
-
aliasCollisions,
|
|
1067
|
+
unlabelledLinks,
|
|
1027
1068
|
frontmatterLinks,
|
|
1028
1069
|
homepageLinks,
|
|
1029
1070
|
usedManifest,
|
|
@@ -1039,62 +1080,19 @@ function linksCommand() {
|
|
|
1039
1080
|
`heading in ${d.dest.rel} declares`,
|
|
1040
1081
|
});
|
|
1041
1082
|
}
|
|
1042
|
-
//
|
|
1043
|
-
//
|
|
1044
|
-
// because the corrections differ.
|
|
1045
|
-
|
|
1083
|
+
// Every link is an address (#180) and every address must
|
|
1084
|
+
// resolve (#184), so all of these are errors — but they read
|
|
1085
|
+
// differently because the corrections differ. The wording comes
|
|
1086
|
+
// from the shared table, so the checker cannot describe a
|
|
1087
|
+
// defect differently from the build that also refuses it.
|
|
1088
|
+
for (const d of [...deadAddresses, ...unlabelledLinks]) {
|
|
1046
1089
|
emitDiagnostic({
|
|
1047
1090
|
file: d.note.file,
|
|
1048
1091
|
...positionOfLiteral(d.note.raw, d.text, d.occurrence),
|
|
1049
1092
|
severity: "error",
|
|
1050
|
-
message:
|
|
1051
|
-
d.reason === "not-an-address" ?
|
|
1052
|
-
`"${d.target}" is written as an address — the ` +
|
|
1053
|
-
`"|" says so — but it is not one; write ` +
|
|
1054
|
-
`[[type-shortcode|Text]], or drop the "|" to ` +
|
|
1055
|
-
`name it as an alias within this note's type`
|
|
1056
|
-
: d.reason === "unknown-type" ?
|
|
1057
|
-
`address [[${d.target}]] names no known ` + `content type`
|
|
1058
|
-
: `dead address [[${d.target}]] — no document ` + `has that identity`,
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
// An alias that names nothing may be a worldbuilding
|
|
1062
|
-
// placeholder — a long-standing convention in the setting
|
|
1063
|
-
// trees — so it is reported and does not fail the build. The
|
|
1064
|
-
// ambiguous case is the collision below, reported at its
|
|
1065
|
-
// claimants rather than here.
|
|
1066
|
-
for (const d of deadAliases) {
|
|
1067
|
-
emitDiagnostic({
|
|
1068
|
-
file: d.note.file,
|
|
1069
|
-
...positionOfLiteral(d.note.raw, d.text, d.occurrence),
|
|
1070
|
-
severity: "warning",
|
|
1071
|
-
message:
|
|
1072
|
-
d.ambiguous ?
|
|
1073
|
-
`alias [[${d.target}]] is claimed by ` +
|
|
1074
|
-
`${d.claimants.length} ${d.note.type} notes, ` +
|
|
1075
|
-
`so it names none of them; address the ` +
|
|
1076
|
-
`intended one as [[type-shortcode|Text]]`
|
|
1077
|
-
: `unresolved alias [[${d.target}]] — no ` +
|
|
1078
|
-
`${d.note.type} note claims that name`,
|
|
1093
|
+
message: linkFindingMessage(d),
|
|
1079
1094
|
});
|
|
1080
1095
|
}
|
|
1081
|
-
// Reported once per claimant, at the claimant, because the
|
|
1082
|
-
// note that merely cites an ambiguous alias is innocent (#13).
|
|
1083
|
-
for (const c of aliasCollisions) {
|
|
1084
|
-
const others = c.claimants.map((n) => n.rel).join(", ");
|
|
1085
|
-
for (const claimant of c.claimants) {
|
|
1086
|
-
emitDiagnostic({
|
|
1087
|
-
file: claimant.file,
|
|
1088
|
-
...positionInFrontmatter(claimant.raw, "aliases", c.alias),
|
|
1089
|
-
severity: "error",
|
|
1090
|
-
message:
|
|
1091
|
-
`alias "${c.alias}" is claimed by ` +
|
|
1092
|
-
`${c.claimants.length} ${c.type} notes ` +
|
|
1093
|
-
`(${others}), so [[${c.alias}]] names none of ` +
|
|
1094
|
-
`them; rename all but one`,
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
1096
|
for (const f of frontmatterLinks) {
|
|
1099
1097
|
emitDiagnostic({
|
|
1100
1098
|
file: f.note.file,
|
|
@@ -1119,12 +1117,10 @@ function linksCommand() {
|
|
|
1119
1117
|
});
|
|
1120
1118
|
}
|
|
1121
1119
|
|
|
1122
|
-
// Warnings are reported and do not fail: a dead alias may be a
|
|
1123
|
-
// note not yet written, which is the placeholder convention.
|
|
1124
1120
|
const failures =
|
|
1125
1121
|
deadAnchors.length +
|
|
1126
1122
|
deadAddresses.length +
|
|
1127
|
-
|
|
1123
|
+
unlabelledLinks.length +
|
|
1128
1124
|
frontmatterLinks.length +
|
|
1129
1125
|
homepageLinks.length;
|
|
1130
1126
|
if (failures) {
|
|
@@ -1132,21 +1128,14 @@ function linksCommand() {
|
|
|
1132
1128
|
process.exitCode = 1;
|
|
1133
1129
|
} else {
|
|
1134
1130
|
log.info(
|
|
1135
|
-
`${index.notes.length} notes: every
|
|
1136
|
-
`
|
|
1137
|
-
`(${usedManifest.size}
|
|
1138
|
-
`via manifest), no
|
|
1131
|
+
`${index.notes.length} notes: every link is a labelled ` +
|
|
1132
|
+
`address, every anchor link lands and every ` +
|
|
1133
|
+
`address resolves (${usedManifest.size} ` +
|
|
1134
|
+
`cross-package reference(s) via manifest), no ` +
|
|
1139
1135
|
`wikilink in frontmatter, every homepage address ` +
|
|
1140
1136
|
`resolvable.`,
|
|
1141
1137
|
);
|
|
1142
1138
|
}
|
|
1143
|
-
if (deadAliases.length) {
|
|
1144
|
-
log.warn(
|
|
1145
|
-
`${deadAliases.length} unresolved alias(es) — a bare ` +
|
|
1146
|
-
`[[Name]] naming no note may be a placeholder, so ` +
|
|
1147
|
-
`these are reported rather than failed.`,
|
|
1148
|
-
);
|
|
1149
|
-
}
|
|
1150
1139
|
} catch (err) {
|
|
1151
1140
|
reportFailure(err);
|
|
1152
1141
|
process.exitCode = 1;
|
|
@@ -1275,16 +1264,13 @@ function siteCommand() {
|
|
|
1275
1264
|
`page verbatim, and reaches the reader as brackets`,
|
|
1276
1265
|
});
|
|
1277
1266
|
}
|
|
1278
|
-
for (const f of gates.
|
|
1267
|
+
for (const f of gates.addressErrors) {
|
|
1279
1268
|
emitDiagnostic({
|
|
1280
1269
|
file: f.file,
|
|
1281
1270
|
severity: "error",
|
|
1282
1271
|
message: `cannot derive a URL: ${f.reason}`,
|
|
1283
1272
|
});
|
|
1284
1273
|
}
|
|
1285
|
-
for (const c of gates.collisions) {
|
|
1286
|
-
log.error(`${c.url} claimed by ${c.sources.join(", ")}`);
|
|
1287
|
-
}
|
|
1288
1274
|
for (const s of gates.staleManifests) {
|
|
1289
1275
|
emitDiagnostic({
|
|
1290
1276
|
file: path.join(loadPackConfig().paths.manifests, `${s.package}.json`),
|
|
@@ -1310,8 +1296,21 @@ function siteCommand() {
|
|
|
1310
1296
|
for (const e of result.tableErrors) {
|
|
1311
1297
|
log.error(`bad content table: ${e.reason} (${e.source})`);
|
|
1312
1298
|
}
|
|
1299
|
+
// Reported the way the pack build reports the very same
|
|
1300
|
+
// finding: `file:line:column: error: message`, path first, and
|
|
1301
|
+
// the message from the shared table (#184). It used to be a
|
|
1302
|
+
// `log.error` whose timestamp prefix sat where a parser reads
|
|
1303
|
+
// the path from, and whose text named a `reason` code rather
|
|
1304
|
+
// than saying what to do — so one authored link produced a
|
|
1305
|
+
// machine-readable diagnostic from one build and prose from
|
|
1306
|
+
// another.
|
|
1313
1307
|
for (const e of result.wikiErrors) {
|
|
1314
|
-
|
|
1308
|
+
emitDiagnostic({
|
|
1309
|
+
file: e.file,
|
|
1310
|
+
...positionOfLiteral(readRawNote(e.file), e.link, e.occurrence),
|
|
1311
|
+
severity: "error",
|
|
1312
|
+
message: linkFindingMessage(e),
|
|
1313
|
+
});
|
|
1315
1314
|
}
|
|
1316
1315
|
if (result.tableErrors.length || result.wikiErrors.length) {
|
|
1317
1316
|
process.exitCode = 1;
|
|
@@ -1319,11 +1318,16 @@ function siteCommand() {
|
|
|
1319
1318
|
}
|
|
1320
1319
|
|
|
1321
1320
|
if (result.manifests && !result.manifests.complete) {
|
|
1321
|
+
// Not a softening any more (#184): an address into one of
|
|
1322
|
+
// these packages fails like any other that resolves
|
|
1323
|
+
// nowhere. The warning names them so an author meeting that
|
|
1324
|
+
// failure knows the fix may be to vendor a manifest rather
|
|
1325
|
+
// than to correct a shortcode.
|
|
1322
1326
|
log.warn(
|
|
1323
|
-
`
|
|
1324
|
-
|
|
1325
|
-
`
|
|
1326
|
-
`
|
|
1327
|
+
`no link manifest vendored for ` +
|
|
1328
|
+
`${result.manifests.missing.join(", ")} — an ` +
|
|
1329
|
+
`address into one of those packages resolves ` +
|
|
1330
|
+
`nowhere and fails the build.`,
|
|
1327
1331
|
);
|
|
1328
1332
|
}
|
|
1329
1333
|
|
package/content-config.mjs
CHANGED
|
@@ -210,6 +210,34 @@ export function publishesContentPages(config) {
|
|
|
210
210
|
return config.publish.site === "content";
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Every URL section this repository names, in declaration order (#197).
|
|
215
|
+
*
|
|
216
|
+
* A section is *declared* by describing it: `site.sections` for one whose
|
|
217
|
+
* landing this build generates, `site.readmeSections` for one whose landing is
|
|
218
|
+
* a `README`. Between them they are the open set of addresses a repository
|
|
219
|
+
* says it publishes under — which is what a `README` landing's `subType` names,
|
|
220
|
+
* since `sectionOf` reads that field as the section rather than as a genre.
|
|
221
|
+
*
|
|
222
|
+
* Read from the same two maps the site build renders each landing from, so the
|
|
223
|
+
* set a note is checked against and the set a landing is written from cannot
|
|
224
|
+
* come to disagree. A repository that describes no section declares none, and
|
|
225
|
+
* the answer is empty rather than a guess assembled from the tree.
|
|
226
|
+
*
|
|
227
|
+
* @param {{site: {sections: object, readmeSections: object}}} config - A
|
|
228
|
+
* resolved configuration.
|
|
229
|
+
* @returns {readonly string[]} The section names, deduplicated.
|
|
230
|
+
*/
|
|
231
|
+
export function declaredSections(config) {
|
|
232
|
+
const site = config?.site ?? {};
|
|
233
|
+
return Object.freeze([
|
|
234
|
+
...new Set([
|
|
235
|
+
...Object.keys(site.sections ?? {}),
|
|
236
|
+
...Object.keys(site.readmeSections ?? {}),
|
|
237
|
+
]),
|
|
238
|
+
]);
|
|
239
|
+
}
|
|
240
|
+
|
|
213
241
|
/**
|
|
214
242
|
* @typedef {"systems" | "modules"} PackageKind
|
|
215
243
|
*/
|
package/docs/content-format.md
CHANGED
|
@@ -74,8 +74,8 @@ silently become a theme parameter, which is exactly why these cannot live
|
|
|
74
74
|
together.
|
|
75
75
|
|
|
76
76
|
**Tags are open, except the ones that classify.** `tags:` shares the top level's
|
|
77
|
-
openness: a tag naming a theme
|
|
78
|
-
`byzaria`, `
|
|
77
|
+
openness: a tag naming a theme or a region — `underworld`,
|
|
78
|
+
`byzaria`, `riverlands` — is the author's own and this build has no opinion about it.
|
|
79
79
|
A tag that classifies the subject is different, because something queries it. A
|
|
80
80
|
settlement tagged `village` appears in the list of villages and an untagged one
|
|
81
81
|
does not, so `vilage` does not merely look wrong: it removes the note from an
|
|
@@ -91,6 +91,17 @@ is declared, so a near miss is a finding that names what you probably meant.
|
|
|
91
91
|
| **being station** | `being` | `tradesfolk`, `common-folk`, `soldiery`, `administration`, `clergy`, `mages`, `underworld`, `dependents`, `guilded`, `unguilded` |
|
|
92
92
|
| **state** | any | `draft` |
|
|
93
93
|
|
|
94
|
+
**`draft` is the one tag either build reads.** A note tagged `draft` exists so a
|
|
95
|
+
link into it is not dead, and a link into it renders marked — as
|
|
96
|
+
`<span class="sohl-draft-link" title="Draft — not yet written">…</span>` in a
|
|
97
|
+
compiled journal and on the website alike, with the appearance supplied by the
|
|
98
|
+
consuming system's stylesheet or the site theme. **Nothing else changes**: the
|
|
99
|
+
note compiles, validates, publishes and resolves exactly as any other, and it
|
|
100
|
+
stays in the packs, in the link manifest and on the site. That is what separates
|
|
101
|
+
the tag from the retired `draft:` field, whose whole effect was to move a note
|
|
102
|
+
from published to unresolvable without saying so — and which is refused, by name,
|
|
103
|
+
if you write it.
|
|
104
|
+
|
|
94
105
|
**The group's scope is what makes the check work.** A group names the types it
|
|
95
106
|
applies to, and a place's kinds are only ever checked on a place. Without that
|
|
96
107
|
the rule is wrong on every note it touches: `azravan` on a faith, `barter` on an
|
|
@@ -235,49 +246,78 @@ resolution with nothing to say so.
|
|
|
235
246
|
`type/shortcode` with a slash is the legacy form, still resolved so links written
|
|
236
247
|
before the vault migrated do not silently die. A slash is _unconditionally_ an
|
|
237
248
|
address separator — pipe or no pipe — so an unknown type before one is an error
|
|
238
|
-
rather than
|
|
249
|
+
rather than something to guess at.
|
|
239
250
|
|
|
240
|
-
####
|
|
251
|
+
#### Every link is an address, and every link carries a label
|
|
241
252
|
|
|
242
|
-
There
|
|
253
|
+
There is one namespace, and the pipe is required:
|
|
243
254
|
|
|
244
255
|
| written | resolved as | displays |
|
|
245
256
|
| -------------------- | ----------- | -------------------------- |
|
|
246
|
-
| `[[Alias]]` | an alias | the alias, as written |
|
|
247
257
|
| `[[WikiLink\|]]` | an address | the target note's own name |
|
|
248
258
|
| `[[WikiLink\|Text]]` | an address | `Text` |
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
####
|
|
259
|
+
| `[[Name]]` | nothing | a finding |
|
|
260
|
+
|
|
261
|
+
**A link written without a label addresses nothing** (#180), and the correction
|
|
262
|
+
is always the same: write `[[type-shortcode|Text]]`.
|
|
263
|
+
|
|
264
|
+
The bare form used to name an **alias** — a note's own display name, or one of
|
|
265
|
+
the names it listed in `aliases:` — looked up within the citing note's type. It
|
|
266
|
+
was measured before it was retired, and the namespace was empty in practice:
|
|
267
|
+
across 8,305 wikilinks in three content trees, **not one** bare link resolved to
|
|
268
|
+
a note. What the index behind it did do was fold every note's `name.full` into
|
|
269
|
+
itself, so two notes of one type could not share a display name — a rules page
|
|
270
|
+
and a user-guide page both called "Gear" were a build failure whose every
|
|
271
|
+
available fix moved a published URL (#179).
|
|
272
|
+
|
|
273
|
+
The top-level `aliases:` that fed it is **retired** and refused. The nested
|
|
274
|
+
`name.aliases:` is **not**: it is reserved for a use that does not exist yet, so
|
|
275
|
+
it is permitted and read by nothing — no index, no resolver, no lint rule, no
|
|
276
|
+
derived address. A note carrying one behaves exactly as one without it.
|
|
277
|
+
|
|
278
|
+
Requiring the label is also what makes positional parsing safe. Note names
|
|
279
|
+
contain hyphens — `Grukar-ahk` is a name, not a `Grukar` of type `ahk` — so a
|
|
280
|
+
target that does not parse as an address is reported as one that does not, rather
|
|
281
|
+
than split at an arbitrary place or quietly looked up somewhere else.
|
|
282
|
+
|
|
283
|
+
The **empty** label is not a way of writing no label. It says _address this
|
|
284
|
+
target, and show whatever it calls itself_ — so a note renamed later takes its new
|
|
285
|
+
name at every citation with no link edited. `[[x|]]` is labelled; `[[x]]` is not.
|
|
286
|
+
|
|
287
|
+
The link part may still be an anchor: `[[#slug|Text]]` addresses a section of the
|
|
288
|
+
page it is written on. It is the label that is required, not a target.
|
|
289
|
+
|
|
290
|
+
#### Every address resolves, and every build says so the same way
|
|
291
|
+
|
|
292
|
+
An address that names no note **fails the build** (#184) — in the link checker,
|
|
293
|
+
in the pack compilers and in the site build alike.
|
|
294
|
+
|
|
295
|
+
It was a warning in the checker and, in the site build, nothing at all while any
|
|
296
|
+
linkable package had no vendored manifest. The reasoning was that `[[Sunless
|
|
297
|
+
Vault]]` might be a placeholder for a note somebody meant to write. That was a
|
|
298
|
+
property of the **bare** form, which is retired, and the intent behind it has a
|
|
299
|
+
real spelling now: a note tagged `draft` exists, resolves, compiles and
|
|
300
|
+
publishes, and a link to it renders visibly marked (#183). So an address landing
|
|
301
|
+
nowhere is a typo or an omission, and both want fixing.
|
|
302
|
+
|
|
303
|
+
There are six ways a link can fail, and each is one **error** with one message
|
|
304
|
+
wherever it is met:
|
|
305
|
+
|
|
306
|
+
| finding | what it means | the fix |
|
|
307
|
+
| ---------------- | ---------------------------------------------- | ----------------------------------------- |
|
|
308
|
+
| `unlabelled` | no `\|`, so the link addresses nothing | write `[[type-shortcode\|Text]]` |
|
|
309
|
+
| `not-an-address` | labelled, but the target is not an address | write the address, not the name |
|
|
310
|
+
| `unknown-type` | qualified, but names no type this build knows | correct the type segment |
|
|
311
|
+
| `unresolved` | parses as an address; nothing publishes it | fix the shortcode, or vendor the manifest |
|
|
312
|
+
| `ambiguous` | more than one package publishes the short form | write `[[package-type-shortcode\|Text]]` |
|
|
313
|
+
| `unknown-anchor` | the address resolves; the `#section` does not | correct the anchor |
|
|
314
|
+
|
|
315
|
+
The vocabulary and the messages live in one module (`engine/wikilink-syntax.mjs`)
|
|
316
|
+
precisely because an author meets whichever build ran first. Three resolvers read
|
|
317
|
+
one authored link; they must not describe the same mistake in three ways, and
|
|
318
|
+
they must never disagree about whether it is a mistake at all.
|
|
319
|
+
|
|
320
|
+
#### In frontmatter, a link is a bare address
|
|
281
321
|
|
|
282
322
|
A `WikiLink` **field** takes the address with no brackets:
|
|
283
323
|
|
|
@@ -292,17 +332,10 @@ not `[[hexhodai]]`. The field is declared as a `WikiLink`, so the schema already
|
|
|
292
332
|
knows the value is an address and reads it as one; brackets would be punctuation
|
|
293
333
|
the reader has to strip before it can do anything.
|
|
294
334
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
The reason is that frontmatter is structure rather than prose. An alias is an
|
|
301
|
-
authorial convenience for writing a sentence that reads well; a field value is a
|
|
302
|
-
reference something else will compile against, and it should say exactly what it
|
|
303
|
-
points at. There is also nowhere to put the pipe: the distinction body text draws
|
|
304
|
-
with punctuation has no equivalent in a YAML scalar, so the region as a whole
|
|
305
|
-
picks one namespace and keeps it.
|
|
335
|
+
A frontmatter value is parsed by the address grammar above, so a single-segment
|
|
336
|
+
value such as `hexhodai` is a _shortcode_. Frontmatter is structure rather than
|
|
337
|
+
prose: a field value is a reference something else will compile against, and it
|
|
338
|
+
should say exactly what it points at.
|
|
306
339
|
|
|
307
340
|
**The field supplies the type.** Every `WikiLink` field declares the note type it
|
|
308
341
|
targets — `seat` a `place`, `parents` an `affiliation`, `stations` a `lore` — so
|
|
@@ -339,15 +372,10 @@ So the ladder has one rung where the field helps and three where it only checks:
|
|
|
339
372
|
a bare shortcode takes its type from the declaration, and every longer form
|
|
340
373
|
states the type itself and is verified against it.
|
|
341
374
|
|
|
342
|
-
This also settles the alias question on its own. Aliases resolve within the
|
|
343
|
-
_source_ note's type, and a field almost always points at a different type — a
|
|
344
|
-
being's `stations` are `lore` notes — so an alias in a field would be looked up
|
|
345
|
-
in the wrong namespace even where one existed to find.
|
|
346
|
-
|
|
347
375
|
This is enforced rather than merely preferred: the build walks every frontmatter
|
|
348
|
-
value, reports each bracketed link it finds, each value
|
|
349
|
-
|
|
350
|
-
of what `content-build links` reports when it passes.
|
|
376
|
+
value, reports each bracketed link it finds, and reports each value whose
|
|
377
|
+
qualification contradicts its field. A successful run says so — _no wikilink in
|
|
378
|
+
frontmatter_ is part of what `content-build links` reports when it passes.
|
|
351
379
|
|
|
352
380
|
Brackets belong in prose, where a link sits inside a sentence and needs marking
|
|
353
381
|
off from the words around it. A frontmatter value has nothing to be marked off
|
|
@@ -355,21 +383,16 @@ from.
|
|
|
355
383
|
|
|
356
384
|
#### Not yet implemented
|
|
357
385
|
|
|
358
|
-
|
|
386
|
+
One rule in this section is settled but unbuilt, and describes the target rather
|
|
359
387
|
than current behaviour:
|
|
360
388
|
|
|
361
389
|
- **The `<system>` segment.** `readQualifier` reads package, type and shortcode;
|
|
362
390
|
there is no system segment. A four-segment target today parses as
|
|
363
391
|
`package-type-shortcode` with a hyphenated shortcode, or fails.
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
The corpus is close to the rule already. Of 12,056 links, 10,413 are
|
|
370
|
-
`[[type-shortcode|Label]]`, which is correct as written. Two authored links use
|
|
371
|
-
an unpiped multi-segment target, and 69 use a pipe with a note name where an
|
|
372
|
-
address belongs; those are the migration.
|
|
392
|
+
The corpus is close to the rule already. Of 12,056 links, 10,413 are
|
|
393
|
+
`[[type-shortcode|Label]]`, which is correct as written. Two authored links use
|
|
394
|
+
an unpiped multi-segment target, and 69 use a pipe with a note name where an
|
|
395
|
+
address belongs; those are the migration.
|
|
373
396
|
|
|
374
397
|
### What a note produces
|
|
375
398
|
|
package/engine/base-compiler.mjs
CHANGED
|
@@ -77,7 +77,7 @@ import {
|
|
|
77
77
|
} from "./helpers.mjs";
|
|
78
78
|
import { emitDiagnostic } from "./diagnostics.mjs";
|
|
79
79
|
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
80
|
-
import { assertNoDraftField } from "./retired-fields.mjs";
|
|
80
|
+
import { assertNoAliasesField, assertNoDraftField } from "./retired-fields.mjs";
|
|
81
81
|
import { assertTypeNotRetired, packForType } from "./ids.mjs";
|
|
82
82
|
import { carriesSystemBlock } from "./system-block.mjs";
|
|
83
83
|
import { checkAuthoredSystemData, checkEmittedSystemData } from "./schema-check.mjs";
|
|
@@ -770,6 +770,11 @@ export class BasePackCompiler {
|
|
|
770
770
|
// - `draft:` (#69): it excluded the note from the packs, the
|
|
771
771
|
// manifest and the site, and no checker reported the links that
|
|
772
772
|
// left dangling.
|
|
773
|
+
// - `aliases:` (#180): it fed the alias index, which the bare
|
|
774
|
+
// `[[Alias]]` form was looked up in; the form is retired, so the
|
|
775
|
+
// list has no reader left. The nested `name.aliases` is a
|
|
776
|
+
// different field and is **not** refused — it is reserved, and
|
|
777
|
+
// deliberately neither read nor validated.
|
|
773
778
|
//
|
|
774
779
|
// Both are reported and counted — never skipped, which is how a
|
|
775
780
|
// tree naming a package nothing answers to used to compile zero
|
|
@@ -778,6 +783,7 @@ export class BasePackCompiler {
|
|
|
778
783
|
try {
|
|
779
784
|
assertNoDeclaredPackage(fm, { absPath });
|
|
780
785
|
assertNoDraftField(fm, { absPath });
|
|
786
|
+
assertNoAliasesField(fm, { absPath });
|
|
781
787
|
} catch (err) {
|
|
782
788
|
stats.declined++;
|
|
783
789
|
this.errorCount++;
|