@heroiclands/package-build 23.7.1 → 23.9.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 (96) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +32 -0
  3. package/bin/content-commands.mjs +32 -10
  4. package/ci/check-banners.mjs +37 -19
  5. package/container.mjs +48 -10
  6. package/docs/api.md +402 -58
  7. package/docs/authoring/first-note.md +1 -1
  8. package/docs/authoring/frontmatter.md +30 -1
  9. package/docs/authoring/links-and-markup.md +154 -56
  10. package/docs/commands.md +31 -12
  11. package/docs/content-format.md +25 -2
  12. package/docs/design.md +31 -0
  13. package/docs/diagnostics.md +1 -1
  14. package/docs/guides/e2e.md +37 -1
  15. package/docs/guides/packs.md +2 -1
  16. package/docs/guides/reference-pages.md +86 -63
  17. package/docs/project-setup.md +30 -15
  18. package/docs/reference/format-details.md +313 -132
  19. package/docs/reference/note-types.md +191 -174
  20. package/e2e.mjs +89 -2
  21. package/engine/anchors.mjs +7 -20
  22. package/engine/base-compiler.mjs +11 -18
  23. package/engine/compile-corpus.mjs +5 -3
  24. package/engine/content-alerts.mjs +4 -2
  25. package/engine/content-blocks.mjs +18 -36
  26. package/engine/content-format.yaml +1 -0
  27. package/engine/content-index.mjs +447 -33
  28. package/engine/content-links.mjs +43 -15
  29. package/engine/content-poetry.mjs +9 -0
  30. package/engine/content-tables.mjs +8 -4
  31. package/engine/culture-reach.mjs +61 -34
  32. package/engine/data-keys.mjs +3 -1
  33. package/engine/diagnostics.mjs +24 -11
  34. package/engine/dictionary-layout.mjs +151 -19
  35. package/engine/dictionary-rows.mjs +129 -79
  36. package/engine/foundry-entries.mjs +21 -3
  37. package/engine/frontmatter-lint.mjs +1 -1
  38. package/engine/generate.mjs +12 -22
  39. package/engine/glossary-entries.mjs +11 -11
  40. package/engine/heading-attributes.mjs +21 -28
  41. package/engine/helpers.mjs +20 -12
  42. package/engine/infobox.mjs +3 -0
  43. package/engine/journals.mjs +15 -2
  44. package/engine/language-data.mjs +53 -0
  45. package/engine/language-terms.mjs +86 -0
  46. package/engine/language-words.mjs +2 -2
  47. package/engine/lexical-sections.mjs +247 -0
  48. package/engine/markdownlint-run.mjs +12 -2
  49. package/engine/markup-validation.mjs +34 -0
  50. package/engine/note-addresses.mjs +1 -0
  51. package/engine/note-claims.mjs +21 -10
  52. package/engine/note-package.mjs +3 -5
  53. package/engine/note-state.mjs +9 -3
  54. package/engine/note-vocabulary.mjs +36 -7
  55. package/engine/pdf-build.mjs +17 -10
  56. package/engine/prose-lint.mjs +12 -6
  57. package/engine/related-pages.mjs +22 -3
  58. package/engine/see-also.mjs +150 -0
  59. package/engine/site-build.mjs +21 -3
  60. package/engine/site-preview.mjs +4 -2
  61. package/engine/sql-tables.mjs +287 -53
  62. package/engine/standings.mjs +14 -12
  63. package/engine/stub-lint.mjs +6 -2
  64. package/engine/web-wikilinks.mjs +8 -6
  65. package/engine/word-rows.mjs +20 -12
  66. package/engine/word-terms.mjs +15 -6
  67. package/hugo-theme/layouts/partials/related.html +25 -8
  68. package/package.json +2 -1
  69. package/types/container.d.mts +4 -3
  70. package/types/e2e.d.mts +28 -0
  71. package/types/engine/base-compiler.d.mts +2 -2
  72. package/types/engine/compile-corpus.d.mts +2 -1
  73. package/types/engine/content-index.d.mts +85 -8
  74. package/types/engine/content-tables.d.mts +2 -1
  75. package/types/engine/culture-reach.d.mts +22 -2
  76. package/types/engine/data-keys.d.mts +7 -0
  77. package/types/engine/diagnostics.d.mts +7 -6
  78. package/types/engine/dictionary-layout.d.mts +35 -4
  79. package/types/engine/dictionary-rows.d.mts +9 -0
  80. package/types/engine/foundry-entries.d.mts +3 -1
  81. package/types/engine/heading-attributes.d.mts +2 -2
  82. package/types/engine/helpers.d.mts +11 -5
  83. package/types/engine/language-terms.d.mts +7 -0
  84. package/types/engine/lexical-sections.d.mts +95 -0
  85. package/types/engine/markdownlint-run.d.mts +3 -1
  86. package/types/engine/markup-validation.d.mts +14 -0
  87. package/types/engine/note-addresses.d.mts +10 -0
  88. package/types/engine/note-claims.d.mts +7 -1
  89. package/types/engine/note-package.d.mts +3 -1
  90. package/types/engine/note-state.d.mts +2 -1
  91. package/types/engine/note-vocabulary.d.mts +1 -0
  92. package/types/engine/related-pages.d.mts +10 -1
  93. package/types/engine/see-also.d.mts +55 -0
  94. package/types/engine/site-build.d.mts +4 -1
  95. package/types/engine/sql-tables.d.mts +40 -6
  96. package/types/engine/word-rows.d.mts +9 -7
