@heroiclands/package-build 22.1.0 → 22.1.1
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 +15 -0
- package/CONTENT.md +22 -0
- package/bin/content-build.mjs +29 -3
- package/content-config.mjs +30 -1
- package/docs/api.md +25 -23
- package/docs/configuration.md +57 -36
- package/engine/content-links.mjs +19 -9
- package/engine/helpers.mjs +2 -1
- package/engine/metadata-index.mjs +32 -0
- package/engine/note-vocabulary.mjs +20 -0
- package/engine/site-build.mjs +14 -3
- package/engine/site-config.mjs +47 -6
- package/engine/site-index.mjs +10 -1
- package/engine/web-wikilinks.mjs +16 -6
- package/engine/wikilink-syntax.mjs +10 -0
- package/engine/wikilinks.mjs +34 -7
- package/package.json +1 -1
- package/types/content-config.d.mts +11 -0
- package/types/engine/content-links.d.mts +3 -2
- package/types/engine/metadata-index.d.mts +22 -0
- package/types/engine/note-vocabulary.d.mts +12 -0
- package/types/engine/site-build.d.mts +5 -1
- package/types/engine/site-config.d.mts +18 -5
- package/types/engine/site-index.d.mts +6 -1
- package/types/engine/web-wikilinks.d.mts +9 -5
- package/types/engine/wikilink-syntax.d.mts +3 -0
- package/types/engine/wikilinks.d.mts +16 -5
|
@@ -496,6 +496,26 @@ export function hasTag(fm, tag) {
|
|
|
496
496
|
return false;
|
|
497
497
|
}
|
|
498
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Whether a note carries any `tags:` at all, however authored.
|
|
501
|
+
*
|
|
502
|
+
* The one question the site build asks of tags in aggregate — whether the
|
|
503
|
+
* tree publishes taxonomy pages — rather than about a particular tag. Reads
|
|
504
|
+
* `tags` and `tag` exactly as {@link hasTag} does, and treats an empty list
|
|
505
|
+
* or a blank string as carrying none.
|
|
506
|
+
*
|
|
507
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
508
|
+
* @returns {boolean} Whether the note carries at least one tag.
|
|
509
|
+
*/
|
|
510
|
+
export function hasAnyTag(fm) {
|
|
511
|
+
const raw = fm?.tags ?? fm?.tag;
|
|
512
|
+
if (raw == null) return false;
|
|
513
|
+
for (const entry of Array.isArray(raw) ? raw : [raw]) {
|
|
514
|
+
if (typeof entry === "string" && entry.trim() !== "") return true;
|
|
515
|
+
}
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
|
|
499
519
|
/**
|
|
500
520
|
* Whether a note is tagged as an unfinished **draft**.
|
|
501
521
|
*
|
package/engine/site-build.mjs
CHANGED
|
@@ -57,13 +57,14 @@ import { renderImageFigures } from "./content-images.mjs";
|
|
|
57
57
|
import { pathnameProblem, resolvePathname } from "./pathnames.mjs";
|
|
58
58
|
import { buildSiteIndex, resolveInfoboxRef, wikiContext } from "./site-index.mjs";
|
|
59
59
|
import { frontmatterWikilinks, resolveWebWikilinks } from "./web-wikilinks.mjs";
|
|
60
|
-
import { loadForeignIndexes } from "./metadata-index.mjs";
|
|
60
|
+
import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs";
|
|
61
61
|
import { noteInfoboxes } from "./infobox-registry.mjs";
|
|
62
62
|
import { formatUnaddressableFinding, unaddressableForeignPackages } from "./metadata-index.mjs";
|
|
63
63
|
import { deriveBeingInfo, isBeing } from "../sohl/being-info.mjs";
|
|
64
64
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
65
65
|
import { routerFor } from "./pack-router.mjs";
|
|
66
66
|
import { searchableFrontmatter } from "./note-package.mjs";
|
|
67
|
+
import { hasAnyTag } from "./note-vocabulary.mjs";
|
|
67
68
|
// The corpus, from the one pass that derives it.
|
|
68
69
|
import { indexRecordsFor } from "./content-index.mjs";
|
|
69
70
|
import { isNoteRecord, noteFile } from "./index-records.mjs";
|
|
@@ -476,7 +477,10 @@ export function siteGates(pages, findings, { config }) {
|
|
|
476
477
|
out.unaddressable = unaddressableForeignPackages(foreign.index);
|
|
477
478
|
if (out.unaddressable.length) return out;
|
|
478
479
|
|
|
479
|
-
const index = buildSiteIndex(pages, {
|
|
480
|
+
const index = buildSiteIndex(pages, {
|
|
481
|
+
foreignIndex: foreign.index,
|
|
482
|
+
noIndexPackages: noContentIndexPackages(config),
|
|
483
|
+
});
|
|
480
484
|
out.conflicts = index.conflicts;
|
|
481
485
|
if (out.conflicts.length) return out;
|
|
482
486
|
|
|
@@ -1043,7 +1047,10 @@ export function resolveSitePass(name, options) {
|
|
|
1043
1047
|
* `sql` directive with none prepared is a table error: nothing here runs a
|
|
1044
1048
|
* query.
|
|
1045
1049
|
* @returns {{gates: object, stats: object|null, tableErrors: object[],
|
|
1046
|
-
* wikiErrors: object[], imageErrors: object[], manifests: object|null
|
|
1050
|
+
* wikiErrors: object[], imageErrors: object[], manifests: object|null,
|
|
1051
|
+
* hasTags: boolean}} `hasTags` is whether any note the walk read carries
|
|
1052
|
+
* `tags:` — what {@link module:engine/site-config.hugoConfig} reads to
|
|
1053
|
+
* decide whether the site emits taxonomy pages.
|
|
1047
1054
|
*/
|
|
1048
1055
|
export function buildSite({ config, sqlTables } = {}) {
|
|
1049
1056
|
const resolved = config ?? loadPackConfig();
|
|
@@ -1142,6 +1149,7 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1142
1149
|
wikiErrors: [],
|
|
1143
1150
|
imageErrors: [],
|
|
1144
1151
|
stats: null,
|
|
1152
|
+
hasTags: homepages.some((p) => hasAnyTag(p.fm)),
|
|
1145
1153
|
};
|
|
1146
1154
|
}
|
|
1147
1155
|
|
|
@@ -1164,6 +1172,7 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1164
1172
|
landings: 0,
|
|
1165
1173
|
out: homeRoot,
|
|
1166
1174
|
},
|
|
1175
|
+
hasTags: homepages.some((p) => hasAnyTag(p.fm)),
|
|
1167
1176
|
};
|
|
1168
1177
|
}
|
|
1169
1178
|
|
|
@@ -1211,6 +1220,7 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1211
1220
|
tableErrors: [],
|
|
1212
1221
|
wikiErrors: [],
|
|
1213
1222
|
imageErrors: [],
|
|
1223
|
+
hasTags: [...pages, ...homepageEntries].some((p) => hasAnyTag(p.fm)),
|
|
1214
1224
|
};
|
|
1215
1225
|
}
|
|
1216
1226
|
|
|
@@ -1269,6 +1279,7 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1269
1279
|
landings,
|
|
1270
1280
|
out,
|
|
1271
1281
|
},
|
|
1282
|
+
hasTags: [...pages, ...homepageEntries].some((p) => hasAnyTag(p.fm)),
|
|
1272
1283
|
};
|
|
1273
1284
|
}
|
|
1274
1285
|
|
package/engine/site-config.mjs
CHANGED
|
@@ -83,14 +83,39 @@ export const BRAND = Object.freeze({
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
* The kinds
|
|
86
|
+
* The kinds a site with no tagged notes renders.
|
|
87
87
|
*
|
|
88
88
|
* A section exists only where `site.sections` declares one, so a tree holding
|
|
89
|
-
* only the homepage emits nothing beyond it;
|
|
90
|
-
* empty shells
|
|
89
|
+
* only the homepage emits nothing beyond it; a taxonomy nobody's notes fill
|
|
90
|
+
* and a feed would be empty shells. A site whose notes carry `tags:` emits
|
|
91
|
+
* `taxonomy` and `term` after all — see {@link hugoConfig} — but `RSS` is
|
|
92
|
+
* disabled either way: nothing here publishes a feed.
|
|
91
93
|
*/
|
|
92
94
|
export const DISABLE_KINDS = Object.freeze(["taxonomy", "term", "RSS"]);
|
|
93
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The kinds a site with at least one tagged note renders — everything but
|
|
98
|
+
* `RSS`.
|
|
99
|
+
*/
|
|
100
|
+
const DISABLE_KINDS_TAGGED = Object.freeze(["RSS"]);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The single taxonomy a tagged site declares.
|
|
104
|
+
*
|
|
105
|
+
* Only `tag` — Hugo's default pair also declares `category`, which nothing
|
|
106
|
+
* here authors and which would publish an empty `/categories/`.
|
|
107
|
+
*/
|
|
108
|
+
const TAXONOMIES = Object.freeze({ tag: "tags" });
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The taxonomy output formats a tagged site declares — `HTML` only, so no
|
|
112
|
+
* feed is produced for `/tags/` or a single tag.
|
|
113
|
+
*/
|
|
114
|
+
const TAXONOMY_OUTPUTS = Object.freeze({
|
|
115
|
+
taxonomy: Object.freeze(["HTML"]),
|
|
116
|
+
term: Object.freeze(["HTML"]),
|
|
117
|
+
});
|
|
118
|
+
|
|
94
119
|
/**
|
|
95
120
|
* The markup settings the toolchain's own output requires.
|
|
96
121
|
*
|
|
@@ -359,11 +384,14 @@ function deepMerge(base, overrides) {
|
|
|
359
384
|
* @param {string} [options.description] - `package.json`'s `description`.
|
|
360
385
|
* @param {readonly NavigationEntry[]} options.navigation - The navigation.
|
|
361
386
|
* @param {string} options.themesDir - From {@link resolveThemesDir}.
|
|
387
|
+
* @param {boolean} [options.hasTags] - Whether any note the site build walked
|
|
388
|
+
* carries `tags:`, from {@link module:engine/site-build.buildSite}'s
|
|
389
|
+
* `hasTags`. Defaults to `false` — no tagged note, no taxonomy pages.
|
|
362
390
|
* @returns {Record<string, any>} The configuration Hugo reads.
|
|
363
391
|
* @throws {TypeError} When `homepage` fails `checkHomepage`, or the
|
|
364
392
|
* configuration declares no `packageBuild.manifest.title`.
|
|
365
393
|
*/
|
|
366
|
-
export function hugoConfig({ config, description, navigation, themesDir }) {
|
|
394
|
+
export function hugoConfig({ config, description, navigation, themesDir, hasTags = false }) {
|
|
367
395
|
checkHomepage(config.homepage, config.contentPackage);
|
|
368
396
|
|
|
369
397
|
const title = config.packageBuild?.manifest?.title;
|
|
@@ -391,11 +419,18 @@ export function hugoConfig({ config, description, navigation, themesDir }) {
|
|
|
391
419
|
themesDir,
|
|
392
420
|
theme: THEME,
|
|
393
421
|
contentDir: path.posix.relative(HUGO_SOURCE, HUGO_CONTENT),
|
|
394
|
-
disableKinds: [...DISABLE_KINDS],
|
|
422
|
+
disableKinds: hasTags ? [...DISABLE_KINDS_TAGGED] : [...DISABLE_KINDS],
|
|
395
423
|
params,
|
|
396
424
|
markup: structuredClone(MARKUP),
|
|
397
425
|
menu: { main: menuEntries(navigation) },
|
|
398
426
|
};
|
|
427
|
+
if (hasTags) {
|
|
428
|
+
generated.taxonomies = { ...TAXONOMIES };
|
|
429
|
+
generated.outputs = {
|
|
430
|
+
taxonomy: [...TAXONOMY_OUTPUTS.taxonomy],
|
|
431
|
+
term: [...TAXONOMY_OUTPUTS.term],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
399
434
|
return deepMerge(generated, config.site.hugo);
|
|
400
435
|
}
|
|
401
436
|
|
|
@@ -434,15 +469,21 @@ function packageDescription(rootDir) {
|
|
|
434
469
|
* intact.
|
|
435
470
|
*
|
|
436
471
|
* @param {object} config - The resolved build configuration.
|
|
472
|
+
* @param {object} [options] - Options.
|
|
473
|
+
* @param {boolean} [options.hasTags] - Whether any note the site build walked
|
|
474
|
+
* carries `tags:`. Defaults to `false`, so a caller generating the
|
|
475
|
+
* configuration before the walk (to fail fast on a missing source) gets the
|
|
476
|
+
* untagged shape; pass the site build's own `hasTags` once it is known.
|
|
437
477
|
* @returns {Record<string, any>} The configuration Hugo reads.
|
|
438
478
|
* @throws {Error} When any source is missing or wrong.
|
|
439
479
|
*/
|
|
440
|
-
export function generateHugoConfig(config) {
|
|
480
|
+
export function generateHugoConfig(config, { hasTags = false } = {}) {
|
|
441
481
|
return hugoConfig({
|
|
442
482
|
config,
|
|
443
483
|
description: packageDescription(config.rootDir),
|
|
444
484
|
navigation: readCachedNavigation(config),
|
|
445
485
|
themesDir: resolveThemesDir(config.rootDir),
|
|
486
|
+
hasTags,
|
|
446
487
|
});
|
|
447
488
|
}
|
|
448
489
|
|
package/engine/site-index.mjs
CHANGED
|
@@ -166,9 +166,16 @@ function mergeForeign(index, foreignIndex) {
|
|
|
166
166
|
* @param {Map<string, {package: string, type?: string}>} [options.foreignIndex]
|
|
167
167
|
* The merged index from `loadForeignIndexes`. Omit when the build publishes
|
|
168
168
|
* no cross-package links.
|
|
169
|
+
* @param {Set<string>} [options.noIndexPackages] - Packages declared
|
|
170
|
+
* `contentIndex: false` — a Foundry dependency only, with no fetched index.
|
|
171
|
+
* A link naming one fails naming the key, rather than reading as prose or an
|
|
172
|
+
* ordinary dead address.
|
|
169
173
|
* @returns {SiteIndex} The index, and what could not be addressed unambiguously.
|
|
170
174
|
*/
|
|
171
|
-
export function buildSiteIndex(
|
|
175
|
+
export function buildSiteIndex(
|
|
176
|
+
entries,
|
|
177
|
+
{ foreignIndex = new Map(), noIndexPackages = new Set() } = {},
|
|
178
|
+
) {
|
|
172
179
|
const index = new Map();
|
|
173
180
|
const contentTypes = new Set();
|
|
174
181
|
const sections = new Set();
|
|
@@ -298,6 +305,7 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
|
298
305
|
contentTypes,
|
|
299
306
|
sections,
|
|
300
307
|
packages,
|
|
308
|
+
noIndexPackages,
|
|
301
309
|
refIndex,
|
|
302
310
|
conflicts,
|
|
303
311
|
};
|
|
@@ -347,6 +355,7 @@ export function wikiContext(
|
|
|
347
355
|
sections: built.sections,
|
|
348
356
|
contentTypes: built.contentTypes,
|
|
349
357
|
packages: built.packages,
|
|
358
|
+
noIndexPackages: built.noIndexPackages,
|
|
350
359
|
// The package a link written on this page defaults to when it names
|
|
351
360
|
// none. Taken from the resolved configuration, the same source
|
|
352
361
|
// the index's own addresses are built from, so a bare link cannot
|
package/engine/web-wikilinks.mjs
CHANGED
|
@@ -279,9 +279,10 @@ function isPlainMap(value) {
|
|
|
279
279
|
*
|
|
280
280
|
* **Every target that resolves nowhere fails the build**, and is
|
|
281
281
|
* classified into the vocabulary all three resolvers share — `unlabelled`,
|
|
282
|
-
* `not-an-address`, `unknown-type`, `ambiguous`, `unresolved
|
|
283
|
-
* collected in `ctx.errors`, each carrying
|
|
284
|
-
* `occurrence` so a caller can report the line and
|
|
282
|
+
* `not-an-address`, `unknown-type`, `ambiguous`, `unresolved`,
|
|
283
|
+
* `no-content-index`. Failures are collected in `ctx.errors`, each carrying
|
|
284
|
+
* the authored `link` and its `occurrence` so a caller can report the line and
|
|
285
|
+
* column it sits on.
|
|
285
286
|
*
|
|
286
287
|
* There is deliberately no exception letting a hyphen-form address through while
|
|
287
288
|
* any linkable package had no vendored manifest, since a real cross-package
|
|
@@ -312,9 +313,12 @@ function isPlainMap(value) {
|
|
|
312
313
|
*
|
|
313
314
|
* @param {string} body - The markdown body.
|
|
314
315
|
* @param {object} ctx - `{ index, assets, collide, sections, contentTypes,
|
|
315
|
-
* packages, foreign, type, errors, src, file }`.
|
|
316
|
+
* packages, noIndexPackages, foreign, type, errors, src, file }`.
|
|
316
317
|
* `packages` is every package an address may name, without which the leading
|
|
317
|
-
* package segment of a canonical address reads as an unknown type;
|
|
318
|
+
* package segment of a canonical address reads as an unknown type;
|
|
319
|
+
* `noIndexPackages` is every package declared `contentIndex: false`, so a
|
|
320
|
+
* qualified address naming one fails with `no-content-index` rather than
|
|
321
|
+
* `not-an-address`; `foreign`
|
|
318
322
|
* is the cross-package manifest index; `assets` is the address space an embed
|
|
319
323
|
* resolves against. `src` is the page's display
|
|
320
324
|
* path and `file` the source file a diagnostic should name — absent, `src`
|
|
@@ -402,7 +406,12 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
402
406
|
// The canonical separator has to be resolved, not merely
|
|
403
407
|
// recognised. `null` here means the target is not an address at all,
|
|
404
408
|
// which is a defect: there is no other namespace to try.
|
|
405
|
-
const read = readQualifier(
|
|
409
|
+
const read = readQualifier(
|
|
410
|
+
target,
|
|
411
|
+
ctx.contentTypes ?? new Set(),
|
|
412
|
+
ctx.packages,
|
|
413
|
+
ctx.noIndexPackages,
|
|
414
|
+
);
|
|
406
415
|
const rawKey = target.toLowerCase();
|
|
407
416
|
const hit =
|
|
408
417
|
lookupRead(ctx.index, read, ctx.contentPackage) ??
|
|
@@ -478,6 +487,7 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
478
487
|
// a key: a partial address has no single key to be non-null.
|
|
479
488
|
: (read && !read.reason) || siteAddress ? "unresolved"
|
|
480
489
|
: read?.reason === "unknown-type" ? "unknown-type"
|
|
490
|
+
: read?.reason === "no-content-index" ? "no-content-index"
|
|
481
491
|
// Every link is an address, and this is not one. Distinct from
|
|
482
492
|
// a dead address, because the fix is different: a name has to
|
|
483
493
|
// become an address, not be corrected.
|
|
@@ -184,6 +184,9 @@ export function unlabelledLinkMessage(target) {
|
|
|
184
184
|
* - `unresolved` — parses as an address, and nothing publishes it.
|
|
185
185
|
* - `ambiguous` — more than one package publishes the short address.
|
|
186
186
|
* - `unknown-anchor` — the address resolved, the `#section` it names did not.
|
|
187
|
+
* - `no-content-index` — the address names a package declared
|
|
188
|
+
* `contentIndex: false`, a Foundry dependency only, so no index was fetched
|
|
189
|
+
* for it to resolve against.
|
|
187
190
|
*
|
|
188
191
|
* @type {ReadonlySet<string>}
|
|
189
192
|
*/
|
|
@@ -197,6 +200,7 @@ export const LINK_FINDING_REASONS = Object.freeze(
|
|
|
197
200
|
"unresolved",
|
|
198
201
|
"ambiguous",
|
|
199
202
|
"unknown-anchor",
|
|
203
|
+
"no-content-index",
|
|
200
204
|
]),
|
|
201
205
|
);
|
|
202
206
|
|
|
@@ -312,6 +316,12 @@ export function linkFindingMessage({ reason, target, packages, anchor, type }) {
|
|
|
312
316
|
);
|
|
313
317
|
case "unresolved":
|
|
314
318
|
return unresolvedAddressMessage(target);
|
|
319
|
+
case "no-content-index":
|
|
320
|
+
return (
|
|
321
|
+
`address [[${target}]] names a package declared \`contentIndex: false\` — ` +
|
|
322
|
+
`it is a Foundry dependency only, and no content index was fetched for it, ` +
|
|
323
|
+
`so nothing it publishes can be cited`
|
|
324
|
+
);
|
|
315
325
|
default:
|
|
316
326
|
throw new Error(
|
|
317
327
|
`linkFindingMessage: "${reason}" is not one of ` +
|
package/engine/wikilinks.mjs
CHANGED
|
@@ -203,13 +203,18 @@ export function resolveItemDocType(qualifier, types) {
|
|
|
203
203
|
* @param {Set<string>} types - Every type the content tree contains.
|
|
204
204
|
* @param {Set<string>} [packages] - Every package an address may name. Omitted
|
|
205
205
|
* by callers that resolve within one package, where the form cannot occur.
|
|
206
|
+
* @param {Set<string>} [noIndexPackages] - Packages declared `contentIndex:
|
|
207
|
+
* false` — a Foundry dependency only, with no fetched index. A fully
|
|
208
|
+
* qualified target naming one is refused with `no-content-index` before its
|
|
209
|
+
* type is even considered, since there is no index to resolve it against.
|
|
206
210
|
* @returns {{type: string, shortcode: string, itemDoc: boolean,
|
|
207
211
|
* package?: string, system?: string, reason?: undefined}
|
|
208
|
-
* | {reason: "unknown-type"} | null}
|
|
212
|
+
* | {reason: "unknown-type"|"no-content-index", package?: string} | null}
|
|
209
213
|
* The resolved qualifier; a `reason` when the target is definitely qualified
|
|
210
|
-
* but names no known type; or `null` when it is not an
|
|
214
|
+
* but names no known type or no fetched index; or `null` when it is not an
|
|
215
|
+
* address at all.
|
|
211
216
|
*/
|
|
212
|
-
export function readQualifier(target, types, packages) {
|
|
217
|
+
export function readQualifier(target, types, packages, noIndexPackages) {
|
|
213
218
|
// **Package, system and type are lowercase; the shortcode is not.** A
|
|
214
219
|
// shortcode is case-sensitive and routinely mixed — `Clb`, `LtShoe`,
|
|
215
220
|
// `HsTunic` — so it is written as the note declares it. The three segments
|
|
@@ -221,7 +226,7 @@ export function readQualifier(target, types, packages) {
|
|
|
221
226
|
// (`[[Shock State]]`), and calling that a badly-cased address rather than
|
|
222
227
|
// not an address would name the wrong mistake. Neither tree carries a
|
|
223
228
|
// violation — 10,538 authored targets — so this pins a rule already kept.
|
|
224
|
-
const read = readQualifierCased(target, types, packages);
|
|
229
|
+
const read = readQualifierCased(target, types, packages, noIndexPackages);
|
|
225
230
|
if (read && !read.reason && qualifyingSegments(target).some((s) => /[A-Z]/.test(s))) {
|
|
226
231
|
return { reason: "not-lowercase" };
|
|
227
232
|
}
|
|
@@ -249,9 +254,10 @@ function qualifyingSegments(target) {
|
|
|
249
254
|
* @param {string} target
|
|
250
255
|
* @param {Set<string>} types
|
|
251
256
|
* @param {Set<string>} [packages]
|
|
257
|
+
* @param {Set<string>} [noIndexPackages]
|
|
252
258
|
* @returns {object|null}
|
|
253
259
|
*/
|
|
254
|
-
function readQualifierCased(target, types, packages) {
|
|
260
|
+
function readQualifierCased(target, types, packages, noIndexPackages) {
|
|
255
261
|
// The slash form is legacy and states neither package nor system, so it is
|
|
256
262
|
// read first and separately. A slash is unconditionally a qualifier —
|
|
257
263
|
// nothing else uses one — which is why an unknown type before it is
|
|
@@ -281,6 +287,10 @@ function readQualifierCased(target, types, packages) {
|
|
|
281
287
|
// qualified.
|
|
282
288
|
case 4: {
|
|
283
289
|
const pkg = norm(parts[0]);
|
|
290
|
+
// Checked before the type: a package with no fetched index has no
|
|
291
|
+
// vocabulary to resolve the rest of the target against, and the
|
|
292
|
+
// fix is the config declaration, not the shortcode.
|
|
293
|
+
if (noIndexPackages?.has(pkg)) return { reason: "no-content-index", package: pkg };
|
|
284
294
|
if (!packages?.has(pkg)) return null;
|
|
285
295
|
const system = norm(parts[1]);
|
|
286
296
|
if (!isSystemSegment(system)) return null;
|
|
@@ -359,11 +369,21 @@ export function anchorPageId(noteId, anchorSlug) {
|
|
|
359
369
|
* @param {Map<string, object>} [opts.assets] - The files this package ships, by
|
|
360
370
|
* canonical address. They resolve no link — an asset is not a document — and
|
|
361
371
|
* answer only the art fields, which name a file and never a document.
|
|
372
|
+
* @param {Set<string>} [opts.noIndexPackages] - Packages declared
|
|
373
|
+
* `contentIndex: false` — a Foundry dependency only. A link naming one fails
|
|
374
|
+
* with `no-content-index` rather than resolving, ambiguously, as either a
|
|
375
|
+
* typo or an undeclared package.
|
|
362
376
|
* @returns {{byShortcode: Map<string, object>, types: Set<string>}} `types` is
|
|
363
377
|
* every type the tree actually contains, so a qualifier naming no real type
|
|
364
378
|
* can be told apart from a missing target.
|
|
365
379
|
*/
|
|
366
|
-
export function buildWikilinkIndex(
|
|
380
|
+
export function buildWikilinkIndex(
|
|
381
|
+
docs,
|
|
382
|
+
packageId,
|
|
383
|
+
foreign,
|
|
384
|
+
contentPackage,
|
|
385
|
+
{ assets, noIndexPackages } = {},
|
|
386
|
+
) {
|
|
367
387
|
if (!packageId) {
|
|
368
388
|
throw new Error(
|
|
369
389
|
"buildWikilinkIndex: packageId is required — it is the first " +
|
|
@@ -454,6 +474,8 @@ export function buildWikilinkIndex(docs, packageId, foreign, contentPackage, { a
|
|
|
454
474
|
foreign: foreignByKey,
|
|
455
475
|
/** The files this package ships, by canonical address. */
|
|
456
476
|
assets: assets ?? new Map(),
|
|
477
|
+
/** Packages declared `contentIndex: false`, a Foundry dependency only. */
|
|
478
|
+
noIndexPackages: noIndexPackages ?? new Set(),
|
|
457
479
|
};
|
|
458
480
|
}
|
|
459
481
|
|
|
@@ -656,7 +678,12 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
656
678
|
if (target === "" && slug) {
|
|
657
679
|
doc = { type, id, pack, docPack };
|
|
658
680
|
} else {
|
|
659
|
-
const qualified = readQualifier(
|
|
681
|
+
const qualified = readQualifier(
|
|
682
|
+
target,
|
|
683
|
+
index.types,
|
|
684
|
+
index.packages,
|
|
685
|
+
index.noIndexPackages,
|
|
686
|
+
);
|
|
660
687
|
qualifiedRead = qualified;
|
|
661
688
|
// A target that does not parse as an address is a defect: there is
|
|
662
689
|
// no second namespace left to fall through to.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.1.
|
|
3
|
+
"version": "22.1.1",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -505,6 +505,17 @@ export type RelationshipSpec = {
|
|
|
505
505
|
* `_stats.systemVersion` is stamped from.
|
|
506
506
|
*/
|
|
507
507
|
compatibility?: CompatibilitySpec | undefined;
|
|
508
|
+
/**
|
|
509
|
+
* Whether `deps fetch` fetches this
|
|
510
|
+
* dependency's content index. Default
|
|
511
|
+
* `true`. `false` declares the dependency
|
|
512
|
+
* for the Foundry manifest only — nothing
|
|
513
|
+
* this tree cites by wikilink — and refuses
|
|
514
|
+
* `itemCatalog: true` on the same entry,
|
|
515
|
+
* since a catalogue is fetched from the same
|
|
516
|
+
* index.
|
|
517
|
+
*/
|
|
518
|
+
contentIndex?: boolean | undefined;
|
|
508
519
|
};
|
|
509
520
|
/**
|
|
510
521
|
* How a generated documentation page is framed in the repository publishing it.
|
|
@@ -144,8 +144,9 @@ export function auditHomepageLinks(index: ReturnType<typeof buildLinkIndex>): Ar
|
|
|
144
144
|
* which addresses a foreign manifest answered. Each `deadAddresses` entry
|
|
145
145
|
* carries a `reason` from {@link LINK_FINDING_REASONS} —
|
|
146
146
|
* `"not-an-address"`, `"unknown-type"`, `"ambiguous"` (with the claiming
|
|
147
|
-
* `packages`), or `"unresolved"` — and every one of
|
|
148
|
-
* the three resolvers agree on severity for every
|
|
147
|
+
* `packages`), `"no-content-index"`, or `"unresolved"` — and every one of
|
|
148
|
+
* them is an **error**: the three resolvers agree on severity for every
|
|
149
|
+
* class.
|
|
149
150
|
*/
|
|
150
151
|
export function auditLinks(index: ReturnType<typeof buildLinkIndex>): {
|
|
151
152
|
deadAnchors: object[];
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* and needing no items is the mirror of it. Gating the index on the catalogue
|
|
10
10
|
* flag would serve neither.
|
|
11
11
|
*
|
|
12
|
+
* **Excludes a relationship declaring `contentIndex: false`.** That opts a
|
|
13
|
+
* dependency out of both edges at once: it is a Foundry dependency only, cited
|
|
14
|
+
* by neither a wikilink nor an item reference, so there is nothing here for
|
|
15
|
+
* `deps fetch` to fill and no cache this build will ever read.
|
|
16
|
+
*
|
|
12
17
|
* The declaration is the one already in the emitted `system.json` /
|
|
13
18
|
* `module.json`, so it cannot drift from what Foundry itself installs, and
|
|
14
19
|
* there is no new configuration key to keep in step. Each entry carries the
|
|
@@ -25,6 +30,23 @@ export function metadataRelationships(config: object): Array<{
|
|
|
25
30
|
kind: string;
|
|
26
31
|
verified: string | undefined;
|
|
27
32
|
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Every package a relationship declares `contentIndex: false` on, keyed by
|
|
35
|
+
* the content package name a link into it would use.
|
|
36
|
+
*
|
|
37
|
+
* A separate set from {@link metadataRelationships}, which answers "what does
|
|
38
|
+
* `deps fetch` fill" — this answers "what does the link resolver recognise as
|
|
39
|
+
* a package with no fetched index", which a wikilink checker or pack compiler
|
|
40
|
+
* needs to tell that case apart from a package nobody declared at all.
|
|
41
|
+
*
|
|
42
|
+
* Walked across every relationship kind, not only the citable ones: the
|
|
43
|
+
* config validation refuses the flag nowhere by kind, so a resolver reading it
|
|
44
|
+
* back should not assume one either.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} config - The resolved build configuration.
|
|
47
|
+
* @returns {ReadonlySet<string>} The content package names.
|
|
48
|
+
*/
|
|
49
|
+
export function noContentIndexPackages(config: object): ReadonlySet<string>;
|
|
28
50
|
/**
|
|
29
51
|
* The cache directory for one dependency's index at one version.
|
|
30
52
|
*
|
|
@@ -50,6 +50,18 @@ export function exclusiveTagGroups(type: string, groups?: object): {
|
|
|
50
50
|
* @returns {boolean} Whether the note carries it.
|
|
51
51
|
*/
|
|
52
52
|
export function hasTag(fm: object | null | undefined, tag: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Whether a note carries any `tags:` at all, however authored.
|
|
55
|
+
*
|
|
56
|
+
* The one question the site build asks of tags in aggregate — whether the
|
|
57
|
+
* tree publishes taxonomy pages — rather than about a particular tag. Reads
|
|
58
|
+
* `tags` and `tag` exactly as {@link hasTag} does, and treats an empty list
|
|
59
|
+
* or a blank string as carrying none.
|
|
60
|
+
*
|
|
61
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
62
|
+
* @returns {boolean} Whether the note carries at least one tag.
|
|
63
|
+
*/
|
|
64
|
+
export function hasAnyTag(fm: object | null | undefined): boolean;
|
|
53
65
|
/**
|
|
54
66
|
* Whether a note is tagged as an unfinished **draft**.
|
|
55
67
|
*
|
|
@@ -396,7 +396,10 @@ export function resolveSitePass(name: string | undefined, options: object): {
|
|
|
396
396
|
* `sql` directive with none prepared is a table error: nothing here runs a
|
|
397
397
|
* query.
|
|
398
398
|
* @returns {{gates: object, stats: object|null, tableErrors: object[],
|
|
399
|
-
* wikiErrors: object[], imageErrors: object[], manifests: object|null
|
|
399
|
+
* wikiErrors: object[], imageErrors: object[], manifests: object|null,
|
|
400
|
+
* hasTags: boolean}} `hasTags` is whether any note the walk read carries
|
|
401
|
+
* `tags:` — what {@link module:engine/site-config.hugoConfig} reads to
|
|
402
|
+
* decide whether the site emits taxonomy pages.
|
|
400
403
|
*/
|
|
401
404
|
export function buildSite({ config, sqlTables }?: {
|
|
402
405
|
config?: object | undefined;
|
|
@@ -408,6 +411,7 @@ export function buildSite({ config, sqlTables }?: {
|
|
|
408
411
|
wikiErrors: object[];
|
|
409
412
|
imageErrors: object[];
|
|
410
413
|
manifests: object | null;
|
|
414
|
+
hasTags: boolean;
|
|
411
415
|
};
|
|
412
416
|
export { formatUnaddressableFinding };
|
|
413
417
|
import { formatUnaddressableFinding } from "./metadata-index.mjs";
|
|
@@ -92,15 +92,19 @@ export function resolveThemesDir(rootDir: string): string;
|
|
|
92
92
|
* @param {string} [options.description] - `package.json`'s `description`.
|
|
93
93
|
* @param {readonly NavigationEntry[]} options.navigation - The navigation.
|
|
94
94
|
* @param {string} options.themesDir - From {@link resolveThemesDir}.
|
|
95
|
+
* @param {boolean} [options.hasTags] - Whether any note the site build walked
|
|
96
|
+
* carries `tags:`, from {@link module:engine/site-build.buildSite}'s
|
|
97
|
+
* `hasTags`. Defaults to `false` — no tagged note, no taxonomy pages.
|
|
95
98
|
* @returns {Record<string, any>} The configuration Hugo reads.
|
|
96
99
|
* @throws {TypeError} When `homepage` fails `checkHomepage`, or the
|
|
97
100
|
* configuration declares no `packageBuild.manifest.title`.
|
|
98
101
|
*/
|
|
99
|
-
export function hugoConfig({ config, description, navigation, themesDir }: {
|
|
102
|
+
export function hugoConfig({ config, description, navigation, themesDir, hasTags }: {
|
|
100
103
|
config: object;
|
|
101
104
|
description?: string | undefined;
|
|
102
105
|
navigation: readonly NavigationEntry[];
|
|
103
106
|
themesDir: string;
|
|
107
|
+
hasTags?: boolean | undefined;
|
|
104
108
|
}): Record<string, any>;
|
|
105
109
|
/**
|
|
106
110
|
* The configuration as the TOML Hugo reads.
|
|
@@ -118,10 +122,17 @@ export function hugoToml(generated: Record<string, unknown>): string;
|
|
|
118
122
|
* intact.
|
|
119
123
|
*
|
|
120
124
|
* @param {object} config - The resolved build configuration.
|
|
125
|
+
* @param {object} [options] - Options.
|
|
126
|
+
* @param {boolean} [options.hasTags] - Whether any note the site build walked
|
|
127
|
+
* carries `tags:`. Defaults to `false`, so a caller generating the
|
|
128
|
+
* configuration before the walk (to fail fast on a missing source) gets the
|
|
129
|
+
* untagged shape; pass the site build's own `hasTags` once it is known.
|
|
121
130
|
* @returns {Record<string, any>} The configuration Hugo reads.
|
|
122
131
|
* @throws {Error} When any source is missing or wrong.
|
|
123
132
|
*/
|
|
124
|
-
export function generateHugoConfig(config: object
|
|
133
|
+
export function generateHugoConfig(config: object, { hasTags }?: {
|
|
134
|
+
hasTags?: boolean | undefined;
|
|
135
|
+
}): Record<string, any>;
|
|
125
136
|
/**
|
|
126
137
|
* Write `build/hugo/hugo.toml`.
|
|
127
138
|
*
|
|
@@ -162,11 +173,13 @@ export const BRAND: Readonly<{
|
|
|
162
173
|
discordURL: "https://discord.gg/EwMfkNd3az";
|
|
163
174
|
}>;
|
|
164
175
|
/**
|
|
165
|
-
* The kinds
|
|
176
|
+
* The kinds a site with no tagged notes renders.
|
|
166
177
|
*
|
|
167
178
|
* A section exists only where `site.sections` declares one, so a tree holding
|
|
168
|
-
* only the homepage emits nothing beyond it;
|
|
169
|
-
* empty shells
|
|
179
|
+
* only the homepage emits nothing beyond it; a taxonomy nobody's notes fill
|
|
180
|
+
* and a feed would be empty shells. A site whose notes carry `tags:` emits
|
|
181
|
+
* `taxonomy` and `term` after all — see {@link hugoConfig} — but `RSS` is
|
|
182
|
+
* disabled either way: nothing here publishes a feed.
|
|
170
183
|
*/
|
|
171
184
|
export const DISABLE_KINDS: readonly string[];
|
|
172
185
|
/**
|
|
@@ -7,13 +7,18 @@
|
|
|
7
7
|
* @param {Map<string, {package: string, type?: string}>} [options.foreignIndex]
|
|
8
8
|
* The merged index from `loadForeignIndexes`. Omit when the build publishes
|
|
9
9
|
* no cross-package links.
|
|
10
|
+
* @param {Set<string>} [options.noIndexPackages] - Packages declared
|
|
11
|
+
* `contentIndex: false` — a Foundry dependency only, with no fetched index.
|
|
12
|
+
* A link naming one fails naming the key, rather than reading as prose or an
|
|
13
|
+
* ordinary dead address.
|
|
10
14
|
* @returns {SiteIndex} The index, and what could not be addressed unambiguously.
|
|
11
15
|
*/
|
|
12
|
-
export function buildSiteIndex(entries: readonly SiteEntry[], { foreignIndex }?: {
|
|
16
|
+
export function buildSiteIndex(entries: readonly SiteEntry[], { foreignIndex, noIndexPackages }?: {
|
|
13
17
|
foreignIndex?: Map<string, {
|
|
14
18
|
package: string;
|
|
15
19
|
type?: string;
|
|
16
20
|
}> | undefined;
|
|
21
|
+
noIndexPackages?: Set<string> | undefined;
|
|
17
22
|
}): SiteIndex;
|
|
18
23
|
/**
|
|
19
24
|
* The per-page context a wikilink resolver takes.
|
|
@@ -44,9 +44,10 @@ export function frontmatterWikilinks(fm: unknown): Array<{
|
|
|
44
44
|
*
|
|
45
45
|
* **Every target that resolves nowhere fails the build**, and is
|
|
46
46
|
* classified into the vocabulary all three resolvers share — `unlabelled`,
|
|
47
|
-
* `not-an-address`, `unknown-type`, `ambiguous`, `unresolved
|
|
48
|
-
* collected in `ctx.errors`, each carrying
|
|
49
|
-
* `occurrence` so a caller can report the line and
|
|
47
|
+
* `not-an-address`, `unknown-type`, `ambiguous`, `unresolved`,
|
|
48
|
+
* `no-content-index`. Failures are collected in `ctx.errors`, each carrying
|
|
49
|
+
* the authored `link` and its `occurrence` so a caller can report the line and
|
|
50
|
+
* column it sits on.
|
|
50
51
|
*
|
|
51
52
|
* There is deliberately no exception letting a hyphen-form address through while
|
|
52
53
|
* any linkable package had no vendored manifest, since a real cross-package
|
|
@@ -77,9 +78,12 @@ export function frontmatterWikilinks(fm: unknown): Array<{
|
|
|
77
78
|
*
|
|
78
79
|
* @param {string} body - The markdown body.
|
|
79
80
|
* @param {object} ctx - `{ index, assets, collide, sections, contentTypes,
|
|
80
|
-
* packages, foreign, type, errors, src, file }`.
|
|
81
|
+
* packages, noIndexPackages, foreign, type, errors, src, file }`.
|
|
81
82
|
* `packages` is every package an address may name, without which the leading
|
|
82
|
-
* package segment of a canonical address reads as an unknown type;
|
|
83
|
+
* package segment of a canonical address reads as an unknown type;
|
|
84
|
+
* `noIndexPackages` is every package declared `contentIndex: false`, so a
|
|
85
|
+
* qualified address naming one fails with `no-content-index` rather than
|
|
86
|
+
* `not-an-address`; `foreign`
|
|
83
87
|
* is the cross-package manifest index; `assets` is the address space an embed
|
|
84
88
|
* resolves against. `src` is the page's display
|
|
85
89
|
* path and `file` the source file a diagnostic should name — absent, `src`
|
|
@@ -223,6 +223,9 @@ export const WIKILINK: RegExp;
|
|
|
223
223
|
* - `unresolved` — parses as an address, and nothing publishes it.
|
|
224
224
|
* - `ambiguous` — more than one package publishes the short address.
|
|
225
225
|
* - `unknown-anchor` — the address resolved, the `#section` it names did not.
|
|
226
|
+
* - `no-content-index` — the address names a package declared
|
|
227
|
+
* `contentIndex: false`, a Foundry dependency only, so no index was fetched
|
|
228
|
+
* for it to resolve against.
|
|
226
229
|
*
|
|
227
230
|
* @type {ReadonlySet<string>}
|
|
228
231
|
*/
|