@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.
@@ -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 (typeof note !== "string" || !contentPackage) return undefined;
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. A language with no `note`, or one naming no language note, has none.
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 languageRecordsByName(records, config))
261
- lexicons.set(name, lexiconOf(name, record.data));
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
 
@@ -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 stages instead from a declared replacement's cached archive,
170
- * when `opts.resolveReplacement` — the pure function
171
- * {@link module:engine/asset-replacement.resolveAssetReplacement} —
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, opts = {}) {
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, resolveReplacement });
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, resolveReplacement }) {
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, world.stagingOpts);
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, stagingOpts } = world;
1015
+ const { records, site, gates, byFile, sqlTables, assets, md, glyphs } = world;
1185
1016
 
1186
1017
  fs.mkdirSync(outDir, { recursive: true });
1187
1018
 
@@ -36,7 +36,7 @@
36
36
  * @module
37
37
  */
38
38
 
39
- import { languageRecordsByName } from "./map-lexicon.mjs";
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 languages = languageRecordsByName(list, config);
96
- if (!languages.size) return books;
97
- for (const [name, record] of languages) {
98
- const own = lexiconWords(record.data);
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({
@@ -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(/&amp;/g, "&") : "";
233
+ if (href === "" || !outside(href)) return span;
234
+ return anchor.replace(/^<a\b[^>]*>/, "").replace(/<\/a>$/, "");
235
+ });
236
+ }
@@ -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 { pageAddress, pruneGenerated, reachablePages, splitContent } from "./site-reach.mjs";
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) => splitContent(page.content).authored;
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 authored + pruneGenerated(generated, outside);
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.2",
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` or
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` a list.
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. A language with no `note`, or one naming no language note, has none.
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 -