@heroiclands/package-build 22.15.0 → 22.16.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 +103 -0
- package/README.md +39 -678
- package/bin/{content-build.mjs → content-commands.mjs} +197 -118
- package/bin/package-build.mjs +303 -42
- package/bin/report.mjs +1 -1
- package/ci/ci-docker.mjs +6 -3
- package/ci/ci-steps.mjs +6 -4
- package/ci/content-format-reference.mjs +106 -0
- package/config.mjs +22 -2
- package/content-config.mjs +74 -134
- package/docs/api.md +294 -234
- package/docs/authoring/assets.md +31 -0
- package/docs/authoring/dates-and-calendars.md +33 -0
- package/docs/authoring/first-note.md +48 -0
- package/docs/authoring/frontmatter.md +40 -0
- package/docs/authoring/links-and-markup.md +137 -0
- package/docs/commands.md +631 -162
- package/docs/configuration.md +176 -140
- package/docs/content-format.md +50 -4229
- package/docs/design.md +32 -0
- package/docs/diagnostics.md +29 -22
- package/docs/getting-started.md +21 -877
- package/docs/guides/book.md +153 -0
- package/docs/guides/e2e.md +143 -0
- package/docs/guides/icons.md +126 -0
- package/docs/guides/localization.md +120 -0
- package/docs/guides/module-project.md +915 -0
- package/docs/guides/packs.md +167 -0
- package/docs/guides/reference-pages.md +148 -0
- package/docs/guides/release-and-deploy.md +114 -0
- package/docs/guides/site.md +152 -0
- package/docs/guides/system-project.md +45 -0
- package/docs/index.md +16 -0
- package/docs/project-setup.md +54 -35
- package/docs/reference/format-details.md +4420 -0
- package/docs/reference/note-types.md +531 -0
- package/engine/address-render.mjs +26 -0
- package/engine/address.mjs +3 -20
- package/engine/anchors.mjs +15 -6
- package/engine/asset-bindings.mjs +6 -2
- package/engine/base-compiler.mjs +47 -11
- package/engine/being-measurements.mjs +70 -0
- package/engine/calendar-artifacts.mjs +1 -1
- package/engine/calendar-choice.mjs +32 -0
- package/engine/calendar-human.mjs +71 -0
- package/engine/calendar-notes.mjs +41 -3
- package/engine/calendars.mjs +8 -1
- package/engine/changelog-lint.mjs +2 -2
- package/engine/code-fences.mjs +23 -97
- package/engine/compile-corpus.mjs +6 -7
- package/engine/content-admonitions.mjs +114 -0
- package/engine/content-captions.mjs +181 -0
- package/engine/content-embeds.mjs +2 -2
- package/engine/content-footnotes.mjs +34 -0
- package/engine/content-format-check.mjs +2 -2
- package/engine/content-format.mjs +127 -73
- package/engine/content-format.yaml +745 -0
- package/engine/content-html.mjs +1 -1
- package/engine/content-icons.mjs +45 -92
- package/engine/content-images.mjs +22 -74
- package/engine/content-index.mjs +12 -3
- package/engine/content-links.mjs +21 -26
- package/engine/content-lint.mjs +1 -5
- package/engine/content-secrets.mjs +3 -1
- package/engine/content-tables.mjs +47 -1257
- package/engine/culture-choice.mjs +32 -0
- package/engine/date-conversion.mjs +47 -0
- package/engine/declaration-check.mjs +87 -0
- package/engine/docs-checks.mjs +140 -0
- package/engine/documentation-links.mjs +31 -0
- package/engine/exported-scene.mjs +1 -1
- package/engine/extension-attributes.mjs +74 -0
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +4 -0
- package/engine/foreign-catalog.mjs +5 -5
- package/engine/format-generated.mjs +15 -0
- package/engine/foundry-entries.mjs +3 -1
- package/engine/frontmatter-lint.mjs +5 -1
- package/engine/frontmatter.mjs +10 -0
- package/engine/generate.mjs +2 -2
- package/engine/helpers.mjs +71 -117
- package/engine/homepage.mjs +2 -3
- package/engine/index-records.mjs +1 -0
- package/engine/index.mjs +18 -3
- package/engine/infobox-registry.mjs +3 -1
- package/engine/infobox.mjs +38 -20
- package/engine/item-compiler.mjs +10 -8
- package/engine/item-docs.mjs +7 -6
- package/engine/itinerary-scenes.mjs +4 -2
- package/engine/journals.mjs +51 -10
- package/engine/macros.mjs +1 -1
- package/engine/map-build.mjs +1 -1
- package/engine/map-dot.mjs +1 -1
- package/engine/map-raster.mjs +4 -2
- package/engine/markdown-expressions.mjs +117 -0
- package/engine/metadata-index.mjs +7 -3
- package/engine/note-addresses.mjs +1 -0
- package/engine/note-dates.mjs +283 -61
- package/engine/note-package.mjs +5 -25
- package/engine/note-state.mjs +6 -2
- package/engine/note-vocabulary.mjs +74 -19
- package/engine/number-words.mjs +75 -0
- package/engine/office-holders.mjs +188 -0
- package/engine/pack-config.mjs +2 -15
- package/engine/pack-router.mjs +24 -5
- package/engine/packages.mjs +3 -2
- package/engine/pathnames.mjs +21 -24
- package/engine/pdf-build.mjs +217 -55
- package/engine/pdf-fonts.mjs +2 -2
- package/engine/pdf-images.mjs +82 -0
- package/engine/pdf-render.mjs +174 -22
- package/engine/populations.mjs +3 -2
- package/engine/project-init.mjs +499 -0
- package/engine/prose-config.mjs +1 -1
- package/engine/prose-lint.mjs +7 -6
- package/engine/readability-lint.mjs +86 -0
- package/engine/reckoning-markers.mjs +205 -93
- package/engine/scenes.mjs +5 -2
- package/engine/schema-check.mjs +3 -3
- package/engine/site-build.mjs +87 -37
- package/engine/site-config.mjs +8 -8
- package/engine/site-index.mjs +4 -0
- package/engine/site-maps.mjs +1 -1
- package/engine/site-preview.mjs +19 -8
- package/engine/social-ties.mjs +5 -1
- package/engine/sql-tables.mjs +92 -22
- package/engine/web-wikilinks.mjs +15 -31
- package/engine/wikilink-syntax.mjs +4 -1
- package/engine/wikilinks.mjs +12 -14
- package/hm3/item-fields.mjs +1 -1
- package/hm3/items.mjs +12 -45
- package/index.mjs +2 -14
- package/manifest.mjs +31 -4
- package/markdownlint-config.mjs +1 -1
- package/package.json +32 -11
- package/prettier-config.mjs +1 -1
- package/sohl/being-info.mjs +3 -145
- package/sohl/index.mjs +1 -1
- package/sohl/infobox.mjs +2 -1
- package/sohl/item-builders.mjs +3 -27
- package/sohl/item-fields.mjs +61 -22
- package/sohl/kb-passes.mjs +2 -2
- package/sohl/note-schemas.mjs +0 -8
- package/stage.mjs +19 -0
- package/types/config.d.mts +12 -3
- package/types/content-config.d.mts +15 -81
- package/types/engine/address-render.d.mts +13 -0
- package/types/engine/address.d.mts +2 -11
- package/types/engine/anchors.d.mts +3 -4
- package/types/engine/asset-bindings.d.mts +9 -2
- package/types/engine/base-compiler.d.mts +1 -2
- package/types/engine/being-measurements.d.mts +16 -0
- package/types/engine/calendar-choice.d.mts +14 -0
- package/types/engine/calendar-human.d.mts +4 -0
- package/types/engine/calendar-notes.d.mts +2 -0
- package/types/engine/calendars.d.mts +5 -3
- package/types/engine/code-fences.d.mts +6 -72
- package/types/engine/compile-corpus.d.mts +1 -3
- package/types/engine/content-admonitions.d.mts +24 -0
- package/types/engine/content-captions.d.mts +45 -0
- package/types/engine/content-footnotes.d.mts +10 -0
- package/types/engine/content-format-check.d.mts +17 -4
- package/types/engine/content-format.d.mts +24 -11
- package/types/engine/content-icons.d.mts +14 -23
- package/types/engine/content-images.d.mts +2 -2
- package/types/engine/content-index.d.mts +9 -2
- package/types/engine/content-links.d.mts +3 -5
- package/types/engine/content-tables.d.mts +11 -140
- package/types/engine/culture-choice.d.mts +14 -0
- package/types/engine/date-conversion.d.mts +4 -0
- package/types/engine/declaration-check.d.mts +24 -0
- package/types/engine/docs-checks.d.mts +28 -0
- package/types/engine/documentation-links.d.mts +7 -0
- package/types/engine/exported-scene.d.mts +6 -2
- package/types/engine/extension-attributes.d.mts +14 -0
- package/types/engine/field-spec.d.mts +9 -0
- package/types/engine/format-generated.d.mts +7 -0
- package/types/engine/frontmatter.d.mts +14 -4
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +37 -92
- package/types/engine/homepage.d.mts +2 -3
- package/types/engine/index.d.mts +5 -0
- package/types/engine/infobox-registry.d.mts +3 -1
- package/types/engine/infobox.d.mts +17 -6
- package/types/engine/item-compiler.d.mts +4 -4
- package/types/engine/item-docs.d.mts +4 -3
- package/types/engine/journals.d.mts +10 -7
- package/types/engine/map-graphviz.d.mts +1 -1
- package/types/engine/markdown-expressions.d.mts +17 -0
- package/types/engine/note-dates.d.mts +47 -1
- package/types/engine/note-package.d.mts +3 -19
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/note-vocabulary.d.mts +9 -0
- package/types/engine/number-words.d.mts +4 -0
- package/types/engine/office-holders.d.mts +14 -0
- package/types/engine/pack-config.d.mts +1 -1
- package/types/engine/packages.d.mts +3 -2
- package/types/engine/pdf-images.d.mts +38 -0
- package/types/engine/populations.d.mts +2 -2
- package/types/engine/project-init.d.mts +17 -0
- package/types/engine/prose-config.d.mts +1 -1
- package/types/engine/prose-lint.d.mts +3 -2
- package/types/engine/readability-lint.d.mts +22 -0
- package/types/engine/reckoning-markers.d.mts +4 -2
- package/types/engine/schema-check.d.mts +3 -3
- package/types/engine/site-build.d.mts +38 -28
- package/types/engine/site-config.d.mts +2 -2
- package/types/engine/site-index.d.mts +4 -0
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/social-ties.d.mts +8 -2
- package/types/engine/sql-tables.d.mts +13 -2
- package/types/engine/web-wikilinks.d.mts +2 -1
- package/types/engine/wikilinks.d.mts +7 -1
- package/types/hm3/items.d.mts +5 -6
- package/types/labels.d.mts +40 -0
- package/types/manifest.d.mts +3 -3
- package/types/sohl/actors.d.mts +2 -2
- package/types/sohl/being-info.d.mts +3 -43
- package/types/sohl/index.d.mts +1 -1
- package/types/sohl/infobox.d.mts +5 -2
- package/types/sohl/item-fields.d.mts +4 -13
- package/types/stage.d.mts +9 -0
- package/CONTENT.md +0 -3239
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
2
|
+
|
|
3
|
+
import { isAddressTuple, parseAddress, renderAddress } from "./address.mjs";
|
|
4
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Require a being's primary culture to address a culture lore note.
|
|
8
|
+
* @param {object} note
|
|
9
|
+
* @param {{index?: object}} [options]
|
|
10
|
+
*/
|
|
11
|
+
export function checkCultureChoice(note, { index } = {}) {
|
|
12
|
+
const value = note.fm?.data?.culture;
|
|
13
|
+
if (value === undefined || value === null || !index?.addressHit) return [];
|
|
14
|
+
const tuple = parseAddress(value, {
|
|
15
|
+
package: index.contentPackage,
|
|
16
|
+
system: "note",
|
|
17
|
+
type: "lore",
|
|
18
|
+
types: index.types,
|
|
19
|
+
packages: index.packages,
|
|
20
|
+
});
|
|
21
|
+
if (tuple.reason || tuple.type !== "lore") return [];
|
|
22
|
+
const target = index.addressHit(renderAddress(tuple));
|
|
23
|
+
if (!target || (target.fm ?? target).subType === "culture") return [];
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
file: note.file,
|
|
27
|
+
...positionOfFrontmatterPath(note.raw ?? "", ["data", "culture"]),
|
|
28
|
+
severity: "error",
|
|
29
|
+
message: `data.culture ${isAddressTuple(value) ? renderAddress(value) : value} names lore whose subType is not culture`,
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
2
|
+
|
|
3
|
+
import { formatCanonicalDate, parseCanonicalDate } from "./calendars.mjs";
|
|
4
|
+
import { addressedCalendarDate } from "./calendar-human.mjs";
|
|
5
|
+
import { calendarEras, eraCovering, formatNoteDate, parseNoteDate } from "./note-dates.mjs";
|
|
6
|
+
|
|
7
|
+
/** Convert a day in an addressed calendar to its canonical spelling. */
|
|
8
|
+
export function dateFromCalendar(reference, value, context) {
|
|
9
|
+
const eras = calendarEras(reference, context);
|
|
10
|
+
const text = String(value ?? "").trim();
|
|
11
|
+
const approximate = text.startsWith("~");
|
|
12
|
+
const named = addressedCalendarDate(approximate ? text.slice(1).trim() : text, eras);
|
|
13
|
+
if (!named) throw new RangeError("datefrom needs a named calendar date precise to the day");
|
|
14
|
+
const authored = `${approximate ? "~" : ""}${named}`;
|
|
15
|
+
const parsed = parseNoteDate(authored, { ...context, allowUnknown: false });
|
|
16
|
+
const error = parsed.findings.find((finding) => finding.severity === "error");
|
|
17
|
+
if (error) throw new RangeError(error.message);
|
|
18
|
+
const date = parsed.date;
|
|
19
|
+
if (!date || date.precision !== "day" || !Number.isSafeInteger(date.canonicalDay))
|
|
20
|
+
throw new RangeError("datefrom needs a calendar date precise to the day");
|
|
21
|
+
if (!eras.includes(context.eras.get(date.qualifier)))
|
|
22
|
+
throw new RangeError(`date belongs to a different calendar than ${reference}`);
|
|
23
|
+
if (eraCovering(date, eras, context.daysPerYear) !== context.eras.get(date.qualifier))
|
|
24
|
+
throw new RangeError(`date falls outside calendar ${reference}`);
|
|
25
|
+
return `${date.approximate ? "~" : ""}${formatCanonicalDate(
|
|
26
|
+
{ year: date.canonicalYear, day: date.canonicalDay, seconds: date.seconds },
|
|
27
|
+
context.daysPerYear,
|
|
28
|
+
)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Convert a canonical day to the date in the covering era of a calendar. */
|
|
32
|
+
export function dateToCalendar(reference, value, context) {
|
|
33
|
+
const text = String(value ?? "");
|
|
34
|
+
const canonical = parseCanonicalDate(
|
|
35
|
+
text.startsWith("~") ? text.slice(1) : text,
|
|
36
|
+
context?.daysPerYear,
|
|
37
|
+
);
|
|
38
|
+
if (!canonical)
|
|
39
|
+
throw new RangeError("dateto needs <year>.<day>[:HHMMSS] within the world's year");
|
|
40
|
+
const parsed = parseNoteDate(value, context).date;
|
|
41
|
+
const era = eraCovering(parsed, calendarEras(reference, context), context.daysPerYear);
|
|
42
|
+
if (!era) throw new RangeError(`canonical date falls outside calendar ${reference}`);
|
|
43
|
+
const printed = formatNoteDate(parsed, era, context.daysPerYear);
|
|
44
|
+
if (!printed || printed.day === null)
|
|
45
|
+
throw new RangeError(`canonical date cannot be expressed in calendar ${reference}`);
|
|
46
|
+
return printed.text;
|
|
47
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { globSync } from "glob";
|
|
8
|
+
import ts from "typescript";
|
|
9
|
+
|
|
10
|
+
/** All declaration entry points advertised by a package's exports map. */
|
|
11
|
+
export function exportedDeclarationFiles(root) {
|
|
12
|
+
const manifest = path.join(root, "package.json");
|
|
13
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
14
|
+
const files = new Set();
|
|
15
|
+
const findings = [];
|
|
16
|
+
for (const entry of Object.values(pkg.exports ?? {})) {
|
|
17
|
+
const declared = typeof entry === "object" && entry !== null ? entry.types : null;
|
|
18
|
+
if (typeof declared !== "string") continue;
|
|
19
|
+
const pattern = path.resolve(root, declared);
|
|
20
|
+
const matches = pattern.includes("*") ? globSync(pattern) : [pattern];
|
|
21
|
+
if (!matches.length || matches.some((file) => !fs.existsSync(file))) {
|
|
22
|
+
findings.push({
|
|
23
|
+
file: manifest,
|
|
24
|
+
severity: "error",
|
|
25
|
+
message: `exported declaration ${declared} does not exist`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
for (const file of matches) {
|
|
29
|
+
if (fs.existsSync(file)) files.add(file);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { files: [...files].sort(), findings };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Type-check project declarations with library checking enabled.
|
|
37
|
+
* @param {string} project - TypeScript project file.
|
|
38
|
+
* @param {{exports?: boolean}} [options]
|
|
39
|
+
* @returns {Array<{file: string, line?: number, column?: number, severity: "error", message: string}>}
|
|
40
|
+
*/
|
|
41
|
+
export function checkDeclarations(project, options = {}) {
|
|
42
|
+
const requestedProject = path.resolve(project);
|
|
43
|
+
const configFile =
|
|
44
|
+
fs.existsSync(requestedProject) ? fs.realpathSync(requestedProject) : requestedProject;
|
|
45
|
+
const root = path.dirname(configFile);
|
|
46
|
+
const findings = [];
|
|
47
|
+
const report = (diagnostic) => {
|
|
48
|
+
if (diagnostic.file?.fileName.split(path.sep).includes("node_modules")) return;
|
|
49
|
+
const position =
|
|
50
|
+
diagnostic.file && diagnostic.start !== undefined ?
|
|
51
|
+
diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start)
|
|
52
|
+
: null;
|
|
53
|
+
findings.push({
|
|
54
|
+
file: diagnostic.file?.fileName ?? configFile,
|
|
55
|
+
...(position ? { line: position.line + 1, column: position.character + 1 } : {}),
|
|
56
|
+
severity: "error",
|
|
57
|
+
message: `TS${diagnostic.code}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, " ")}`,
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const source = ts.readConfigFile(configFile, ts.sys.readFile);
|
|
61
|
+
if (source.error) {
|
|
62
|
+
report(source.error);
|
|
63
|
+
return findings;
|
|
64
|
+
}
|
|
65
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
66
|
+
source.config,
|
|
67
|
+
ts.sys,
|
|
68
|
+
root,
|
|
69
|
+
{
|
|
70
|
+
skipLibCheck: false,
|
|
71
|
+
noEmit: true,
|
|
72
|
+
},
|
|
73
|
+
configFile,
|
|
74
|
+
);
|
|
75
|
+
for (const diagnostic of parsed.errors) {
|
|
76
|
+
if (!(options.exports && diagnostic.code === 18003)) report(diagnostic);
|
|
77
|
+
}
|
|
78
|
+
const roots = new Set(parsed.fileNames);
|
|
79
|
+
if (options.exports) {
|
|
80
|
+
const exported = exportedDeclarationFiles(root);
|
|
81
|
+
findings.push(...exported.findings);
|
|
82
|
+
for (const file of exported.files) roots.add(file);
|
|
83
|
+
}
|
|
84
|
+
const program = ts.createProgram([...roots], parsed.options);
|
|
85
|
+
for (const diagnostic of ts.getPreEmitDiagnostics(program)) report(diagnostic);
|
|
86
|
+
return findings;
|
|
87
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
/** @param {string} root */
|
|
9
|
+
function* markdownFiles(root) {
|
|
10
|
+
for (const entry of fs
|
|
11
|
+
.readdirSync(root, { withFileTypes: true })
|
|
12
|
+
.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
13
|
+
const file = path.join(root, entry.name);
|
|
14
|
+
if (entry.isDirectory()) yield* markdownFiles(file);
|
|
15
|
+
else if (entry.isFile() && file.endsWith(".md")) yield file;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Preserve offsets while excluding literal examples from link scanning. */
|
|
20
|
+
export function maskMarkdownCode(source, spans = true) {
|
|
21
|
+
const pattern =
|
|
22
|
+
spans ?
|
|
23
|
+
/```[\s\S]*?```|~~~[\s\S]*?~~~|``[^`]*``|`[^`]*`/g
|
|
24
|
+
: /```[\s\S]*?```|~~~[\s\S]*?~~~/g;
|
|
25
|
+
return source.replace(pattern, (match) => match.replace(/[^\n]/g, " "));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** GitHub heading slug, retaining separate spaces after punctuation removal. */
|
|
29
|
+
export function docHeadingSlug(value) {
|
|
30
|
+
return value
|
|
31
|
+
.trim()
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replace(/[^\w\s-]/g, "")
|
|
34
|
+
.trim()
|
|
35
|
+
.replace(/\s/g, "-");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @param {string} source */
|
|
39
|
+
export function docAnchors(source) {
|
|
40
|
+
const body = maskMarkdownCode(source, false);
|
|
41
|
+
const anchors = new Set();
|
|
42
|
+
for (const [, heading] of body.matchAll(/^#{1,6}\s+(.+?)\s*#*$/gm)) {
|
|
43
|
+
const explicit = heading.match(/\{#([^}]+)\}\s*$/);
|
|
44
|
+
anchors.add(
|
|
45
|
+
explicit ?
|
|
46
|
+
explicit[1].trim()
|
|
47
|
+
: docHeadingSlug(heading.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
for (const [, anchor] of body.matchAll(/<a\s[^>]*(?:id|name)="([^"]+)"/g)) {
|
|
51
|
+
anchors.add(anchor);
|
|
52
|
+
}
|
|
53
|
+
return anchors;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check relative Markdown links beneath a documentation root.
|
|
58
|
+
* @param {string} root
|
|
59
|
+
* @returns {Array<{file: string, line: number, column: number, severity: "error", message: string}>}
|
|
60
|
+
*/
|
|
61
|
+
export function checkDocLinks(root) {
|
|
62
|
+
const findings = [];
|
|
63
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
64
|
+
return [{ file: root, severity: "error", message: "documentation root does not exist" }];
|
|
65
|
+
}
|
|
66
|
+
const anchors = new Map();
|
|
67
|
+
for (const file of markdownFiles(root)) {
|
|
68
|
+
const body = maskMarkdownCode(fs.readFileSync(file, "utf8"));
|
|
69
|
+
for (const match of body.matchAll(/\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)) {
|
|
70
|
+
const link = match[1];
|
|
71
|
+
if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(link) || link.startsWith("/")) continue;
|
|
72
|
+
const hash = link.indexOf("#");
|
|
73
|
+
const relative = hash < 0 ? link : link.slice(0, hash);
|
|
74
|
+
const anchor = hash < 0 ? "" : link.slice(hash + 1);
|
|
75
|
+
const offset = match.index + 2;
|
|
76
|
+
const location = {
|
|
77
|
+
file,
|
|
78
|
+
line: body.slice(0, match.index).split("\n").length,
|
|
79
|
+
column: offset - body.lastIndexOf("\n", offset),
|
|
80
|
+
severity: /** @type {const} */ ("error"),
|
|
81
|
+
};
|
|
82
|
+
let target;
|
|
83
|
+
try {
|
|
84
|
+
target = relative ? path.resolve(path.dirname(file), decodeURI(relative)) : file;
|
|
85
|
+
} catch {
|
|
86
|
+
findings.push({ ...location, message: `link ${link} has invalid URI encoding` });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!fs.existsSync(target)) {
|
|
90
|
+
findings.push({
|
|
91
|
+
...location,
|
|
92
|
+
message: `link ${link} resolves to ${path.relative(process.cwd(), target)}, which does not exist`,
|
|
93
|
+
});
|
|
94
|
+
} else if (anchor && target.endsWith(".md")) {
|
|
95
|
+
if (!anchors.has(target))
|
|
96
|
+
anchors.set(target, docAnchors(fs.readFileSync(target, "utf8")));
|
|
97
|
+
if (!anchors.get(target).has(anchor)) {
|
|
98
|
+
findings.push({
|
|
99
|
+
...location,
|
|
100
|
+
message: `link ${link} points at an anchor nobody declares`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return findings;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check each direct Markdown page in each section against the root README.
|
|
111
|
+
* @param {string} root
|
|
112
|
+
* @returns {Array<{file: string, severity: "error", message: string}>}
|
|
113
|
+
*/
|
|
114
|
+
export function checkDocIndex(root) {
|
|
115
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
116
|
+
return [{ file: root, severity: "error", message: "documentation root does not exist" }];
|
|
117
|
+
}
|
|
118
|
+
const index = path.join(root, "README.md");
|
|
119
|
+
if (!fs.existsSync(index))
|
|
120
|
+
return [{ file: index, severity: "error", message: "documentation index does not exist" }];
|
|
121
|
+
const readme = fs.readFileSync(index, "utf8");
|
|
122
|
+
const findings = [];
|
|
123
|
+
for (const section of fs
|
|
124
|
+
.readdirSync(root, { withFileTypes: true })
|
|
125
|
+
.filter((entry) => entry.isDirectory())) {
|
|
126
|
+
const dir = path.join(root, section.name);
|
|
127
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
128
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
129
|
+
const relative = `${section.name}/${entry.name}`;
|
|
130
|
+
if (!readme.includes(relative)) {
|
|
131
|
+
findings.push({
|
|
132
|
+
file: path.join(dir, entry.name),
|
|
133
|
+
severity: /** @type {const} */ ("error"),
|
|
134
|
+
message: `not linked from ${index}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return findings;
|
|
140
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { parseMarkdownFile } from "./helpers.mjs";
|
|
7
|
+
|
|
8
|
+
const LOCAL_MARKDOWN_LINK = /\[([^\]\n]+)\]\(([^\s)]+\.md(?:#[^\s)]*)?)\)/g;
|
|
9
|
+
|
|
10
|
+
/** Keep repository-relative documentation links usable on addressed pages. */
|
|
11
|
+
export function documentationLinksPass({ config, book = false }) {
|
|
12
|
+
const root = config.paths.content;
|
|
13
|
+
const cache = new Map();
|
|
14
|
+
return {
|
|
15
|
+
beforeLinks(text, page) {
|
|
16
|
+
return text.replace(LOCAL_MARKDOWN_LINK, (whole, label, target) => {
|
|
17
|
+
const [fileName, fragment] = target.split("#", 2);
|
|
18
|
+
const file = path.resolve(path.dirname(page.file), fileName);
|
|
19
|
+
if (!file.startsWith(`${root}${path.sep}`) || !fs.existsSync(file)) return whole;
|
|
20
|
+
let address = cache.get(file);
|
|
21
|
+
if (!address) {
|
|
22
|
+
const { frontmatter } = parseMarkdownFile(file);
|
|
23
|
+
if (!frontmatter?.type || !frontmatter?.shortcode) return whole;
|
|
24
|
+
address = `/${config.contentPackage}/${frontmatter.type}-${frontmatter.shortcode}/`;
|
|
25
|
+
cache.set(file, address);
|
|
26
|
+
}
|
|
27
|
+
return `[${label}](${address}${fragment && !book ? `#${fragment}` : ""})`;
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -147,7 +147,7 @@ function keyDocument(doc, collection, parentIds, sceneId) {
|
|
|
147
147
|
*
|
|
148
148
|
* @param {object} fm - The map note's frontmatter.
|
|
149
149
|
* @param {string} markdown - Its converted Markdown body.
|
|
150
|
-
* @param {object} [opts] - Pack metadata, derived JournalEntry ID, and asset resolver.
|
|
150
|
+
* @param {{journalEntryId?: string, stats?: object, resolveAddress?: Function}} [opts] - Pack metadata, derived JournalEntry ID, and asset resolver.
|
|
151
151
|
* @returns {object} The keyed Scene document.
|
|
152
152
|
*/
|
|
153
153
|
export function buildExportedScene(fm, markdown, { journalEntryId, stats, resolveAddress } = {}) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read the shared braced attribute grammar used by body extensions.
|
|
5
|
+
* Callers validate which names and classes their extension understands.
|
|
6
|
+
* @param {string} source - Text inside the braces.
|
|
7
|
+
* @returns {{id: string, classes: string[], values: Record<string, string>, problems: string[]}}
|
|
8
|
+
*/
|
|
9
|
+
export function parseExtensionAttributes(source) {
|
|
10
|
+
const text = String(source ?? "");
|
|
11
|
+
const result = { id: "", classes: [], values: {}, problems: [] };
|
|
12
|
+
let at = 0;
|
|
13
|
+
while (at < text.length) {
|
|
14
|
+
while (/\s/.test(text[at] ?? "")) at++;
|
|
15
|
+
if (at >= text.length) break;
|
|
16
|
+
const start = at;
|
|
17
|
+
if (text[at] === "#" || text[at] === ".") {
|
|
18
|
+
const kind = text[at++];
|
|
19
|
+
while (at < text.length && !/\s/.test(text[at])) at++;
|
|
20
|
+
const name = text.slice(start + 1, at);
|
|
21
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name)) {
|
|
22
|
+
result.problems.push(
|
|
23
|
+
`invalid ${kind === "#" ? "id" : "class"} attribute ${text.slice(start, at)}`,
|
|
24
|
+
);
|
|
25
|
+
} else if (kind === "#") {
|
|
26
|
+
if (result.id) result.problems.push("an attribute list has more than one id");
|
|
27
|
+
else result.id = name;
|
|
28
|
+
} else if (result.classes.includes(name)) {
|
|
29
|
+
result.problems.push(`class .${name} is written twice`);
|
|
30
|
+
} else result.classes.push(name);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const key = /^[A-Za-z][A-Za-z0-9_-]*/.exec(text.slice(at))?.[0];
|
|
34
|
+
if (!key || text[at + key.length] !== "=") {
|
|
35
|
+
while (at < text.length && !/\s/.test(text[at])) at++;
|
|
36
|
+
result.problems.push(`${text.slice(start, at)} is not a key=value attribute`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
at += key.length + 1;
|
|
40
|
+
let value = "";
|
|
41
|
+
if (text[at] === '"') {
|
|
42
|
+
at++;
|
|
43
|
+
let closed = false;
|
|
44
|
+
while (at < text.length) {
|
|
45
|
+
if (text[at] === '"') {
|
|
46
|
+
at++;
|
|
47
|
+
closed = true;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
if (text[at] === "\\" && ["\\", '"'].includes(text[at + 1])) at++;
|
|
51
|
+
value += text[at++];
|
|
52
|
+
}
|
|
53
|
+
if (!closed) result.problems.push(`${key} has an unclosed quoted value`);
|
|
54
|
+
if (at < text.length && !/\s/.test(text[at])) {
|
|
55
|
+
while (at < text.length && !/\s/.test(text[at])) at++;
|
|
56
|
+
result.problems.push(`${key} has text after its quoted value`);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
while (at < text.length && !/\s/.test(text[at])) value += text[at++];
|
|
60
|
+
if (/[{},"\[\]]/.test(value)) result.problems.push(`${key} needs a quoted value`);
|
|
61
|
+
}
|
|
62
|
+
if (!value) result.problems.push(`${key} needs a value`);
|
|
63
|
+
if (Object.hasOwn(result.values, key)) result.problems.push(`${key} is written twice`);
|
|
64
|
+
else result.values[key] = value;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A Boolean attribute is spelled exactly `true` or `false`. */
|
|
70
|
+
export function booleanAttribute(value, key) {
|
|
71
|
+
if (value === "true") return true;
|
|
72
|
+
if (value === "false") return false;
|
|
73
|
+
throw new TypeError(`${key} needs the literal value true or false`);
|
|
74
|
+
}
|
|
@@ -248,7 +248,7 @@ function workedExample(type, fields) {
|
|
|
248
248
|
export function renderItemFieldReference({
|
|
249
249
|
title = "Item Note Frontmatter",
|
|
250
250
|
preamble = [],
|
|
251
|
-
generatedBy = "the
|
|
251
|
+
generatedBy = "the package-build field reference generator",
|
|
252
252
|
config = loadPackConfig(),
|
|
253
253
|
} = {}) {
|
|
254
254
|
const declared = config.itemFields ?? {};
|
package/engine/field-spec.mjs
CHANGED
|
@@ -161,6 +161,9 @@ export { legacyKeyOf, retiredTopLevelKey, setPath };
|
|
|
161
161
|
* which is a Shortcode at both ends and resolves nothing.
|
|
162
162
|
* @property {any|((fm: object) => any)} [value] - For a field with no `name`:
|
|
163
163
|
* the constant, or a function deriving it from the frontmatter.
|
|
164
|
+
* @property {(fm: object, out: object) => boolean} [when] - Emit this field
|
|
165
|
+
* only when the note and fields already built meet the condition. The path
|
|
166
|
+
* remains declared for schema checks and authoring validation.
|
|
164
167
|
* @property {boolean} [omitWhenAbsent] - **The key is left out entirely when
|
|
165
168
|
* the note does not carry the field**, rather than written from a
|
|
166
169
|
* declared default.
|
|
@@ -460,6 +463,7 @@ export function buildFromFields(fields, { block = "sohl", onLegacyKey, onRetired
|
|
|
460
463
|
return function buildDeclaredSystem(fm) {
|
|
461
464
|
const out = {};
|
|
462
465
|
for (const field of fields) {
|
|
466
|
+
if (field.when && !field.when(fm, out)) continue;
|
|
463
467
|
// A runtime-only field is not this builder's to write. It is
|
|
464
468
|
// declared so that the path is *claimed* — so the verbatim
|
|
465
469
|
// passthrough leaves it alone and the refusal has a name — not so
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* item came from.
|
|
40
40
|
*
|
|
41
41
|
* **The network is never touched by a compile.** Fetching is its own command
|
|
42
|
-
* (`
|
|
42
|
+
* (`package-build deps fetch`), and a compile whose cache is cold fails saying
|
|
43
43
|
* so. A build that silently downloads is not reproducible, fails strangely
|
|
44
44
|
* offline, and hides a version change behind a passing run.
|
|
45
45
|
*
|
|
@@ -223,7 +223,7 @@ function cachedItemPacks(dir) {
|
|
|
223
223
|
* ways of proceeding without it are both wrong: reading every pack resolves an
|
|
224
224
|
* `hm3` reference against `sohl` documents — the silent-wrong-output failure
|
|
225
225
|
* this scoping exists to remove — and reading none fails a build that was
|
|
226
|
-
* working. Treating it as incomplete makes `
|
|
226
|
+
* working. Treating it as incomplete makes `package-build deps fetch` refill
|
|
227
227
|
* it, which is a command the cold-cache path already tells anyone to run.
|
|
228
228
|
*
|
|
229
229
|
* @param {string} dir - The dependency's cache directory.
|
|
@@ -481,7 +481,7 @@ export async function fetchCatalogFromPath(config, rel, source) {
|
|
|
481
481
|
throw new Error(`${rel.id}: nothing at ${source}`);
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
-
const staging = fs.mkdtempSync(path.join(os.tmpdir(), `
|
|
484
|
+
const staging = fs.mkdtempSync(path.join(os.tmpdir(), `package-build-${rel.id}-`));
|
|
485
485
|
try {
|
|
486
486
|
let root;
|
|
487
487
|
if (fs.statSync(source).isDirectory()) {
|
|
@@ -608,7 +608,7 @@ export async function fetchMetadataFromPath(config, rel, source) {
|
|
|
608
608
|
throw new Error(`${rel.id}: nothing at ${source}`);
|
|
609
609
|
}
|
|
610
610
|
|
|
611
|
-
const staging = fs.mkdtempSync(path.join(os.tmpdir(), `
|
|
611
|
+
const staging = fs.mkdtempSync(path.join(os.tmpdir(), `package-build-meta-${rel.id}-`));
|
|
612
612
|
try {
|
|
613
613
|
let root = source;
|
|
614
614
|
if (!fs.statSync(source).isDirectory()) {
|
|
@@ -752,7 +752,7 @@ export function foreignItemCatalogDirs(config, system = null) {
|
|
|
752
752
|
if (!cached.length) {
|
|
753
753
|
throw new Error(
|
|
754
754
|
`${rel.id} declares \`itemCatalog: true\` but has not been ` +
|
|
755
|
-
`fetched. Run \`
|
|
755
|
+
`fetched. Run \`package-build deps fetch\` first.`,
|
|
756
756
|
);
|
|
757
757
|
}
|
|
758
758
|
// Newest wins if several versions are cached; a fetch always writes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format generated text using the destination file's Prettier configuration.
|
|
7
|
+
* @param {string} text
|
|
8
|
+
* @param {string} filepath
|
|
9
|
+
* @returns {Promise<string>}
|
|
10
|
+
*/
|
|
11
|
+
export async function formatGenerated(text, filepath) {
|
|
12
|
+
const prettier = await import("prettier");
|
|
13
|
+
const config = await prettier.resolveConfig(filepath);
|
|
14
|
+
return prettier.format(text, { ...config, filepath });
|
|
15
|
+
}
|
|
@@ -59,6 +59,7 @@ import fs from "node:fs";
|
|
|
59
59
|
import path from "node:path";
|
|
60
60
|
|
|
61
61
|
import { noteAddressContext } from "./note-addresses.mjs";
|
|
62
|
+
import { isGmNote } from "./note-vocabulary.mjs";
|
|
62
63
|
import { canonicalKey, packageAddress } from "./content-address.mjs";
|
|
63
64
|
import { ownDocumentSystem } from "./address.mjs";
|
|
64
65
|
import { NOTE_SYSTEM } from "./systems.mjs";
|
|
@@ -211,6 +212,7 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
211
212
|
routeFm ?
|
|
212
213
|
packRouter.resolveOrNull(routeFm, docType, system)
|
|
213
214
|
: packRouter.defaultOf("JournalEntry");
|
|
215
|
+
if (isGmNote(fm) && !packRouter.privateOf?.(pack)) return undefined;
|
|
214
216
|
|
|
215
217
|
// **A system block is what makes a game document**, so a note carrying
|
|
216
218
|
// none compiles into no Actor and no Item — and an address naming one
|
|
@@ -255,7 +257,7 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
255
257
|
// however many system blocks the note carries.
|
|
256
258
|
const docKey = canonicalKey(contentPackage, NOTE_SYSTEM, fm.type, fm.shortcode);
|
|
257
259
|
const docEntryId = fm.id ? itemDocEntryId(fm.id) : undefined;
|
|
258
|
-
const docUuid = uuidFor("doc", docEntryId);
|
|
260
|
+
const docUuid = uuidFor("doc", docEntryId, isGmNote(fm) ? fm : undefined);
|
|
259
261
|
return [
|
|
260
262
|
{
|
|
261
263
|
key,
|
|
@@ -1689,7 +1689,11 @@ export function lintNote(
|
|
|
1689
1689
|
`\`${field.name}\``
|
|
1690
1690
|
: `\`sohl.${field.name}\``;
|
|
1691
1691
|
|
|
1692
|
-
if (
|
|
1692
|
+
if (
|
|
1693
|
+
field.required &&
|
|
1694
|
+
absent &&
|
|
1695
|
+
(!field.when || field.when(fm, { subType: fm.subType ?? fm.sohl?.subType }))
|
|
1696
|
+
) {
|
|
1693
1697
|
findings.push({
|
|
1694
1698
|
file: note.file,
|
|
1695
1699
|
...at("type", type),
|
package/engine/frontmatter.mjs
CHANGED
|
@@ -36,6 +36,10 @@ import { AFFILIATION_STANDINGS } from "../sohl/affiliation-standings.mjs";
|
|
|
36
36
|
/**
|
|
37
37
|
* Resolves a dotted frontmatter key (e.g., "name.full") into the nested
|
|
38
38
|
* value. Returns `defaultValue` if any path segment is missing.
|
|
39
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
40
|
+
* @param {string} key - A top-level or dotted property path.
|
|
41
|
+
* @param {unknown} [defaultValue] - Value for a missing property.
|
|
42
|
+
* @returns {unknown} The property value or default.
|
|
39
43
|
*/
|
|
40
44
|
export function getFrontmatter(fm, key, defaultValue = undefined) {
|
|
41
45
|
if (fm == null || typeof fm !== "object") return defaultValue;
|
|
@@ -53,6 +57,10 @@ export function getFrontmatter(fm, key, defaultValue = undefined) {
|
|
|
53
57
|
* Reads a key from `fm.sohl` (the vault's nested system-fields block).
|
|
54
58
|
* Supports dotted notation, e.g. sohlField(fm, "charges.value", 0).
|
|
55
59
|
* Falls back to top-level `fm[key]` if `sohl` doesn't carry the key.
|
|
60
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
61
|
+
* @param {string} key - A top-level or dotted property path.
|
|
62
|
+
* @param {unknown} [defaultValue] - Value for a missing property.
|
|
63
|
+
* @returns {unknown} The system value, top-level value, or default.
|
|
56
64
|
*/
|
|
57
65
|
export function sohlField(fm, key, defaultValue = undefined) {
|
|
58
66
|
if (fm == null || typeof fm !== "object") return defaultValue;
|
|
@@ -321,6 +329,8 @@ export function requireSubType(fm, ctx) {
|
|
|
321
329
|
* - Array of "Label:MaxValue" strings, e.g. ["Ugly:4", "Plain:12"]
|
|
322
330
|
* - Array of objects, e.g. [{ label, maxValue }]
|
|
323
331
|
* Returns a normalized array of `{ label, maxValue: number }`.
|
|
332
|
+
* @param {unknown} raw - An authored threshold array.
|
|
333
|
+
* @returns {Array<{label: string, maxValue: number}>} Normalized thresholds.
|
|
324
334
|
*/
|
|
325
335
|
export function parseValueDesc(raw) {
|
|
326
336
|
if (!raw || !Array.isArray(raw)) return [];
|
package/engine/generate.mjs
CHANGED
|
@@ -430,7 +430,7 @@ export function orderPassesByDependency(packs) {
|
|
|
430
430
|
* would produce them is not in it.
|
|
431
431
|
*
|
|
432
432
|
* Ordering answers the whole-package build; a run restricted to one pack
|
|
433
|
-
* (`
|
|
433
|
+
* (`package-build package compile <name>`) cannot conjure the passes it left
|
|
434
434
|
* out. Where their output is already on disk from an earlier run that is fine
|
|
435
435
|
* — it is how compiling one pack at a time is meant to work — so this reports
|
|
436
436
|
* only what is genuinely absent, and names the pack that would write it rather
|
|
@@ -573,7 +573,7 @@ async function generatePack(
|
|
|
573
573
|
// The catalogue of a package this repository depends on but does
|
|
574
574
|
// not contain, for a repository that authors beings without
|
|
575
575
|
// holding the items they are assembled from. Cache-only: a cold
|
|
576
|
-
// cache throws naming `
|
|
576
|
+
// cache throws naming `package-build deps fetch` rather than
|
|
577
577
|
// downloading inside a compile. Scoped to this pack's system for the
|
|
578
578
|
// reason the local half is: both halves answer one lookup, so a
|
|
579
579
|
// dependency shipping two systems' items would otherwise supply the
|