@heroiclands/package-build 23.9.0 → 23.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +2 -0
  3. package/bin/content-commands.mjs +197 -68
  4. package/docs/api.md +336 -68
  5. package/docs/authoring/links-and-markup.md +114 -47
  6. package/docs/commands.md +92 -3
  7. package/docs/content-format.md +6 -1
  8. package/docs/reference/format-details.md +129 -14
  9. package/docs/reference/note-types.md +4 -4
  10. package/engine/anchors.mjs +8 -6
  11. package/engine/base-compiler.mjs +6 -4
  12. package/engine/changelog-group.mjs +58 -34
  13. package/engine/changelog-lint.mjs +6 -2
  14. package/engine/compendiums.mjs +15 -1
  15. package/engine/compile-corpus.mjs +14 -1
  16. package/engine/content-charset.mjs +13 -1
  17. package/engine/content-html.mjs +18 -4
  18. package/engine/content-icons.mjs +15 -1
  19. package/engine/content-images.mjs +28 -9
  20. package/engine/content-index.mjs +102 -23
  21. package/engine/content-links.mjs +27 -12
  22. package/engine/content-session.mjs +247 -0
  23. package/engine/content-sources.mjs +110 -0
  24. package/engine/content-spans.mjs +87 -8
  25. package/engine/content-tables.mjs +1 -0
  26. package/engine/content-tasklists.mjs +20 -4
  27. package/engine/diagnostics.mjs +17 -6
  28. package/engine/extension-attributes.mjs +28 -5
  29. package/engine/first-use.mjs +371 -0
  30. package/engine/foundry-entries.mjs +12 -9
  31. package/engine/generate.mjs +13 -4
  32. package/engine/helpers.mjs +43 -14
  33. package/engine/index-records.mjs +1 -0
  34. package/engine/index.mjs +3 -0
  35. package/engine/journals.mjs +9 -5
  36. package/engine/language-data.mjs +2 -2
  37. package/engine/language-rows.mjs +4 -4
  38. package/engine/language-terms.mjs +4 -4
  39. package/engine/lexical-sections.mjs +5 -5
  40. package/engine/markup-validation.mjs +7 -2
  41. package/engine/metadata-index.mjs +1 -0
  42. package/engine/name-markup.mjs +114 -0
  43. package/engine/name-rows.mjs +70 -0
  44. package/engine/names.mjs +460 -0
  45. package/engine/note-addresses.mjs +25 -4
  46. package/engine/path-charset.mjs +24 -1
  47. package/engine/pdf-render.mjs +15 -1
  48. package/engine/site-index.mjs +5 -1
  49. package/engine/sql-tables.mjs +76 -15
  50. package/engine/stub-lint.mjs +4 -3
  51. package/engine/yaml-lint.mjs +3 -5
  52. package/hugo-theme/static/css/style.css +4 -0
  53. package/package.json +1 -1
  54. package/types/engine/anchors.d.mts +4 -2
  55. package/types/engine/compendiums.d.mts +5 -1
  56. package/types/engine/compile-corpus.d.mts +5 -2
  57. package/types/engine/content-charset.d.mts +5 -1
  58. package/types/engine/content-html.d.mts +5 -1
  59. package/types/engine/content-icons.d.mts +5 -1
  60. package/types/engine/content-images.d.mts +5 -1
  61. package/types/engine/content-index.d.mts +39 -19
  62. package/types/engine/content-links.d.mts +22 -8
  63. package/types/engine/content-session.d.mts +108 -0
  64. package/types/engine/content-sources.d.mts +32 -0
  65. package/types/engine/content-spans.d.mts +3 -5
  66. package/types/engine/content-tasklists.d.mts +5 -1
  67. package/types/engine/diagnostics.d.mts +10 -0
  68. package/types/engine/extension-attributes.d.mts +16 -2
  69. package/types/engine/first-use.d.mts +31 -0
  70. package/types/engine/foundry-entries.d.mts +11 -4
  71. package/types/engine/generate.d.mts +5 -1
  72. package/types/engine/helpers.d.mts +30 -7
  73. package/types/engine/index.d.mts +1 -0
  74. package/types/engine/journals.d.mts +5 -2
  75. package/types/engine/lexical-sections.d.mts +3 -2
  76. package/types/engine/markup-validation.d.mts +2 -1
  77. package/types/engine/name-markup.d.mts +176 -0
  78. package/types/engine/name-rows.d.mts +35 -0
  79. package/types/engine/names.d.mts +356 -0
  80. package/types/engine/note-addresses.d.mts +3 -3
  81. package/types/engine/path-charset.d.mts +16 -1
  82. package/types/engine/site-build.d.mts +2 -2
  83. package/types/engine/site-index.d.mts +3 -1
  84. package/types/engine/sql-tables.d.mts +7 -2
  85. package/types/engine/stub-lint.d.mts +3 -1
@@ -32,6 +32,7 @@ import { expandContentTables } from "./content-tables.mjs";
32
32
  import { NOTE_VOCABULARY, isGmNote } from "./note-vocabulary.mjs";
