@riebeckite/plugin-bases 0.0.27 → 0.0.29
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/README.md +146 -162
- package/README_ja.md +137 -135
- package/dist/index.d.ts +5 -2
- package/dist/index.js +1588 -170
- package/dist/src/dependencies.d.ts +3 -0
- package/dist/src/evaluate.d.ts +5 -1
- package/dist/src/expression.d.ts +9 -0
- package/dist/src/page.d.ts +17 -0
- package/dist/src/runtime.d.ts +4 -1
- package/dist/src/transform.d.ts +10 -0
- package/dist/src/types.d.ts +55 -4
- package/dist/src/yaml.d.ts +7 -0
- package/package.json +2 -2
- package/style.css +94 -5
package/dist/index.js
CHANGED
|
@@ -4,9 +4,403 @@ import {
|
|
|
4
4
|
definePlugin
|
|
5
5
|
} from "@riebeckite/core";
|
|
6
6
|
|
|
7
|
-
// src/
|
|
8
|
-
import {
|
|
9
|
-
|
|
7
|
+
// src/page.ts
|
|
8
|
+
import {
|
|
9
|
+
escapeHtml as escapeHtml3,
|
|
10
|
+
readContentSourceEntry
|
|
11
|
+
} from "@riebeckite/core";
|
|
12
|
+
|
|
13
|
+
// src/dependencies.ts
|
|
14
|
+
function analyzeBasesDependencies(spec) {
|
|
15
|
+
const properties = /* @__PURE__ */ new Set();
|
|
16
|
+
const active = /* @__PURE__ */ new Set();
|
|
17
|
+
let unknown = false;
|
|
18
|
+
const analyzeFormula = (name) => {
|
|
19
|
+
if (active.has(name)) {
|
|
20
|
+
unknown = true;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const formula = spec.formulas[name];
|
|
24
|
+
if (!formula) {
|
|
25
|
+
unknown = true;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
active.add(name);
|
|
29
|
+
analyzeExpression(formula, properties, analyzeFormula, () => {
|
|
30
|
+
unknown = true;
|
|
31
|
+
});
|
|
32
|
+
active.delete(name);
|
|
33
|
+
};
|
|
34
|
+
analyzeCondition(spec.filter, properties, analyzeFormula, () => {
|
|
35
|
+
unknown = true;
|
|
36
|
+
});
|
|
37
|
+
for (const view of spec.views) {
|
|
38
|
+
analyzeCondition(view.filter, properties, analyzeFormula, () => {
|
|
39
|
+
unknown = true;
|
|
40
|
+
});
|
|
41
|
+
for (const property of [
|
|
42
|
+
...view.columns,
|
|
43
|
+
...view.sort?.map((item) => item.property) ?? [],
|
|
44
|
+
...view.groupBy ? [view.groupBy.property] : [],
|
|
45
|
+
...Object.keys(view.summaries ?? {})
|
|
46
|
+
]) {
|
|
47
|
+
analyzeProperty(property, properties, analyzeFormula, () => {
|
|
48
|
+
unknown = true;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
type: "content-query",
|
|
54
|
+
properties: [...properties].sort(),
|
|
55
|
+
...unknown ? { unknown: true } : {}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function analyzeCondition(condition, properties, formula, markUnknown) {
|
|
59
|
+
if (!condition) return;
|
|
60
|
+
switch (condition.kind) {
|
|
61
|
+
case "and":
|
|
62
|
+
case "or":
|
|
63
|
+
for (const child of condition.conditions)
|
|
64
|
+
analyzeCondition(child, properties, formula, markUnknown);
|
|
65
|
+
return;
|
|
66
|
+
case "not":
|
|
67
|
+
analyzeCondition(condition.condition, properties, formula, markUnknown);
|
|
68
|
+
return;
|
|
69
|
+
case "hasTag":
|
|
70
|
+
properties.add("file.tags");
|
|
71
|
+
return;
|
|
72
|
+
case "inFolder":
|
|
73
|
+
properties.add("file.folder");
|
|
74
|
+
return;
|
|
75
|
+
case "hasLink":
|
|
76
|
+
properties.add("file.links");
|
|
77
|
+
return;
|
|
78
|
+
case "compare":
|
|
79
|
+
properties.add(
|
|
80
|
+
condition.left.source === "builtin" ? condition.left.name : condition.left.key
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
case "expression":
|
|
84
|
+
analyzeExpression(condition.expression, properties, formula, markUnknown);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function analyzeProperty(property, properties, formula, markUnknown) {
|
|
89
|
+
if (property.startsWith("formula.")) {
|
|
90
|
+
formula(property.slice(8));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (property === "file" || property === "note") {
|
|
94
|
+
markUnknown();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (property.startsWith("note.")) properties.add(property.slice(5));
|
|
98
|
+
else if (property.startsWith("file.frontmatter."))
|
|
99
|
+
properties.add(property.slice(17));
|
|
100
|
+
else properties.add(property);
|
|
101
|
+
}
|
|
102
|
+
function analyzeExpression(expression, properties, formula, markUnknown) {
|
|
103
|
+
switch (expression.kind) {
|
|
104
|
+
case "literal":
|
|
105
|
+
return;
|
|
106
|
+
case "reference":
|
|
107
|
+
analyzeReference(expression.path, properties, formula, markUnknown);
|
|
108
|
+
return;
|
|
109
|
+
case "array":
|
|
110
|
+
for (const value of expression.values)
|
|
111
|
+
analyzeExpression(value, properties, formula, markUnknown);
|
|
112
|
+
return;
|
|
113
|
+
case "unary":
|
|
114
|
+
analyzeExpression(expression.value, properties, formula, markUnknown);
|
|
115
|
+
return;
|
|
116
|
+
case "binary":
|
|
117
|
+
analyzeExpression(expression.left, properties, formula, markUnknown);
|
|
118
|
+
analyzeExpression(expression.right, properties, formula, markUnknown);
|
|
119
|
+
return;
|
|
120
|
+
case "call":
|
|
121
|
+
if (isFilePredicate(expression.callee, "hasTag")) {
|
|
122
|
+
properties.add("file.tags");
|
|
123
|
+
} else if (isFilePredicate(expression.callee, "hasLink")) {
|
|
124
|
+
properties.add("file.links");
|
|
125
|
+
} else if (isFilePredicate(expression.callee, "inFolder")) {
|
|
126
|
+
properties.add("file.folder");
|
|
127
|
+
} else if (isFilePredicate(expression.callee, "hasProperty")) {
|
|
128
|
+
const property = literalString(expression.arguments[0]);
|
|
129
|
+
if (property === void 0) markUnknown();
|
|
130
|
+
else properties.add(property);
|
|
131
|
+
} else if (expression.callee.kind === "member")
|
|
132
|
+
analyzeExpression(
|
|
133
|
+
expression.callee.object,
|
|
134
|
+
properties,
|
|
135
|
+
formula,
|
|
136
|
+
markUnknown
|
|
137
|
+
);
|
|
138
|
+
else
|
|
139
|
+
analyzeExpression(expression.callee, properties, formula, markUnknown);
|
|
140
|
+
for (const argument of expression.arguments)
|
|
141
|
+
analyzeExpression(argument, properties, formula, markUnknown);
|
|
142
|
+
return;
|
|
143
|
+
case "member":
|
|
144
|
+
analyzeMember(expression, properties, formula, markUnknown);
|
|
145
|
+
return;
|
|
146
|
+
case "index":
|
|
147
|
+
analyzeExpression(expression.object, properties, formula, markUnknown);
|
|
148
|
+
analyzeExpression(expression.index, properties, formula, markUnknown);
|
|
149
|
+
markUnknown();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function literalString(expression) {
|
|
154
|
+
return expression?.kind === "literal" && typeof expression.value === "string" ? expression.value : void 0;
|
|
155
|
+
}
|
|
156
|
+
function isFilePredicate(expression, predicate) {
|
|
157
|
+
const path = expressionPath(expression);
|
|
158
|
+
return path?.length === 2 && path[0] === "file" && path[1] === predicate;
|
|
159
|
+
}
|
|
160
|
+
function analyzeMember(expression, properties, formula, markUnknown) {
|
|
161
|
+
const path = expressionPath(expression);
|
|
162
|
+
if (path) {
|
|
163
|
+
analyzeReference(path, properties, formula, markUnknown);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
analyzeExpression(expression.object, properties, formula, markUnknown);
|
|
167
|
+
}
|
|
168
|
+
function expressionPath(expression) {
|
|
169
|
+
if (expression.kind === "reference") return expression.path;
|
|
170
|
+
if (expression.kind !== "member") return void 0;
|
|
171
|
+
const parent = expressionPath(expression.object);
|
|
172
|
+
return parent ? [...parent, expression.property] : void 0;
|
|
173
|
+
}
|
|
174
|
+
function analyzeReference(path, properties, formula, markUnknown) {
|
|
175
|
+
const [first, ...rest] = path;
|
|
176
|
+
if (!first || first === "value" || first === "index" || first === "acc")
|
|
177
|
+
return;
|
|
178
|
+
if (first === "formula") {
|
|
179
|
+
if (rest.length !== 1) markUnknown();
|
|
180
|
+
else formula(rest[0] ?? "");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (first === "file") {
|
|
184
|
+
if (rest.length === 0) markUnknown();
|
|
185
|
+
else if (rest[0] === "frontmatter") {
|
|
186
|
+
if (rest.length === 1) markUnknown();
|
|
187
|
+
else properties.add(rest.slice(1).join("."));
|
|
188
|
+
} else properties.add(`file.${rest.join(".")}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (first === "note") {
|
|
192
|
+
if (rest.length === 0) markUnknown();
|
|
193
|
+
else properties.add(rest.join("."));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
properties.add(path.join("."));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/expression.ts
|
|
200
|
+
function parseBasesExpression(source) {
|
|
201
|
+
try {
|
|
202
|
+
const parser = new Parser(tokenize(source));
|
|
203
|
+
const value = parser.expression();
|
|
204
|
+
if (parser.current.kind !== "eof")
|
|
205
|
+
throw new Error(`Unexpected token \`${parser.current.text}\`.`);
|
|
206
|
+
return { ok: true, value };
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
message: error instanceof Error ? error.message : String(error)
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
var Parser = class {
|
|
215
|
+
constructor(tokens) {
|
|
216
|
+
this.tokens = tokens;
|
|
217
|
+
}
|
|
218
|
+
tokens;
|
|
219
|
+
index = 0;
|
|
220
|
+
get current() {
|
|
221
|
+
return this.tokens[this.index] ?? { kind: "eof", text: "" };
|
|
222
|
+
}
|
|
223
|
+
expression(minimum = 0) {
|
|
224
|
+
let left = this.prefix();
|
|
225
|
+
while (true) {
|
|
226
|
+
if (this.current.text === ".") {
|
|
227
|
+
this.next();
|
|
228
|
+
left = {
|
|
229
|
+
kind: "member",
|
|
230
|
+
object: left,
|
|
231
|
+
property: this.expect("identifier").text
|
|
232
|
+
};
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (this.current.text === "[") {
|
|
236
|
+
this.next();
|
|
237
|
+
const index = this.expression();
|
|
238
|
+
this.expectText("]");
|
|
239
|
+
left = { kind: "index", object: left, index };
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (this.current.text === "(") {
|
|
243
|
+
left = { kind: "call", callee: left, arguments: this.arguments() };
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const precedence = precedenceOf(this.current.text);
|
|
247
|
+
if (precedence < minimum) break;
|
|
248
|
+
const operator = this.current.text;
|
|
249
|
+
this.next();
|
|
250
|
+
left = {
|
|
251
|
+
kind: "binary",
|
|
252
|
+
operator,
|
|
253
|
+
left,
|
|
254
|
+
right: this.expression(precedence + 1)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return left;
|
|
258
|
+
}
|
|
259
|
+
prefix() {
|
|
260
|
+
if (this.current.text === "!" || this.current.text === "-") {
|
|
261
|
+
const operator = this.current.text;
|
|
262
|
+
this.next();
|
|
263
|
+
return { kind: "unary", operator, value: this.expression(8) };
|
|
264
|
+
}
|
|
265
|
+
if (this.current.text === "(") {
|
|
266
|
+
this.next();
|
|
267
|
+
const value = this.expression();
|
|
268
|
+
this.expectText(")");
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
if (this.current.text === "[") {
|
|
272
|
+
this.next();
|
|
273
|
+
const values = this.list("]");
|
|
274
|
+
return { kind: "array", values };
|
|
275
|
+
}
|
|
276
|
+
if (this.current.kind === "number") {
|
|
277
|
+
const value = Number(this.current.text);
|
|
278
|
+
this.next();
|
|
279
|
+
return { kind: "literal", value };
|
|
280
|
+
}
|
|
281
|
+
if (this.current.kind === "string") {
|
|
282
|
+
const value = this.current.text;
|
|
283
|
+
this.next();
|
|
284
|
+
return { kind: "literal", value };
|
|
285
|
+
}
|
|
286
|
+
if (this.current.kind === "identifier") {
|
|
287
|
+
const text = this.current.text;
|
|
288
|
+
this.next();
|
|
289
|
+
if (text === "true" || text === "false" || text === "null")
|
|
290
|
+
return {
|
|
291
|
+
kind: "literal",
|
|
292
|
+
value: text === "null" ? null : text === "true"
|
|
293
|
+
};
|
|
294
|
+
return { kind: "reference", path: [text] };
|
|
295
|
+
}
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Expected an expression, found \`${this.current.text || "end of input"}\`.`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
arguments() {
|
|
301
|
+
this.expectText("(");
|
|
302
|
+
return this.list(")");
|
|
303
|
+
}
|
|
304
|
+
list(end) {
|
|
305
|
+
const values = [];
|
|
306
|
+
if (this.current.text === end) {
|
|
307
|
+
this.next();
|
|
308
|
+
return values;
|
|
309
|
+
}
|
|
310
|
+
while (true) {
|
|
311
|
+
values.push(this.expression());
|
|
312
|
+
if (this.current.text === end) {
|
|
313
|
+
this.next();
|
|
314
|
+
return values;
|
|
315
|
+
}
|
|
316
|
+
this.expectText(",");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
expect(kind) {
|
|
320
|
+
if (this.current.kind !== kind)
|
|
321
|
+
throw new Error(
|
|
322
|
+
`Expected ${kind}, found \`${this.current.text || "end of input"}\`.`
|
|
323
|
+
);
|
|
324
|
+
const value = this.current;
|
|
325
|
+
this.next();
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
expectText(text) {
|
|
329
|
+
if (this.current.text !== text)
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Expected \`${text}\`, found \`${this.current.text || "end of input"}\`.`
|
|
332
|
+
);
|
|
333
|
+
this.next();
|
|
334
|
+
}
|
|
335
|
+
next() {
|
|
336
|
+
this.index += 1;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
function precedenceOf(operator) {
|
|
340
|
+
return {
|
|
341
|
+
"||": 1,
|
|
342
|
+
"&&": 2,
|
|
343
|
+
"==": 3,
|
|
344
|
+
"!=": 3,
|
|
345
|
+
">": 4,
|
|
346
|
+
"<": 4,
|
|
347
|
+
">=": 4,
|
|
348
|
+
"<=": 4,
|
|
349
|
+
"+": 5,
|
|
350
|
+
"-": 5,
|
|
351
|
+
"*": 6,
|
|
352
|
+
"/": 6,
|
|
353
|
+
"%": 6
|
|
354
|
+
}[operator] ?? -1;
|
|
355
|
+
}
|
|
356
|
+
function tokenize(source) {
|
|
357
|
+
const tokens = [];
|
|
358
|
+
let index = 0;
|
|
359
|
+
while (index < source.length) {
|
|
360
|
+
const rest = source.slice(index);
|
|
361
|
+
const whitespace = rest.match(/^\s+/);
|
|
362
|
+
if (whitespace) {
|
|
363
|
+
index += whitespace[0].length;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const string = rest.match(/^(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)')/);
|
|
367
|
+
if (string) {
|
|
368
|
+
tokens.push({
|
|
369
|
+
kind: "string",
|
|
370
|
+
text: JSON.parse(
|
|
371
|
+
`"${(string[1] ?? string[2] ?? "").replace(/\\"/g, '\\"')}"`
|
|
372
|
+
)
|
|
373
|
+
});
|
|
374
|
+
index += string[0].length;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const number = rest.match(/^\d+(?:\.\d+)?/);
|
|
378
|
+
if (number) {
|
|
379
|
+
tokens.push({ kind: "number", text: number[0] });
|
|
380
|
+
index += number[0].length;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const identifier = rest.match(/^[A-Za-z_$][\w$]*/);
|
|
384
|
+
if (identifier) {
|
|
385
|
+
tokens.push({ kind: "identifier", text: identifier[0] });
|
|
386
|
+
index += identifier[0].length;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const operator = rest.match(/^(?:&&|\|\||==|!=|>=|<=|[+\-*/%><!])/);
|
|
390
|
+
if (operator) {
|
|
391
|
+
tokens.push({ kind: "operator", text: operator[0] });
|
|
392
|
+
index += operator[0].length;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if ("().,[]".includes(rest[0] ?? "")) {
|
|
396
|
+
tokens.push({ kind: "punctuation", text: rest[0] ?? "" });
|
|
397
|
+
index += 1;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
throw new Error(`Unexpected character \`${rest[0]}\`.`);
|
|
401
|
+
}
|
|
402
|
+
return [...tokens, { kind: "eof", text: "" }];
|
|
403
|
+
}
|
|
10
404
|
|
|
11
405
|
// src/parse.ts
|
|
12
406
|
var DEFAULT_COLUMNS = ["file.name", "file.tags"];
|
|
@@ -22,13 +416,14 @@ var BUILTIN_ALIASES = {
|
|
|
22
416
|
"file.permalink": "file.permalink",
|
|
23
417
|
"file.url": "file.link",
|
|
24
418
|
"file.tags": "file.tags",
|
|
25
|
-
"file.
|
|
26
|
-
"file.
|
|
27
|
-
"file.
|
|
419
|
+
"file.ext": "file.ext",
|
|
420
|
+
"file.size": "file.size",
|
|
421
|
+
"file.mtime": "file.mtime",
|
|
422
|
+
"file.ctime": "file.ctime"
|
|
28
423
|
};
|
|
29
424
|
function parseBases(document) {
|
|
30
425
|
if (document === null || document === void 0) {
|
|
31
|
-
return ok({ properties: {}, views: [] });
|
|
426
|
+
return ok({ properties: {}, formulas: {}, summaries: {}, views: [] });
|
|
32
427
|
}
|
|
33
428
|
if (typeof document !== "object" || Array.isArray(document)) {
|
|
34
429
|
return fail("Base definition must be a YAML mapping.");
|
|
@@ -38,11 +433,17 @@ function parseBases(document) {
|
|
|
38
433
|
if (filter.ok === false) return filter;
|
|
39
434
|
const properties = parseProperties(root.properties);
|
|
40
435
|
if (properties.ok === false) return properties;
|
|
436
|
+
const formulas = parseFormulas(root.formulas);
|
|
437
|
+
if (formulas.ok === false) return formulas;
|
|
438
|
+
const summaries = parseFormulas(root.summaries);
|
|
439
|
+
if (summaries.ok === false) return summaries;
|
|
41
440
|
const views = parseViews(root.views, properties.value);
|
|
42
441
|
if (views.ok === false) return views;
|
|
43
442
|
return ok({
|
|
44
443
|
...filter.value === void 0 ? {} : { filter: filter.value },
|
|
45
444
|
properties: properties.value,
|
|
445
|
+
formulas: formulas.value,
|
|
446
|
+
summaries: summaries.value,
|
|
46
447
|
views: views.value
|
|
47
448
|
});
|
|
48
449
|
}
|
|
@@ -137,6 +538,9 @@ function toList(value) {
|
|
|
137
538
|
return Array.isArray(value) ? value : [value];
|
|
138
539
|
}
|
|
139
540
|
function parseExpression(raw, path) {
|
|
541
|
+
const expression = parseBasesExpression(raw);
|
|
542
|
+
if (expression.ok)
|
|
543
|
+
return ok({ kind: "expression", expression: expression.value });
|
|
140
544
|
let text = raw.trim();
|
|
141
545
|
while (text.startsWith("(") && text.endsWith(")") && isBalanced(text.slice(1, -1))) {
|
|
142
546
|
text = text.slice(1, -1).trim();
|
|
@@ -189,7 +593,6 @@ function parseValueRef(raw) {
|
|
|
189
593
|
const lower = key.toLowerCase();
|
|
190
594
|
const alias = BUILTIN_ALIASES[lower];
|
|
191
595
|
if (alias !== void 0) {
|
|
192
|
-
if (alias === "date") return { source: "frontmatter", key: "date" };
|
|
193
596
|
return { source: "builtin", name: alias };
|
|
194
597
|
}
|
|
195
598
|
if (lower.startsWith("note.")) {
|
|
@@ -253,6 +656,23 @@ function parseProperties(value) {
|
|
|
253
656
|
}
|
|
254
657
|
return ok(labels);
|
|
255
658
|
}
|
|
659
|
+
function parseFormulas(value) {
|
|
660
|
+
if (value === void 0 || value === null) return ok({});
|
|
661
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
662
|
+
return fail("`formulas` must be a YAML mapping.");
|
|
663
|
+
const formulas = {};
|
|
664
|
+
for (const [name, source] of Object.entries(
|
|
665
|
+
value
|
|
666
|
+
)) {
|
|
667
|
+
if (typeof source !== "string")
|
|
668
|
+
return fail(`formulas.${name} must be a string expression.`);
|
|
669
|
+
const expression = parseBasesExpression(source);
|
|
670
|
+
if (expression.ok === false)
|
|
671
|
+
return fail(`formulas.${name} is invalid (${expression.message}).`);
|
|
672
|
+
formulas[name] = expression.value;
|
|
673
|
+
}
|
|
674
|
+
return ok(formulas);
|
|
675
|
+
}
|
|
256
676
|
function propertyLabel(key, raw) {
|
|
257
677
|
if (typeof raw === "string" && raw.trim() !== "") return raw;
|
|
258
678
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
@@ -286,6 +706,12 @@ function parseView(value, properties, path) {
|
|
|
286
706
|
const columns = parseColumns(object, properties);
|
|
287
707
|
const sort = parseSort(object.sort, `${path}.sort`);
|
|
288
708
|
if (sort.ok === false) return sort;
|
|
709
|
+
const groupBy = parseGroupBy(object.groupBy, `${path}.groupBy`);
|
|
710
|
+
if (groupBy.ok === false) return groupBy;
|
|
711
|
+
const groupOrder = parseGroupOrder(object.groupOrder, `${path}.groupOrder`);
|
|
712
|
+
if (groupOrder.ok === false) return groupOrder;
|
|
713
|
+
const summaries = parseViewSummaries(object.summaries, `${path}.summaries`);
|
|
714
|
+
if (summaries.ok === false) return summaries;
|
|
289
715
|
const limit = parseLimit(object.limit, `${path}.limit`);
|
|
290
716
|
if (limit.ok === false) return limit;
|
|
291
717
|
const name = typeof object.name === "string" && object.name.trim() !== "" ? object.name : void 0;
|
|
@@ -295,6 +721,9 @@ function parseView(value, properties, path) {
|
|
|
295
721
|
columns,
|
|
296
722
|
...filter.value === void 0 ? {} : { filter: filter.value },
|
|
297
723
|
...sort.value === void 0 ? {} : { sort: sort.value },
|
|
724
|
+
...groupBy.value === void 0 ? {} : { groupBy: groupBy.value },
|
|
725
|
+
...groupOrder.value === void 0 ? {} : { groupOrder: groupOrder.value },
|
|
726
|
+
...summaries.value === void 0 ? {} : { summaries: summaries.value },
|
|
298
727
|
...limit.value === void 0 ? {} : { limit: limit.value }
|
|
299
728
|
});
|
|
300
729
|
}
|
|
@@ -330,8 +759,8 @@ function parseSort(value, path) {
|
|
|
330
759
|
if (text === "") continue;
|
|
331
760
|
const descending = text.startsWith("-");
|
|
332
761
|
sorts.push({
|
|
333
|
-
|
|
334
|
-
|
|
762
|
+
property: descending ? text.slice(1) : text,
|
|
763
|
+
direction: descending ? "desc" : "asc"
|
|
335
764
|
});
|
|
336
765
|
continue;
|
|
337
766
|
}
|
|
@@ -342,10 +771,13 @@ function parseSort(value, path) {
|
|
|
342
771
|
return fail(`${path}[${index}] must name a \`property\` or \`field\`.`);
|
|
343
772
|
}
|
|
344
773
|
const direction = record.direction ?? record.order;
|
|
345
|
-
const
|
|
774
|
+
const normalizedDirection = direction === void 0 ? "asc" : typeof direction === "string" ? direction.trim().toLowerCase() : "";
|
|
775
|
+
if (normalizedDirection !== "asc" && normalizedDirection !== "desc") {
|
|
776
|
+
return fail(`${path}[${index}].direction must be "ASC" or "DESC".`);
|
|
777
|
+
}
|
|
346
778
|
sorts.push({
|
|
347
|
-
|
|
348
|
-
|
|
779
|
+
property,
|
|
780
|
+
direction: normalizedDirection
|
|
349
781
|
});
|
|
350
782
|
continue;
|
|
351
783
|
}
|
|
@@ -353,26 +785,51 @@ function parseSort(value, path) {
|
|
|
353
785
|
}
|
|
354
786
|
return ok(sorts.length === 0 ? void 0 : sorts);
|
|
355
787
|
}
|
|
356
|
-
function
|
|
357
|
-
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
788
|
+
function parseGroupBy(value, path) {
|
|
789
|
+
if (value === void 0 || value === null) return ok(void 0);
|
|
790
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
791
|
+
return fail(`${path} must be a mapping.`);
|
|
792
|
+
}
|
|
793
|
+
const record = value;
|
|
794
|
+
if (typeof record.property !== "string" || record.property.trim() === "") {
|
|
795
|
+
return fail(`${path}.property must be a non-empty property name.`);
|
|
796
|
+
}
|
|
797
|
+
const direction = String(record.direction ?? "asc").trim().toLowerCase();
|
|
798
|
+
if (direction !== "asc" && direction !== "desc") {
|
|
799
|
+
return fail(`${path}.direction must be "ASC" or "DESC".`);
|
|
800
|
+
}
|
|
801
|
+
return ok({ property: record.property, direction });
|
|
802
|
+
}
|
|
803
|
+
function parseGroupOrder(value, path) {
|
|
804
|
+
if (value === void 0) return ok(void 0);
|
|
805
|
+
if (!Array.isArray(value)) return fail(`${path} must be a list.`);
|
|
806
|
+
const values = [];
|
|
807
|
+
for (const item of value) {
|
|
808
|
+
if (item === null || typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
|
|
809
|
+
values.push(item);
|
|
810
|
+
} else {
|
|
811
|
+
return fail(
|
|
812
|
+
`${path} values must be strings, numbers, booleans, or null.`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return ok(values);
|
|
817
|
+
}
|
|
818
|
+
function parseViewSummaries(value, path) {
|
|
819
|
+
if (value === void 0 || value === null) return ok(void 0);
|
|
820
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
821
|
+
return fail(`${path} must be a mapping.`);
|
|
375
822
|
}
|
|
823
|
+
const summaries = {};
|
|
824
|
+
for (const [property, summary] of Object.entries(
|
|
825
|
+
value
|
|
826
|
+
)) {
|
|
827
|
+
if (typeof summary !== "string" || summary.trim() === "") {
|
|
828
|
+
return fail(`${path}.${property} must name a summary.`);
|
|
829
|
+
}
|
|
830
|
+
summaries[property] = summary;
|
|
831
|
+
}
|
|
832
|
+
return ok(summaries);
|
|
376
833
|
}
|
|
377
834
|
function parseLimit(value, path) {
|
|
378
835
|
if (value === void 0 || value === null) return ok(void 0);
|
|
@@ -382,97 +839,22 @@ function parseLimit(value, path) {
|
|
|
382
839
|
return ok(Math.floor(value));
|
|
383
840
|
}
|
|
384
841
|
|
|
385
|
-
// src/placeholder.ts
|
|
386
|
-
var BASES_ATTRIBUTE = "data-rr-bases";
|
|
387
|
-
function encodeBasesPayload(payload) {
|
|
388
|
-
return encodeURIComponent(JSON.stringify(payload));
|
|
389
|
-
}
|
|
390
|
-
function decodeBasesPayload(encoded) {
|
|
391
|
-
try {
|
|
392
|
-
const parsed = JSON.parse(decodeURIComponent(encoded));
|
|
393
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
394
|
-
const candidate = parsed;
|
|
395
|
-
if (typeof candidate.source !== "string" || !candidate.spec) return null;
|
|
396
|
-
return candidate;
|
|
397
|
-
} catch {
|
|
398
|
-
return null;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
function createBasesPlaceholder(payload) {
|
|
402
|
-
return `<div ${BASES_ATTRIBUTE}="${encodeBasesPayload(payload)}"></div>`;
|
|
403
|
-
}
|
|
404
|
-
function createBasesPlaceholderPattern() {
|
|
405
|
-
return new RegExp(
|
|
406
|
-
`<div\\b[^>]*\\b${BASES_ATTRIBUTE}="([^"]*)"[^>]*>\\s*</div>`,
|
|
407
|
-
"g"
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// src/remark.ts
|
|
412
|
-
function remarkBases(options = {}) {
|
|
413
|
-
const language = options.language ?? "base";
|
|
414
|
-
return (tree, file) => {
|
|
415
|
-
visit(tree, "code", (node, index, parent) => {
|
|
416
|
-
if (node.lang !== language) return;
|
|
417
|
-
if (!parent || index === void 0) return;
|
|
418
|
-
const parsed = parseYaml(node.value);
|
|
419
|
-
if (parsed.ok === false) {
|
|
420
|
-
reportDiagnostic(file, parsed.message, node);
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
const spec = parseBases(parsed.value);
|
|
424
|
-
if (spec.ok === false) {
|
|
425
|
-
reportDiagnostic(file, spec.message, node);
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
const html = {
|
|
429
|
-
type: "html",
|
|
430
|
-
value: createBasesPlaceholder({ spec: spec.value, source: node.value })
|
|
431
|
-
};
|
|
432
|
-
parent.children.splice(index, 1, html);
|
|
433
|
-
});
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
function parseYaml(source) {
|
|
437
|
-
try {
|
|
438
|
-
const document = `---
|
|
439
|
-
${source}
|
|
440
|
-
---
|
|
441
|
-
`;
|
|
442
|
-
const file = {
|
|
443
|
-
value: document,
|
|
444
|
-
data: {},
|
|
445
|
-
toString: () => document
|
|
446
|
-
};
|
|
447
|
-
matter(file);
|
|
448
|
-
return { ok: true, value: file.data.matter };
|
|
449
|
-
} catch (error) {
|
|
450
|
-
return {
|
|
451
|
-
ok: false,
|
|
452
|
-
message: `Base block is not valid YAML (${formatError(error)}).`
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
function reportDiagnostic(file, message, node) {
|
|
457
|
-
const reporter = file?.message;
|
|
458
|
-
if (typeof reporter !== "function") return;
|
|
459
|
-
const diagnostic2 = reporter.call(file, message, node);
|
|
460
|
-
if (diagnostic2 && typeof diagnostic2 === "object") {
|
|
461
|
-
Object.assign(diagnostic2, { source: "@riebeckite/plugin-bases" });
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
function formatError(error) {
|
|
465
|
-
return error instanceof Error ? error.message : String(error);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
842
|
// src/render.ts
|
|
469
843
|
import {
|
|
470
|
-
escapeHtml,
|
|
471
|
-
escapeHtmlAttribute
|
|
472
|
-
queryContentEntries
|
|
844
|
+
escapeHtml as escapeHtml2,
|
|
845
|
+
escapeHtmlAttribute
|
|
473
846
|
} from "@riebeckite/core";
|
|
474
847
|
|
|
475
848
|
// src/evaluate.ts
|
|
849
|
+
var MAX_EVALUATION_STEPS = 1e4;
|
|
850
|
+
var MAX_EVALUATION_DEPTH = 128;
|
|
851
|
+
var EVALUATION_BUDGET = /* @__PURE__ */ Symbol("bases-evaluation-budget");
|
|
852
|
+
var BasesEvaluationError = class extends Error {
|
|
853
|
+
constructor(message) {
|
|
854
|
+
super(message);
|
|
855
|
+
this.name = "BasesEvaluationError";
|
|
856
|
+
}
|
|
857
|
+
};
|
|
476
858
|
function matchesCondition(condition, entry) {
|
|
477
859
|
switch (condition.kind) {
|
|
478
860
|
case "and":
|
|
@@ -497,21 +879,607 @@ function matchesCondition(condition, entry) {
|
|
|
497
879
|
condition.operator,
|
|
498
880
|
condition.right
|
|
499
881
|
);
|
|
882
|
+
case "expression":
|
|
883
|
+
return Boolean(evaluateBasesExpression(condition.expression, entry));
|
|
500
884
|
default:
|
|
501
885
|
return false;
|
|
502
886
|
}
|
|
503
887
|
}
|
|
888
|
+
function evaluateBasesExpression(expression, entry, formulas = {}, scope = {}, active = /* @__PURE__ */ new Set()) {
|
|
889
|
+
const budget = evaluationBudget(scope);
|
|
890
|
+
if (budget.remaining <= 0) {
|
|
891
|
+
throw new BasesEvaluationError(
|
|
892
|
+
`Formula evaluation exceeded ${MAX_EVALUATION_STEPS} steps.`
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
if (budget.depth >= MAX_EVALUATION_DEPTH) {
|
|
896
|
+
throw new BasesEvaluationError(
|
|
897
|
+
`Formula evaluation exceeded ${MAX_EVALUATION_DEPTH} nested expressions.`
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
budget.remaining -= 1;
|
|
901
|
+
budget.depth += 1;
|
|
902
|
+
try {
|
|
903
|
+
switch (expression.kind) {
|
|
904
|
+
case "literal":
|
|
905
|
+
return expression.value;
|
|
906
|
+
case "array":
|
|
907
|
+
return expression.values.map(
|
|
908
|
+
(value) => evaluateBasesExpression(value, entry, formulas, scope, active)
|
|
909
|
+
);
|
|
910
|
+
case "reference":
|
|
911
|
+
return resolvePath(expression.path, entry, formulas, scope, active);
|
|
912
|
+
case "member":
|
|
913
|
+
return readMember(
|
|
914
|
+
evaluateBasesExpression(
|
|
915
|
+
expression.object,
|
|
916
|
+
entry,
|
|
917
|
+
formulas,
|
|
918
|
+
scope,
|
|
919
|
+
active
|
|
920
|
+
),
|
|
921
|
+
expression.property
|
|
922
|
+
);
|
|
923
|
+
case "index":
|
|
924
|
+
return readMember(
|
|
925
|
+
evaluateBasesExpression(
|
|
926
|
+
expression.object,
|
|
927
|
+
entry,
|
|
928
|
+
formulas,
|
|
929
|
+
scope,
|
|
930
|
+
active
|
|
931
|
+
),
|
|
932
|
+
String(
|
|
933
|
+
evaluateBasesExpression(
|
|
934
|
+
expression.index,
|
|
935
|
+
entry,
|
|
936
|
+
formulas,
|
|
937
|
+
scope,
|
|
938
|
+
active
|
|
939
|
+
)
|
|
940
|
+
)
|
|
941
|
+
);
|
|
942
|
+
case "unary": {
|
|
943
|
+
const value = evaluateBasesExpression(
|
|
944
|
+
expression.value,
|
|
945
|
+
entry,
|
|
946
|
+
formulas,
|
|
947
|
+
scope,
|
|
948
|
+
active
|
|
949
|
+
);
|
|
950
|
+
return expression.operator === "!" ? !value : typeof value === "number" ? -value : Number.NaN;
|
|
951
|
+
}
|
|
952
|
+
case "binary":
|
|
953
|
+
return evaluateBinary(expression, entry, formulas, scope, active);
|
|
954
|
+
case "call":
|
|
955
|
+
return evaluateCall(expression, entry, formulas, scope, active);
|
|
956
|
+
}
|
|
957
|
+
} finally {
|
|
958
|
+
budget.depth -= 1;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
function evaluationBudget(scope) {
|
|
962
|
+
const values = scope;
|
|
963
|
+
const existing = values[EVALUATION_BUDGET];
|
|
964
|
+
if (existing && typeof existing === "object") {
|
|
965
|
+
return existing;
|
|
966
|
+
}
|
|
967
|
+
const budget = {
|
|
968
|
+
remaining: MAX_EVALUATION_STEPS,
|
|
969
|
+
depth: 0
|
|
970
|
+
};
|
|
971
|
+
values[EVALUATION_BUDGET] = budget;
|
|
972
|
+
return budget;
|
|
973
|
+
}
|
|
974
|
+
function resolvePath(path, entry, formulas, scope, active) {
|
|
975
|
+
const [first] = path;
|
|
976
|
+
if (!first) return void 0;
|
|
977
|
+
if (Object.hasOwn(scope, first)) return scope[first];
|
|
978
|
+
if (first === "file") return fileValue(entry);
|
|
979
|
+
if (first === "note") return entry.frontmatter;
|
|
980
|
+
if (first === "formula")
|
|
981
|
+
return new Proxy(
|
|
982
|
+
{},
|
|
983
|
+
{
|
|
984
|
+
get: (_target, property) => resolveFormula(String(property), entry, formulas, scope, active),
|
|
985
|
+
getOwnPropertyDescriptor: (_target, property) => ({
|
|
986
|
+
configurable: true,
|
|
987
|
+
enumerable: true,
|
|
988
|
+
value: resolveFormula(
|
|
989
|
+
String(property),
|
|
990
|
+
entry,
|
|
991
|
+
formulas,
|
|
992
|
+
scope,
|
|
993
|
+
active
|
|
994
|
+
)
|
|
995
|
+
})
|
|
996
|
+
}
|
|
997
|
+
);
|
|
998
|
+
return Object.hasOwn(entry.frontmatter, first) ? entry.frontmatter[first] : void 0;
|
|
999
|
+
}
|
|
1000
|
+
function resolveFormula(name, entry, formulas, scope, active) {
|
|
1001
|
+
const expression = formulas[name];
|
|
1002
|
+
if (!expression || active.has(name)) return void 0;
|
|
1003
|
+
return evaluateBasesExpression(
|
|
1004
|
+
expression,
|
|
1005
|
+
entry,
|
|
1006
|
+
formulas,
|
|
1007
|
+
scope,
|
|
1008
|
+
/* @__PURE__ */ new Set([...active, name])
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
function fileValue(entry) {
|
|
1012
|
+
const path = `${entry.slug}.md`;
|
|
1013
|
+
const name = path.split("/").at(-1) ?? path;
|
|
1014
|
+
return {
|
|
1015
|
+
name,
|
|
1016
|
+
basename: name.replace(/\.md$/i, ""),
|
|
1017
|
+
path,
|
|
1018
|
+
folder: folderOf(entry.slug),
|
|
1019
|
+
ext: "md",
|
|
1020
|
+
size: entry.file?.size,
|
|
1021
|
+
ctime: entry.file?.ctime,
|
|
1022
|
+
mtime: entry.file?.mtime,
|
|
1023
|
+
tags: entry.tags,
|
|
1024
|
+
links: entry.links.map((link) => link.slug ?? link.raw),
|
|
1025
|
+
properties: entry.frontmatter,
|
|
1026
|
+
link: entry.permalink
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
function readMember(value, property) {
|
|
1030
|
+
if (value === null || value === void 0) return void 0;
|
|
1031
|
+
if (property === "__proto__" || property === "prototype" || property === "constructor")
|
|
1032
|
+
return void 0;
|
|
1033
|
+
if (value instanceof Date) return dateMember(value, property);
|
|
1034
|
+
if (property === "length" && (typeof value === "string" || Array.isArray(value)))
|
|
1035
|
+
return value.length;
|
|
1036
|
+
if (typeof value === "object" && Object.hasOwn(value, property))
|
|
1037
|
+
return value[property];
|
|
1038
|
+
return void 0;
|
|
1039
|
+
}
|
|
1040
|
+
function evaluateBinary(expression, entry, formulas, scope, active) {
|
|
1041
|
+
const left = evaluateBasesExpression(
|
|
1042
|
+
expression.left,
|
|
1043
|
+
entry,
|
|
1044
|
+
formulas,
|
|
1045
|
+
scope,
|
|
1046
|
+
active
|
|
1047
|
+
);
|
|
1048
|
+
if (expression.operator === "&&")
|
|
1049
|
+
return left && evaluateBasesExpression(expression.right, entry, formulas, scope, active);
|
|
1050
|
+
if (expression.operator === "||")
|
|
1051
|
+
return left || evaluateBasesExpression(expression.right, entry, formulas, scope, active);
|
|
1052
|
+
const right = evaluateBasesExpression(
|
|
1053
|
+
expression.right,
|
|
1054
|
+
entry,
|
|
1055
|
+
formulas,
|
|
1056
|
+
scope,
|
|
1057
|
+
active
|
|
1058
|
+
);
|
|
1059
|
+
if (["==", "!=", ">", "<", ">=", "<="].includes(expression.operator))
|
|
1060
|
+
return compare(
|
|
1061
|
+
left,
|
|
1062
|
+
expression.operator,
|
|
1063
|
+
right
|
|
1064
|
+
);
|
|
1065
|
+
const dateArithmetic = evaluateDateArithmetic(
|
|
1066
|
+
left,
|
|
1067
|
+
expression.operator,
|
|
1068
|
+
right
|
|
1069
|
+
);
|
|
1070
|
+
if (dateArithmetic !== void 0) return dateArithmetic;
|
|
1071
|
+
if (expression.operator === "+")
|
|
1072
|
+
return typeof left === "string" || typeof right === "string" ? `${left ?? ""}${right ?? ""}` : Number(left) + Number(right);
|
|
1073
|
+
if (expression.operator === "-") return Number(left) - Number(right);
|
|
1074
|
+
if (expression.operator === "*") return Number(left) * Number(right);
|
|
1075
|
+
if (expression.operator === "/")
|
|
1076
|
+
return Number(right) === 0 ? Number.NaN : Number(left) / Number(right);
|
|
1077
|
+
return Number(left) % Number(right);
|
|
1078
|
+
}
|
|
1079
|
+
function evaluateDateArithmetic(left, operator, right) {
|
|
1080
|
+
if (!(left instanceof Date) || Number.isNaN(left.getTime())) return void 0;
|
|
1081
|
+
if (operator === "-" && right instanceof Date) {
|
|
1082
|
+
return left.getTime() - right.getTime();
|
|
1083
|
+
}
|
|
1084
|
+
if (typeof right !== "string" || operator !== "+" && operator !== "-") {
|
|
1085
|
+
return void 0;
|
|
1086
|
+
}
|
|
1087
|
+
const duration2 = parseDuration(right);
|
|
1088
|
+
if (!duration2) return void 0;
|
|
1089
|
+
return offsetDate(left, duration2, operator === "+" ? 1 : -1);
|
|
1090
|
+
}
|
|
1091
|
+
function parseDuration(value) {
|
|
1092
|
+
const match = /^\s*(-?\d+(?:\.\d+)?)\s*(y|year|years|M|month|months|d|day|days|w|week|weeks|h|hour|hours|m|minute|minutes|s|second|seconds)\s*$/.exec(
|
|
1093
|
+
value
|
|
1094
|
+
);
|
|
1095
|
+
if (!match) return void 0;
|
|
1096
|
+
const amount = Number(match[1]);
|
|
1097
|
+
const unit = match[2];
|
|
1098
|
+
return Number.isFinite(amount) && unit ? { amount, unit } : void 0;
|
|
1099
|
+
}
|
|
1100
|
+
function offsetDate(source, duration2, direction) {
|
|
1101
|
+
const date = new Date(source.getTime());
|
|
1102
|
+
const amount = duration2.amount * direction;
|
|
1103
|
+
switch (duration2.unit) {
|
|
1104
|
+
case "y":
|
|
1105
|
+
case "year":
|
|
1106
|
+
case "years":
|
|
1107
|
+
date.setUTCFullYear(date.getUTCFullYear() + amount);
|
|
1108
|
+
return date;
|
|
1109
|
+
case "M":
|
|
1110
|
+
case "month":
|
|
1111
|
+
case "months":
|
|
1112
|
+
date.setUTCMonth(date.getUTCMonth() + amount);
|
|
1113
|
+
return date;
|
|
1114
|
+
case "w":
|
|
1115
|
+
case "week":
|
|
1116
|
+
case "weeks":
|
|
1117
|
+
date.setTime(date.getTime() + amount * 7 * 864e5);
|
|
1118
|
+
return date;
|
|
1119
|
+
case "d":
|
|
1120
|
+
case "day":
|
|
1121
|
+
case "days":
|
|
1122
|
+
date.setTime(date.getTime() + amount * 864e5);
|
|
1123
|
+
return date;
|
|
1124
|
+
case "h":
|
|
1125
|
+
case "hour":
|
|
1126
|
+
case "hours":
|
|
1127
|
+
date.setTime(date.getTime() + amount * 36e5);
|
|
1128
|
+
return date;
|
|
1129
|
+
case "m":
|
|
1130
|
+
case "minute":
|
|
1131
|
+
case "minutes":
|
|
1132
|
+
date.setTime(date.getTime() + amount * 6e4);
|
|
1133
|
+
return date;
|
|
1134
|
+
default:
|
|
1135
|
+
date.setTime(date.getTime() + amount * 1e3);
|
|
1136
|
+
return date;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
function evaluateCall(expression, entry, formulas, scope, active) {
|
|
1140
|
+
if (expression.callee.kind === "member") {
|
|
1141
|
+
const target2 = evaluateBasesExpression(
|
|
1142
|
+
expression.callee.object,
|
|
1143
|
+
entry,
|
|
1144
|
+
formulas,
|
|
1145
|
+
scope,
|
|
1146
|
+
active
|
|
1147
|
+
);
|
|
1148
|
+
const higherOrder = evaluateHigherOrderCall(
|
|
1149
|
+
target2,
|
|
1150
|
+
expression.callee.property,
|
|
1151
|
+
expression.arguments,
|
|
1152
|
+
entry,
|
|
1153
|
+
formulas,
|
|
1154
|
+
scope,
|
|
1155
|
+
active
|
|
1156
|
+
);
|
|
1157
|
+
if (higherOrder.handled) return higherOrder.value;
|
|
1158
|
+
}
|
|
1159
|
+
const args = expression.arguments.map(
|
|
1160
|
+
(argument) => evaluateBasesExpression(argument, entry, formulas, scope, active)
|
|
1161
|
+
);
|
|
1162
|
+
if (expression.callee.kind === "reference")
|
|
1163
|
+
return globalCall(expression.callee.path[0] ?? "", args);
|
|
1164
|
+
if (expression.callee.kind !== "member") return void 0;
|
|
1165
|
+
const target = evaluateBasesExpression(
|
|
1166
|
+
expression.callee.object,
|
|
1167
|
+
entry,
|
|
1168
|
+
formulas,
|
|
1169
|
+
scope,
|
|
1170
|
+
active
|
|
1171
|
+
);
|
|
1172
|
+
return memberCall(target, expression.callee.property, args);
|
|
1173
|
+
}
|
|
1174
|
+
function globalCall(name, args) {
|
|
1175
|
+
if (name === "if") return args[0] ? args[1] : args[2] ?? null;
|
|
1176
|
+
if (name === "list")
|
|
1177
|
+
return args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
1178
|
+
if (name === "number") return Number(args[0]);
|
|
1179
|
+
if (name === "length")
|
|
1180
|
+
return Array.isArray(args[0]) || typeof args[0] === "string" ? args[0].length : 0;
|
|
1181
|
+
if (name === "min") return Math.min(...args.map(Number));
|
|
1182
|
+
if (name === "max") return Math.max(...args.map(Number));
|
|
1183
|
+
if (name === "date") return parseDate(args[0]);
|
|
1184
|
+
if (name === "duration") return duration(args[0], args[1]);
|
|
1185
|
+
if (name === "isTruthy") return Boolean(args[0]);
|
|
1186
|
+
if (name === "isType") return isType(args[0], args[1]);
|
|
1187
|
+
if (name === "toString") return stringValue(args[0]);
|
|
1188
|
+
if (name === "escapeHTML") return escapeHtml(String(args[0] ?? ""));
|
|
1189
|
+
return void 0;
|
|
1190
|
+
}
|
|
1191
|
+
function memberCall(target, name, args) {
|
|
1192
|
+
if (name === "hasTag" && target && typeof target === "object" && "tags" in target)
|
|
1193
|
+
return args.some(
|
|
1194
|
+
(value) => hasTag(target.tags, String(value))
|
|
1195
|
+
);
|
|
1196
|
+
if (name === "hasLink" && target && typeof target === "object" && "links" in target)
|
|
1197
|
+
return hasLink(
|
|
1198
|
+
target.links,
|
|
1199
|
+
String(args[0])
|
|
1200
|
+
);
|
|
1201
|
+
if (name === "inFolder" && target && typeof target === "object" && "folder" in target)
|
|
1202
|
+
return inFolder(
|
|
1203
|
+
String(target.folder),
|
|
1204
|
+
String(args[0])
|
|
1205
|
+
);
|
|
1206
|
+
if (name === "hasProperty" && target && typeof target === "object" && "properties" in target) {
|
|
1207
|
+
const properties = target.properties;
|
|
1208
|
+
return properties !== null && typeof properties === "object" && Object.hasOwn(properties, String(args[0] ?? ""));
|
|
1209
|
+
}
|
|
1210
|
+
if (target instanceof Date) {
|
|
1211
|
+
return dateMethod(target, name, args) ?? objectMethod(target, name);
|
|
1212
|
+
}
|
|
1213
|
+
if (typeof target === "string") return stringMethod(target, name, args);
|
|
1214
|
+
if (Array.isArray(target)) {
|
|
1215
|
+
return arrayMethod(target, name, args) ?? objectMethod(target, name);
|
|
1216
|
+
}
|
|
1217
|
+
if (typeof target === "number") return numberMethod(target, name, args);
|
|
1218
|
+
if (target !== null && typeof target === "object") {
|
|
1219
|
+
return objectMethod(target, name);
|
|
1220
|
+
}
|
|
1221
|
+
return void 0;
|
|
1222
|
+
}
|
|
1223
|
+
function dateMethod(date, name, args) {
|
|
1224
|
+
if (name === "format") return formatDate(date, String(args[0] ?? ""));
|
|
1225
|
+
if (name === "date") return startOfUtcDate(date);
|
|
1226
|
+
if (name === "time") return date.getTime();
|
|
1227
|
+
return void 0;
|
|
1228
|
+
}
|
|
1229
|
+
function stringMethod(target, name, args) {
|
|
1230
|
+
if (name === "lower") return target.toLowerCase();
|
|
1231
|
+
if (name === "upper") return target.toUpperCase();
|
|
1232
|
+
if (name === "contains") return target.includes(String(args[0]));
|
|
1233
|
+
if (name === "containsAll")
|
|
1234
|
+
return args.every((value) => target.includes(String(value)));
|
|
1235
|
+
if (name === "containsAny")
|
|
1236
|
+
return args.some((value) => target.includes(String(value)));
|
|
1237
|
+
if (name === "startsWith") return target.startsWith(String(args[0]));
|
|
1238
|
+
if (name === "endsWith") return target.endsWith(String(args[0]));
|
|
1239
|
+
if (name === "trim") return target.trim();
|
|
1240
|
+
if (name === "reverse") return [...target].reverse().join("");
|
|
1241
|
+
if (name === "repeat")
|
|
1242
|
+
return target.repeat(Math.max(0, Number(args[0] ?? 0)));
|
|
1243
|
+
if (name === "slice")
|
|
1244
|
+
return target.slice(
|
|
1245
|
+
Number(args[0] ?? 0),
|
|
1246
|
+
args[1] === void 0 ? void 0 : Number(args[1])
|
|
1247
|
+
);
|
|
1248
|
+
if (name === "isEmpty") return target.length === 0;
|
|
1249
|
+
if (name === "split") return target.split(String(args[0]));
|
|
1250
|
+
if (name === "replace")
|
|
1251
|
+
return target.split(String(args[0])).join(String(args[1] ?? ""));
|
|
1252
|
+
if (name === "title") return titleCase(target);
|
|
1253
|
+
return void 0;
|
|
1254
|
+
}
|
|
1255
|
+
function arrayMethod(target, name, args) {
|
|
1256
|
+
if (name === "contains")
|
|
1257
|
+
return target.some((value) => scalarEquals(value, args[0]));
|
|
1258
|
+
if (name === "join") return target.join(String(args[0] ?? ","));
|
|
1259
|
+
if (name === "containsAll")
|
|
1260
|
+
return args.every(
|
|
1261
|
+
(value) => target.some((item) => scalarEquals(item, value))
|
|
1262
|
+
);
|
|
1263
|
+
if (name === "containsAny")
|
|
1264
|
+
return args.some(
|
|
1265
|
+
(value) => target.some((item) => scalarEquals(item, value))
|
|
1266
|
+
);
|
|
1267
|
+
if (name === "unique") return [...new Set(target)];
|
|
1268
|
+
if (name === "flat") return target.flat();
|
|
1269
|
+
if (name === "sort") return [...target].sort();
|
|
1270
|
+
if (name === "reverse") return [...target].reverse();
|
|
1271
|
+
if (name === "slice")
|
|
1272
|
+
return target.slice(
|
|
1273
|
+
Number(args[0] ?? 0),
|
|
1274
|
+
args[1] === void 0 ? void 0 : Number(args[1])
|
|
1275
|
+
);
|
|
1276
|
+
if (name === "isEmpty") return target.length === 0;
|
|
1277
|
+
if (name === "mean") return mean(target);
|
|
1278
|
+
if (name === "sum")
|
|
1279
|
+
return target.reduce((total, value) => total + Number(value), 0);
|
|
1280
|
+
if (name === "min") return Math.min(...target.map(Number));
|
|
1281
|
+
if (name === "max") return Math.max(...target.map(Number));
|
|
1282
|
+
return void 0;
|
|
1283
|
+
}
|
|
1284
|
+
function numberMethod(target, name, args) {
|
|
1285
|
+
if (name === "round") {
|
|
1286
|
+
const digits = Number(args[0] ?? 0);
|
|
1287
|
+
const factor = 10 ** digits;
|
|
1288
|
+
return Math.round(target * factor) / factor;
|
|
1289
|
+
}
|
|
1290
|
+
if (name === "floor") return Math.floor(target);
|
|
1291
|
+
if (name === "ceil") return Math.ceil(target);
|
|
1292
|
+
if (name === "abs") return Math.abs(target);
|
|
1293
|
+
if (name === "toFixed") return target.toFixed(Number(args[0] ?? 0));
|
|
1294
|
+
return void 0;
|
|
1295
|
+
}
|
|
1296
|
+
function objectMethod(target, name) {
|
|
1297
|
+
if (name === "isEmpty") return Object.keys(target).length === 0;
|
|
1298
|
+
if (name === "keys") return Object.keys(target);
|
|
1299
|
+
if (name === "values") return Object.values(target);
|
|
1300
|
+
return void 0;
|
|
1301
|
+
}
|
|
1302
|
+
function dateMember(date, property) {
|
|
1303
|
+
if (Number.isNaN(date.getTime())) return void 0;
|
|
1304
|
+
if (property === "year") return date.getUTCFullYear();
|
|
1305
|
+
if (property === "month") return date.getUTCMonth() + 1;
|
|
1306
|
+
if (property === "day") return date.getUTCDate();
|
|
1307
|
+
if (property === "hour") return date.getUTCHours();
|
|
1308
|
+
if (property === "minute") return date.getUTCMinutes();
|
|
1309
|
+
if (property === "second") return date.getUTCSeconds();
|
|
1310
|
+
if (property === "millisecond") return date.getUTCMilliseconds();
|
|
1311
|
+
if (property === "time") return date.getTime();
|
|
1312
|
+
if (property === "date") return startOfUtcDate(date);
|
|
1313
|
+
return void 0;
|
|
1314
|
+
}
|
|
1315
|
+
function parseDate(value) {
|
|
1316
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
1317
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
1318
|
+
return new Date(value);
|
|
1319
|
+
if (typeof value !== "string") return void 0;
|
|
1320
|
+
const source = value.trim();
|
|
1321
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(source)) {
|
|
1322
|
+
const [year, month, day] = source.split("-").map(Number);
|
|
1323
|
+
const date2 = new Date(Date.UTC(year, (month ?? 1) - 1, day));
|
|
1324
|
+
return date2.getUTCFullYear() === year && date2.getUTCMonth() === (month ?? 1) - 1 && date2.getUTCDate() === day ? date2 : void 0;
|
|
1325
|
+
}
|
|
1326
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?Z$/.test(source))
|
|
1327
|
+
return void 0;
|
|
1328
|
+
const date = new Date(source);
|
|
1329
|
+
return Number.isNaN(date.getTime()) ? void 0 : date;
|
|
1330
|
+
}
|
|
1331
|
+
function duration(amount, unit) {
|
|
1332
|
+
if (typeof amount !== "number" || !Number.isFinite(amount)) return void 0;
|
|
1333
|
+
const normalized = String(unit ?? "").trim().toLowerCase();
|
|
1334
|
+
const units = {
|
|
1335
|
+
year: "years",
|
|
1336
|
+
years: "years",
|
|
1337
|
+
month: "months",
|
|
1338
|
+
months: "months",
|
|
1339
|
+
week: "weeks",
|
|
1340
|
+
weeks: "weeks",
|
|
1341
|
+
day: "days",
|
|
1342
|
+
days: "days",
|
|
1343
|
+
hour: "hours",
|
|
1344
|
+
hours: "hours",
|
|
1345
|
+
minute: "minutes",
|
|
1346
|
+
minutes: "minutes",
|
|
1347
|
+
second: "seconds",
|
|
1348
|
+
seconds: "seconds"
|
|
1349
|
+
};
|
|
1350
|
+
const suffix = units[normalized];
|
|
1351
|
+
return suffix ? `${amount} ${suffix}` : void 0;
|
|
1352
|
+
}
|
|
1353
|
+
function isType(value, expected) {
|
|
1354
|
+
const type = String(expected ?? "").toLowerCase();
|
|
1355
|
+
if (type === "null") return value === null;
|
|
1356
|
+
if (type === "array" || type === "list") return Array.isArray(value);
|
|
1357
|
+
if (type === "date")
|
|
1358
|
+
return value instanceof Date && !Number.isNaN(value.getTime());
|
|
1359
|
+
if (type === "object")
|
|
1360
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
1361
|
+
return typeof value === type;
|
|
1362
|
+
}
|
|
1363
|
+
function stringValue(value) {
|
|
1364
|
+
if (value instanceof Date) return value.toISOString();
|
|
1365
|
+
if (Array.isArray(value)) return value.map(stringValue).join(", ");
|
|
1366
|
+
return value === null || value === void 0 ? "" : String(value);
|
|
1367
|
+
}
|
|
1368
|
+
function escapeHtml(value) {
|
|
1369
|
+
return value.replace(
|
|
1370
|
+
/[&<>"']/g,
|
|
1371
|
+
(character) => ({
|
|
1372
|
+
"&": "&",
|
|
1373
|
+
"<": "<",
|
|
1374
|
+
">": ">",
|
|
1375
|
+
'"': """,
|
|
1376
|
+
"'": "'"
|
|
1377
|
+
})[character] ?? character
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
function startOfUtcDate(value) {
|
|
1381
|
+
return new Date(
|
|
1382
|
+
Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate())
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
function formatDate(value, pattern) {
|
|
1386
|
+
if (Number.isNaN(value.getTime())) return void 0;
|
|
1387
|
+
const fields = {
|
|
1388
|
+
yyyy: String(value.getUTCFullYear()).padStart(4, "0"),
|
|
1389
|
+
MM: String(value.getUTCMonth() + 1).padStart(2, "0"),
|
|
1390
|
+
dd: String(value.getUTCDate()).padStart(2, "0"),
|
|
1391
|
+
HH: String(value.getUTCHours()).padStart(2, "0"),
|
|
1392
|
+
mm: String(value.getUTCMinutes()).padStart(2, "0"),
|
|
1393
|
+
ss: String(value.getUTCSeconds()).padStart(2, "0")
|
|
1394
|
+
};
|
|
1395
|
+
return pattern.replace(
|
|
1396
|
+
/yyyy|MM|dd|HH|mm|ss/g,
|
|
1397
|
+
(token) => fields[token] ?? token
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
function titleCase(value) {
|
|
1401
|
+
return value.replace(
|
|
1402
|
+
/(^|[\s_-])(\p{L})/gu,
|
|
1403
|
+
(_match, prefix, letter) => `${prefix}${letter.toLocaleUpperCase()}`
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
function mean(values) {
|
|
1407
|
+
const numbers = values.map(Number).filter(Number.isFinite);
|
|
1408
|
+
return numbers.length === 0 ? Number.NaN : numbers.reduce((total, value) => total + value, 0) / numbers.length;
|
|
1409
|
+
}
|
|
1410
|
+
function evaluateHigherOrderCall(target, name, arguments_, entry, formulas, scope, active) {
|
|
1411
|
+
if (!Array.isArray(target) || !["map", "filter", "reduce"].includes(name)) {
|
|
1412
|
+
return { handled: false, value: void 0 };
|
|
1413
|
+
}
|
|
1414
|
+
const expression = arguments_[0];
|
|
1415
|
+
if (!expression) return { handled: true, value: void 0 };
|
|
1416
|
+
if (name === "map") {
|
|
1417
|
+
return {
|
|
1418
|
+
handled: true,
|
|
1419
|
+
value: target.map(
|
|
1420
|
+
(value, index) => evaluateBasesExpression(
|
|
1421
|
+
expression,
|
|
1422
|
+
entry,
|
|
1423
|
+
formulas,
|
|
1424
|
+
{ ...scope, value, index },
|
|
1425
|
+
active
|
|
1426
|
+
)
|
|
1427
|
+
)
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
if (name === "filter") {
|
|
1431
|
+
return {
|
|
1432
|
+
handled: true,
|
|
1433
|
+
value: target.filter(
|
|
1434
|
+
(value, index) => Boolean(
|
|
1435
|
+
evaluateBasesExpression(
|
|
1436
|
+
expression,
|
|
1437
|
+
entry,
|
|
1438
|
+
formulas,
|
|
1439
|
+
{ ...scope, value, index },
|
|
1440
|
+
active
|
|
1441
|
+
)
|
|
1442
|
+
)
|
|
1443
|
+
)
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
let accumulator = arguments_[1] ? evaluateBasesExpression(arguments_[1], entry, formulas, scope, active) : target[0];
|
|
1447
|
+
const start = arguments_[1] ? 0 : 1;
|
|
1448
|
+
for (let index = start; index < target.length; index += 1) {
|
|
1449
|
+
accumulator = evaluateBasesExpression(
|
|
1450
|
+
expression,
|
|
1451
|
+
entry,
|
|
1452
|
+
formulas,
|
|
1453
|
+
{
|
|
1454
|
+
...scope,
|
|
1455
|
+
acc: accumulator,
|
|
1456
|
+
value: target[index],
|
|
1457
|
+
index
|
|
1458
|
+
},
|
|
1459
|
+
active
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
return { handled: true, value: accumulator };
|
|
1463
|
+
}
|
|
504
1464
|
function resolveValueRef(reference, entry) {
|
|
505
1465
|
if (reference.source === "frontmatter") {
|
|
506
|
-
return entry.frontmatter
|
|
1466
|
+
return readFrontmatter(entry.frontmatter, reference.key);
|
|
507
1467
|
}
|
|
508
1468
|
return resolveBuiltin(reference.name, entry);
|
|
509
1469
|
}
|
|
1470
|
+
function readFrontmatter(frontmatter, key) {
|
|
1471
|
+
if (Object.hasOwn(frontmatter, key)) return frontmatter[key];
|
|
1472
|
+
return key.split(".").reduce((value, part) => {
|
|
1473
|
+
if (!value || typeof value !== "object") return void 0;
|
|
1474
|
+
return Object.hasOwn(value, part) ? value[part] : void 0;
|
|
1475
|
+
}, frontmatter);
|
|
1476
|
+
}
|
|
510
1477
|
function resolveBuiltin(name, entry) {
|
|
511
1478
|
switch (name) {
|
|
512
1479
|
case "file.name":
|
|
513
|
-
return entry.
|
|
1480
|
+
return `${entry.slug.split("/").at(-1) ?? entry.slug}.md`;
|
|
514
1481
|
case "file.path":
|
|
1482
|
+
return `${entry.slug}.md`;
|
|
515
1483
|
case "file.slug":
|
|
516
1484
|
return entry.slug;
|
|
517
1485
|
case "file.folder":
|
|
@@ -523,6 +1491,14 @@ function resolveBuiltin(name, entry) {
|
|
|
523
1491
|
return entry.permalink;
|
|
524
1492
|
case "file.tags":
|
|
525
1493
|
return entry.tags;
|
|
1494
|
+
case "file.ext":
|
|
1495
|
+
return "md";
|
|
1496
|
+
case "file.size":
|
|
1497
|
+
return entry.file?.size;
|
|
1498
|
+
case "file.mtime":
|
|
1499
|
+
return entry.file?.mtime;
|
|
1500
|
+
case "file.ctime":
|
|
1501
|
+
return entry.file?.ctime;
|
|
526
1502
|
default:
|
|
527
1503
|
return void 0;
|
|
528
1504
|
}
|
|
@@ -640,6 +1616,158 @@ function toComparableNumber(value) {
|
|
|
640
1616
|
return null;
|
|
641
1617
|
}
|
|
642
1618
|
|
|
1619
|
+
// src/transform.ts
|
|
1620
|
+
function resolveBasesProperty(entry, property, spec) {
|
|
1621
|
+
if (property.toLowerCase().startsWith("formula.")) {
|
|
1622
|
+
const expression = spec.formulas[property.slice("formula.".length)];
|
|
1623
|
+
return expression ? evaluateBasesExpression(expression, entry, spec.formulas) : void 0;
|
|
1624
|
+
}
|
|
1625
|
+
return resolveValueRef(parseValueRef(property), entry);
|
|
1626
|
+
}
|
|
1627
|
+
function sortBasesEntries(entries, view, spec) {
|
|
1628
|
+
const indexed = entries.map((entry, index) => ({ entry, index }));
|
|
1629
|
+
return indexed.sort((left, right) => {
|
|
1630
|
+
for (const sort of view.sort ?? []) {
|
|
1631
|
+
const result = compareValues(
|
|
1632
|
+
resolveBasesProperty(left.entry, sort.property, spec),
|
|
1633
|
+
resolveBasesProperty(right.entry, sort.property, spec)
|
|
1634
|
+
);
|
|
1635
|
+
if (result !== 0) return sort.direction === "desc" ? -result : result;
|
|
1636
|
+
}
|
|
1637
|
+
return left.index - right.index;
|
|
1638
|
+
}).map(({ entry }) => entry);
|
|
1639
|
+
}
|
|
1640
|
+
function groupBasesEntries(entries, view, spec) {
|
|
1641
|
+
if (!view.groupBy) return [{ key: null, entries }];
|
|
1642
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1643
|
+
for (const entry of entries) {
|
|
1644
|
+
const value = resolveBasesProperty(entry, view.groupBy.property, spec);
|
|
1645
|
+
for (const key of groupKeys(value)) {
|
|
1646
|
+
const identity = groupIdentity(key);
|
|
1647
|
+
const group = groups.get(identity);
|
|
1648
|
+
if (group) group.entries.push(entry);
|
|
1649
|
+
else groups.set(identity, { key, entries: [entry] });
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
const all = [...groups.values()];
|
|
1653
|
+
const ordered = view.groupOrder ? view.groupOrder.map(
|
|
1654
|
+
(key) => groups.get(groupIdentity(key)) ?? {
|
|
1655
|
+
key,
|
|
1656
|
+
entries: []
|
|
1657
|
+
}
|
|
1658
|
+
) : all.sort((left, right) => {
|
|
1659
|
+
const result = compareValues(left.key, right.key);
|
|
1660
|
+
return view.groupBy?.direction === "desc" ? -result : result;
|
|
1661
|
+
});
|
|
1662
|
+
return ordered.map((group) => ({ key: group.key, entries: group.entries }));
|
|
1663
|
+
}
|
|
1664
|
+
function summarizeBasesValues(values, name, spec) {
|
|
1665
|
+
const custom = spec.summaries[name];
|
|
1666
|
+
if (custom) {
|
|
1667
|
+
const entry = emptyEntry;
|
|
1668
|
+
return evaluateBasesExpression(custom, entry, spec.formulas, { values });
|
|
1669
|
+
}
|
|
1670
|
+
const normalized = name.trim().toLowerCase();
|
|
1671
|
+
const present = values.filter((value) => !isEmpty(value));
|
|
1672
|
+
const numbers = present.filter(isFiniteNumber);
|
|
1673
|
+
const dates = present.map(toDate).filter((value) => value !== null);
|
|
1674
|
+
if (normalized === "empty") return values.length - present.length;
|
|
1675
|
+
if (normalized === "filled") return present.length;
|
|
1676
|
+
if (normalized === "unique") return new Set(present.map(valueIdentity)).size;
|
|
1677
|
+
if (normalized === "checked")
|
|
1678
|
+
return present.filter((value) => value === true).length;
|
|
1679
|
+
if (normalized === "unchecked")
|
|
1680
|
+
return present.filter((value) => value === false).length;
|
|
1681
|
+
if (normalized === "earliest")
|
|
1682
|
+
return dates.length ? new Date(Math.min(...dates.map((value) => value.getTime()))) : null;
|
|
1683
|
+
if (normalized === "latest")
|
|
1684
|
+
return dates.length ? new Date(Math.max(...dates.map((value) => value.getTime()))) : null;
|
|
1685
|
+
if (normalized === "range" && dates.length)
|
|
1686
|
+
return Math.max(...dates.map((value) => value.getTime())) - Math.min(...dates.map((value) => value.getTime()));
|
|
1687
|
+
if (!numbers.length) return null;
|
|
1688
|
+
if (normalized === "sum")
|
|
1689
|
+
return numbers.reduce((sum, value) => sum + value, 0);
|
|
1690
|
+
if (normalized === "average")
|
|
1691
|
+
return numbers.reduce((sum, value) => sum + value, 0) / numbers.length;
|
|
1692
|
+
if (normalized === "min") return Math.min(...numbers);
|
|
1693
|
+
if (normalized === "max") return Math.max(...numbers);
|
|
1694
|
+
if (normalized === "range")
|
|
1695
|
+
return Math.max(...numbers) - Math.min(...numbers);
|
|
1696
|
+
if (normalized === "median") {
|
|
1697
|
+
const ordered = [...numbers].sort((left, right) => left - right);
|
|
1698
|
+
const middle = Math.floor(ordered.length / 2);
|
|
1699
|
+
return ordered.length % 2 ? ordered[middle] : ((ordered[middle - 1] ?? 0) + (ordered[middle] ?? 0)) / 2;
|
|
1700
|
+
}
|
|
1701
|
+
if (normalized === "stddev") {
|
|
1702
|
+
const mean2 = numbers.reduce((sum, value) => sum + value, 0) / numbers.length;
|
|
1703
|
+
return Math.sqrt(
|
|
1704
|
+
numbers.reduce((sum, value) => sum + (value - mean2) ** 2, 0) / numbers.length
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1709
|
+
function groupKeys(value) {
|
|
1710
|
+
if (Array.isArray(value))
|
|
1711
|
+
return value.length ? value.flatMap(groupKeys) : [null];
|
|
1712
|
+
return isGroupKey(value) ? [value] : [null];
|
|
1713
|
+
}
|
|
1714
|
+
function isGroupKey(value) {
|
|
1715
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date && Number.isFinite(value.getTime());
|
|
1716
|
+
}
|
|
1717
|
+
function compareValues(left, right) {
|
|
1718
|
+
const leftEmpty = isEmpty(left);
|
|
1719
|
+
const rightEmpty = isEmpty(right);
|
|
1720
|
+
if (leftEmpty && rightEmpty) return 0;
|
|
1721
|
+
if (leftEmpty) return 1;
|
|
1722
|
+
if (rightEmpty) return -1;
|
|
1723
|
+
const leftDate = toDate(left);
|
|
1724
|
+
const rightDate = toDate(right);
|
|
1725
|
+
if (leftDate && rightDate) return leftDate.getTime() - rightDate.getTime();
|
|
1726
|
+
if (typeof left === "number" && typeof right === "number")
|
|
1727
|
+
return left - right;
|
|
1728
|
+
if (typeof left === "boolean" && typeof right === "boolean")
|
|
1729
|
+
return Number(left) - Number(right);
|
|
1730
|
+
const typeOrder = valueTypeOrder(left) - valueTypeOrder(right);
|
|
1731
|
+
if (typeOrder !== 0) return typeOrder;
|
|
1732
|
+
return String(left).localeCompare(String(right), "en");
|
|
1733
|
+
}
|
|
1734
|
+
function valueTypeOrder(value) {
|
|
1735
|
+
if (typeof value === "number") return 0;
|
|
1736
|
+
if (toDate(value)) return 1;
|
|
1737
|
+
if (typeof value === "boolean") return 2;
|
|
1738
|
+
return 3;
|
|
1739
|
+
}
|
|
1740
|
+
function isEmpty(value) {
|
|
1741
|
+
return value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
|
|
1742
|
+
}
|
|
1743
|
+
function isFiniteNumber(value) {
|
|
1744
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1745
|
+
}
|
|
1746
|
+
function toDate(value) {
|
|
1747
|
+
if (value instanceof Date && Number.isFinite(value.getTime())) return value;
|
|
1748
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}(?:T|\s|$)/.test(value.trim()))
|
|
1749
|
+
return null;
|
|
1750
|
+
const time = Date.parse(value);
|
|
1751
|
+
return Number.isNaN(time) ? null : new Date(time);
|
|
1752
|
+
}
|
|
1753
|
+
function valueIdentity(value) {
|
|
1754
|
+
if (value instanceof Date) return `date:${value.getTime()}`;
|
|
1755
|
+
return `${typeof value}:${String(value)}`;
|
|
1756
|
+
}
|
|
1757
|
+
function groupIdentity(value) {
|
|
1758
|
+
if (value === null) return "null";
|
|
1759
|
+
if (value instanceof Date) return `date:${value.getTime()}`;
|
|
1760
|
+
return `${typeof value}:${String(value)}`;
|
|
1761
|
+
}
|
|
1762
|
+
var emptyEntry = {
|
|
1763
|
+
slug: "",
|
|
1764
|
+
permalink: "",
|
|
1765
|
+
title: "",
|
|
1766
|
+
tags: [],
|
|
1767
|
+
links: [],
|
|
1768
|
+
frontmatter: {}
|
|
1769
|
+
};
|
|
1770
|
+
|
|
643
1771
|
// src/render.ts
|
|
644
1772
|
var TITLE_PROPERTIES = /* @__PURE__ */ new Set(["file.name", "file.title", "title", "name"]);
|
|
645
1773
|
var LINK_PROPERTIES = /* @__PURE__ */ new Set([
|
|
@@ -665,14 +1793,16 @@ function renderBases(spec, options, entries, source) {
|
|
|
665
1793
|
const className = options.className ?? "rb-bases";
|
|
666
1794
|
const views = selectViews(spec, options.view);
|
|
667
1795
|
const limit = resolveLimit(options.limit);
|
|
668
|
-
const sections = views.map(
|
|
1796
|
+
const sections = views.map(
|
|
1797
|
+
(view, index) => renderView(view, spec, className, entries, limit, index)
|
|
1798
|
+
).join("");
|
|
669
1799
|
const fallback = options.showFallback === false ? "" : renderFallback(className, source);
|
|
670
1800
|
const firstType = views[0]?.type ?? "table";
|
|
671
1801
|
return `<div class="${escapeHtmlAttribute(className)}" data-bases data-bases-view="${firstType}">${sections}${fallback}</div>`;
|
|
672
1802
|
}
|
|
673
1803
|
function renderBasesError(message, options) {
|
|
674
1804
|
const className = options.className ?? "rb-bases";
|
|
675
|
-
return `<div class="${escapeHtmlAttribute(className)} ${escapeHtmlAttribute(className)}--error" role="status">${
|
|
1805
|
+
return `<div class="${escapeHtmlAttribute(className)} ${escapeHtmlAttribute(className)}--error" role="status">${escapeHtml2(message)}</div>`;
|
|
676
1806
|
}
|
|
677
1807
|
function selectViews(spec, requested) {
|
|
678
1808
|
const configured = spec.views;
|
|
@@ -698,76 +1828,105 @@ function resolveLimit(limit) {
|
|
|
698
1828
|
}
|
|
699
1829
|
return 100;
|
|
700
1830
|
}
|
|
701
|
-
function renderView(view, spec, className, entries, limit) {
|
|
1831
|
+
function renderView(view, spec, className, entries, limit, index) {
|
|
702
1832
|
const pool = entries.filter(
|
|
703
|
-
(entry) => view.filter === void 0 || matchesCondition(view.filter, entry)
|
|
1833
|
+
(entry) => (spec.filter === void 0 || matchesCondition(spec.filter, entry)) && (view.filter === void 0 || matchesCondition(view.filter, entry))
|
|
704
1834
|
);
|
|
705
1835
|
const effectiveLimit = Math.min(view.limit ?? limit, limit);
|
|
706
|
-
const selected =
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (
|
|
1836
|
+
const selected = sortBasesEntries(pool, view, spec).slice(0, effectiveLimit);
|
|
1837
|
+
const groups = groupBasesEntries(selected, view, spec);
|
|
1838
|
+
const name = view.name ? `<h3 class="${escapeHtmlAttribute(className)}__view-name">${escapeHtml2(view.name)}</h3>` : "";
|
|
1839
|
+
const count = `<p class="${escapeHtmlAttribute(className)}__count">${selected.length} matching ${selected.length === 1 ? "note" : "notes"}</p>`;
|
|
1840
|
+
const body = view.type === "cards" ? renderCards(view, groups, spec, className, index) : renderTable(view, groups, spec, className);
|
|
1841
|
+
return `<section class="${escapeHtmlAttribute(className)}__view" data-bases-view="${view.type}" aria-label="${escapeHtmlAttribute(view.name ?? `${view.type} view ${index + 1}`)}">${name}${count}${body}</section>`;
|
|
1842
|
+
}
|
|
1843
|
+
function renderTable(view, groups, spec, className) {
|
|
1844
|
+
const entries = groups.flatMap((group) => group.entries);
|
|
1845
|
+
if (groups.length === 0) return renderEmpty(className);
|
|
716
1846
|
const head = view.columns.map(
|
|
717
|
-
(property) => `<th class="${escapeHtmlAttribute(className)}__heading" scope="col">${
|
|
1847
|
+
(property) => `<th class="${escapeHtmlAttribute(className)}__heading" scope="col">${escapeHtml2(columnLabel(property, spec))}</th>`
|
|
718
1848
|
).join("");
|
|
719
|
-
const rows =
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1849
|
+
const rows = groups.map((group) => {
|
|
1850
|
+
const heading = view.groupBy ? `<tr class="${escapeHtmlAttribute(className)}__group"><th colspan="${view.columns.length}" scope="rowgroup">${escapeHtml2(formatValue(group.key) || "No value")} <span class="${escapeHtmlAttribute(className)}__group-count">(${group.entries.length})</span></th></tr>${renderSummaryRow(view, group.entries, spec, className, "group")}` : "";
|
|
1851
|
+
const values = group.entries.map((entry) => {
|
|
1852
|
+
const cells = view.columns.map(
|
|
1853
|
+
(property) => `<td class="${escapeHtmlAttribute(className)}__cell">${renderCell(entry, property, className, spec)}</td>`
|
|
1854
|
+
).join("");
|
|
1855
|
+
return `<tr class="${escapeHtmlAttribute(className)}__row">${cells}</tr>`;
|
|
1856
|
+
}).join("");
|
|
1857
|
+
const empty = group.entries.length === 0 ? `<tr class="${escapeHtmlAttribute(className)}__empty-row"><td colspan="${view.columns.length}">${escapeHtml2("No matching notes in this group.")}</td></tr>` : "";
|
|
1858
|
+
return `<tbody>${heading}${values}${empty}</tbody>`;
|
|
724
1859
|
}).join("");
|
|
725
|
-
|
|
1860
|
+
const footer = renderSummaryRow(view, entries, spec, className, "overall");
|
|
1861
|
+
const table = [
|
|
726
1862
|
`<table class="${escapeHtmlAttribute(className)}__table">`,
|
|
1863
|
+
`<caption class="${escapeHtmlAttribute(className)}__sr-only">${entries.length} matching ${entries.length === 1 ? "note" : "notes"}</caption>`,
|
|
727
1864
|
`<thead><tr>${head}</tr></thead>`,
|
|
728
|
-
|
|
1865
|
+
rows,
|
|
1866
|
+
footer ? `<tfoot>${footer}</tfoot>` : "",
|
|
729
1867
|
"</table>"
|
|
730
1868
|
].join("");
|
|
1869
|
+
return `<div class="${escapeHtmlAttribute(className)}__table-scroll" role="region" aria-label="${escapeHtmlAttribute(view.name ?? "Table view")}" tabindex="0">${table}</div>`;
|
|
731
1870
|
}
|
|
732
|
-
function renderCards(view,
|
|
733
|
-
if (
|
|
1871
|
+
function renderCards(view, groups, spec, className, viewIndex) {
|
|
1872
|
+
if (groups.length === 0) return renderEmpty(className);
|
|
734
1873
|
const titleProperty = view.columns.find(
|
|
735
1874
|
(property) => TITLE_PROPERTIES.has(property.toLowerCase())
|
|
736
1875
|
);
|
|
737
1876
|
const fields = view.columns.filter((property) => property !== titleProperty);
|
|
738
|
-
const
|
|
739
|
-
const
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
const
|
|
743
|
-
|
|
1877
|
+
const cards = groups.map((group, index) => {
|
|
1878
|
+
const groupId = `rb-bases-group-${viewIndex}-${index}`;
|
|
1879
|
+
const heading = view.groupBy ? `<h4 class="${escapeHtmlAttribute(className)}__group-title" id="${escapeHtmlAttribute(groupId)}">${escapeHtml2(formatValue(group.key) || "No value")} <span class="${escapeHtmlAttribute(className)}__group-count">(${group.entries.length})</span></h4>` : "";
|
|
1880
|
+
const items = group.entries.map((entry) => {
|
|
1881
|
+
const title = `<a class="${escapeHtmlAttribute(className)}__link" href="${escapeHtmlAttribute(entry.permalink)}">${escapeHtml2(entry.title)}</a>`;
|
|
1882
|
+
const fieldHtml = fields.map((property) => {
|
|
1883
|
+
const label = escapeHtml2(columnLabel(property, spec));
|
|
1884
|
+
const value = renderCell(entry, property, className, spec);
|
|
1885
|
+
return `<div class="${escapeHtmlAttribute(className)}__field"><dt class="${escapeHtmlAttribute(className)}__field-label">${label}</dt><dd class="${escapeHtmlAttribute(className)}__field-value">${value}</dd></div>`;
|
|
1886
|
+
}).join("");
|
|
1887
|
+
const fieldsBlock = fieldHtml ? `<dl class="${escapeHtmlAttribute(className)}__fields">${fieldHtml}</dl>` : "";
|
|
1888
|
+
return `<li class="${escapeHtmlAttribute(className)}__card"><div class="${escapeHtmlAttribute(className)}__card-title">${title}</div>${fieldsBlock}</li>`;
|
|
744
1889
|
}).join("");
|
|
745
|
-
const
|
|
746
|
-
|
|
1890
|
+
const empty = group.entries.length === 0 ? `<li class="${escapeHtmlAttribute(className)}__card-empty">No matching notes in this group.</li>` : "";
|
|
1891
|
+
const labelledBy = view.groupBy ? ` aria-labelledby="${escapeHtmlAttribute(groupId)}"` : ' aria-label="Cards"';
|
|
1892
|
+
return `<section class="${escapeHtmlAttribute(className)}__card-group">${heading}<ul class="${escapeHtmlAttribute(className)}__cards"${labelledBy}>${items}${empty}</ul></section>`;
|
|
747
1893
|
}).join("");
|
|
748
|
-
return
|
|
1894
|
+
return cards;
|
|
749
1895
|
}
|
|
750
1896
|
function renderEmpty(className) {
|
|
751
1897
|
return `<p class="${escapeHtmlAttribute(className)}__empty">No matching notes.</p>`;
|
|
752
1898
|
}
|
|
753
1899
|
function renderFallback(className, source) {
|
|
754
|
-
return `<details class="${escapeHtmlAttribute(className)}__fallback"><summary>Base definition</summary><pre><code>${
|
|
1900
|
+
return `<details class="${escapeHtmlAttribute(className)}__fallback"><summary>Base definition</summary><pre><code>${escapeHtml2(source)}</code></pre></details>`;
|
|
755
1901
|
}
|
|
756
|
-
function renderCell(entry, property, className) {
|
|
1902
|
+
function renderCell(entry, property, className, spec) {
|
|
757
1903
|
const key = property.toLowerCase();
|
|
758
1904
|
if (TITLE_PROPERTIES.has(key)) {
|
|
759
|
-
return `<a class="${escapeHtmlAttribute(className)}__link" href="${escapeHtmlAttribute(entry.permalink)}">${
|
|
1905
|
+
return `<a class="${escapeHtmlAttribute(className)}__link" href="${escapeHtmlAttribute(entry.permalink)}">${escapeHtml2(entry.title)}</a>`;
|
|
760
1906
|
}
|
|
761
1907
|
if (LINK_PROPERTIES.has(key)) {
|
|
762
|
-
return `<a class="${escapeHtmlAttribute(className)}__link" href="${escapeHtmlAttribute(entry.permalink)}">${
|
|
1908
|
+
return `<a class="${escapeHtmlAttribute(className)}__link" href="${escapeHtmlAttribute(entry.permalink)}">${escapeHtml2(entry.permalink)}</a>`;
|
|
763
1909
|
}
|
|
764
1910
|
if (TAG_PROPERTIES.has(key)) {
|
|
765
1911
|
return entry.tags.length === 0 ? "" : `<span class="${escapeHtmlAttribute(className)}__tags">${entry.tags.map(
|
|
766
|
-
(tag) => `<span class="${escapeHtmlAttribute(className)}__tag">#${
|
|
1912
|
+
(tag) => `<span class="${escapeHtmlAttribute(className)}__tag">#${escapeHtml2(tag)}</span>`
|
|
767
1913
|
).join(" ")}</span>`;
|
|
768
1914
|
}
|
|
769
|
-
const value =
|
|
770
|
-
return
|
|
1915
|
+
const value = resolveBasesProperty(entry, property, spec);
|
|
1916
|
+
return escapeHtml2(formatValue(value));
|
|
1917
|
+
}
|
|
1918
|
+
function renderSummaryRow(view, entries, spec, className, kind) {
|
|
1919
|
+
if (!view.summaries) return "";
|
|
1920
|
+
const cells = view.columns.map((property) => {
|
|
1921
|
+
const summary = view.summaries?.[property];
|
|
1922
|
+
if (!summary)
|
|
1923
|
+
return `<td class="${escapeHtmlAttribute(className)}__summary"></td>`;
|
|
1924
|
+
const values = entries.map(
|
|
1925
|
+
(entry) => resolveBasesProperty(entry, property, spec)
|
|
1926
|
+
);
|
|
1927
|
+
return `<td class="${escapeHtmlAttribute(className)}__summary">${escapeHtml2(formatValue(summarizeBasesValues(values, summary, spec)))}<span class="${escapeHtmlAttribute(className)}__sr-only"> ${escapeHtml2(`${columnLabel(property, spec)} summary`)}</span></td>`;
|
|
1928
|
+
}).join("");
|
|
1929
|
+
return `<tr class="${escapeHtmlAttribute(className)}__summary-row ${escapeHtmlAttribute(className)}__summary-row--${kind}">${cells}</tr>`;
|
|
771
1930
|
}
|
|
772
1931
|
function columnLabel(property, spec) {
|
|
773
1932
|
const configured = spec.properties[property];
|
|
@@ -780,7 +1939,7 @@ function columnLabel(property, spec) {
|
|
|
780
1939
|
}
|
|
781
1940
|
function formatValue(value) {
|
|
782
1941
|
if (value === void 0 || value === null) return "";
|
|
783
|
-
if (value instanceof Date) return
|
|
1942
|
+
if (value instanceof Date) return formatDate2(value);
|
|
784
1943
|
if (Array.isArray(value))
|
|
785
1944
|
return value.map((item) => formatValue(item)).join(", ");
|
|
786
1945
|
if (typeof value === "string") {
|
|
@@ -788,45 +1947,272 @@ function formatValue(value) {
|
|
|
788
1947
|
}
|
|
789
1948
|
return String(value);
|
|
790
1949
|
}
|
|
791
|
-
function
|
|
1950
|
+
function formatDate2(value) {
|
|
792
1951
|
const time = value.getTime();
|
|
793
1952
|
if (!Number.isFinite(time)) return "";
|
|
794
1953
|
return value.toISOString().slice(0, 10);
|
|
795
1954
|
}
|
|
796
1955
|
|
|
1956
|
+
// src/yaml.ts
|
|
1957
|
+
import { matter } from "vfile-matter";
|
|
1958
|
+
function parseBasesYaml(source) {
|
|
1959
|
+
try {
|
|
1960
|
+
const document = `---
|
|
1961
|
+
${source}
|
|
1962
|
+
---
|
|
1963
|
+
`;
|
|
1964
|
+
const file = {
|
|
1965
|
+
value: document,
|
|
1966
|
+
data: {},
|
|
1967
|
+
toString: () => document
|
|
1968
|
+
};
|
|
1969
|
+
matter(file);
|
|
1970
|
+
return { ok: true, value: file.data.matter };
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
return {
|
|
1973
|
+
ok: false,
|
|
1974
|
+
message: `Base definition is not valid YAML (${error instanceof Error ? error.message : String(error)}).`
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// src/page.ts
|
|
1980
|
+
var BASE_SUFFIX = /\.base$/i;
|
|
1981
|
+
async function listBaseDocuments(input) {
|
|
1982
|
+
if (!input.contentSource) return [];
|
|
1983
|
+
const documents = [];
|
|
1984
|
+
for (const slug of publicBaseSlugs(input.manifest)) {
|
|
1985
|
+
const pathname = basePath(slug);
|
|
1986
|
+
if (!pathname || input.manifest.byRoutablePermalink.has(pathname) || input.manifest.publicRedirects.has(pathname)) {
|
|
1987
|
+
if (pathname)
|
|
1988
|
+
input.diagnostics.push(
|
|
1989
|
+
diagnostic(
|
|
1990
|
+
slug,
|
|
1991
|
+
`Base page \`${pathname}\` conflicts with an existing public route.`
|
|
1992
|
+
)
|
|
1993
|
+
);
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1996
|
+
const source = await readBase(input.contentSource, slug);
|
|
1997
|
+
if (!source) {
|
|
1998
|
+
input.diagnostics.push(
|
|
1999
|
+
diagnostic(
|
|
2000
|
+
slug,
|
|
2001
|
+
`Base \`${slug}\` is not a valid Bases YAML document.`
|
|
2002
|
+
)
|
|
2003
|
+
);
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
const parsed = parseBase(source);
|
|
2007
|
+
if (parsed.ok === false) {
|
|
2008
|
+
input.diagnostics.push(
|
|
2009
|
+
diagnostic(
|
|
2010
|
+
slug,
|
|
2011
|
+
`Base \`${slug}\` is not a valid Bases YAML document.`
|
|
2012
|
+
)
|
|
2013
|
+
);
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
documents.push({
|
|
2017
|
+
pathname,
|
|
2018
|
+
dependencies: [
|
|
2019
|
+
{ type: "file", path: slug },
|
|
2020
|
+
analyzeBasesDependencies(parsed.value)
|
|
2021
|
+
]
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
return documents;
|
|
2025
|
+
}
|
|
2026
|
+
async function resolveBaseDocument(input, options) {
|
|
2027
|
+
if (!input.contentSource) return null;
|
|
2028
|
+
const slug = publicBaseSlugs(input.manifest).find(
|
|
2029
|
+
(candidate) => basePath(candidate) === input.pathname
|
|
2030
|
+
);
|
|
2031
|
+
if (!slug || input.manifest.byRoutablePermalink.has(input.pathname) || input.manifest.publicRedirects.has(input.pathname))
|
|
2032
|
+
return null;
|
|
2033
|
+
const source = await readBase(input.contentSource, slug);
|
|
2034
|
+
if (!source) return null;
|
|
2035
|
+
const parsed = parseBase(source);
|
|
2036
|
+
if (parsed.ok === false) {
|
|
2037
|
+
input.diagnostics.push(diagnostic(slug, parsed.message));
|
|
2038
|
+
return null;
|
|
2039
|
+
}
|
|
2040
|
+
const title = slug.split("/").at(-1)?.replace(BASE_SUFFIX, "") ?? slug;
|
|
2041
|
+
let body;
|
|
2042
|
+
try {
|
|
2043
|
+
body = renderBases(
|
|
2044
|
+
parsed.value,
|
|
2045
|
+
options,
|
|
2046
|
+
input.manifest.discoverableEntries,
|
|
2047
|
+
source
|
|
2048
|
+
);
|
|
2049
|
+
} catch (error) {
|
|
2050
|
+
const message = `Base \`${slug}\` failed to render (${formatError(error)}).`;
|
|
2051
|
+
input.diagnostics.push(diagnostic(slug, message));
|
|
2052
|
+
body = renderBasesError(message, options);
|
|
2053
|
+
}
|
|
2054
|
+
return {
|
|
2055
|
+
type: "bases-document",
|
|
2056
|
+
pathname: input.pathname,
|
|
2057
|
+
title,
|
|
2058
|
+
body: `<main class="rb-bases-page"><h1 class="rb-bases-page__title">${escapeHtml3(title)}</h1>${body}</main>`
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
function publicBaseSlugs(manifest) {
|
|
2062
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
2063
|
+
for (const entry of manifest.publicEntries)
|
|
2064
|
+
for (const link of entry.links)
|
|
2065
|
+
if (link.kind === "attachment" && link.slug && BASE_SUFFIX.test(link.slug))
|
|
2066
|
+
slugs.add(link.slug);
|
|
2067
|
+
return [...slugs].sort();
|
|
2068
|
+
}
|
|
2069
|
+
function basePath(slug) {
|
|
2070
|
+
const pathname = `/${slug.replace(BASE_SUFFIX, "")}`.replace(/\/+/g, "/");
|
|
2071
|
+
return pathname === "/" ? null : pathname;
|
|
2072
|
+
}
|
|
2073
|
+
async function readBase(source, slug) {
|
|
2074
|
+
try {
|
|
2075
|
+
const raw = await readContentSourceEntry(source, slug);
|
|
2076
|
+
return raw === null ? null : typeof raw === "string" ? raw : new TextDecoder().decode(raw);
|
|
2077
|
+
} catch {
|
|
2078
|
+
return null;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
function parseBase(source) {
|
|
2082
|
+
const yaml = parseBasesYaml(source);
|
|
2083
|
+
if (yaml.ok === false) return yaml;
|
|
2084
|
+
return parseBases(yaml.value);
|
|
2085
|
+
}
|
|
2086
|
+
function formatError(error) {
|
|
2087
|
+
return error instanceof Error ? error.message : String(error);
|
|
2088
|
+
}
|
|
2089
|
+
function diagnostic(slug, message) {
|
|
2090
|
+
return {
|
|
2091
|
+
code: "bases-invalid",
|
|
2092
|
+
severity: "warning",
|
|
2093
|
+
pluginName: "bases",
|
|
2094
|
+
target: slug,
|
|
2095
|
+
message
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// src/remark.ts
|
|
2100
|
+
import { visit } from "unist-util-visit";
|
|
2101
|
+
|
|
2102
|
+
// src/placeholder.ts
|
|
2103
|
+
var BASES_ATTRIBUTE = "data-rr-bases";
|
|
2104
|
+
function encodeBasesPayload(payload) {
|
|
2105
|
+
return encodeURIComponent(JSON.stringify(payload));
|
|
2106
|
+
}
|
|
2107
|
+
function decodeBasesPayload(encoded) {
|
|
2108
|
+
try {
|
|
2109
|
+
const parsed = JSON.parse(decodeURIComponent(encoded));
|
|
2110
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
2111
|
+
const candidate = parsed;
|
|
2112
|
+
if (typeof candidate.source !== "string" || !candidate.spec) return null;
|
|
2113
|
+
return candidate;
|
|
2114
|
+
} catch {
|
|
2115
|
+
return null;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
function createBasesPlaceholder(payload) {
|
|
2119
|
+
return `<div ${BASES_ATTRIBUTE}="${encodeBasesPayload(payload)}"></div>`;
|
|
2120
|
+
}
|
|
2121
|
+
function createBasesPlaceholderPattern() {
|
|
2122
|
+
return new RegExp(
|
|
2123
|
+
`<div\\b[^>]*\\b${BASES_ATTRIBUTE}="([^"]*)"[^>]*>\\s*</div>`,
|
|
2124
|
+
"g"
|
|
2125
|
+
);
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// src/remark.ts
|
|
2129
|
+
function remarkBases(options = {}) {
|
|
2130
|
+
const language = options.language ?? "base";
|
|
2131
|
+
return (tree, file) => {
|
|
2132
|
+
visit(tree, "code", (node, index, parent) => {
|
|
2133
|
+
if (node.lang !== language) return;
|
|
2134
|
+
if (!parent || index === void 0) return;
|
|
2135
|
+
const parsed = parseBasesYaml(node.value);
|
|
2136
|
+
if (parsed.ok === false) {
|
|
2137
|
+
reportDiagnostic(file, parsed.message, node);
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
const spec = parseBases(parsed.value);
|
|
2141
|
+
if (spec.ok === false) {
|
|
2142
|
+
reportDiagnostic(file, spec.message, node);
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
const html = {
|
|
2146
|
+
type: "html",
|
|
2147
|
+
value: createBasesPlaceholder({ spec: spec.value, source: node.value })
|
|
2148
|
+
};
|
|
2149
|
+
parent.children.splice(index, 1, html);
|
|
2150
|
+
});
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
function reportDiagnostic(file, message, node) {
|
|
2154
|
+
const reporter = file?.message;
|
|
2155
|
+
if (typeof reporter !== "function") return;
|
|
2156
|
+
const diagnostic3 = reporter.call(file, message, node);
|
|
2157
|
+
if (diagnostic3 && typeof diagnostic3 === "object") {
|
|
2158
|
+
Object.assign(diagnostic3, { source: "@riebeckite/plugin-bases" });
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
797
2162
|
// src/runtime.ts
|
|
798
2163
|
function createBasesRuntime(options) {
|
|
2164
|
+
const dependencies = /* @__PURE__ */ new Map();
|
|
2165
|
+
let documentDependencies = /* @__PURE__ */ new Map();
|
|
799
2166
|
return {
|
|
800
2167
|
resolve(manifest, diagnostics, _config) {
|
|
801
2168
|
const entries = manifest.discoverableEntries;
|
|
802
2169
|
for (const entry of manifest.publicEntries) {
|
|
803
2170
|
if (!entry.html.includes(BASES_ATTRIBUTE)) continue;
|
|
804
|
-
const
|
|
2171
|
+
const resolved = replacePlaceholders(
|
|
2172
|
+
entry,
|
|
2173
|
+
entries,
|
|
2174
|
+
options,
|
|
2175
|
+
diagnostics
|
|
2176
|
+
);
|
|
2177
|
+
dependencies.set(entry.slug, resolved.dependencies);
|
|
2178
|
+
const html = resolved.html;
|
|
805
2179
|
if (html === entry.html) continue;
|
|
806
2180
|
entry.html = html;
|
|
807
2181
|
}
|
|
2182
|
+
},
|
|
2183
|
+
dependenciesFor(slug) {
|
|
2184
|
+
return dependencies.get(slug) ?? [];
|
|
2185
|
+
},
|
|
2186
|
+
dependenciesForPage(pathname) {
|
|
2187
|
+
return documentDependencies.get(pathname) ?? [{ type: "unknown" }];
|
|
2188
|
+
},
|
|
2189
|
+
setDocumentDependencies(nextDependencies) {
|
|
2190
|
+
documentDependencies = new Map(nextDependencies);
|
|
808
2191
|
}
|
|
809
2192
|
};
|
|
810
2193
|
}
|
|
811
2194
|
function replacePlaceholders(entry, entries, options, diagnostics) {
|
|
812
2195
|
const pattern = createBasesPlaceholderPattern();
|
|
813
|
-
|
|
2196
|
+
const dependencies = [];
|
|
2197
|
+
const html = entry.html.replace(pattern, (_match, encoded) => {
|
|
814
2198
|
const payload = decodeBasesPayload(encoded);
|
|
815
2199
|
if (!payload) {
|
|
816
2200
|
const message = `Base block in \`${entry.slug}\` could not be decoded.`;
|
|
817
|
-
diagnostics.push(
|
|
2201
|
+
diagnostics.push(diagnostic2(entry, message));
|
|
818
2202
|
return renderBasesError(message, options);
|
|
819
2203
|
}
|
|
820
2204
|
try {
|
|
2205
|
+
dependencies.push(analyzeBasesDependencies(payload.spec));
|
|
821
2206
|
return renderBases(payload.spec, options, entries, payload.source);
|
|
822
2207
|
} catch (error) {
|
|
823
2208
|
const message = `Base block in \`${entry.slug}\` failed to render (${formatError2(error)}).`;
|
|
824
|
-
diagnostics.push(
|
|
2209
|
+
diagnostics.push(diagnostic2(entry, message));
|
|
825
2210
|
return renderBasesError(message, options);
|
|
826
2211
|
}
|
|
827
2212
|
});
|
|
2213
|
+
return { html, dependencies };
|
|
828
2214
|
}
|
|
829
|
-
function
|
|
2215
|
+
function diagnostic2(entry, message) {
|
|
830
2216
|
return {
|
|
831
2217
|
code: "bases-invalid",
|
|
832
2218
|
severity: "error",
|
|
@@ -845,9 +2231,10 @@ function bases(options = {}) {
|
|
|
845
2231
|
const runtime = createBasesRuntime(options);
|
|
846
2232
|
return definePlugin({
|
|
847
2233
|
name: "bases",
|
|
848
|
-
|
|
2234
|
+
contentFileMetadata: true,
|
|
2235
|
+
resolveContentOutputDependencies: ({ entry }) => runtime.dependenciesFor(entry.slug),
|
|
849
2236
|
processedContentCache: {
|
|
850
|
-
version: "bases-
|
|
2237
|
+
version: "bases-v3",
|
|
851
2238
|
dependencyMode: "none"
|
|
852
2239
|
},
|
|
853
2240
|
options,
|
|
@@ -859,6 +2246,29 @@ function bases(options = {}) {
|
|
|
859
2246
|
onManifestCreated: (context) => {
|
|
860
2247
|
runtime.resolve(context.manifest, context.diagnostics, context.config);
|
|
861
2248
|
},
|
|
2249
|
+
pageTypes: [
|
|
2250
|
+
{
|
|
2251
|
+
id: "bases-document",
|
|
2252
|
+
paths: async ({ manifest, contentSource, diagnostics }) => {
|
|
2253
|
+
const documents = await listBaseDocuments({
|
|
2254
|
+
manifest,
|
|
2255
|
+
contentSource,
|
|
2256
|
+
diagnostics
|
|
2257
|
+
});
|
|
2258
|
+
runtime.setDocumentDependencies(
|
|
2259
|
+
new Map(
|
|
2260
|
+
documents.map((document) => [
|
|
2261
|
+
document.pathname,
|
|
2262
|
+
document.dependencies
|
|
2263
|
+
])
|
|
2264
|
+
)
|
|
2265
|
+
);
|
|
2266
|
+
return documents.map((document) => document.pathname);
|
|
2267
|
+
},
|
|
2268
|
+
outputDependencies: ({ pathname }) => runtime.dependenciesForPage(pathname),
|
|
2269
|
+
resolve: (context) => resolveBaseDocument(context, options)
|
|
2270
|
+
}
|
|
2271
|
+
],
|
|
862
2272
|
assets: [createStyleAsset("bases", "@riebeckite/plugin-bases/style.css")]
|
|
863
2273
|
});
|
|
864
2274
|
}
|
|
@@ -887,10 +2297,18 @@ function validateBasesOptions(options) {
|
|
|
887
2297
|
return issues;
|
|
888
2298
|
}
|
|
889
2299
|
export {
|
|
2300
|
+
BasesEvaluationError,
|
|
2301
|
+
analyzeBasesDependencies,
|
|
890
2302
|
bases,
|
|
891
2303
|
basesPlugin,
|
|
2304
|
+
evaluateBasesExpression,
|
|
2305
|
+
groupBasesEntries,
|
|
892
2306
|
matchesCondition,
|
|
893
2307
|
parseBases,
|
|
2308
|
+
parseBasesExpression,
|
|
894
2309
|
remarkBases,
|
|
895
|
-
|
|
2310
|
+
resolveBasesProperty,
|
|
2311
|
+
resolveValueRef,
|
|
2312
|
+
sortBasesEntries,
|
|
2313
|
+
summarizeBasesValues
|
|
896
2314
|
};
|