@heroiclands/package-build 22.15.1 → 22.17.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 +110 -0
- package/README.md +39 -678
- package/bin/{content-build.mjs → content-commands.mjs} +197 -118
- package/bin/package-build.mjs +315 -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 +296 -236
- package/docs/authoring/assets.md +31 -0
- package/docs/authoring/dates-and-calendars.md +85 -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 +633 -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 +4362 -0
- package/docs/reference/note-types.md +532 -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-format.mjs +441 -0
- package/engine/calendar-human.mjs +133 -0
- package/engine/calendar-notes.mjs +307 -88
- 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 +746 -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/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/markdown-expressions.mjs +118 -0
- package/engine/metadata-index.mjs +7 -3
- package/engine/note-addresses.mjs +1 -0
- package/engine/note-dates.mjs +314 -64
- 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 +176 -166
- 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-format.d.mts +33 -0
- package/types/engine/calendar-human.d.mts +4 -0
- package/types/engine/calendar-notes.d.mts +5 -3
- 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 +13 -26
- 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
package/engine/code-fences.mjs
CHANGED
|
@@ -46,110 +46,36 @@
|
|
|
46
46
|
* prevent.
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
|
+
import { parseExtensionAttributes } from "./extension-attributes.mjs";
|
|
50
|
+
|
|
49
51
|
/** A fence line, capturing its indent, its marker, and its info string. */
|
|
50
52
|
export const FENCE_LINE = /^([ \t]*)(`{3,}|~{3,})[ \t]*([^\r\n]*)$/;
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* section takes, later a caption. Those started as an ad-hoc bare word
|
|
58
|
-
* (`allow-empty`) and a `key=value` (`section-level=3`), each matched by its own
|
|
59
|
-
* regex: a grammar only in the sense that two regexes are one, and with no room
|
|
60
|
-
* to add a third property without adding a third spelling.
|
|
61
|
-
*
|
|
62
|
-
* Org-mode settled this long ago. A babel source block writes them after the
|
|
63
|
-
* language as `:key value`, which is a real grammar with a specification, a
|
|
64
|
-
* parser, and an editor that already completes it.
|
|
65
|
-
*
|
|
66
|
-
* ```text
|
|
67
|
-
* ```sql :section-level 3 :allow-empty
|
|
68
|
-
* ```
|
|
69
|
-
*
|
|
70
|
-
* **The language word stays first and stays plain.** `sql` is what GitHub,
|
|
71
|
-
* Prettier and every other markdown reader match on to syntax-highlight the
|
|
72
|
-
* block, so it leads and the header args follow; a reader that does not know
|
|
73
|
-
* them sees an ordinary SQL block.
|
|
74
|
-
*
|
|
75
|
-
* The grammar, matching org's:
|
|
76
|
-
*
|
|
77
|
-
* - a key is `:name` **starting a word** — a colon inside or ending a word is
|
|
78
|
-
* text, so `:caption Gear: the tables` is one argument;
|
|
79
|
-
* - a value runs to the next key or the end of the string, spaces included, and
|
|
80
|
-
* is trimmed;
|
|
81
|
-
* - a key with no value is `true`, which is what a statement like
|
|
82
|
-
* `:allow-empty` is;
|
|
83
|
-
* - a value may be `"quoted"` to hold a word that would otherwise read as a
|
|
84
|
-
* key — the one ambiguity org has too;
|
|
85
|
-
* - a repeated key takes its last value.
|
|
86
|
-
*
|
|
87
|
-
* @param {string} info - The text after the fence marker.
|
|
88
|
-
* @returns {{language: string, args: Record<string, string|true>}} The language,
|
|
89
|
-
* lowercased, and the header arguments in written order.
|
|
55
|
+
* Read a language name and optional braced attributes from a code fence.
|
|
56
|
+
* @param {string} info - Text after the fence marker.
|
|
57
|
+
* @returns {{language: string, args: Record<string, string>, problems: string[]}}
|
|
58
|
+
* Parsed values and syntax findings.
|
|
90
59
|
*/
|
|
91
60
|
export function parseHeaderArgs(info) {
|
|
92
61
|
const text = String(info ?? "").trim();
|
|
93
|
-
if (!text) return { language: "", args: {} };
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// A key-like word inside a quoted value is part of the value — that is
|
|
111
|
-
// the whole reason quoting exists here.
|
|
112
|
-
if (quoted.some(([from, to]) => m.index >= from && m.index < to)) continue;
|
|
113
|
-
keys.push({ name: m[1], at: m.index, end: re.lastIndex });
|
|
114
|
-
}
|
|
115
|
-
for (let i = 0; i < keys.length; i += 1) {
|
|
116
|
-
const from = keys[i].end;
|
|
117
|
-
const to = i + 1 < keys.length ? keys[i + 1].at : rest.length;
|
|
118
|
-
const raw = rest.slice(from, to).trim();
|
|
119
|
-
args[keys[i].name] = raw === "" ? true : unquote(raw);
|
|
120
|
-
}
|
|
121
|
-
return { language, args };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* The `"…"` and `'…'` spans in a header-argument string, as [start, end) offsets.
|
|
126
|
-
*
|
|
127
|
-
* Scanned once before the keys are, so a key-like word inside a value is never
|
|
128
|
-
* read as the next key.
|
|
129
|
-
*
|
|
130
|
-
* @param {string} text - The header-argument text.
|
|
131
|
-
* @returns {Array<[number, number]>} The quoted spans.
|
|
132
|
-
*/
|
|
133
|
-
function quotedSpans(text) {
|
|
134
|
-
const spans = [];
|
|
135
|
-
const re = /"[^"]*"|'[^']*'/g;
|
|
136
|
-
let m;
|
|
137
|
-
while ((m = re.exec(text)) !== null) spans.push([m.index, m.index + m[0].length]);
|
|
138
|
-
return spans;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* A header-argument value with its surrounding quotes removed.
|
|
143
|
-
*
|
|
144
|
-
* Quoting is how a value holds a word that would otherwise read as the next key
|
|
145
|
-
* — the same escape hatch org offers, and the only one needed.
|
|
146
|
-
*
|
|
147
|
-
* @param {string} value - The raw value.
|
|
148
|
-
* @returns {string} The value, unquoted.
|
|
149
|
-
*/
|
|
150
|
-
function unquote(value) {
|
|
151
|
-
const m = /^"([^"]*)"$|^'([^']*)'$/.exec(value);
|
|
152
|
-
return m ? (m[1] ?? m[2]) : value;
|
|
62
|
+
if (!text) return { language: "", args: {}, problems: [] };
|
|
63
|
+
const match = /^([^\s{}]+)(?:\s+(.*))?$/.exec(text);
|
|
64
|
+
const language = match?.[1]?.toLowerCase() ?? "";
|
|
65
|
+
const rest = match?.[2]?.trim() ?? "";
|
|
66
|
+
if (!rest) return { language, args: {}, problems: [] };
|
|
67
|
+
if (!/^\{[\s\S]*\}$/.test(rest))
|
|
68
|
+
return { language, args: {}, problems: ["fence attributes need {key=value} syntax"] };
|
|
69
|
+
const parsed = parseExtensionAttributes(rest.slice(1, -1));
|
|
70
|
+
return {
|
|
71
|
+
language,
|
|
72
|
+
args: parsed.values,
|
|
73
|
+
problems: [
|
|
74
|
+
...parsed.problems,
|
|
75
|
+
...(parsed.id ? ["a code fence does not accept an id"] : []),
|
|
76
|
+
...(parsed.classes.length ? ["a code fence does not accept classes"] : []),
|
|
77
|
+
],
|
|
78
|
+
};
|
|
153
79
|
}
|
|
154
80
|
|
|
155
81
|
/** A list item's opening line, capturing the indent and the marker itself. */
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
import { indexRecordsFor } from "./content-index.mjs";
|
|
42
|
-
import { buildContentLinkIndex
|
|
42
|
+
import { buildContentLinkIndex } from "./helpers.mjs";
|
|
43
43
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
44
44
|
import { prepareTreeSqlTables } from "./sql-tables.mjs";
|
|
45
45
|
|
|
@@ -60,8 +60,7 @@ import { prepareTreeSqlTables } from "./sql-tables.mjs";
|
|
|
60
60
|
* created when none is passed, and returned on the corpus either way — a note
|
|
61
61
|
* the index refuses is a note the compile must still *report*, exactly as the
|
|
62
62
|
* compile loop reported it when the loop was the first to see it.
|
|
63
|
-
* @returns {Promise<{records: object[], linkIndex: object,
|
|
64
|
-
* sqlTables: Map<string, object[]>|undefined, problems: object[]}>} The corpus,
|
|
63
|
+
* @returns {Promise<{records: object[], linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[]}>} The corpus,
|
|
65
64
|
* its indexes, and the notes it could not record.
|
|
66
65
|
*/
|
|
67
66
|
export async function buildCompileCorpus({
|
|
@@ -81,12 +80,12 @@ export async function buildCompileCorpus({
|
|
|
81
80
|
skipDirectories,
|
|
82
81
|
problems: collected,
|
|
83
82
|
});
|
|
84
|
-
const
|
|
83
|
+
const corpusOptions = { config: resolved, records };
|
|
84
|
+
const sqlOptions = { ...corpusOptions, skipDirectories };
|
|
85
85
|
return {
|
|
86
86
|
records,
|
|
87
87
|
problems: collected,
|
|
88
|
-
linkIndex: buildContentLinkIndex(contentBase, router,
|
|
89
|
-
|
|
90
|
-
sqlTables: await prepareTreeSqlTables(contentBase, scope),
|
|
88
|
+
linkIndex: buildContentLinkIndex(contentBase, router, corpusOptions),
|
|
89
|
+
sqlTables: await prepareTreeSqlTables(contentBase, sqlOptions),
|
|
91
90
|
};
|
|
92
91
|
}
|
|
@@ -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 }) {
|