@@ -73,12 +73,12 @@ import { resolveEmbeds } from "./content-embeds.mjs";
73
73
  /**
74
74
  * Whether a page's declared anchors include the slug a link names.
75
75
  *
76
- * A local page's entry carries a `Set` (built by {@link module:engine/site-index});
77
- * a foreign one carries the `{slug: uuid}` map a fetched manifest publishes.
78
- * `undefined` means neither build recorded an answer, which keeps a target
79
- * this check cannot speak for exactly as unchecked as it always was.
76
+ * A local page carries a `Set` built by {@link module:engine/site-index}.
77
+ * A foreign page declares prose anchors in its note metadata, independently
78
+ * of whether it has a Foundry target map. An empty collection declares none.
79
+ * An absent collection leaves the target unchecked.
80
80
  *
81
- * @param {Set<string>|Record<string, string>|undefined} anchors - The page's
81
+ * @param {Set<string>|Record<string, string>|Array<{slug: string, kind: string}>|undefined} anchors - The page's
82
82
  * declared anchors, or `undefined` where none were recorded.
83
83
  * @param {string} slug - The anchor a link names.
84
84
  * @returns {boolean} Whether the anchor is declared, or `true` when `anchors`
@@ -87,6 +87,8 @@ import { resolveEmbeds } from "./content-embeds.mjs";
87
87
  */
88
88
  function hasAnchor(anchors, slug) {
89
89
  if (!anchors) return true;
90
+ if (Array.isArray(anchors))
91
+ return anchors.some((anchor) => anchor.kind === "prose" && anchor.slug === slug);
90
92
  return anchors instanceof Set ? anchors.has(slug) : Object.hasOwn(anchors, slug);
91
93
  }
92
94
 
@@ -478,7 +480,7 @@ export function resolveWebWikilinks(body, ctx) {
478
480
  label ?? hit.name ?? target,
479
481
  );
480
482
  }
