@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.
- package/CHANGELOG.md +41 -0
- package/README.md +32 -0
- package/bin/content-commands.mjs +32 -10
- package/ci/check-banners.mjs +37 -19
- package/container.mjs +48 -10
- package/docs/api.md +402 -58
- package/docs/authoring/first-note.md +1 -1
- package/docs/authoring/frontmatter.md +30 -1
- package/docs/authoring/links-and-markup.md +154 -56
- package/docs/commands.md +31 -12
- package/docs/content-format.md +25 -2
- package/docs/design.md +31 -0
- package/docs/diagnostics.md +1 -1
- package/docs/guides/e2e.md +37 -1
- package/docs/guides/packs.md +2 -1
- package/docs/guides/reference-pages.md +86 -63
- package/docs/project-setup.md +30 -15
- package/docs/reference/format-details.md +313 -132
- package/docs/reference/note-types.md +191 -174
- package/e2e.mjs +89 -2
- package/engine/anchors.mjs +7 -20
- package/engine/base-compiler.mjs +11 -18
- package/engine/compile-corpus.mjs +5 -3
- package/engine/content-alerts.mjs +4 -2
- package/engine/content-blocks.mjs +18 -36
- package/engine/content-format.yaml +1 -0
- package/engine/content-index.mjs +447 -33
- package/engine/content-links.mjs +43 -15
- package/engine/content-poetry.mjs +9 -0
- package/engine/content-tables.mjs +8 -4
- package/engine/culture-reach.mjs +61 -34
- package/engine/data-keys.mjs +3 -1
- package/engine/diagnostics.mjs +24 -11
- package/engine/dictionary-layout.mjs +151 -19
- package/engine/dictionary-rows.mjs +129 -79
- package/engine/foundry-entries.mjs +21 -3
- package/engine/frontmatter-lint.mjs +1 -1
- package/engine/generate.mjs +12 -22
- package/engine/glossary-entries.mjs +11 -11
- package/engine/heading-attributes.mjs +21 -28
- package/engine/helpers.mjs +20 -12
- package/engine/infobox.mjs +3 -0
- package/engine/journals.mjs +15 -2
- package/engine/language-data.mjs +53 -0
- package/engine/language-terms.mjs +86 -0
- package/engine/language-words.mjs +2 -2
- package/engine/lexical-sections.mjs +247 -0
- package/engine/markdownlint-run.mjs +12 -2
- package/engine/markup-validation.mjs +34 -0
- package/engine/note-addresses.mjs +1 -0
- package/engine/note-claims.mjs +21 -10
- package/engine/note-package.mjs +3 -5
- package/engine/note-state.mjs +9 -3
- package/engine/note-vocabulary.mjs +36 -7
- package/engine/pdf-build.mjs +17 -10
- package/engine/prose-lint.mjs +12 -6
- package/engine/related-pages.mjs +22 -3
- package/engine/see-also.mjs +150 -0
- package/engine/site-build.mjs +21 -3
- package/engine/site-preview.mjs +4 -2
- package/engine/sql-tables.mjs +287 -53
- package/engine/standings.mjs +14 -12
- package/engine/stub-lint.mjs +6 -2
- package/engine/web-wikilinks.mjs +8 -6
- package/engine/word-rows.mjs +20 -12
- package/engine/word-terms.mjs +15 -6
- package/hugo-theme/layouts/partials/related.html +25 -8
- package/package.json +2 -1
- package/types/container.d.mts +4 -3
- package/types/e2e.d.mts +28 -0
- package/types/engine/base-compiler.d.mts +2 -2
- package/types/engine/compile-corpus.d.mts +2 -1
- package/types/engine/content-index.d.mts +85 -8
- package/types/engine/content-tables.d.mts +2 -1
- package/types/engine/culture-reach.d.mts +22 -2
- package/types/engine/data-keys.d.mts +7 -0
- package/types/engine/diagnostics.d.mts +7 -6
- package/types/engine/dictionary-layout.d.mts +35 -4
- package/types/engine/dictionary-rows.d.mts +9 -0
- package/types/engine/foundry-entries.d.mts +3 -1
- package/types/engine/heading-attributes.d.mts +2 -2
- package/types/engine/helpers.d.mts +11 -5
- package/types/engine/language-terms.d.mts +7 -0
- package/types/engine/lexical-sections.d.mts +95 -0
- package/types/engine/markdownlint-run.d.mts +3 -1
- package/types/engine/markup-validation.d.mts +14 -0
- package/types/engine/note-addresses.d.mts +10 -0
- package/types/engine/note-claims.d.mts +7 -1
- package/types/engine/note-package.d.mts +3 -1
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/note-vocabulary.d.mts +1 -0
- package/types/engine/related-pages.d.mts +10 -1
- package/types/engine/see-also.d.mts +55 -0
- package/types/engine/site-build.d.mts +4 -1
- package/types/engine/sql-tables.d.mts +40 -6
- package/types/engine/word-rows.d.mts +9 -7
package/engine/content-index.mjs
CHANGED
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
noteAddressContext,
|
|
100
100
|
encodeAddresses,
|
|
101
101
|
publishAddresses,
|
|
102
|
+
cloneAddressState,
|
|
102
103
|
} from "./note-addresses.mjs";
|
|
103
104
|
import { AddressLink } from "./address-values.mjs";
|
|
104
105
|
import fs from "node:fs";
|
|
@@ -113,9 +114,24 @@ import { addressSlug, canonicalKey } from "./content-address.mjs";
|
|
|
113
114
|
import { isAddressTuple, ownDocumentSystem, readCanonicalKey, renderAddress } from "./address.mjs";
|
|
114
115
|
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
115
116
|
import { dictionaryAndTermRows, serializeRows } from "./dictionary-rows.mjs";
|
|
117
|
+
import { insertLexicalSections, lexicalSectionsFor } from "./lexical-sections.mjs";
|
|
118
|
+
import { isGmNote } from "./note-vocabulary.mjs";
|
|
116
119
|
import { attachCitations } from "./word-citations.mjs";
|
|
117
120
|
import { assetAddressIndex } from "./art-fields.mjs";
|
|
118
121
|
import { embedRole } from "./content-embeds.mjs";
|
|
122
|
+
import { expandContentTables } from "./content-tables.mjs";
|
|
123
|
+
import {
|
|
124
|
+
codeRegions,
|
|
125
|
+
FENCE_LINE,
|
|
126
|
+
matchAllOutsideCode,
|
|
127
|
+
parseHeaderArgs,
|
|
128
|
+
replaceOutsideCode,
|
|
129
|
+
} from "./code-fences.mjs";
|
|
130
|
+
import { PAGE_LIST_LANGUAGE } from "./page-lists.mjs";
|
|
131
|
+
import { EXPRESSION, renderMarkdownExpressions } from "./markdown-expressions.mjs";
|
|
132
|
+
import { scanFigures } from "./content-figures.mjs";
|
|
133
|
+
import { eventNoteIndex } from "./event-fields.mjs";
|
|
134
|
+
import { derivedSections } from "./derived-sections.mjs";
|
|
119
135
|
// One reader for a note's anchors, shared with the link checker and with the
|
|
120
136
|
// builds that emit a link. Re-exported because this is where callers
|
|
121
137
|
// have always addressed it.
|
|
@@ -133,7 +149,7 @@ import { NO_SYSTEM } from "./document-subtypes.mjs";
|
|
|
133
149
|
*/
|
|
134
150
|
|
|
135
151
|
export { collectAnchors };
|
|
136
|
-
import { entriesForNote, foundryIdentities } from "./foundry-entries.mjs";
|
|
152
|
+
import { entriesForNote, foundryIdentities, LEAD_ANCHOR } from "./foundry-entries.mjs";
|
|
137
153
|
import { walkMarkdownTree } from "./helpers.mjs";
|
|
138
154
|
// The one statement of what an empty body means, shared with the lint and the
|
|
139
155
|
// SQL view that derives the ladder from the absence this writes.
|
|
@@ -410,23 +426,25 @@ function foundryBlock(entry, system) {
|
|
|
410
426
|
* @param {string} [absPath] - The file, read only on the failing path to locate
|
|
411
427
|
* the offending key.
|
|
412
428
|
* @param {string} [contentPackage] - The package this tree compiles as.
|
|
429
|
+
* @param {string} [raw] - Source text already read by the caller.
|
|
413
430
|
* @returns {void}
|
|
414
431
|
* @throws {Error} When the note authors a derived key. `file` and `position`
|
|
415
432
|
* ride on the error.
|
|
416
433
|
*/
|
|
417
|
-
function assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage) {
|
|
434
|
+
function assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage, raw) {
|
|
418
435
|
for (const key of DERIVED_KEYS) {
|
|
419
436
|
if (!Object.hasOwn(frontmatter ?? {}, key)) continue;
|
|
420
437
|
if (key === "package") {
|
|
421
438
|
// Throws with its own wording, and its own position.
|
|
422
|
-
assertNoDeclaredPackage(frontmatter, { absPath, configured: contentPackage });
|
|
439
|
+
assertNoDeclaredPackage(frontmatter, { absPath, configured: contentPackage, raw });
|
|
423
440
|
}
|
|
424
441
|
const err = new Error(
|
|
425
442
|
`\`${key}:\` is derived by the content index and cannot be ` +
|
|
426
443
|
`authored — rename the frontmatter field`,
|
|
427
444
|
);
|
|
428
445
|
err.file = relPath;
|
|
429
|
-
const position =
|
|
446
|
+
const position =
|
|
447
|
+
absPath ? locateFrontmatterKey(absPath, key, undefined, { raw }) : undefined;
|
|
430
448
|
if (position) err.position = position;
|
|
431
449
|
throw err;
|
|
432
450
|
}
|
|
@@ -445,6 +463,7 @@ function assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage) {
|
|
|
445
463
|
* @param {string} options.relPath - Its path below the content root, POSIX-separated.
|
|
446
464
|
* @param {string} [options.absPath] - The file, read only on the failing path to
|
|
447
465
|
* locate the offending key.
|
|
466
|
+
* @param {string} [options.raw] - Source text already read by the caller.
|
|
448
467
|
* @param {string} options.contentPackage - The package the tree compiles as.
|
|
449
468
|
* @param {string} [options.body] - The note's markdown body, for its anchors
|
|
450
469
|
* and for the one question that decides whether the note publishes a page. A
|
|
@@ -473,6 +492,7 @@ export function buildIndexRecord({
|
|
|
473
492
|
frontmatter,
|
|
474
493
|
relPath,
|
|
475
494
|
absPath,
|
|
495
|
+
raw,
|
|
476
496
|
contentPackage,
|
|
477
497
|
body,
|
|
478
498
|
bodyLine,
|
|
@@ -481,7 +501,7 @@ export function buildIndexRecord({
|
|
|
481
501
|
dateContext,
|
|
482
502
|
resolveRole,
|
|
483
503
|
}) {
|
|
484
|
-
assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage);
|
|
504
|
+
assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage, raw);
|
|
485
505
|
|
|
486
506
|
const posix = relPath.split(path.sep).join("/");
|
|
487
507
|
const folder = posix.includes("/") ? posix.slice(0, posix.lastIndexOf("/")) : "";
|
|
@@ -490,9 +510,7 @@ export function buildIndexRecord({
|
|
|
490
510
|
} catch (error) {
|
|
491
511
|
error.file = absPath ?? relPath;
|
|
492
512
|
if (absPath && error.keyPath) {
|
|
493
|
-
const yamlText =
|
|
494
|
-
.readFileSync(absPath, "utf8")
|
|
495
|
-
.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
|
|
513
|
+
const yamlText = (raw ?? rawNote(absPath)).match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
|
|
496
514
|
const locate = (/** @type {any} */ found) => {
|
|
497
515
|
const position = positionOfYamlPath(yamlText, found.keyPath, {
|
|
498
516
|
key: found.addressKey,
|
|
@@ -540,7 +558,7 @@ export function buildIndexRecord({
|
|
|
540
558
|
stub ? null : (
|
|
541
559
|
[
|
|
542
560
|
...collectAnchors(body, bodyLine, resolveRole),
|
|
543
|
-
...eventAnchors(frontmatter, rawNote(absPath)),
|
|
561
|
+
...eventAnchors(frontmatter, raw ?? rawNote(absPath)),
|
|
544
562
|
]
|
|
545
563
|
.sort((a, b) => (a.line ?? 0) - (b.line ?? 0))
|
|
546
564
|
.map((a) => ({
|
|
@@ -641,11 +659,21 @@ function buildDocRecord({ frontmatter, address, entry, file, contentPackage, anc
|
|
|
641
659
|
// entry's derivation.
|
|
642
660
|
id: made ? entry.id : null,
|
|
643
661
|
nameAscii: asciiName(frontmatter?.name?.full),
|
|
644
|
-
address: made ? { slug: address.slug, canonical: entry.key } : null,
|
|
662
|
+
address: made ? { slug: address.slug, canonical: readCanonicalKey(entry.key) } : null,
|
|
645
663
|
// The record this is the documentation *for*. `documentation` is
|
|
646
664
|
// the forward link on that record, so either end reaches the other.
|
|
647
|
-
documents: address.canonical,
|
|
648
|
-
anchors:
|
|
665
|
+
documents: readCanonicalKey(address.canonical),
|
|
666
|
+
anchors:
|
|
667
|
+
made ?
|
|
668
|
+
anchors.map((anchor) => ({
|
|
669
|
+
...anchor,
|
|
670
|
+
link: new AddressLink(
|
|
671
|
+
readCanonicalKey(entry.key),
|
|
672
|
+
anchor.slug,
|
|
673
|
+
anchor.kind,
|
|
674
|
+
),
|
|
675
|
+
}))
|
|
676
|
+
: null,
|
|
649
677
|
foundry: foundryBlock(entry, NOTE_SYSTEM),
|
|
650
678
|
file,
|
|
651
679
|
})
|
|
@@ -659,6 +687,7 @@ export function indexRecordsForNote({
|
|
|
659
687
|
bodyLine,
|
|
660
688
|
relPath,
|
|
661
689
|
absPath,
|
|
690
|
+
raw,
|
|
662
691
|
contentPackage,
|
|
663
692
|
manifest,
|
|
664
693
|
addressContext,
|
|
@@ -672,6 +701,7 @@ export function indexRecordsForNote({
|
|
|
672
701
|
bodyLine,
|
|
673
702
|
relPath,
|
|
674
703
|
absPath,
|
|
704
|
+
raw,
|
|
675
705
|
contentPackage,
|
|
676
706
|
manifest,
|
|
677
707
|
addressContext,
|
|
@@ -783,10 +813,14 @@ function cultureTuple(value) {
|
|
|
783
813
|
* immediate parent whose package is not this one.
|
|
784
814
|
* @param {object[]} [options.problems] - Receives each conflict as a finding.
|
|
785
815
|
* Omitted, every conflict is collected and one Error lists them all.
|
|
816
|
+
* @param {Map<string, {raw: string}>} [options.parsed] - Notes already read, keyed by file.
|
|
786
817
|
* @returns {Array<Record<string, any>>} The same records.
|
|
787
818
|
* @throws {Error} When `problems` is omitted and any note's sources disagree.
|
|
788
819
|
*/
|
|
789
|
-
export function resolveCultures(
|
|
820
|
+
export function resolveCultures(
|
|
821
|
+
records,
|
|
822
|
+
{ contentPackage, contentBase, foreignRecord, problems, parsed },
|
|
823
|
+
) {
|
|
790
824
|
// Each note under every key a link may name it by: its record's canonical
|
|
791
825
|
// address, and its page address in the `note` system, which is what a
|
|
792
826
|
// `data.parents`, `data.seat` or `data.government` link names.
|
|
@@ -879,7 +913,7 @@ export function resolveCultures(records, { contentPackage, contentBase, foreignR
|
|
|
879
913
|
*/
|
|
880
914
|
const conflict = (record, keyPath, subject, distinct) => {
|
|
881
915
|
const file = contentBase ? noteFile(contentBase, record) : record.file?.path;
|
|
882
|
-
const raw =
|
|
916
|
+
const raw = parsed?.get(file)?.raw ?? (contentBase ? rawNote(file) : undefined);
|
|
883
917
|
let position = positionOfFrontmatterPath(raw, keyPath, { key: true });
|
|
884
918
|
if (position.line === undefined && keyPath.length > 1)
|
|
885
919
|
position = positionOfFrontmatterPath(raw, ["data"], { key: true });
|
|
@@ -1026,9 +1060,9 @@ export function resolveCultures(records, { contentPackage, contentBase, foreignR
|
|
|
1026
1060
|
* @param {(key: string) => ({culture?: unknown}|undefined)} [options.foreignRecord] -
|
|
1027
1061
|
* Another package's entry for a canonical address, read for a parent that
|
|
1028
1062
|
* package publishes; see {@link resolveCultures}.
|
|
1029
|
-
* @param {Map<string, {body: string, frontmatter: object|null}>} [options.parsed] -
|
|
1030
|
-
* Filled with each note's body and
|
|
1031
|
-
*
|
|
1063
|
+
* @param {Map<string, {body: string, frontmatter: object|null, raw: string, bodyLine?: number, bodyColumn?: number}>} [options.parsed] -
|
|
1064
|
+
* Filled with each note's source text, body coordinates and frontmatter as
|
|
1065
|
+
* authored, keyed by file, for reuse throughout the command.
|
|
1032
1066
|
* @returns {Array<Record<string, any>>} The records, in a total order that does
|
|
1033
1067
|
* not depend on directory-read order.
|
|
1034
1068
|
*/
|
|
@@ -1042,7 +1076,7 @@ export function collectContentIndex(
|
|
|
1042
1076
|
problems,
|
|
1043
1077
|
addressContext,
|
|
1044
1078
|
foreignRecord,
|
|
1045
|
-
parsed,
|
|
1079
|
+
parsed = new Map(),
|
|
1046
1080
|
},
|
|
1047
1081
|
) {
|
|
1048
1082
|
const records = [];
|
|
@@ -1115,10 +1149,16 @@ export function collectContentIndex(
|
|
|
1115
1149
|
|
|
1116
1150
|
// Each recorded note with its body, for the words its prose cites.
|
|
1117
1151
|
const bodies = [];
|
|
1118
|
-
for (const { frontmatter, body, bodyLine, absPath } of parsedNotes) {
|
|
1152
|
+
for (const { frontmatter, body, bodyLine, bodyColumn, raw, absPath } of parsedNotes) {
|
|
1119
1153
|
// Before the index derives anything onto the frontmatter, so a later
|
|
1120
1154
|
// pass reads the note as authored.
|
|
1121
|
-
parsed
|
|
1155
|
+
parsed.set(absPath, {
|
|
1156
|
+
body,
|
|
1157
|
+
bodyLine,
|
|
1158
|
+
bodyColumn,
|
|
1159
|
+
raw,
|
|
1160
|
+
frontmatter: structuredClone(frontmatter),
|
|
1161
|
+
});
|
|
1122
1162
|
const fm = frontmatter ?? {};
|
|
1123
1163
|
applyComputedBeingAge(fm, present, dates);
|
|
1124
1164
|
const relPath = path.relative(contentBase, absPath);
|
|
@@ -1127,6 +1167,7 @@ export function collectContentIndex(
|
|
|
1127
1167
|
frontmatter: fm,
|
|
1128
1168
|
relPath,
|
|
1129
1169
|
absPath,
|
|
1170
|
+
raw,
|
|
1130
1171
|
contentPackage,
|
|
1131
1172
|
body,
|
|
1132
1173
|
bodyLine,
|
|
@@ -1142,9 +1183,7 @@ export function collectContentIndex(
|
|
|
1142
1183
|
err.identity = { type: fm.type, shortcode: fm.shortcode };
|
|
1143
1184
|
err.keyPath = ["shortcode"];
|
|
1144
1185
|
err.message = `shortcode "${fm.shortcode}" is not strictly alphanumeric — lowercase letters and digits only`;
|
|
1145
|
-
const yamlText =
|
|
1146
|
-
.readFileSync(absPath, "utf8")
|
|
1147
|
-
.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
|
|
1186
|
+
const yamlText = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
|
|
1148
1187
|
err.position = positionOfYamlPath(yamlText, err.keyPath);
|
|
1149
1188
|
if (err.position.line) err.position.line++;
|
|
1150
1189
|
}
|
|
@@ -1186,13 +1225,13 @@ export function collectContentIndex(
|
|
|
1186
1225
|
// Every note record exists and every Address in it is a tuple, so a parent
|
|
1187
1226
|
// is found by its canonical address. Before the sort, which reads nothing
|
|
1188
1227
|
// the pass writes.
|
|
1189
|
-
resolveCultures(records, { contentPackage, contentBase, foreignRecord, problems });
|
|
1228
|
+
resolveCultures(records, { contentPackage, contentBase, foreignRecord, problems, parsed });
|
|
1190
1229
|
const cultureNotes = records
|
|
1191
1230
|
.filter((record) => isNoteRecord(record) && record.type === "lore")
|
|
1192
1231
|
.map((record) => ({
|
|
1193
1232
|
fm: record,
|
|
1194
1233
|
file: noteFile(contentBase, record),
|
|
1195
|
-
raw:
|
|
1234
|
+
raw: parsed.get(noteFile(contentBase, record)).raw,
|
|
1196
1235
|
}));
|
|
1197
1236
|
const cultureByKey = new Map(
|
|
1198
1237
|
cultureNotes.map((note) => [
|
|
@@ -1209,6 +1248,26 @@ export function collectContentIndex(
|
|
|
1209
1248
|
for (const [note, inherited] of inheritance.values)
|
|
1210
1249
|
if (Object.keys(inherited).length) note.fm.data = { ...note.fm.data, ...inherited };
|
|
1211
1250
|
|
|
1251
|
+
// Automatic lexical sections require only the recorded frontmatter. Their
|
|
1252
|
+
// published anchors and journal targets therefore use the same pure body
|
|
1253
|
+
// projection as the surfaces, without making the index emitter async.
|
|
1254
|
+
// Public notes resolve providers in their audience, including lineage
|
|
1255
|
+
// through hidden languages, just as their build does.
|
|
1256
|
+
const visible = records.filter((record) => !isGmNote(record));
|
|
1257
|
+
const publicSections = lexicalSectionsFor(visible, records);
|
|
1258
|
+
const allSections = lexicalSectionsFor(records);
|
|
1259
|
+
for (const { record, body } of bodies) {
|
|
1260
|
+
const key = canonicalKey(contentPackage, "note", record.type, record.shortcode);
|
|
1261
|
+
const sections = (isGmNote(record) ? allSections : publicSections).get(key);
|
|
1262
|
+
if (!sections) continue;
|
|
1263
|
+
const expanded = insertLexicalSections(body, sections).markdown;
|
|
1264
|
+
projectPublicationBody(records, record, expanded, {
|
|
1265
|
+
contentPackage,
|
|
1266
|
+
manifest,
|
|
1267
|
+
resolveRole,
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1212
1271
|
// Sorted at every depth like a note's record, and for the same reason: the
|
|
1213
1272
|
// declaration order of the `asset` fields is a fact about the emitter, not
|
|
1214
1273
|
// about the content, and the artifact is meant to be byte-identical across
|
|
@@ -1352,7 +1411,7 @@ function dependencyAddresses(config) {
|
|
|
1352
1411
|
* aborting the derivation. Omitted, the note throws — which is the contract
|
|
1353
1412
|
* the emitter needs, since an index missing a note asserts that it does not
|
|
1354
1413
|
* exist.
|
|
1355
|
-
* @param {Map<string, {body: string, frontmatter: object|null}>} [opts.parsed] -
|
|
1414
|
+
* @param {Map<string, {body: string, frontmatter: object|null, raw: string, bodyLine?: number, bodyColumn?: number}>} [opts.parsed] -
|
|
1356
1415
|
* Filled with each note's body and authored frontmatter, keyed by the note's
|
|
1357
1416
|
* file. Handed to {@link module:engine/sql-tables.prepareTreeSqlTables}, it
|
|
1358
1417
|
* spares that pass reading and parsing every note again.
|
|
@@ -1394,6 +1453,283 @@ export function indexRecordsFor({
|
|
|
1394
1453
|
});
|
|
1395
1454
|
}
|
|
1396
1455
|
|
|
1456
|
+
/** Active body directives whose output cannot be inferred synchronously. */
|
|
1457
|
+
function dynamicPublicationBody(body) {
|
|
1458
|
+
const source = String(body ?? "");
|
|
1459
|
+
return (
|
|
1460
|
+
[...matchAllOutsideCode(source, EXPRESSION)].length > 0 ||
|
|
1461
|
+
codeRegions(source, { spans: false, poetryAsProse: false }).some(({ start, end }) => {
|
|
1462
|
+
const opener = FENCE_LINE.exec(source.slice(start, end).split("\n", 1)[0]);
|
|
1463
|
+
const language = opener && parseHeaderArgs(opener[3]).language;
|
|
1464
|
+
return language === "sql" || language === PAGE_LIST_LANGUAGE;
|
|
1465
|
+
})
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/** Publication errors are located and always precede output creation. */
|
|
1470
|
+
function publicationProblems(problems) {
|
|
1471
|
+
if (!problems.length) return;
|
|
1472
|
+
throw Object.assign(new Error(problems.map(formatDiagnostic).join("\n")), { located: true });
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/** Render the shared expression language and carry inserted scalar lines back to their source. */
|
|
1476
|
+
function publicationExpressions(expanded, context) {
|
|
1477
|
+
const source = expanded.markdown;
|
|
1478
|
+
const additions = [];
|
|
1479
|
+
const findings = [];
|
|
1480
|
+
const markdown = replaceOutsideCode(source, EXPRESSION, (expression, _inside, offset) => {
|
|
1481
|
+
const line = source.slice(0, offset).split("\n").length - 1;
|
|
1482
|
+
const location = expanded.lineMap[line] ?? { line, generated: false };
|
|
1483
|
+
const result = renderMarkdownExpressions(expression, {
|
|
1484
|
+
...context,
|
|
1485
|
+
bodyLine: (context.bodyLine ?? 1) + location.line,
|
|
1486
|
+
});
|
|
1487
|
+
const column = offset - source.lastIndexOf("\n", offset - 1) - 1;
|
|
1488
|
+
findings.push(
|
|
1489
|
+
...result.findings.map((finding) => ({ ...finding, column: finding.column + column })),
|
|
1490
|
+
);
|
|
1491
|
+
const count = result.markdown.split("\n").length - 1;
|
|
1492
|
+
if (count) additions.push({ line, count, location });
|
|
1493
|
+
return result.markdown;
|
|
1494
|
+
});
|
|
1495
|
+
publicationProblems(findings);
|
|
1496
|
+
const lineMap = expanded.lineMap.map((location) => ({ ...location }));
|
|
1497
|
+
let generatedFrom = expanded.generatedFrom;
|
|
1498
|
+
for (const addition of additions.sort((a, b) => b.line - a.line)) {
|
|
1499
|
+
lineMap.splice(
|
|
1500
|
+
addition.line + 1,
|
|
1501
|
+
0,
|
|
1502
|
+
...Array.from({ length: addition.count }, () => ({
|
|
1503
|
+
...addition.location,
|
|
1504
|
+
generated: true,
|
|
1505
|
+
})),
|
|
1506
|
+
);
|
|
1507
|
+
if (generatedFrom !== undefined && addition.line < expanded.generatedFrom)
|
|
1508
|
+
generatedFrom += addition.count;
|
|
1509
|
+
}
|
|
1510
|
+
return { markdown, lineMap, ...(generatedFrom === undefined ? {} : { generatedFrom }) };
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* Prepare the complete bodies a content-index publication describes.
|
|
1515
|
+
* Prose declarations use each source's audience. Native SQL uses the complete
|
|
1516
|
+
* corpus, with target maps limited to that source's declared prose slugs.
|
|
1517
|
+
* No output files are written, and the returned inputs are read-only to the emitter.
|
|
1518
|
+
*
|
|
1519
|
+
* @param {{contentBase?: string, config?: object}} [options]
|
|
1520
|
+
* @returns {Promise<PreparedContentIndex>}
|
|
1521
|
+
*/
|
|
1522
|
+
export async function prepareContentIndex({ contentBase, config } = {}) {
|
|
1523
|
+
const resolved = config ?? loadPackConfig();
|
|
1524
|
+
const tree = path.resolve(contentBase ?? resolved.paths.content);
|
|
1525
|
+
const parsed = new Map();
|
|
1526
|
+
const records = indexRecordsFor({ contentBase: tree, config: resolved, parsed });
|
|
1527
|
+
const expandedBodies = new Map();
|
|
1528
|
+
const foundryBodies = new Map();
|
|
1529
|
+
const foreign = loadForeignIndexes(resolved, [resolved.contentPackage]);
|
|
1530
|
+
publicationProblems(
|
|
1531
|
+
foreign.stale.map((entry) => ({
|
|
1532
|
+
file: tree,
|
|
1533
|
+
severity: "error",
|
|
1534
|
+
message: `unusable ${entry.package} content index: ${entry.reason}`,
|
|
1535
|
+
})),
|
|
1536
|
+
);
|
|
1537
|
+
// Dynamic import keeps SQL preparation out of the synchronous reader's import cycle.
|
|
1538
|
+
const { prepareTreeSqlTables } = await import("./sql-tables.mjs");
|
|
1539
|
+
const { buildSiteIndex, figureIndexKeys, resolveCrossNoteFigures } =
|
|
1540
|
+
await import("./site-index.mjs");
|
|
1541
|
+
const notes = records.filter(isNoteRecord);
|
|
1542
|
+
for (const audience of new Set([
|
|
1543
|
+
...notes.map((record) => (isGmNote(record) ? "all" : "public")),
|
|
1544
|
+
"all",
|
|
1545
|
+
])) {
|
|
1546
|
+
const visible = records.filter((record) => audience === "all" || !isGmNote(record));
|
|
1547
|
+
const sources = audience === "all" ? notes : notes.filter((record) => !isGmNote(record));
|
|
1548
|
+
const prepared = await prepareTreeSqlTables(tree, {
|
|
1549
|
+
config: resolved,
|
|
1550
|
+
records,
|
|
1551
|
+
parsed,
|
|
1552
|
+
audience,
|
|
1553
|
+
foreignIndex: foreign.index,
|
|
1554
|
+
});
|
|
1555
|
+
const bodies = new Map();
|
|
1556
|
+
const figuresByFile = new Map();
|
|
1557
|
+
const figuresByAddress = new Map();
|
|
1558
|
+
const art = assetAddressIndex(visible, { config: resolved, foreign });
|
|
1559
|
+
const resolveRole = (address) => embedRole(art, address);
|
|
1560
|
+
const pages = visible.filter(isNoteRecord).flatMap((record) => {
|
|
1561
|
+
const file = noteFile(tree, record);
|
|
1562
|
+
const authored = parsed.get(file);
|
|
1563
|
+
const expanded = expandContentTables(authored.body, {
|
|
1564
|
+
source: file,
|
|
1565
|
+
sqlTables: prepared?.get(file),
|
|
1566
|
+
pageLists: prepared?.pageLists?.get(file),
|
|
1567
|
+
});
|
|
1568
|
+
publicationProblems(
|
|
1569
|
+
expanded.errors.map((problem) => ({
|
|
1570
|
+
file,
|
|
1571
|
+
line: (authored.bodyLine ?? 1) + problem.line,
|
|
1572
|
+
column: problem.column,
|
|
1573
|
+
severity: "error",
|
|
1574
|
+
message: problem.reason,
|
|
1575
|
+
})),
|
|
1576
|
+
);
|
|
1577
|
+
bodies.set(file, expanded);
|
|
1578
|
+
const byId = new Map(
|
|
1579
|
+
scanFigures(expanded.markdown, { resolveRole })
|
|
1580
|
+
.figures.filter((figure) => figure.id)
|
|
1581
|
+
.map((figure) => [
|
|
1582
|
+
figure.id,
|
|
1583
|
+
{
|
|
1584
|
+
label: figure.label,
|
|
1585
|
+
caption: figure.caption,
|
|
1586
|
+
hasCaption: figure.hasCaption,
|
|
1587
|
+
},
|
|
1588
|
+
]),
|
|
1589
|
+
);
|
|
1590
|
+
figuresByFile.set(file, byId);
|
|
1591
|
+
const address = noteAddress(record, resolved.contentPackage);
|
|
1592
|
+
if (!address) return [];
|
|
1593
|
+
for (const key of figureIndexKeys(
|
|
1594
|
+
resolved.contentPackage,
|
|
1595
|
+
record.type,
|
|
1596
|
+
record.shortcode,
|
|
1597
|
+
))
|
|
1598
|
+
figuresByAddress.set(key, byId);
|
|
1599
|
+
const slug = address.slug;
|
|
1600
|
+
return [
|
|
1601
|
+
{
|
|
1602
|
+
kind: "content",
|
|
1603
|
+
file,
|
|
1604
|
+
fm: authoredFrontmatter(record),
|
|
1605
|
+
body: authored.body,
|
|
1606
|
+
pkg: resolved.contentPackage,
|
|
1607
|
+
slug,
|
|
1608
|
+
url: `${resolved.site?.base || `/${resolved.contentPackage}/`}${slug}/`,
|
|
1609
|
+
name: record.name?.full,
|
|
1610
|
+
relPath: record.file.path,
|
|
1611
|
+
},
|
|
1612
|
+
];
|
|
1613
|
+
});
|
|
1614
|
+
const index = buildSiteIndex(pages, {
|
|
1615
|
+
package: resolved.contentPackage,
|
|
1616
|
+
records: visible,
|
|
1617
|
+
foreignIndex: foreign.index,
|
|
1618
|
+
});
|
|
1619
|
+
const dates = index.dateContext;
|
|
1620
|
+
const events = eventNoteIndex(visible, {
|
|
1621
|
+
contentPackage: resolved.contentPackage,
|
|
1622
|
+
foreignIndex: foreign.index,
|
|
1623
|
+
dates,
|
|
1624
|
+
});
|
|
1625
|
+
for (const record of sources) {
|
|
1626
|
+
const file = noteFile(tree, record);
|
|
1627
|
+
const authored = parsed.get(file);
|
|
1628
|
+
const figures = figuresByFile.get(file);
|
|
1629
|
+
const expanded = publicationExpressions(bodies.get(file), {
|
|
1630
|
+
fm: authoredFrontmatter(record),
|
|
1631
|
+
dates,
|
|
1632
|
+
events,
|
|
1633
|
+
file,
|
|
1634
|
+
bodyLine: authored.bodyLine,
|
|
1635
|
+
sqlResults: prepared?.inline?.get(file),
|
|
1636
|
+
figures: {
|
|
1637
|
+
get: (id) => figures?.get(id),
|
|
1638
|
+
note: (target) =>
|
|
1639
|
+
resolveCrossNoteFigures(target, {
|
|
1640
|
+
contentTypes: index.contentTypes,
|
|
1641
|
+
packages: index.packages,
|
|
1642
|
+
noIndexPackages: index.noIndexPackages,
|
|
1643
|
+
contentPackage: index.contentPackage,
|
|
1644
|
+
siteIndexMap: index.index,
|
|
1645
|
+
figuresByAddress,
|
|
1646
|
+
}),
|
|
1647
|
+
},
|
|
1648
|
+
});
|
|
1649
|
+
if (audience === "all") foundryBodies.set(file, expanded);
|
|
1650
|
+
if ((isGmNote(record) ? "all" : "public") === audience)
|
|
1651
|
+
expandedBodies.set(file, expanded);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return { config: resolved, contentBase: tree, records, parsed, expandedBodies, foundryBodies };
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* A captured source context, consumed without mutation by the synchronous emitter.
|
|
1659
|
+
* @typedef {object} PreparedContentIndex
|
|
1660
|
+
* @property {object} config - The exact resolved configuration for preparation.
|
|
1661
|
+
* @property {string} contentBase - The absolute captured source root.
|
|
1662
|
+
* @property {object[]} records - Complete index records, including hidden providers.
|
|
1663
|
+
* @property {Map<string, {frontmatter: object|null, body: string, raw: string, bodyLine?: number, bodyColumn?: number}>} parsed - Authored sources by absolute file.
|
|
1664
|
+
* @property {Map<string, {markdown: string, lineMap: Array<{line: number, generated: boolean}>, generatedFrom?: number}>} expandedBodies - Complete rendered bodies in each note's audience.
|
|
1665
|
+
* @property {Map<string, {markdown: string, lineMap: Array<{line: number, generated: boolean}>, generatedFrom?: number}>} foundryBodies - Complete native bodies with explicit SQL using the ALL corpus.
|
|
1666
|
+
*/
|
|
1667
|
+
|
|
1668
|
+
/** Update owned copies from an actual output body, retaining authored locations and events. */
|
|
1669
|
+
function projectPublicationBody(
|
|
1670
|
+
records,
|
|
1671
|
+
record,
|
|
1672
|
+
body,
|
|
1673
|
+
{ contentPackage, manifest, resolveRole, foundryBody = body },
|
|
1674
|
+
) {
|
|
1675
|
+
const fm = authoredFrontmatter(record);
|
|
1676
|
+
const address = noteAddress(fm, contentPackage);
|
|
1677
|
+
if (!address) return;
|
|
1678
|
+
const original = new Map(
|
|
1679
|
+
(record.anchors ?? [])
|
|
1680
|
+
.filter((anchor) => anchor.kind === "prose")
|
|
1681
|
+
.map((anchor) => [anchor.slug, anchor]),
|
|
1682
|
+
);
|
|
1683
|
+
const stub = isStubNote(fm, body);
|
|
1684
|
+
record.address = stub ? null : { ...address, canonical: readCanonicalKey(address.canonical) };
|
|
1685
|
+
record.anchors =
|
|
1686
|
+
stub ? null : (
|
|
1687
|
+
[
|
|
1688
|
+
...collectAnchors(body, 1, resolveRole).map((anchor) => {
|
|
1689
|
+
const { line: _, ...generated } = anchor;
|
|
1690
|
+
return {
|
|
1691
|
+
...(original.get(anchor.slug) ?? generated),
|
|
1692
|
+
link: new AddressLink(
|
|
1693
|
+
readCanonicalKey(address.canonical),
|
|
1694
|
+
anchor.slug,
|
|
1695
|
+
anchor.kind,
|
|
1696
|
+
),
|
|
1697
|
+
};
|
|
1698
|
+
}),
|
|
1699
|
+
...(record.anchors ?? []).filter((anchor) => anchor.kind === "event"),
|
|
1700
|
+
]
|
|
1701
|
+
);
|
|
1702
|
+
const entries = foundryEntries({ frontmatter: fm, address, body: foundryBody, manifest });
|
|
1703
|
+
const declared = new Set(
|
|
1704
|
+
(record.anchors ?? [])
|
|
1705
|
+
.filter((anchor) => anchor.kind === "prose")
|
|
1706
|
+
.map((anchor) => anchor.slug),
|
|
1707
|
+
);
|
|
1708
|
+
// Native SQL keeps its complete corpus; only a declared prose target is public.
|
|
1709
|
+
for (const entry of [entries?.own, entries?.doc])
|
|
1710
|
+
if (entry?.anchors)
|
|
1711
|
+
entry.anchors = Object.fromEntries(
|
|
1712
|
+
Object.entries(entry.anchors).filter(
|
|
1713
|
+
([slug]) => slug === LEAD_ANCHOR || declared.has(slug),
|
|
1714
|
+
),
|
|
1715
|
+
);
|
|
1716
|
+
record.foundry = foundryBlock(entries?.own, ownDocumentSystem(fm.type));
|
|
1717
|
+
record.documentation = entries?.doc?.key ? readCanonicalKey(entries.doc.key) : null;
|
|
1718
|
+
if (entries?.doc?.key) {
|
|
1719
|
+
const doc = buildDocRecord({
|
|
1720
|
+
frontmatter: fm,
|
|
1721
|
+
address,
|
|
1722
|
+
entry: entries.doc,
|
|
1723
|
+
file: record.file,
|
|
1724
|
+
contentPackage,
|
|
1725
|
+
anchors: record.anchors ?? [],
|
|
1726
|
+
});
|
|
1727
|
+
const existing = records.find((one) => one.documents && one.file.path === record.file.path);
|
|
1728
|
+
if (existing) Object.assign(existing, doc);
|
|
1729
|
+
else records.push(doc);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1397
1733
|
/**
|
|
1398
1734
|
* Emit this package's content index.
|
|
1399
1735
|
*
|
|
@@ -1403,6 +1739,7 @@ export function indexRecordsFor({
|
|
|
1403
1739
|
* @param {string} [options.outDir] - Where to write; defaults to the configured
|
|
1404
1740
|
* `paths.contentIndex`.
|
|
1405
1741
|
* @param {object} [options.config] - A resolved configuration; loaded when omitted.
|
|
1742
|
+
* @param {PreparedContentIndex} [options.publication] - Complete preparation for this tree and configuration.
|
|
1406
1743
|
* @returns {{file: string, notes: number, assets: number, records: number,
|
|
1407
1744
|
* bytes: number, full: number, draft: number, stub: number,
|
|
1408
1745
|
* dictionary: {file: string, rows: number}, terms: {file: string, rows: number}}}
|
|
@@ -1416,9 +1753,9 @@ export function indexRecordsFor({
|
|
|
1416
1753
|
* reader would take it as the authoritative statement that this package has
|
|
1417
1754
|
* no content.
|
|
1418
1755
|
*/
|
|
1419
|
-
export function emitContentIndex({ contentBase, outDir, config } = {}) {
|
|
1420
|
-
const resolved = config ?? loadPackConfig();
|
|
1421
|
-
const tree = contentBase ?? resolved.paths.content;
|
|
1756
|
+
export function emitContentIndex({ contentBase, outDir, config, publication } = {}) {
|
|
1757
|
+
const resolved = config ?? publication?.config ?? loadPackConfig();
|
|
1758
|
+
const tree = path.resolve(contentBase ?? publication?.contentBase ?? resolved.paths.content);
|
|
1422
1759
|
const dir = outDir ?? resolved.paths.contentIndex;
|
|
1423
1760
|
const contentPackage = resolved.contentPackage;
|
|
1424
1761
|
|
|
@@ -1435,7 +1772,81 @@ export function emitContentIndex({ contentBase, outDir, config } = {}) {
|
|
|
1435
1772
|
// publishes pages is no part of an address, and depending on it would make
|
|
1436
1773
|
// the index refuse to build for a configuration that is perfectly able to
|
|
1437
1774
|
// state one.
|
|
1438
|
-
const
|
|
1775
|
+
const parsed = publication?.parsed ?? new Map();
|
|
1776
|
+
if (
|
|
1777
|
+
publication &&
|
|
1778
|
+
(publication.config !== resolved ||
|
|
1779
|
+
publication.contentBase !== tree ||
|
|
1780
|
+
!Array.isArray(publication.records) ||
|
|
1781
|
+
!(publication.parsed instanceof Map) ||
|
|
1782
|
+
!(publication.expandedBodies instanceof Map) ||
|
|
1783
|
+
!(publication.foundryBodies instanceof Map))
|
|
1784
|
+
)
|
|
1785
|
+
throw new Error(
|
|
1786
|
+
"Use await prepareContentIndex({ config, contentBase }) for this publication's source tree and configuration",
|
|
1787
|
+
);
|
|
1788
|
+
const records = cloneAddressState(
|
|
1789
|
+
publication?.records ?? indexRecordsFor({ contentBase: tree, config: resolved, parsed }),
|
|
1790
|
+
);
|
|
1791
|
+
const notes = records.filter(isNoteRecord);
|
|
1792
|
+
const needsPreparation =
|
|
1793
|
+
!publication &&
|
|
1794
|
+
(notes.some(
|
|
1795
|
+
(record) =>
|
|
1796
|
+
dynamicPublicationBody(parsed.get(noteFile(tree, record))?.body) ||
|
|
1797
|
+
record.data?.events?.length,
|
|
1798
|
+
) ||
|
|
1799
|
+
derivedSections(notes, {
|
|
1800
|
+
fileOf: (record) => noteFile(tree, record),
|
|
1801
|
+
config: resolved,
|
|
1802
|
+
foreignIndex: loadForeignIndexes(resolved, [contentPackage]).index,
|
|
1803
|
+
}).size > 0);
|
|
1804
|
+
if (needsPreparation)
|
|
1805
|
+
throw new Error(
|
|
1806
|
+
"Dynamic content-index publication requires await prepareContentIndex({ config, contentBase }); pass its result as emitContentIndex({ config, contentBase, publication })",
|
|
1807
|
+
);
|
|
1808
|
+
if (
|
|
1809
|
+
publication &&
|
|
1810
|
+
notes.some((record) => {
|
|
1811
|
+
const file = noteFile(tree, record);
|
|
1812
|
+
const body = publication.expandedBodies.get(file);
|
|
1813
|
+
const native = publication.foundryBodies.get(file);
|
|
1814
|
+
return (
|
|
1815
|
+
!parsed.has(file) ||
|
|
1816
|
+
typeof body?.markdown !== "string" ||
|
|
1817
|
+
!Array.isArray(body.lineMap) ||
|
|
1818
|
+
typeof native?.markdown !== "string" ||
|
|
1819
|
+
!Array.isArray(native.lineMap)
|
|
1820
|
+
);
|
|
1821
|
+
})
|
|
1822
|
+
)
|
|
1823
|
+
throw new Error(
|
|
1824
|
+
"Incomplete content-index bodies: run await prepareContentIndex({ config, contentBase }) before publication",
|
|
1825
|
+
);
|
|
1826
|
+
const art = assetAddressIndex(records, { config: resolved });
|
|
1827
|
+
const publicLexical =
|
|
1828
|
+
publication ? undefined : (
|
|
1829
|
+
lexicalSectionsFor(
|
|
1830
|
+
records.filter((record) => !isGmNote(record)),
|
|
1831
|
+
records,
|
|
1832
|
+
)
|
|
1833
|
+
);
|
|
1834
|
+
const allLexical = publication ? undefined : lexicalSectionsFor(records);
|
|
1835
|
+
for (const record of notes) {
|
|
1836
|
+
const file = noteFile(tree, record);
|
|
1837
|
+
const lexical = (isGmNote(record) ? allLexical : publicLexical)?.get(
|
|
1838
|
+
canonicalKey(contentPackage, "note", record.type, record.shortcode),
|
|
1839
|
+
);
|
|
1840
|
+
const body =
|
|
1841
|
+
publication?.expandedBodies.get(file)?.markdown ??
|
|
1842
|
+
insertLexicalSections(parsed.get(file).body, lexical).markdown;
|
|
1843
|
+
projectPublicationBody(records, record, body, {
|
|
1844
|
+
contentPackage,
|
|
1845
|
+
manifest: foundryIdentities(resolved),
|
|
1846
|
+
resolveRole: (address) => embedRole(art, address),
|
|
1847
|
+
foundryBody: publication?.foundryBodies.get(file).markdown,
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1439
1850
|
const assetBindings = checkForeignAssetBindings(resolved);
|
|
1440
1851
|
if (assetBindings.length)
|
|
1441
1852
|
throw Object.assign(new Error(assetBindings.map(formatDiagnostic).join("\n")), {
|
|
@@ -1450,16 +1861,19 @@ export function emitContentIndex({ contentBase, outDir, config } = {}) {
|
|
|
1450
1861
|
|
|
1451
1862
|
const text = serializeContentIndex(records);
|
|
1452
1863
|
const file = path.join(dir, metadataFileName(contentPackage));
|
|
1453
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1454
|
-
fs.writeFileSync(file, text);
|
|
1455
1864
|
|
|
1456
1865
|
// The `dictionary` and `terms` relations as datafiles beside the index, for
|
|
1457
1866
|
// `jq`, an editor or another package. Nothing in the build reads them back.
|
|
1458
1867
|
const derived = dictionaryAndTermRows(records.map((record) => encodeAddresses(record)));
|
|
1868
|
+
const serialized = Object.fromEntries(
|
|
1869
|
+
["dictionary", "terms"].map((relation) => [relation, serializeRows(derived[relation])]),
|
|
1870
|
+
);
|
|
1871
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1872
|
+
fs.writeFileSync(file, text);
|
|
1459
1873
|
const datafiles = {};
|
|
1460
1874
|
for (const relation of ["dictionary", "terms"]) {
|
|
1461
1875
|
const rowsFile = path.join(dir, `${contentPackage}-${relation}.jsonl`);
|
|
1462
|
-
fs.writeFileSync(rowsFile,
|
|
1876
|
+
fs.writeFileSync(rowsFile, serialized[relation]);
|
|
1463
1877
|
datafiles[relation] = { file: rowsFile, rows: derived[relation].length };
|
|
1464
1878
|
}
|
|
1465
1879
|
|