@heroiclands/package-build 24.0.2 → 24.0.3
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 +18 -0
- package/bin/content-commands.mjs +8 -10
- package/content-config.mjs +1 -52
- package/docs/api.md +14 -8
- package/docs/authoring/links-and-markup.md +1 -1
- package/docs/commands.md +4 -6
- package/docs/configuration.md +13 -69
- package/docs/guides/site.md +3 -1
- package/docs/reference/format-details.md +19 -71
- package/docs/reference/note-types.md +3 -2
- package/engine/content-charset.mjs +1 -0
- package/engine/dictionary-layout.mjs +16 -3
- package/engine/foreign-catalog.mjs +11 -122
- package/engine/language-rows.mjs +3 -3
- package/engine/language-terms.mjs +7 -1
- package/engine/pdf-build.mjs +9 -178
- package/engine/site-reach.mjs +23 -0
- package/engine/site-render.mjs +18 -3
- package/package.json +2 -1
- package/types/content-config.d.mts +1 -19
- package/types/engine/dictionary-layout.d.mts +5 -0
- package/types/engine/foreign-catalog.d.mts +0 -38
- package/types/engine/language-rows.d.mts +1 -1
- package/types/engine/pdf-build.d.mts +5 -27
- package/types/engine/site-reach.d.mts +11 -0
- package/engine/asset-replacement.mjs +0 -93
- package/types/engine/asset-replacement.d.mts +0 -22
|
@@ -68,6 +68,19 @@ export const COMMON_MARK = "•";
|
|
|
68
68
|
/** The word-entry span class whose Foundry rendering is a native TOC heading. */
|
|
69
69
|
export const DICTIONARY_ENTRY_CLASS = "dictionary-entry";
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The class on every link an entry writes to another note, so a site can set
|
|
73
|
+
* the link as plain text where its target is not a page a reader can reach.
|
|
74
|
+
*/
|
|
75
|
+
export const LEXICAL_LINK_CLASS = "lexical-ref";
|
|
76
|
+
|
|
77
|
+
/** Wikilinks in a gloss, marked with {@link LEXICAL_LINK_CLASS}. */
|
|
78
|
+
const markLinks = (text) =>
|
|
79
|
+
String(text ?? "").replace(
|
|
80
|
+
/\[\[[^\]]*\]\](?!\{)/g,
|
|
81
|
+
(link) => `${link}{.${LEXICAL_LINK_CLASS}}`,
|
|
82
|
+
);
|
|
83
|
+
|
|
71
84
|
/**
|
|
72
85
|
* Render a dictionary entry's marked headword as an inline native heading in
|
|
73
86
|
* Foundry. Its paragraph becomes a div, so the heading has a valid container.
|
|
@@ -327,7 +340,7 @@ function render(
|
|
|
327
340
|
const text = label(address);
|
|
328
341
|
const ref = parseAddress(address, addressContext ?? {}, { declared: true });
|
|
329
342
|
if (ref.reason || !linkable(ref)) return text;
|
|
330
|
-
return `[[${renderAddress(ref)}|${String(text).replace(/\|/g, "/")}]]`;
|
|
343
|
+
return `[[${renderAddress(ref)}|${String(text).replace(/\|/g, "/")}]]{.${LEXICAL_LINK_CLASS}}`;
|
|
331
344
|
};
|
|
332
345
|
const tag = (language, text) => {
|
|
333
346
|
if (!text) return "";
|
|
@@ -367,7 +380,7 @@ function render(
|
|
|
367
380
|
const { address, anchor } = splitAnchor(row.word);
|
|
368
381
|
const ref = readCanonicalKey(address);
|
|
369
382
|
if (ref && anchor && linkable(ref))
|
|
370
|
-
headword = `**[[${renderAddress(ref)}#${anchor}|${String(row.name).replace(/\|/g, "/")}]]**`;
|
|
383
|
+
headword = `**[[${renderAddress(ref)}#${anchor}|${String(row.name).replace(/\|/g, "/")}]]{.${LEXICAL_LINK_CLASS}}**`;
|
|
371
384
|
}
|
|
372
385
|
parts.push(headword);
|
|
373
386
|
if (row.pronunciation) parts.push(`/${row.pronunciation}/`);
|
|
@@ -401,7 +414,7 @@ function render(
|
|
|
401
414
|
const bodies = Array.isArray(sense?.bodies) ? sense.bodies : [];
|
|
402
415
|
const by =
|
|
403
416
|
bodies.length && !sense.omitBody ? `(_${bodies.map(label).join(", ")}_) ` : "";
|
|
404
|
-
return `${by}${sentence(sense?.text)}`;
|
|
417
|
+
return `${by}${markLinks(sentence(sense?.text))}`;
|
|
405
418
|
};
|
|
406
419
|
if (senses.length === 1) parts.push(senseText(senses[0]));
|
|
407
420
|
else senses.forEach((sense, i) => parts.push(`**${i + 1}** ${senseText(sense)}`));
|
|
@@ -38,12 +38,6 @@
|
|
|
38
38
|
* local pack's output — the resolution logic needs no knowledge of where an
|
|
39
39
|
* item came from.
|
|
40
40
|
*
|
|
41
|
-
* `assetArchive: true` is the sibling flag, for a dependency whose bytes are
|
|
42
|
-
* wanted and whose items are not: an asset-replacement source such as
|
|
43
|
-
* `thalornaaltart` ships no Item packs at all. It unpacks the same release
|
|
44
|
-
* archive without extracting anything from it, so a relationship declaring
|
|
45
|
-
* both flags on one entry is fetched once rather than twice.
|
|
46
|
-
*
|
|
47
41
|
* **The network is never touched by a compile.** Fetching is its own command
|
|
48
42
|
* (`package-build deps fetch`), and a compile whose cache is cold fails saying
|
|
49
43
|
* so. A build that silently downloads is not reproducible, fails strangely
|
|
@@ -78,22 +72,17 @@ import {
|
|
|
78
72
|
const STAMP = ".complete";
|
|
79
73
|
|
|
80
74
|
/**
|
|
81
|
-
* Every declared relationship
|
|
82
|
-
*
|
|
83
|
-
* Shared by {@link itemCatalogRelationships} and
|
|
84
|
-
* {@link assetArchiveRelationships}: both read the same shape out of the same
|
|
85
|
-
* walk, filtered on a different key.
|
|
75
|
+
* Every declared relationship that opted into supplying an item catalogue.
|
|
86
76
|
*
|
|
87
77
|
* @param {object} config - The resolved build configuration.
|
|
88
|
-
* @param {"itemCatalog"|"assetArchive"} flag - Which boolean to filter on.
|
|
89
78
|
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
90
|
-
* The opted-in relationships.
|
|
79
|
+
* The opted-in relationships, in declaration order.
|
|
91
80
|
*/
|
|
92
|
-
function
|
|
81
|
+
export function itemCatalogRelationships(config) {
|
|
93
82
|
const out = [];
|
|
94
83
|
for (const [kind, entries] of Object.entries(config.relationships ?? {})) {
|
|
95
84
|
for (const rel of entries ?? []) {
|
|
96
|
-
if (rel
|
|
85
|
+
if (rel.itemCatalog) {
|
|
97
86
|
out.push({
|
|
98
87
|
id: rel.id,
|
|
99
88
|
manifest: rel.manifest,
|
|
@@ -106,29 +95,6 @@ function flaggedRelationships(config, flag) {
|
|
|
106
95
|
return out;
|
|
107
96
|
}
|
|
108
97
|
|
|
109
|
-
/**
|
|
110
|
-
* Every declared relationship that opted into supplying an item catalogue.
|
|
111
|
-
*
|
|
112
|
-
* @param {object} config - The resolved build configuration.
|
|
113
|
-
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
114
|
-
* The opted-in relationships, in declaration order.
|
|
115
|
-
*/
|
|
116
|
-
export function itemCatalogRelationships(config) {
|
|
117
|
-
return flaggedRelationships(config, "itemCatalog");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Every declared relationship that opted into supplying its release archive
|
|
122
|
-
* for asset bytes alone, with no item catalogue built from it.
|
|
123
|
-
*
|
|
124
|
-
* @param {object} config - The resolved build configuration.
|
|
125
|
-
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
126
|
-
* The opted-in relationships, in declaration order.
|
|
127
|
-
*/
|
|
128
|
-
export function assetArchiveRelationships(config) {
|
|
129
|
-
return flaggedRelationships(config, "assetArchive");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
98
|
/**
|
|
133
99
|
* The cache directory for one dependency at one version.
|
|
134
100
|
*
|
|
@@ -401,21 +367,14 @@ export function pinnedManifestUrl(url, verified) {
|
|
|
401
367
|
}
|
|
402
368
|
|
|
403
369
|
/**
|
|
404
|
-
* Resolve a dependency's manifest, pinned to `compatibility.verified
|
|
405
|
-
* both fetch paths require.
|
|
406
|
-
*
|
|
407
|
-
* Shared by {@link fetchCatalog} and {@link fetchAssetArchive}: both read the
|
|
408
|
-
* same manifest before deciding what to unpack from it.
|
|
370
|
+
* Resolve a dependency's manifest, pinned to `compatibility.verified`.
|
|
409
371
|
*
|
|
410
372
|
* @param {{id: string, manifest: string, verified?: string}} rel - The
|
|
411
373
|
* declared relationship.
|
|
412
|
-
* @param {string} subject - What floats when no version is verified —
|
|
413
|
-
* "catalogue" or "archive" — named in the warning a reader would otherwise
|
|
414
|
-
* get no word for.
|
|
415
374
|
* @returns {Promise<{manifest: object, version: string}>} The manifest and
|
|
416
375
|
* its resolved version.
|
|
417
376
|
*/
|
|
418
|
-
async function resolveManifest(rel
|
|
377
|
+
async function resolveManifest(rel) {
|
|
419
378
|
const { url, pinned } = pinnedManifestUrl(rel.manifest, rel.verified);
|
|
420
379
|
const manifest = await fetchManifest(url);
|
|
421
380
|
const version = manifest.version;
|
|
@@ -432,7 +391,7 @@ async function resolveManifest(rel, subject) {
|
|
|
432
391
|
}
|
|
433
392
|
if (!rel.verified) {
|
|
434
393
|
log.warn(
|
|
435
|
-
`${rel.id}: no \`compatibility.verified\`, so its
|
|
394
|
+
`${rel.id}: no \`compatibility.verified\`, so its catalogue floats ` +
|
|
436
395
|
`with whatever ${url} currently serves`,
|
|
437
396
|
);
|
|
438
397
|
}
|
|
@@ -449,7 +408,7 @@ async function resolveManifest(rel, subject) {
|
|
|
449
408
|
* @returns {Promise<string>} The dependency's cache directory.
|
|
450
409
|
*/
|
|
451
410
|
export async function fetchCatalog(config, rel) {
|
|
452
|
-
const { manifest, version } = await resolveManifest(rel
|
|
411
|
+
const { manifest, version } = await resolveManifest(rel);
|
|
453
412
|
const dir = catalogDir(config, rel.id, version);
|
|
454
413
|
if (isComplete(dir)) {
|
|
455
414
|
log.info(`${rel.id}@${version}: already cached`);
|
|
@@ -475,61 +434,6 @@ export async function fetchCatalog(config, rel) {
|
|
|
475
434
|
return dir;
|
|
476
435
|
}
|
|
477
436
|
|
|
478
|
-
/**
|
|
479
|
-
* Whether an asset-archive-only cache is present and complete.
|
|
480
|
-
*
|
|
481
|
-
* No item catalogue is built on this path, so completeness never asks for
|
|
482
|
-
* `item-packs.json` — an asset-only dependency, such as `thalornaaltart`,
|
|
483
|
-
* ships none.
|
|
484
|
-
*
|
|
485
|
-
* @param {string} dir - The dependency's cache directory.
|
|
486
|
-
* @returns {boolean} True when the archive was fetched to completion.
|
|
487
|
-
*/
|
|
488
|
-
const isArchiveComplete = (dir) => fs.existsSync(path.join(dir, STAMP));
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Fetch one dependency's release archive for its asset bytes alone, building
|
|
492
|
-
* no item catalogue from it.
|
|
493
|
-
*
|
|
494
|
-
* Idempotent: a complete cache for the resolved version is left alone. Where a
|
|
495
|
-
* relationship also declares `itemCatalog: true`, {@link fetchCatalog} already
|
|
496
|
-
* unpacked this same archive into this same cache directory, and
|
|
497
|
-
* {@link fetchAllCatalogs} does not call this function for it — the two never
|
|
498
|
-
* download the same bytes twice.
|
|
499
|
-
*
|
|
500
|
-
* @param {object} config - The resolved build configuration.
|
|
501
|
-
* @param {{id: string, manifest: string, verified?: string}} rel - The
|
|
502
|
-
* declared relationship.
|
|
503
|
-
* @returns {Promise<string>} The dependency's cache directory.
|
|
504
|
-
*/
|
|
505
|
-
export async function fetchAssetArchive(config, rel) {
|
|
506
|
-
const { manifest, version } = await resolveManifest(rel, "archive");
|
|
507
|
-
const dir = catalogDir(config, rel.id, version);
|
|
508
|
-
if (isArchiveComplete(dir)) {
|
|
509
|
-
log.info(`${rel.id}@${version}: already cached`);
|
|
510
|
-
return dir;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const download = manifest.download;
|
|
514
|
-
if (!download) {
|
|
515
|
-
throw new Error(`${rel.id}@${version}: its manifest declares no \`download\``);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Rebuild from empty: a previous run may have died partway, and a stale
|
|
519
|
-
// half-tree is worse than no tree.
|
|
520
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
521
|
-
const raw = path.join(dir, "package");
|
|
522
|
-
fs.mkdirSync(raw, { recursive: true });
|
|
523
|
-
|
|
524
|
-
log.info(`${rel.id}@${version}: downloading ${download}`);
|
|
525
|
-
await downloadAndUnzip(download, raw);
|
|
526
|
-
|
|
527
|
-
cacheSchemaArtifact(raw, dir);
|
|
528
|
-
// Last, so a fetch that died partway is never mistaken for a complete one.
|
|
529
|
-
fs.writeFileSync(path.join(dir, STAMP), `${version}\n`);
|
|
530
|
-
return dir;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
437
|
/**
|
|
534
438
|
* Locate a declared pack inside an unpacked archive.
|
|
535
439
|
*
|
|
@@ -811,33 +715,18 @@ export async function fetchAllMetadata(config) {
|
|
|
811
715
|
/**
|
|
812
716
|
* Fetch every opted-in dependency. The `deps fetch` command.
|
|
813
717
|
*
|
|
814
|
-
* A relationship declaring both `itemCatalog: true` and `assetArchive: true`
|
|
815
|
-
* is fetched once: {@link fetchCatalog} already unpacks the archive on its
|
|
816
|
-
* way to extracting the item catalogue, so this skips the archive-only fetch
|
|
817
|
-
* for any id {@link fetchCatalog} already handled.
|
|
818
718
|
*
|
|
819
719
|
* @param {object} config - The resolved build configuration.
|
|
820
720
|
* @returns {Promise<number>} How many dependencies were fetched.
|
|
821
721
|
*/
|
|
822
722
|
export async function fetchAllCatalogs(config) {
|
|
823
723
|
const itemRels = itemCatalogRelationships(config);
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
log.info(
|
|
827
|
-
"No relationship declares `itemCatalog: true` or `assetArchive: true`; " +
|
|
828
|
-
"nothing to fetch.",
|
|
829
|
-
);
|
|
724
|
+
if (!itemRels.length) {
|
|
725
|
+
log.info("No relationship declares `itemCatalog: true`; nothing to fetch.");
|
|
830
726
|
return 0;
|
|
831
727
|
}
|
|
832
728
|
for (const rel of itemRels) await fetchCatalog(config, rel);
|
|
833
|
-
|
|
834
|
-
let archiveFetched = 0;
|
|
835
|
-
for (const rel of archiveRels) {
|
|
836
|
-
if (alreadyFetched.has(rel.id)) continue;
|
|
837
|
-
await fetchAssetArchive(config, rel);
|
|
838
|
-
archiveFetched++;
|
|
839
|
-
}
|
|
840
|
-
return itemRels.length + archiveFetched;
|
|
729
|
+
return itemRels.length;
|
|
841
730
|
}
|
|
842
731
|
|
|
843
732
|
/**
|
package/engine/language-rows.mjs
CHANGED
|
@@ -253,7 +253,7 @@ export const LANGUAGE_COLUMN_TYPES = Object.freeze({
|
|
|
253
253
|
joining: { position: "BIGINT", first: LIST, second: LIST },
|
|
254
254
|
joins: { position: "BIGINT", letters: LIST },
|
|
255
255
|
marks: { position: "BIGINT", letters: LIST },
|
|
256
|
-
skeleton: { position: "BIGINT", second: LIST },
|
|
256
|
+
skeleton: { position: "BIGINT", second: LIST, third: LIST },
|
|
257
257
|
frames: {
|
|
258
258
|
position: "BIGINT",
|
|
259
259
|
class: LIST,
|
|
@@ -911,14 +911,14 @@ export function markRows(records) {
|
|
|
911
911
|
}
|
|
912
912
|
|
|
913
913
|
/**
|
|
914
|
-
* The `skeleton` rows: one per `data.sounds.skeleton` entry, `second`
|
|
914
|
+
* The `skeleton` rows: one per `data.sounds.skeleton` entry, `second` and `third` lists.
|
|
915
915
|
*
|
|
916
916
|
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
917
917
|
* @returns {object[]}
|
|
918
918
|
*/
|
|
919
919
|
export function skeletonRows(records) {
|
|
920
920
|
return listRows(records, (data) => soundsOf(data).skeleton, SKELETON_KEYS, {
|
|
921
|
-
lists: ["second"],
|
|
921
|
+
lists: ["second", "third"],
|
|
922
922
|
});
|
|
923
923
|
}
|
|
924
924
|
|
|
@@ -152,7 +152,7 @@ export const JOIN_MEETINGS = Object.freeze(["C+C", "V+C", "V+V"]);
|
|
|
152
152
|
export const JOIN_RULES = Object.freeze(["merge", "insert", "drop", "double"]);
|
|
153
153
|
|
|
154
154
|
/** What a written mark says of its letter. */
|
|
155
|
-
export const MARK_KINDS = Object.freeze(["stress", "length"]);
|
|
155
|
+
export const MARK_KINDS = Object.freeze(["stress", "length", "apart"]);
|
|
156
156
|
|
|
157
157
|
/** How many of a mark a word may carry: `one` at most, or `any` number. */
|
|
158
158
|
export const MARK_LIMITS = Object.freeze(["one", "any"]);
|
|
@@ -831,6 +831,12 @@ export const SKELETON_FIELDS = Object.freeze([
|
|
|
831
831
|
required: true,
|
|
832
832
|
describe: "The second consonants of a root the row holds for; `any` for every other.",
|
|
833
833
|
}),
|
|
834
|
+
Object.freeze({
|
|
835
|
+
name: "third",
|
|
836
|
+
...SOUNDS_OR_ANY,
|
|
837
|
+
describe:
|
|
838
|
+
"The third consonants of a root the row holds for; every third consonant where absent.",
|
|
839
|
+
}),
|
|
834
840
|
Object.freeze({
|
|
835
841
|
name: "between",
|
|
836
842
|
kind: "string",
|
package/engine/pdf-build.mjs
CHANGED
|
@@ -124,15 +124,7 @@ import {
|
|
|
124
124
|
pdfTextWidth,
|
|
125
125
|
resamplePdfImages,
|
|
126
126
|
} from "./pdf-images.mjs";
|
|
127
|
-
import {
|
|
128
|
-
ASSETS_SEGMENT,
|
|
129
|
-
assetPathnameKey,
|
|
130
|
-
pathnameProblem,
|
|
131
|
-
resolvePathname,
|
|
132
|
-
} from "./pathnames.mjs";
|
|
133
|
-
import { ASSET_SYSTEM, assetTypeOfRoot, isAssetShortcode } from "./asset-types.mjs";
|
|
134
|
-
import { canonicalKey } from "./address.mjs";
|
|
135
|
-
import { isComplete as isForeignCacheComplete, newestVersionDir } from "./metadata-index.mjs";
|
|
127
|
+
import { assetPathnameKey, pathnameProblem, resolvePathname } from "./pathnames.mjs";
|
|
136
128
|
import {
|
|
137
129
|
createParser,
|
|
138
130
|
labelFor,
|
|
@@ -147,7 +139,6 @@ import { noteInfoboxes } from "./infobox-registry.mjs";
|
|
|
147
139
|
import { resolveIconGlyphs } from "./pdf-fonts.mjs";
|
|
148
140
|
import { drawMapsFrom } from "./derived-sections.mjs";
|
|
149
141
|
import { generatedDrawing } from "./generated-sections.mjs";
|
|
150
|
-
import { resolveAssetReplacement } from "./asset-replacement.mjs";
|
|
151
142
|
import { typstScriptFontPath } from "./script-font-delivery.mjs";
|
|
152
143
|
|
|
153
144
|
/**
|
|
@@ -166,39 +157,18 @@ import { typstScriptFontPath } from "./script-font-delivery.mjs";
|
|
|
166
157
|
* a build from touching a path outside its own output.
|
|
167
158
|
*
|
|
168
159
|
* A file **this** package ships stages directly. A pathname naming another
|
|
169
|
-
* package's file
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* answers it; `opts.foreignIndex` is the merged index that answer is read
|
|
173
|
-
* against. A pathname naming a package no replacement carries it for, or a
|
|
174
|
-
* URL, names something no build here can open — a build reaches no network —
|
|
175
|
-
* and the caller reports it as a picture the book will not carry.
|
|
160
|
+
* package's file, or a URL, names something no build here can open — a build
|
|
161
|
+
* reaches no network — and the caller reports it as a picture the book will
|
|
162
|
+
* not carry.
|
|
176
163
|
*
|
|
177
164
|
* @param {string} src - The pathname, as authored.
|
|
178
165
|
* @param {object} config - The resolved configuration.
|
|
179
|
-
* @param {object} [opts]
|
|
180
|
-
* @param {Map<string, object>} [opts.foreignIndex] - Every fetched
|
|
181
|
-
* dependency's own records, keyed by canonical address — the index a
|
|
182
|
-
* replacement's record is read out of.
|
|
183
|
-
* @param {Function} [opts.resolveReplacement] - {@link
|
|
184
|
-
* module:engine/asset-replacement.resolveAssetReplacement}, or an
|
|
185
|
-
* equivalent a caller injects for a test. Omitted, a foreign pathname this
|
|
186
|
-
* package does not own stays declined exactly as it always has.
|
|
187
166
|
* @returns {{from: string, to: string}|null} The file, and where under the
|
|
188
167
|
* output directory it is staged.
|
|
189
|
-
* @throws {Error} When a declared replacement answers the address but its
|
|
190
|
-
* archive was never fetched.
|
|
191
168
|
*/
|
|
192
|
-
export function stagedImagePath(src, config
|
|
169
|
+
export function stagedImagePath(src, config) {
|
|
193
170
|
const forms = resolvePathname(src, config);
|
|
194
171
|
if (!forms || forms.state !== "package") return null;
|
|
195
|
-
// A declared replacement answers before the package's own file does, which
|
|
196
|
-
// is the whole of what declaring one buys: the address a note writes is
|
|
197
|
-
// this package's own, and the picture staged for it is the replacement's.
|
|
198
|
-
// A miss falls through, so a package declaring none stages exactly what it
|
|
199
|
-
// ships.
|
|
200
|
-
const replaced = opts.resolveReplacement ? stagedReplacementPath(forms, config, opts) : null;
|
|
201
|
-
if (replaced) return replaced;
|
|
202
172
|
if (forms.own) {
|
|
203
173
|
return {
|
|
204
174
|
from: path.resolve(config.rootDir, forms.local),
|
|
@@ -208,126 +178,6 @@ export function stagedImagePath(src, config, opts = {}) {
|
|
|
208
178
|
return null;
|
|
209
179
|
}
|
|
210
180
|
|
|
211
|
-
/**
|
|
212
|
-
* Every declared relationship that opted a dependency in as this package's
|
|
213
|
-
* asset replacement, in declaration order.
|
|
214
|
-
*
|
|
215
|
-
* The same shape {@link module:engine/foreign-catalog.itemCatalogRelationships}
|
|
216
|
-
* returns, read directly rather than through that module: this file stays a
|
|
217
|
-
* leaf over the configuration it is handed, and the flag is as small to read
|
|
218
|
-
* back as it was to validate.
|
|
219
|
-
*
|
|
220
|
-
* @param {object} config - The resolved build configuration.
|
|
221
|
-
* @returns {Array<{id: string, package: string}>} The relationships, each
|
|
222
|
-
* with the package segment its own content index publishes under.
|
|
223
|
-
*/
|
|
224
|
-
function assetReplacementRelationships(config) {
|
|
225
|
-
const out = [];
|
|
226
|
-
for (const entries of Object.values(config.relationships ?? {})) {
|
|
227
|
-
for (const rel of entries ?? []) {
|
|
228
|
-
if (rel.assetReplacement) {
|
|
229
|
-
out.push({ id: rel.id, package: rel.contentPackage ?? rel.id });
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return out;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* The address a pathname this package does not own is read back as, for
|
|
238
|
-
* asking whether a declared replacement answers it instead of the package
|
|
239
|
-
* the pathname names.
|
|
240
|
-
*
|
|
241
|
-
* {@link module:engine/asset-replacement.resolveAssetReplacement} only ever
|
|
242
|
-
* matches an address whose package segment is the compiling package's own,
|
|
243
|
-
* so the pathname's own package is set aside and its suffix — the one piece
|
|
244
|
-
* every surface derives from, and the piece a replacement publishes under
|
|
245
|
-
* its own package instead — is read back as this package's type and
|
|
246
|
-
* shortcode.
|
|
247
|
-
*
|
|
248
|
-
* @param {import("./pathnames.mjs").PathnameForms} forms - The pathname,
|
|
249
|
-
* resolved.
|
|
250
|
-
* @param {object} config - The resolved build configuration.
|
|
251
|
-
* @returns {string|null} The canonical address, or `null` when the suffix is
|
|
252
|
-
* not shaped like an asset's.
|
|
253
|
-
*/
|
|
254
|
-
function localAssetAddress(forms, config) {
|
|
255
|
-
const segments = String(forms.suffix ?? "").split("/");
|
|
256
|
-
const assetType = assetTypeOfRoot(segments[0]);
|
|
257
|
-
if (!assetType) return null;
|
|
258
|
-
const base = segments[segments.length - 1];
|
|
259
|
-
const ext = path.extname(base);
|
|
260
|
-
const shortcode = (ext ? base.slice(0, -ext.length) : base).toLowerCase();
|
|
261
|
-
if (!isAssetShortcode(shortcode)) return null;
|
|
262
|
-
return canonicalKey(config.contentPackage, ASSET_SYSTEM, assetType.type, shortcode);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* The cache directory one relationship's archive was fetched into, found the
|
|
267
|
-
* way {@link module:engine/foreign-catalog.foreignItemCatalogDirs} finds an
|
|
268
|
-
* item catalogue's: the newest `.complete`-stamped `<id>@<version>`
|
|
269
|
-
* directory under `config.paths.foreignCache`.
|
|
270
|
-
*
|
|
271
|
-
* @param {object} config - The resolved build configuration.
|
|
272
|
-
* @param {string} id - The relationship's dependency id.
|
|
273
|
-
* @returns {string|null} The directory, or `null` when nothing is cached.
|
|
274
|
-
*/
|
|
275
|
-
function cachedReplacementDir(config, id) {
|
|
276
|
-
const root = config.paths.foreignCache;
|
|
277
|
-
if (!fs.existsSync(root)) return null;
|
|
278
|
-
const cached = fs
|
|
279
|
-
.readdirSync(root)
|
|
280
|
-
.filter((name) => name.startsWith(`${id}@`))
|
|
281
|
-
.map((name) => path.join(root, name))
|
|
282
|
-
.filter(isForeignCacheComplete);
|
|
283
|
-
return cached.length ? newestVersionDir(cached) : null;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* The file a declared replacement stages in place of a pathname this package
|
|
288
|
-
* does not own, or `null` when no replacement answers it.
|
|
289
|
-
*
|
|
290
|
-
* @param {import("./pathnames.mjs").PathnameForms} forms - The pathname,
|
|
291
|
-
* resolved, with `forms.own` already `false`.
|
|
292
|
-
* @param {object} config - The resolved build configuration.
|
|
293
|
-
* @param {object} opts
|
|
294
|
-
* @param {Map<string, object>} [opts.foreignIndex] - Every fetched
|
|
295
|
-
* dependency's own records, keyed by canonical address.
|
|
296
|
-
* @param {Function} opts.resolveReplacement - {@link
|
|
297
|
-
* module:engine/asset-replacement.resolveAssetReplacement}.
|
|
298
|
-
* @returns {{from: string, to: string}|null} The file, staged at the same
|
|
299
|
-
* `pdf`-relative path this address would have taken had this package
|
|
300
|
-
* shipped it.
|
|
301
|
-
* @throws {Error} When the replacement that answers the address has not
|
|
302
|
-
* been fetched.
|
|
303
|
-
*/
|
|
304
|
-
function stagedReplacementPath(forms, config, { foreignIndex = new Map(), resolveReplacement }) {
|
|
305
|
-
const address = localAssetAddress(forms, config);
|
|
306
|
-
if (!address) return null;
|
|
307
|
-
const relationships = assetReplacementRelationships(config);
|
|
308
|
-
if (!relationships.length) return null;
|
|
309
|
-
const hit = resolveReplacement(address, {
|
|
310
|
-
localPackage: config.contentPackage,
|
|
311
|
-
replacements: relationships.map((rel) => rel.package),
|
|
312
|
-
index: foreignIndex,
|
|
313
|
-
});
|
|
314
|
-
if (!hit) return null;
|
|
315
|
-
const assetPath = hit.record?.asset?.path;
|
|
316
|
-
if (!assetPath) return null;
|
|
317
|
-
const rel = relationships.find((entry) => entry.package === hit.package);
|
|
318
|
-
const dir = cachedReplacementDir(config, rel?.id ?? hit.package);
|
|
319
|
-
if (!dir) {
|
|
320
|
-
throw new Error(
|
|
321
|
-
`\`${hit.package}\` declares \`assetReplacement: true\` and carries \`${address}\`, ` +
|
|
322
|
-
"but its archive has not been fetched. Run `package-build deps fetch` first.",
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
return {
|
|
326
|
-
from: path.join(dir, ASSETS_SEGMENT, assetPath),
|
|
327
|
-
to: forms.pdf,
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
181
|
/**
|
|
332
182
|
* Where the book keeps the pictures its section plates are drawn over.
|
|
333
183
|
*
|
|
@@ -553,10 +403,6 @@ function configFileFor(config, rootDir) {
|
|
|
553
403
|
* {@link module:engine/pdf-pages}.
|
|
554
404
|
* @param {Function} [opts.readPages] - {@link
|
|
555
405
|
* module:engine/pdf-pages.readEntryPages}, for tests.
|
|
556
|
-
* @param {Function} [opts.resolveReplacement] - {@link
|
|
557
|
-
* module:engine/asset-replacement.resolveAssetReplacement}, for tests.
|
|
558
|
-
* Loaded from that module otherwise, and only when a relationship declares
|
|
559
|
-
* `assetReplacement: true` — a build with none never imports it.
|
|
560
406
|
* @returns {Promise<{built: boolean, reason: string|null, findings: object[],
|
|
561
407
|
* imageReports: object[], books: Array<{id: string, title: string,
|
|
562
408
|
* built: boolean, typ: string, pdf: string|null, stats: object}>,
|
|
@@ -576,7 +422,6 @@ export async function buildPdf({
|
|
|
576
422
|
version = "",
|
|
577
423
|
compile = true,
|
|
578
424
|
readPages = readEntryPages,
|
|
579
|
-
resolveReplacement,
|
|
580
425
|
} = {}) {
|
|
581
426
|
const resolved = config ?? loadPackConfig();
|
|
582
427
|
const findings = [];
|
|
@@ -642,7 +487,7 @@ export async function buildPdf({
|
|
|
642
487
|
out || resolved.pdf.out || path.join("build", "dist"),
|
|
643
488
|
);
|
|
644
489
|
|
|
645
|
-
const world = await prepareWorld(resolved, { contentBase, findings
|
|
490
|
+
const world = await prepareWorld(resolved, { contentBase, findings });
|
|
646
491
|
if (!world) return none(null);
|
|
647
492
|
|
|
648
493
|
const books = [];
|
|
@@ -944,11 +789,10 @@ function locateType(contentBase, record) {
|
|
|
944
789
|
* @param {object} shared - Options.
|
|
945
790
|
* @param {string} shared.contentBase - The content tree.
|
|
946
791
|
* @param {object[]} shared.findings - Collected here rather than thrown.
|
|
947
|
-
* @param {Function} [shared.resolveReplacement] - As {@link buildPdf}.
|
|
948
792
|
* @returns {Promise<object|null>} The world, or null when a site gate fired
|
|
949
793
|
* and the findings say why.
|
|
950
794
|
*/
|
|
951
|
-
async function prepareWorld(resolved, { contentBase, findings
|
|
795
|
+
async function prepareWorld(resolved, { contentBase, findings }) {
|
|
952
796
|
// The corpus, once — the same records the index, the site and the SQL
|
|
953
797
|
// tables read, so the book cannot select a note the other surfaces do not
|
|
954
798
|
// have.
|
|
@@ -1038,17 +882,6 @@ async function prepareWorld(resolved, { contentBase, findings, resolveReplacemen
|
|
|
1038
882
|
const md = createParser(resolved.icons);
|
|
1039
883
|
const glyphs = resolveIconGlyphs(resolved.icons, resolved.pdf.iconFonts, findings);
|
|
1040
884
|
|
|
1041
|
-
// Asked only where a relationship declares `assetReplacement: true`: with
|
|
1042
|
-
// none declared the resolver is never consulted, and staging gives the
|
|
1043
|
-
// answer it gives a package that declares no replacement. A caller may
|
|
1044
|
-
// supply its own, which is how the staging tests drive it.
|
|
1045
|
-
const stagingOpts = {
|
|
1046
|
-
foreignIndex: gates.foreign.index,
|
|
1047
|
-
resolveReplacement:
|
|
1048
|
-
resolveReplacement ??
|
|
1049
|
-
(assetReplacementRelationships(resolved).length ? resolveAssetReplacement : null),
|
|
1050
|
-
};
|
|
1051
|
-
|
|
1052
885
|
return {
|
|
1053
886
|
records,
|
|
1054
887
|
db,
|
|
@@ -1060,7 +893,6 @@ async function prepareWorld(resolved, { contentBase, findings, resolveReplacemen
|
|
|
1060
893
|
assets,
|
|
1061
894
|
md,
|
|
1062
895
|
glyphs,
|
|
1063
|
-
stagingOpts,
|
|
1064
896
|
};
|
|
1065
897
|
}
|
|
1066
898
|
|
|
@@ -1150,7 +982,6 @@ async function selectBook(book, index, { world, configFile, configText, findings
|
|
|
1150
982
|
* @param {{get: Function}} [shared.elsewhere] - From
|
|
1151
983
|
* {@link module:engine/pdf-pages.elsewhereFor}: the notes another book is
|
|
1152
984
|
* home to, which this book names by book and page rather than links.
|
|
1153
|
-
* @param {Function} [shared.resolveReplacement] - As {@link buildPdf}.
|
|
1154
985
|
* @param {object[]} shared.findings - Collected here rather than thrown.
|
|
1155
986
|
* @returns {Promise<{built: boolean, typ: string|null, pdf: string|null,
|
|
1156
987
|
* stats: object|null, imageReports?: object[]}>} What was written: the
|
|
@@ -1178,10 +1009,10 @@ async function buildBook(book, shared) {
|
|
|
1178
1009
|
* @returns {{from: string, to: string}|null} As {@link stagedImagePath}.
|
|
1179
1010
|
*/
|
|
1180
1011
|
const stage = (src) => {
|
|
1181
|
-
const staged = stagedImagePath(src, resolved
|
|
1012
|
+
const staged = stagedImagePath(src, resolved);
|
|
1182
1013
|
return staged && { ...staged, to: inBook(staged.to) };
|
|
1183
1014
|
};
|
|
1184
|
-
const { records, site, gates, byFile, sqlTables, assets, md, glyphs
|
|
1015
|
+
const { records, site, gates, byFile, sqlTables, assets, md, glyphs } = world;
|
|
1185
1016
|
|
|
1186
1017
|
fs.mkdirSync(outDir, { recursive: true });
|
|
1187
1018
|
|
package/engine/site-reach.mjs
CHANGED
|
@@ -211,3 +211,26 @@ export function pruneGenerated(html, outside) {
|
|
|
211
211
|
});
|
|
212
212
|
return kept.join("");
|
|
213
213
|
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Entries' links set as plain text where they lead outside a set of pages.
|
|
217
|
+
* Each link is the only child of a span carrying the class.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} html - A page body.
|
|
220
|
+
* @param {(href: string) => boolean} outside - Whether an `href` names a page
|
|
221
|
+
* of the site that is not to be linked.
|
|
222
|
+
* @param {string} className - The class the entries' links carry.
|
|
223
|
+
* @returns {string} The body with each such `<a>` replaced by its content.
|
|
224
|
+
*/
|
|
225
|
+
export function unlinkEntries(html, outside, className) {
|
|
226
|
+
const marked = new RegExp(
|
|
227
|
+
`<span\\b[^>]*?\\sclass=(?:"[^"]*\\b${className}\\b[^"]*"|'[^']*\\b${className}\\b[^']*')[^>]*>(<a\\b[^>]*>[\\s\\S]*?</a>)</span>`,
|
|
228
|
+
"gi",
|
|
229
|
+
);
|
|
230
|
+
return String(html ?? "").replace(marked, (span, anchor) => {
|
|
231
|
+
const match = new RegExp(ANCHOR.source, "i").exec(anchor);
|
|
232
|
+
const href = match ? (match[1] ?? match[2] ?? match[3] ?? "").replace(/&/g, "&") : "";
|
|
233
|
+
if (href === "" || !outside(href)) return span;
|
|
234
|
+
return anchor.replace(/^<a\b[^>]*>/, "").replace(/<\/a>$/, "");
|
|
235
|
+
});
|
|
236
|
+
}
|
package/engine/site-render.mjs
CHANGED
|
@@ -69,6 +69,7 @@ import fs from "node:fs";
|
|
|
69
69
|
import path from "node:path";
|
|
70
70
|
import { fileURLToPath } from "node:url";
|
|
71
71
|
|
|
72
|
+
import { LEXICAL_LINK_CLASS } from "./dictionary-layout.mjs";
|
|
72
73
|
import { createRenderer } from "./content-render.mjs";
|
|
73
74
|
import { resolvePathname } from "./pathnames.mjs";
|
|
74
75
|
import { minifyHtml } from "./html-minify.mjs";
|
|
@@ -82,7 +83,13 @@ import {
|
|
|
82
83
|
import { buildSite, gatesFailed } from "./site-build.mjs";
|
|
83
84
|
import { readCachedNavigation, siteSettings } from "./site-config.mjs";
|
|
84
85
|
import { atomFeed, robotsTxt, sitemapIndexXml, sitemapXml } from "./site-files.mjs";
|
|
85
|
-
import {
|
|
86
|
+
import {
|
|
87
|
+
pageAddress,
|
|
88
|
+
pruneGenerated,
|
|
89
|
+
reachablePages,
|
|
90
|
+
splitContent,
|
|
91
|
+
unlinkEntries,
|
|
92
|
+
} from "./site-reach.mjs";
|
|
86
93
|
import { prepareTreeSqlTables } from "./sql-tables.mjs";
|
|
87
94
|
import { placeGeography, searchMetadata } from "./site-search.mjs";
|
|
88
95
|
import {
|
|
@@ -369,7 +376,12 @@ export function writeSite({ site, pages, out, report, staticDir = SITE_STATIC_DI
|
|
|
369
376
|
},
|
|
370
377
|
report: sink,
|
|
371
378
|
});
|
|
372
|
-
const authoredOf = (page) =>
|
|
379
|
+
const authoredOf = (page) =>
|
|
380
|
+
unlinkEntries(
|
|
381
|
+
splitContent(page.content).authored,
|
|
382
|
+
(url) => pageOfUrl(url) !== undefined,
|
|
383
|
+
LEXICAL_LINK_CLASS,
|
|
384
|
+
);
|
|
373
385
|
const first = makeTheme({
|
|
374
386
|
visible: (url) => pageOfUrl(url) === undefined,
|
|
375
387
|
contentOf: authoredOf,
|
|
@@ -405,7 +417,10 @@ export function writeSite({ site, pages, out, report, staticDir = SITE_STATIC_DI
|
|
|
405
417
|
const contentOf = (page) => {
|
|
406
418
|
if (page.content === undefined) return page.content;
|
|
407
419
|
const { authored, generated } = splitContent(page.content);
|
|
408
|
-
return
|
|
420
|
+
return (
|
|
421
|
+
unlinkEntries(authored, outside, LEXICAL_LINK_CLASS) +
|
|
422
|
+
pruneGenerated(generated, outside)
|
|
423
|
+
);
|
|
409
424
|
};
|
|
410
425
|
const theme = makeTheme({ visible: (url) => !outside(url), contentOf, report });
|
|
411
426
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "24.0.
|
|
3
|
+
"version": "24.0.3",
|
|
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",
|
|
@@ -184,6 +184,7 @@
|
|
|
184
184
|
},
|
|
185
185
|
"devDependencies": {
|
|
186
186
|
"@changesets/cli": "^3.0.0",
|
|
187
|
+
"@heroiclands/fonts": "^0.1.5",
|
|
187
188
|
"@types/node": "^26.2.0",
|
|
188
189
|
"esbuild": "^0.28.2",
|
|
189
190
|
"markdownlint": "^0.42.0",
|