481
- if (anchor && hit.url && !hasAnchor(hit.anchors, anchor)) {
483
+ if (anchor && hit.url && !hasAnchor(hit.noteAnchors ?? hit.anchors, anchor)) {
482
484
  return report(
483
485
  all,
484
486
  { target, reason: "unknown-anchor", anchor, addressed: true },
@@ -82,15 +82,17 @@ const isMap = (value) => Boolean(value) && typeof value === "object" && !Array.i
82
82
  const bare = (form) => String(form).replace(/^-+|-+$/g, "");
83
83
 
84
84
  /**
85
- * A lookup from a built piece to its gloss, over the words (stems included)
86
- * and affixes of one or more languages.
85
+ * A lookup from a built piece to its gloss, over the words (stems included),
86
+ * affixes and elements of one or more languages.
87
87
  *
88
- * Languages are given nearest first: a language's own words, then its own
89
- * affixes, then the next language's, so a daughter's entry wins over its
90
- * parent's. A piece matches an entry written exactly as it is, and failing
91
- * that one written with the same letters and different joining hyphens, so
88
+ * For each spelling, languages are given nearest first: a language's own
89
+ * words, then its affixes and elements, then the next language's. A piece
90
+ * matches an exact spelling across the lineage first, and failing that one
91
+ * written with the same letters and different joining hyphens, so
92
92
  * `wulf-` finds the word `wulf`. Within a language a word's own gloss wins over
93
- * an affix's.
93
+ * an affix's, and both precede an element's sense. Elements also match their
94
+ * declared variants; affixes also match their front-vowel forms. Spelling,
95
+ * including case and accents, is preserved.
94
96
  *
95
97
  * @param {...object} datas - Each language note's `data`, nearest first.
96
98
  * @returns {(piece: string) => string|null} The gloss, or `null` where none resolves.
@@ -98,18 +100,24 @@ const bare = (form) => String(form).replace(/^-+|-+$/g, "");
98
100
  export function pieceGlosses(...datas) {
99
101
  const exact = new Map();
100
102
  const loose = new Map();
101
- const take = (entries) => {
103
+ const take = (entries, key = "gloss", alternateKey = "") => {
102
104
  for (const entry of Array.isArray(entries) ? entries : []) {
103
105
  if (!isMap(entry) || typeof entry.form !== "string") continue;
104
- const gloss = glossText(entry.gloss);
106
+ const gloss = glossText(entry[key]);
105
107
  if (gloss === null) continue;
106
- if (!exact.has(entry.form)) exact.set(entry.form, gloss);
107
- if (!loose.has(bare(entry.form))) loose.set(bare(entry.form), gloss);
108
+ const alternate = entry[alternateKey];
109
+ const forms = [entry.form, ...(Array.isArray(alternate) ? alternate : [alternate])];
110
+ for (const form of forms) {
111
+ if (typeof form !== "string") continue;
112
+ if (!exact.has(form)) exact.set(form, gloss);
113
+ if (!loose.has(bare(form))) loose.set(bare(form), gloss);
114
+ }
108
115
  }
109
116
  };
110
117
  for (const data of datas) {
111
118
  take(data?.words);
112
- take(data?.affixes);
119
+ take(data?.affixes, "gloss", "front");
120
+ take(data?.elements, "sense", "variants");
113
121
  }
114
122
  return (piece) => exact.get(piece) ?? loose.get(bare(piece)) ?? null;
115
123
  }
@@ -136,12 +136,20 @@ export const WORD_FIELDS = Object.freeze([
136
136
  "A class from the master word classes: a base, then the modifiers the language " +
137
137
  "declares in `classModifiers`.",
138
138
  }),
139
+ Object.freeze({
140
+ name: "plural",
141
+ kind: "string",
142
+ shape: "a string",
143
+ describe:
144
+ "The noun's explicit plural, where it differs from the language's optional plural " +
145
+ "rule. A dictionary uses this form before the rule; no rule agreement is checked.",
146
+ }),
139
147
  Object.freeze({
140
148
  name: "field",
141
149
  kind: "string",
142
150
  required: true,
143
151
  shape: "one of the language's `fields` names",
144
- describe: "The field the word is grouped under, named in the language's `fields`.",
152
+ describe: "The field tag beside the dictionary entry, named in the language's `fields`.",
145
153
  }),
146
154
  Object.freeze({
147
155
  name: "gloss",
@@ -376,27 +384,28 @@ export const GLOSSARY_ENTRY_FIELDS = Object.freeze([
376
384
  name: "name_sort",
377
385
  kind: "string",
378
386
  shape: "a string",
379
- describe: "Its sort key; the name when unstated.",
387
+ describe: "Its sort key; the dictionary word's sort key, or the name, when unstated.",
380
388
  }),
381
389
  Object.freeze({
382
390
  name: "common",
383
391
  kind: "boolean",
384
392
  shape: "`true` or `false`",
385
- describe: "A word a player is expected to know by heart; `false` when unstated.",
393
+ describe: "Core vocabulary for this glossary's owner; `false` when unstated.",
386
394
  }),
387
395
  Object.freeze({
388
396
  name: "definition",
389
397
  kind: "string",
390
398
  shape: "a string",
391
399
  describe:
392
- "The sense text. Stated, it creates the entry or replaces every sense the " +
393
- "entry's other sources give it.",
400
+ "The sense text, overriding the word's or governance sources' senses. Unstated, " +
401
+ "a named dictionary word supplies all its senses and class; a named rank or office supplies its descriptions.",
394
402
  }),
395
403
  Object.freeze({
396
404
  name: "pronunciation",
397
405
  kind: "string",
398
406
  shape: "a respelling such as `sol-MEH-reh`",
399
- describe: "A respelling, held to the same rule as a word's.",
407
+ describe:
408
+ "A respelling, held to the same rule as a word's; the dictionary word's when unstated.",
400
409
  }),
401
410
  Object.freeze({
402
411
  name: "language",
@@ -6,11 +6,13 @@
6
6
 
7
7
  Structure in front matter:
8
8
  related:
9
+ seeAlso: [{ title, url, type, gloss? }, ...] # deliberate references
9
10
  backlinks: [{ title, url, type }, ...] # pages that link here
10
11
  mentions: [{ title, url, type }, ...] # pages this page links to
11
12
 
12
13
  Rendering strategy:
13
- - Merge both directions into a single deduped pool (direction
14
+ - Render deliberate references in authored order, with their glosses.
15
+ - Merge both inferred directions into a single deduped pool (direction
14
16
  doesn't matter to a reader — a connection is a connection).
15
17
  - Group by the referenced page's `type`.
16
18
  - Emit one labelled group per type, its entries laid out as a
@@ -20,7 +22,7 @@
20
22
  - The whole block is wrapped in a bordered, tinted card so it
21
23
  visually sets off the end of the article.
22
24
 
23
- Silent disappear: a page with no `related`, or whose two lists are both
25
+ Silent disappear: a page with no `related`, or whose lists are all
24
26
  empty, emits nothing at all rather than an empty heading. The rows are
25
27
  computed first and the <section> is rendered only when there is something
26
28
  to put in it.
@@ -34,15 +36,23 @@
34
36
  {{- /* `default dict` keeps the lookups below safe on a page with no
35
37
  `related` at all, so the whole computation stays flat. */ -}}
36
38
  {{- $rel := .Params.related | default dict -}}
39
+ {{- $seeAlso := or $rel.seealso slice -}}
37
40
  {{- $backlinks := or $rel.backlinks slice -}}
38
41
  {{- $mentions := or $rel.mentions slice -}}
39
42
 
40
43
  {{- /* Merge both directions, deduped by URL. `union` can't hash
41
44
  slices of maps, so we walk the two lists in order and dedupe
42
- by URL manually. Mentions come first so explicit author
43
- choices win when a page appears in both directions. */ -}}
45
+ by URL manually. Deliberate references take priority; mentions
46
+ precede backlinks among inferred connections. */ -}}
44
47
  {{- $seen := slice -}}
