@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,114 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
2
|
+
|
|
3
|
+
import MarkdownIt from "markdown-it";
|
|
4
|
+
import footnotePlugin from "markdown-it-footnote";
|
|
5
|
+
import deflistPlugin from "markdown-it-deflist";
|
|
6
|
+
import { parseExtensionAttributes } from "./extension-attributes.mjs";
|
|
7
|
+
|
|
8
|
+
const parser = new MarkdownIt({ html: true }).use(footnotePlugin).use(deflistPlugin);
|
|
9
|
+
const OPEN = /^:::(info|warn)(?:\s+(.*))?\s*$/;
|
|
10
|
+
const CLOSE = /^:::\s*$/;
|
|
11
|
+
const STYLES = Object.freeze({
|
|
12
|
+
info: { label: "Info", icon: "ℹ", color: "#2f6f9f", background: "#eef6fb" },
|
|
13
|
+
warn: { label: "Warning", icon: "⚠", color: "#9a6700", background: "#fff5db" },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/** Find inline info and warning blocks without interpreting code examples. */
|
|
17
|
+
export function scanAdmonitions(source) {
|
|
18
|
+
const lines = String(source ?? "").split("\n");
|
|
19
|
+
const blocks = [];
|
|
20
|
+
const errors = [];
|
|
21
|
+
let opening = null;
|
|
22
|
+
let codeFence = null;
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const line = lines[i];
|
|
25
|
+
const fence = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
26
|
+
if (codeFence) {
|
|
27
|
+
if (fence && fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length)
|
|
28
|
+
codeFence = null;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (fence) {
|
|
32
|
+
codeFence = fence[1];
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const open = OPEN.exec(line);
|
|
36
|
+
if (open) {
|
|
37
|
+
if (opening) {
|
|
38
|
+
errors.push({
|
|
39
|
+
line: i + 1,
|
|
40
|
+
column: 1,
|
|
41
|
+
message: "nested info and warning blocks are not supported",
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const raw = open[2]?.trim() ?? "";
|
|
46
|
+
let id = "";
|
|
47
|
+
if (raw) {
|
|
48
|
+
if (!/^\{[^}\n]*\}$/.test(raw))
|
|
49
|
+
errors.push({
|
|
50
|
+
line: i + 1,
|
|
51
|
+
column: 1,
|
|
52
|
+
message: "block attributes need {#id} syntax",
|
|
53
|
+
});
|
|
54
|
+
else {
|
|
55
|
+
const attrs = parseExtensionAttributes(raw.slice(1, -1));
|
|
56
|
+
id = attrs.id;
|
|
57
|
+
for (const message of attrs.problems)
|
|
58
|
+
errors.push({ line: i + 1, column: 1, message });
|
|
59
|
+
if (attrs.classes.length || Object.keys(attrs.values).length)
|
|
60
|
+
errors.push({
|
|
61
|
+
line: i + 1,
|
|
62
|
+
column: 1,
|
|
63
|
+
message: "info and warn accept only an id",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
opening = { start: i, kind: open[1], id };
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (CLOSE.test(line) && opening) {
|
|
71
|
+
const body = lines
|
|
72
|
+
.slice(opening.start + 1, i)
|
|
73
|
+
.join("\n")
|
|
74
|
+
.trim();
|
|
75
|
+
if (!body)
|
|
76
|
+
errors.push({
|
|
77
|
+
line: opening.start + 1,
|
|
78
|
+
column: 1,
|
|
79
|
+
message: `${opening.kind} block is empty`,
|
|
80
|
+
});
|
|
81
|
+
blocks.push({ ...opening, end: i, body });
|
|
82
|
+
opening = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (opening)
|
|
86
|
+
errors.push({
|
|
87
|
+
line: opening.start + 1,
|
|
88
|
+
column: 1,
|
|
89
|
+
message: `${opening.kind} block needs a closing ::: line`,
|
|
90
|
+
});
|
|
91
|
+
return { blocks, errors };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Render info and warning blocks as styled, labelled HTML. */
|
|
95
|
+
export function renderAdmonitions(source, renderMarkdown = parser.render.bind(parser)) {
|
|
96
|
+
const { blocks, errors } = scanAdmonitions(source);
|
|
97
|
+
if (errors.length) return { markdown: source, errors };
|
|
98
|
+
const lines = String(source ?? "").split("\n");
|
|
99
|
+
const output = [];
|
|
100
|
+
let cursor = 0;
|
|
101
|
+
for (const block of blocks) {
|
|
102
|
+
const style = STYLES[block.kind];
|
|
103
|
+
output.push(...lines.slice(cursor, block.start));
|
|
104
|
+
output.push(
|
|
105
|
+
`<aside${block.id ? ` id="${block.id}"` : ""} class="sohl-admonition sohl-admonition-${block.kind}" style="border-left: 3px solid ${style.color}; background: ${style.background}; padding: 0.5em 0.8em; margin: 0.75em 0;">`,
|
|
106
|
+
`<p><strong><span aria-hidden="true">${style.icon}</span> ${style.label}</strong></p>`,
|
|
107
|
+
renderMarkdown(block.body).trim(),
|
|
108
|
+
"</aside>",
|
|
109
|
+
);
|
|
110
|
+
cursor = block.end + 1;
|
|
111
|
+
}
|
|
112
|
+
output.push(...lines.slice(cursor));
|
|
113
|
+
return { markdown: output.join("\n"), errors: [] };
|
|
114
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import MarkdownIt from "markdown-it";
|
|
6
|
+
import footnotePlugin from "markdown-it-footnote";
|
|
7
|
+
import deflistPlugin from "markdown-it-deflist";
|
|
8
|
+
|
|
9
|
+
import { slugify } from "./content-slug.mjs";
|
|
10
|
+
import { parseExtensionAttributes } from "./extension-attributes.mjs";
|
|
11
|
+
|
|
12
|
+
const parser = new MarkdownIt({ html: true }).use(footnotePlugin).use(deflistPlugin);
|
|
13
|
+
const OPEN = /^:::caption\s+(\{[^}\n]*\})\s*$/;
|
|
14
|
+
const CAPTION_LINE = /^:::caption\b/;
|
|
15
|
+
const CLOSE = /^:::\s*$/;
|
|
16
|
+
|
|
17
|
+
/** A caption's automatically determined kind. */
|
|
18
|
+
export const CAPTION_NAMES = Object.freeze({
|
|
19
|
+
code: "Code",
|
|
20
|
+
table: "Table",
|
|
21
|
+
figure: "Figure",
|
|
22
|
+
prose: "Prose",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** Number a sequence of captions with counters shared across documents. */
|
|
26
|
+
export function numberCaptions(captions, counts) {
|
|
27
|
+
return captions.map((caption) => {
|
|
28
|
+
const number = (counts[caption.kind] ?? 0) + 1;
|
|
29
|
+
counts[caption.kind] = number;
|
|
30
|
+
return { ...caption, number, label: `${CAPTION_NAMES[caption.kind]} ${number}` };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read caption directives and the Markdown block immediately after each one.
|
|
36
|
+
* SQL fences are expanded before this pass, so their output is a table.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} source - A note body with expanded tables.
|
|
39
|
+
* @returns {{captions: Array<{id: string, text: string, kind: string, number: number,
|
|
40
|
+
* label: string, line: number, blockStart: number, blockEnd: number,
|
|
41
|
+
* directiveEnd: number}>, errors: Array<{line: number, column: number, message: string}>}}
|
|
42
|
+
*/
|
|
43
|
+
export function scanCaptions(source) {
|
|
44
|
+
const lines = String(source ?? "").split("\n");
|
|
45
|
+
const captions = [];
|
|
46
|
+
const errors = [];
|
|
47
|
+
const counts = { code: 0, table: 0, figure: 0, prose: 0 };
|
|
48
|
+
const ids = new Set();
|
|
49
|
+
const headingIds = [];
|
|
50
|
+
let codeFence = null;
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
const line = lines[i];
|
|
54
|
+
const fence = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
55
|
+
if (codeFence) {
|
|
56
|
+
if (fence && fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length)
|
|
57
|
+
codeFence = null;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (fence) {
|
|
61
|
+
codeFence = fence[1];
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const heading = /^\s*#{1,6}\s+.+?\{#([^}]+)\}\s*$/.exec(line);
|
|
65
|
+
if (heading) headingIds.push({ id: slugify(heading[1].trim()), line: i + 1 });
|
|
66
|
+
if (!CAPTION_LINE.test(line)) continue;
|
|
67
|
+
const open = OPEN.exec(line);
|
|
68
|
+
if (!open) {
|
|
69
|
+
errors.push({ line: i + 1, column: 1, message: "caption needs {#anchor} attributes" });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const attributes = parseExtensionAttributes(open[1].slice(1, -1));
|
|
73
|
+
if (
|
|
74
|
+
attributes.problems.length ||
|
|
75
|
+
!attributes.id ||
|
|
76
|
+
attributes.classes.length ||
|
|
77
|
+
Object.keys(attributes.values).length
|
|
78
|
+
) {
|
|
79
|
+
errors.push({
|
|
80
|
+
line: i + 1,
|
|
81
|
+
column: 1,
|
|
82
|
+
message: attributes.problems[0] ?? "caption accepts only {#anchor}",
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const id = attributes.id;
|
|
87
|
+
const start = i;
|
|
88
|
+
let close = i + 1;
|
|
89
|
+
while (close < lines.length && !CLOSE.test(lines[close])) close++;
|
|
90
|
+
if (close === lines.length) {
|
|
91
|
+
errors.push({ line: start + 1, column: 1, message: "caption needs a closing :::" });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const caption = lines
|
|
95
|
+
.slice(i + 1, close)
|
|
96
|
+
.join("\n")
|
|
97
|
+
.trim();
|
|
98
|
+
if (!caption) errors.push({ line: start + 1, column: 1, message: "caption text is empty" });
|
|
99
|
+
if (ids.has(slugify(id)))
|
|
100
|
+
errors.push({ line: start + 1, column: 1, message: `duplicate caption id "${id}"` });
|
|
101
|
+
ids.add(slugify(id));
|
|
102
|
+
|
|
103
|
+
let next = close + 1;
|
|
104
|
+
while (next < lines.length && !lines[next].trim()) next++;
|
|
105
|
+
if (next === lines.length || CAPTION_LINE.test(lines[next])) {
|
|
106
|
+
errors.push({ line: start + 1, column: 1, message: "caption needs a following block" });
|
|
107
|
+
i = close;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const tokens = parser.parse(lines.slice(next).join("\n"), {});
|
|
111
|
+
const first = tokens.find((token) => token.level === 0 && token.map);
|
|
112
|
+
if (!first) {
|
|
113
|
+
errors.push({ line: start + 1, column: 1, message: "caption needs a following block" });
|
|
114
|
+
i = close;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const blockEnd = next + first.map[1];
|
|
118
|
+
const block = lines.slice(next, blockEnd).join("\n");
|
|
119
|
+
const kind =
|
|
120
|
+
first.type === "table_open" ? "table"
|
|
121
|
+
: first.type === "fence" && /^\s*sql\b/i.test(first.info ?? "") ? "table"
|
|
122
|
+
: first.type === "fence" || first.type === "code_block" ? "code"
|
|
123
|
+
: /^(?:!\[|!\[\[|<figure\b|<img\b)/.test(block.trim()) ? "figure"
|
|
124
|
+
: "prose";
|
|
125
|
+
const number = ++counts[kind];
|
|
126
|
+
captions.push({
|
|
127
|
+
id,
|
|
128
|
+
text: caption,
|
|
129
|
+
kind,
|
|
130
|
+
number,
|
|
131
|
+
label: `${CAPTION_NAMES[kind]} ${number}`,
|
|
132
|
+
line: start + 1,
|
|
133
|
+
blockStart: next,
|
|
134
|
+
blockEnd,
|
|
135
|
+
directiveEnd: close + 1,
|
|
136
|
+
});
|
|
137
|
+
i = close;
|
|
138
|
+
}
|
|
139
|
+
for (const heading of headingIds) {
|
|
140
|
+
if (ids.has(heading.id))
|
|
141
|
+
errors.push({
|
|
142
|
+
line: heading.line,
|
|
143
|
+
column: 1,
|
|
144
|
+
message: `heading and caption declare the same anchor "${heading.id}"`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return { captions, errors };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Render captioned blocks as HTML, leaving other Markdown untouched. */
|
|
151
|
+
export function renderCaptionBlocks(source, renderMarkdown = parser.render.bind(parser), numbers) {
|
|
152
|
+
const { captions, errors } = scanCaptions(source);
|
|
153
|
+
if (errors.length) return { markdown: source, errors };
|
|
154
|
+
const byId = numbers ? new Map(numbers.map((entry) => [entry.id, entry])) : new Map();
|
|
155
|
+
const lines = String(source ?? "").split("\n");
|
|
156
|
+
const output = [];
|
|
157
|
+
let cursor = 0;
|
|
158
|
+
const escape = (value) =>
|
|
159
|
+
String(value)
|
|
160
|
+
.replace(/&/g, "&")
|
|
161
|
+
.replace(/</g, "<")
|
|
162
|
+
.replace(/>/g, ">")
|
|
163
|
+
.replace(/"/g, """);
|
|
164
|
+
for (const caption of captions) {
|
|
165
|
+
output.push(...lines.slice(cursor, caption.line - 1));
|
|
166
|
+
const numbered = byId.get(caption.id) ?? caption;
|
|
167
|
+
let html = renderMarkdown(lines.slice(caption.blockStart, caption.blockEnd).join("\n"));
|
|
168
|
+
if (caption.kind === "figure")
|
|
169
|
+
html = html.replace(/<figcaption\b[^>]*>[\s\S]*?<\/figcaption>/i, "");
|
|
170
|
+
const captionHtml = parser.renderInline(caption.text);
|
|
171
|
+
output.push(
|
|
172
|
+
`<div id="${escape(slugify(caption.id))}" class="content-caption content-caption-${caption.kind}">`,
|
|
173
|
+
html.trim(),
|
|
174
|
+
`<p class="content-caption-label">${escape(numbered.label)}: ${captionHtml}</p>`,
|
|
175
|
+
"</div>",
|
|
176
|
+
);
|
|
177
|
+
cursor = caption.blockEnd;
|
|
178
|
+
}
|
|
179
|
+
output.push(...lines.slice(cursor));
|
|
180
|
+
return { markdown: output.join("\n"), errors: [] };
|
|
181
|
+
}
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
*
|
|
24
24
|
* ## An embed resolves to an ordinary image, and that is the whole design
|
|
25
25
|
*
|
|
26
|
-
* `![[thorn|Thorn]]{float
|
|
27
|
-
* `{float
|
|
26
|
+
* `![[thorn|Thorn]]{float=top-left}` becomes
|
|
27
|
+
* `{float=top-left}` in the
|
|
28
28
|
* source text, before any surface renders it. Everything downstream is machinery
|
|
29
29
|
* that already exists: {@link module:engine/content-images.imagePlugin} draws the
|
|
30
30
|
* figure for a Foundry journal and for the book's Typst walk,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Separate top-level footnote definitions from prose while retaining line numbers.
|
|
5
|
+
* Definitions belong to the note and can be used by any rendered page.
|
|
6
|
+
* @param {string} source - Authored Markdown.
|
|
7
|
+
* @returns {{markdown: string, definitions: string}} Body with blanked definitions and their source.
|
|
8
|
+
*/
|
|
9
|
+
export function separateFootnotes(source) {
|
|
10
|
+
const lines = String(source ?? "").split("\n");
|
|
11
|
+
const body = [...lines];
|
|
12
|
+
const definitions = [];
|
|
13
|
+
let fence = null;
|
|
14
|
+
for (let i = 0; i < lines.length; i++) {
|
|
15
|
+
const marker = /^ {0,3}(`{3,}|~{3,})/.exec(lines[i]);
|
|
16
|
+
if (fence) {
|
|
17
|
+
if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length)
|
|
18
|
+
fence = null;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (marker) {
|
|
22
|
+
fence = marker[1];
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!/^\[\^[^\]\s]+\]:/.test(lines[i])) continue;
|
|
26
|
+
const start = i;
|
|
27
|
+
i++;
|
|
28
|
+
while (i < lines.length && (!lines[i].trim() || /^(?: {4}|\t)/.test(lines[i]))) i++;
|
|
29
|
+
definitions.push(lines.slice(start, i).join("\n").trimEnd());
|
|
30
|
+
for (let at = start; at < i; at++) body[at] = "";
|
|
31
|
+
i--;
|
|
32
|
+
}
|
|
33
|
+
return { markdown: body.join("\n"), definitions: definitions.join("\n\n") };
|
|
34
|
+
}
|
|
@@ -141,7 +141,7 @@ export function declaredPaths(artifact) {
|
|
|
141
141
|
* mappings" instead of "on a `weapongear`". Reading `on a \`the shared
|
|
142
142
|
* mappings\`` would be the alternative, and a diagnostic is prose.
|
|
143
143
|
*
|
|
144
|
-
* @param {
|
|
144
|
+
* @param {{system: string, systemVersion: string, noteType: string, source: string, target: string, shared: boolean}} finding - The unresolved mapping.
|
|
145
145
|
* @returns {string} The message.
|
|
146
146
|
*/
|
|
147
147
|
export function undeclaredTargetMessage({
|
|
@@ -468,7 +468,7 @@ function under(path, prefix) {
|
|
|
468
468
|
/**
|
|
469
469
|
* What an author is told when the specification and the declaration disagree.
|
|
470
470
|
*
|
|
471
|
-
* @param {
|
|
471
|
+
* @param {{noteType: string, source: string, target: string, name: string, to: string}} finding - The differing mapping.
|
|
472
472
|
* @returns {string} The message.
|
|
473
473
|
*/
|
|
474
474
|
export function fieldDriftMessage({ noteType, source, target, name, to }) {
|
|
@@ -12,68 +12,12 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Reads the structured note-format contract shipped with this package.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* were checked by throwaway scripts while it was being drafted:
|
|
22
|
-
*
|
|
23
|
-
* - **every `system.*` target it names** must exist in the naming system's
|
|
24
|
-
* published `schema.json`, or the specification and the system disagree; and
|
|
25
|
-
* - **every authored note** should carry only the keys the format declares for
|
|
26
|
-
* its type, which during the migration is a progress bar as much as a
|
|
27
|
-
* check.
|
|
28
|
-
*
|
|
29
|
-
* Both need the document as data, and this is the module that supplies it.
|
|
30
|
-
*
|
|
31
|
-
* **It reads the document's own tables rather than a transcription of them.**
|
|
32
|
-
* A hardcoded list of targets and per-type vocabularies would be a second copy
|
|
33
|
-
* of the specification, free to drift from the first the moment either is
|
|
34
|
-
* edited — which is exactly the failure the checks exist to prevent, moved one
|
|
35
|
-
* level up. So the parser knows the *shape* of the tables the document uses and
|
|
36
|
-
* nothing about their contents: no type name, no field name and no system name
|
|
37
|
-
* is written here. Editing the document changes what the checks assert.
|
|
38
|
-
*
|
|
39
|
-
* **Three table shapes carry everything.**
|
|
40
|
-
*
|
|
41
|
-
* | table | recognised by its first header cell | yields |
|
|
42
|
-
* | --- | --- | --- |
|
|
43
|
-
* | the per-type vocabulary | `` `data` property `` | the keys that type's `data:` block may carry |
|
|
44
|
-
* | the per-type mapping | `shared source` | one claim per `system.*` cell |
|
|
45
|
-
* | a closed vocabulary | `` `<name>` value `` | the values `<name>` admits |
|
|
46
|
-
*
|
|
47
|
-
* A mapping table's remaining header cells name the systems (`→ sohl`,
|
|
48
|
-
* `→ hm3`), so the system vocabulary comes from the document too.
|
|
49
|
-
*
|
|
50
|
-
* **A closed vocabulary is a table because it is a list with consequences.**
|
|
51
|
-
* Where the format admits a fixed set of names and refuses the rest, each name
|
|
52
|
-
* takes a row beside what it means, which is the only place a reader can
|
|
53
|
-
* compare them. The header names the vocabulary rather than the parser, so a
|
|
54
|
-
* second one costs a table and nothing here.
|
|
55
|
-
*
|
|
56
|
-
* **The other half of a type's vocabulary is a bullet list, not a table.** A
|
|
57
|
-
* type's `subType` values are stated as `**subType**:` followed by one bullet
|
|
58
|
-
* per value, `- <value>` or `- <value>: <definition>`, and that is read here
|
|
59
|
-
* for the same reason the tables are: so the specification and
|
|
60
|
-
* `note-vocabulary.mjs` cannot disagree about which genres exist. The
|
|
61
|
-
* one shape is enforced rather than guessed at — the document wrote them five
|
|
62
|
-
* ways, and a reader that accepted every spelling would accept the sixth by
|
|
63
|
-
* reading the section as declaring nothing, which is the drift it exists to
|
|
64
|
-
* catch. An unrecognised shape throws.
|
|
65
|
-
*
|
|
66
|
-
* **A mapping table before the first `### type:` heading is the shared one.**
|
|
67
|
-
* The document states the rows every type maps identically once, at the top,
|
|
68
|
-
* and omits them from all sixteen per-type tables — so a parser that only ever
|
|
69
|
-
* looked inside a type's section could not see them, and the eight rows they
|
|
70
|
-
* cover were checked by nothing. Position is the whole distinction:
|
|
71
|
-
* there is no marker to read and none is wanted, since the document's own
|
|
72
|
-
* argument for stating them once is that they belong to no type in particular. A cell that
|
|
73
|
-
* names no field — `NA`, `**see above**`, a `flags.*` path — is not a claim,
|
|
74
|
-
* and is skipped rather than reported: the check is about `system.*` targets,
|
|
75
|
-
* and a column reading NA is the document saying this type produces no document
|
|
76
|
-
* there.
|
|
17
|
+
* Types, subtype values, field paths, system mappings, and closed vocabularies
|
|
18
|
+
* have located declarations. The guides can change their headings and tables
|
|
19
|
+
* without changing validation. A caller may supply a Markdown specification
|
|
20
|
+
* explicitly through `--spec`; the bundled contract is YAML.
|
|
77
21
|
*
|
|
78
22
|
* @module
|
|
79
23
|
*/
|
|
@@ -81,35 +25,34 @@
|
|
|
81
25
|
import fs from "node:fs";
|
|
82
26
|
import path from "node:path";
|
|
83
27
|
import { fileURLToPath } from "node:url";
|
|
28
|
+
import YAML from "yaml";
|
|
84
29
|
|
|
85
30
|
/**
|
|
86
|
-
* The
|
|
31
|
+
* The structured format contract this package ships.
|
|
87
32
|
*
|
|
88
33
|
* Resolved from this module rather than from the working directory: a consumer
|
|
89
|
-
* runs `
|
|
90
|
-
*
|
|
34
|
+
* runs `package-build content-format` inside its own repository, and the
|
|
35
|
+
* contract it should be checked against is the one that came with the toolchain
|
|
91
36
|
* version it resolved — the same rule `--version` follows.
|
|
92
37
|
*
|
|
93
38
|
* @type {string}
|
|
94
39
|
*/
|
|
95
40
|
export const CONTENT_FORMAT_PATH = path.join(
|
|
96
41
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
97
|
-
"
|
|
98
|
-
"docs",
|
|
99
|
-
"content-format.md",
|
|
42
|
+
"content-format.yaml",
|
|
100
43
|
);
|
|
101
44
|
|
|
102
45
|
/**
|
|
103
46
|
* What one note type's section declares.
|
|
104
47
|
*
|
|
105
48
|
* @typedef {object} TypeSpec
|
|
106
|
-
* @property {string} name - The note type
|
|
107
|
-
* @property {number} line - 1-based line of that
|
|
49
|
+
* @property {string} name - The note type as declared in the contract.
|
|
50
|
+
* @property {number} line - 1-based line of that declaration.
|
|
108
51
|
* @property {Set<string>} dataKeys - The head segment of each declared `data`
|
|
109
52
|
* property — what a note actually writes. `appearance.eye_color` is authored
|
|
110
53
|
* as `appearance`, so that is the key recorded.
|
|
111
54
|
* @property {Set<string>} dataPaths - The declared paths, whole.
|
|
112
|
-
* @property {string[]} subTypes - The `subType` values
|
|
55
|
+
* @property {string[]} subTypes - The declared `subType` values,
|
|
113
56
|
* in document order — empty when it states none, which is the ordinary case
|
|
114
57
|
* for a type that has no `subType` at all.
|
|
115
58
|
*/
|
|
@@ -426,11 +369,122 @@ export function parseContentFormat(text, { file = CONTENT_FORMAT_PATH } = {}) {
|
|
|
426
369
|
}
|
|
427
370
|
|
|
428
371
|
/**
|
|
429
|
-
* Read and parse the
|
|
372
|
+
* Read and parse the bundled contract or a caller-supplied specification.
|
|
430
373
|
*
|
|
431
|
-
* @param {string} [file] - The
|
|
374
|
+
* @param {string} [file] - The contract. Defaults to {@link CONTENT_FORMAT_PATH}.
|
|
432
375
|
* @returns {ContentFormat} What it declares.
|
|
433
376
|
*/
|
|
434
377
|
export function loadContentFormat(file = CONTENT_FORMAT_PATH) {
|
|
435
|
-
|
|
378
|
+
const source = fs.readFileSync(file, "utf8");
|
|
379
|
+
return file.endsWith(".md") ?
|
|
380
|
+
parseContentFormat(source, { file })
|
|
381
|
+
: parseStructuredContentFormat(source, { file });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Read the machine contract without depending on the presentation of a guide.
|
|
386
|
+
* A Markdown file remains accepted by `--spec` for callers carrying a custom
|
|
387
|
+
* specification; the shipped contract is YAML.
|
|
388
|
+
*
|
|
389
|
+
* @param {string} text - YAML content.
|
|
390
|
+
* @param {object} [opts]
|
|
391
|
+
* @param {string} [opts.file] - Location reported in diagnostics.
|
|
392
|
+
* @returns {ContentFormat} Types, mappings, and closed vocabularies.
|
|
393
|
+
*/
|
|
394
|
+
export function parseStructuredContentFormat(text, { file = CONTENT_FORMAT_PATH } = {}) {
|
|
395
|
+
const lines = new YAML.LineCounter();
|
|
396
|
+
const document = YAML.parseDocument(text, { lineCounter: lines, uniqueKeys: true });
|
|
397
|
+
if (document.errors.length) {
|
|
398
|
+
const fault = document.errors[0];
|
|
399
|
+
const position = fault.pos?.[0] === undefined ? null : lines.linePos(fault.pos[0]);
|
|
400
|
+
throw new Error(
|
|
401
|
+
`${file}${position ? `:${position.line}:${position.col}` : ""}: error: ${fault.message}`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const root = document.toJS();
|
|
405
|
+
const mapping = (value) => value && typeof value === "object" && !Array.isArray(value);
|
|
406
|
+
if (
|
|
407
|
+
root?.version !== 1 ||
|
|
408
|
+
!mapping(root.types) ||
|
|
409
|
+
!Array.isArray(root.claims) ||
|
|
410
|
+
!mapping(root.vocabularies)
|
|
411
|
+
) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
`${file}: error: expected format contract version 1 with types, claims, and vocabularies`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
const at = (node) => (node?.range?.[0] === undefined ? null : lines.linePos(node.range[0]));
|
|
417
|
+
const errorAt = (node, message) => {
|
|
418
|
+
const position = at(node);
|
|
419
|
+
return new Error(
|
|
420
|
+
`${file}${position ? `:${position.line}:${position.col}` : ""}: error: ${message}`,
|
|
421
|
+
);
|
|
422
|
+
};
|
|
423
|
+
const types = new Map();
|
|
424
|
+
for (const [name, spec] of Object.entries(root.types)) {
|
|
425
|
+
const node = document.getIn(["types", name], true);
|
|
426
|
+
if (!Array.isArray(spec.data) || !Array.isArray(spec.subTypes)) {
|
|
427
|
+
throw errorAt(node, `type ${name} needs data and subTypes lists`);
|
|
428
|
+
}
|
|
429
|
+
for (const [index, field] of spec.data.entries()) {
|
|
430
|
+
if (typeof field !== "string" || !field.trim()) {
|
|
431
|
+
throw errorAt(
|
|
432
|
+
document.getIn(["types", name, "data", index], true),
|
|
433
|
+
`type ${name} has an empty or non-string data field`,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
for (const [index, subType] of spec.subTypes.entries()) {
|
|
438
|
+
if (typeof subType !== "string" || !subType.trim()) {
|
|
439
|
+
throw errorAt(
|
|
440
|
+
document.getIn(["types", name, "subTypes", index], true),
|
|
441
|
+
`type ${name} has an empty or non-string subType`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
types.set(name, {
|
|
446
|
+
name,
|
|
447
|
+
line: at(node)?.line,
|
|
448
|
+
dataKeys: new Set(spec.data.map((field) => field.split(".")[0])),
|
|
449
|
+
dataPaths: new Set(spec.data),
|
|
450
|
+
subTypes: spec.subTypes,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
const claims = root.claims.map((claim, index) => {
|
|
454
|
+
const node = document.getIn(["claims", index], true);
|
|
455
|
+
const position = at(node);
|
|
456
|
+
if (
|
|
457
|
+
!mapping(claim) ||
|
|
458
|
+
["noteType", "system", "source", "target"].some(
|
|
459
|
+
(key) => typeof claim[key] !== "string" || !claim[key].trim(),
|
|
460
|
+
)
|
|
461
|
+
) {
|
|
462
|
+
throw errorAt(node, "a mapping needs noteType, system, source, and target");
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
...claim,
|
|
466
|
+
...(position ? { line: position.line, column: position.col } : {}),
|
|
467
|
+
};
|
|
468
|
+
});
|
|
469
|
+
const vocabularies = new Map();
|
|
470
|
+
for (const [name, values] of Object.entries(root.vocabularies)) {
|
|
471
|
+
const node = document.getIn(["vocabularies", name], true);
|
|
472
|
+
if (!Array.isArray(values)) {
|
|
473
|
+
throw errorAt(node, `vocabulary ${name} needs a values list`);
|
|
474
|
+
}
|
|
475
|
+
for (const [index, value] of values.entries()) {
|
|
476
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
477
|
+
throw errorAt(
|
|
478
|
+
document.getIn(["vocabularies", name, index], true),
|
|
479
|
+
`vocabulary ${name} has an empty or non-string value`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
vocabularies.set(name, {
|
|
484
|
+
name,
|
|
485
|
+
line: at(node)?.line,
|
|
486
|
+
values,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
return { file, types, claims, vocabularies };
|
|
436
490
|
}
|