@heroiclands/package-build 23.7.1 → 23.9.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 +41 -0
- package/README.md +32 -0
- package/bin/content-commands.mjs +32 -10
- package/ci/check-banners.mjs +37 -19
- package/container.mjs +48 -10
- package/docs/api.md +402 -58
- package/docs/authoring/first-note.md +1 -1
- package/docs/authoring/frontmatter.md +30 -1
- package/docs/authoring/links-and-markup.md +154 -56
- package/docs/commands.md +31 -12
- package/docs/content-format.md +25 -2
- package/docs/design.md +31 -0
- package/docs/diagnostics.md +1 -1
- package/docs/guides/e2e.md +37 -1
- package/docs/guides/packs.md +2 -1
- package/docs/guides/reference-pages.md +86 -63
- package/docs/project-setup.md +30 -15
- package/docs/reference/format-details.md +313 -132
- package/docs/reference/note-types.md +191 -174
- package/e2e.mjs +89 -2
- package/engine/anchors.mjs +7 -20
- package/engine/base-compiler.mjs +11 -18
- package/engine/compile-corpus.mjs +5 -3
- package/engine/content-alerts.mjs +4 -2
- package/engine/content-blocks.mjs +18 -36
- package/engine/content-format.yaml +1 -0
- package/engine/content-index.mjs +447 -33
- package/engine/content-links.mjs +43 -15
- package/engine/content-poetry.mjs +9 -0
- package/engine/content-tables.mjs +8 -4
- package/engine/culture-reach.mjs +61 -34
- package/engine/data-keys.mjs +3 -1
- package/engine/diagnostics.mjs +24 -11
- package/engine/dictionary-layout.mjs +151 -19
- package/engine/dictionary-rows.mjs +129 -79
- package/engine/foundry-entries.mjs +21 -3
- package/engine/frontmatter-lint.mjs +1 -1
- package/engine/generate.mjs +12 -22
- package/engine/glossary-entries.mjs +11 -11
- package/engine/heading-attributes.mjs +21 -28
- package/engine/helpers.mjs +20 -12
- package/engine/infobox.mjs +3 -0
- package/engine/journals.mjs +15 -2
- package/engine/language-data.mjs +53 -0
- package/engine/language-terms.mjs +86 -0
- package/engine/language-words.mjs +2 -2
- package/engine/lexical-sections.mjs +247 -0
- package/engine/markdownlint-run.mjs +12 -2
- package/engine/markup-validation.mjs +34 -0
- package/engine/note-addresses.mjs +1 -0
- package/engine/note-claims.mjs +21 -10
- package/engine/note-package.mjs +3 -5
- package/engine/note-state.mjs +9 -3
- package/engine/note-vocabulary.mjs +36 -7
- package/engine/pdf-build.mjs +17 -10
- package/engine/prose-lint.mjs +12 -6
- package/engine/related-pages.mjs +22 -3
- package/engine/see-also.mjs +150 -0
- package/engine/site-build.mjs +21 -3
- package/engine/site-preview.mjs +4 -2
- package/engine/sql-tables.mjs +287 -53
- package/engine/standings.mjs +14 -12
- package/engine/stub-lint.mjs +6 -2
- package/engine/web-wikilinks.mjs +8 -6
- package/engine/word-rows.mjs +20 -12
- package/engine/word-terms.mjs +15 -6
- package/hugo-theme/layouts/partials/related.html +25 -8
- package/package.json +2 -1
- package/types/container.d.mts +4 -3
- package/types/e2e.d.mts +28 -0
- package/types/engine/base-compiler.d.mts +2 -2
- package/types/engine/compile-corpus.d.mts +2 -1
- package/types/engine/content-index.d.mts +85 -8
- package/types/engine/content-tables.d.mts +2 -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 +7 -6
- package/types/engine/dictionary-layout.d.mts +35 -4
- package/types/engine/dictionary-rows.d.mts +9 -0
- package/types/engine/foundry-entries.d.mts +3 -1
- package/types/engine/heading-attributes.d.mts +2 -2
- package/types/engine/helpers.d.mts +11 -5
- package/types/engine/language-terms.d.mts +7 -0
- package/types/engine/lexical-sections.d.mts +95 -0
- package/types/engine/markdownlint-run.d.mts +3 -1
- package/types/engine/markup-validation.d.mts +14 -0
- package/types/engine/note-addresses.d.mts +10 -0
- package/types/engine/note-claims.d.mts +7 -1
- package/types/engine/note-package.d.mts +3 -1
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/note-vocabulary.d.mts +1 -0
- package/types/engine/related-pages.d.mts +10 -1
- package/types/engine/see-also.d.mts +55 -0
- package/types/engine/site-build.d.mts +4 -1
- package/types/engine/sql-tables.d.mts +40 -6
- package/types/engine/word-rows.d.mts +9 -7
package/e2e.mjs
CHANGED
|
@@ -43,6 +43,8 @@ import crypto from "node:crypto";
|
|
|
43
43
|
import { readdirSync, statSync } from "node:fs";
|
|
44
44
|
import fs from "node:fs/promises";
|
|
45
45
|
import path from "node:path";
|
|
46
|
+
import os from "node:os";
|
|
47
|
+
import { Browser, BrowserPlatform, computeExecutablePath, install } from "@puppeteer/browsers";
|
|
46
48
|
import process from "node:process";
|
|
47
49
|
import { spawnSync } from "node:child_process";
|
|
48
50
|
|
|
@@ -796,6 +798,91 @@ export function suiteExecutables(command) {
|
|
|
796
798
|
return [program];
|
|
797
799
|
}
|
|
798
800
|
|
|
801
|
+
/** The Chrome for Testing build shared by local and CI Cypress runs. */
|
|
802
|
+
export const CHROME_BUILD_ID = "155.0.8059.84";
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* The browser cache survives reseeding and container teardown.
|
|
806
|
+
* @param {NodeJS.ProcessEnv} [env] - Environment overrides.
|
|
807
|
+
* @returns {string} The browser cache directory.
|
|
808
|
+
*/
|
|
809
|
+
export function browserCacheDir(env = process.env) {
|
|
810
|
+
if (env.HL_BROWSER_CACHE) return path.resolve(env.HL_BROWSER_CACHE);
|
|
811
|
+
if (env.FOUNDRYVTT_CACHE) return path.resolve(env.FOUNDRYVTT_CACHE, "..", "browser-cache");
|
|
812
|
+
if (env.HL_E2E_ROOT) return path.resolve(env.HL_E2E_ROOT, "browser-cache");
|
|
813
|
+
const cache =
|
|
814
|
+
env.XDG_CACHE_HOME ||
|
|
815
|
+
(process.platform === "darwin" ?
|
|
816
|
+
path.join(os.homedir(), "Library", "Caches")
|
|
817
|
+
: path.join(os.homedir(), ".cache"));
|
|
818
|
+
return path.join(cache, "heroiclands", "chrome-for-testing");
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Supply pinned Chrome only to a recognizable Cypress command without a browser.
|
|
823
|
+
* Explicit browser arguments remain verbatim, including Electron and paths.
|
|
824
|
+
* @param {object} opts
|
|
825
|
+
* @param {readonly string[]} opts.command - The configured suite command.
|
|
826
|
+
* @param {string[]} [opts.args] - Extra suite arguments.
|
|
827
|
+
* @param {NodeJS.ProcessEnv} [opts.env] - Cache overrides.
|
|
828
|
+
* @param {string} [opts.platform] - Host operating system.
|
|
829
|
+
* @param {string} [opts.arch] - Host architecture.
|
|
830
|
+
* @param {(message: string) => void} [opts.log] - Progress reporting.
|
|
831
|
+
* @returns {Promise<string[]>} Arguments to append to the suite command.
|
|
832
|
+
*/
|
|
833
|
+
export async function prepareSuiteBrowser({
|
|
834
|
+
command,
|
|
835
|
+
args = [],
|
|
836
|
+
env = process.env,
|
|
837
|
+
platform = process.platform,
|
|
838
|
+
arch = process.arch,
|
|
839
|
+
log = () => {},
|
|
840
|
+
}) {
|
|
841
|
+
const tool = suiteExecutables(command).at(-1);
|
|
842
|
+
if (!tool || executableName(tool) !== "cypress") return args;
|
|
843
|
+
const all = [...command, ...args];
|
|
844
|
+
const explicit = all.findLastIndex(
|
|
845
|
+
(arg) => arg === "--browser" || arg.startsWith("--browser="),
|
|
846
|
+
);
|
|
847
|
+
if (explicit !== -1) {
|
|
848
|
+
const selected =
|
|
849
|
+
all[explicit] === "--browser" ?
|
|
850
|
+
all[explicit + 1]
|
|
851
|
+
: all[explicit].slice("--browser=".length);
|
|
852
|
+
log(
|
|
853
|
+
`▸ Cypress browser: ${selected ?? "(missing --browser value)"} (explicit; version reported by Cypress)`,
|
|
854
|
+
);
|
|
855
|
+
return args;
|
|
856
|
+
}
|
|
857
|
+
const browserPlatform =
|
|
858
|
+
platform === "darwin" && arch === "arm64" ? BrowserPlatform.MAC_ARM
|
|
859
|
+
: platform === "linux" && arch === "x64" ? BrowserPlatform.LINUX
|
|
860
|
+
: null;
|
|
861
|
+
if (!browserPlatform)
|
|
862
|
+
throw new Error(
|
|
863
|
+
`Chrome for Testing has no configured build for ${platform}/${arch}. Pass --browser <name-or-path> to select an installed browser.`,
|
|
864
|
+
);
|
|
865
|
+
const options = {
|
|
866
|
+
browser: Browser.CHROME,
|
|
867
|
+
buildId: CHROME_BUILD_ID,
|
|
868
|
+
platform: browserPlatform,
|
|
869
|
+
cacheDir: browserCacheDir(env),
|
|
870
|
+
};
|
|
871
|
+
let executablePath = computeExecutablePath(options);
|
|
872
|
+
if (!isExecutableFile(executablePath)) {
|
|
873
|
+
try {
|
|
874
|
+
executablePath = (await install(options)).executablePath;
|
|
875
|
+
} catch (cause) {
|
|
876
|
+
throw new Error(
|
|
877
|
+
`Cannot install Chrome for Testing ${CHROME_BUILD_ID} for ${platform}/${arch} in ${options.cacheDir}. Check network access or pass --browser <name-or-path>.`,
|
|
878
|
+
{ cause },
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
log(`▸ Cypress browser: Chrome for Testing ${CHROME_BUILD_ID} (${executablePath})`);
|
|
883
|
+
return [...args, "--browser", executablePath];
|
|
884
|
+
}
|
|
885
|
+
|
|
799
886
|
/**
|
|
800
887
|
* @param {string} candidate - An absolute path.
|
|
801
888
|
* @returns {boolean} Whether it is there and is a file.
|
|
@@ -1128,7 +1215,7 @@ export async function e2eRun({
|
|
|
1128
1215
|
});
|
|
1129
1216
|
return runSuite({
|
|
1130
1217
|
command,
|
|
1131
|
-
args: suiteArgs,
|
|
1218
|
+
args: await prepareSuiteBrowser({ command, args: suiteArgs, env, log }),
|
|
1132
1219
|
cwd: config.rootDir,
|
|
1133
1220
|
// Only a headless run makes a claim worth checking. `open` hands
|
|
1134
1221
|
// the runner to a person, who decides what to execute and when to
|
|
@@ -1211,7 +1298,7 @@ export async function e2eFast({ config, argv = [], env = process.env, log = () =
|
|
|
1211
1298
|
}
|
|
1212
1299
|
return runSuite({
|
|
1213
1300
|
command,
|
|
1214
|
-
args: suiteArgs,
|
|
1301
|
+
args: await prepareSuiteBrowser({ command, args: suiteArgs, env, log }),
|
|
1215
1302
|
cwd: config.rootDir,
|
|
1216
1303
|
results: config.e2eResults,
|
|
1217
1304
|
env,
|
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
|
/**
|
|
@@ -71,25 +70,13 @@ export const ANCHOR_KINDS = Object.freeze(["prose", "event"]);
|
|
|
71
70
|
*/
|
|
72
71
|
export function collectAnchors(body, bodyLine = 1, resolveRole) {
|
|
73
72
|
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;
|
|
73
|
+
for (const heading of scanHeadingAttributes(body, bodyLine).headings) {
|
|
74
|
+
if (!heading.id) continue;
|
|
88
75
|
anchors.push({
|
|
89
|
-
slug:
|
|
90
|
-
name:
|
|
91
|
-
level: heading
|
|
92
|
-
line:
|
|
76
|
+
slug: heading.id,
|
|
77
|
+
name: heading.text,
|
|
78
|
+
level: heading.level,
|
|
79
|
+
line: heading.line,
|
|
93
80
|
kind: "prose",
|
|
94
81
|
});
|
|
95
82
|
}
|
package/engine/base-compiler.mjs
CHANGED
|
@@ -1254,19 +1254,18 @@ export class BasePackCompiler {
|
|
|
1254
1254
|
}
|
|
1255
1255
|
}
|
|
1256
1256
|
|
|
1257
|
-
//
|
|
1258
|
-
//
|
|
1259
|
-
|
|
1260
|
-
// nor the `bodyLine`/`bodyColumn` a diagnostic needs, and this pass has
|
|
1261
|
-
// to have the body anyway. So the read is one this pass makes anyway;
|
|
1262
|
-
// the index decides which files it covers.
|
|
1257
|
+
// Parsed source and body coordinates belong to the shared corpus.
|
|
1258
|
+
// A directly supplied corpus can fill missing entries on first use.
|
|
1259
|
+
this.corpus.parsed ??= new Map();
|
|
1263
1260
|
const dates = reckoningContext({ notes: this.corpus.records });
|
|
1264
1261
|
/** The note first filed under each document id in this pass. @type {Map<string, string>} */
|
|
1265
1262
|
const idOwners = new Map();
|
|
1266
1263
|
for (const record of this.corpus.records) {
|
|
1267
1264
|
if (!isNoteRecord(record)) continue;
|
|
1268
1265
|
const absPath = noteFile(this.contentBase, record);
|
|
1269
|
-
const
|
|
1266
|
+
const parsed = this.corpus.parsed.get(absPath) ?? parseMarkdownFile(absPath);
|
|
1267
|
+
this.corpus.parsed.set(absPath, parsed);
|
|
1268
|
+
const { body, bodyLine, bodyColumn, raw } = parsed;
|
|
1270
1269
|
const fm = cloneAddressState(authoredFrontmatter(record));
|
|
1271
1270
|
// Which note this pass is on, so anything it calls can report a
|
|
1272
1271
|
// position without every method having to be handed one.
|
|
@@ -1284,7 +1283,7 @@ export class BasePackCompiler {
|
|
|
1284
1283
|
// comes from the diagnostic locator, so the message must not
|
|
1285
1284
|
// repeat it.
|
|
1286
1285
|
try {
|
|
1287
|
-
assertNoDeclaredPackage(fm, { absPath });
|
|
1286
|
+
assertNoDeclaredPackage(fm, { absPath, raw });
|
|
1288
1287
|
} catch (err) {
|
|
1289
1288
|
stats.declined++;
|
|
1290
1289
|
this.errorCount++;
|
|
@@ -1375,7 +1374,7 @@ export class BasePackCompiler {
|
|
|
1375
1374
|
`${path.relative(process.cwd(), owner)}. Give one of the two notes a ` +
|
|
1376
1375
|
`different \`id\` under \`data:\`, or remove it to take the id derived ` +
|
|
1377
1376
|
`from its address`,
|
|
1378
|
-
locateFrontmatterKey(absPath, "id"),
|
|
1377
|
+
locateFrontmatterKey(absPath, "id", undefined, { raw }),
|
|
1379
1378
|
);
|
|
1380
1379
|
continue;
|
|
1381
1380
|
}
|
|
@@ -1398,15 +1397,9 @@ export class BasePackCompiler {
|
|
|
1398
1397
|
);
|
|
1399
1398
|
const position =
|
|
1400
1399
|
err.position ??
|
|
1401
|
-
(err.keyPath ?
|
|
1402
|
-
|
|
1403
|
-
:
|
|
1404
|
-
positionOfLiteral(
|
|
1405
|
-
fs.readFileSync(absPath, "utf8"),
|
|
1406
|
-
`{#${repeatedAnchor[1]}}`,
|
|
1407
|
-
2,
|
|
1408
|
-
)
|
|
1409
|
-
: undefined);
|
|
1400
|
+
(err.keyPath ? positionOfFrontmatterPath(raw, err.keyPath)
|
|
1401
|
+
: repeatedAnchor ? positionOfLiteral(raw, `{#${repeatedAnchor[1]}}`, 2)
|
|
1402
|
+
: undefined);
|
|
1410
1403
|
// `position` is set by whatever failed if it knew where — an
|
|
1411
1404
|
// unresolved address, a bad table directive — so the report
|
|
1412
1405
|
// points at the line rather than at the note.
|
|
@@ -60,7 +60,7 @@ import { prepareTreeSqlTables } from "./sql-tables.mjs";
|
|
|
60
60
|
* created when none is passed, and returned on the corpus either way — a note
|
|
61
61
|
* the index refuses is a note the compile must still *report*, exactly as the
|
|
62
62
|
* compile loop reported it when the loop was the first to see it.
|
|
63
|
-
* @returns {Promise<{records: object[], linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[]}>} The corpus,
|
|
63
|
+
* @returns {Promise<{records: object[], parsed: Map<string, object>, linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[]}>} The corpus,
|
|
64
64
|
* its indexes, and the notes it could not record.
|
|
65
65
|
*/
|
|
66
66
|
export async function buildCompileCorpus({
|
|
@@ -84,10 +84,12 @@ export async function buildCompileCorpus({
|
|
|
84
84
|
});
|
|
85
85
|
const corpusOptions = { config: resolved, records };
|
|
86
86
|
const sqlOptions = { ...corpusOptions, skipDirectories, parsed };
|
|
87
|
+
const sqlTables = await prepareTreeSqlTables(contentBase, sqlOptions);
|
|
87
88
|
return {
|
|
88
89
|
records,
|
|
90
|
+
parsed,
|
|
89
91
|
problems: collected,
|
|
90
|
-
linkIndex: buildContentLinkIndex(contentBase, router, corpusOptions),
|
|
91
|
-
sqlTables
|
|
92
|
+
linkIndex: buildContentLinkIndex(contentBase, router, { ...corpusOptions, sqlTables }),
|
|
93
|
+
sqlTables,
|
|
92
94
|
};
|
|
93
95
|
}
|
|
@@ -48,7 +48,7 @@ export const ALERT_TYPES = Object.freeze({
|
|
|
48
48
|
),
|
|
49
49
|
},
|
|
50
50
|
});
|
|
51
|
-
const parser = new MarkdownIt();
|
|
51
|
+
const parser = new MarkdownIt({ html: true });
|
|
52
52
|
const titleParser = new MarkdownIt({ html: false });
|
|
53
53
|
const marker = /^ {0,3}>[ \t]?\[!([^\]\n]+)\](.*)$/;
|
|
54
54
|
/** Read alerts outside code examples, retaining inclusive source line ranges.
|
|
@@ -85,7 +85,9 @@ export function scanAlerts(source) {
|
|
|
85
85
|
if (match[1] !== match[1].toUpperCase() || !Object.hasOwn(ALERT_TYPES, type)) {
|
|
86
86
|
errors.push({
|
|
87
87
|
...at,
|
|
88
|
-
message: `unknown alert type ${match[1]}; use
|
|
88
|
+
message: `unknown alert type ${match[1]}; use ${Object.keys(ALERT_TYPES)
|
|
89
|
+
.map((name) => name.toUpperCase())
|
|
90
|
+
.join(", ")}`,
|
|
89
91
|
});
|
|
90
92
|
continue;
|
|
91
93
|
}
|
|
@@ -62,6 +62,7 @@ import { WITHHELD_CLASS, parseHeadingLine, withheldSections } from "./heading-at
|
|
|
62
62
|
* and a stray `<section>` would close the block early.
|
|
63
63
|
*/
|
|
64
64
|
const titleParser = new MarkdownIt({ html: false });
|
|
65
|
+
const structureParser = new MarkdownIt({ html: true });
|
|
65
66
|
|
|
66
67
|
/** The blocks an author may open, with the heading each takes by default. */
|
|
67
68
|
export const BLOCK_NAMES = Object.freeze({
|
|
@@ -99,28 +100,24 @@ function escapeAttribute(value) {
|
|
|
99
100
|
* @returns {{blocks: Array<{name: string, title: string, id: string, classes: string[], attributes: Record<string, string>, start: number, end: number, body: string}>, errors: Array<{line: number, column: number, message: string}>}}
|
|
100
101
|
*/
|
|
101
102
|
export function scanBlocks(source) {
|
|
102
|
-
const lines = String(source ?? "").split(
|
|
103
|
+
const lines = String(source ?? "").split(/\r?\n/),
|
|
103
104
|
blocks = [],
|
|
104
105
|
errors = [];
|
|
105
106
|
const stack = [];
|
|
106
|
-
|
|
107
|
+
// A bare fence can belong to a quote through lazy continuation.
|
|
108
|
+
// Parser ranges decide whether it can delimit a container.
|
|
109
|
+
const literalLines = new Set();
|
|
110
|
+
for (const token of structureParser.parse(String(source ?? ""), {})) {
|
|
111
|
+
if (
|
|
112
|
+
!token.map ||
|
|
113
|
+
!["blockquote_open", "fence", "code_block", "html_block"].includes(token.type)
|
|
114
|
+
)
|
|
115
|
+
continue;
|
|
116
|
+
for (let line = token.map[0]; line < token.map[1]; line++) literalLines.add(line);
|
|
117
|
+
}
|
|
107
118
|
for (let i = 0; i < lines.length; i++) {
|
|
119
|
+
if (literalLines.has(i)) continue;
|
|
108
120
|
const line = lines[i];
|
|
109
|
-
const code = /^ *(`{3,}|~{3,})(.*)$/.exec(line);
|
|
110
|
-
if (fence) {
|
|
111
|
-
if (
|
|
112
|
-
code &&
|
|
113
|
-
code[1][0] === fence[0] &&
|
|
114
|
-
code[1].length >= fence.length &&
|
|
115
|
-
!code[2].trim()
|
|
116
|
-
)
|
|
117
|
-
fence = null;
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
if (code) {
|
|
121
|
-
fence = code[1];
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
121
|
const close = /^ *:::\s*$/.test(line);
|
|
125
122
|
const named = /^ *:::([A-Za-z][A-Za-z0-9-]*)(?:\s+(.*))?$/.exec(line);
|
|
126
123
|
const div = /^ *:::\s+(\{.*)$/.exec(line);
|
|
@@ -135,7 +132,7 @@ export function scanBlocks(source) {
|
|
|
135
132
|
if (!block.body)
|
|
136
133
|
errors.push({
|
|
137
134
|
line: block.start + 1,
|
|
138
|
-
column: 1,
|
|
135
|
+
column: lines[block.start].indexOf(":::") + 1,
|
|
139
136
|
message: `${block.name} block is empty`,
|
|
140
137
|
});
|
|
141
138
|
if (!stack.length) blocks.push(block);
|
|
@@ -144,7 +141,7 @@ export function scanBlocks(source) {
|
|
|
144
141
|
if (!close && !named && !div) continue;
|
|
145
142
|
const name = named?.[1] ?? "div",
|
|
146
143
|
raw = named?.[2] ?? div?.[1] ?? "";
|
|
147
|
-
const at = { line: i + 1, column: 1 };
|
|
144
|
+
const at = { line: i + 1, column: line.indexOf(":::") + 1 };
|
|
148
145
|
if (name !== "div" && !Object.hasOwn(BLOCK_NAMES, name)) {
|
|
149
146
|
errors.push({ ...at, message: `there is no ${name} block; the blocks are ${names()}` });
|
|
150
147
|
stack.push({ start: i, rejected: true });
|
|
@@ -189,27 +186,12 @@ export function scanBlocks(source) {
|
|
|
189
186
|
if (!block.rejected)
|
|
190
187
|
errors.push({
|
|
191
188
|
line: block.start + 1,
|
|
192
|
-
column: 1,
|
|
189
|
+
column: lines[block.start].indexOf(":::") + 1,
|
|
193
190
|
message: `${block.name} block needs a closing ::: line`,
|
|
194
191
|
});
|
|
195
192
|
for (const block of blocks) {
|
|
196
|
-
let literal = null;
|
|
197
193
|
for (let i = block.start + 1; i < block.end; i++) {
|
|
198
|
-
|
|
199
|
-
if (literal) {
|
|
200
|
-
if (
|
|
201
|
-
marker &&
|
|
202
|
-
marker[1][0] === literal[0] &&
|
|
203
|
-
marker[1].length >= literal.length &&
|
|
204
|
-
!marker[2].trim()
|
|
205
|
-
)
|
|
206
|
-
literal = null;
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
if (marker) {
|
|
210
|
-
literal = marker[1];
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
194
|
+
if (literalLines.has(i)) continue;
|
|
213
195
|
if (parseHeadingLine(lines[i])?.startsPage)
|
|
214
196
|
errors.push({
|
|
215
197
|
line: i + 1,
|