@heroiclands/package-build 8.0.0 → 9.0.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 +173 -0
- package/bin/content-build.mjs +44 -118
- package/bin/package-build.mjs +27 -69
- package/bin/report.mjs +1 -2
- package/bundle.mjs +2 -10
- package/config.mjs +31 -106
- package/container.mjs +13 -57
- package/content-config.mjs +45 -164
- package/coverage.mjs +14 -55
- package/deploy.mjs +4 -13
- package/e2e.mjs +16 -55
- package/engine/address-diff.mjs +1 -4
- package/engine/base-compiler.mjs +10 -28
- package/engine/code-fences.mjs +4 -13
- package/engine/compendiums.mjs +13 -37
- package/engine/content-address.mjs +2 -6
- package/engine/content-links.mjs +13 -44
- package/engine/content-lint.mjs +4 -15
- package/engine/content-slug.mjs +2 -6
- package/engine/content-tables.mjs +26 -79
- package/engine/diagnostics.mjs +4 -15
- package/engine/field-reference.mjs +6 -20
- package/engine/field-spec.mjs +1 -3
- package/engine/foreign-catalog.mjs +7 -22
- package/engine/foreign-manifests.mjs +1 -4
- package/engine/frontmatter-lint.mjs +6 -18
- package/engine/frontmatter.mjs +3 -8
- package/engine/generate.mjs +4 -16
- package/engine/helpers.mjs +13 -52
- package/engine/homepage.mjs +4 -15
- package/engine/ids.mjs +3 -12
- package/engine/item-registry.mjs +2 -6
- package/engine/journals.mjs +4 -14
- package/engine/kb-manifest.mjs +5 -17
- package/engine/macros.mjs +2 -10
- package/engine/manifest-emit.mjs +6 -17
- package/engine/map-notes.mjs +19 -69
- package/engine/note-package.mjs +1 -4
- package/engine/pack-config.mjs +17 -38
- package/engine/pack-router.mjs +1 -2
- package/engine/prose-config.mjs +20 -4
- package/engine/prose-lint.mjs +6 -14
- package/engine/region-events.mjs +1 -3
- package/engine/scene-levels.mjs +8 -22
- package/engine/scenes.mjs +13 -47
- package/engine/schema-check.mjs +1 -4
- package/engine/schema-extract.mjs +70 -45
- package/engine/site-build.mjs +12 -37
- package/engine/site-index.mjs +3 -15
- package/engine/web-wikilinks.mjs +4 -13
- package/engine/wikilinks.mjs +115 -167
- package/index.mjs +1 -5
- package/lang.mjs +1 -3
- package/manifest.mjs +10 -37
- package/markdownlint-config.mjs +1 -5
- package/package.json +1 -1
- package/sohl/actors.mjs +10 -40
- package/sohl/being-info.mjs +3 -6
- package/sohl/index.mjs +1 -6
- package/sohl/item-builders.mjs +1 -3
- package/sohl/item-fields.mjs +16 -34
- package/sohl/items.mjs +1 -3
- package/sohl/kb-passes.mjs +29 -39
- package/sohl/skill-base.mjs +7 -23
- package/stage.mjs +3 -13
- package/templates.mjs +4 -15
- package/types/bundle.d.mts +1 -1
- package/types/container.d.mts +2 -2
- package/types/coverage.d.mts +1 -1
- package/types/e2e.d.mts +4 -4
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +1 -1
- package/types/engine/schema-extract.d.mts +1 -1
- package/types/engine/site-index.d.mts +1 -1
- package/types/manifest.d.mts +1 -1
package/engine/content-links.mjs
CHANGED
|
@@ -99,18 +99,12 @@ export function anchorsOf(body) {
|
|
|
99
99
|
* @param {readonly string[]} [opts.skipDirectories] - Passed to the walk.
|
|
100
100
|
* @returns {object} The notes, the index, and the resolvers built over it.
|
|
101
101
|
*/
|
|
102
|
-
export function buildLinkIndex(
|
|
103
|
-
contentBase,
|
|
104
|
-
{ manifestDir, skipDirectories } = {},
|
|
105
|
-
) {
|
|
102
|
+
export function buildLinkIndex(contentBase, { manifestDir, skipDirectories } = {}) {
|
|
106
103
|
const notes = [];
|
|
107
104
|
const frontmatterLinks = [];
|
|
108
105
|
const walkOpts = skipDirectories ? { skipDirectories } : undefined;
|
|
109
106
|
|
|
110
|
-
for (const { frontmatter: fm, absPath } of walkMarkdownTree(
|
|
111
|
-
contentBase,
|
|
112
|
-
walkOpts,
|
|
113
|
-
)) {
|
|
107
|
+
for (const { frontmatter: fm, absPath } of walkMarkdownTree(contentBase, walkOpts)) {
|
|
114
108
|
if (!fm || typeof fm.type !== "string") continue;
|
|
115
109
|
// The raw text is kept beside the parsed body: a consumer's own checks
|
|
116
110
|
// may need what frontmatter carried, which the body has dropped.
|
|
@@ -182,10 +176,7 @@ export function buildLinkIndex(
|
|
|
182
176
|
: { index: new Map(), packages: new Set(), stale: [] };
|
|
183
177
|
for (const v of foreign.index.values()) if (v.type) types.add(v.type);
|
|
184
178
|
|
|
185
|
-
const packages = new Set([
|
|
186
|
-
...(byKey.size ? [pkg] : []),
|
|
187
|
-
...foreign.packages,
|
|
188
|
-
]);
|
|
179
|
+
const packages = new Set([...(byKey.size ? [pkg] : []), ...foreign.packages]);
|
|
189
180
|
|
|
190
181
|
/** The searchable universe a `dataview` table draws its rows from. */
|
|
191
182
|
const tableDocs = notes.map((n) => ({
|
|
@@ -223,10 +214,7 @@ export function buildLinkIndex(
|
|
|
223
214
|
const seen = new Map();
|
|
224
215
|
// Code is verbatim, so a `[[…]]` inside a fence, an indented block or
|
|
225
216
|
// an inline span is not a link — the compilers make none of it either.
|
|
226
|
-
for (const [all, rawInner] of matchAllOutsideCode(
|
|
227
|
-
body,
|
|
228
|
-
new RegExp(WIKILINK.source, "g"),
|
|
229
|
-
)) {
|
|
217
|
+
for (const [all, rawInner] of matchAllOutsideCode(body, new RegExp(WIKILINK.source, "g"))) {
|
|
230
218
|
const { target, anchor } = parseWikilink(rawInner);
|
|
231
219
|
const occurrence = (seen.get(all) ?? 0) + 1;
|
|
232
220
|
seen.set(all, occurrence);
|
|
@@ -258,18 +246,13 @@ export function buildLinkIndex(
|
|
|
258
246
|
*/
|
|
259
247
|
function resolve(note, target) {
|
|
260
248
|
const direct =
|
|
261
|
-
byAlias.get(`${note.type}|${target}`.toLowerCase()) ??
|
|
262
|
-
byKey.get(target.toLowerCase());
|
|
249
|
+
byAlias.get(`${note.type}|${target}`.toLowerCase()) ?? byKey.get(target.toLowerCase());
|
|
263
250
|
if (direct) return direct;
|
|
264
251
|
const qualified = readQualifier(target, types, packages);
|
|
265
252
|
if (!qualified || qualified.reason) return undefined;
|
|
266
253
|
return byKey.get(
|
|
267
254
|
qualified.package ?
|
|
268
|
-
canonicalKey(
|
|
269
|
-
qualified.package,
|
|
270
|
-
qualified.type,
|
|
271
|
-
qualified.shortcode,
|
|
272
|
-
)
|
|
255
|
+
canonicalKey(qualified.package, qualified.type, qualified.shortcode)
|
|
273
256
|
: `${qualified.type}/${qualified.shortcode}`.toLowerCase(),
|
|
274
257
|
);
|
|
275
258
|
}
|
|
@@ -284,11 +267,7 @@ export function buildLinkIndex(
|
|
|
284
267
|
const q = readQualifier(target, types, packages);
|
|
285
268
|
if (!q || q.reason) return null;
|
|
286
269
|
if (q.package) {
|
|
287
|
-
return (
|
|
288
|
-
foreign.index.get(
|
|
289
|
-
canonicalKey(q.package, q.type, q.shortcode),
|
|
290
|
-
) ?? null
|
|
291
|
-
);
|
|
270
|
+
return foreign.index.get(canonicalKey(q.package, q.type, q.shortcode)) ?? null;
|
|
292
271
|
}
|
|
293
272
|
// A bare address names no package, so it resolves against any foreign
|
|
294
273
|
// one that publishes it. Claimed by two, it is ambiguous and the author
|
|
@@ -397,8 +376,7 @@ function landingTarget(url, bases) {
|
|
|
397
376
|
}
|
|
398
377
|
if (!/^https?:$/.test(parsed.protocol)) return null;
|
|
399
378
|
if (!SITE_HOST.test(parsed.hostname)) return null;
|
|
400
|
-
const pathname =
|
|
401
|
-
parsed.pathname.endsWith("/") ? parsed.pathname : `${parsed.pathname}/`;
|
|
379
|
+
const pathname = parsed.pathname.endsWith("/") ? parsed.pathname : `${parsed.pathname}/`;
|
|
402
380
|
for (const [pkg, base] of bases) {
|
|
403
381
|
if (pathname === base) return { pkg, base };
|
|
404
382
|
}
|
|
@@ -444,8 +422,7 @@ function readAddress(url, packages) {
|
|
|
444
422
|
segments = value.split("?")[0].split("#")[0].split("/").filter(Boolean);
|
|
445
423
|
}
|
|
446
424
|
|
|
447
|
-
const prefix =
|
|
448
|
-
shape !== "relative" && packages.has(segments[0]) ? segments[0] : null;
|
|
425
|
+
const prefix = shape !== "relative" && packages.has(segments[0]) ? segments[0] : null;
|
|
449
426
|
return { shape, segments, prefix };
|
|
450
427
|
}
|
|
451
428
|
|
|
@@ -533,10 +510,7 @@ export function auditHomepageLinks(index) {
|
|
|
533
510
|
);
|
|
534
511
|
}
|
|
535
512
|
|
|
536
|
-
for (const { field, url, kind } of homepageAddresses(
|
|
537
|
-
note.fm,
|
|
538
|
-
note.body,
|
|
539
|
-
)) {
|
|
513
|
+
for (const { field, url, kind } of homepageAddresses(note.fm, note.body)) {
|
|
540
514
|
// Counted for every address, checked or not, so the count is
|
|
541
515
|
// the literal's nth appearance in the file rather than the nth
|
|
542
516
|
// *finding* about it — two rules can fire on one address.
|
|
@@ -583,8 +557,7 @@ export function auditHomepageLinks(index) {
|
|
|
583
557
|
`relocation does not leave this page behind`,
|
|
584
558
|
);
|
|
585
559
|
} else if (shape === "rooted" && kind === "url") {
|
|
586
|
-
const rest =
|
|
587
|
-
prefix ? segments.slice(1).join("/") : segments.join("/");
|
|
560
|
+
const rest = prefix ? segments.slice(1).join("/") : segments.join("/");
|
|
588
561
|
report(
|
|
589
562
|
field,
|
|
590
563
|
url,
|
|
@@ -596,9 +569,7 @@ export function auditHomepageLinks(index) {
|
|
|
596
569
|
// `href:` is the field for an address already resolved.
|
|
597
570
|
!rest ?
|
|
598
571
|
`url "${url}" addresses ` +
|
|
599
|
-
(prefix ?
|
|
600
|
-
`package "${prefix}"'s landing`
|
|
601
|
-
: `the site root`) +
|
|
572
|
+
(prefix ? `package "${prefix}"'s landing` : `the site root`) +
|
|
602
573
|
`, but a landing's url: is package-relative and ` +
|
|
603
574
|
`cannot leave this package — write ` +
|
|
604
575
|
`href: "${url}", which is used verbatim`
|
|
@@ -737,9 +708,7 @@ export function auditLinks(index) {
|
|
|
737
708
|
export function walkReachability(index, { root, scope, stopAt = () => false }) {
|
|
738
709
|
const rootNote = index.notes.find((n) => n.rel === root);
|
|
739
710
|
if (!rootNote) {
|
|
740
|
-
throw new Error(
|
|
741
|
-
`no note at ${root}, so the corpus has no page to be read from`,
|
|
742
|
-
);
|
|
711
|
+
throw new Error(`no note at ${root}, so the corpus has no page to be read from`);
|
|
743
712
|
}
|
|
744
713
|
|
|
745
714
|
const reached = new Set([rootNote]);
|
package/engine/content-lint.mjs
CHANGED
|
@@ -106,10 +106,7 @@ export function isValidShortcode(value) {
|
|
|
106
106
|
function collectNotes(contentBase, { skipDirectories } = {}) {
|
|
107
107
|
const notes = [];
|
|
108
108
|
const walkOpts = skipDirectories ? { skipDirectories } : undefined;
|
|
109
|
-
for (const { frontmatter: fm, absPath } of walkMarkdownTree(
|
|
110
|
-
contentBase,
|
|
111
|
-
walkOpts,
|
|
112
|
-
)) {
|
|
109
|
+
for (const { frontmatter: fm, absPath } of walkMarkdownTree(contentBase, walkOpts)) {
|
|
113
110
|
if (!fm || !fm.type) continue;
|
|
114
111
|
notes.push({
|
|
115
112
|
fm,
|
|
@@ -134,10 +131,7 @@ function collectNotes(contentBase, { skipDirectories } = {}) {
|
|
|
134
131
|
* severity: "error"|"warning", message: string}>, notes: number,
|
|
135
132
|
* keys: number}} The findings, and what was inspected to produce them.
|
|
136
133
|
*/
|
|
137
|
-
export function lintContentTree(
|
|
138
|
-
contentBase,
|
|
139
|
-
{ skipDirectories, contentPackage } = {},
|
|
140
|
-
) {
|
|
134
|
+
export function lintContentTree(contentBase, { skipDirectories, contentPackage } = {}) {
|
|
141
135
|
const findings = [];
|
|
142
136
|
const notes = collectNotes(contentBase, { skipDirectories });
|
|
143
137
|
|
|
@@ -211,15 +205,10 @@ export function lintContentTree(
|
|
|
211
205
|
// is a place an author has to go and edit, and a finding naming only
|
|
212
206
|
// the key sends them hunting for the other one.
|
|
213
207
|
for (const { file, absPath } of files) {
|
|
214
|
-
const others = files
|
|
215
|
-
.filter((f) => f.file !== file)
|
|
216
|
-
.map((f) => f.file);
|
|
208
|
+
const others = files.filter((f) => f.file !== file).map((f) => f.file);
|
|
217
209
|
findings.push({
|
|
218
210
|
file,
|
|
219
|
-
...positionInFrontmatter(
|
|
220
|
-
fs.readFileSync(absPath, "utf8"),
|
|
221
|
-
"shortcode",
|
|
222
|
-
),
|
|
211
|
+
...positionInFrontmatter(fs.readFileSync(absPath, "utf8"), "shortcode"),
|
|
223
212
|
severity: "error",
|
|
224
213
|
message:
|
|
225
214
|
`duplicate address "${key}", also declared by ` +
|
package/engine/content-slug.mjs
CHANGED
|
@@ -133,13 +133,9 @@ export function contentSlug(name) {
|
|
|
133
133
|
throw new Error("content note has no name, so it has no URL");
|
|
134
134
|
}
|
|
135
135
|
const normalised = slugify(raw);
|
|
136
|
-
const slug = abbreviateTokens(normalised.split("-").filter(Boolean)).join(
|
|
137
|
-
"-",
|
|
138
|
-
);
|
|
136
|
+
const slug = abbreviateTokens(normalised.split("-").filter(Boolean)).join("-");
|
|
139
137
|
if (!slug) {
|
|
140
|
-
throw new Error(
|
|
141
|
-
`name "${raw}" has no URL-safe characters, so it cannot address a page`,
|
|
142
|
-
);
|
|
138
|
+
throw new Error(`name "${raw}" has no URL-safe characters, so it cannot address a page`);
|
|
143
139
|
}
|
|
144
140
|
return slug;
|
|
145
141
|
}
|
|
@@ -199,13 +199,7 @@ function tokenize(source) {
|
|
|
199
199
|
const CLAUSE_RANK = { TABLE: 0, FROM: 1, WHERE: 2, SORT: 3, LIMIT: 4 };
|
|
200
200
|
|
|
201
201
|
/** Query types and data commands that are recognised only to be refused. */
|
|
202
|
-
const UNSUPPORTED_CLAUSES = new Set([
|
|
203
|
-
"LIST",
|
|
204
|
-
"TASK",
|
|
205
|
-
"CALENDAR",
|
|
206
|
-
"GROUP",
|
|
207
|
-
"FLATTEN",
|
|
208
|
-
]);
|
|
202
|
+
const UNSUPPORTED_CLAUSES = new Set(["LIST", "TASK", "CALENDAR", "GROUP", "FLATTEN"]);
|
|
209
203
|
|
|
210
204
|
/**
|
|
211
205
|
* Group a query's tokens by clause.
|
|
@@ -227,21 +221,15 @@ function splitClauses(tokens) {
|
|
|
227
221
|
if (token.value === "(" || token.value === "[") depth++;
|
|
228
222
|
else if (token.value === ")" || token.value === "]") depth--;
|
|
229
223
|
}
|
|
230
|
-
const word =
|
|
231
|
-
token.kind === "ident" ? token.value.toUpperCase() : undefined;
|
|
224
|
+
const word = token.kind === "ident" ? token.value.toUpperCase() : undefined;
|
|
232
225
|
if (depth === 0 && word && UNSUPPORTED_CLAUSES.has(word)) {
|
|
233
226
|
// `GROUP BY` is two words; report it the way an author wrote it.
|
|
234
227
|
const label =
|
|
235
|
-
(
|
|
236
|
-
word === "GROUP" &&
|
|
237
|
-
tokens[i + 1]?.value?.toUpperCase?.() === "BY"
|
|
238
|
-
) ?
|
|
228
|
+
word === "GROUP" && tokens[i + 1]?.value?.toUpperCase?.() === "BY" ?
|
|
239
229
|
"GROUP BY"
|
|
240
230
|
: word;
|
|
241
231
|
if (current === null) {
|
|
242
|
-
throw new Error(
|
|
243
|
-
`only TABLE queries are supported; this block is a ${label} query`,
|
|
244
|
-
);
|
|
232
|
+
throw new Error(`only TABLE queries are supported; this block is a ${label} query`);
|
|
245
233
|
}
|
|
246
234
|
throw new Error(`the ${label} clause is not supported`);
|
|
247
235
|
}
|
|
@@ -333,25 +321,19 @@ class ExprParser {
|
|
|
333
321
|
/** Is the next token this operator/punctuation? */
|
|
334
322
|
at(value) {
|
|
335
323
|
const t = this.peek();
|
|
336
|
-
return (
|
|
337
|
-
(t.kind === "op" || t.kind === "punct") && t.value === String(value)
|
|
338
|
-
);
|
|
324
|
+
return (t.kind === "op" || t.kind === "punct") && t.value === String(value);
|
|
339
325
|
}
|
|
340
326
|
|
|
341
327
|
/** Is the next token this (case-insensitive) bare word? */
|
|
342
328
|
atWord(word) {
|
|
343
329
|
const t = this.peek();
|
|
344
|
-
return (
|
|
345
|
-
t.kind === "ident" && t.value.toLowerCase() === word.toLowerCase()
|
|
346
|
-
);
|
|
330
|
+
return t.kind === "ident" && t.value.toLowerCase() === word.toLowerCase();
|
|
347
331
|
}
|
|
348
332
|
|
|
349
333
|
/** Consume the next token if it matches; report otherwise. */
|
|
350
334
|
expect(value) {
|
|
351
335
|
if (!this.at(value)) {
|
|
352
|
-
throw new Error(
|
|
353
|
-
`expected "${value}", got "${this.peek().text || "end of query"}"`,
|
|
354
|
-
);
|
|
336
|
+
throw new Error(`expected "${value}", got "${this.peek().text || "end of query"}"`);
|
|
355
337
|
}
|
|
356
338
|
return this.next();
|
|
357
339
|
}
|
|
@@ -394,10 +376,7 @@ class ExprParser {
|
|
|
394
376
|
parseComparison() {
|
|
395
377
|
const left = this.parseUnary();
|
|
396
378
|
const t = this.peek();
|
|
397
|
-
if (
|
|
398
|
-
t.kind === "op" &&
|
|
399
|
-
["=", "==", "!=", ">", ">=", "<", "<="].includes(t.value)
|
|
400
|
-
) {
|
|
379
|
+
if (t.kind === "op" && ["=", "==", "!=", ">", ">=", "<", "<="].includes(t.value)) {
|
|
401
380
|
this.next();
|
|
402
381
|
return {
|
|
403
382
|
kind: "compare",
|
|
@@ -502,9 +481,7 @@ class ExprParser {
|
|
|
502
481
|
}
|
|
503
482
|
return { kind: "field", path: t.value };
|
|
504
483
|
}
|
|
505
|
-
throw new Error(
|
|
506
|
-
`unexpected "${t.text || "end of query"}" in expression`,
|
|
507
|
-
);
|
|
484
|
+
throw new Error(`unexpected "${t.text || "end of query"}" in expression`);
|
|
508
485
|
}
|
|
509
486
|
|
|
510
487
|
/**
|
|
@@ -544,9 +521,7 @@ export function parseDataviewQuery(source) {
|
|
|
544
521
|
if (parser.atWord("without")) {
|
|
545
522
|
parser.next();
|
|
546
523
|
if (!parser.atWord("id")) {
|
|
547
|
-
throw new Error(
|
|
548
|
-
`expected "WITHOUT ID", got "WITHOUT ${parser.peek().text}"`,
|
|
549
|
-
);
|
|
524
|
+
throw new Error(`expected "WITHOUT ID", got "WITHOUT ${parser.peek().text}"`);
|
|
550
525
|
}
|
|
551
526
|
parser.next();
|
|
552
527
|
withoutId = true;
|
|
@@ -586,9 +561,7 @@ export function parseDataviewQuery(source) {
|
|
|
586
561
|
const fromParser = new ExprParser(clauses.get("FROM"), text);
|
|
587
562
|
from = fromParser.parseExpression();
|
|
588
563
|
if (!fromParser.done) {
|
|
589
|
-
throw new Error(
|
|
590
|
-
`unexpected "${fromParser.peek().text}" in the FROM clause`,
|
|
591
|
-
);
|
|
564
|
+
throw new Error(`unexpected "${fromParser.peek().text}" in the FROM clause`);
|
|
592
565
|
}
|
|
593
566
|
}
|
|
594
567
|
|
|
@@ -598,9 +571,7 @@ export function parseDataviewQuery(source) {
|
|
|
598
571
|
const whereParser = new ExprParser(clauses.get("WHERE"), text);
|
|
599
572
|
where = whereParser.parseExpression();
|
|
600
573
|
if (!whereParser.done) {
|
|
601
|
-
throw new Error(
|
|
602
|
-
`unexpected "${whereParser.peek().text}" in the WHERE clause`,
|
|
603
|
-
);
|
|
574
|
+
throw new Error(`unexpected "${whereParser.peek().text}" in the WHERE clause`);
|
|
604
575
|
}
|
|
605
576
|
}
|
|
606
577
|
|
|
@@ -614,10 +585,7 @@ export function parseDataviewQuery(source) {
|
|
|
614
585
|
if (sortParser.atWord("desc") || sortParser.atWord("descending")) {
|
|
615
586
|
sortParser.next();
|
|
616
587
|
descending = true;
|
|
617
|
-
} else if (
|
|
618
|
-
sortParser.atWord("asc") ||
|
|
619
|
-
sortParser.atWord("ascending")
|
|
620
|
-
) {
|
|
588
|
+
} else if (sortParser.atWord("asc") || sortParser.atWord("ascending")) {
|
|
621
589
|
sortParser.next();
|
|
622
590
|
}
|
|
623
591
|
sort.push({ expr, descending });
|
|
@@ -648,8 +616,7 @@ const makeLink = (display) => ({ __link: true, display: String(display) });
|
|
|
648
616
|
|
|
649
617
|
const isLink = (v) => Boolean(v) && typeof v === "object" && v.__link === true;
|
|
650
618
|
|
|
651
|
-
const isEmpty = (v) =>
|
|
652
|
-
v == null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
619
|
+
const isEmpty = (v) => v == null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
653
620
|
|
|
654
621
|
/** Dataview treats an absent, empty, zero, or false value as false. */
|
|
655
622
|
const truthy = (v) => {
|
|
@@ -813,9 +780,7 @@ function containsValue(haystack, needle, mode) {
|
|
|
813
780
|
if (haystack == null) return false;
|
|
814
781
|
if (Array.isArray(haystack)) {
|
|
815
782
|
return haystack.some((v) =>
|
|
816
|
-
mode === "exact" ?
|
|
817
|
-
looseEquals(v, needle)
|
|
818
|
-
: containsValue(v, needle, mode),
|
|
783
|
+
mode === "exact" ? looseEquals(v, needle) : containsValue(v, needle, mode),
|
|
819
784
|
);
|
|
820
785
|
}
|
|
821
786
|
if (typeof haystack === "object" && !isLink(haystack)) {
|
|
@@ -842,8 +807,7 @@ function toRegExp(pattern, anchored) {
|
|
|
842
807
|
|
|
843
808
|
/** The implementations behind {@link FUNCTIONS}. */
|
|
844
809
|
const CALLS = {
|
|
845
|
-
link: (args) =>
|
|
846
|
-
makeLink(args.length > 1 && !isEmpty(args[1]) ? asText(args[1]) : ""),
|
|
810
|
+
link: (args) => makeLink(args.length > 1 && !isEmpty(args[1]) ? asText(args[1]) : ""),
|
|
847
811
|
contains: ([a, b]) => containsValue(a, b, "loose"),
|
|
848
812
|
icontains: ([a, b]) => containsValue(a, b, "insensitive"),
|
|
849
813
|
econtains: ([a, b]) => containsValue(a, b, "exact"),
|
|
@@ -867,10 +831,8 @@ const CALLS = {
|
|
|
867
831
|
.filter((v) => !isEmpty(v))
|
|
868
832
|
.map(asText)
|
|
869
833
|
.join(sep === undefined ? ", " : asText(sep)),
|
|
870
|
-
regexmatch: ([pattern, value]) =>
|
|
871
|
-
|
|
872
|
-
regextest: ([pattern, value]) =>
|
|
873
|
-
toRegExp(pattern, false).test(asText(value)),
|
|
834
|
+
regexmatch: ([pattern, value]) => toRegExp(pattern, true).test(asText(value)),
|
|
835
|
+
regextest: ([pattern, value]) => toRegExp(pattern, false).test(asText(value)),
|
|
874
836
|
};
|
|
875
837
|
|
|
876
838
|
/**
|
|
@@ -980,11 +942,7 @@ function matchesSource(node, doc) {
|
|
|
980
942
|
const folder = value.replace(/\/+$/, "");
|
|
981
943
|
const path = doc.path ?? "";
|
|
982
944
|
if (folder === "" || folder === "/") return true;
|
|
983
|
-
return (
|
|
984
|
-
path === folder ||
|
|
985
|
-
path === `${folder}.md` ||
|
|
986
|
-
path.startsWith(`${folder}/`)
|
|
987
|
-
);
|
|
945
|
+
return path === folder || path === `${folder}.md` || path.startsWith(`${folder}/`);
|
|
988
946
|
}
|
|
989
947
|
default:
|
|
990
948
|
throw new Error(
|
|
@@ -1015,10 +973,7 @@ export function selectRows(spec, docs, self) {
|
|
|
1015
973
|
});
|
|
1016
974
|
matched.sort((a, b) => {
|
|
1017
975
|
for (const { expr, descending } of spec.sort) {
|
|
1018
|
-
const order = compareValues(
|
|
1019
|
-
evaluate(expr, a, self),
|
|
1020
|
-
evaluate(expr, b, self),
|
|
1021
|
-
);
|
|
976
|
+
const order = compareValues(evaluate(expr, a, self), evaluate(expr, b, self));
|
|
1022
977
|
if (order !== 0) return descending ? -order : order;
|
|
1023
978
|
}
|
|
1024
979
|
const byPath = compareValues(a.path, b.path);
|
|
@@ -1032,8 +987,7 @@ export function selectRows(spec, docs, self) {
|
|
|
1032
987
|
/* ------------------------------------------------------------------------ */
|
|
1033
988
|
|
|
1034
989
|
/** Escape the characters that would break out of a markdown table cell. */
|
|
1035
|
-
const escapeCell = (text) =>
|
|
1036
|
-
text.replace(/\|/g, "\\|").replace(/[\r\n]+/g, " ");
|
|
990
|
+
const escapeCell = (text) => text.replace(/\|/g, "\\|").replace(/[\r\n]+/g, " ");
|
|
1037
991
|
|
|
1038
992
|
/**
|
|
1039
993
|
* Render one evaluated value as table text.
|
|
@@ -1074,8 +1028,7 @@ export function renderContentTable(spec, rows, linkable, self) {
|
|
|
1074
1028
|
spec.columns.map((column) => {
|
|
1075
1029
|
const value = evaluate(column.expr, doc, self);
|
|
1076
1030
|
if (!isLink(value)) return formatValue(value, column.header);
|
|
1077
|
-
const display =
|
|
1078
|
-
value.display || asText(FILE_FIELDS.name(doc)) || EMPTY_CELL;
|
|
1031
|
+
const display = value.display || asText(FILE_FIELDS.name(doc)) || EMPTY_CELL;
|
|
1079
1032
|
const text = escapeCell(display);
|
|
1080
1033
|
if (!linkable(doc)) return text;
|
|
1081
1034
|
// A wikilink's own separator is a literal `|`, written `\|` inside a
|
|
@@ -1087,12 +1040,9 @@ export function renderContentTable(spec, rows, linkable, self) {
|
|
|
1087
1040
|
);
|
|
1088
1041
|
|
|
1089
1042
|
const align = spec.columns.map((_column, i) => {
|
|
1090
|
-
const shown = cells
|
|
1091
|
-
.map((row) => row[i])
|
|
1092
|
-
.filter((c) => c !== EMPTY_CELL);
|
|
1043
|
+
const shown = cells.map((row) => row[i]).filter((c) => c !== EMPTY_CELL);
|
|
1093
1044
|
const numeric =
|
|
1094
|
-
shown.length > 0 &&
|
|
1095
|
-
shown.every((c) => Number.isFinite(Number(c)) && c.trim() !== "");
|
|
1045
|
+
shown.length > 0 && shown.every((c) => Number.isFinite(Number(c)) && c.trim() !== "");
|
|
1096
1046
|
return numeric ? "---:" : "---";
|
|
1097
1047
|
});
|
|
1098
1048
|
|
|
@@ -1172,9 +1122,7 @@ export function expandContentTables(
|
|
|
1172
1122
|
// Find this fence's closing line: the same marker character, at least
|
|
1173
1123
|
// as long, with nothing after it.
|
|
1174
1124
|
let close = i + 1;
|
|
1175
|
-
const closer = new RegExp(
|
|
1176
|
-
`^[ \\t]*${marker[0]}{${marker.length},}[ \\t]*$`,
|
|
1177
|
-
);
|
|
1125
|
+
const closer = new RegExp(`^[ \\t]*${marker[0]}{${marker.length},}[ \\t]*$`);
|
|
1178
1126
|
while (close < lines.length && !closer.test(lines[close])) close++;
|
|
1179
1127
|
const isQuery = /^dataview\b/i.test(info.trim());
|
|
1180
1128
|
const block = lines.slice(i, Math.min(close + 1, lines.length));
|
|
@@ -1204,8 +1152,7 @@ export function expandContentTables(
|
|
|
1204
1152
|
}
|
|
1205
1153
|
// A markdown table must be its own block: keep one blank line on each
|
|
1206
1154
|
// side of it, without inventing a leading or trailing one.
|
|
1207
|
-
if (out.length > 0 && out[out.length - 1].trim() !== "")
|
|
1208
|
-
emit("", i, true);
|
|
1155
|
+
if (out.length > 0 && out[out.length - 1].trim() !== "") emit("", i, true);
|
|
1209
1156
|
for (const row of table.split("\n")) emit(`${indent}${row}`, i, true);
|
|
1210
1157
|
if (close + 1 < lines.length && lines[close + 1].trim() !== "") {
|
|
1211
1158
|
emit("", i, true);
|
package/engine/diagnostics.mjs
CHANGED
|
@@ -145,11 +145,7 @@ export function emitDiagnostic(d) {
|
|
|
145
145
|
* {@link expandContentTables}.
|
|
146
146
|
* @returns {{line: number, column: number|undefined, generated: boolean}}
|
|
147
147
|
*/
|
|
148
|
-
export function positionInBody(
|
|
149
|
-
body,
|
|
150
|
-
offset,
|
|
151
|
-
{ bodyLine = 1, bodyColumn = 1, lineMap } = {},
|
|
152
|
-
) {
|
|
148
|
+
export function positionInBody(body, offset, { bodyLine = 1, bodyColumn = 1, lineMap } = {}) {
|
|
153
149
|
const upTo = String(body ?? "").slice(0, Math.max(0, offset));
|
|
154
150
|
const nl = upTo.lastIndexOf("\n");
|
|
155
151
|
const scannedLine = upTo.length === 0 ? 0 : upTo.split("\n").length - 1;
|
|
@@ -209,10 +205,7 @@ export function positionInFrontmatter(raw, key, value = undefined) {
|
|
|
209
205
|
keyLine = i;
|
|
210
206
|
break;
|
|
211
207
|
}
|
|
212
|
-
if (
|
|
213
|
-
keyLine === -1 &&
|
|
214
|
-
new RegExp(`^\\s*${escape(key)}\\s*:`).test(lines[i])
|
|
215
|
-
) {
|
|
208
|
+
if (keyLine === -1 && new RegExp(`^\\s*${escape(key)}\\s*:`).test(lines[i])) {
|
|
216
209
|
keyLine = i;
|
|
217
210
|
if (wanted == null) break;
|
|
218
211
|
}
|
|
@@ -321,13 +314,9 @@ export function positionOfYamlPath(text, keyPath, { key = false } = {}) {
|
|
|
321
314
|
if (key && node !== undefined) {
|
|
322
315
|
const last = keyPath[keyPath.length - 1];
|
|
323
316
|
const parent =
|
|
324
|
-
keyPath.length === 1 ?
|
|
325
|
-
doc.contents
|
|
326
|
-
: doc.getIn(keyPath.slice(0, -1), true);
|
|
317
|
+
keyPath.length === 1 ? doc.contents : doc.getIn(keyPath.slice(0, -1), true);
|
|
327
318
|
const pair = parent?.items?.find?.(
|
|
328
|
-
(item) =>
|
|
329
|
-
item?.key != null &&
|
|
330
|
-
String(item.key.value) === String(last),
|
|
319
|
+
(item) => item?.key != null && String(item.key.value) === String(last),
|
|
331
320
|
);
|
|
332
321
|
if (pair?.key?.range) node = pair.key;
|
|
333
322
|
}
|
|
@@ -47,10 +47,8 @@ import { loadPackConfig } from "./pack-config.mjs";
|
|
|
47
47
|
function renderDefault(value) {
|
|
48
48
|
if (value === undefined) return "—";
|
|
49
49
|
if (value === null) return "`null`";
|
|
50
|
-
if (typeof value === "string")
|
|
51
|
-
|
|
52
|
-
if (Array.isArray(value))
|
|
53
|
-
return value.length === 0 ? "`[]`" : `\`${JSON.stringify(value)}\``;
|
|
50
|
+
if (typeof value === "string") return value === "" ? '`""`' : `\`${value}\``;
|
|
51
|
+
if (Array.isArray(value)) return value.length === 0 ? "`[]`" : `\`${JSON.stringify(value)}\``;
|
|
54
52
|
if (typeof value === "object") {
|
|
55
53
|
const json = JSON.stringify(value);
|
|
56
54
|
return json === "{}" ? "`{}`" : `\`${json}\``;
|
|
@@ -83,17 +81,10 @@ function cell(text) {
|
|
|
83
81
|
* @returns {string[]} Markdown lines.
|
|
84
82
|
*/
|
|
85
83
|
function padTable(rows) {
|
|
86
|
-
const widths = rows[0].map((_, column) =>
|
|
87
|
-
|
|
88
|
-
);
|
|
89
|
-
const line = (cells) =>
|
|
90
|
-
`| ${cells.map((c, i) => c.padEnd(widths[i])).join(" | ")} |`;
|
|
84
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
|
|
85
|
+
const line = (cells) => `| ${cells.map((c, i) => c.padEnd(widths[i])).join(" | ")} |`;
|
|
91
86
|
const [header, ...body] = rows;
|
|
92
|
-
return [
|
|
93
|
-
line(header),
|
|
94
|
-
`| ${widths.map((w) => "-".repeat(w)).join(" | ")} |`,
|
|
95
|
-
...body.map(line),
|
|
96
|
-
];
|
|
87
|
+
return [line(header), `| ${widths.map((w) => "-".repeat(w)).join(" | ")} |`, ...body.map(line)];
|
|
97
88
|
}
|
|
98
89
|
|
|
99
90
|
/**
|
|
@@ -156,12 +147,7 @@ function workedExample(type, fields) {
|
|
|
156
147
|
for (const field of required) {
|
|
157
148
|
lines.push(` ${field.name}: <${field.shape ?? "value"}>`);
|
|
158
149
|
}
|
|
159
|
-
lines.push(
|
|
160
|
-
"---",
|
|
161
|
-
"",
|
|
162
|
-
"The prose here compiles into the item's documentation.",
|
|
163
|
-
"```",
|
|
164
|
-
);
|
|
150
|
+
lines.push("---", "", "The prose here compiles into the item's documentation.", "```");
|
|
165
151
|
return lines;
|
|
166
152
|
}
|
|
167
153
|
|
package/engine/field-spec.mjs
CHANGED
|
@@ -178,9 +178,7 @@ export function setPath(target, dotted, value) {
|
|
|
178
178
|
*/
|
|
179
179
|
export function readField(field, fm) {
|
|
180
180
|
if (field.name === undefined) {
|
|
181
|
-
return typeof field.value === "function" ?
|
|
182
|
-
field.value(fm)
|
|
183
|
-
: field.value;
|
|
181
|
+
return typeof field.value === "function" ? field.value(fm) : field.value;
|
|
184
182
|
}
|
|
185
183
|
const raw = sohlField(fm, field.name, field.default);
|
|
186
184
|
return field.read ? field.read(raw, { fm, field }) : raw;
|
|
@@ -203,9 +203,7 @@ export function writeZipEntries(files, dest) {
|
|
|
203
203
|
* @returns {Promise<void>}
|
|
204
204
|
*/
|
|
205
205
|
async function extractItemPacks(id, version, manifest, root, dir) {
|
|
206
|
-
const itemPacks = (manifest.packs ?? []).filter(
|
|
207
|
-
(pack) => pack.type === "Item",
|
|
208
|
-
);
|
|
206
|
+
const itemPacks = (manifest.packs ?? []).filter((pack) => pack.type === "Item");
|
|
209
207
|
if (!itemPacks.length) {
|
|
210
208
|
throw new Error(
|
|
211
209
|
`${id}@${version}: its manifest declares no Item packs, so it ` +
|
|
@@ -239,9 +237,7 @@ async function extractItemPacks(id, version, manifest, root, dir) {
|
|
|
239
237
|
async function downloadAndUnzip(url, dest) {
|
|
240
238
|
const res = await fetch(url, { redirect: "follow" });
|
|
241
239
|
if (!res.ok) {
|
|
242
|
-
throw new Error(
|
|
243
|
-
`could not download ${url}: HTTP ${res.status} ${res.statusText}`,
|
|
244
|
-
);
|
|
240
|
+
throw new Error(`could not download ${url}: HTTP ${res.status} ${res.statusText}`);
|
|
245
241
|
}
|
|
246
242
|
writeZipEntries(unzipSync(new Uint8Array(await res.arrayBuffer())), dest);
|
|
247
243
|
}
|
|
@@ -272,10 +268,7 @@ export function pinnedManifestUrl(url, verified) {
|
|
|
272
268
|
if (at === -1) return { url, pinned: false };
|
|
273
269
|
const tag = verified.startsWith("v") ? verified : `v${verified}`;
|
|
274
270
|
return {
|
|
275
|
-
url:
|
|
276
|
-
url.slice(0, at) +
|
|
277
|
-
`/releases/download/${tag}/` +
|
|
278
|
-
url.slice(at + marker.length),
|
|
271
|
+
url: url.slice(0, at) + `/releases/download/${tag}/` + url.slice(at + marker.length),
|
|
279
272
|
pinned: true,
|
|
280
273
|
};
|
|
281
274
|
}
|
|
@@ -318,9 +311,7 @@ export async function fetchCatalog(config, rel) {
|
|
|
318
311
|
|
|
319
312
|
const download = manifest.download;
|
|
320
313
|
if (!download) {
|
|
321
|
-
throw new Error(
|
|
322
|
-
`${rel.id}@${version}: its manifest declares no \`download\``,
|
|
323
|
-
);
|
|
314
|
+
throw new Error(`${rel.id}@${version}: its manifest declares no \`download\``);
|
|
324
315
|
}
|
|
325
316
|
|
|
326
317
|
// Rebuild from empty: a previous run may have died partway, and a stale
|
|
@@ -398,9 +389,7 @@ export async function fetchCatalogFromPath(config, rel, source) {
|
|
|
398
389
|
throw new Error(`${rel.id}: nothing at ${source}`);
|
|
399
390
|
}
|
|
400
391
|
|
|
401
|
-
const staging = fs.mkdtempSync(
|
|
402
|
-
path.join(os.tmpdir(), `content-build-${rel.id}-`),
|
|
403
|
-
);
|
|
392
|
+
const staging = fs.mkdtempSync(path.join(os.tmpdir(), `content-build-${rel.id}-`));
|
|
404
393
|
try {
|
|
405
394
|
let root;
|
|
406
395
|
if (fs.statSync(source).isDirectory()) {
|
|
@@ -419,9 +408,7 @@ export async function fetchCatalogFromPath(config, rel, source) {
|
|
|
419
408
|
);
|
|
420
409
|
}
|
|
421
410
|
if (manifest.id && manifest.id !== rel.id) {
|
|
422
|
-
throw new Error(
|
|
423
|
-
`${rel.id}: ${source} is package "${manifest.id}", not "${rel.id}"`,
|
|
424
|
-
);
|
|
411
|
+
throw new Error(`${rel.id}: ${source} is package "${manifest.id}", not "${rel.id}"`);
|
|
425
412
|
}
|
|
426
413
|
const version = manifest.version;
|
|
427
414
|
if (!version) {
|
|
@@ -448,9 +435,7 @@ export async function fetchCatalogFromPath(config, rel, source) {
|
|
|
448
435
|
export async function fetchAllCatalogs(config) {
|
|
449
436
|
const rels = itemCatalogRelationships(config);
|
|
450
437
|
if (!rels.length) {
|
|
451
|
-
log.info(
|
|
452
|
-
"No relationship declares `itemCatalog: true`; nothing to fetch.",
|
|
453
|
-
);
|
|
438
|
+
log.info("No relationship declares `itemCatalog: true`; nothing to fetch.");
|
|
454
439
|
return 0;
|
|
455
440
|
}
|
|
456
441
|
for (const rel of rels) await fetchCatalog(config, rel);
|
|
@@ -106,10 +106,7 @@ export function formatUnaddressableFinding(finding, manifestDir) {
|
|
|
106
106
|
const file = path.join(manifestDir, `${finding.package}.json`);
|
|
107
107
|
let at = {};
|
|
108
108
|
try {
|
|
109
|
-
at = positionOfLiteral(
|
|
110
|
-
fs.readFileSync(file, "utf8"),
|
|
111
|
-
`"${finding.sampleKey}"`,
|
|
112
|
-
);
|
|
109
|
+
at = positionOfLiteral(fs.readFileSync(file, "utf8"), `"${finding.sampleKey}"`);
|
|
113
110
|
} catch {
|
|
114
111
|
// Unreadable here is not itself the finding — `loadForeignManifests`
|
|
115
112
|
// already reports that as a stale manifest. The file is simply all that
|