@riebeckite/plugin-bases 0.0.1

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 ADDED
@@ -0,0 +1,909 @@
1
+ // index.ts
2
+ import {
3
+ createStyleAsset,
4
+ definePlugin
5
+ } from "@riebeckite/core";
6
+
7
+ // src/remark.ts
8
+ import { visit } from "unist-util-visit";
9
+ import { matter } from "vfile-matter";
10
+
11
+ // src/parse.ts
12
+ var DEFAULT_COLUMNS = ["file.name", "file.tags"];
13
+ var ok = (value) => ({ ok: true, value });
14
+ var fail = (message) => ({ ok: false, message });
15
+ var BUILTIN_ALIASES = {
16
+ "file.name": "file.name",
17
+ "file.path": "file.path",
18
+ "file.slug": "file.slug",
19
+ "file.folder": "file.folder",
20
+ "file.title": "file.title",
21
+ "file.link": "file.link",
22
+ "file.permalink": "file.permalink",
23
+ "file.url": "file.link",
24
+ "file.tags": "file.tags",
25
+ "file.mtime": "date",
26
+ "file.ctime": "date",
27
+ "file.date": "date"
28
+ };
29
+ function parseBases(document) {
30
+ if (document === null || document === void 0) {
31
+ return ok({ properties: {}, views: [] });
32
+ }
33
+ if (typeof document !== "object" || Array.isArray(document)) {
34
+ return fail("Base definition must be a YAML mapping.");
35
+ }
36
+ const root = document;
37
+ const filter = parseFilters(root.filters, "filters");
38
+ if (filter.ok === false) return filter;
39
+ const properties = parseProperties(root.properties);
40
+ if (properties.ok === false) return properties;
41
+ const views = parseViews(root.views, properties.value);
42
+ if (views.ok === false) return views;
43
+ return ok({
44
+ ...filter.value === void 0 ? {} : { filter: filter.value },
45
+ properties: properties.value,
46
+ views: views.value
47
+ });
48
+ }
49
+ function parseFilters(value, path) {
50
+ if (value === void 0 || value === null) return ok(void 0);
51
+ if (typeof value === "string") {
52
+ const text = value.trim();
53
+ return text === "" ? ok(void 0) : parseExpression(text, path);
54
+ }
55
+ if (Array.isArray(value)) {
56
+ return combineAnd(value, path);
57
+ }
58
+ if (typeof value === "object") {
59
+ return parseFilterObject(value, path);
60
+ }
61
+ return fail(`${path} must be an expression, a list, or a filter object.`);
62
+ }
63
+ function parseFilterObject(object, path) {
64
+ const children = [];
65
+ if ("and" in object || "or" in object || "not" in object) {
66
+ if ("and" in object) {
67
+ const parsed = combineAnd(
68
+ toList(object.and),
69
+ `${path}.and`
70
+ );
71
+ if (parsed.ok === false) return parsed;
72
+ if (parsed.value) children.push(parsed.value);
73
+ }
74
+ if ("or" in object) {
75
+ const parsed = combineOr(toList(object.or), `${path}.or`);
76
+ if (parsed.ok === false) return parsed;
77
+ if (parsed.value) children.push(parsed.value);
78
+ }
79
+ if ("not" in object) {
80
+ const parsed = combineAnd(toList(object.not), `${path}.not`);
81
+ if (parsed.ok === false) return parsed;
82
+ if (parsed.value) {
83
+ children.push({ kind: "not", condition: parsed.value });
84
+ }
85
+ }
86
+ return combineAndNodes(children);
87
+ }
88
+ for (const [key, raw] of Object.entries(object)) {
89
+ const parsed = propertyEquality(key, raw, `${path}.${key}`);
90
+ if (parsed.ok === false) return parsed;
91
+ children.push(parsed.value);
92
+ }
93
+ return combineAndNodes(children);
94
+ }
95
+ function propertyEquality(key, value, path) {
96
+ const ref = parseValueRef(key);
97
+ if (Array.isArray(value)) {
98
+ const conditions = value.map((item) => ({
99
+ kind: "compare",
100
+ left: ref,
101
+ operator: "==",
102
+ right: scalarLiteral(item)
103
+ }));
104
+ return conditions.length === 0 ? fail(`${path} must not be an empty list.`) : ok({ kind: "or", conditions });
105
+ }
106
+ return ok({
107
+ kind: "compare",
108
+ left: ref,
109
+ operator: "==",
110
+ right: scalarLiteral(value)
111
+ });
112
+ }
113
+ function combineAnd(values, path) {
114
+ const children = [];
115
+ for (let index = 0; index < values.length; index += 1) {
116
+ const parsed = parseFilters(values[index], `${path}[${index}]`);
117
+ if (parsed.ok === false) return parsed;
118
+ if (parsed.value) children.push(parsed.value);
119
+ }
120
+ return combineAndNodes(children);
121
+ }
122
+ function combineOr(values, path) {
123
+ const children = [];
124
+ for (let index = 0; index < values.length; index += 1) {
125
+ const parsed = parseFilters(values[index], `${path}[${index}]`);
126
+ if (parsed.ok === false) return parsed;
127
+ if (parsed.value) children.push(parsed.value);
128
+ }
129
+ if (children.length === 0) return ok(void 0);
130
+ if (children.length === 1) return ok(children[0]);
131
+ return ok({ kind: "or", conditions: children });
132
+ }
133
+ function combineAndNodes(children) {
134
+ if (children.length === 0) return ok(void 0);
135
+ if (children.length === 1) return ok(children[0]);
136
+ return ok({ kind: "and", conditions: [...children] });
137
+ }
138
+ function toList(value) {
139
+ if (value === void 0 || value === null) return [];
140
+ return Array.isArray(value) ? value : [value];
141
+ }
142
+ function parseExpression(raw, path) {
143
+ let text = raw.trim();
144
+ while (text.startsWith("(") && text.endsWith(")") && isBalanced(text.slice(1, -1))) {
145
+ text = text.slice(1, -1).trim();
146
+ }
147
+ if (text === "") return fail(`${path} contains an empty filter expression.`);
148
+ const call = text.match(/^(?:file|note)\.(\w+)\s*\(\s*(.*?)\s*\)$/i);
149
+ if (call) {
150
+ const name = call[1].toLowerCase();
151
+ const argument = stripQuotes(call[2]);
152
+ if (argument === "") {
153
+ return fail(`${path} has a ${name}() call without an argument.`);
154
+ }
155
+ if (name === "hastag") return ok({ kind: "hasTag", tag: argument });
156
+ if (name === "infolder") return ok({ kind: "inFolder", folder: argument });
157
+ if (name === "haslink") return ok({ kind: "hasLink", link: argument });
158
+ return fail(`${path} uses an unsupported function \`${name}()\`.`);
159
+ }
160
+ if (/^[a-z_][\w.]*\s*\(/i.test(text)) {
161
+ return fail(`${path} uses an unsupported function in \`${text}\`.`);
162
+ }
163
+ const contains = text.match(/^(.+?)\s+contains\s+(.+)$/i);
164
+ if (contains) {
165
+ return comparisonFrom(contains[1], "contains", contains[2], path);
166
+ }
167
+ const comparison = text.match(/^(.+?)\s*(==|!=|>=|<=|>|<)\s*(.+)$/);
168
+ if (comparison) {
169
+ return comparisonFrom(
170
+ comparison[1],
171
+ comparison[2],
172
+ comparison[3],
173
+ path
174
+ );
175
+ }
176
+ return fail(`${path} uses an unsupported filter expression \`${raw}\`.`);
177
+ }
178
+ function comparisonFrom(left, operator, right, path) {
179
+ const reference = left.trim();
180
+ if (reference === "") {
181
+ return fail(`${path} is missing a left-hand side for \`${operator}\`.`);
182
+ }
183
+ return ok({
184
+ kind: "compare",
185
+ left: parseValueRef(reference),
186
+ operator,
187
+ right: parseLiteral(right)
188
+ });
189
+ }
190
+ function parseValueRef(raw) {
191
+ const key = stripQuotes(raw.trim());
192
+ const lower = key.toLowerCase();
193
+ const alias = BUILTIN_ALIASES[lower];
194
+ if (alias !== void 0) {
195
+ if (alias === "date") return { source: "frontmatter", key: "date" };
196
+ return { source: "builtin", name: alias };
197
+ }
198
+ if (lower.startsWith("note.")) {
199
+ return { source: "frontmatter", key: key.slice("note.".length) };
200
+ }
201
+ if (lower.startsWith("file.frontmatter.")) {
202
+ return {
203
+ source: "frontmatter",
204
+ key: key.slice("file.frontmatter.".length)
205
+ };
206
+ }
207
+ if (lower.startsWith("file.")) {
208
+ return { source: "frontmatter", key: key.slice("file.".length) };
209
+ }
210
+ return { source: "frontmatter", key };
211
+ }
212
+ function parseLiteral(raw) {
213
+ const text = raw.trim();
214
+ if (isQuoted(text)) return stripQuotes(text);
215
+ if (text === "true") return true;
216
+ if (text === "false") return false;
217
+ if (text === "null" || text === "~") return null;
218
+ if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text);
219
+ return text;
220
+ }
221
+ function scalarLiteral(value) {
222
+ if (value === null || value === void 0) return null;
223
+ if (typeof value === "string") return value;
224
+ if (typeof value === "number" || typeof value === "boolean") return value;
225
+ return String(value);
226
+ }
227
+ function isQuoted(value) {
228
+ if (value.length < 2) return false;
229
+ const first = value[0];
230
+ const last = value[value.length - 1];
231
+ return first === '"' && last === '"' || first === "'" && last === "'";
232
+ }
233
+ function stripQuotes(value) {
234
+ const text = value.trim();
235
+ return isQuoted(text) ? text.slice(1, -1) : text;
236
+ }
237
+ function isBalanced(value) {
238
+ let depth = 0;
239
+ for (const char of value) {
240
+ if (char === "(") depth += 1;
241
+ else if (char === ")") {
242
+ depth -= 1;
243
+ if (depth < 0) return false;
244
+ }
245
+ }
246
+ return depth === 0;
247
+ }
248
+ function parseProperties(value) {
249
+ if (value === void 0 || value === null) return ok({});
250
+ if (typeof value !== "object" || Array.isArray(value)) {
251
+ return fail("`properties` must be a YAML mapping.");
252
+ }
253
+ const labels = {};
254
+ for (const [key, raw] of Object.entries(value)) {
255
+ labels[key] = propertyLabel(key, raw);
256
+ }
257
+ return ok(labels);
258
+ }
259
+ function propertyLabel(key, raw) {
260
+ if (typeof raw === "string" && raw.trim() !== "") return raw;
261
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
262
+ const displayName = raw.displayName;
263
+ if (typeof displayName === "string" && displayName.trim() !== "") {
264
+ return displayName;
265
+ }
266
+ }
267
+ return key;
268
+ }
269
+ function parseViews(value, properties) {
270
+ if (value === void 0 || value === null) return ok([]);
271
+ if (!Array.isArray(value)) return fail("`views` must be a list.");
272
+ const views = [];
273
+ for (let index = 0; index < value.length; index += 1) {
274
+ const parsed = parseView(value[index], properties, `views[${index}]`);
275
+ if (parsed.ok === false) return parsed;
276
+ views.push(parsed.value);
277
+ }
278
+ return ok(views);
279
+ }
280
+ function parseView(value, properties, path) {
281
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
282
+ return fail(`${path} must be a mapping.`);
283
+ }
284
+ const object = value;
285
+ const type = parseViewType(object.type, path);
286
+ if (type.ok === false) return type;
287
+ const filter = parseFilters(object.filters, `${path}.filters`);
288
+ if (filter.ok === false) return filter;
289
+ const columns = parseColumns(object, properties);
290
+ const sort = parseSort(object.sort, `${path}.sort`);
291
+ if (sort.ok === false) return sort;
292
+ const limit = parseLimit(object.limit, `${path}.limit`);
293
+ if (limit.ok === false) return limit;
294
+ const name = typeof object.name === "string" && object.name.trim() !== "" ? object.name : void 0;
295
+ return ok({
296
+ type: type.value,
297
+ ...name === void 0 ? {} : { name },
298
+ columns,
299
+ ...filter.value === void 0 ? {} : { filter: filter.value },
300
+ ...sort.value === void 0 ? {} : { sort: sort.value },
301
+ ...limit.value === void 0 ? {} : { limit: limit.value }
302
+ });
303
+ }
304
+ function parseViewType(value, path) {
305
+ if (value === void 0 || value === null) return ok("table");
306
+ const text = String(value).trim().toLowerCase();
307
+ if (text === "table" || text === "cards") return ok(text);
308
+ return fail(`${path}.type must be "table" or "cards".`);
309
+ }
310
+ function parseColumns(object, properties) {
311
+ const configured = readStringList(object.order) ?? readStringList(object.columns);
312
+ if (configured && configured.length > 0) return configured;
313
+ const fromProperties = Object.keys(properties);
314
+ if (fromProperties.length > 0) return fromProperties;
315
+ return [...DEFAULT_COLUMNS];
316
+ }
317
+ function readStringList(value) {
318
+ if (value === void 0 || value === null) return null;
319
+ const list = Array.isArray(value) ? value : [value];
320
+ const out = list.filter((item) => {
321
+ return typeof item === "string" || typeof item === "number";
322
+ }).map((item) => String(item).trim()).filter((item) => item !== "");
323
+ return out;
324
+ }
325
+ function parseSort(value, path) {
326
+ if (value === void 0 || value === null) return ok(void 0);
327
+ const list = Array.isArray(value) ? value : [value];
328
+ const sorts = [];
329
+ for (let index = 0; index < list.length; index += 1) {
330
+ const entry = list[index];
331
+ if (typeof entry === "string") {
332
+ const text = entry.trim();
333
+ if (text === "") continue;
334
+ const descending = text.startsWith("-");
335
+ sorts.push({
336
+ field: mapSortField(descending ? text.slice(1) : text),
337
+ order: descending ? "desc" : "asc"
338
+ });
339
+ continue;
340
+ }
341
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
342
+ const record = entry;
343
+ const property = record.property ?? record.field;
344
+ if (typeof property !== "string" || property.trim() === "") {
345
+ return fail(`${path}[${index}] must name a \`property\` or \`field\`.`);
346
+ }
347
+ const direction = record.direction ?? record.order;
348
+ const descending = typeof direction === "string" && direction.trim().toLowerCase() === "desc";
349
+ sorts.push({
350
+ field: mapSortField(property),
351
+ order: descending ? "desc" : "asc"
352
+ });
353
+ continue;
354
+ }
355
+ return fail(`${path}[${index}] must be a property name or a mapping.`);
356
+ }
357
+ return ok(sorts.length === 0 ? void 0 : sorts);
358
+ }
359
+ function mapSortField(property) {
360
+ const ref = parseValueRef(property);
361
+ if (ref.source === "frontmatter") return ref.key;
362
+ switch (ref.name) {
363
+ case "file.name":
364
+ case "file.title":
365
+ return "title";
366
+ case "file.path":
367
+ case "file.slug":
368
+ return "slug";
369
+ case "file.link":
370
+ case "file.permalink":
371
+ return "permalink";
372
+ case "file.tags":
373
+ return "tags";
374
+ case "file.folder":
375
+ return "slug";
376
+ default:
377
+ return property;
378
+ }
379
+ }
380
+ function parseLimit(value, path) {
381
+ if (value === void 0 || value === null) return ok(void 0);
382
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
383
+ return fail(`${path} must be a non-negative number.`);
384
+ }
385
+ return ok(Math.floor(value));
386
+ }
387
+
388
+ // src/placeholder.ts
389
+ var BASES_ATTRIBUTE = "data-rr-bases";
390
+ function encodeBasesPayload(payload) {
391
+ return encodeURIComponent(JSON.stringify(payload));
392
+ }
393
+ function decodeBasesPayload(encoded) {
394
+ try {
395
+ const parsed = JSON.parse(decodeURIComponent(encoded));
396
+ if (!parsed || typeof parsed !== "object") return null;
397
+ const candidate = parsed;
398
+ if (typeof candidate.source !== "string" || !candidate.spec) return null;
399
+ return candidate;
400
+ } catch {
401
+ return null;
402
+ }
403
+ }
404
+ function createBasesPlaceholder(payload) {
405
+ return `<div ${BASES_ATTRIBUTE}="${encodeBasesPayload(payload)}"></div>`;
406
+ }
407
+ function createBasesPlaceholderPattern() {
408
+ return new RegExp(
409
+ `<div\\b[^>]*\\b${BASES_ATTRIBUTE}="([^"]*)"[^>]*>\\s*</div>`,
410
+ "g"
411
+ );
412
+ }
413
+
414
+ // src/remark.ts
415
+ function remarkBases(options = {}) {
416
+ const language = options.language ?? "base";
417
+ return (tree, file) => {
418
+ visit(tree, "code", (node, index, parent) => {
419
+ if (node.lang !== language) return;
420
+ if (!parent || index === void 0) return;
421
+ const parsed = parseYaml(node.value);
422
+ if (parsed.ok === false) {
423
+ reportDiagnostic(file, parsed.message, node);
424
+ return;
425
+ }
426
+ const spec = parseBases(parsed.value);
427
+ if (spec.ok === false) {
428
+ reportDiagnostic(file, spec.message, node);
429
+ return;
430
+ }
431
+ const html = {
432
+ type: "html",
433
+ value: createBasesPlaceholder({ spec: spec.value, source: node.value })
434
+ };
435
+ parent.children.splice(index, 1, html);
436
+ });
437
+ };
438
+ }
439
+ function parseYaml(source) {
440
+ try {
441
+ const document = `---
442
+ ${source}
443
+ ---
444
+ `;
445
+ const file = {
446
+ value: document,
447
+ data: {},
448
+ toString: () => document
449
+ };
450
+ matter(file);
451
+ return { ok: true, value: file.data.matter };
452
+ } catch (error) {
453
+ return {
454
+ ok: false,
455
+ message: `Base block is not valid YAML (${formatError(error)}).`
456
+ };
457
+ }
458
+ }
459
+ function reportDiagnostic(file, message, node) {
460
+ const reporter = file?.message;
461
+ if (typeof reporter !== "function") return;
462
+ const diagnostic2 = reporter.call(file, message, node);
463
+ if (diagnostic2 && typeof diagnostic2 === "object") {
464
+ Object.assign(diagnostic2, { source: "@riebeckite/plugin-bases" });
465
+ }
466
+ }
467
+ function formatError(error) {
468
+ return error instanceof Error ? error.message : String(error);
469
+ }
470
+
471
+ // src/runtime.ts
472
+ import { isPublished } from "@riebeckite/core";
473
+
474
+ // src/render.ts
475
+ import {
476
+ escapeHtml,
477
+ escapeHtmlAttribute,
478
+ queryContentEntries
479
+ } from "@riebeckite/core";
480
+
481
+ // src/evaluate.ts
482
+ function matchesCondition(condition, entry) {
483
+ switch (condition.kind) {
484
+ case "and":
485
+ return condition.conditions.every(
486
+ (child) => matchesCondition(child, entry)
487
+ );
488
+ case "or":
489
+ return condition.conditions.some(
490
+ (child) => matchesCondition(child, entry)
491
+ );
492
+ case "not":
493
+ return !matchesCondition(condition.condition, entry);
494
+ case "hasTag":
495
+ return hasTag(entry.tags, condition.tag);
496
+ case "inFolder":
497
+ return inFolder(entry.slug, condition.folder);
498
+ case "hasLink":
499
+ return hasLink(entry.links, condition.link);
500
+ case "compare":
501
+ return compare(
502
+ resolveValueRef(condition.left, entry),
503
+ condition.operator,
504
+ condition.right
505
+ );
506
+ default:
507
+ return false;
508
+ }
509
+ }
510
+ function resolveValueRef(reference, entry) {
511
+ if (reference.source === "frontmatter") {
512
+ return entry.frontmatter[reference.key];
513
+ }
514
+ return resolveBuiltin(reference.name, entry);
515
+ }
516
+ function resolveBuiltin(name, entry) {
517
+ switch (name) {
518
+ case "file.name":
519
+ return entry.title;
520
+ case "file.path":
521
+ case "file.slug":
522
+ return entry.slug;
523
+ case "file.folder":
524
+ return folderOf(entry.slug);
525
+ case "file.title":
526
+ return entry.title;
527
+ case "file.link":
528
+ case "file.permalink":
529
+ return entry.permalink;
530
+ case "file.tags":
531
+ return entry.tags;
532
+ default:
533
+ return void 0;
534
+ }
535
+ }
536
+ function hasTag(tags, tag) {
537
+ const target = tag.trim().replace(/^#/, "").toLowerCase();
538
+ if (target === "") return false;
539
+ return tags.some((value) => {
540
+ const normalized = value.toLowerCase();
541
+ return normalized === target || normalized.startsWith(`${target}/`);
542
+ });
543
+ }
544
+ function inFolder(slug, folder) {
545
+ const target = folder.trim().replace(/^\/+|\/+$/g, "");
546
+ if (target === "") return true;
547
+ return slug === target || slug.startsWith(`${target}/`);
548
+ }
549
+ function hasLink(links, link) {
550
+ const target = normalizeLink(link);
551
+ if (target === "") return false;
552
+ return links.some((candidate) => {
553
+ if (candidate.slug !== null && candidate.slug.toLowerCase() === target) {
554
+ return true;
555
+ }
556
+ const raw = normalizeLink(candidate.raw);
557
+ return raw === target || baseName(raw) === baseName(target);
558
+ });
559
+ }
560
+ function normalizeLink(value) {
561
+ return value.trim().replace(/^\[\[|\]\]$/g, "").split("|")[0].split("#")[0].replace(/\.md$/i, "").replace(/^\.?\//, "").toLowerCase();
562
+ }
563
+ function baseName(value) {
564
+ const parts = value.split("/");
565
+ return parts[parts.length - 1] ?? value;
566
+ }
567
+ function folderOf(slug) {
568
+ const index = slug.lastIndexOf("/");
569
+ return index === -1 ? "" : slug.slice(0, index);
570
+ }
571
+ function compare(actual, operator, expected) {
572
+ if (operator === "contains") return containsValue(actual, expected);
573
+ if (operator === "==" || operator === "!=") {
574
+ const equal = equalsAny(actual, expected);
575
+ return operator === "==" ? equal : !equal;
576
+ }
577
+ const order = compareOrder(actual, expected);
578
+ if (order === null) return false;
579
+ switch (operator) {
580
+ case ">":
581
+ return order > 0;
582
+ case "<":
583
+ return order < 0;
584
+ case ">=":
585
+ return order >= 0;
586
+ case "<=":
587
+ return order <= 0;
588
+ default:
589
+ return false;
590
+ }
591
+ }
592
+ function equalsAny(actual, expected) {
593
+ if (Array.isArray(actual)) {
594
+ return actual.some((item) => scalarEquals(item, expected));
595
+ }
596
+ return scalarEquals(actual, expected);
597
+ }
598
+ function scalarEquals(actual, expected) {
599
+ if (actual === void 0 || actual === null) return expected === null;
600
+ if (typeof actual === "string" && typeof expected === "string") {
601
+ return actual.toLowerCase() === expected.toLowerCase();
602
+ }
603
+ if (actual instanceof Date) {
604
+ const time = toComparableNumber(expected);
605
+ return time !== null && actual.getTime() === time;
606
+ }
607
+ return actual === expected;
608
+ }
609
+ function containsValue(actual, expected) {
610
+ if (Array.isArray(actual)) {
611
+ return actual.some(
612
+ (item) => typeof item === "string" && typeof expected === "string" ? item.toLowerCase().includes(expected.toLowerCase()) : scalarEquals(item, expected)
613
+ );
614
+ }
615
+ if (typeof actual === "string") {
616
+ return actual.toLowerCase().includes(String(expected ?? "").toLowerCase());
617
+ }
618
+ return false;
619
+ }
620
+ function compareOrder(actual, expected) {
621
+ if (actual === void 0 || actual === null) return null;
622
+ const left = toComparableNumber(actual);
623
+ const right = toComparableNumber(expected);
624
+ if (left !== null && right !== null) {
625
+ return Math.sign(left - right);
626
+ }
627
+ if (actual instanceof Date) return null;
628
+ return Math.sign(String(actual).localeCompare(String(expected ?? ""), "en"));
629
+ }
630
+ function toComparableNumber(value) {
631
+ if (typeof value === "number") {
632
+ return Number.isFinite(value) ? value : null;
633
+ }
634
+ if (typeof value === "boolean") return value ? 1 : 0;
635
+ if (value instanceof Date) {
636
+ const time = value.getTime();
637
+ return Number.isFinite(time) ? time : null;
638
+ }
639
+ if (typeof value === "string") {
640
+ const trimmed = value.trim();
641
+ if (trimmed === "") return null;
642
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
643
+ const time = Date.parse(trimmed);
644
+ if (!Number.isNaN(time)) return time;
645
+ }
646
+ return null;
647
+ }
648
+
649
+ // src/render.ts
650
+ var TITLE_PROPERTIES = /* @__PURE__ */ new Set(["file.name", "file.title", "title", "name"]);
651
+ var LINK_PROPERTIES = /* @__PURE__ */ new Set([
652
+ "file.link",
653
+ "file.permalink",
654
+ "file.url",
655
+ "permalink",
656
+ "url"
657
+ ]);
658
+ var TAG_PROPERTIES = /* @__PURE__ */ new Set(["file.tags", "tags"]);
659
+ var BUILTIN_LABELS = {
660
+ "file.name": "Name",
661
+ "file.path": "Path",
662
+ "file.slug": "Slug",
663
+ "file.folder": "Folder",
664
+ "file.title": "Title",
665
+ "file.link": "Link",
666
+ "file.permalink": "Permalink",
667
+ "file.tags": "Tags"
668
+ };
669
+ var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
670
+ function renderBases(spec, options, entries, source) {
671
+ const className = options.className ?? "rb-bases";
672
+ const views = selectViews(spec, options.view);
673
+ const limit = resolveLimit(options.limit);
674
+ const sections = views.map((view) => renderView(view, spec, className, entries, limit)).join("");
675
+ const fallback = options.showFallback === false ? "" : renderFallback(className, source);
676
+ const firstType = views[0]?.type ?? "table";
677
+ return `<div class="${escapeHtmlAttribute(className)}" data-bases data-bases-view="${firstType}">${sections}${fallback}</div>`;
678
+ }
679
+ function renderBasesError(message, options) {
680
+ const className = options.className ?? "rb-bases";
681
+ return `<div class="${escapeHtmlAttribute(className)} ${escapeHtmlAttribute(className)}--error" role="status">${escapeHtml(message)}</div>`;
682
+ }
683
+ function selectViews(spec, requested) {
684
+ const configured = spec.views;
685
+ if (requested && requested.trim() !== "") {
686
+ const target = requested.trim().toLowerCase();
687
+ const matches = configured.filter(
688
+ (view) => view.name?.toLowerCase() === target
689
+ );
690
+ if (matches.length > 0) return matches;
691
+ }
692
+ if (configured.length > 0) return [...configured];
693
+ const properties = Object.keys(spec.properties);
694
+ return [
695
+ {
696
+ type: "table",
697
+ columns: properties.length > 0 ? properties : [...DEFAULT_COLUMNS]
698
+ }
699
+ ];
700
+ }
701
+ function resolveLimit(limit) {
702
+ if (typeof limit === "number" && Number.isFinite(limit) && limit >= 0) {
703
+ return Math.floor(limit);
704
+ }
705
+ return 100;
706
+ }
707
+ function renderView(view, spec, className, entries, limit) {
708
+ const pool = entries.filter(
709
+ (entry) => view.filter === void 0 || matchesCondition(view.filter, entry)
710
+ );
711
+ const effectiveLimit = Math.min(view.limit ?? limit, limit);
712
+ const selected = queryContentEntries(pool, {
713
+ ...view.sort === void 0 ? {} : { sort: view.sort },
714
+ limit: effectiveLimit
715
+ });
716
+ const name = view.name ? `<h3 class="${escapeHtmlAttribute(className)}__view-name">${escapeHtml(view.name)}</h3>` : "";
717
+ const body = view.type === "cards" ? renderCards(view, selected, spec, className) : renderTable(view, selected, spec, className);
718
+ return `<section class="${escapeHtmlAttribute(className)}__view" data-bases-view="${view.type}">${name}${body}</section>`;
719
+ }
720
+ function renderTable(view, entries, spec, className) {
721
+ if (entries.length === 0) return renderEmpty(className);
722
+ const head = view.columns.map(
723
+ (property) => `<th class="${escapeHtmlAttribute(className)}__heading" scope="col">${escapeHtml(columnLabel(property, spec))}</th>`
724
+ ).join("");
725
+ const rows = entries.map((entry) => {
726
+ const cells = view.columns.map(
727
+ (property) => `<td class="${escapeHtmlAttribute(className)}__cell">${renderCell(entry, property, className)}</td>`
728
+ ).join("");
729
+ return `<tr class="${escapeHtmlAttribute(className)}__row">${cells}</tr>`;
730
+ }).join("");
731
+ return [
732
+ `<table class="${escapeHtmlAttribute(className)}__table">`,
733
+ `<thead><tr>${head}</tr></thead>`,
734
+ `<tbody>${rows}</tbody>`,
735
+ "</table>"
736
+ ].join("");
737
+ }
738
+ function renderCards(view, entries, spec, className) {
739
+ if (entries.length === 0) return renderEmpty(className);
740
+ const titleProperty = view.columns.find(
741
+ (property) => TITLE_PROPERTIES.has(property.toLowerCase())
742
+ );
743
+ const fields = view.columns.filter(
744
+ (property) => property !== titleProperty
745
+ );
746
+ const items = entries.map((entry) => {
747
+ const title = `<a class="${escapeHtmlAttribute(className)}__link" href="${escapeHtmlAttribute(entry.permalink)}">${escapeHtml(entry.title)}</a>`;
748
+ const fieldHtml = fields.map((property) => {
749
+ const label = escapeHtml(columnLabel(property, spec));
750
+ const value = renderCell(entry, property, className);
751
+ return `<div class="${escapeHtmlAttribute(className)}__field"><dt class="${escapeHtmlAttribute(className)}__field-label">${label}</dt><dd class="${escapeHtmlAttribute(className)}__field-value">${value}</dd></div>`;
752
+ }).join("");
753
+ const fieldsBlock = fieldHtml ? `<dl class="${escapeHtmlAttribute(className)}__fields">${fieldHtml}</dl>` : "";
754
+ return `<li class="${escapeHtmlAttribute(className)}__card"><div class="${escapeHtmlAttribute(className)}__card-title">${title}</div>${fieldsBlock}</li>`;
755
+ }).join("");
756
+ return `<ul class="${escapeHtmlAttribute(className)}__cards">${items}</ul>`;
757
+ }
758
+ function renderEmpty(className) {
759
+ return `<p class="${escapeHtmlAttribute(className)}__empty">No matching notes.</p>`;
760
+ }
761
+ function renderFallback(className, source) {
762
+ return `<details class="${escapeHtmlAttribute(className)}__fallback"><summary>Base definition</summary><pre><code>${escapeHtml(source)}</code></pre></details>`;
763
+ }
764
+ function renderCell(entry, property, className) {
765
+ const key = property.toLowerCase();
766
+ if (TITLE_PROPERTIES.has(key)) {
767
+ return `<a class="${escapeHtmlAttribute(className)}__link" href="${escapeHtmlAttribute(entry.permalink)}">${escapeHtml(entry.title)}</a>`;
768
+ }
769
+ if (LINK_PROPERTIES.has(key)) {
770
+ return `<a class="${escapeHtmlAttribute(className)}__link" href="${escapeHtmlAttribute(entry.permalink)}">${escapeHtml(entry.permalink)}</a>`;
771
+ }
772
+ if (TAG_PROPERTIES.has(key)) {
773
+ return entry.tags.length === 0 ? "" : `<span class="${escapeHtmlAttribute(className)}__tags">${entry.tags.map(
774
+ (tag) => `<span class="${escapeHtmlAttribute(className)}__tag">#${escapeHtml(tag)}</span>`
775
+ ).join(" ")}</span>`;
776
+ }
777
+ const value = resolveValueRef(parseValueRef(property), entry);
778
+ return escapeHtml(formatValue(value));
779
+ }
780
+ function columnLabel(property, spec) {
781
+ const configured = spec.properties[property];
782
+ if (configured && configured !== "") return configured;
783
+ const key = property.toLowerCase();
784
+ const builtin = BUILTIN_LABELS[key];
785
+ if (builtin) return builtin;
786
+ const normalized = property.replace(/^note\./i, "").replace(/^file\.frontmatter\./i, "");
787
+ return normalized;
788
+ }
789
+ function formatValue(value) {
790
+ if (value === void 0 || value === null) return "";
791
+ if (value instanceof Date) return formatDate(value);
792
+ if (Array.isArray(value)) return value.map((item) => formatValue(item)).join(", ");
793
+ if (typeof value === "string") {
794
+ return DATE_ONLY.test(value.trim()) ? value.trim() : value;
795
+ }
796
+ return String(value);
797
+ }
798
+ function formatDate(value) {
799
+ const time = value.getTime();
800
+ if (!Number.isFinite(time)) return "";
801
+ return value.toISOString().slice(0, 10);
802
+ }
803
+
804
+ // src/runtime.ts
805
+ function createBasesRuntime(options) {
806
+ const tracked = /* @__PURE__ */ new Map();
807
+ return {
808
+ track(slug, content) {
809
+ tracked.set(slug, content);
810
+ },
811
+ resolve(manifest, diagnostics, config) {
812
+ const entries = config ? manifest.entries.filter(
813
+ (entry) => isPublished(config, entry.frontmatter)
814
+ ) : manifest.entries;
815
+ for (const entry of entries) {
816
+ if (!entry.html.includes(BASES_ATTRIBUTE)) continue;
817
+ const html = replacePlaceholders(entry, entries, options, diagnostics);
818
+ if (html === entry.html) continue;
819
+ entry.html = html;
820
+ const content = tracked.get(entry.slug);
821
+ if (content) content.html = html;
822
+ }
823
+ }
824
+ };
825
+ }
826
+ function replacePlaceholders(entry, entries, options, diagnostics) {
827
+ const pattern = createBasesPlaceholderPattern();
828
+ return entry.html.replace(pattern, (_match, encoded) => {
829
+ const payload = decodeBasesPayload(encoded);
830
+ if (!payload) {
831
+ const message = `Base block in \`${entry.slug}\` could not be decoded.`;
832
+ diagnostics.push(diagnostic(entry, message));
833
+ return renderBasesError(message, options);
834
+ }
835
+ try {
836
+ return renderBases(payload.spec, options, entries, payload.source);
837
+ } catch (error) {
838
+ const message = `Base block in \`${entry.slug}\` failed to render (${formatError2(error)}).`;
839
+ diagnostics.push(diagnostic(entry, message));
840
+ return renderBasesError(message, options);
841
+ }
842
+ });
843
+ }
844
+ function diagnostic(entry, message) {
845
+ return {
846
+ code: "bases-invalid",
847
+ severity: "error",
848
+ pluginName: "bases",
849
+ slug: entry.slug,
850
+ message
851
+ };
852
+ }
853
+ function formatError2(error) {
854
+ return error instanceof Error ? error.message : String(error);
855
+ }
856
+
857
+ // index.ts
858
+ function bases(options = {}) {
859
+ const language = options.language ?? "base";
860
+ const runtime = createBasesRuntime(options);
861
+ return definePlugin({
862
+ name: "bases",
863
+ options,
864
+ provides: ["content.bases"],
865
+ validateOptions: validateBasesOptions,
866
+ extendMarkdownPipeline: (pipeline) => {
867
+ pipeline.use(remarkBases, { language });
868
+ },
869
+ onPostProcessed: (context) => {
870
+ runtime.track(context.slug, context.content);
871
+ },
872
+ onManifestCreated: (context) => {
873
+ runtime.resolve(context.manifest, context.diagnostics, context.config);
874
+ },
875
+ assets: [createStyleAsset("bases")]
876
+ });
877
+ }
878
+ var basesPlugin = bases;
879
+ function validateBasesOptions(options) {
880
+ if (!options) return [];
881
+ const issues = [];
882
+ if (options.className !== void 0 && (typeof options.className !== "string" || options.className.trim() === "")) {
883
+ issues.push({ path: "className", message: "Expected a non-empty string." });
884
+ }
885
+ if (options.language !== void 0 && (typeof options.language !== "string" || options.language.trim() === "")) {
886
+ issues.push({ path: "language", message: "Expected a non-empty string." });
887
+ }
888
+ if (options.limit !== void 0 && (!Number.isFinite(options.limit) || options.limit < 0)) {
889
+ issues.push({
890
+ path: "limit",
891
+ message: "Expected a non-negative finite number."
892
+ });
893
+ }
894
+ if (options.showFallback !== void 0 && typeof options.showFallback !== "boolean") {
895
+ issues.push({ path: "showFallback", message: "Expected a boolean." });
896
+ }
897
+ if (options.view !== void 0 && (typeof options.view !== "string" || options.view.trim() === "")) {
898
+ issues.push({ path: "view", message: "Expected a non-empty string." });
899
+ }
900
+ return issues;
901
+ }
902
+ export {
903
+ bases,
904
+ basesPlugin,
905
+ matchesCondition,
906
+ parseBases,
907
+ remarkBases,
908
+ resolveValueRef
909
+ };