@heroiclands/package-build 23.8.0 → 23.10.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 +53 -0
- package/README.md +2 -0
- package/bin/content-commands.mjs +207 -70
- package/docs/api.md +646 -78
- package/docs/authoring/first-note.md +1 -1
- package/docs/authoring/links-and-markup.md +253 -102
- package/docs/commands.md +111 -13
- package/docs/content-format.md +7 -2
- package/docs/diagnostics.md +1 -1
- package/docs/guides/packs.md +2 -1
- package/docs/guides/reference-pages.md +86 -63
- package/docs/reference/format-details.md +398 -137
- package/docs/reference/note-types.md +188 -174
- package/engine/anchors.mjs +15 -26
- package/engine/base-compiler.mjs +6 -4
- package/engine/changelog-group.mjs +58 -34
- package/engine/changelog-lint.mjs +6 -2
- package/engine/compendiums.mjs +15 -1
- package/engine/compile-corpus.mjs +17 -3
- package/engine/content-charset.mjs +13 -1
- package/engine/content-format.yaml +1 -0
- package/engine/content-html.mjs +18 -4
- package/engine/content-icons.mjs +15 -1
- package/engine/content-images.mjs +28 -9
- package/engine/content-index.mjs +496 -16
- package/engine/content-links.mjs +70 -27
- package/engine/content-session.mjs +247 -0
- package/engine/content-sources.mjs +110 -0
- package/engine/content-spans.mjs +87 -8
- package/engine/content-tables.mjs +9 -4
- package/engine/content-tasklists.mjs +20 -4
- package/engine/culture-reach.mjs +61 -34
- package/engine/data-keys.mjs +3 -1
- package/engine/diagnostics.mjs +17 -6
- package/engine/dictionary-layout.mjs +151 -19
- package/engine/dictionary-rows.mjs +129 -79
- package/engine/extension-attributes.mjs +28 -5
- package/engine/first-use.mjs +371 -0
- package/engine/foundry-entries.mjs +29 -8
- package/engine/generate.mjs +13 -4
- package/engine/glossary-entries.mjs +11 -11
- package/engine/heading-attributes.mjs +21 -28
- package/engine/helpers.mjs +54 -19
- package/engine/index-records.mjs +1 -0
- package/engine/index.mjs +3 -0
- package/engine/journals.mjs +13 -6
- package/engine/language-data.mjs +55 -2
- package/engine/language-rows.mjs +4 -4
- package/engine/language-terms.mjs +90 -4
- package/engine/language-words.mjs +2 -2
- package/engine/lexical-sections.mjs +247 -0
- package/engine/markup-validation.mjs +7 -2
- package/engine/metadata-index.mjs +1 -0
- package/engine/name-markup.mjs +114 -0
- package/engine/name-rows.mjs +70 -0
- package/engine/names.mjs +460 -0
- package/engine/note-addresses.mjs +25 -4
- package/engine/note-claims.mjs +2 -0
- package/engine/note-state.mjs +9 -3
- package/engine/note-vocabulary.mjs +7 -6
- package/engine/path-charset.mjs +24 -1
- package/engine/pdf-build.mjs +9 -9
- package/engine/pdf-render.mjs +15 -1
- package/engine/site-build.mjs +16 -2
- package/engine/site-index.mjs +5 -1
- package/engine/sql-tables.mjs +324 -44
- package/engine/stub-lint.mjs +10 -5
- package/engine/web-wikilinks.mjs +8 -6
- package/engine/word-rows.mjs +20 -12
- package/engine/word-terms.mjs +15 -6
- package/engine/yaml-lint.mjs +3 -5
- package/hugo-theme/static/css/style.css +4 -0
- package/package.json +1 -1
- package/types/engine/anchors.d.mts +4 -2
- package/types/engine/compendiums.d.mts +5 -1
- package/types/engine/compile-corpus.d.mts +5 -2
- package/types/engine/content-charset.d.mts +5 -1
- package/types/engine/content-html.d.mts +5 -1
- package/types/engine/content-icons.d.mts +5 -1
- package/types/engine/content-images.d.mts +5 -1
- package/types/engine/content-index.d.mts +102 -18
- package/types/engine/content-links.d.mts +22 -8
- package/types/engine/content-session.d.mts +108 -0
- package/types/engine/content-sources.d.mts +32 -0
- package/types/engine/content-spans.d.mts +3 -5
- package/types/engine/content-tables.d.mts +2 -1
- package/types/engine/content-tasklists.d.mts +5 -1
- package/types/engine/culture-reach.d.mts +22 -2
- package/types/engine/data-keys.d.mts +7 -0
- package/types/engine/diagnostics.d.mts +10 -0
- package/types/engine/dictionary-layout.d.mts +35 -4
- package/types/engine/dictionary-rows.d.mts +9 -0
- package/types/engine/extension-attributes.d.mts +16 -2
- package/types/engine/first-use.d.mts +31 -0
- package/types/engine/foundry-entries.d.mts +14 -5
- package/types/engine/generate.d.mts +5 -1
- package/types/engine/heading-attributes.d.mts +2 -2
- package/types/engine/helpers.d.mts +33 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/journals.d.mts +5 -2
- package/types/engine/language-terms.d.mts +7 -0
- package/types/engine/lexical-sections.d.mts +96 -0
- package/types/engine/markup-validation.d.mts +2 -1
- package/types/engine/name-markup.d.mts +176 -0
- package/types/engine/name-rows.d.mts +35 -0
- package/types/engine/names.d.mts +356 -0
- package/types/engine/note-addresses.d.mts +3 -3
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/path-charset.d.mts +16 -1
- package/types/engine/site-build.d.mts +6 -3
- package/types/engine/site-index.d.mts +3 -1
- package/types/engine/sql-tables.d.mts +39 -4
- package/types/engine/stub-lint.d.mts +3 -1
- package/types/engine/word-rows.d.mts +9 -7
package/engine/anchors.mjs
CHANGED
|
@@ -39,10 +39,9 @@ import { scanAlerts } from "./content-alerts.mjs";
|
|
|
39
39
|
import { scanBlocks } from "./content-blocks.mjs";
|
|
40
40
|
import { scanPoetry } from "./content-poetry.mjs";
|
|
41
41
|
import { scanSpans } from "./content-spans.mjs";
|
|
42
|
-
import { codeRegions } from "./code-fences.mjs";
|
|
43
42
|
import { slugify } from "./content-slug.mjs";
|
|
44
43
|
import { scanFigures } from "./content-figures.mjs";
|
|
45
|
-
import {
|
|
44
|
+
import { scanHeadingAttributes } from "./heading-attributes.mjs";
|
|
46
45
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
47
46
|
|
|
48
47
|
/**
|
|
@@ -66,30 +65,19 @@ export const ANCHOR_KINDS = Object.freeze(["prose", "event"]);
|
|
|
66
65
|
* picture's address to the role its asset declares, so a figure anchor's
|
|
67
66
|
* `name` reads `Map 1` rather than `Figure 1` where it is due — see
|
|
68
67
|
* {@link module:engine/content-figures.scanFigures}.
|
|
68
|
+
* @param {import("./content-session.mjs").ContentSyntax} [syntax] - Exact-text scanner projections.
|
|
69
69
|
* @returns {Array<{slug: string, name: string, level: number, line: number,
|
|
70
70
|
* kind: string}>} In document order.
|
|
71
71
|
*/
|
|
72
|
-
export function collectAnchors(body, bodyLine = 1, resolveRole) {
|
|
72
|
+
export function collectAnchors(body, bodyLine = 1, resolveRole, syntax) {
|
|
73
73
|
const anchors = [];
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
const lines = String(body ?? "").split("\n");
|
|
77
|
-
|
|
78
|
-
for (let i = 0; i < lines.length; i++) {
|
|
79
|
-
const lineOffset = offset;
|
|
80
|
-
offset += lines[i].length + 1;
|
|
81
|
-
if (literal.some((region) => lineOffset >= region.start && lineOffset < region.end))
|
|
82
|
-
continue;
|
|
83
|
-
|
|
84
|
-
const heading = HEADING_LINE.exec(lines[i]);
|
|
85
|
-
if (!heading) continue;
|
|
86
|
-
const attributes = splitHeadingAttributes(heading[2]);
|
|
87
|
-
if (!attributes.id) continue;
|
|
74
|
+
for (const heading of scanHeadingAttributes(body, bodyLine).headings) {
|
|
75
|
+
if (!heading.id) continue;
|
|
88
76
|
anchors.push({
|
|
89
|
-
slug:
|
|
90
|
-
name:
|
|
91
|
-
level: heading
|
|
92
|
-
line:
|
|
77
|
+
slug: heading.id,
|
|
78
|
+
name: heading.text,
|
|
79
|
+
level: heading.level,
|
|
80
|
+
line: heading.line,
|
|
93
81
|
kind: "prose",
|
|
94
82
|
});
|
|
95
83
|
}
|
|
@@ -105,7 +93,7 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
|
|
|
105
93
|
}
|
|
106
94
|
const blockAnchors = (source, firstLine) => {
|
|
107
95
|
const sourceLines = source.split("\n");
|
|
108
|
-
for (const block of scanBlocks(source).blocks) {
|
|
96
|
+
for (const block of (syntax ? syntax(source).blocks : scanBlocks(source)).blocks) {
|
|
109
97
|
if (block.id)
|
|
110
98
|
anchors.push({
|
|
111
99
|
slug: slugify(block.id),
|
|
@@ -121,7 +109,7 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
|
|
|
121
109
|
}
|
|
122
110
|
};
|
|
123
111
|
blockAnchors(String(body ?? ""), bodyLine);
|
|
124
|
-
for (const alert of scanAlerts(body).blocks) {
|
|
112
|
+
for (const alert of (syntax ? syntax(body).alerts : scanAlerts(body)).blocks) {
|
|
125
113
|
if (alert.id)
|
|
126
114
|
anchors.push({
|
|
127
115
|
slug: slugify(alert.id),
|
|
@@ -141,7 +129,7 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
|
|
|
141
129
|
kind: "prose",
|
|
142
130
|
});
|
|
143
131
|
}
|
|
144
|
-
for (const span of scanSpans(body).spans) {
|
|
132
|
+
for (const span of (syntax ? syntax(body).spans : scanSpans(body)).spans) {
|
|
145
133
|
if (span.id)
|
|
146
134
|
anchors.push({
|
|
147
135
|
slug: slugify(span.id),
|
|
@@ -157,11 +145,12 @@ export function collectAnchors(body, bodyLine = 1, resolveRole) {
|
|
|
157
145
|
/** Collisions involving a div, verse or inline-span anchor.
|
|
158
146
|
* Heading and caption collisions are diagnosed by their existing scanners.
|
|
159
147
|
* @param {string} body - Authored Markdown.
|
|
148
|
+
* @param {import("./content-session.mjs").ContentSyntax} [syntax] - Exact-text scanner projections.
|
|
160
149
|
* @returns {Array<{line: number, column: number, message: string}>} Located collisions.
|
|
161
150
|
*/
|
|
162
|
-
export function markupAnchorFindings(body) {
|
|
151
|
+
export function markupAnchorFindings(body, syntax) {
|
|
163
152
|
const groups = new Map();
|
|
164
|
-
for (const anchor of collectAnchors(body)) {
|
|
153
|
+
for (const anchor of collectAnchors(body, 1, undefined, syntax)) {
|
|
165
154
|
const slug = slugify(anchor.slug);
|
|
166
155
|
if (!groups.has(slug)) groups.set(slug, []);
|
|
167
156
|
groups.get(slug).push(anchor);
|
package/engine/base-compiler.mjs
CHANGED
|
@@ -686,11 +686,13 @@ export class BasePackCompiler {
|
|
|
686
686
|
// it made — `errors` or `findings`, never just the first — so nothing
|
|
687
687
|
// here re-scans; it only stops taking `[0]`.
|
|
688
688
|
const findings = [];
|
|
689
|
+
const syntax = this.corpus?.syntax;
|
|
690
|
+
const scanned = syntax?.(body);
|
|
689
691
|
for (const secretError of [
|
|
690
|
-
...scanBlocks(body).errors,
|
|
691
|
-
...scanAlerts(body).errors,
|
|
692
|
-
...scanSpans(body).errors,
|
|
693
|
-
...markupAnchorFindings(body),
|
|
692
|
+
...(scanned?.blocks ?? scanBlocks(body)).errors,
|
|
693
|
+
...(scanned?.alerts ?? scanAlerts(body)).errors,
|
|
694
|
+
...(scanned?.spans ?? scanSpans(body)).errors,
|
|
695
|
+
...markupAnchorFindings(body, syntax),
|
|
694
696
|
]) {
|
|
695
697
|
findings.push({
|
|
696
698
|
message: secretError.message,
|
|
@@ -46,33 +46,37 @@ import {
|
|
|
46
46
|
const CHANGES_HEADING_RE = /^### (?:Major|Minor|Patch) Changes$/gm;
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* A
|
|
49
|
+
* A label, its complete opening paragraph and its remaining items.
|
|
50
50
|
*
|
|
51
51
|
* @param {string} blockText - One block, as {@link topLevelBlocks} collects
|
|
52
52
|
* it — its first line is the label line for a labelled block.
|
|
53
|
-
* @returns {{labelLine: string,
|
|
54
|
-
*
|
|
55
|
-
* line yields `""`. `hadBlankLine` says whether the label line was
|
|
56
|
-
* followed by a blank line in `blockText` — false for a bold sentence a
|
|
57
|
-
* hard-wrapped paragraph continues directly beneath, with no blank line
|
|
58
|
-
* of its own.
|
|
53
|
+
* @returns {{labelLine: string, introduction: string, rest: string}} The
|
|
54
|
+
* introduction retains hard wrapping and stops before a bullet or code.
|
|
59
55
|
*/
|
|
60
|
-
function
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
const
|
|
56
|
+
function splitLabelBlock(blockText) {
|
|
57
|
+
const lines = blockText.replace(/\s+$/, "").split("\n");
|
|
58
|
+
const labelLine = /^\*\*[^*]+\*\*/.exec(lines[0])[0];
|
|
59
|
+
const inline = lines[0].slice(labelLine.length).trimStart();
|
|
60
|
+
const introduction = inline ? [inline] : [];
|
|
61
|
+
const codeLines = codeLineSet(blockText);
|
|
62
|
+
let cursor = 1;
|
|
63
|
+
while (
|
|
64
|
+
cursor < lines.length &&
|
|
65
|
+
lines[cursor].trim() !== "" &&
|
|
66
|
+
!/^-\s/.test(lines[cursor]) &&
|
|
67
|
+
!codeLines.has(cursor + 1)
|
|
68
|
+
)
|
|
69
|
+
introduction.push(lines[cursor++]);
|
|
64
70
|
return {
|
|
65
|
-
labelLine
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
labelLine,
|
|
72
|
+
introduction: introduction.join("\n"),
|
|
73
|
+
rest: lines.slice(cursor).join("\n").replace(/^\n+/, ""),
|
|
68
74
|
};
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
/**
|
|
72
|
-
* A block's top-level
|
|
73
|
-
*
|
|
74
|
-
* deduplicates, so a bulleted `**Compendiums**` block and a prose one merge
|
|
75
|
-
* on the same footing.
|
|
78
|
+
* A block's top-level bullets and the prose or code between them, in source
|
|
79
|
+
* order. A bullet retains its indented continuation and nested items.
|
|
76
80
|
*
|
|
77
81
|
* @param {string} text - A block's content, label line already stripped (or
|
|
78
82
|
* a lead block's whole text).
|
|
@@ -83,7 +87,26 @@ function itemsOf(text) {
|
|
|
83
87
|
if (!trimmed) return [];
|
|
84
88
|
const bullets = topLevelBullets(trimmed, codeLineSet(trimmed));
|
|
85
89
|
if (bullets.length === 0) return [trimmed];
|
|
86
|
-
|
|
90
|
+
const starts = [];
|
|
91
|
+
let offset = 0;
|
|
92
|
+
for (const line of trimmed.split("\n")) {
|
|
93
|
+
starts.push(offset);
|
|
94
|
+
offset += line.length + 1;
|
|
95
|
+
}
|
|
96
|
+
const items = [];
|
|
97
|
+
const append = (part) => {
|
|
98
|
+
const item = part.replace(/^(?:[ \t]*\n)+/, "").replace(/\s+$/, "");
|
|
99
|
+
if (item) items.push(item);
|
|
100
|
+
};
|
|
101
|
+
let cursor = 0;
|
|
102
|
+
for (const bullet of bullets) {
|
|
103
|
+
const start = starts[bullet.startLine - 1];
|
|
104
|
+
append(trimmed.slice(cursor, start));
|
|
105
|
+
append(bullet.text);
|
|
106
|
+
cursor = start + bullet.text.length;
|
|
107
|
+
}
|
|
108
|
+
append(trimmed.slice(cursor));
|
|
109
|
+
return items;
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
/**
|
|
@@ -124,13 +147,9 @@ function joinItems(items) {
|
|
|
124
147
|
* One label's blocks (or every unlabelled one), folded into the single
|
|
125
148
|
* block `group` writes for it.
|
|
126
149
|
*
|
|
127
|
-
* A single
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* beneath must round-trip unchanged, not gain a blank line it never had.
|
|
131
|
-
* Merging two or more blocks, or a label whose lone block carries several
|
|
132
|
-
* items (bullets), still separates the label from its body with a blank
|
|
133
|
-
* line — the convention every existing multi-item block already uses.
|
|
150
|
+
* A single block with no duplicate items retains its own spacing. Merged
|
|
151
|
+
* opening paragraphs form one hard-wrapped introduction before the items,
|
|
152
|
+
* so paragraph boundaries cannot turn labelled text into unlabelled lead.
|
|
134
153
|
*
|
|
135
154
|
* @param {Array<{label: string|null, text: string}>} blocks - Every block
|
|
136
155
|
* sharing one label, in file order. All carry the same `label`.
|
|
@@ -140,13 +159,15 @@ function mergeGroup(blocks) {
|
|
|
140
159
|
if (blocks[0].label === null) {
|
|
141
160
|
return joinItems(dedupeItems(blocks.flatMap((block) => itemsOf(block.text))));
|
|
142
161
|
}
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
162
|
+
const parts = blocks.map((block) => splitLabelBlock(block.text));
|
|
163
|
+
const introductions = dedupeItems(parts.map((part) => part.introduction).filter(Boolean));
|
|
164
|
+
const originalItems = parts.flatMap((part) => itemsOf(part.rest));
|
|
165
|
+
const items = dedupeItems(originalItems);
|
|
166
|
+
if (blocks.length === 1 && items.length === originalItems.length)
|
|
167
|
+
return blocks[0].text.replace(/\s+$/, "");
|
|
168
|
+
const heading =
|
|
169
|
+
parts[0].labelLine + (introductions.length ? ` ${introductions.join("\n")}` : "");
|
|
170
|
+
return heading + (items.length ? `\n\n${joinItems(items)}` : "");
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
/**
|
|
@@ -245,7 +266,10 @@ export function groupChangelogText(text, { labels = null } = {}) {
|
|
|
245
266
|
if (!range) return { text, findings: [] };
|
|
246
267
|
|
|
247
268
|
const section = text.slice(range.start, range.end);
|
|
248
|
-
const
|
|
269
|
+
const codeLines = codeLineSet(section);
|
|
270
|
+
const headings = [...section.matchAll(CHANGES_HEADING_RE)].filter(
|
|
271
|
+
(heading) => !codeLines.has(lineColOf(section, heading.index).line),
|
|
272
|
+
);
|
|
249
273
|
if (headings.length === 0) return { text, findings: [] };
|
|
250
274
|
|
|
251
275
|
let rebuilt = section.slice(0, headings[0].index);
|
|
@@ -277,7 +277,8 @@ export function topLevelBlocks(text, codeLines, scaffoldLines = new Set()) {
|
|
|
277
277
|
const raw = lines[i];
|
|
278
278
|
|
|
279
279
|
if (codeLines.has(lineNo)) {
|
|
280
|
-
|
|
280
|
+
current ??= { startLine: lineNo, label: null, parts: [] };
|
|
281
|
+
current.parts.push(raw);
|
|
281
282
|
afterBlank = raw.trim() === "";
|
|
282
283
|
continue;
|
|
283
284
|
}
|
|
@@ -800,7 +801,10 @@ export function lintReleaseText(text, { labels = null } = {}) {
|
|
|
800
801
|
* of the file.
|
|
801
802
|
*/
|
|
802
803
|
export function releaseSectionRange(text) {
|
|
803
|
-
const
|
|
804
|
+
const codeLines = codeLineSet(text);
|
|
805
|
+
const matches = [...text.matchAll(/^## /gm)].filter(
|
|
806
|
+
(heading) => !codeLines.has(lineColOf(text, heading.index).line),
|
|
807
|
+
);
|
|
804
808
|
if (matches.length === 0) return null;
|
|
805
809
|
const start = matches[0].index;
|
|
806
810
|
const end = matches.length > 1 ? matches[1].index : text.length;
|
package/engine/compendiums.mjs
CHANGED
|
@@ -70,6 +70,8 @@ import { loadPackConfig } from "./pack-config.mjs";
|
|
|
70
70
|
* @param {string} [opts.stageDest] Directory the LevelDB packs are built
|
|
71
71
|
* into, one subdirectory per pack. Defaults to the configured stage.
|
|
72
72
|
* @param {string} [opts.packName] Restrict the run to a single pack.
|
|
73
|
+
* @param {import("./content-session.mjs").ContentSession} [opts.session] - Captured context shared with other operations.
|
|
74
|
+
* @param {boolean} [opts.reportPreparation=true] - Emit shared preparation findings unless the caller already reported them.
|
|
73
75
|
* @throws {Error} If pack JSON generation reported any error. Packs compiled
|
|
74
76
|
* from incomplete or empty JSON ship blank or short compendiums, and the
|
|
75
77
|
* omission is invisible until a player looks for content that is not there
|
|
@@ -86,7 +88,14 @@ export async function compilePacks({
|
|
|
86
88
|
sourcePacks = config.packDirectories,
|
|
87
89
|
stageDest = config.paths.stage,
|
|
88
90
|
packName,
|
|
91
|
+
session,
|
|
92
|
+
reportPreparation = true,
|
|
89
93
|
} = {}) {
|
|
94
|
+
session?.assertContext({
|
|
95
|
+
config,
|
|
96
|
+
contentBase: config.paths.content,
|
|
97
|
+
skipDirectories: config.skipDirectories,
|
|
98
|
+
});
|
|
90
99
|
const packNames = sourcePacks.filter((name) => !packName || name === packName);
|
|
91
100
|
|
|
92
101
|
// A prebuilt pack's JSON is checked in rather than generated, so it is
|
|
@@ -105,7 +114,12 @@ export async function compilePacks({
|
|
|
105
114
|
// content tree, and a package that ships only prebuilt packs has none at
|
|
106
115
|
// all — so running it would fail the build over a tree nothing reads.
|
|
107
116
|
if (needsGeneration) {
|
|
108
|
-
const errors = await generatePacksJson({
|
|
117
|
+
const errors = await generatePacksJson({
|
|
118
|
+
only: packName,
|
|
119
|
+
config,
|
|
120
|
+
session,
|
|
121
|
+
reportPreparation,
|
|
122
|
+
});
|
|
109
123
|
if (errors > 0) {
|
|
110
124
|
throw new Error(
|
|
111
125
|
`Pack JSON generation reported ${errors} error(s); refusing to ` +
|
|
@@ -60,7 +60,8 @@ 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
|
-
* @
|
|
63
|
+
* @param {import("./content-session.mjs").ContentSession} [opts.session] - Captured context shared with other operations.
|
|
64
|
+
* @returns {Promise<{records: object[], parsed: Map<string, object>, linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[], syntax?: import("./content-session.mjs").ContentSyntax}>} The corpus,
|
|
64
65
|
* its indexes, and the notes it could not record.
|
|
65
66
|
*/
|
|
66
67
|
export async function buildCompileCorpus({
|
|
@@ -70,8 +71,20 @@ export async function buildCompileCorpus({
|
|
|
70
71
|
router,
|
|
71
72
|
config,
|
|
72
73
|
problems,
|
|
74
|
+
session,
|
|
73
75
|
}) {
|
|
74
76
|
const resolved = config ?? loadPackConfig();
|
|
77
|
+
if (session) {
|
|
78
|
+
session.assertContext({
|
|
79
|
+
config: resolved,
|
|
80
|
+
contentBase,
|
|
81
|
+
assetsBase,
|
|
82
|
+
skipDirectories,
|
|
83
|
+
router,
|
|
84
|
+
});
|
|
85
|
+
if (problems && problems !== session.problems) problems.push(...session.problems);
|
|
86
|
+
return session.compileCorpus();
|
|
87
|
+
}
|
|
75
88
|
const collected = problems ?? [];
|
|
76
89
|
const parsed = new Map();
|
|
77
90
|
const records = indexRecordsFor({
|
|
@@ -84,11 +97,12 @@ export async function buildCompileCorpus({
|
|
|
84
97
|
});
|
|
85
98
|
const corpusOptions = { config: resolved, records };
|
|
86
99
|
const sqlOptions = { ...corpusOptions, skipDirectories, parsed };
|
|
100
|
+
const sqlTables = await prepareTreeSqlTables(contentBase, sqlOptions);
|
|
87
101
|
return {
|
|
88
102
|
records,
|
|
89
103
|
parsed,
|
|
90
104
|
problems: collected,
|
|
91
|
-
linkIndex: buildContentLinkIndex(contentBase, router, corpusOptions),
|
|
92
|
-
sqlTables
|
|
105
|
+
linkIndex: buildContentLinkIndex(contentBase, router, { ...corpusOptions, sqlTables }),
|
|
106
|
+
sqlTables,
|
|
93
107
|
};
|
|
94
108
|
}
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
|
|
86
86
|
import fs from "node:fs";
|
|
87
87
|
import path from "node:path";
|
|
88
|
+
import { capturedSources } from "./content-sources.mjs";
|
|
88
89
|
|
|
89
90
|
/**
|
|
90
91
|
* Tier 1 — the letters, and the two whitespace characters a file is made of.
|
|
@@ -407,16 +408,27 @@ export function checkText(text, file) {
|
|
|
407
408
|
* @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore
|
|
408
409
|
* in addition to the dot-directories always skipped.
|
|
409
410
|
* @param {readonly string[]} [opts.extensions] - File extensions to read.
|
|
411
|
+
* @param {Map<string, {raw: string}>} [opts.sources] - Exact captured source strings; omitted, walks and reads the tree.
|
|
410
412
|
* @returns {{findings: Array<{file: string, line: number, column: number,
|
|
411
413
|
* severity: "warning", message: string}>, files: number}} The findings, and how
|
|
412
414
|
* many files produced them.
|
|
413
415
|
*/
|
|
414
|
-
export function lintContentCharset(
|
|
416
|
+
export function lintContentCharset(
|
|
417
|
+
contentBase,
|
|
418
|
+
{ skipDirectories = [], extensions, sources } = {},
|
|
419
|
+
) {
|
|
415
420
|
const exts = new Set(extensions ?? [".md", ".markdown", ".yaml", ".yml", ".json"]);
|
|
416
421
|
const skip = new Set(skipDirectories);
|
|
417
422
|
/** @type {Array<{file: string, line: number, column: number, severity: "warning", message: string}>} */
|
|
418
423
|
const findings = [];
|
|
419
424
|
let files = 0;
|
|
425
|
+
if (sources) {
|
|
426
|
+
for (const [full, text] of capturedSources(contentBase, sources, skipDirectories, exts)) {
|
|
427
|
+
files++;
|
|
428
|
+
findings.push(...checkText(text, path.relative(process.cwd(), full)));
|
|
429
|
+
}
|
|
430
|
+
return { findings, files };
|
|
431
|
+
}
|
|
420
432
|
|
|
421
433
|
/** @param {string} dir - Directory to descend into. */
|
|
422
434
|
const walk = (dir) => {
|
package/engine/content-html.mjs
CHANGED
|
@@ -52,9 +52,10 @@
|
|
|
52
52
|
|
|
53
53
|
import fs from "node:fs";
|
|
54
54
|
import path from "node:path";
|
|
55
|
+
import { capturedSources } from "./content-sources.mjs";
|
|
55
56
|
|
|
56
57
|
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
57
|
-
import { positionInBody } from "./diagnostics.mjs";
|
|
58
|
+
import { positionInBody, splitFrontmatter } from "./diagnostics.mjs";
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
61
|
* A raw HTML tag, opening, closing or self-closing.
|
|
@@ -133,15 +134,28 @@ export function checkHtml(body, file, { bodyLine = 1, bodyColumn = 1 } = {}) {
|
|
|
133
134
|
* @param {object} [opts]
|
|
134
135
|
* @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore
|
|
135
136
|
* in addition to the dot-directories always skipped.
|
|
137
|
+
* @param {Map<string, {raw: string}>} [opts.sources] - Exact captured source strings; omitted, walks and reads the tree.
|
|
136
138
|
* @returns {{findings: Array<{file: string, line: number, column: number,
|
|
137
139
|
* severity: "warning", message: string}>, files: number}} The findings, and
|
|
138
140
|
* how many files were read.
|
|
139
141
|
*/
|
|
140
|
-
export function lintContentHtml(contentBase, { skipDirectories = [] } = {}) {
|
|
142
|
+
export function lintContentHtml(contentBase, { skipDirectories = [], sources } = {}) {
|
|
141
143
|
const skip = new Set(skipDirectories);
|
|
142
144
|
/** @type {Array<{file: string, line: number, column: number, severity: "warning", message: string}>} */
|
|
143
145
|
const findings = [];
|
|
144
146
|
let files = 0;
|
|
147
|
+
if (sources) {
|
|
148
|
+
for (const [full, content] of capturedSources(
|
|
149
|
+
contentBase,
|
|
150
|
+
sources,
|
|
151
|
+
skipDirectories,
|
|
152
|
+
new Set([".md", ".markdown"]),
|
|
153
|
+
)) {
|
|
154
|
+
files++;
|
|
155
|
+
findings.push(...checkHtml(...bodyOf(content, path.relative(process.cwd(), full))));
|
|
156
|
+
}
|
|
157
|
+
return { findings, files };
|
|
158
|
+
}
|
|
145
159
|
|
|
146
160
|
/** @param {string} dir - Directory to descend into. */
|
|
147
161
|
const walk = (dir) => {
|
|
@@ -188,10 +202,10 @@ export function lintContentHtml(contentBase, { skipDirectories = [] } = {}) {
|
|
|
188
202
|
* arguments {@link checkHtml} takes.
|
|
189
203
|
*/
|
|
190
204
|
function bodyOf(content, file) {
|
|
191
|
-
const match = content
|
|
205
|
+
const match = splitFrontmatter(content);
|
|
192
206
|
if (!match) return [content, file, { bodyLine: 1, bodyColumn: 1 }];
|
|
193
207
|
|
|
194
|
-
const raw = match
|
|
208
|
+
const raw = match.body;
|
|
195
209
|
const body = raw.trim();
|
|
196
210
|
const bodyStart = content.length - raw.length + (raw.length - raw.trimStart().length);
|
|
197
211
|
const before = content.slice(0, bodyStart);
|
package/engine/content-icons.mjs
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import fs from "node:fs";
|
|
29
29
|
import path from "node:path";
|
|
30
|
+
import { capturedSources } from "./content-sources.mjs";
|
|
30
31
|
import { parseExtensionAttributes } from "./extension-attributes.mjs";
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -524,13 +525,26 @@ export function checkIconRegistry(registry, where = "icons") {
|
|
|
524
525
|
* @param {object} [opts]
|
|
525
526
|
* @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore.
|
|
526
527
|
* @param {IconRegistry} [opts.registry] - The package's registry.
|
|
528
|
+
* @param {Map<string, {raw: string}>} [opts.sources] - Exact captured source strings; omitted, walks and reads the tree.
|
|
527
529
|
* @returns {{findings: Array<{file: string, line: number, column: number,
|
|
528
530
|
* severity: "warning", message: string}>, files: number}} What it found.
|
|
529
531
|
*/
|
|
530
|
-
export function lintContentIcons(contentBase, { skipDirectories = [], registry } = {}) {
|
|
532
|
+
export function lintContentIcons(contentBase, { skipDirectories = [], registry, sources } = {}) {
|
|
531
533
|
const skip = new Set(skipDirectories);
|
|
532
534
|
const findings = [];
|
|
533
535
|
let files = 0;
|
|
536
|
+
if (sources) {
|
|
537
|
+
for (const [full, text] of capturedSources(
|
|
538
|
+
contentBase,
|
|
539
|
+
sources,
|
|
540
|
+
skipDirectories,
|
|
541
|
+
new Set([".md", ".markdown"]),
|
|
542
|
+
)) {
|
|
543
|
+
files++;
|
|
544
|
+
findings.push(...lintIcons(text, path.relative(process.cwd(), full), registry));
|
|
545
|
+
}
|
|
546
|
+
return { findings, files };
|
|
547
|
+
}
|
|
534
548
|
|
|
535
549
|
/** @param {string} dir - Directory to descend into. */
|
|
536
550
|
const walk = (dir) => {
|
|
@@ -80,10 +80,11 @@
|
|
|
80
80
|
|
|
81
81
|
import fs from "node:fs";
|
|
82
82
|
import path from "node:path";
|
|
83
|
+
import { capturedSources } from "./content-sources.mjs";
|
|
83
84
|
|
|
84
85
|
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
85
86
|
import { parseExtensionAttributes } from "./extension-attributes.mjs";
|
|
86
|
-
import { positionInBody } from "./diagnostics.mjs";
|
|
87
|
+
import { positionInBody, splitFrontmatter } from "./diagnostics.mjs";
|
|
87
88
|
import { FOOTNOTE_REFERENCE } from "./content-footnotes.mjs";
|
|
88
89
|
import { foundryAddressProblem, pathnameProblem, servesFoundry } from "./pathnames.mjs";
|
|
89
90
|
import { ASSET_ROLES } from "./asset-index.mjs";
|
|
@@ -431,16 +432,20 @@ export function standsAlone(text, start, end) {
|
|
|
431
432
|
const lineEnd = src.indexOf("\n", end);
|
|
432
433
|
const afterMatch = src.slice(end, lineEnd === -1 ? src.length : lineEnd);
|
|
433
434
|
const reference = TRAILING_FOOTNOTE_REFERENCE.exec(afterMatch);
|
|
434
|
-
if (!/^[ \t]
|
|
435
|
+
if (!/^[ \t]*\r?$/.test(reference ? afterMatch.slice(reference[0].length) : afterMatch))
|
|
435
436
|
return false;
|
|
436
437
|
|
|
437
438
|
const before = src.slice(0, lineStart);
|
|
438
|
-
const previous =
|
|
439
|
-
|
|
439
|
+
const previous =
|
|
440
|
+
before
|
|
441
|
+
.replace(/\r?\n$/, "")
|
|
442
|
+
.split(/\r?\n/)
|
|
443
|
+
.pop() ?? "";
|
|
444
|
+
if (before.trim() && !/\n[ \t]*\r?\n[ \t]*$/.test(before) && !DELIMITER.test(previous))
|
|
440
445
|
return false;
|
|
441
446
|
const after = lineEnd === -1 ? "" : src.slice(lineEnd);
|
|
442
|
-
const next = after.replace(/^\n/, "").split(
|
|
443
|
-
if (after.trim() && !/^\n[ \t]*\n/.test(after) && !DELIMITER.test(next)) return false;
|
|
447
|
+
const next = after.replace(/^\n/, "").split(/\r?\n/)[0] ?? "";
|
|
448
|
+
if (after.trim() && !/^\n[ \t]*\r?\n/.test(after) && !DELIMITER.test(next)) return false;
|
|
444
449
|
return true;
|
|
445
450
|
}
|
|
446
451
|
|
|
@@ -586,10 +591,10 @@ export function checkImages(
|
|
|
586
591
|
* arguments {@link checkImages} takes.
|
|
587
592
|
*/
|
|
588
593
|
function bodyOf(content, file) {
|
|
589
|
-
const match = content
|
|
594
|
+
const match = splitFrontmatter(content);
|
|
590
595
|
if (!match) return [content, file, { bodyLine: 1, bodyColumn: 1 }];
|
|
591
596
|
|
|
592
|
-
const raw = match
|
|
597
|
+
const raw = match.body;
|
|
593
598
|
const body = raw.trim();
|
|
594
599
|
const bodyStart = content.length - raw.length + (raw.length - raw.trimStart().length);
|
|
595
600
|
const before = content.slice(0, bodyStart);
|
|
@@ -619,15 +624,29 @@ function bodyOf(content, file) {
|
|
|
619
624
|
* in addition to the dot-directories always skipped.
|
|
620
625
|
* @param {object} [opts.config] - The resolved build configuration, passed to
|
|
621
626
|
* {@link checkImages} so an address is held to the Foundry surface too.
|
|
627
|
+
* @param {Map<string, {raw: string}>} [opts.sources] - Exact captured source strings; omitted, walks and reads the tree.
|
|
622
628
|
* @returns {{findings: Array<{file: string, line: number, column: number|undefined,
|
|
623
629
|
* severity: "error", message: string}>, files: number}} The findings, and how
|
|
624
630
|
* many files were read.
|
|
625
631
|
*/
|
|
626
|
-
export function lintContentImages(contentBase, { skipDirectories = [], config } = {}) {
|
|
632
|
+
export function lintContentImages(contentBase, { skipDirectories = [], config, sources } = {}) {
|
|
627
633
|
const skip = new Set(skipDirectories);
|
|
628
634
|
/** @type {Array<{file: string, line: number, column: number|undefined, severity: "error", message: string}>} */
|
|
629
635
|
const findings = [];
|
|
630
636
|
let files = 0;
|
|
637
|
+
if (sources) {
|
|
638
|
+
for (const [full, content] of capturedSources(
|
|
639
|
+
contentBase,
|
|
640
|
+
sources,
|
|
641
|
+
skipDirectories,
|
|
642
|
+
new Set([".md", ".markdown"]),
|
|
643
|
+
)) {
|
|
644
|
+
files++;
|
|
645
|
+
const [body, rel, at] = bodyOf(content, path.relative(process.cwd(), full));
|
|
646
|
+
findings.push(...checkImages(body, rel, { ...at, config }));
|
|
647
|
+
}
|
|
648
|
+
return { findings, files };
|
|
649
|
+
}
|
|
631
650
|
|
|
632
651
|
/** @param {string} dir - Directory to descend into. */
|
|
633
652
|
const walk = (dir) => {
|