@heroiclands/package-build 24.0.2 → 24.0.4
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 +35 -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 +12 -3
- 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 +31 -74
- package/docs/reference/note-types.md +3 -2
- package/engine/content-charset.mjs +1 -0
- package/engine/content-scripts.mjs +22 -1
- package/engine/core-report.mjs +10 -2
- 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/map-lexicon.mjs +37 -5
- package/engine/pdf-build.mjs +9 -178
- package/engine/run-words.mjs +7 -7
- 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/map-lexicon.d.mts +3 -1
- package/types/engine/pdf-build.d.mts +5 -27
- package/types/engine/run-words.d.mts +2 -1
- 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
package/engine/map-lexicon.mjs
CHANGED
|
@@ -59,7 +59,7 @@ import { fromWrittenIn } from "./written-in.mjs";
|
|
|
59
59
|
import { writingsOf } from "./logographic.mjs";
|
|
60
60
|
import { FONTS_PACKAGE } from "./script-fonts.mjs";
|
|
61
61
|
import { GENERIC_FAMILIES, applyMap } from "./text-maps.mjs";
|
|
62
|
-
import { isLanguageRecord, noteAddressOf } from "./word-rows.mjs";
|
|
62
|
+
import { isLanguageRecord, lineageChain, noteAddressOf } from "./word-rows.mjs";
|
|
63
63
|
import { derivedForms } from "./word-forms.mjs";
|
|
64
64
|
import { LANGUAGE_SUBTYPE, glossText } from "./word-terms.mjs";
|
|
65
65
|
|
|
@@ -183,7 +183,7 @@ function lexiconOf(language, data) {
|
|
|
183
183
|
* @returns {string|undefined} The Address.
|
|
184
184
|
*/
|
|
185
185
|
function noteKey(note, contentPackage, types) {
|
|
186
|
-
if (
|
|
186
|
+
if (!contentPackage) return undefined;
|
|
187
187
|
const tuple = parseAddress(
|
|
188
188
|
note,
|
|
189
189
|
{ package: contentPackage, system: "note", type: "skill", types: new Set(types) },
|
|
@@ -248,7 +248,9 @@ function languageNotes(languages, contentPackage, records) {
|
|
|
248
248
|
|
|
249
249
|
/**
|
|
250
250
|
* Each configured language's lexicon, read from the language note its `note`
|
|
251
|
-
* names
|
|
251
|
+
* names and its family chain. The nearest exact NFC form supplies the whole
|
|
252
|
+
* word; grammar, registers and other settings remain the daughter's own.
|
|
253
|
+
* A language with no `note`, or one naming no language note, has none.
|
|
252
254
|
*
|
|
253
255
|
* @param {Iterable<Record<string, any>>|undefined} records - Index records.
|
|
254
256
|
* @param {{languages?: Readonly<Record<string, {tag: string, note?: string}>>, contentPackage?: string}} config -
|
|
@@ -256,9 +258,39 @@ function languageNotes(languages, contentPackage, records) {
|
|
|
256
258
|
* @returns {Map<string, Lexicon>} The lexicons by language name.
|
|
257
259
|
*/
|
|
258
260
|
export function lexiconsFromRecords(records, config = {}) {
|
|
261
|
+
const list = [...(records ?? [])];
|
|
262
|
+
const byKey = recordsByKey(list, config.contentPackage);
|
|
263
|
+
const types = new Set(["skill", ...list.map((record) => record?.type).filter(Boolean)]);
|
|
264
|
+
const lineage = {
|
|
265
|
+
has: (key) => isLanguage(byKey.get(key)),
|
|
266
|
+
parentOf: (key) => {
|
|
267
|
+
const record = byKey.get(key);
|
|
268
|
+
return noteKey(record.data?.family, record.package ?? config.contentPackage, types);
|
|
269
|
+
},
|
|
270
|
+
};
|
|
259
271
|
const lexicons = new Map();
|
|
260
|
-
for (const [name, record] of
|
|
261
|
-
|
|
272
|
+
for (const [name, { key, record }] of languageNotes(
|
|
273
|
+
config.languages,
|
|
274
|
+
config.contentPackage,
|
|
275
|
+
list,
|
|
276
|
+
)) {
|
|
277
|
+
if (!record) continue;
|
|
278
|
+
const chain = lineageChain(key, lineage);
|
|
279
|
+
let data = record.data;
|
|
280
|
+
if (chain.length > 1) {
|
|
281
|
+
const words = new Map();
|
|
282
|
+
for (const ancestor of chain)
|
|
283
|
+
for (const word of listOf(byKey.get(ancestor).data?.words)) {
|
|
284
|
+
if (!isMap(word) || typeof word.form !== "string") continue;
|
|
285
|
+
// The nearest complete definition wins before word matching
|
|
286
|
+
// folds case or splits compounds. Only words are inherited.
|
|
287
|
+
const form = word.form.normalize("NFC");
|
|
288
|
+
if (!words.has(form)) words.set(form, word);
|
|
289
|
+
}
|
|
290
|
+
data = { ...data, words: [...words.values()] };
|
|
291
|
+
}
|
|
292
|
+
lexicons.set(name, lexiconOf(name, data));
|
|
293
|
+
}
|
|
262
294
|
return lexicons;
|
|
263
295
|
}
|
|
264
296
|
|
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/run-words.mjs
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* @module
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
import {
|
|
39
|
+
import { lexiconsFromRecords } from "./map-lexicon.mjs";
|
|
40
40
|
import { RESERVED_TONGUE } from "./language-terms.mjs";
|
|
41
41
|
import { addWords, foldWord, lexiconWords } from "./word-forms.mjs";
|
|
42
42
|
import { WORD } from "./word-pattern.mjs";
|
|
@@ -82,7 +82,8 @@ export function registeredNameWords(data) {
|
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
84
|
* Each configured language's word book: one per language whose `note` names a
|
|
85
|
-
* language note.
|
|
85
|
+
* language note. Its effective lexicon shares the renderer's nearest inherited
|
|
86
|
+
* words, and derives forms and registered names from its own settings.
|
|
86
87
|
*
|
|
87
88
|
* @param {Iterable<Record<string, any>>|undefined} records - Index records.
|
|
88
89
|
* @param {{languages?: Readonly<Record<string, {tag: string, note?: string}>>, contentPackage?: string}} config -
|
|
@@ -92,11 +93,10 @@ export function registeredNameWords(data) {
|
|
|
92
93
|
export function wordBooksFromRecords(records, config = {}) {
|
|
93
94
|
const list = [...(records ?? [])];
|
|
94
95
|
const books = new Map();
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const names = registeredNameWords(record.data);
|
|
96
|
+
const lexicons = lexiconsFromRecords(list, config);
|
|
97
|
+
for (const [name, lexicon] of lexicons) {
|
|
98
|
+
const own = lexiconWords(lexicon.data);
|
|
99
|
+
const names = registeredNameWords(lexicon.data);
|
|
100
100
|
books.set(
|
|
101
101
|
name,
|
|
102
102
|
Object.freeze({
|
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.4",
|
|
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",
|
|
@@ -313,8 +313,7 @@ export type PathsInput = {
|
|
|
313
313
|
unpack?: string | undefined;
|
|
314
314
|
/**
|
|
315
315
|
* Where a dependency declaring
|
|
316
|
-
* `itemCatalog: true`
|
|
317
|
-
* `assetArchive: true` is unpacked.
|
|
316
|
+
* `itemCatalog: true` is unpacked.
|
|
318
317
|
* Inbound, and fetched rather than
|
|
319
318
|
* committed.
|
|
320
319
|
*/
|
|
@@ -486,13 +485,6 @@ export type RelationshipSpec = {
|
|
|
486
485
|
* does not hold. Needs a `manifest`.
|
|
487
486
|
*/
|
|
488
487
|
itemCatalog?: boolean | undefined;
|
|
489
|
-
/**
|
|
490
|
-
* Whether `deps fetch` unpacks this
|
|
491
|
-
* dependency's release archive for its
|
|
492
|
-
* asset bytes alone, building no item
|
|
493
|
-
* catalogue from it. Needs a `manifest`.
|
|
494
|
-
*/
|
|
495
|
-
assetArchive?: boolean | undefined;
|
|
496
488
|
/**
|
|
497
489
|
* Whether `deps fetch` fetches this
|
|
498
490
|
* dependency's content index. Default
|
|
@@ -504,16 +496,6 @@ export type RelationshipSpec = {
|
|
|
504
496
|
* index.
|
|
505
497
|
*/
|
|
506
498
|
contentIndex?: boolean | undefined;
|
|
507
|
-
/**
|
|
508
|
-
* Whether this dependency's asset
|
|
509
|
-
* tree answers this package's own asset
|
|
510
|
-
* addresses ahead of its local record.
|
|
511
|
-
* Default `false`. Refuses `contentIndex:
|
|
512
|
-
* false` on the same entry, since a
|
|
513
|
-
* replacement with no fetched index to
|
|
514
|
-
* check against resolves nothing.
|
|
515
|
-
*/
|
|
516
|
-
assetReplacement?: boolean | undefined;
|
|
517
499
|
};
|
|
518
500
|
/**
|
|
519
501
|
* How a generated documentation page is framed in the repository publishing it.
|
|
@@ -60,6 +60,11 @@ export const LAYOUT_REQUIRED_COLUMNS: Readonly<{
|
|
|
60
60
|
export const COMMON_MARK: "\u2022";
|
|
61
61
|
/** The word-entry span class whose Foundry rendering is a native TOC heading. */
|
|
62
62
|
export const DICTIONARY_ENTRY_CLASS: "dictionary-entry";
|
|
63
|
+
/**
|
|
64
|
+
* The class on every link an entry writes to another note, so a site can set
|
|
65
|
+
* the link as plain text where its target is not a page a reader can reach.
|
|
66
|
+
*/
|
|
67
|
+
export const LEXICAL_LINK_CLASS: "lexical-ref";
|
|
63
68
|
export type LayoutOptions = {
|
|
64
69
|
/**
|
|
65
70
|
* - Whether an Address can be
|
|
@@ -11,20 +11,6 @@ export function itemCatalogRelationships(config: object): Array<{
|
|
|
11
11
|
kind: string;
|
|
12
12
|
verified: string | undefined;
|
|
13
13
|
}>;
|
|
14
|
-
/**
|
|
15
|
-
* Every declared relationship that opted into supplying its release archive
|
|
16
|
-
* for asset bytes alone, with no item catalogue built from it.
|
|
17
|
-
*
|
|
18
|
-
* @param {object} config - The resolved build configuration.
|
|
19
|
-
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
20
|
-
* The opted-in relationships, in declaration order.
|
|
21
|
-
*/
|
|
22
|
-
export function assetArchiveRelationships(config: object): Array<{
|
|
23
|
-
id: string;
|
|
24
|
-
manifest: string;
|
|
25
|
-
kind: string;
|
|
26
|
-
verified: string | undefined;
|
|
27
|
-
}>;
|
|
28
14
|
/**
|
|
29
15
|
* The cache directory for one dependency at one version.
|
|
30
16
|
*
|
|
@@ -109,26 +95,6 @@ export function fetchCatalog(config: object, rel: {
|
|
|
109
95
|
id: string;
|
|
110
96
|
manifest: string;
|
|
111
97
|
}): Promise<string>;
|
|
112
|
-
/**
|
|
113
|
-
* Fetch one dependency's release archive for its asset bytes alone, building
|
|
114
|
-
* no item catalogue from it.
|
|
115
|
-
*
|
|
116
|
-
* Idempotent: a complete cache for the resolved version is left alone. Where a
|
|
117
|
-
* relationship also declares `itemCatalog: true`, {@link fetchCatalog} already
|
|
118
|
-
* unpacked this same archive into this same cache directory, and
|
|
119
|
-
* {@link fetchAllCatalogs} does not call this function for it — the two never
|
|
120
|
-
* download the same bytes twice.
|
|
121
|
-
*
|
|
122
|
-
* @param {object} config - The resolved build configuration.
|
|
123
|
-
* @param {{id: string, manifest: string, verified?: string}} rel - The
|
|
124
|
-
* declared relationship.
|
|
125
|
-
* @returns {Promise<string>} The dependency's cache directory.
|
|
126
|
-
*/
|
|
127
|
-
export function fetchAssetArchive(config: object, rel: {
|
|
128
|
-
id: string;
|
|
129
|
-
manifest: string;
|
|
130
|
-
verified?: string;
|
|
131
|
-
}): Promise<string>;
|
|
132
98
|
/**
|
|
133
99
|
* Fill the cache from a locally built artifact rather than a release.
|
|
134
100
|
*
|
|
@@ -202,10 +168,6 @@ export function fetchAllMetadata(config: object): Promise<number>;
|
|
|
202
168
|
/**
|
|
203
169
|
* Fetch every opted-in dependency. The `deps fetch` command.
|
|
204
170
|
*
|
|
205
|
-
* A relationship declaring both `itemCatalog: true` and `assetArchive: true`
|
|
206
|
-
* is fetched once: {@link fetchCatalog} already unpacks the archive on its
|
|
207
|
-
* way to extracting the item catalogue, so this skips the archive-only fetch
|
|
208
|
-
* for any id {@link fetchCatalog} already handled.
|
|
209
171
|
*
|
|
210
172
|
* @param {object} config - The resolved build configuration.
|
|
211
173
|
* @returns {Promise<number>} How many dependencies were fetched.
|
|
@@ -215,7 +215,7 @@ export function joinRows(records: Iterable<Record<string, any>>): object[];
|
|
|
215
215
|
*/
|
|
216
216
|
export function markRows(records: Iterable<Record<string, any>>): object[];
|
|
217
217
|
/**
|
|
218
|
-
* The `skeleton` rows: one per `data.sounds.skeleton` entry, `second`
|
|
218
|
+
* The `skeleton` rows: one per `data.sounds.skeleton` entry, `second` and `third` lists.
|
|
219
219
|
*
|
|
220
220
|
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
221
221
|
* @returns {object[]}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Each configured language's lexicon, read from the language note its `note`
|
|
3
|
-
* names
|
|
3
|
+
* names and its family chain. The nearest exact NFC form supplies the whole
|
|
4
|
+
* word; grammar, registers and other settings remain the daughter's own.
|
|
5
|
+
* A language with no `note`, or one naming no language note, has none.
|
|
4
6
|
*
|
|
5
7
|
* @param {Iterable<Record<string, any>>|undefined} records - Index records.
|
|
6
8
|
* @param {{languages?: Readonly<Record<string, {tag: string, note?: string}>>, contentPackage?: string}} config -
|