@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/dist/index.js CHANGED
@@ -4,9 +4,403 @@ import {
4
4
  definePlugin
5
5
  } from "@riebeckite/core";
6
6
 
7
- // src/remark.ts
8
- import { visit } from "unist-util-visit";
9
- import { matter } from "vfile-matter";
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.mtime": "date",
26
- "file.ctime": "date",
27
- "file.date": "date"
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
- field: mapSortField(descending ? text.slice(1) : text),
334
- order: descending ? "desc" : "asc"
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 descending = typeof direction === "string" && direction.trim().toLowerCase() === "desc";
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
- field: mapSortField(property),
348
- order: descending ? "desc" : "asc"
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 mapSortField(property) {
357
- const ref = parseValueRef(property);
358
- if (ref.source === "frontmatter") return ref.key;
359
- switch (ref.name) {
360
- case "file.name":
361
- case "file.title":
362
- return "title";
363
- case "file.path":
364
- case "file.slug":
365
- return "slug";
366
- case "file.link":
367
- case "file.permalink":
368
- return "permalink";
369
- case "file.tags":
370
- return "tags";
371
- case "file.folder":
372
- return "slug";
373
- default:
374
- return property;
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
+ "&": "&amp;",
1373
+ "<": "&lt;",
1374
+ ">": "&gt;",
1375
+ '"': "&quot;",
1376
+ "'": "&#39;"
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[reference.key];
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.title;
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((view) => renderView(view, spec, className, entries, limit)).join("");
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">${escapeHtml(message)}</div>`;
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 = queryContentEntries(pool, {
707
- ...view.sort === void 0 ? {} : { sort: view.sort },
708
- limit: effectiveLimit
709
- });
710
- const name = view.name ? `<h3 class="${escapeHtmlAttribute(className)}__view-name">${escapeHtml(view.name)}</h3>` : "";
711
- const body = view.type === "cards" ? renderCards(view, selected, spec, className) : renderTable(view, selected, spec, className);
712
- return `<section class="${escapeHtmlAttribute(className)}__view" data-bases-view="${view.type}">${name}${body}</section>`;
713
- }
714
- function renderTable(view, entries, spec, className) {
715
- if (entries.length === 0) return renderEmpty(className);
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">${escapeHtml(columnLabel(property, spec))}</th>`
1847
+ (property) => `<th class="${escapeHtmlAttribute(className)}__heading" scope="col">${escapeHtml2(columnLabel(property, spec))}</th>`
718
1848
  ).join("");
719
- const rows = entries.map((entry) => {
720
- const cells = view.columns.map(
721
- (property) => `<td class="${escapeHtmlAttribute(className)}__cell">${renderCell(entry, property, className)}</td>`
722
- ).join("");
723
- return `<tr class="${escapeHtmlAttribute(className)}__row">${cells}</tr>`;
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
- return [
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
- `<tbody>${rows}</tbody>`,
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, entries, spec, className) {
733
- if (entries.length === 0) return renderEmpty(className);
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 items = entries.map((entry) => {
739
- const title = `<a class="${escapeHtmlAttribute(className)}__link" href="${escapeHtmlAttribute(entry.permalink)}">${escapeHtml(entry.title)}</a>`;
740
- const fieldHtml = fields.map((property) => {
741
- const label = escapeHtml(columnLabel(property, spec));
742
- const value = renderCell(entry, property, className);
743
- return `<div class="${escapeHtmlAttribute(className)}__field"><dt class="${escapeHtmlAttribute(className)}__field-label">${label}</dt><dd class="${escapeHtmlAttribute(className)}__field-value">${value}</dd></div>`;
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 fieldsBlock = fieldHtml ? `<dl class="${escapeHtmlAttribute(className)}__fields">${fieldHtml}</dl>` : "";
746
- return `<li class="${escapeHtmlAttribute(className)}__card"><div class="${escapeHtmlAttribute(className)}__card-title">${title}</div>${fieldsBlock}</li>`;
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 `<ul class="${escapeHtmlAttribute(className)}__cards">${items}</ul>`;
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>${escapeHtml(source)}</code></pre></details>`;
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)}">${escapeHtml(entry.title)}</a>`;
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)}">${escapeHtml(entry.permalink)}</a>`;
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">#${escapeHtml(tag)}</span>`
1912
+ (tag) => `<span class="${escapeHtmlAttribute(className)}__tag">#${escapeHtml2(tag)}</span>`
767
1913
  ).join(" ")}</span>`;
768
1914
  }
769
- const value = resolveValueRef(parseValueRef(property), entry);
770
- return escapeHtml(formatValue(value));
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 formatDate(value);
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 formatDate(value) {
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 html = replacePlaceholders(entry, entries, options, diagnostics);
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
- return entry.html.replace(pattern, (_match, encoded) => {
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(diagnostic(entry, message));
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(diagnostic(entry, message));
2209
+ diagnostics.push(diagnostic2(entry, message));
825
2210
  return renderBasesError(message, options);
826
2211
  }
827
2212
  });
2213
+ return { html, dependencies };
828
2214
  }
829
- function diagnostic(entry, message) {
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
- contentOutputDependencies: [{ type: "global" }],
2234
+ contentFileMetadata: true,
2235
+ resolveContentOutputDependencies: ({ entry }) => runtime.dependenciesFor(entry.slug),
849
2236
  processedContentCache: {
850
- version: "bases-v1",
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
- resolveValueRef
2310
+ resolveBasesProperty,
2311
+ resolveValueRef,
2312
+ sortBasesEntries,
2313
+ summarizeBasesValues
896
2314
  };