33
33
  import { encodeAddresses, flattenPublishedAddresses } from "./address-values.mjs";
34
34
  import { EVENT_COLUMNS, EVENT_SORT_COLUMNS, eventRows } from "./event-rows.mjs";
35
+ import { NAME_COLUMNS, namesRows } from "./name-rows.mjs";
35
36
  import {
36
37
  GLOSSARY_COLUMNS,
37
38
  AFFIX_COLUMNS,
@@ -272,6 +273,7 @@ export async function openNotesDatabase(
272
273
  await connection.run("SET threads=1");
273
274
  await createRelations(connection, jsonl, encoded, "", audience);
274
275
  await createEventRelation(connection, path.join(base, "events.jsonl"), encoded, "", audience);
276
+ await createNameRelation(connection, path.join(base, "names.jsonl"), encoded, "", audience);
275
277
  await createWordRelations(connection, path.join(base, "notes"), encoded, "", audience);
276
278
 
277
279
  // One schema per declared dependency, so `FROM sohl.notes` reads the notes
@@ -301,6 +303,13 @@ export async function openNotesDatabase(
301
303
  `${schema}.`,
302
304
  audience,
303
305
  );
306
+ await createNameRelation(
307
+ connection,
308
+ flat.replace(/\.jsonl$/, "-names.jsonl"),
309
+ depRecords,
310
+ `${schema}.`,
311
+ audience,
312
+ );
304
313
  await createWordRelations(
305
314
  connection,
306
315
  flat.replace(/\.jsonl$/, ""),
@@ -334,6 +343,7 @@ export async function openNotesDatabase(
334
343
  await connection.run(`CREATE VIEW ${WITH_STUBS_SCHEMA}.notes AS SELECT * FROM entries`);
335
344
  for (const relation of [
336
345
  "events",
346
+ "names",
337
347
  "words",
338
348
  "affixes",
339
349
  "fields",
@@ -542,6 +552,41 @@ async function createEventRelation(connection, file, records, prefix, audience)
542
552
  await createTableFromFile(connection, `${prefix}events`, readJsonAuto(file));
543
553
  }
544
554
 
555
+ /**
556
+ * Create authored name rows from own-note records, with audience filtering and
557
+ * fixed scalar types even when every optional classification is null.
558
+ * @param {object} connection - An open DuckDB connection.
559
+ * @param {string} file - Where to write the rows.
560
+ * @param {object[]} records - Index records, every Address a string.
561
+ * @param {string} prefix - A schema to qualify the relation name with.
562
+ * @param {"all"|"public"} audience - Whether to leave out GM notes.
563
+ * @returns {Promise<void>}
564
+ */
565
+ async function createNameRelation(connection, file, records, prefix, audience) {
566
+ const rows = namesRows(
567
+ audience === "public" ? records.filter((record) => !isGmNote(record)) : records,
568
+ );
569
+ const columnTypes = NAME_COLUMNS.map((column) => [
570
+ column,
571
+ column === "line" || column === "column" ? "BIGINT" : "VARCHAR",
572
+ ]);
573
+ if (!rows.length) {
574
+ const columns = columnTypes.map(([column, type]) => `NULL::${type} AS "${column}"`);
575
+ await connection.run(
576
+ `CREATE VIEW ${prefix}names AS SELECT ${columns.join(", ")} WHERE false`,
577
+ );
578
+ return;
579
+ }
580
+ fs.writeFileSync(file, rows.map((row) => JSON.stringify(row)).join("\n"));
581
+ const schema = columnTypes.map(([column, type]) => `'${column}': '${type}'`).join(", ");
582
+ await createTableFromFile(
583
+ connection,
584
+ `${prefix}names`,
585
+ `SELECT * FROM read_json('${file.replace(/'/g, "''")}', columns={${schema}}, ` +
586
+ "format='newline_delimited', maximum_object_size=20000000)",
587
+ );
588
+ }
589
+
545
590
  /**
546
591
  * Create a relation as a table, so the JSONL behind it is parsed once when the
547
592
  * database opens rather than on every query. A view over `read_json_auto`
@@ -1303,21 +1348,22 @@ export function attachGeneratedSections(prepared, generated) {
1303
1348
  * @param {object} [config] - The resolved build configuration.
1304
1349
  * @returns {Promise<void>}
1305
1350
  */
1306
- async function attachPreparedAnchors(prepared, records, contentBase, parsed, config) {
1351
+ async function attachPreparedAnchors(prepared, records, contentBase, parsed, config, syntax) {
1307
1352
  const { entriesForNote, foundryIdentities } = await import("./foundry-entries.mjs");
1308
- const identities = config ? foundryIdentities(config) : undefined;
1353
+ const identities = config ? { ...foundryIdentities(config), syntax } : undefined;
1309
1354
  for (const record of records) {
1310
1355
  if (!isNoteRecord(record)) continue;
1311
1356
  const file = noteFile(contentBase, record);
1312
1357
  const results = prepared.get(file);
1313
1358
  if (!results) continue;
1359
+ if (syntax) results.syntax = syntax;
1314
1360
  const { body } = parsed.get(file) ?? parseMarkdownFile(file);
1315
1361
  const expanded = expandContentTables(body, {
1316
1362
  source: file,
1317
1363
  sqlTables: results,
1318
1364
  pageLists: prepared.pageLists?.get(file),
1319
1365
  }).markdown;
1320
- results.anchors = collectAnchors(expanded);
1366
+ results.anchors = collectAnchors(expanded, 1, undefined, syntax);
1321
1367
  if (!identities || !record.address?.slug) continue;
1322
1368
  const frontmatter = authoredFrontmatter(record);
1323
1369
  const entries = entriesForNote(
@@ -1429,13 +1475,23 @@ export async function cachedDependencies(config) {
1429
1475
  * @param {Map<string, object>} [opts.foreignIndex] - The fetched indexes the
1430
1476
  * generated sections take in, where the caller holds them already; absent,
1431
1477
  * they are read from the cache.
1478
+ * @param {import("./content-session.mjs").ContentSyntax} [opts.syntax] - Exact-text scanner projections.
1432
1479
  * @returns {Promise<Map<string, object[]>|undefined>} Results by note path,
1433
1480
  * carrying `inline` and `pageLists` beside them, or nothing when the tree
1434
1481
  * holds no such directive and no note is given a generated section.
1435
1482
  */
1436
1483
  export async function prepareTreeSqlTables(
1437
1484
  contentBase,
1438
- { config, skipDirectories, records, parsed, database, audience = "all", foreignIndex } = {},
1485
+ {
1486
+ config,
1487
+ skipDirectories,
1488
+ records,
1489
+ parsed,
1490
+ database,
1491
+ audience = "all",
1492
+ foreignIndex,
1493
+ syntax,
1494
+ } = {},
1439
1495
  ) {
1440
1496
  // Imported here rather than at module scope: the index reaches the pack
1441
1497
  // compilers through `manifest-emit` → `journals`, so a static import from a
@@ -1514,7 +1570,7 @@ export async function prepareTreeSqlTables(
1514
1570
  pageOnly.inline = new Map();
1515
1571
  pageOnly.pageLists = preparePageLists(indexRecords, sources);
1516
1572
  attachGeneratedSections(pageOnly, derivedOnly);
1517
- await attachPreparedAnchors(pageOnly, indexRecords, contentBase, notes, config);
1573
+ await attachPreparedAnchors(pageOnly, indexRecords, contentBase, notes, config, syntax);
1518
1574
  return pageOnly;
1519
1575
  }
1520
1576
  // A cell links only where the address it would emit resolves, so a table
@@ -1559,7 +1615,7 @@ export async function prepareTreeSqlTables(
1559
1615
  });
1560
1616
  prepared.inline = await prepareInlineSqlExpressions(db, sources);
1561
1617
  attachGeneratedSections(prepared, generated);
1562
- await attachPreparedAnchors(prepared, indexRecords, contentBase, notes, config);
1618
+ await attachPreparedAnchors(prepared, indexRecords, contentBase, notes, config, syntax);
1563
1619
  return prepared;
1564
1620
  } finally {
1565
1621
  if (!database) await db.close();
@@ -1578,11 +1634,13 @@ export async function prepareTreeSqlTables(
1578
1634
  * @param {object[]} [opts.records] - Complete index records already derived.
1579
1635
  * @param {Map<string, {body: string, frontmatter: object|null}>} [opts.parsed] - Parsed notes.
1580
1636
  * @param {Map<string, object>} [opts.foreignIndex] - Published dependency indexes.
1637
+ * @param {(audience: "all"|"public") => Promise<Map<string, object[]>|undefined>} [opts.prepareAudience] -
1638
+ * Context-bound preparation shared with other operations in the same session.
1581
1639
  * @returns {Promise<Map<string, object[]>|undefined>} Prepared tables by source.
1582
1640
  */
1583
1641
  export async function prepareLinkSqlTables(
1584
1642
  contentBase,
1585
- { config, skipDirectories, records, parsed, foreignIndex } = {},
1643
+ { config, skipDirectories, records, parsed, foreignIndex, prepareAudience } = {},
1586
1644
  ) {
1587
1645
  const { indexRecordsFor } = await import("./content-index.mjs");
1588
1646
  const notes = parsed ?? new Map();
@@ -1619,14 +1677,17 @@ export async function prepareLinkSqlTables(
1619
1677
  selected.pageLists = new Map();
1620
1678
  selected.inline = new Map();
1621
1679
  for (const audience of new Set(sources.values())) {
1622
- const prepared = await prepareTreeSqlTables(contentBase, {
1623
- config,
1624
- skipDirectories,
1625
- records: allRecords,
1626
- parsed: notes,
1627
- foreignIndex,
1628
- audience,
1629
- });
1680
+ const prepared =
1681
+ prepareAudience ?
1682
+ await prepareAudience(audience)
1683
+ : await prepareTreeSqlTables(contentBase, {
1684
+ config,
1685
+ skipDirectories,
1686
+ records: allRecords,
1687
+ parsed: notes,
1688
+ foreignIndex,
1689
+ audience,
1690
+ });
1630
1691
  for (const [file, sourceAudience] of sources) {
1631
1692
  if (sourceAudience !== audience) continue;
1632
1693
  if (prepared?.has(file)) selected.set(file, prepared.get(file));
@@ -124,13 +124,14 @@ function checkNote({ fm, body, file, raw }) {
124
124
  * @param {object} opts
125
125
  * @param {readonly object[]} opts.records - Index records the caller derived.
126
126
  * @param {string} [opts.contentPackage] - Named in the counts line.
127
+ * @param {Map<string, import("./content-session.mjs").ContentNoteSource>} [opts.parsed] - Captured sources, with the file reader as fallback.
127
128
  * @returns {{findings: object[], counts: {full: number, draft: number,
128
129
  * stub: number}, byType: Array<{type: string, full: number, draft: number,
129
130
  * stub: number}>, oldestDrafts: Array<{file: string, modified: Date}>,
130
131
  * summary: string[]}} The findings, the ladder's counts, and the lines a
131
132
  * reader is shown as prose rather than as findings.
132
133
  */
133
- export function lintNoteStates(contentBase, { records, contentPackage } = {}) {
134
+ export function lintNoteStates(contentBase, { records, contentPackage, parsed } = {}) {
134
135
  const findings = [];
135
136
  const counts = { full: 0, draft: 0, stub: 0 };
136
137
  /** @type {Map<string, {full: number, draft: number, stub: number}>} */
@@ -149,8 +150,8 @@ export function lintNoteStates(contentBase, { records, contentPackage } = {}) {
149
150
  const absPath = noteFile(contentBase, record);
150
151
  const file = path.relative(process.cwd(), absPath);
151
152
  const fm = authoredFrontmatter(record);
152
- const { body } = parseMarkdownFile(absPath);
153
- const raw = fs.readFileSync(absPath, "utf8");
153
+ const source = parsed?.get(absPath) ?? parseMarkdownFile(absPath);
154
+ const { body, raw } = source;
154
155
 
155
156
  // The ladder, read exactly as the `entries` view reads it: the absent
156
157
  // address is the state, not a second body test beside it. A note with
@@ -29,6 +29,7 @@
29
29
  import { execFileSync } from "node:child_process";
30
30
  import fs from "node:fs";
31
31
  import path from "node:path";
32
+ import { splitFrontmatter } from "./diagnostics.mjs";
32
33
 
33
34
  /** File extensions that carry YAML, either wholly or in a frontmatter fence. */
34
35
  export const YAML_GLOBS = Object.freeze(["*.md", "*.yaml", "*.yml"]);
@@ -42,9 +43,6 @@ export const YAML_GLOBS = Object.freeze(["*.md", "*.yaml", "*.yml"]);
42
43
  */
43
44
  const ALWAYS_SKIP = Object.freeze([".git/", ".claude/"]);
44
45
 
45
- /** A markdown file's leading frontmatter fence. Mirrors `parseMarkdownFile`. */
46
- const FENCE = /^---\n([\s\S]*?)\n---/;
47
-
48
46
  /**
49
47
  * Present a markdown file to ESLint as the YAML it begins with.
50
48
  *
@@ -68,8 +66,8 @@ export const frontmatterProcessor = {
68
66
  // container file. Nothing is gained by claiming support for it.
69
67
  supportsAutofix: false,
70
68
  preprocess(text) {
71
- const fence = FENCE.exec(text);
72
- return fence ? [{ text: fence[1], filename: "0.yaml" }] : [];
69
+ const fence = splitFrontmatter(text);
70
+ return fence ? [{ text: fence.yaml, filename: "0.yaml" }] : [];
73
71
  },
74
72
  postprocess(messages) {
75
73
  return (messages[0] ?? []).map((message) => ({
@@ -86,6 +86,10 @@ body {
86
86
  -webkit-font-smoothing: antialiased;
87
87
  }
88
88
 
89
+ .name {
90
+ font-weight: 700;
91
+ }
92
+
89
93
  a {
90
94
  color: var(--accent);
91
95
  text-decoration: none;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "23.9.0",
3
+ "version": "23.10.0",
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",
@@ -12,10 +12,11 @@
12
12
  * picture's address to the role its asset declares, so a figure anchor's
13
13
  * `name` reads `Map 1` rather than `Figure 1` where it is due — see
14
14
  * {@link module:engine/content-figures.scanFigures}.
15
+ * @param {import("./content-session.mjs").ContentSyntax} [syntax] - Exact-text scanner projections.
15
16
  * @returns {Array<{slug: string, name: string, level: number, line: number,
16
17
  * kind: string}>} In document order.
17
18
  */
18
- export function collectAnchors(body: string, bodyLine?: number, resolveRole?: (address: string) => string | undefined): Array<{
19
+ export function collectAnchors(body: string, bodyLine?: number, resolveRole?: (address: string) => string | undefined, syntax?: import("./content-session.mjs").ContentSyntax): Array<{
19
20
  slug: string;
20
21
  name: string;
21
22
  level: number;
@@ -25,9 +26,10 @@ export function collectAnchors(body: string, bodyLine?: number, resolveRole?: (a
25
26
  /** Collisions involving a div, verse or inline-span anchor.
26
27
  * Heading and caption collisions are diagnosed by their existing scanners.
27
28
  * @param {string} body - Authored Markdown.
29
+ * @param {import("./content-session.mjs").ContentSyntax} [syntax] - Exact-text scanner projections.
28
30
  * @returns {Array<{line: number, column: number, message: string}>} Located collisions.
29
31
  */
30
- export function markupAnchorFindings(body: string): Array<{
32
+ export function markupAnchorFindings(body: string, syntax?: import("./content-session.mjs").ContentSyntax): Array<{
31
33
  line: number;
32
34
  column: number;
33
35
  message: string;
@@ -12,6 +12,8 @@
12
12
  * @param {string} [opts.stageDest] Directory the LevelDB packs are built
13
13
  * into, one subdirectory per pack. Defaults to the configured stage.
14
14
  * @param {string} [opts.packName] Restrict the run to a single pack.
15
+ * @param {import("./content-session.mjs").ContentSession} [opts.session] - Captured context shared with other operations.
16
+ * @param {boolean} [opts.reportPreparation=true] - Emit shared preparation findings unless the caller already reported them.
15
17
  * @throws {Error} If pack JSON generation reported any error. Packs compiled
16
18
  * from incomplete or empty JSON ship blank or short compendiums, and the
17
19
  * omission is invisible until a player looks for content that is not there
@@ -23,11 +25,13 @@
23
25
  * map image is gone before anyone notices it was ever at risk. See
24
26
  * {@link verifyPackSceneLevels}.
25
27
  */
26
- export function compilePacks({ config, sourcePacks, stageDest, packName, }?: {
28
+ export function compilePacks({ config, sourcePacks, stageDest, packName, session, reportPreparation, }?: {
27
29
  config?: object | undefined;
28
30
  sourcePacks?: string[] | undefined;
29
31
  stageDest?: string | undefined;
30
32
  packName?: string | undefined;
33
+ session?: import("./content-session.mjs").ContentSession | undefined;
34
+ reportPreparation?: boolean | undefined;
31
35
  }): Promise<void>;
32
36
  /**
33
37
  * Cleans and formats source JSON files, removing unnecessary permissions and flags and adding the proper spacing.
@@ -15,20 +15,23 @@
15
15
  * created when none is passed, and returned on the corpus either way — a note
16
16
  * the index refuses is a note the compile must still *report*, exactly as the
17
17
  * compile loop reported it when the loop was the first to see it.
18
- * @returns {Promise<{records: object[], parsed: Map<string, object>, linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[]}>} The corpus,
18
+ * @param {import("./content-session.mjs").ContentSession} [opts.session] - Captured context shared with other operations.
19
+ * @returns {Promise<{records: object[], parsed: Map<string, object>, linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[], syntax?: import("./content-session.mjs").ContentSyntax}>} The corpus,
19
20
  * its indexes, and the notes it could not record.
20
21
  */
21
- export function buildCompileCorpus({ contentBase, assetsBase, skipDirectories, router, config, problems, }: {
22
+ export function buildCompileCorpus({ contentBase, assetsBase, skipDirectories, router, config, problems, session, }: {
22
23
  contentBase: string;
23
24
  assetsBase?: string | undefined;
24
25
  skipDirectories: readonly string[];
25
26
  router: object;
26
27
  config?: object | undefined;
27
28
  problems?: object[] | undefined;
29
+ session?: import("./content-session.mjs").ContentSession | undefined;
28
30
  }): Promise<{
29
31
  records: object[];
30
32
  parsed: Map<string, object>;
31
33
  linkIndex: object;
32
34
  sqlTables: Map<string, object[]> | undefined;
33
35
  problems: object[];
36
+ syntax?: import("./content-session.mjs").ContentSyntax;
34
37
  }>;
@@ -87,13 +87,17 @@ export function checkText(text: string, file: string): Array<{
87
87
  * @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore
88
88
  * in addition to the dot-directories always skipped.
89
89
  * @param {readonly string[]} [opts.extensions] - File extensions to read.
90
+ * @param {Map<string, {raw: string}>} [opts.sources] - Exact captured source strings; omitted, walks and reads the tree.
90
91
  * @returns {{findings: Array<{file: string, line: number, column: number,
91
92
  * severity: "warning", message: string}>, files: number}} The findings, and how
92
93
  * many files produced them.
93
94
  */
94
- export function lintContentCharset(contentBase: string, { skipDirectories, extensions }?: {
95
+ export function lintContentCharset(contentBase: string, { skipDirectories, extensions, sources }?: {
95
96
  skipDirectories?: readonly string[] | undefined;
96
97
  extensions?: readonly string[] | undefined;
98
+ sources?: Map<string, {
99
+ raw: string;
100
+ }> | undefined;
97
101
  }): {
98
102
  findings: Array<{
99
103
  file: string;
@@ -47,12 +47,16 @@ export function checkHtml(body: string, file: string, { bodyLine, bodyColumn }?:
47
47
  * @param {object} [opts]
48
48
  * @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore
49
49
  * in addition to the dot-directories always skipped.
50
+ * @param {Map<string, {raw: string}>} [opts.sources] - Exact captured source strings; omitted, walks and reads the tree.
50
51
  * @returns {{findings: Array<{file: string, line: number, column: number,
51
52
  * severity: "warning", message: string}>, files: number}} The findings, and
52
53
  * how many files were read.
53
54
  */
54
- export function lintContentHtml(contentBase: string, { skipDirectories }?: {
55
+ export function lintContentHtml(contentBase: string, { skipDirectories, sources }?: {
55
56
  skipDirectories?: readonly string[] | undefined;
57
+ sources?: Map<string, {
58
+ raw: string;
59
+ }> | undefined;
56
60
  }): {
57
61
  findings: Array<{
58
62
  file: string;
@@ -119,12 +119,16 @@ export function checkIconRegistry(registry: {
119
119
  * @param {object} [opts]
120
120
  * @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore.
121
121
  * @param {IconRegistry} [opts.registry] - The package's registry.
122
+ * @param {Map<string, {raw: string}>} [opts.sources] - Exact captured source strings; omitted, walks and reads the tree.
122
123
  * @returns {{findings: Array<{file: string, line: number, column: number,
123
124
  * severity: "warning", message: string}>, files: number}} What it found.
124
125
  */
125
- export function lintContentIcons(contentBase: string, { skipDirectories, registry }?: {
126
+ export function lintContentIcons(contentBase: string, { skipDirectories, registry, sources }?: {
126
127
  skipDirectories?: readonly string[] | undefined;
127
128
  registry?: IconRegistry | undefined;
129
+ sources?: Map<string, {
130
+ raw: string;
131
+ }> | undefined;
128
132
  }): {
129
133
  findings: Array<{
130
134
  file: string;
@@ -215,13 +215,17 @@ export function checkImages(body: string, file: string, { bodyLine, bodyColumn,
215
215
  * in addition to the dot-directories always skipped.
216
216
  * @param {object} [opts.config] - The resolved build configuration, passed to
217
217
  * {@link checkImages} so an address is held to the Foundry surface too.
218
+ * @param {Map<string, {raw: string}>} [opts.sources] - Exact captured source strings; omitted, walks and reads the tree.
218
219
  * @returns {{findings: Array<{file: string, line: number, column: number|undefined,
219
220
  * severity: "error", message: string}>, files: number}} The findings, and how
220
221
  * many files were read.
221
222
  */
222
- export function lintContentImages(contentBase: string, { skipDirectories, config }?: {
223
+ export function lintContentImages(contentBase: string, { skipDirectories, config, sources }?: {
223
224
  skipDirectories?: readonly string[] | undefined;
224
225
  config?: object | undefined;
226
+ sources?: Map<string, {
227
+ raw: string;
228
+ }> | undefined;
225
229
  }): {
226
230
  findings: Array<{
227
231
  file: string;
@@ -100,6 +100,8 @@ export function asciiAliases(aliases: unknown): Array<string>;
100
100
  * and for the one question that decides whether the note publishes a page. A
101
101
  * caller that states no body is stating an empty one, and the note is a stub.
102
102
  * @param {number} [options.bodyLine] - The 1-based file line the body starts on.
103
+ * @param {number} [options.bodyColumn] - The 1-based UTF-16 column of its first character.
104
+ * @param {Iterable<string>} [options.nameTypes] - Additional bare name kinds from configured type declarations.
103
105
  * @param {object} [options.manifest] - The package manifest, which the Foundry
104
106
  * entries are derived against.
105
107
  * @param {object} [options.addressContext] - Address resolution context.
@@ -108,6 +110,7 @@ export function asciiAliases(aliases: unknown): Array<string>;
108
110
  * a picture's address to the role its asset declares, so a figure anchor's
109
111
  * `name` reads `Map 1` rather than `Figure 1` where it is due — see
110
112
  * {@link module:engine/content-figures.scanFigures}.
113
+ * @param {import("./content-session.mjs").ContentSyntax} [options.syntax] - Session scanner projections.
111
114
  * @returns {Record<string, any>} The record, keys sorted at every depth. A
112
115
  * **stub** — a note with an empty body, on a type an empty body suppresses —
113
116
  * carries `address` and `anchors` as `null`: it publishes no page, so it
@@ -119,7 +122,7 @@ export function asciiAliases(aliases: unknown): Array<string>;
119
122
  * otherwise be overwritten without a word. `file` and, where the file was
120
123
  * read, `position` ride on the error.
121
124
  */
122
- export function buildIndexRecord({ frontmatter, relPath, absPath, raw, contentPackage, body, bodyLine, manifest, addressContext, dateContext, resolveRole, }: {
125
+ export function buildIndexRecord({ frontmatter, relPath, absPath, raw, contentPackage, body, bodyLine, bodyColumn, nameTypes, manifest, addressContext, dateContext, resolveRole, syntax, }: {
123
126
  frontmatter: Record<string, any>;
124
127
  relPath: string;
125
128
  absPath?: string | undefined;
@@ -127,25 +130,19 @@ export function buildIndexRecord({ frontmatter, relPath, absPath, raw, contentPa
127
130
  contentPackage: string;
128
131
  body?: string | undefined;
129
132
  bodyLine?: number | undefined;
133
+ bodyColumn?: number | undefined;
134
+ nameTypes?: Iterable<string> | undefined;
130
135
  manifest?: object | undefined;
131
136
  addressContext?: object | undefined;
132
137
  dateContext?: object | undefined;
133
138
  resolveRole?: ((address: string) => string | undefined) | undefined;
139
+ syntax?: import("./content-session.mjs").ContentSyntax | undefined;
134
140
  }): Record<string, any>;
135
- /** Derive every index record belonging to one parsed note. */
136
- export function indexRecordsForNote({ frontmatter, body, bodyLine, relPath, absPath, raw, contentPackage, manifest, addressContext, dateContext, resolveRole, }: {
137
- frontmatter: any;
138
- body: any;
139
- bodyLine: any;
140
- relPath: any;
141
- absPath: any;
142
- raw: any;
143
- contentPackage: any;
144
- manifest: any;
145
- addressContext: any;
146
- dateContext: any;
147
- resolveRole: any;
148
- }): object[];
141
+ /**
142
+ * Derive every index record belonging to one parsed note.
143
+ * @param {Parameters<typeof buildIndexRecord>[0]} options - Authored note and its context.
144
+ */
145
+ export function indexRecordsForNote({ frontmatter, body, bodyLine, bodyColumn, nameTypes, relPath, absPath, raw, contentPackage, manifest, addressContext, dateContext, resolveRole, syntax, }: Parameters<typeof buildIndexRecord>[0]): object[];
149
146
  /**
150
147
  * Write each note's resolved culture into its record, as `data.culture`.
151
148
  *
@@ -243,10 +240,13 @@ export function resolveCultures(records: Array<Record<string, any>>, { contentPa
243
240
  * @param {Map<string, {body: string, frontmatter: object|null, raw: string, bodyLine?: number, bodyColumn?: number}>} [options.parsed] -
244
241
  * Filled with each note's source text, body coordinates and frontmatter as
245
242
  * authored, keyed by file, for reuse throughout the command.
243
+ * @param {Map<string, {raw: string}>} [options.sources] - Selected captured source strings.
244
+ * @param {import("./content-session.mjs").ContentSyntax} [options.syntax] - Session scanner projections.
245
+ * @param {Iterable<string>} [options.nameTypes] - Additional bare name kinds from configured type declarations.
246
246
  * @returns {Array<Record<string, any>>} The records, in a total order that does
247
247
  * not depend on directory-read order.
248
248
  */
249
- export function collectContentIndex(contentBase: string, { contentPackage, skipDirectories, assetsBase, manifest, problems, addressContext, foreignRecord, parsed, }: {
249
+ export function collectContentIndex(contentBase: string, { contentPackage, skipDirectories, assetsBase, manifest, problems, addressContext, foreignRecord, parsed, sources, syntax, nameTypes, }: {
250
250
  contentPackage: string;
251
251
  skipDirectories: readonly string[];
252
252
  assetsBase?: string | undefined;
@@ -263,6 +263,11 @@ export function collectContentIndex(contentBase: string, { contentPackage, skipD
263
263
  bodyLine?: number;
264
264
  bodyColumn?: number;
265
265
  }> | undefined;
266
+ sources?: Map<string, {
267
+ raw: string;
268
+ }> | undefined;
269
+ syntax?: import("./content-session.mjs").ContentSyntax | undefined;
270
+ nameTypes?: Iterable<string> | undefined;
266
271
  }): Array<Record<string, any>>;
267
272
  /**
268
273
  * Serialize records as JSON Lines.
@@ -303,9 +308,13 @@ export function serializeContentIndex(records: Array<Record<string, any>>): stri
303
308
  * Filled with each note's body and authored frontmatter, keyed by the note's
304
309
  * file. Handed to {@link module:engine/sql-tables.prepareTreeSqlTables}, it
305
310
  * spares that pass reading and parsing every note again.
311
+ * @param {Map<string, {raw: string}>} [opts.sources] - Selected source strings, in index walk order.
312
+ * @param {import("./content-session.mjs").ContentSyntax} [opts.syntax] - Session scanner projections.
313
+ * @param {Iterable<string>} [opts.nameTypes] - Bare name kinds, defaulting to configured item types.
314
+ * @param {Map<string, object>} [opts.foreignIndex] - Captured dependency address view.
306
315
  * @returns {object[]} One record per note, plus one per documentation entry.
307
316
  */
308
- export function indexRecordsFor({ contentBase, assetsBase, config, skipDirectories, problems, parsed, }?: {
317
+ export function indexRecordsFor({ contentBase, assetsBase, config, skipDirectories, problems, parsed, sources, syntax, nameTypes, foreignIndex, }?: {
309
318
  contentBase?: string | undefined;
310
319
  assetsBase?: string | undefined;
311
320
  config?: object | undefined;
@@ -318,6 +327,12 @@ export function indexRecordsFor({ contentBase, assetsBase, config, skipDirectori
318
327
  bodyLine?: number;
319
328
  bodyColumn?: number;
320
329
  }> | undefined;
330
+ sources?: Map<string, {
331
+ raw: string;
332
+ }> | undefined;
333
+ syntax?: import("./content-session.mjs").ContentSyntax | undefined;
334
+ nameTypes?: Iterable<string> | undefined;
335
+ foreignIndex?: Map<string, object> | undefined;
321
336
  }): object[];
322
337
  /**
323
338
  * Prepare the complete bodies a content-index publication describes.
@@ -325,12 +340,13 @@ export function indexRecordsFor({ contentBase, assetsBase, config, skipDirectori
325
340
  * corpus, with target maps limited to that source's declared prose slugs.
326
341
  * No output files are written, and the returned inputs are read-only to the emitter.
327
342
  *
328
- * @param {{contentBase?: string, config?: object}} [options]
343
+ * @param {{contentBase?: string, config?: object, session?: import("./content-session.mjs").ContentSession}} [options]
329
344
  * @returns {Promise<PreparedContentIndex>}
330
345
  */
331
- export function prepareContentIndex({ contentBase, config }?: {
346
+ export function prepareContentIndex({ contentBase, config, session }?: {
332
347
  contentBase?: string;
333
348
  config?: object;
349
+ session?: import("./content-session.mjs").ContentSession;
334
350
  }): Promise<PreparedContentIndex>;
335
351
  /**
336
352
  * Emit this package's content index.
@@ -426,6 +442,10 @@ export type PreparedContentIndex = {
426
442
  }>;
427
443
  generatedFrom?: number;
428
444
  }>;
445
+ /**
446
+ * - Session-local exact-text scanner projections.
447
+ */
448
+ syntax?: import("./content-session.mjs").ContentSyntax | undefined;
429
449
  };
430
450
  import { collectAnchors } from "./anchors.mjs";
431
451
  import { authoredFrontmatter } from "./index-records.mjs";
@@ -31,13 +31,11 @@ export function anchorsOf(body: string): Set<string>;
31
31
  * not hypothetical: this module carried its own anchor reader until the anchor
32
32
  * anchor half, and the two disagreed about which anchors existed.
33
33
  *
34
- * **The file is opened for its bytes and nothing else.** The index deliberately
35
- * carries no note *body*, and a link lives in the body — so each note is read
36
- * once, here, for the prose. Everything *about* the note — its frontmatter, its
37
- * addresses, its anchors — is already in the record, and none of it is derived
38
- * a second time. That is one read per note rather than the two this module did
39
- * before, since the walk read the file and it then read it again for the raw
40
- * text.
34
+ * **Raw sources can come from the caller's capture.** The published index
35
+ * carries no note body, while a link lives in the prose. A caller that already
36
+ * captured the sources supplies `parsed`, so this pass uses those exact raw
37
+ * strings. Without a captured string, it opens the file for its bytes.
38
+ * Frontmatter, addresses and anchors still come from the records.
41
39
  *
42
40
  * The index holds each compiled document under its complete Address. Readable
43
41
  * content uses `note`, and system documents use their declared system.
@@ -55,17 +53,33 @@ export function anchorsOf(body: string): Set<string>;
55
53
  * @param {object[]} [opts.records] - Index records the caller already derived,
56
54
  * so a command that also needs them — every one of them does, to answer its
57
55
  * `sql` tables — enumerates the corpus once rather than twice.
56
+ * @param {Map<string, {raw: string}>} [opts.parsed] - Sources captured alongside
57
+ * `records` in this invocation, keyed by absolute note path. A raw string is
58
+ * used verbatim; a missing string falls back to reading the file. The caller
59
+ * owns the map, which this function only reads.
58
60
  * @param {object[]} [opts.problems] - Collects the notes the index cannot
59
61
  * record, as diagnostics, instead of letting one of them abort the check
60
62
  * before it has reported anything else.
63
+ * @param {ReturnType<import("./metadata-index.mjs").loadForeignIndexes>} [opts.foreign] - Captured dependency address and reference views.
61
64
  * @returns {object} The notes, the index, and the resolvers built over it.
62
65
  */
63
- export function buildLinkIndex(contentBase: string, { config, skipDirectories, sqlTables, records, problems }?: {
66
+ export function buildLinkIndex(contentBase: string, { config, skipDirectories, sqlTables, records, parsed, problems, foreign: capturedForeign, }?: {
64
67
  config?: object | undefined;
65
68
  skipDirectories?: readonly string[] | undefined;
66
69
  sqlTables?: Map<string, object[]> | undefined;
67
70
  records?: object[] | undefined;
71
+ parsed?: Map<string, {
72
+ raw: string;
73
+ }> | undefined;
68
74
  problems?: object[] | undefined;
75
+ foreign?: {
76
+ index: Map<string, object>;
77
+ packages: Set<string>;
78
+ stale: Array<{
79
+ package: string;
80
+ reason: string;
81
+ }>;
82
+ } | undefined;
69
83
  }): object;
70
84
  /**
71
85
  * Every defect in the addresses a package homepage carries.