@heroiclands/package-build 22.21.0 → 22.23.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 +223 -0
- package/assets/styles/content-base.css +162 -0
- package/bin/content-commands.mjs +77 -17
- package/bin/package-build.mjs +15 -2
- package/ci/content-format-reference.mjs +68 -1
- package/config.mjs +26 -0
- package/content-config.mjs +153 -6
- package/docs/api.md +120 -89
- package/docs/authoring/assets.md +14 -0
- package/docs/authoring/dates-and-calendars.md +41 -3
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +325 -35
- package/docs/commands.md +12 -10
- package/docs/configuration.md +117 -14
- package/docs/content-format.md +4 -2
- package/docs/guides/book.md +5 -4
- package/docs/guides/reference-pages.md +15 -0
- package/docs/reference/format-details.md +545 -104
- package/docs/reference/note-types.md +30 -13
- package/engine/actor-compiler.mjs +23 -0
- package/engine/anchored-sections.mjs +35 -4
- package/engine/anchors.mjs +26 -30
- package/engine/art-fields.mjs +118 -0
- package/engine/art-slots.mjs +7 -1
- package/engine/asset-index.mjs +160 -10
- package/engine/asset-replacement.mjs +93 -0
- package/engine/asset-types.mjs +240 -0
- package/engine/base-compiler.mjs +139 -22
- package/engine/base-styles.mjs +149 -0
- package/engine/bundles.mjs +13 -2
- package/engine/compendiums.mjs +8 -1
- package/engine/content-blocks.mjs +410 -0
- package/engine/content-embeds.mjs +19 -0
- package/engine/content-figures.mjs +468 -0
- package/engine/content-footnotes.mjs +197 -0
- package/engine/content-format-check.mjs +15 -12
- package/engine/content-format.yaml +1 -1
- package/engine/content-images.mjs +126 -25
- package/engine/content-index.mjs +38 -12
- package/engine/content-links.mjs +14 -6
- package/engine/content-tables.mjs +45 -4
- package/engine/content-tasklists.mjs +189 -0
- package/engine/docs-checks.mjs +4 -4
- package/engine/extension-attributes.mjs +48 -2
- package/engine/field-reference.mjs +31 -10
- package/engine/field-spec.mjs +10 -8
- package/engine/foreign-catalog.mjs +143 -18
- package/engine/frontmatter-lint.mjs +120 -65
- package/engine/frontmatter.mjs +3 -3
- package/engine/heading-attributes.mjs +321 -0
- package/engine/helpers.mjs +109 -41
- package/engine/homepage.mjs +8 -8
- package/engine/index.mjs +3 -0
- package/engine/infobox-registry.mjs +4 -1
- package/engine/infobox-render.mjs +15 -3
- package/engine/infobox.mjs +129 -15
- package/engine/itinerary-scenes.mjs +4 -10
- package/engine/journals.mjs +294 -50
- package/engine/lore-events.mjs +197 -0
- package/engine/macros.mjs +1 -1
- package/engine/markdown-expressions.mjs +434 -59
- package/engine/note-dates.mjs +354 -12
- package/engine/note-frontmatter.mjs +7 -16
- package/engine/note-vocabulary.mjs +183 -42
- package/engine/page-lists.mjs +358 -0
- package/engine/pathnames.mjs +36 -0
- package/engine/pdf-build.mjs +431 -74
- package/engine/pdf-images.mjs +64 -0
- package/engine/pdf-render.mjs +563 -66
- package/engine/rank-ladder.mjs +198 -0
- package/engine/reckoning-markers.mjs +1 -1
- package/engine/retired-fields.mjs +8 -13
- package/engine/scenes.mjs +26 -7
- package/engine/site-build.mjs +166 -23
- package/engine/site-config.mjs +10 -1
- package/engine/site-index.mjs +90 -3
- package/engine/site-preview.mjs +20 -7
- package/engine/social-ties.mjs +52 -14
- package/engine/sql-tables.mjs +66 -9
- package/engine/standings.mjs +89 -6
- package/engine/system-block.mjs +56 -30
- package/engine/web-wikilinks.mjs +48 -1
- package/hm3/actors.mjs +8 -5
- package/manifest.mjs +43 -24
- package/package.json +1 -1
- package/packagebuild-metadata.jsonl +13 -13
- package/sohl/actors.mjs +22 -99
- package/sohl/item-fields.mjs +5 -5
- package/sohl/items.mjs +1 -1
- package/sohl/note-schemas.mjs +11 -21
- package/types/config.d.mts +9 -0
- package/types/content-config.d.mts +48 -1
- package/types/engine/actor-compiler.d.mts +11 -0
- package/types/engine/anchored-sections.d.mts +24 -0
- package/types/engine/anchors.d.mts +9 -4
- package/types/engine/art-fields.d.mts +64 -0
- package/types/engine/asset-index.d.mts +43 -0
- package/types/engine/asset-replacement.d.mts +22 -0
- package/types/engine/asset-types.d.mts +18 -0
- package/types/engine/base-compiler.d.mts +19 -17
- package/types/engine/base-styles.d.mts +93 -0
- package/types/engine/content-blocks.d.mts +87 -0
- package/types/engine/content-embeds.d.mts +16 -0
- package/types/engine/content-figures.d.mts +101 -0
- package/types/engine/content-footnotes.d.mts +80 -0
- package/types/engine/content-format-check.d.mts +8 -6
- package/types/engine/content-images.d.mts +78 -12
- package/types/engine/content-index.d.mts +8 -2
- package/types/engine/content-tables.d.mts +12 -4
- package/types/engine/content-tasklists.d.mts +72 -0
- package/types/engine/extension-attributes.d.mts +22 -1
- package/types/engine/field-reference.d.mts +21 -0
- package/types/engine/field-spec.d.mts +20 -16
- package/types/engine/foreign-catalog.d.mts +39 -0
- package/types/engine/frontmatter.d.mts +1 -1
- package/types/engine/heading-attributes.d.mts +179 -0
- package/types/engine/helpers.d.mts +29 -10
- package/types/engine/homepage.d.mts +7 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox-registry.d.mts +4 -1
- package/types/engine/infobox-render.d.mts +6 -1
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/journals.d.mts +41 -12
- package/types/engine/lore-events.d.mts +14 -0
- package/types/engine/markdown-expressions.d.mts +46 -2
- package/types/engine/note-dates.d.mts +64 -1
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-vocabulary.d.mts +82 -6
- package/types/engine/page-lists.d.mts +126 -0
- package/types/engine/pathnames.d.mts +26 -0
- package/types/engine/pdf-build.d.mts +28 -6
- package/types/engine/pdf-images.d.mts +52 -0
- package/types/engine/pdf-render.d.mts +84 -2
- package/types/engine/rank-ladder.d.mts +32 -0
- package/types/engine/retired-fields.d.mts +4 -6
- package/types/engine/site-build.d.mts +27 -4
- package/types/engine/site-config.d.mts +9 -0
- package/types/engine/site-index.d.mts +67 -1
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -7
- package/types/engine/standings.d.mts +21 -3
- package/types/engine/system-block.d.mts +37 -14
- package/types/engine/web-wikilinks.d.mts +3 -1
- package/types/hm3/actors.d.mts +21 -3
- package/types/manifest.d.mts +19 -23
- package/types/sohl/actors.d.mts +6 -7
- package/engine/content-admonitions.mjs +0 -114
- package/engine/content-captions.mjs +0 -181
- package/engine/content-secrets.mjs +0 -112
- package/types/engine/content-admonitions.d.mts +0 -24
- package/types/engine/content-captions.d.mts +0 -45
- package/types/engine/content-secrets.d.mts +0 -17
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A list item opened with `[ ]` or `[x]`, reported.
|
|
16
|
+
*
|
|
17
|
+
* **The format has no checkbox.** A list is written in plain markdown, and
|
|
18
|
+
* `[ ]` or `[x]` right after the marker is read as the start of the item's own
|
|
19
|
+
* text on every surface — stating what is done belongs in the words, not in a
|
|
20
|
+
* bracket.
|
|
21
|
+
*
|
|
22
|
+
* ## What it does not look at
|
|
23
|
+
*
|
|
24
|
+
* - **Fenced blocks and code spans.** A checklist shown as a markup example is
|
|
25
|
+
* prose *about* the two characters, not an authored list — this module's own
|
|
26
|
+
* documentation is written out of such examples. Which runs count as code is
|
|
27
|
+
* {@link module:engine/code-fences.codeRegions}' rule rather than a second
|
|
28
|
+
* copy of it.
|
|
29
|
+
* - **A bracket anywhere else in a line.** Only the marker that opens a list
|
|
30
|
+
* item is read this way; the same two characters in the middle of a sentence
|
|
31
|
+
* are prose.
|
|
32
|
+
*
|
|
33
|
+
* ## A warning, like the checks around it
|
|
34
|
+
*
|
|
35
|
+
* Nothing here is miscompiled — every surface sets the marker as the item's
|
|
36
|
+
* own text, and all three agree. The finding is for the author: the two
|
|
37
|
+
* characters read as an interactive control only on one surface, and nowhere
|
|
38
|
+
* else, so a list reaching for that meaning should say what is done in words
|
|
39
|
+
* instead.
|
|
40
|
+
*
|
|
41
|
+
* @module
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import fs from "node:fs";
|
|
45
|
+
import path from "node:path";
|
|
46
|
+
|
|
47
|
+
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
48
|
+
import { positionInBody } from "./diagnostics.mjs";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A list item's marker, immediately followed by `[ ]` or `[x]` and a space —
|
|
52
|
+
* bulleted (`-`, `*`, `+`) or ordered (`1.` or `1)`), at the start of a line.
|
|
53
|
+
*
|
|
54
|
+
* The leading indentation is a lookbehind rather than part of the match, so a
|
|
55
|
+
* nested item's finding points at the marker itself, not at the line's first
|
|
56
|
+
* column.
|
|
57
|
+
*
|
|
58
|
+
* @type {RegExp}
|
|
59
|
+
*/
|
|
60
|
+
export const TASK_LIST_MARKER = /(?<=^[ \t]*)(?:[-*+]|\d+[.)])[ \t]+\[[ xX]\][ \t]/gm;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What a note opening a list item with `[ ]` or `[x]` is told.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} marker - The matched marker, as written.
|
|
66
|
+
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
67
|
+
*/
|
|
68
|
+
export function taskListMessage(marker) {
|
|
69
|
+
return (
|
|
70
|
+
`\`${marker.trim()}\` opens a list item with no checkbox to become — ` +
|
|
71
|
+
"write a checklist as an ordinary list, stating what is done in the " +
|
|
72
|
+
"item's own words. Inside a fence or a code span it is an example, and " +
|
|
73
|
+
"not reported"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Every list item opened with `[ ]` or `[x]` in one note's body.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} body - The note's markdown, without its frontmatter.
|
|
81
|
+
* @param {string} file - The note's path, for the finding.
|
|
82
|
+
* @param {object} [opts]
|
|
83
|
+
* @param {number} [opts.bodyLine=1] - The 1-based file line the body starts on.
|
|
84
|
+
* @param {number} [opts.bodyColumn=1] - The 1-based file column it starts at.
|
|
85
|
+
* @returns {Array<{file: string, line: number, column: number,
|
|
86
|
+
* severity: "warning", message: string}>} One finding per item, in source
|
|
87
|
+
* order.
|
|
88
|
+
*/
|
|
89
|
+
export function checkTaskLists(body, file, { bodyLine = 1, bodyColumn = 1 } = {}) {
|
|
90
|
+
const text = String(body ?? "");
|
|
91
|
+
if (!text) return [];
|
|
92
|
+
|
|
93
|
+
return matchAllOutsideCode(text, TASK_LIST_MARKER).map((match) => ({
|
|
94
|
+
file,
|
|
95
|
+
...positionInBody(text, /** @type {number} */ (match.index), { bodyLine, bodyColumn }),
|
|
96
|
+
severity: /** @type {"warning"} */ ("warning"),
|
|
97
|
+
message: taskListMessage(match[0]),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Walk a content tree and report every list item opened with `[ ]` or `[x]`.
|
|
103
|
+
*
|
|
104
|
+
* The frontmatter fence is taken off first, so what is scanned is the body and
|
|
105
|
+
* the positions are still the file's. A file with no frontmatter is scanned
|
|
106
|
+
* whole: it is not a note, but a stray `.md` in the tree carrying the marker is
|
|
107
|
+
* the same problem for the same reason.
|
|
108
|
+
*
|
|
109
|
+
* A finding names its file **relative to the working directory**, which is
|
|
110
|
+
* where a reader is standing and what `formatDiagnostic` emits. The content
|
|
111
|
+
* root is where the walk starts, not what a path is measured from.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} contentBase - Root of the content tree.
|
|
114
|
+
* @param {object} [opts]
|
|
115
|
+
* @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore
|
|
116
|
+
* in addition to the dot-directories always skipped.
|
|
117
|
+
* @returns {{findings: Array<{file: string, line: number, column: number,
|
|
118
|
+
* severity: "warning", message: string}>, files: number}} The findings, and
|
|
119
|
+
* how many files were read.
|
|
120
|
+
*/
|
|
121
|
+
export function lintContentTaskLists(contentBase, { skipDirectories = [] } = {}) {
|
|
122
|
+
const skip = new Set(skipDirectories);
|
|
123
|
+
/** @type {Array<{file: string, line: number, column: number, severity: "warning", message: string}>} */
|
|
124
|
+
const findings = [];
|
|
125
|
+
let files = 0;
|
|
126
|
+
|
|
127
|
+
/** @param {string} dir - Directory to descend into. */
|
|
128
|
+
const walk = (dir) => {
|
|
129
|
+
/** @type {import("node:fs").Dirent[]} */
|
|
130
|
+
let entries;
|
|
131
|
+
try {
|
|
132
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
133
|
+
} catch {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
if (entry.name.startsWith(".") || skip.has(entry.name)) continue;
|
|
138
|
+
const full = path.join(dir, entry.name);
|
|
139
|
+
if (entry.isDirectory()) {
|
|
140
|
+
walk(full);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!/\.(md|markdown)$/i.test(entry.name)) continue;
|
|
144
|
+
let content;
|
|
145
|
+
try {
|
|
146
|
+
content = fs.readFileSync(full, "utf8");
|
|
147
|
+
} catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
files += 1;
|
|
151
|
+
findings.push(
|
|
152
|
+
...checkTaskLists(...bodyOf(content, path.relative(process.cwd(), full))),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
walk(contentBase);
|
|
158
|
+
return { findings, files };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A file's body, its path, and where the body starts in the file.
|
|
163
|
+
*
|
|
164
|
+
* The same split {@link module:engine/helpers.parseMarkdownFile} makes, without
|
|
165
|
+
* parsing the YAML: this check has no use for the frontmatter's *values*, and
|
|
166
|
+
* reading them would make an unparseable note silently unscanned.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} content - The whole file.
|
|
169
|
+
* @param {string} file - Its path, for the finding.
|
|
170
|
+
* @returns {[string, string, {bodyLine: number, bodyColumn: number}]} The
|
|
171
|
+
* arguments {@link checkTaskLists} takes.
|
|
172
|
+
*/
|
|
173
|
+
function bodyOf(content, file) {
|
|
174
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
175
|
+
if (!match) return [content, file, { bodyLine: 1, bodyColumn: 1 }];
|
|
176
|
+
|
|
177
|
+
const raw = match[2];
|
|
178
|
+
const body = raw.trim();
|
|
179
|
+
const bodyStart = content.length - raw.length + (raw.length - raw.trimStart().length);
|
|
180
|
+
const before = content.slice(0, bodyStart);
|
|
181
|
+
return [
|
|
182
|
+
body,
|
|
183
|
+
file,
|
|
184
|
+
{
|
|
185
|
+
bodyLine: before.split("\n").length,
|
|
186
|
+
bodyColumn: bodyStart - before.lastIndexOf("\n"),
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
}
|
package/engine/docs-checks.mjs
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
|
|
8
|
+
import { splitHeadingAttributes } from "./heading-attributes.mjs";
|
|
9
|
+
|
|
8
10
|
/** @param {string} root */
|
|
9
11
|
function* markdownFiles(root) {
|
|
10
12
|
for (const entry of fs
|
|
@@ -40,11 +42,9 @@ export function docAnchors(source) {
|
|
|
40
42
|
const body = maskMarkdownCode(source, false);
|
|
41
43
|
const anchors = new Set();
|
|
42
44
|
for (const [, heading] of body.matchAll(/^#{1,6}\s+(.+?)\s*#*$/gm)) {
|
|
43
|
-
const explicit = heading
|
|
45
|
+
const explicit = splitHeadingAttributes(heading);
|
|
44
46
|
anchors.add(
|
|
45
|
-
explicit
|
|
46
|
-
explicit[1].trim()
|
|
47
|
-
: docHeadingSlug(heading.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")),
|
|
47
|
+
explicit.id || docHeadingSlug(explicit.text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")),
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
for (const [, anchor] of body.matchAll(/<a\s[^>]*(?:id|name)="([^"]+)"/g)) {
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A name an id, a class or an attribute key may carry.
|
|
5
|
+
*
|
|
6
|
+
* One letter first, then letters, digits, `_` and `-`: a CSS class and an
|
|
7
|
+
* attribute key are written this way wherever they appear.
|
|
8
|
+
*/
|
|
9
|
+
const NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
10
|
+
|
|
3
11
|
/**
|
|
4
12
|
* Read the shared braced attribute grammar used by body extensions.
|
|
5
13
|
* Callers validate which names and classes their extension understands.
|
|
6
14
|
* @param {string} source - Text inside the braces.
|
|
15
|
+
* @param {object} [options={}]
|
|
16
|
+
* @param {RegExp} [options.idPattern=NAME] - What an `#id` may be named. A
|
|
17
|
+
* construct whose id is published as an HTML `id` admits a digit first and
|
|
18
|
+
* passes a looser pattern; the default is the pattern every other name takes.
|
|
7
19
|
* @returns {{id: string, classes: string[], values: Record<string, string>, problems: string[]}}
|
|
8
20
|
*/
|
|
9
|
-
export function parseExtensionAttributes(source) {
|
|
21
|
+
export function parseExtensionAttributes(source, { idPattern = NAME } = {}) {
|
|
10
22
|
const text = String(source ?? "");
|
|
11
23
|
const result = { id: "", classes: [], values: {}, problems: [] };
|
|
12
24
|
let at = 0;
|
|
@@ -18,7 +30,7 @@ export function parseExtensionAttributes(source) {
|
|
|
18
30
|
const kind = text[at++];
|
|
19
31
|
while (at < text.length && !/\s/.test(text[at])) at++;
|
|
20
32
|
const name = text.slice(start + 1, at);
|
|
21
|
-
if (
|
|
33
|
+
if (!(kind === "#" ? idPattern : NAME).test(name)) {
|
|
22
34
|
result.problems.push(
|
|
23
35
|
`invalid ${kind === "#" ? "id" : "class"} attribute ${text.slice(start, at)}`,
|
|
24
36
|
);
|
|
@@ -72,3 +84,37 @@ export function booleanAttribute(value, key) {
|
|
|
72
84
|
if (value === "false") return false;
|
|
73
85
|
throw new TypeError(`${key} needs the literal value true or false`);
|
|
74
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Attributes a construct's own markup owns, so an author sets them through
|
|
90
|
+
* `#id` and `.class` rather than through a key.
|
|
91
|
+
*/
|
|
92
|
+
const OWNED = Object.freeze(["id", "class"]);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The attribute keys no construct writes onto an element.
|
|
96
|
+
*
|
|
97
|
+
* `id=` and `class=` are refused in favour of `#id` and `.class`, which the
|
|
98
|
+
* grammar already states; a key beginning `on` is an event handler, and nothing
|
|
99
|
+
* authored in a note becomes code on a published page.
|
|
100
|
+
*
|
|
101
|
+
* Shared by every construct that writes an author's attributes onto an element,
|
|
102
|
+
* because a refusal that held on one and not the next would be the hole the
|
|
103
|
+
* other construct is checked for.
|
|
104
|
+
*
|
|
105
|
+
* @param {Record<string, string>} values - The `key=value` attributes parsed.
|
|
106
|
+
* @returns {string[]} One message per refused key, in the order written.
|
|
107
|
+
*/
|
|
108
|
+
export function refusedAttributes(values) {
|
|
109
|
+
const problems = [];
|
|
110
|
+
for (const key of Object.keys(values ?? {})) {
|
|
111
|
+
if (OWNED.includes(key.toLowerCase())) {
|
|
112
|
+
problems.push(
|
|
113
|
+
`set ${key} with ${key.toLowerCase() === "id" ? "#id" : ".class"} rather than ${key}=`,
|
|
114
|
+
);
|
|
115
|
+
} else if (/^on/i.test(key)) {
|
|
116
|
+
problems.push(`${key} is an event handler and is not written`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return problems;
|
|
120
|
+
}
|
|
@@ -123,16 +123,41 @@ function fieldTable(fields) {
|
|
|
123
123
|
return padTable(rows);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* The one line explaining why a single field's shared top-level fallback is
|
|
128
|
+
* off.
|
|
129
|
+
*
|
|
130
|
+
* **The field's reason stands on its own** — a complete sentence, or more
|
|
131
|
+
* than one — rather than completing a clause the frame starts. A field that
|
|
132
|
+
* collides with a top-level spelling meaning something else can say so
|
|
133
|
+
* ("`weight` is the item's own, independent of the note's."); a field whose
|
|
134
|
+
* top-level spelling is refused outright by the closed vocabulary can say
|
|
135
|
+
* that instead, honestly, which "there it means …" cannot: a refused key
|
|
136
|
+
* carries no meaning to report.
|
|
137
|
+
*
|
|
138
|
+
* Exported so a guard can walk every field declaring `topLevelMeans` across
|
|
139
|
+
* every registry and check that each one composes into a grammatical
|
|
140
|
+
* sentence, without reimplementing the frame it is checking.
|
|
141
|
+
*
|
|
142
|
+
* @param {import("./field-spec.mjs").FieldSpec} field - A field declaring
|
|
143
|
+
* `topLevelMeans`.
|
|
144
|
+
* @returns {string} The composed markdown line.
|
|
145
|
+
*/
|
|
146
|
+
export function topLevelExemptionLine(field) {
|
|
147
|
+
return `**\`${field.name}\` is not read from the note's top level.** ${field.topLevelMeans}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
126
150
|
/**
|
|
127
151
|
* The fields of one type that are **not** filled from the note's top level.
|
|
128
152
|
*
|
|
129
153
|
* A field ordinarily falls back to the top-level property spelled like its
|
|
130
154
|
* name, so an author who writes `weight: 3` at the top of a note reasonably
|
|
131
|
-
* expects it to reach the document. Where that spelling
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
155
|
+
* expects it to reach the document. Where that spelling is wrong at the note
|
|
156
|
+
* level — it means something else there, or the closed top-level vocabulary
|
|
157
|
+
* refuses it outright — the fallback is off, and an author has no way to tell
|
|
158
|
+
* from the table — the field is there, the value is written, and the document
|
|
159
|
+
* ships the default. So the reason each such field declares is rendered
|
|
160
|
+
* beside its table rather than left in the source.
|
|
136
161
|
*
|
|
137
162
|
* Below the table, not inside it: the reason is a sentence or two, and
|
|
138
163
|
* {@link padTable} pads every column to its widest cell, so a cell holding it
|
|
@@ -144,11 +169,7 @@ function fieldTable(fields) {
|
|
|
144
169
|
function sharedExemptions(fields) {
|
|
145
170
|
const exempt = authoredFields(fields).filter((field) => field.topLevelMeans);
|
|
146
171
|
if (!exempt.length) return [];
|
|
147
|
-
return exempt.flatMap((field) => [
|
|
148
|
-
`**\`${field.name}\` is not read from the note's top level.** There it means ` +
|
|
149
|
-
`${field.topLevelMeans}`,
|
|
150
|
-
"",
|
|
151
|
-
]);
|
|
172
|
+
return exempt.flatMap((field) => [topLevelExemptionLine(field), ""]);
|
|
152
173
|
}
|
|
153
174
|
|
|
154
175
|
/**
|
package/engine/field-spec.mjs
CHANGED
|
@@ -84,19 +84,21 @@ export { legacyKeyOf, retiredTopLevelKey, setPath };
|
|
|
84
84
|
* retiring one — so a field can move into `data:` while the corpus catches
|
|
85
85
|
* up, instead of on a flag day across four repositories. A declaration that
|
|
86
86
|
* names one is mid-sweep by construction; see {@link readsLegacyKey}.
|
|
87
|
-
* @property {string} [topLevelMeans] - **
|
|
88
|
-
* name
|
|
89
|
-
*
|
|
90
|
-
*
|
|
87
|
+
* @property {string} [topLevelMeans] - **Why the note's top-level key of this
|
|
88
|
+
* name is not this field's source** — declared wherever that key's spelling
|
|
89
|
+
* means something else at the note level, or the closed top-level
|
|
90
|
+
* vocabulary refuses it outright, and stating either removes the shared
|
|
91
|
+
* top-level position from this field's resolution order.
|
|
91
92
|
*
|
|
92
93
|
* A field's `name` doubles as its identity and as the shared property it
|
|
93
94
|
* draws from, which is right wherever the two levels state the same quantity
|
|
94
95
|
* — `data.weight` is the weight, whoever reads it. It is wrong wherever a
|
|
95
|
-
* spelling collides across the two vocabularies
|
|
96
|
+
* spelling collides across the two vocabularies, or names a key the closed
|
|
97
|
+
* top-level vocabulary does not admit at all. An `affiliation` item's
|
|
96
98
|
* `system.title` is the style of address an office carries; a note's
|
|
97
|
-
* top-level `title` is
|
|
98
|
-
*
|
|
99
|
-
*
|
|
99
|
+
* top-level `title` is refused outright. Nothing relates them, and before
|
|
100
|
+
* this key one silently fed the other, stringifying an authored `title:
|
|
101
|
+
* null` into fifteen documents.
|
|
100
102
|
*
|
|
101
103
|
* **The value is the reason**, not a flag with a comment beside it. A boolean
|
|
102
104
|
* would record the decision and lose the case for it, and the next person
|
|
@@ -38,6 +38,12 @@
|
|
|
38
38
|
* local pack's output — the resolution logic needs no knowledge of where an
|
|
39
39
|
* item came from.
|
|
40
40
|
*
|
|
41
|
+
* `assetArchive: true` is the sibling flag, for a dependency whose bytes are
|
|
42
|
+
* wanted and whose items are not: an asset-replacement source such as
|
|
43
|
+
* `thalornaaltart` ships no Item packs at all. It unpacks the same release
|
|
44
|
+
* archive without extracting anything from it, so a relationship declaring
|
|
45
|
+
* both flags on one entry is fetched once rather than twice.
|
|
46
|
+
*
|
|
41
47
|
* **The network is never touched by a compile.** Fetching is its own command
|
|
42
48
|
* (`package-build deps fetch`), and a compile whose cache is cold fails saying
|
|
43
49
|
* so. A build that silently downloads is not reproducible, fails strangely
|
|
@@ -73,17 +79,22 @@ import {
|
|
|
73
79
|
const STAMP = ".complete";
|
|
74
80
|
|
|
75
81
|
/**
|
|
76
|
-
* Every declared relationship
|
|
82
|
+
* Every declared relationship carrying a given flag, in declaration order.
|
|
83
|
+
*
|
|
84
|
+
* Shared by {@link itemCatalogRelationships} and
|
|
85
|
+
* {@link assetArchiveRelationships}: both read the same shape out of the same
|
|
86
|
+
* walk, filtered on a different key.
|
|
77
87
|
*
|
|
78
88
|
* @param {object} config - The resolved build configuration.
|
|
89
|
+
* @param {"itemCatalog"|"assetArchive"} flag - Which boolean to filter on.
|
|
79
90
|
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
80
|
-
* The opted-in relationships
|
|
91
|
+
* The opted-in relationships.
|
|
81
92
|
*/
|
|
82
|
-
|
|
93
|
+
function flaggedRelationships(config, flag) {
|
|
83
94
|
const out = [];
|
|
84
95
|
for (const [kind, entries] of Object.entries(config.relationships ?? {})) {
|
|
85
96
|
for (const rel of entries ?? []) {
|
|
86
|
-
if (rel
|
|
97
|
+
if (rel[flag]) {
|
|
87
98
|
out.push({
|
|
88
99
|
id: rel.id,
|
|
89
100
|
manifest: rel.manifest,
|
|
@@ -96,6 +107,29 @@ export function itemCatalogRelationships(config) {
|
|
|
96
107
|
return out;
|
|
97
108
|
}
|
|
98
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Every declared relationship that opted into supplying an item catalogue.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} config - The resolved build configuration.
|
|
114
|
+
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
115
|
+
* The opted-in relationships, in declaration order.
|
|
116
|
+
*/
|
|
117
|
+
export function itemCatalogRelationships(config) {
|
|
118
|
+
return flaggedRelationships(config, "itemCatalog");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Every declared relationship that opted into supplying its release archive
|
|
123
|
+
* for asset bytes alone, with no item catalogue built from it.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} config - The resolved build configuration.
|
|
126
|
+
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
127
|
+
* The opted-in relationships, in declaration order.
|
|
128
|
+
*/
|
|
129
|
+
export function assetArchiveRelationships(config) {
|
|
130
|
+
return flaggedRelationships(config, "assetArchive");
|
|
131
|
+
}
|
|
132
|
+
|
|
99
133
|
/**
|
|
100
134
|
* The cache directory for one dependency at one version.
|
|
101
135
|
*
|
|
@@ -366,15 +400,21 @@ export function pinnedManifestUrl(url, verified) {
|
|
|
366
400
|
}
|
|
367
401
|
|
|
368
402
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* @
|
|
403
|
+
* Resolve a dependency's manifest, pinned to `compatibility.verified` the way
|
|
404
|
+
* both fetch paths require.
|
|
405
|
+
*
|
|
406
|
+
* Shared by {@link fetchCatalog} and {@link fetchAssetArchive}: both read the
|
|
407
|
+
* same manifest before deciding what to unpack from it.
|
|
408
|
+
*
|
|
409
|
+
* @param {{id: string, manifest: string, verified?: string}} rel - The
|
|
410
|
+
* declared relationship.
|
|
411
|
+
* @param {string} subject - What floats when no version is verified —
|
|
412
|
+
* "catalogue" or "archive" — named in the warning a reader would otherwise
|
|
413
|
+
* get no word for.
|
|
414
|
+
* @returns {Promise<{manifest: object, version: string}>} The manifest and
|
|
415
|
+
* its resolved version.
|
|
376
416
|
*/
|
|
377
|
-
|
|
417
|
+
async function resolveManifest(rel, subject) {
|
|
378
418
|
const { url, pinned } = pinnedManifestUrl(rel.manifest, rel.verified);
|
|
379
419
|
const manifest = await fetchManifest(url);
|
|
380
420
|
const version = manifest.version;
|
|
@@ -391,10 +431,24 @@ export async function fetchCatalog(config, rel) {
|
|
|
391
431
|
}
|
|
392
432
|
if (!rel.verified) {
|
|
393
433
|
log.warn(
|
|
394
|
-
`${rel.id}: no \`compatibility.verified\`, so its
|
|
434
|
+
`${rel.id}: no \`compatibility.verified\`, so its ${subject} floats ` +
|
|
395
435
|
`with whatever ${url} currently serves`,
|
|
396
436
|
);
|
|
397
437
|
}
|
|
438
|
+
return { manifest, version };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Fetch one dependency and extract its Item packs.
|
|
443
|
+
*
|
|
444
|
+
* Idempotent: a complete cache for the resolved version is left alone.
|
|
445
|
+
*
|
|
446
|
+
* @param {object} config - The resolved build configuration.
|
|
447
|
+
* @param {{id: string, manifest: string}} rel - The declared relationship.
|
|
448
|
+
* @returns {Promise<string>} The dependency's cache directory.
|
|
449
|
+
*/
|
|
450
|
+
export async function fetchCatalog(config, rel) {
|
|
451
|
+
const { manifest, version } = await resolveManifest(rel, "catalogue");
|
|
398
452
|
const dir = catalogDir(config, rel.id, version);
|
|
399
453
|
if (isComplete(dir)) {
|
|
400
454
|
log.info(`${rel.id}@${version}: already cached`);
|
|
@@ -420,6 +474,61 @@ export async function fetchCatalog(config, rel) {
|
|
|
420
474
|
return dir;
|
|
421
475
|
}
|
|
422
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Whether an asset-archive-only cache is present and complete.
|
|
479
|
+
*
|
|
480
|
+
* No item catalogue is built on this path, so completeness never asks for
|
|
481
|
+
* `item-packs.json` — an asset-only dependency, such as `thalornaaltart`,
|
|
482
|
+
* ships none.
|
|
483
|
+
*
|
|
484
|
+
* @param {string} dir - The dependency's cache directory.
|
|
485
|
+
* @returns {boolean} True when the archive was fetched to completion.
|
|
486
|
+
*/
|
|
487
|
+
const isArchiveComplete = (dir) => fs.existsSync(path.join(dir, STAMP));
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Fetch one dependency's release archive for its asset bytes alone, building
|
|
491
|
+
* no item catalogue from it.
|
|
492
|
+
*
|
|
493
|
+
* Idempotent: a complete cache for the resolved version is left alone. Where a
|
|
494
|
+
* relationship also declares `itemCatalog: true`, {@link fetchCatalog} already
|
|
495
|
+
* unpacked this same archive into this same cache directory, and
|
|
496
|
+
* {@link fetchAllCatalogs} does not call this function for it — the two never
|
|
497
|
+
* download the same bytes twice.
|
|
498
|
+
*
|
|
499
|
+
* @param {object} config - The resolved build configuration.
|
|
500
|
+
* @param {{id: string, manifest: string, verified?: string}} rel - The
|
|
501
|
+
* declared relationship.
|
|
502
|
+
* @returns {Promise<string>} The dependency's cache directory.
|
|
503
|
+
*/
|
|
504
|
+
export async function fetchAssetArchive(config, rel) {
|
|
505
|
+
const { manifest, version } = await resolveManifest(rel, "archive");
|
|
506
|
+
const dir = catalogDir(config, rel.id, version);
|
|
507
|
+
if (isArchiveComplete(dir)) {
|
|
508
|
+
log.info(`${rel.id}@${version}: already cached`);
|
|
509
|
+
return dir;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const download = manifest.download;
|
|
513
|
+
if (!download) {
|
|
514
|
+
throw new Error(`${rel.id}@${version}: its manifest declares no \`download\``);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Rebuild from empty: a previous run may have died partway, and a stale
|
|
518
|
+
// half-tree is worse than no tree.
|
|
519
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
520
|
+
const raw = path.join(dir, "package");
|
|
521
|
+
fs.mkdirSync(raw, { recursive: true });
|
|
522
|
+
|
|
523
|
+
log.info(`${rel.id}@${version}: downloading ${download}`);
|
|
524
|
+
await downloadAndUnzip(download, raw);
|
|
525
|
+
|
|
526
|
+
cacheSchemaArtifact(raw, dir);
|
|
527
|
+
// Last, so a fetch that died partway is never mistaken for a complete one.
|
|
528
|
+
fs.writeFileSync(path.join(dir, STAMP), `${version}\n`);
|
|
529
|
+
return dir;
|
|
530
|
+
}
|
|
531
|
+
|
|
423
532
|
/**
|
|
424
533
|
* Locate a declared pack inside an unpacked archive.
|
|
425
534
|
*
|
|
@@ -700,17 +809,33 @@ export async function fetchAllMetadata(config) {
|
|
|
700
809
|
/**
|
|
701
810
|
* Fetch every opted-in dependency. The `deps fetch` command.
|
|
702
811
|
*
|
|
812
|
+
* A relationship declaring both `itemCatalog: true` and `assetArchive: true`
|
|
813
|
+
* is fetched once: {@link fetchCatalog} already unpacks the archive on its
|
|
814
|
+
* way to extracting the item catalogue, so this skips the archive-only fetch
|
|
815
|
+
* for any id {@link fetchCatalog} already handled.
|
|
816
|
+
*
|
|
703
817
|
* @param {object} config - The resolved build configuration.
|
|
704
818
|
* @returns {Promise<number>} How many dependencies were fetched.
|
|
705
819
|
*/
|
|
706
820
|
export async function fetchAllCatalogs(config) {
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
821
|
+
const itemRels = itemCatalogRelationships(config);
|
|
822
|
+
const archiveRels = assetArchiveRelationships(config);
|
|
823
|
+
if (!itemRels.length && !archiveRels.length) {
|
|
824
|
+
log.info(
|
|
825
|
+
"No relationship declares `itemCatalog: true` or `assetArchive: true`; " +
|
|
826
|
+
"nothing to fetch.",
|
|
827
|
+
);
|
|
710
828
|
return 0;
|
|
711
829
|
}
|
|
712
|
-
for (const rel of
|
|
713
|
-
|
|
830
|
+
for (const rel of itemRels) await fetchCatalog(config, rel);
|
|
831
|
+
const alreadyFetched = new Set(itemRels.map((rel) => rel.id));
|
|
832
|
+
let archiveFetched = 0;
|
|
833
|
+
for (const rel of archiveRels) {
|
|
834
|
+
if (alreadyFetched.has(rel.id)) continue;
|
|
835
|
+
await fetchAssetArchive(config, rel);
|
|
836
|
+
archiveFetched++;
|
|
837
|
+
}
|
|
838
|
+
return itemRels.length + archiveFetched;
|
|
714
839
|
}
|
|
715
840
|
|
|
716
841
|
/**
|