@heroiclands/package-build 23.7.0 → 23.8.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 +27 -0
- package/README.md +32 -0
- package/bin/content-commands.mjs +20 -6
- package/ci/check-banners.mjs +37 -19
- package/container.mjs +48 -10
- package/docs/api.md +87 -43
- package/docs/authoring/frontmatter.md +30 -1
- package/docs/authoring/links-and-markup.md +229 -36
- package/docs/commands.md +12 -2
- package/docs/content-format.md +24 -1
- package/docs/design.md +31 -0
- package/docs/guides/e2e.md +37 -1
- package/docs/project-setup.md +30 -15
- package/docs/reference/format-details.md +418 -220
- package/docs/reference/note-types.md +90 -31
- package/e2e.mjs +89 -2
- package/engine/base-compiler.mjs +11 -18
- package/engine/compile-corpus.mjs +2 -1
- package/engine/content-alerts.mjs +4 -2
- package/engine/content-blocks.mjs +18 -36
- package/engine/content-format.yaml +3 -0
- package/engine/content-index.mjs +35 -22
- package/engine/content-poetry.mjs +9 -0
- package/engine/diagnostics.mjs +24 -11
- package/engine/frontmatter-lint.mjs +1 -1
- package/engine/generate.mjs +12 -22
- package/engine/helpers.mjs +8 -6
- package/engine/infobox.mjs +3 -0
- package/engine/journals.mjs +11 -1
- package/engine/language-data.mjs +236 -1
- package/engine/language-rows.mjs +235 -3
- package/engine/language-terms.mjs +524 -9
- package/engine/language-words.mjs +75 -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 +19 -10
- package/engine/note-package.mjs +3 -5
- package/engine/note-vocabulary.mjs +29 -1
- package/engine/pdf-build.mjs +8 -1
- 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 +5 -1
- package/engine/site-preview.mjs +4 -2
- package/engine/sql-tables.mjs +37 -12
- package/engine/standings.mjs +14 -12
- package/engine/word-rows.mjs +8 -3
- package/engine/word-terms.mjs +60 -2
- 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 +20 -7
- package/types/engine/diagnostics.d.mts +7 -6
- package/types/engine/helpers.d.mts +7 -4
- package/types/engine/language-rows.d.mts +89 -3
- package/types/engine/language-terms.d.mts +106 -4
- 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-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/sql-tables.d.mts +7 -3
- package/types/engine/word-rows.d.mts +2 -1
- package/types/engine/word-terms.d.mts +11 -0
package/engine/site-preview.mjs
CHANGED
|
@@ -22,7 +22,8 @@ import { foundryIdentities } from "./foundry-entries.mjs";
|
|
|
22
22
|
import { applyComputedBeingAge, presentAmongRecords } from "./being-age.mjs";
|
|
23
23
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
24
24
|
import { cachedMetadataIndexes, noContentIndexPackages } from "./metadata-index.mjs";
|
|
25
|
-
import {
|
|
25
|
+
import { seeAlsoPages } from "./see-also.mjs";
|
|
26
|
+
import { buildSiteIndex, resolveInfoboxRef } from "./site-index.mjs";
|
|
26
27
|
import {
|
|
27
28
|
attachGeneratedSections,
|
|
28
29
|
findSqlBlocks,
|
|
@@ -320,7 +321,8 @@ export async function prepareSitePreview({ config = loadPackConfig() } = {}) {
|
|
|
320
321
|
url: page.url,
|
|
321
322
|
type: String(fm.type),
|
|
322
323
|
});
|
|
323
|
-
const
|
|
324
|
+
const deliberate = seeAlsoPages([page], (ref) => resolveInfoboxRef(index, ref));
|
|
325
|
+
const related = relatedPages(edges, entries, deliberate).get(page.url);
|
|
324
326
|
if (related) result.data.related = related;
|
|
325
327
|
return {
|
|
326
328
|
ok: true,
|
package/engine/sql-tables.mjs
CHANGED
|
@@ -654,7 +654,9 @@ async function createWordRelations(connection, stem, records, prefix, audience)
|
|
|
654
654
|
* @param {readonly string[]} columns - Its columns, for the empty case.
|
|
655
655
|
* @param {Readonly<Record<string, string>>} [types] - Each column's type, for
|
|
656
656
|
* the empty case; a column it leaves out is text. Without it, a whole-number
|
|
657
|
-
* column is a number and every other column text.
|
|
657
|
+
* column is a number and every other column text. A `DOUBLE` column is cast
|
|
658
|
+
* whatever the rows hold, because a fraction written whole reads as an
|
|
659
|
+
* integer.
|
|
658
660
|
* @returns {Promise<void>}
|
|
659
661
|
*/
|
|
660
662
|
async function createRowRelation(connection, file, rows, view, columns, types) {
|
|
@@ -670,7 +672,15 @@ async function createRowRelation(connection, file, rows, view, columns, types) {
|
|
|
670
672
|
return;
|
|
671
673
|
}
|
|
672
674
|
fs.writeFileSync(file, rows.map((row) => JSON.stringify(row)).join("\n"));
|
|
673
|
-
|
|
675
|
+
const fractions = Object.entries(types ?? {})
|
|
676
|
+
.filter(([, type]) => type === "DOUBLE")
|
|
677
|
+
.map(([column]) => `CAST("${column}" AS DOUBLE) AS "${column}"`);
|
|
678
|
+
const source = readJsonAuto(file);
|
|
679
|
+
await createTableFromFile(
|
|
680
|
+
connection,
|
|
681
|
+
view,
|
|
682
|
+
fractions.length ? `SELECT * REPLACE (${fractions.join(", ")}) FROM (${source})` : source,
|
|
683
|
+
);
|
|
674
684
|
}
|
|
675
685
|
|
|
676
686
|
/**
|
|
@@ -869,8 +879,9 @@ export function renderSqlTable(
|
|
|
869
879
|
* its own table and not the whole build's report.
|
|
870
880
|
*
|
|
871
881
|
* @param {object} db - From {@link openNotesDatabase}.
|
|
872
|
-
* @param {Array<{source: string, markdown: string}>} sources -
|
|
873
|
-
* scan.
|
|
882
|
+
* @param {Array<{source: string, markdown: string, frontmatter?: object, authoredSqlBlocks?: number}>} sources -
|
|
883
|
+
* The bodies to scan. `joinGeneratedSections` records the number of authored
|
|
884
|
+
* fences; following generated event fences need no stub comparison.
|
|
874
885
|
* @param {object} [opts]
|
|
875
886
|
* @param {(ref: string) => boolean} [opts.linkable] - Passed to
|
|
876
887
|
* {@link renderSqlTable}.
|
|
@@ -894,26 +905,33 @@ export async function prepareSqlTables(db, sources, { linkable, records, pageLan
|
|
|
894
905
|
const prepared = new Map();
|
|
895
906
|
if (records) prepared.pageLists = preparePageLists(records, sources);
|
|
896
907
|
// The corpus does not change while a preparation runs, so a query written
|
|
897
|
-
// twice has one answer
|
|
898
|
-
//
|
|
908
|
+
// twice has one answer in each stub context. The normal result and the
|
|
909
|
+
// inclusive comparison occupy separate fields of the same cache entry.
|
|
910
|
+
// Each result use gets a copy because a caller rewrites the rows.
|
|
899
911
|
const answers = new Map();
|
|
900
912
|
const reader = {
|
|
901
913
|
...db,
|
|
902
914
|
async query(sql) {
|
|
903
|
-
if (!answers.has(sql)) answers.set(sql, await db.query(sql));
|
|
904
|
-
return structuredClone(answers.get(sql));
|
|
915
|
+
if (!answers.has(sql)) answers.set(sql, { result: await db.query(sql) });
|
|
916
|
+
return structuredClone(answers.get(sql).result);
|
|
917
|
+
},
|
|
918
|
+
async stubsExcluded(sql, rows) {
|
|
919
|
+
const answer = answers.get(sql);
|
|
920
|
+
if (!Object.hasOwn(answer, "excluded"))
|
|
921
|
+
answer.excluded = await db.stubsExcluded?.(sql, rows);
|
|
922
|
+
return answer.excluded;
|
|
905
923
|
},
|
|
906
924
|
};
|
|
907
925
|
let labels;
|
|
908
926
|
const labelOf = (address) => labels.get(address);
|
|
909
927
|
const languageOf =
|
|
910
928
|
pageLanguage ?? ((_source, frontmatter) => frontmatterLanguage(frontmatter, db));
|
|
911
|
-
for (const { source, markdown, frontmatter } of sources) {
|
|
929
|
+
for (const { source, markdown, frontmatter, authoredSqlBlocks = Infinity } of sources) {
|
|
912
930
|
const blocks = findSqlBlocks(markdown);
|
|
913
931
|
if (!blocks.length) continue;
|
|
914
932
|
const forNote = [];
|
|
915
933
|
prepared.set(source, forNote);
|
|
916
|
-
for (const block of blocks) {
|
|
934
|
+
for (const [ordinal, block] of blocks.entries()) {
|
|
917
935
|
try {
|
|
918
936
|
if (block.problems.length) throw new Error(block.problems.join("; "));
|
|
919
937
|
const result = await runSqlQuery(reader, block.query);
|
|
@@ -942,7 +960,12 @@ export async function prepareSqlTables(db, sources, { linkable, records, pageLan
|
|
|
942
960
|
// relation, so a fence that has not adopted stubs says so
|
|
943
961
|
// rather than quietly listing fewer settlements than the
|
|
944
962
|
// region has.
|
|
945
|
-
|
|
963
|
+
// Generated event views read entries and events, whose
|
|
964
|
+
// meaning is identical in both stub contexts.
|
|
965
|
+
stubsExcluded:
|
|
966
|
+
ordinal >= authoredSqlBlocks ? 0 : (
|
|
967
|
+
await reader.stubsExcluded(block.query, result.rows.length)
|
|
968
|
+
),
|
|
946
969
|
allowEmpty: block.allowEmpty,
|
|
947
970
|
});
|
|
948
971
|
} catch (err) {
|
|
@@ -1128,7 +1151,7 @@ export async function generatedSectionsFor(
|
|
|
1128
1151
|
*
|
|
1129
1152
|
* @param {{sections: Map<string, string>, views: Set<string>}} generated -
|
|
1130
1153
|
* From {@link generatedSectionsFor}.
|
|
1131
|
-
* @param {Array<{source: string, markdown: string, frontmatter?: object}>} sources -
|
|
1154
|
+
* @param {Array<{source: string, markdown: string, frontmatter?: object, authoredSqlBlocks?: number}>} sources -
|
|
1132
1155
|
* The bodies to answer, extended in place.
|
|
1133
1156
|
* @param {Map<string, {body: string, frontmatter: object|null}>} [parsed] -
|
|
1134
1157
|
* Notes already parsed, keyed by file; a note absent from it is read.
|
|
@@ -1138,6 +1161,7 @@ export function joinGeneratedSections(generated, sources, parsed) {
|
|
|
1138
1161
|
for (const [absPath, text] of generated.sections) {
|
|
1139
1162
|
const source = sources.find((one) => one.source === absPath);
|
|
1140
1163
|
if (source) {
|
|
1164
|
+
source.authoredSqlBlocks ??= findSqlBlocks(source.markdown).length;
|
|
1141
1165
|
source.markdown = appendGeneratedSections(source.markdown, text);
|
|
1142
1166
|
continue;
|
|
1143
1167
|
}
|
|
@@ -1147,6 +1171,7 @@ export function joinGeneratedSections(generated, sources, parsed) {
|
|
|
1147
1171
|
source: absPath,
|
|
1148
1172
|
markdown: appendGeneratedSections(body, text),
|
|
1149
1173
|
frontmatter,
|
|
1174
|
+
authoredSqlBlocks: findSqlBlocks(body).length,
|
|
1150
1175
|
});
|
|
1151
1176
|
}
|
|
1152
1177
|
}
|
package/engine/standings.mjs
CHANGED
|
@@ -378,17 +378,19 @@ function resolveBody(note, index, body) {
|
|
|
378
378
|
|
|
379
379
|
/** Findings about the body an entry names. */
|
|
380
380
|
function bodyFindings(note, body, path, isKey, found) {
|
|
381
|
-
const at = position(note, path, isKey);
|
|
381
|
+
const at = () => position(note, path, isKey);
|
|
382
382
|
const written = isAddressTuple(body) ? renderAddress(body) : String(body);
|
|
383
383
|
const label = `\`data.affiliations\`${isKey ? " key" : ""} ${JSON.stringify(written)}`;
|
|
384
384
|
if (found.reason)
|
|
385
|
-
return [{ ...at, message: `${label} is not a complete Address (${found.reason})` }];
|
|
385
|
+
return [{ ...at(), message: `${label} is not a complete Address (${found.reason})` }];
|
|
386
386
|
if (found.type)
|
|
387
|
-
return [
|
|
387
|
+
return [
|
|
388
|
+
{ ...at(), message: `\`data.affiliations\` accepts affiliation, not ${found.type}` },
|
|
389
|
+
];
|
|
388
390
|
if (found.missing)
|
|
389
391
|
return [
|
|
390
392
|
{
|
|
391
|
-
...at,
|
|
393
|
+
...at(),
|
|
392
394
|
message:
|
|
393
395
|
`\`data.affiliations\` names ${found.address}, which does not resolve ` +
|
|
394
396
|
"in this package or its declared dependencies",
|
|
@@ -438,12 +440,12 @@ function absentRankFindings(note, body, path) {
|
|
|
438
440
|
function rankFindings(note, standing, body, path, resolved) {
|
|
439
441
|
// Absent, or not a whole number: the inner-key check's finding.
|
|
440
442
|
if (!fitsKind(standing.rank ?? null, "integer")) return [];
|
|
441
|
-
const at = position(note, [...path, "rank"]);
|
|
443
|
+
const at = () => position(note, [...path, "rank"]);
|
|
442
444
|
const level = typeof standing.rank === "number" ? standing.rank : Number(standing.rank);
|
|
443
445
|
if (typeof standing.rank !== "number" || !Number.isInteger(level))
|
|
444
446
|
return [
|
|
445
447
|
{
|
|
446
|
-
...at,
|
|
448
|
+
...at(),
|
|
447
449
|
message:
|
|
448
450
|
`\`data.affiliations\` entry for ${body}: \`rank\` must be the level on that body's own ` +
|
|
449
451
|
`ladder, as a whole number, but reads ${JSON.stringify(standing.rank)}`,
|
|
@@ -455,7 +457,7 @@ function rankFindings(note, standing, body, path, resolved) {
|
|
|
455
457
|
if (!ladder.size)
|
|
456
458
|
return [
|
|
457
459
|
{
|
|
458
|
-
...at,
|
|
460
|
+
...at(),
|
|
459
461
|
message:
|
|
460
462
|
`\`data.affiliations\` entry for ${body} states a rung, and that body ` +
|
|
461
463
|
"declares no `data.governance.ranks` ladder to hold one",
|
|
@@ -464,7 +466,7 @@ function rankFindings(note, standing, body, path, resolved) {
|
|
|
464
466
|
const levels = [...ladder.keys()].sort((a, b) => a - b);
|
|
465
467
|
return [
|
|
466
468
|
{
|
|
467
|
-
...at,
|
|
469
|
+
...at(),
|
|
468
470
|
message:
|
|
469
471
|
`\`data.affiliations\` entry for ${body}: rank ${level} is not a level ` +
|
|
470
472
|
`${resolved.address} confers; its ladder runs ${levels.join(", ")}`,
|
|
@@ -477,11 +479,11 @@ function officeFindings(note, standing, body, path, resolved) {
|
|
|
477
479
|
// Absent is a membership with no post; a value that is not text is the
|
|
478
480
|
// inner-key check's finding.
|
|
479
481
|
if (typeof standing.office !== "string") return [];
|
|
480
|
-
const at = position(note, [...path, "office"]);
|
|
482
|
+
const at = () => position(note, [...path, "office"]);
|
|
481
483
|
if (!standing.office.trim())
|
|
482
484
|
return [
|
|
483
485
|
{
|
|
484
|
-
...at,
|
|
486
|
+
...at(),
|
|
485
487
|
message:
|
|
486
488
|
`\`data.affiliations\` entry for ${body}: \`office\` must name a post from its ` +
|
|
487
489
|
`\`governance.offices\`, but reads ${JSON.stringify(standing.office)}`,
|
|
@@ -493,7 +495,7 @@ function officeFindings(note, standing, body, path, resolved) {
|
|
|
493
495
|
if (!roster.size)
|
|
494
496
|
return [
|
|
495
497
|
{
|
|
496
|
-
...at,
|
|
498
|
+
...at(),
|
|
497
499
|
message:
|
|
498
500
|
`\`data.affiliations\` entry for ${body} names an office, and that body ` +
|
|
499
501
|
`${resolved.address} declares no \`data.governance.offices\` to hold one`,
|
|
@@ -501,7 +503,7 @@ function officeFindings(note, standing, body, path, resolved) {
|
|
|
501
503
|
];
|
|
502
504
|
return [
|
|
503
505
|
{
|
|
504
|
-
...at,
|
|
506
|
+
...at(),
|
|
505
507
|
message:
|
|
506
508
|
`\`data.affiliations\` entry for ${body}: office ${JSON.stringify(standing.office)} is not ` +
|
|
507
509
|
`an office ${resolved.address} names; it holds ${[...roster.keys()].join(", ")}`,
|
package/engine/word-rows.mjs
CHANGED
|
@@ -38,6 +38,7 @@ import { renderAddress } from "./address.mjs";
|
|
|
38
38
|
import { glossaryLanguage } from "./glossary-language.mjs";
|
|
39
39
|
import {
|
|
40
40
|
AFFIX_KEYS,
|
|
41
|
+
ZERO_AFFIX,
|
|
41
42
|
FIELD_KEYS,
|
|
42
43
|
GLOSSARY_ENTRY_KEYS,
|
|
43
44
|
LANGUAGE_SUBTYPE,
|
|
@@ -63,7 +64,7 @@ export const WORD_COLUMNS = Object.freeze([
|
|
|
63
64
|
export const GLOSSARY_COLUMNS = Object.freeze(["note", "position", ...GLOSSARY_ENTRY_KEYS]);
|
|
64
65
|
|
|
65
66
|
/** Every column of the `affixes` relation, in order. */
|
|
66
|
-
export const AFFIX_COLUMNS = Object.freeze([...WORD_IDENTITY_COLUMNS, ...AFFIX_KEYS]);
|
|
67
|
+
export const AFFIX_COLUMNS = Object.freeze([...WORD_IDENTITY_COLUMNS, ...AFFIX_KEYS, "written"]);
|
|
67
68
|
|
|
68
69
|
/** Every column of the `fields` relation, in order. */
|
|
69
70
|
export const FIELD_COLUMNS = Object.freeze([...WORD_IDENTITY_COLUMNS, ...FIELD_KEYS]);
|
|
@@ -299,14 +300,18 @@ function languageListRows(records, list, keys) {
|
|
|
299
300
|
|
|
300
301
|
/**
|
|
301
302
|
* The `affixes` rows of a set of index records: one per `data.affixes` entry
|
|
302
|
-
* of a language note, which is the row's `language
|
|
303
|
+
* of a language note, which is the row's `language`, with `written`, the form
|
|
304
|
+
* as a table prints it: `∅` for a zero affix, whose `form` is `NULL`.
|
|
303
305
|
*
|
|
304
306
|
* @param {Iterable<Record<string, any>>} records - Index records, every
|
|
305
307
|
* Address a string, already narrowed to the audience being published to.
|
|
306
308
|
* @returns {object[]} One row per affix, in record then entry order.
|
|
307
309
|
*/
|
|
308
310
|
export function affixRows(records) {
|
|
309
|
-
return languageListRows(records, "affixes", AFFIX_KEYS)
|
|
311
|
+
return languageListRows(records, "affixes", AFFIX_KEYS).map((row) => ({
|
|
312
|
+
...row,
|
|
313
|
+
written: row.zero === true ? ZERO_AFFIX : row.form,
|
|
314
|
+
}));
|
|
310
315
|
}
|
|
311
316
|
|
|
312
317
|
/**
|
package/engine/word-terms.mjs
CHANGED
|
@@ -44,6 +44,30 @@ export const WORD_SOURCE_KINDS = Object.freeze([
|
|
|
44
44
|
"island",
|
|
45
45
|
]);
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* What an affix makes: a case, a word derived from another, a verb's tense,
|
|
49
|
+
* aspect, mood or voice, a given name's ending, or a lineage name's ending.
|
|
50
|
+
*/
|
|
51
|
+
export const AFFIX_KINDS = Object.freeze([
|
|
52
|
+
"case",
|
|
53
|
+
"derivation",
|
|
54
|
+
"tense",
|
|
55
|
+
"aspect",
|
|
56
|
+
"mood",
|
|
57
|
+
"voice",
|
|
58
|
+
"name",
|
|
59
|
+
"lineage",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/** The affix kind whose ending says who the name is given to. */
|
|
63
|
+
export const NAME_AFFIX_KIND = "name";
|
|
64
|
+
|
|
65
|
+
/** How a zero affix is written where a table prints it. */
|
|
66
|
+
export const ZERO_AFFIX = "∅";
|
|
67
|
+
|
|
68
|
+
/** Who a given name's ending is given to. */
|
|
69
|
+
export const AFFIX_GIVEN = Object.freeze(["man", "woman", "either"]);
|
|
70
|
+
|
|
47
71
|
/** The source kind that requires `language`. */
|
|
48
72
|
export const BORROWED_SOURCE_KIND = "borrowed";
|
|
49
73
|
|
|
@@ -240,9 +264,36 @@ export const AFFIX_FIELDS = Object.freeze([
|
|
|
240
264
|
Object.freeze({
|
|
241
265
|
name: "form",
|
|
242
266
|
kind: "string",
|
|
243
|
-
required: true,
|
|
244
267
|
shape: "the affix as written, such as `-rith`",
|
|
245
|
-
describe: "The affix as a word's `built` names it.",
|
|
268
|
+
describe: "The affix as a word's `built` names it; absent on a zero affix.",
|
|
269
|
+
}),
|
|
270
|
+
Object.freeze({
|
|
271
|
+
name: "zero",
|
|
272
|
+
kind: "boolean",
|
|
273
|
+
shape: "`true`",
|
|
274
|
+
describe:
|
|
275
|
+
"The affix is written as nothing, such as a case with no ending; it states no " +
|
|
276
|
+
`\`form\`, prints as \`${ZERO_AFFIX}\`, and no \`built\` piece names it.`,
|
|
277
|
+
}),
|
|
278
|
+
Object.freeze({
|
|
279
|
+
name: "kind",
|
|
280
|
+
kind: "string",
|
|
281
|
+
allowed: AFFIX_KINDS,
|
|
282
|
+
shape: `one of ${AFFIX_KINDS.join(", ")}`,
|
|
283
|
+
describe: "What the affix makes.",
|
|
284
|
+
}),
|
|
285
|
+
Object.freeze({
|
|
286
|
+
name: "given",
|
|
287
|
+
kind: "string",
|
|
288
|
+
allowed: AFFIX_GIVEN,
|
|
289
|
+
shape: `one of ${AFFIX_GIVEN.join(", ")}`,
|
|
290
|
+
describe: `Who a given name closing on it belongs to; an affix of kind \`${NAME_AFFIX_KIND}\` only.`,
|
|
291
|
+
}),
|
|
292
|
+
Object.freeze({
|
|
293
|
+
name: "where",
|
|
294
|
+
kind: "string",
|
|
295
|
+
shape: "a string, such as `before a stop`",
|
|
296
|
+
describe: "Where this form stands, for an affix written two ways; everywhere where absent.",
|
|
246
297
|
}),
|
|
247
298
|
Object.freeze({
|
|
248
299
|
name: "gloss",
|
|
@@ -286,6 +337,13 @@ export const AFFIX_FIELDS = Object.freeze([
|
|
|
286
337
|
entries: Object.freeze({ kind: "string", shape: "a string" }),
|
|
287
338
|
describe: "The names the affix is read from.",
|
|
288
339
|
}),
|
|
340
|
+
Object.freeze({
|
|
341
|
+
name: "barredAfter",
|
|
342
|
+
kind: "list",
|
|
343
|
+
shape: "a list of sounds",
|
|
344
|
+
entries: Object.freeze({ kind: "string", shape: "a sound as written" }),
|
|
345
|
+
describe: "The sounds a stem may not close on to take the affix.",
|
|
346
|
+
}),
|
|
289
347
|
Object.freeze({
|
|
290
348
|
name: "tones",
|
|
291
349
|
kind: "string",
|
|
@@ -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
|
-
-
|
|
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
|
|
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.
|
|
43
|
-
|
|
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
|
-
{{-
|
|
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.
|
|
3
|
+
"version": "23.8.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",
|
package/types/container.d.mts
CHANGED
|
@@ -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
|
|
251
|
-
*
|
|
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
|
|
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:
|
|
635
|
-
bodyColumn:
|
|
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
|
|
238
|
-
*
|
|
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,6 +314,9 @@ 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[];
|
|
309
322
|
/**
|