@heroiclands/package-build 8.1.0 → 10.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 +748 -0
- package/CONTENT.md +273 -13
- package/bin/content-build.mjs +479 -123
- 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 +300 -188
- package/coverage.mjs +14 -55
- package/deploy.mjs +4 -13
- package/docs/content-format.md +1418 -0
- package/e2e.mjs +16 -55
- package/engine/address-charset.mjs +62 -0
- package/engine/address-diff.mjs +1 -4
- package/engine/alias-index.mjs +153 -0
- package/engine/base-compiler.mjs +203 -31
- package/engine/code-fences.mjs +4 -13
- package/engine/compendiums.mjs +13 -37
- package/engine/content-address.mjs +6 -10
- package/engine/content-format-check.mjs +570 -0
- package/engine/content-format.mjs +253 -0
- package/engine/content-links.mjs +144 -99
- package/engine/content-lint.mjs +12 -16
- package/engine/content-slug.mjs +2 -6
- package/engine/content-tables.mjs +26 -79
- package/engine/diagnostics.mjs +37 -15
- package/engine/document-subtypes.mjs +440 -0
- package/engine/field-reference.mjs +6 -20
- package/engine/field-spec.mjs +49 -45
- package/engine/foreign-catalog.mjs +7 -22
- package/engine/foreign-manifests.mjs +1 -4
- package/engine/frontmatter-lint.mjs +347 -43
- package/engine/frontmatter.mjs +3 -8
- package/engine/generate.mjs +36 -20
- package/engine/helpers.mjs +54 -81
- package/engine/homepage.mjs +4 -15
- package/engine/ids.mjs +21 -12
- package/engine/index.mjs +15 -0
- package/engine/item-registry.mjs +72 -9
- package/engine/journals.mjs +4 -14
- package/engine/kb-manifest.mjs +41 -24
- package/engine/macros.mjs +2 -10
- package/engine/manifest-emit.mjs +6 -17
- package/engine/map-notes.mjs +53 -87
- package/engine/note-claims.mjs +383 -0
- package/engine/note-package.mjs +1 -4
- package/engine/note-vocabulary.mjs +678 -0
- package/engine/pack-config.mjs +56 -60
- package/engine/pack-router.mjs +18 -8
- package/engine/prose-config.mjs +20 -4
- package/engine/prose-lint.mjs +61 -17
- package/engine/region-events.mjs +1 -3
- package/engine/retired-fields.mjs +117 -3
- package/engine/scene-levels.mjs +8 -22
- package/engine/scenes.mjs +31 -47
- package/engine/schema-check.mjs +348 -7
- package/engine/schema-extract.mjs +11 -39
- package/engine/site-build.mjs +13 -38
- package/engine/site-index.mjs +40 -35
- package/engine/system-block.mjs +513 -0
- package/engine/web-wikilinks.mjs +115 -92
- package/engine/wikilink-syntax.mjs +30 -0
- package/engine/wikilinks.mjs +147 -183
- package/index.mjs +1 -5
- package/lang.mjs +1 -3
- package/manifest.mjs +10 -37
- package/markdownlint-config.mjs +1 -5
- package/package.json +6 -2
- package/sohl/actors.mjs +251 -68
- package/sohl/being-info.mjs +3 -6
- package/sohl/document-subtypes.mjs +82 -0
- package/sohl/index.mjs +4 -6
- package/sohl/item-builders.mjs +1 -3
- package/sohl/item-fields.mjs +16 -34
- package/sohl/items.mjs +111 -17
- package/sohl/kb-passes.mjs +29 -39
- package/sohl/note-schemas.mjs +11 -7
- 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/content-config.d.mts +48 -4
- package/types/coverage.d.mts +1 -1
- package/types/e2e.d.mts +4 -4
- package/types/engine/address-charset.d.mts +45 -0
- package/types/engine/alias-index.d.mts +122 -0
- package/types/engine/base-compiler.d.mts +132 -4
- package/types/engine/content-address.d.mts +2 -2
- package/types/engine/content-format-check.d.mts +163 -0
- package/types/engine/content-format.d.mts +101 -0
- package/types/engine/content-links.d.mts +16 -1
- package/types/engine/content-lint.d.mts +6 -0
- package/types/engine/diagnostics.d.mts +29 -0
- package/types/engine/document-subtypes.d.mts +233 -0
- package/types/engine/field-spec.d.mts +76 -23
- package/types/engine/frontmatter-lint.d.mts +47 -2
- package/types/engine/generate.d.mts +15 -2
- package/types/engine/helpers.d.mts +22 -14
- package/types/engine/ids.d.mts +10 -0
- package/types/engine/index.d.mts +5 -0
- package/types/engine/item-registry.d.mts +21 -2
- package/types/engine/kb-manifest.d.mts +35 -8
- package/types/engine/map-notes.d.mts +21 -11
- package/types/engine/note-claims.d.mts +113 -0
- package/types/engine/note-vocabulary.d.mts +251 -0
- package/types/engine/pack-config.d.mts +4 -3
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/prose-lint.d.mts +6 -2
- package/types/engine/retired-fields.d.mts +73 -2
- package/types/engine/schema-check.d.mts +182 -0
- package/types/engine/schema-extract.d.mts +1 -1
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/system-block.d.mts +281 -0
- package/types/engine/web-wikilinks.d.mts +23 -12
- package/types/engine/wikilink-syntax.d.mts +29 -0
- package/types/manifest.d.mts +1 -1
- package/types/sohl/actors.d.mts +62 -6
- package/types/sohl/document-subtypes.d.mts +14 -0
- package/types/sohl/index.d.mts +1 -0
- package/types/sohl/items.d.mts +21 -0
package/engine/content-lint.mjs
CHANGED
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
import fs from "node:fs";
|
|
60
60
|
import path from "node:path";
|
|
61
61
|
|
|
62
|
+
import { ADDRESS_SEGMENT_PATTERN } from "./address-charset.mjs";
|
|
62
63
|
import { positionInFrontmatter } from "./diagnostics.mjs";
|
|
63
64
|
import { walkMarkdownTree } from "./helpers.mjs";
|
|
64
65
|
import { checkHomepageCount, isHomepage } from "./homepage.mjs";
|
|
@@ -66,6 +67,12 @@ import { checkHomepageCount, isHomepage } from "./homepage.mjs";
|
|
|
66
67
|
/**
|
|
67
68
|
* The shape every `shortcode` must match: ASCII letters and digits only.
|
|
68
69
|
*
|
|
70
|
+
* This is {@link ADDRESS_SEGMENT_PATTERN}, not a second copy of it. A shortcode
|
|
71
|
+
* is the last segment of a canonical address, and the rule it is held to is the
|
|
72
|
+
* rule *every* segment is held to — so the two are one constant rather than two
|
|
73
|
+
* free to drift apart (#59). The name survives because this is where the rule
|
|
74
|
+
* is applied to a note.
|
|
75
|
+
*
|
|
69
76
|
* Case is deliberately **not** constrained: hundreds of authored shortcodes are
|
|
70
77
|
* mixed-case and collide with nothing, so tightening that is a separate
|
|
71
78
|
* decision from this one.
|
|
@@ -74,7 +81,7 @@ import { checkHomepageCount, isHomepage } from "./homepage.mjs";
|
|
|
74
81
|
* import a build-time dependency into shipped code — and is expected to pin the
|
|
75
82
|
* two together with a test rather than trust that they still agree.
|
|
76
83
|
*/
|
|
77
|
-
export const SHORTCODE_PATTERN =
|
|
84
|
+
export const SHORTCODE_PATTERN = ADDRESS_SEGMENT_PATTERN;
|
|
78
85
|
|
|
79
86
|
/**
|
|
80
87
|
* Whether a value is a well-formed shortcode.
|
|
@@ -106,10 +113,7 @@ export function isValidShortcode(value) {
|
|
|
106
113
|
function collectNotes(contentBase, { skipDirectories } = {}) {
|
|
107
114
|
const notes = [];
|
|
108
115
|
const walkOpts = skipDirectories ? { skipDirectories } : undefined;
|
|
109
|
-
for (const { frontmatter: fm, absPath } of walkMarkdownTree(
|
|
110
|
-
contentBase,
|
|
111
|
-
walkOpts,
|
|
112
|
-
)) {
|
|
116
|
+
for (const { frontmatter: fm, absPath } of walkMarkdownTree(contentBase, walkOpts)) {
|
|
113
117
|
if (!fm || !fm.type) continue;
|
|
114
118
|
notes.push({
|
|
115
119
|
fm,
|
|
@@ -134,10 +138,7 @@ function collectNotes(contentBase, { skipDirectories } = {}) {
|
|
|
134
138
|
* severity: "error"|"warning", message: string}>, notes: number,
|
|
135
139
|
* keys: number}} The findings, and what was inspected to produce them.
|
|
136
140
|
*/
|
|
137
|
-
export function lintContentTree(
|
|
138
|
-
contentBase,
|
|
139
|
-
{ skipDirectories, contentPackage } = {},
|
|
140
|
-
) {
|
|
141
|
+
export function lintContentTree(contentBase, { skipDirectories, contentPackage } = {}) {
|
|
141
142
|
const findings = [];
|
|
142
143
|
const notes = collectNotes(contentBase, { skipDirectories });
|
|
143
144
|
|
|
@@ -211,15 +212,10 @@ export function lintContentTree(
|
|
|
211
212
|
// is a place an author has to go and edit, and a finding naming only
|
|
212
213
|
// the key sends them hunting for the other one.
|
|
213
214
|
for (const { file, absPath } of files) {
|
|
214
|
-
const others = files
|
|
215
|
-
.filter((f) => f.file !== file)
|
|
216
|
-
.map((f) => f.file);
|
|
215
|
+
const others = files.filter((f) => f.file !== file).map((f) => f.file);
|
|
217
216
|
findings.push({
|
|
218
217
|
file,
|
|
219
|
-
...positionInFrontmatter(
|
|
220
|
-
fs.readFileSync(absPath, "utf8"),
|
|
221
|
-
"shortcode",
|
|
222
|
-
),
|
|
218
|
+
...positionInFrontmatter(fs.readFileSync(absPath, "utf8"), "shortcode"),
|
|
223
219
|
severity: "error",
|
|
224
220
|
message:
|
|
225
221
|
`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
|
}
|
|
@@ -374,3 +363,36 @@ export function yamlKeyPath(field) {
|
|
|
374
363
|
}
|
|
375
364
|
return segments;
|
|
376
365
|
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Where a key sits **inside a note's frontmatter fence**, addressed by path.
|
|
369
|
+
*
|
|
370
|
+
* {@link positionInFrontmatter} searches the fence for a key by name, which is
|
|
371
|
+
* the right tool while a note's vocabulary is flat: a key appears once and the
|
|
372
|
+
* first line matching it is the one. It stops being the right tool the moment
|
|
373
|
+
* the same name is legal in two regions — `data.weight` and a top-level
|
|
374
|
+
* `weight` are different keys, and a search finds whichever comes first.
|
|
375
|
+
*
|
|
376
|
+
* So this is the located form for the closed regions (#128). It reuses
|
|
377
|
+
* {@link positionOfYamlPath} rather than repeating its parse, which is what
|
|
378
|
+
* keeps a finding about `data.weight` pointing at `data.weight`.
|
|
379
|
+
*
|
|
380
|
+
* @param {string} raw - The note's full contents, frontmatter included.
|
|
381
|
+
* @param {ReadonlyArray<string|number>} keyPath - Path to the node, from the
|
|
382
|
+
* top of the frontmatter.
|
|
383
|
+
* @param {object} [opts]
|
|
384
|
+
* @param {boolean} [opts.key=false] - Report where the last segment is
|
|
385
|
+
* *declared* rather than where its value sits.
|
|
386
|
+
* @returns {{line?: number, column?: number}} Spreadable position fields, empty
|
|
387
|
+
* when there is no fence or the path resolves to nothing — dropped rather
|
|
388
|
+
* than guessed.
|
|
389
|
+
*/
|
|
390
|
+
export function positionOfFrontmatterPath(raw, keyPath, { key = false } = {}) {
|
|
391
|
+
if (typeof raw !== "string") return {};
|
|
392
|
+
const fence = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
393
|
+
if (!fence) return {};
|
|
394
|
+
const position = positionOfYamlPath(fence[1], keyPath, { key });
|
|
395
|
+
// +1: the fence's line 1 is the file's line 2, the opening `---` being the
|
|
396
|
+
// first. `positionOfYamlPath` already counts from 1 within the block.
|
|
397
|
+
return position.line === undefined ? {} : { ...position, line: position.line + 1 };
|
|
398
|
+
}
|