@heroiclands/package-build 23.9.0 → 23.11.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 +33 -0
- package/README.md +2 -0
- package/bin/content-commands.mjs +197 -68
- package/docs/api.md +365 -70
- package/docs/authoring/first-note.md +10 -0
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +186 -49
- package/docs/commands.md +92 -3
- package/docs/content-format.md +10 -3
- package/docs/design.md +18 -0
- package/docs/guides/book.md +7 -0
- package/docs/guides/site.md +7 -0
- package/docs/reference/format-details.md +186 -15
- package/docs/reference/note-types.md +4 -4
- package/engine/anchors.mjs +43 -9
- 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 +14 -1
- package/engine/content-charset.mjs +13 -1
- 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 +102 -23
- package/engine/content-links.mjs +68 -30
- package/engine/content-session.mjs +247 -0
- package/engine/content-sources.mjs +110 -0
- package/engine/content-spans.mjs +118 -14
- package/engine/content-tables.mjs +1 -0
- package/engine/content-tasklists.mjs +20 -4
- package/engine/diagnostics.mjs +17 -6
- package/engine/extension-attributes.mjs +28 -5
- package/engine/first-use.mjs +371 -0
- package/engine/foundry-entries.mjs +12 -9
- package/engine/generate.mjs +13 -4
- package/engine/helpers.mjs +47 -16
- package/engine/index-records.mjs +1 -0
- package/engine/index.mjs +3 -0
- package/engine/journals.mjs +9 -5
- package/engine/language-data.mjs +2 -2
- package/engine/language-rows.mjs +4 -4
- package/engine/language-terms.mjs +4 -4
- package/engine/lexical-sections.mjs +5 -5
- 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/path-charset.mjs +24 -1
- package/engine/pdf-render.mjs +15 -1
- package/engine/site-index.mjs +25 -2
- package/engine/sql-tables.mjs +76 -15
- package/engine/stub-lint.mjs +4 -3
- package/engine/web-wikilinks.mjs +33 -12
- package/engine/wikilink-syntax.mjs +65 -8
- package/engine/wikilinks.mjs +29 -11
- package/engine/yaml-lint.mjs +3 -5
- package/hugo-theme/layouts/partials/footer.html +2 -0
- package/hugo-theme/static/css/style.css +20 -0
- package/hugo-theme/static/js/inline-spoilers.js +23 -0
- package/package.json +1 -1
- package/types/engine/anchors.d.mts +6 -3
- 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 +39 -19
- 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 +8 -6
- package/types/engine/content-tasklists.d.mts +5 -1
- package/types/engine/diagnostics.d.mts +10 -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 +11 -4
- package/types/engine/generate.d.mts +5 -1
- package/types/engine/helpers.d.mts +30 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/journals.d.mts +5 -2
- package/types/engine/lexical-sections.d.mts +3 -2
- 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/path-charset.d.mts +16 -1
- package/types/engine/site-build.d.mts +2 -2
- package/types/engine/site-index.d.mts +3 -1
- package/types/engine/sql-tables.d.mts +7 -2
- package/types/engine/stub-lint.d.mts +3 -1
- package/types/engine/wikilink-syntax.d.mts +40 -3
|
@@ -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({
|
|
@@ -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) => {
|