45
48
  {{- $merged := slice -}}
49
+ {{- $deliberate := slice -}}
50
+ {{- range $seeAlso -}}
51
+ {{- if not (in $seen .url) -}}
52
+ {{- $seen = $seen | append .url -}}
53
+ {{- $deliberate = $deliberate | append . -}}
54
+ {{- end -}}
55
+ {{- end -}}
46
56
  {{- range $mentions -}}
47
57
  {{- if not (in $seen .url) -}}
48
58
  {{- $seen = $seen | append .url -}}
@@ -101,12 +111,19 @@
101
111
  {{- $rows = sort $rows "label" "asc" -}}
102
112
  {{- end -}}
103
113
 
104
- {{- /* Deliberately not indented under the `if`: keeping the markup at
105
- column 0 leaves the emitted HTML byte-identical to what pages with
106
- `related` rendered before the guard was added. */ -}}
107
- {{- if $rows -}}
114
+ {{- if or $rows $deliberate -}}
108
115
  <section class="related" aria-labelledby="related-heading" data-pagefind-ignore>
109
116
  <h2 id="related-heading" class="related-heading">Related</h2>
117
+ {{- with $deliberate }}
118
+ <div class="related-group">
119
+ <h3 class="related-group-label">See also</h3>
120
+ <ul class="related-group-grid">
121
+ {{- range . }}
122
+ <li><a href="{{ .url }}">{{ .title }}</a>{{ with .gloss }} — {{ . }}{{ end }}</li>
123
+ {{- end }}
124
+ </ul>
125
+ </div>
126
+ {{- end }}
110
127
  {{- range $rows }}
111
128
  <div class="related-group">
112
129
  <h3 class="related-group-label">{{ .label }}</h3>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "23.7.1",
3
+ "version": "23.9.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",
@@ -132,6 +132,7 @@
132
132
  "dependencies": {
133
133
  "@duckdb/node-api": "^1.5.5-r.4",
134
134
  "@foundryvtt/foundryvtt-cli": "^3.0.4",
135
+ "@puppeteer/browsers": "^3.2.5",
135
136
  "@viz-js/viz": "^3.30.0",
136
137
  "acorn": "^8.18.0",
137
138
  "archiver": "^8.0.0",
@@ -247,8 +247,8 @@ export function s3RunArgs(plan: S3Plan): string[];
247
247
  */
248
248
  export function s3ShellArgs(plan: S3Plan, command: string): string[];
249
249
  /**
250
- * Start the S3 server on its network and create the bucket, unless it is
251
- * already running, and copy the `awsConfig` into the data root.
250
+ * Start or reuse the S3 server on its network, wait for readiness, ensure the
251
+ * bucket exists, and copy the `awsConfig` into the data root.
252
252
  *
253
253
  * @param {object} opts
254
254
  * @param {S3Plan} opts.plan - The S3 plan.
@@ -261,7 +261,8 @@ export function s3ShellArgs(plan: S3Plan, command: string): string[];
261
261
  * @param {(ms: number) => void} [opts.sleep] - Waits.
262
262
  * @param {number} [opts.attempts] - How often to ask the server whether it is
263
263
  * ready.
264
- * @throws {Error} When an input file is missing or the server never answers.
264
+ * @throws {Error} When an input is not a readable regular file, a reused
265
+ * identities mount is incompatible, or the S3 HTTP service never answers.
265
266
  */
266
267
  export function ensureS3({ plan, dataRoot, log, run, capture, sleep, attempts, }: {
267
268
  plan: S3Plan;
package/types/e2e.d.mts CHANGED
@@ -236,6 +236,32 @@ export function waitForWorld({ url, container, stage, dataRoot, readLog, timeout
236
236
  * @returns {string[]} The executables to resolve, in the order to report them.
237
237
  */
238
238
  export function suiteExecutables(command: readonly string[]): string[];
239
+ /**
240
+ * The browser cache survives reseeding and container teardown.
241
+ * @param {NodeJS.ProcessEnv} [env] - Environment overrides.
242
+ * @returns {string} The browser cache directory.
243
+ */
244
+ export function browserCacheDir(env?: NodeJS.ProcessEnv): string;
245
+ /**
246
+ * Supply pinned Chrome only to a recognizable Cypress command without a browser.
247
+ * Explicit browser arguments remain verbatim, including Electron and paths.
248
+ * @param {object} opts
249
+ * @param {readonly string[]} opts.command - The configured suite command.
250
+ * @param {string[]} [opts.args] - Extra suite arguments.
251
+ * @param {NodeJS.ProcessEnv} [opts.env] - Cache overrides.
252
+ * @param {string} [opts.platform] - Host operating system.
253
+ * @param {string} [opts.arch] - Host architecture.
254
+ * @param {(message: string) => void} [opts.log] - Progress reporting.
255
+ * @returns {Promise<string[]>} Arguments to append to the suite command.
256
+ */
257
+ export function prepareSuiteBrowser({ command, args, env, platform, arch, log, }: {
258
+ command: readonly string[];
259
+ args?: string[] | undefined;
260
+ env?: NodeJS.ProcessEnv | undefined;
261
+ platform?: string | undefined;
262
+ arch?: string | undefined;
263
+ log?: ((message: string) => void) | undefined;
264
+ }): Promise<string[]>;
239
265
  /**
240
266
  * Find an executable the way the child process will: a path is a path, and a
241
267
  * bare name is looked for in the repository's `node_modules/.bin` first, then
@@ -467,6 +493,8 @@ export const E2E_GM_ID: "heroiclandsE2EGM";
467
493
  export const E2E_SCENE_ID: "heroiclandsScene";
468
494
  /** How the suite may be run. */
469
495
  export const E2E_MODES: readonly string[];
496
+ /** The Chrome for Testing build shared by local and CI Cypress runs. */
497
+ export const CHROME_BUILD_ID: "155.0.8059.84";
470
498
  /**
471
499
  * The disposable world's identity and credentials.
472
500
  */
@@ -631,8 +631,8 @@ export class BasePackCompiler {
631
631
  compile(): Promise<void>;
632
632
  currentNote: {
633
633
  absPath: string;
634
- bodyLine: number | undefined;
635
- bodyColumn: number | undefined;
634
+ bodyLine: any;
635
+ bodyColumn: any;
636
636
  } | undefined;
637
637
  #private;
638
638
  }
@@ -15,7 +15,7 @@
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[], linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[]}>} The corpus,
18
+ * @returns {Promise<{records: object[], parsed: Map<string, object>, linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[]}>} The corpus,
19
19
  * its indexes, and the notes it could not record.
20
20
  */
21
21
  export function buildCompileCorpus({ contentBase, assetsBase, skipDirectories, router, config, problems, }: {
@@ -27,6 +27,7 @@ export function buildCompileCorpus({ contentBase, assetsBase, skipDirectories, r
27
27
  problems?: object[] | undefined;
28
28
  }): Promise<{
29
29
  records: object[];
30
+ parsed: Map<string, object>;
30
31
  linkIndex: object;
31
32
  sqlTables: Map<string, object[]> | undefined;
32
33
  problems: object[];
@@ -94,6 +94,7 @@ export function asciiAliases(aliases: unknown): Array<string>;
94
94
  * @param {string} options.relPath - Its path below the content root, POSIX-separated.
95
95
  * @param {string} [options.absPath] - The file, read only on the failing path to
96
96
  * locate the offending key.
97
+ * @param {string} [options.raw] - Source text already read by the caller.
97
98
  * @param {string} options.contentPackage - The package the tree compiles as.
98
99
  * @param {string} [options.body] - The note's markdown body, for its anchors
99
100
  * and for the one question that decides whether the note publishes a page. A
@@ -118,10 +119,11 @@ export function asciiAliases(aliases: unknown): Array<string>;
118
119
  * otherwise be overwritten without a word. `file` and, where the file was
119
120
  * read, `position` ride on the error.
120
121
  */
121
- export function buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest, addressContext, dateContext, resolveRole, }: {
122
+ export function buildIndexRecord({ frontmatter, relPath, absPath, raw, contentPackage, body, bodyLine, manifest, addressContext, dateContext, resolveRole, }: {
122
123
  frontmatter: Record<string, any>;
123
124
  relPath: string;
124
125
  absPath?: string | undefined;
126
+ raw?: string | undefined;
125
127
  contentPackage: string;
126
128
  body?: string | undefined;
127
129
  bodyLine?: number | undefined;
@@ -131,12 +133,13 @@ export function buildIndexRecord({ frontmatter, relPath, absPath, contentPackage
131
133
  resolveRole?: ((address: string) => string | undefined) | undefined;
132
134
  }): Record<string, any>;
133
135
  /** Derive every index record belonging to one parsed note. */
134
- export function indexRecordsForNote({ frontmatter, body, bodyLine, relPath, absPath, contentPackage, manifest, addressContext, dateContext, resolveRole, }: {
136
+ export function indexRecordsForNote({ frontmatter, body, bodyLine, relPath, absPath, raw, contentPackage, manifest, addressContext, dateContext, resolveRole, }: {
135
137
  frontmatter: any;
136
138
  body: any;
137
139
  bodyLine: any;
138
140
  relPath: any;
139
141
  absPath: any;
142
+ raw: any;
140
143
  contentPackage: any;
141
144
  manifest: any;
142
145
  addressContext: any;
@@ -188,16 +191,20 @@ export function indexRecordsForNote({ frontmatter, body, bodyLine, relPath, absP
188
191
  * immediate parent whose package is not this one.
189
192
  * @param {object[]} [options.problems] - Receives each conflict as a finding.
190
193
  * Omitted, every conflict is collected and one Error lists them all.
194
+ * @param {Map<string, {raw: string}>} [options.parsed] - Notes already read, keyed by file.
191
195
  * @returns {Array<Record<string, any>>} The same records.
192
196
  * @throws {Error} When `problems` is omitted and any note's sources disagree.
193
197
  */
194
- export function resolveCultures(records: Array<Record<string, any>>, { contentPackage, contentBase, foreignRecord, problems }: {
198
+ export function resolveCultures(records: Array<Record<string, any>>, { contentPackage, contentBase, foreignRecord, problems, parsed }: {
195
199
  contentPackage: string;
196
200
  contentBase?: string | undefined;
197
201
  foreignRecord?: ((key: string) => ({
198
202
  culture?: unknown;
199
203
  } | undefined)) | undefined;
200
204
  problems?: object[] | undefined;
205
+ parsed?: Map<string, {
206
+ raw: string;
207
+ }> | undefined;
201
208
  }): Array<Record<string, any>>;
202
209
  /**
203
210
  * Read a content tree into index records, in the order they will be written.
@@ -233,9 +240,9 @@ export function resolveCultures(records: Array<Record<string, any>>, { contentPa
233
240
  * @param {(key: string) => ({culture?: unknown}|undefined)} [options.foreignRecord] -
234
241
  * Another package's entry for a canonical address, read for a parent that
235
242
  * package publishes; see {@link resolveCultures}.
236
- * @param {Map<string, {body: string, frontmatter: object|null}>} [options.parsed] -
237
- * Filled with each note's body and its frontmatter as authored, keyed by the
238
- * note's file, so a later pass reads the notes this walk already parsed.
243
+ * @param {Map<string, {body: string, frontmatter: object|null, raw: string, bodyLine?: number, bodyColumn?: number}>} [options.parsed] -
244
+ * Filled with each note's source text, body coordinates and frontmatter as
245
+ * authored, keyed by file, for reuse throughout the command.
239
246
  * @returns {Array<Record<string, any>>} The records, in a total order that does
240
247
  * not depend on directory-read order.
241
248
  */
@@ -252,6 +259,9 @@ export function collectContentIndex(contentBase: string, { contentPackage, skipD
252
259
  parsed?: Map<string, {
253
260
  body: string;
254
261
  frontmatter: object | null;
262
+ raw: string;
263
+ bodyLine?: number;
264
+ bodyColumn?: number;
255
265
  }> | undefined;
256
266
  }): Array<Record<string, any>>;
257
267
  /**
@@ -289,7 +299,7 @@ export function serializeContentIndex(records: Array<Record<string, any>>): stri
289
299
  * aborting the derivation. Omitted, the note throws — which is the contract
290
300
  * the emitter needs, since an index missing a note asserts that it does not
291
301
  * exist.
292
- * @param {Map<string, {body: string, frontmatter: object|null}>} [opts.parsed] -
302
+ * @param {Map<string, {body: string, frontmatter: object|null, raw: string, bodyLine?: number, bodyColumn?: number}>} [opts.parsed] -
293
303
  * Filled with each note's body and authored frontmatter, keyed by the note's
294
304
  * file. Handed to {@link module:engine/sql-tables.prepareTreeSqlTables}, it
295
305
  * spares that pass reading and parsing every note again.
@@ -304,8 +314,24 @@ export function indexRecordsFor({ contentBase, assetsBase, config, skipDirectori
304
314
  parsed?: Map<string, {
305
315
  body: string;
306
316
  frontmatter: object | null;
317
+ raw: string;
318
+ bodyLine?: number;
319
+ bodyColumn?: number;
307
320
  }> | undefined;
308
321
  }): object[];
322
+ /**
323
+ * Prepare the complete bodies a content-index publication describes.
324
+ * Prose declarations use each source's audience. Native SQL uses the complete
325
+ * corpus, with target maps limited to that source's declared prose slugs.
326
+ * No output files are written, and the returned inputs are read-only to the emitter.
327
+ *
328
+ * @param {{contentBase?: string, config?: object}} [options]
329
+ * @returns {Promise<PreparedContentIndex>}
330
+ */
331
+ export function prepareContentIndex({ contentBase, config }?: {
332
+ contentBase?: string;
333
+ config?: object;
334
+ }): Promise<PreparedContentIndex>;
309
335
  /**
310
336
  * Emit this package's content index.
311
337
  *
@@ -315,6 +341,7 @@ export function indexRecordsFor({ contentBase, assetsBase, config, skipDirectori
315
341
  * @param {string} [options.outDir] - Where to write; defaults to the configured
316
342
  * `paths.contentIndex`.
317
343
  * @param {object} [options.config] - A resolved configuration; loaded when omitted.
344
+ * @param {PreparedContentIndex} [options.publication] - Complete preparation for this tree and configuration.
318
345
  * @returns {{file: string, notes: number, assets: number, records: number,
319
346
  * bytes: number, full: number, draft: number, stub: number,
320
347
  * dictionary: {file: string, rows: number}, terms: {file: string, rows: number}}}
@@ -328,10 +355,11 @@ export function indexRecordsFor({ contentBase, assetsBase, config, skipDirectori
328
355
  * reader would take it as the authoritative statement that this package has
329
356
  * no content.
330
357
  */
331
- export function emitContentIndex({ contentBase, outDir, config }?: {
358
+ export function emitContentIndex({ contentBase, outDir, config, publication }?: {
332
359
  contentBase?: string | undefined;
333
360
  outDir?: string | undefined;
334
361
  config?: object | undefined;
362
+ publication?: PreparedContentIndex | undefined;
335
363
  }): {
336
364
  file: string;
337
365
  notes: number;
@@ -350,6 +378,55 @@ export function emitContentIndex({ contentBase, outDir, config }?: {
350
378
  rows: number;
351
379
  };
352
380
  };
381
+ /**
382
+ * A captured source context, consumed without mutation by the synchronous emitter.
383
+ */
384
+ export type PreparedContentIndex = {
385
+ /**
386
+ * - The exact resolved configuration for preparation.
387
+ */
388
+ config: object;
389
+ /**
390
+ * - The absolute captured source root.
391
+ */
392
+ contentBase: string;
393
+ /**
394
+ * - Complete index records, including hidden providers.
395
+ */
396
+ records: object[];
397
+ /**
398
+ * - Authored sources by absolute file.
399
+ */
400
+ parsed: Map<string, {
401
+ frontmatter: object | null;
402
+ body: string;
403
+ raw: string;
404
+ bodyLine?: number;
405
+ bodyColumn?: number;
406
+ }>;
407
+ /**
408
+ * - Complete rendered bodies in each note's audience.
409
+ */
410
+ expandedBodies: Map<string, {
411
+ markdown: string;
412
+ lineMap: Array<{
413
+ line: number;
414
+ generated: boolean;
415
+ }>;
416
+ generatedFrom?: number;
417
+ }>;
418
+ /**
419
+ * - Complete native bodies with explicit SQL using the ALL corpus.
420
+ */
421
+ foundryBodies: Map<string, {
422
+ markdown: string;
423
+ lineMap: Array<{
424
+ line: number;
425
+ generated: boolean;
426
+ }>;
427
+ generatedFrom?: number;
428
+ }>;
429
+ };
353
430
  import { collectAnchors } from "./anchors.mjs";
354
431
  import { authoredFrontmatter } from "./index-records.mjs";
355
432
  import { DERIVED_KEYS } from "./index-records.mjs";
@@ -12,7 +12,8 @@
12
12
  * Prepared results.
13
13
  * A note's generated sections — see {@link module:engine/generated-sections} —
14
14
  * are appended to the body first, from the `generated` text the preparation
15
- * set on its results, and expanded with it.
15
+ * set on its results, and expanded with it. Automatic lexical lists, carried
16
+ * on `lexical`, then fill their authored sections or append a new section.
16
17
  *
17
18
  * @returns {{markdown: string, errors: object[], warnings: object[], lineMap: object[],
18
19
  * generatedFrom?: number}} `generatedFrom` is the 0-based line of the
@@ -32,12 +32,15 @@ export function glossaryCorpus(index: object): GlossaryCorpus;
32
32
  *
33
33
  * @param {object} index - The link index.
34
34
  * @param {object} note - The culture or affiliation note.
35
+ * @param {object} [entry] - A glossary entry, whose resolved language selects
36
+ * its dictionary words. Absent, the note's own language is used.
35
37
  * @returns {Map<string, string[]>} The terms; empty for a note no source reaches.
36
38
  */
37
- export function noteTerms(index: object, note: object): Map<string, string[]>;
39
+ export function noteTerms(index: object, note: object, entry?: object): Map<string, string[]>;
38
40
  /**
39
41
  * Warn of an affiliation that reaches no culture, its own or one it inherits,
40
- * in a tree that describes peoples.
42
+ * in a tree that describes peoples. Its ranks and offices then have no
43
+ * culture associations for term queries.
41
44
  *
42
45
  * @param {object} note - An `affiliation` note.
43
46
  * @param {{index?: object}} [context]
@@ -63,4 +66,21 @@ export type GlossaryCorpus = {
63
66
  * give it.
64
67
  */
65
68
  terms: Map<string, Map<string, string[]>>;
69
+ /**
70
+ * - The ranks and
71
+ * offices each owner can name.
72
+ */
73
+ governance: Map<string, Map<string, string[]>>;
74
+ /**
75
+ * - Each
76
+ * language's dictionary words.
77
+ */
78
+ words: Map<string, Array<{
79
+ form: string;
80
+ gloss: unknown;
81
+ }>>;
82
+ /**
83
+ * - Each language's parent.
84
+ */
85
+ families: Map<string, string | undefined>;
66
86
  };
@@ -13,6 +13,8 @@
13
13
  * every surface shows it exactly as written, never recased.
14
14
  * @property {string} shape - The shape in words, for a finding and the reference.
15
15
  * @property {boolean} [required] - The key must be stated, with a value.
16
+ * @property {boolean} [allowEmpty] - A required string may be empty where
17
+ * empty text is a stated value.
16
18
  * @property {boolean} [nullable] - An explicit `null` states the key, for a
17
19
  * required key whose null has a meaning of its own.
18
20
  * @property {string} [label] - The infobox row label, where humanising the key
@@ -117,6 +119,11 @@ export type InnerKeySpec = {
117
119
  * - The key must be stated, with a value.
118
120
  */
119
121
  required?: boolean | undefined;
122
+ /**
123
+ * - A required string may be empty where
124
+ * empty text is a stated value.
125
+ */
126
+ allowEmpty?: boolean | undefined;
120
127
  /**
121
128
  * - An explicit `null` states the key, for a
122
129
  * required key whose null has a meaning of its own.
@@ -150,12 +150,14 @@ export function positionInFrontmatter(raw: string, key: string, value?: string,
150
150
  * {@link positionInFrontmatter}.
151
151
  * @param {boolean} [options.topLevel=false] - Require the key at column 1, so
152
152
  * an identically named nested key cannot answer for it.
153
+ * @param {string} [options.raw] - Source text already read by the caller.
153
154
  * @returns {{line?: number, column?: number}|undefined} Spreadable position
154
155
  * fields, dropped rather than guessed when the file cannot be read or the key
155
156
  * cannot be found — as `formatDiagnostic` requires.
156
157
  */
157
- export function locateFrontmatterKey(absPath: string | undefined, key: string, value?: string, { topLevel }?: {
158
+ export function locateFrontmatterKey(absPath: string | undefined, key: string, value?: string, { topLevel, raw }?: {
158
159
  topLevel?: boolean | undefined;
160
+ raw?: string | undefined;
159
161
  }): {
160
162
  line?: number;
161
163
  column?: number;
@@ -195,11 +197,10 @@ export function positionOfLiteral(text: string, needle: string, occurrence?: num
195
197
  * first occurrence is routinely not the one the finding is about — which is the
196
198
  * one thing the located form exists to prevent.
197
199
  *
198
- * A path resolves the exact node instead. The document is re-parsed here rather
199
- * than threaded from the loader because the loader returns plain data: `yaml`
200
- * discards ranges once a document is materialised, and carrying a parallel
201
- * position tree through configuration resolution would be a second
202
- * representation of the same file to keep in step.
200
+ * A path resolves the exact node. The most recently located text retains its
201
+ * document and line counter, so several findings in one note share a parse.
202
+ * A different text replaces that entry, including an edited copy of the same
203
+ * file. The loader returns plain data without source ranges.
203
204
  *
204
205
  * Every failure — unparseable text, an `.mjs` configuration, a path that
205
206
  * resolves to nothing — yields `{}`, so a caller spreads the result and the