@aliou/obsdx-base-engine 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.d.mts +95 -0
- package/dist/index.mjs +1159 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +32 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1159 @@
|
|
|
1
|
+
import { parseExpression } from "@aliou/obsdx-base-ast";
|
|
2
|
+
//#region src/math.ts
|
|
3
|
+
function numericValues(values) {
|
|
4
|
+
return values.filter((value) => typeof value === "number" && Number.isFinite(value));
|
|
5
|
+
}
|
|
6
|
+
function sumNumbers(values) {
|
|
7
|
+
return numericValues(values).reduce((total, value) => total + value, 0);
|
|
8
|
+
}
|
|
9
|
+
function meanNumbers(values) {
|
|
10
|
+
const numbers = numericValues(values);
|
|
11
|
+
return numbers.length > 0 ? sumNumbers(numbers) / numbers.length : null;
|
|
12
|
+
}
|
|
13
|
+
function minNumber(values) {
|
|
14
|
+
const numbers = numericValues(values);
|
|
15
|
+
return numbers.length > 0 ? Math.min(...numbers) : null;
|
|
16
|
+
}
|
|
17
|
+
function maxNumber(values) {
|
|
18
|
+
const numbers = numericValues(values);
|
|
19
|
+
return numbers.length > 0 ? Math.max(...numbers) : null;
|
|
20
|
+
}
|
|
21
|
+
function medianNumbers(values) {
|
|
22
|
+
const numbers = numericValues(values).sort((left, right) => left - right);
|
|
23
|
+
if (numbers.length === 0) return null;
|
|
24
|
+
const middle = Math.floor(numbers.length / 2);
|
|
25
|
+
if (numbers.length % 2 === 1) return numbers[middle] ?? null;
|
|
26
|
+
return ((numbers[middle - 1] ?? 0) + (numbers[middle] ?? 0)) / 2;
|
|
27
|
+
}
|
|
28
|
+
function stddevNumbers(values) {
|
|
29
|
+
const numbers = numericValues(values);
|
|
30
|
+
const average = meanNumbers(numbers);
|
|
31
|
+
if (average === null) return null;
|
|
32
|
+
return Math.sqrt(numbers.reduce((total, value) => total + (value - average) ** 2, 0) / numbers.length);
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/expressions/evaluator.ts
|
|
36
|
+
var BaseEngineError = class extends Error {
|
|
37
|
+
code;
|
|
38
|
+
details;
|
|
39
|
+
constructor(code, message, details = {}) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.code = code;
|
|
42
|
+
this.details = details;
|
|
43
|
+
this.name = "BaseEngineError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
function evaluateExpression(expr, context) {
|
|
47
|
+
switch (expr.kind) {
|
|
48
|
+
case "literal": return expr.value;
|
|
49
|
+
case "regex": return new RegExp(expr.pattern, expr.flags);
|
|
50
|
+
case "array": return expr.elements.map((element) => evaluateExpression(element, context));
|
|
51
|
+
case "identifier": return readIdentifier(expr.name, context);
|
|
52
|
+
case "member": return readMember(evaluateExpression(expr.object, context), expr.property, context);
|
|
53
|
+
case "index": return readIndex(evaluateExpression(expr.object, context), evaluateExpression(expr.index, context));
|
|
54
|
+
case "call":
|
|
55
|
+
if (expr.callee.kind === "member" && expr.callee.property === "filter") {
|
|
56
|
+
const source = listValue(evaluateExpression(expr.callee.object, context));
|
|
57
|
+
const predicate = expr.args[0];
|
|
58
|
+
return predicate ? source.filter((value) => truthy(evaluateExpression(predicate, {
|
|
59
|
+
...context,
|
|
60
|
+
value
|
|
61
|
+
}))) : source;
|
|
62
|
+
}
|
|
63
|
+
if (expr.callee.kind === "member" && expr.callee.property === "map") {
|
|
64
|
+
const source = listValue(evaluateExpression(expr.callee.object, context));
|
|
65
|
+
const mapper = expr.args[0];
|
|
66
|
+
return mapper ? source.map((value) => evaluateExpression(mapper, {
|
|
67
|
+
...context,
|
|
68
|
+
value
|
|
69
|
+
})) : source;
|
|
70
|
+
}
|
|
71
|
+
if (expr.callee.kind === "member" && expr.callee.property === "reduce") {
|
|
72
|
+
const source = listValue(evaluateExpression(expr.callee.object, context));
|
|
73
|
+
const reducer = expr.args[0];
|
|
74
|
+
if (!reducer) return null;
|
|
75
|
+
let acc = expr.args.length > 1 ? evaluateExpression(expr.args[1], context) : source[0];
|
|
76
|
+
const values = expr.args.length > 1 ? source : source.slice(1);
|
|
77
|
+
for (const value of values) acc = evaluateExpression(reducer, {
|
|
78
|
+
...context,
|
|
79
|
+
acc,
|
|
80
|
+
value
|
|
81
|
+
});
|
|
82
|
+
return acc;
|
|
83
|
+
}
|
|
84
|
+
if (expr.callee.kind === "member" && expr.callee.property === "matches") {
|
|
85
|
+
if (typeof evaluateExpression(expr.callee.object, context) === "string") throw new BaseEngineError("FORMULA_EVAL_ERROR", "Cannot find function \"matches\" on type String");
|
|
86
|
+
}
|
|
87
|
+
if (expr.callee.kind === "identifier") return callValue(readFunctionIdentifier(expr.callee.name, context) ?? evaluateExpression(expr.callee, context), expr.args.map((arg) => evaluateExpression(arg, context)));
|
|
88
|
+
return callValue(evaluateExpression(expr.callee, context), expr.args.map((arg) => evaluateExpression(arg, context)));
|
|
89
|
+
case "unary": {
|
|
90
|
+
const value = evaluateExpression(expr.right, context);
|
|
91
|
+
return expr.operator === "!" ? !truthy(value) : -numberValue(value);
|
|
92
|
+
}
|
|
93
|
+
case "binary": return evaluateBinary(expr.operator, evaluateExpression(expr.left, context), evaluateExpression(expr.right, context));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function readIdentifier(name, context) {
|
|
97
|
+
if (name === "file") return fileObject(context.row, context);
|
|
98
|
+
if (name === "note") return noteObject(context.row);
|
|
99
|
+
if (name === "property") return null;
|
|
100
|
+
if (name === "formula") return context.formulas;
|
|
101
|
+
if (name === "this") return context.context ? thisObject(context.context, context) : null;
|
|
102
|
+
if (name === "value") return context.value ?? null;
|
|
103
|
+
if (name === "acc") return context.acc ?? null;
|
|
104
|
+
if (name === "values") return context.values ?? null;
|
|
105
|
+
const property = readProperty(context.row, name);
|
|
106
|
+
if (property !== null) return property;
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
function readFunctionIdentifier(name, context) {
|
|
110
|
+
if (name === "list") return callable((args) => {
|
|
111
|
+
if (args.length > 1) throw new BaseEngineError("FORMULA_EVAL_ERROR", "Cannot call function \"list\", too many arguments.");
|
|
112
|
+
return args.flatMap((arg) => listValue(arg));
|
|
113
|
+
});
|
|
114
|
+
if (name === "link") return callable((args) => {
|
|
115
|
+
const target = String(args[0] ?? "");
|
|
116
|
+
const display = args[1];
|
|
117
|
+
const unwrapped = target.replace(/^\[\[/u, "").replace(/\]\]$/u, "");
|
|
118
|
+
if (display !== void 0 && display !== null) return `[[${unwrapped}|${String(display)}]]`;
|
|
119
|
+
return `[[${unwrapped}]]`;
|
|
120
|
+
});
|
|
121
|
+
if (name === "if") return callable((args) => truthy(args[0]) ? args[1] : args[2] ?? null);
|
|
122
|
+
if (name === "now") return callable(() => /* @__PURE__ */ new Date());
|
|
123
|
+
if (name === "today") return callable(() => {
|
|
124
|
+
const d = /* @__PURE__ */ new Date();
|
|
125
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
126
|
+
});
|
|
127
|
+
if (name === "date") return callable((args) => dateValue(args[0]));
|
|
128
|
+
if (name === "duration") return callable((args) => {
|
|
129
|
+
if (typeof args[0] === "string") return parseDuration(args[0]) ?? null;
|
|
130
|
+
if (typeof args[0] === "number") return durationValue(args[0]);
|
|
131
|
+
return null;
|
|
132
|
+
});
|
|
133
|
+
if (name === "number") return callable((args) => {
|
|
134
|
+
const val = args[0];
|
|
135
|
+
if (typeof val === "boolean") return val ? 1 : 0;
|
|
136
|
+
const result = numberValue(val);
|
|
137
|
+
if (typeof val === "string" && val.trim() !== "" && !Number.isFinite(Number(val))) throw new BaseEngineError("FORMULA_EVAL_ERROR", `Unable to parse "${val}" as a number.`);
|
|
138
|
+
return result;
|
|
139
|
+
});
|
|
140
|
+
if (name === "file") return callable((args) => {
|
|
141
|
+
const target = String(args[0] ?? "");
|
|
142
|
+
const inspection = context.byPath.get(target) ?? context.byBasename.get(normalizeComparable$1(target));
|
|
143
|
+
return inspection ? fileObject(inspection, context) : null;
|
|
144
|
+
});
|
|
145
|
+
if (name === "min") return callable((args) => Math.min(...args.map((a) => numberValue(a))));
|
|
146
|
+
if (name === "max") return callable((args) => Math.max(...args.map((a) => numberValue(a))));
|
|
147
|
+
if (name === "escapeHTML") return callable((args) => escapeHtml(String(args[0] ?? "")));
|
|
148
|
+
if (name === "html") return callable((args) => String(args[0] ?? ""));
|
|
149
|
+
if (name === "image") return callable((args) => `})`);
|
|
150
|
+
if (name === "icon") return callable((args) => String(args[0] ?? ""));
|
|
151
|
+
if (name === "random") return callable(() => Math.random());
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function readMember(value, property, context) {
|
|
155
|
+
if (value === null || value === void 0) {
|
|
156
|
+
if (property === "isEmpty") return callable(() => true);
|
|
157
|
+
if (property === "contains" || property === "containsAny") return callable(() => false);
|
|
158
|
+
if (property === "asLink" || property === "asFile" || property === "hasLink" || property === "hasProperty" || property === "hasTag" || property === "inFolder") return callable(() => null);
|
|
159
|
+
if (property === "round" || property === "floor" || property === "ceil") return callable(() => null);
|
|
160
|
+
if (property === "toFixed") return callable(() => null);
|
|
161
|
+
if (property === "toString") return callable(() => "");
|
|
162
|
+
if (property === "matches") return callable(() => false);
|
|
163
|
+
if (property === "join" || property === "sort" || property === "map") return callable(() => []);
|
|
164
|
+
if (property === "containsAll") return callable(() => false);
|
|
165
|
+
if (property === "format") return callable(() => null);
|
|
166
|
+
if (property === "keys" || property === "values") return callable(() => []);
|
|
167
|
+
if (property === "length") return 0;
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
if (isDuration(value) && property in value) return value[property];
|
|
171
|
+
if (value instanceof RegExp) {
|
|
172
|
+
if (property === "matches") return callable((args) => value.test(String(args[0] ?? "")));
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
if (property === "length") return value.length;
|
|
176
|
+
if (property === "contains") return callable((args) => value.some((item) => sameComparable(item, args[0])));
|
|
177
|
+
if (property === "containsAny") return callable((args) => value.some((item) => args.flatMap((arg) => listValue(arg)).some((arg) => sameComparable(item, arg))));
|
|
178
|
+
if (property === "isEmpty") return callable(() => value.length === 0);
|
|
179
|
+
if (property === "unique") return callable(() => uniqueValues(value));
|
|
180
|
+
if (property === "sum") return callable(() => sumNumbers(value));
|
|
181
|
+
if (property === "mean" || property === "average") return callable(() => meanNumbers(value));
|
|
182
|
+
if (property === "min") return callable(() => minNumber(value));
|
|
183
|
+
if (property === "max") return callable(() => maxNumber(value));
|
|
184
|
+
if (property === "median") return callable(() => medianNumbers(value));
|
|
185
|
+
if (property === "stddev") return callable(() => stddevNumbers(value));
|
|
186
|
+
if (property === "filter") return callable((args) => {
|
|
187
|
+
const predicate = args[0];
|
|
188
|
+
return typeof predicate === "function" ? value.filter((item) => Boolean(predicate(item))) : value;
|
|
189
|
+
});
|
|
190
|
+
if (property === "containsAll") return callable((args) => {
|
|
191
|
+
return args.flatMap((arg) => listValue(arg)).every((item) => value.some((v) => sameComparable(v, item)));
|
|
192
|
+
});
|
|
193
|
+
if (property === "join") return callable((args) => value.join(String(args[0] ?? ", ")));
|
|
194
|
+
if (property === "sort") return callable(() => [...value].sort(compareValuesForSort));
|
|
195
|
+
if (property === "map") return callable((args) => {
|
|
196
|
+
const fn = args[0];
|
|
197
|
+
return typeof fn === "function" ? value.map((item) => fn(item)) : value;
|
|
198
|
+
});
|
|
199
|
+
if (property === "reduce") return callable(() => null);
|
|
200
|
+
if (property === "flat") return callable(() => value.flat(Number.POSITIVE_INFINITY));
|
|
201
|
+
if (property === "reverse") return callable(() => [...value].reverse());
|
|
202
|
+
if (property === "slice") return callable((args) => {
|
|
203
|
+
const start = numberValue(args[0]);
|
|
204
|
+
const end = args[1] !== void 0 ? numberValue(args[1]) : void 0;
|
|
205
|
+
return value.slice(start, end);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (typeof value === "string") {
|
|
209
|
+
if (property === "length") return value.length;
|
|
210
|
+
if (property === "contains") return callable((args) => normalizeComparable$1(value).includes(normalizeComparable$1(args[0])));
|
|
211
|
+
if (property === "containsAny") return callable((args) => args.flatMap((arg) => listValue(arg)).some((arg) => normalizeComparable$1(value).includes(normalizeComparable$1(arg))));
|
|
212
|
+
if (property === "isEmpty") return callable(() => value.length === 0);
|
|
213
|
+
if (property === "toString") return callable(() => value);
|
|
214
|
+
if (property === "asFile") return callable(() => lookupFile(value, context));
|
|
215
|
+
if (property === "linksTo") return callable((args) => {
|
|
216
|
+
const source = lookupFile(value, context);
|
|
217
|
+
const target = linkTarget(args[0]);
|
|
218
|
+
return Boolean(source && sameComparable(isRecord$1(source) && isRecord$1(source.file) ? source.file.path : source.path, target));
|
|
219
|
+
});
|
|
220
|
+
if (property === "lower") return callable(() => value.toLowerCase());
|
|
221
|
+
if (property === "title") return callable(() => value.replace(/\b\w/gu, (char) => char.toUpperCase()));
|
|
222
|
+
if (property === "trim") return callable(() => value.trim());
|
|
223
|
+
if (property === "startsWith") return callable((args) => value.startsWith(String(args[0] ?? "")));
|
|
224
|
+
if (property === "endsWith") return callable((args) => value.endsWith(String(args[0] ?? "")));
|
|
225
|
+
if (property === "replace") return callable((args) => {
|
|
226
|
+
const pattern = String(args[0] ?? "");
|
|
227
|
+
const replacement = String(args[1] ?? "");
|
|
228
|
+
return value.split(pattern).join(replacement);
|
|
229
|
+
});
|
|
230
|
+
if (property === "repeat") return callable((args) => value.repeat(numberValue(args[0])));
|
|
231
|
+
if (property === "reverse") return callable(() => [...value].reverse().join(""));
|
|
232
|
+
if (property === "slice") return callable((args) => {
|
|
233
|
+
const start = numberValue(args[0]);
|
|
234
|
+
const end = args[1] !== void 0 ? numberValue(args[1]) : void 0;
|
|
235
|
+
return value.slice(start, end);
|
|
236
|
+
});
|
|
237
|
+
if (property === "split") return callable((args) => value.split(String(args[0] ?? "")));
|
|
238
|
+
if (property === "relative") {
|
|
239
|
+
const d = dateValue(value);
|
|
240
|
+
if (d) return callable(() => relativeTime(d));
|
|
241
|
+
}
|
|
242
|
+
if (property === "date") {
|
|
243
|
+
const d = dateValue(value);
|
|
244
|
+
if (d) return callable(() => new Date(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
245
|
+
}
|
|
246
|
+
if (property === "time") {
|
|
247
|
+
const d = dateValue(value);
|
|
248
|
+
if (d) return callable(() => {
|
|
249
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
250
|
+
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (property === "format") {
|
|
254
|
+
const d = dateValue(value);
|
|
255
|
+
if (d) return callable((args) => {
|
|
256
|
+
return formatDate(d, String(args[0] ?? "YYYY-MM-DD"));
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (typeof value === "number") {
|
|
261
|
+
if (property === "round") return callable((args) => {
|
|
262
|
+
const digits = args[0];
|
|
263
|
+
if (digits === void 0 || digits === null) return Math.round(value);
|
|
264
|
+
const factor = 10 ** numberValue(digits);
|
|
265
|
+
return Math.round(value * factor) / factor;
|
|
266
|
+
});
|
|
267
|
+
if (property === "floor") return callable(() => Math.floor(value));
|
|
268
|
+
if (property === "ceil") return callable(() => Math.ceil(value));
|
|
269
|
+
if (property === "abs") return callable(() => Math.abs(value));
|
|
270
|
+
if (property === "toFixed") return callable((args) => value.toFixed(numberValue(args[0])));
|
|
271
|
+
if (property === "isEmpty") return callable(() => false);
|
|
272
|
+
}
|
|
273
|
+
if (value instanceof Date) {
|
|
274
|
+
if (property === "year") return value.getFullYear();
|
|
275
|
+
if (property === "month") return value.getMonth() + 1;
|
|
276
|
+
if (property === "day") return value.getDate();
|
|
277
|
+
if (property === "hour") return value.getHours();
|
|
278
|
+
if (property === "minute") return value.getMinutes();
|
|
279
|
+
if (property === "second") return value.getSeconds();
|
|
280
|
+
if (property === "millisecond") return value.getMilliseconds();
|
|
281
|
+
if (property === "date") return callable(() => new Date(value.getFullYear(), value.getMonth(), value.getDate()));
|
|
282
|
+
if (property === "time") return callable(() => {
|
|
283
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
284
|
+
return `${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}`;
|
|
285
|
+
});
|
|
286
|
+
if (property === "relative") return callable(() => relativeTime(value));
|
|
287
|
+
if (property === "isEmpty") return callable(() => false);
|
|
288
|
+
if (property === "toString") return callable(() => formatDateTimeForOutput(value));
|
|
289
|
+
if (property === "format") return callable((args) => {
|
|
290
|
+
return formatDate(value, String(args[0] ?? "YYYY-MM-DD"));
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (property === "isTruthy") return callable(() => truthy(value));
|
|
294
|
+
if (property === "isType") return callable((args) => {
|
|
295
|
+
const expected = String(args[0] ?? "");
|
|
296
|
+
if (expected === "number") return typeof value === "number";
|
|
297
|
+
if (expected === "string") return typeof value === "string";
|
|
298
|
+
if (expected === "boolean") return typeof value === "boolean";
|
|
299
|
+
if (expected === "null") return value === null;
|
|
300
|
+
if (expected === "date") return value instanceof Date;
|
|
301
|
+
if (expected === "list") return Array.isArray(value);
|
|
302
|
+
if (expected === "object") return isRecord$1(value);
|
|
303
|
+
return false;
|
|
304
|
+
});
|
|
305
|
+
if (property === "toString") return callable(() => String(value ?? ""));
|
|
306
|
+
if (property === "isEmpty") return callable(() => isEmpty(value));
|
|
307
|
+
if (isRecord$1(value)) {
|
|
308
|
+
if (property === "keys") return callable(() => Object.keys(value));
|
|
309
|
+
if (property === "values") return callable(() => Object.values(value));
|
|
310
|
+
const direct = value[property];
|
|
311
|
+
if (direct !== void 0) return direct;
|
|
312
|
+
if (property in value) return value[property];
|
|
313
|
+
const normalized = normalizeName(property);
|
|
314
|
+
for (const [key, item] of Object.entries(value)) if (normalizeName(key) === normalized) return item;
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
function readIndex(value, index) {
|
|
319
|
+
if (isRecord$1(value) && typeof index === "string") {
|
|
320
|
+
if (index in value) return value[index];
|
|
321
|
+
const normalized = normalizeName(index);
|
|
322
|
+
for (const [key, item] of Object.entries(value)) if (normalizeName(key) === normalized) return item;
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
if (!Array.isArray(value)) return null;
|
|
326
|
+
return value[numberValue(index)] ?? null;
|
|
327
|
+
}
|
|
328
|
+
function callValue(callee, args) {
|
|
329
|
+
if (isCallable(callee)) return callee.call(args);
|
|
330
|
+
throw new BaseEngineError("FORMULA_EVAL_ERROR", "Expression value is not callable");
|
|
331
|
+
}
|
|
332
|
+
function evaluateBinary(operator, left, right) {
|
|
333
|
+
switch (operator) {
|
|
334
|
+
case "&&": return truthy(left) && truthy(right);
|
|
335
|
+
case "||": return truthy(left) || truthy(right);
|
|
336
|
+
case "==": return equalValues(left, right);
|
|
337
|
+
case "!=": return !equalValues(left, right);
|
|
338
|
+
case "<": return comparableNumber(left) < comparableNumber(right);
|
|
339
|
+
case "<=": return comparableNumber(left) <= comparableNumber(right);
|
|
340
|
+
case ">": return comparableNumber(left) > comparableNumber(right);
|
|
341
|
+
case ">=": return comparableNumber(left) >= comparableNumber(right);
|
|
342
|
+
case "+": return addValues(left, right);
|
|
343
|
+
case "-": return subtractValues(left, right);
|
|
344
|
+
case "*": return multiplyValues(left, right);
|
|
345
|
+
case "/": return numberValue(left) / numberValue(right);
|
|
346
|
+
case "%": return numberValue(left) % numberValue(right);
|
|
347
|
+
default: throw new BaseEngineError("UNSUPPORTED_FORMULA", `Unsupported operator: ${operator}`, { operator });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function addValues(left, right) {
|
|
351
|
+
const leftDate = dateValue(left);
|
|
352
|
+
if (leftDate && typeof right === "string") {
|
|
353
|
+
const duration = parseDuration(right);
|
|
354
|
+
if (duration) return addDurationToDate(leftDate, duration, 1);
|
|
355
|
+
}
|
|
356
|
+
if (typeof left === "string" || typeof right === "string") return String(left ?? "") + String(right ?? "");
|
|
357
|
+
return numberValue(left) + numberValue(right);
|
|
358
|
+
}
|
|
359
|
+
function subtractValues(left, right) {
|
|
360
|
+
const leftDate = dateValue(left);
|
|
361
|
+
const rightDate = dateValue(right);
|
|
362
|
+
if (leftDate && isEmpty(right) || rightDate && isEmpty(left)) return null;
|
|
363
|
+
if (leftDate && rightDate) return durationValue(leftDate.valueOf() - rightDate.valueOf());
|
|
364
|
+
if (leftDate && typeof right === "string") {
|
|
365
|
+
const duration = parseDuration(right);
|
|
366
|
+
if (duration) return addDurationToDate(leftDate, duration, -1);
|
|
367
|
+
}
|
|
368
|
+
return numberValue(left) - numberValue(right);
|
|
369
|
+
}
|
|
370
|
+
function multiplyValues(left, right) {
|
|
371
|
+
if (isDuration(left)) {
|
|
372
|
+
const scalar = numberValue(right);
|
|
373
|
+
return durationValue(left.milliseconds * scalar);
|
|
374
|
+
}
|
|
375
|
+
if (isDuration(right)) {
|
|
376
|
+
const scalar = numberValue(left);
|
|
377
|
+
return durationValue(right.milliseconds * scalar);
|
|
378
|
+
}
|
|
379
|
+
return numberValue(left) * numberValue(right);
|
|
380
|
+
}
|
|
381
|
+
function fileObject(inspection, _context) {
|
|
382
|
+
const file = fileReference(inspection);
|
|
383
|
+
return {
|
|
384
|
+
...file,
|
|
385
|
+
file,
|
|
386
|
+
tags: inspection.tags.map((tag) => `#${tag.tag.replace(/^#/, "")}`),
|
|
387
|
+
links: inspection.links.flatMap((link) => link.resolvedPath ?? []),
|
|
388
|
+
backlinks: inspection.backlinks,
|
|
389
|
+
embeds: inspection.embeds.map((link) => link.resolvedPath ?? link.targetText),
|
|
390
|
+
properties: noteObject(inspection),
|
|
391
|
+
asLink: callable(() => `[[${inspection.file.path}]]`),
|
|
392
|
+
inFolder: callable((args) => inspection.file.folder === String(args[0] ?? "") || inspection.file.folder.startsWith(`${String(args[0] ?? "")}/`)),
|
|
393
|
+
hasTag: callable((args) => inspection.tags.some((tag) => args.some((arg) => tagMatches(tag.tag, arg)))),
|
|
394
|
+
hasLink: callable((args) => {
|
|
395
|
+
const target = linkTarget(args[0]);
|
|
396
|
+
return inspection.links.some((link) => sameComparable(link.resolvedPath ?? link.targetText, target));
|
|
397
|
+
}),
|
|
398
|
+
hasProperty: callable((args) => inspection.properties.some((property) => sameComparable(property.name, args[0])))
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function thisObject(inspection, context) {
|
|
402
|
+
return {
|
|
403
|
+
file: fileObject(inspection, context),
|
|
404
|
+
asLink: callable(() => `[[${inspection.file.path}]]`),
|
|
405
|
+
...noteObject(inspection)
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function fileReference(inspection) {
|
|
409
|
+
return {
|
|
410
|
+
name: inspection.file.basename,
|
|
411
|
+
basename: inspection.file.basename,
|
|
412
|
+
path: inspection.file.path,
|
|
413
|
+
folder: inspection.file.folder,
|
|
414
|
+
ext: inspection.file.ext,
|
|
415
|
+
size: inspection.file.size,
|
|
416
|
+
ctime: dateTimeString(inspection.file.ctime),
|
|
417
|
+
mtime: dateTimeString(inspection.file.mtime)
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function noteObject(inspection) {
|
|
421
|
+
return Object.fromEntries(inspection.properties.map((property) => [property.name, property.value]));
|
|
422
|
+
}
|
|
423
|
+
function readProperty(inspection, name) {
|
|
424
|
+
const property = inspection.properties.find((candidate) => normalizeName(candidate.name) === normalizeName(name));
|
|
425
|
+
if (!property) return null;
|
|
426
|
+
if (property.name === "tags" && Array.isArray(property.value)) return property.value.map((tag) => `#${String(tag).replace(/^#/, "")}`);
|
|
427
|
+
return property.value;
|
|
428
|
+
}
|
|
429
|
+
function linkTarget(value) {
|
|
430
|
+
if (isRecord$1(value) && typeof value.path === "string") return value.path;
|
|
431
|
+
if (isRecord$1(value) && isRecord$1(value.file) && typeof value.file.path === "string") return value.file.path;
|
|
432
|
+
return value;
|
|
433
|
+
}
|
|
434
|
+
function lookupFile(value, context) {
|
|
435
|
+
const target = normalizeComparable$1(value);
|
|
436
|
+
const inspection = context.byPath.get(String(value)) ?? context.byBasename.get(target);
|
|
437
|
+
return inspection ? fileObject(inspection, context) : null;
|
|
438
|
+
}
|
|
439
|
+
function callable(call) {
|
|
440
|
+
return { call };
|
|
441
|
+
}
|
|
442
|
+
function isCallable(value) {
|
|
443
|
+
return isRecord$1(value) && typeof value.call === "function";
|
|
444
|
+
}
|
|
445
|
+
function listValue(value) {
|
|
446
|
+
if (Array.isArray(value)) return value;
|
|
447
|
+
if (value === null || value === void 0 || value === "") return [];
|
|
448
|
+
return [value];
|
|
449
|
+
}
|
|
450
|
+
function truthy(value) {
|
|
451
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
452
|
+
return Boolean(value);
|
|
453
|
+
}
|
|
454
|
+
function isEmpty(value) {
|
|
455
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
456
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
function numberValue(value) {
|
|
460
|
+
if (typeof value === "number") return value;
|
|
461
|
+
if (value instanceof Date) return value.valueOf();
|
|
462
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
463
|
+
if (typeof value === "string") {
|
|
464
|
+
const parsed = Number(value);
|
|
465
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
466
|
+
}
|
|
467
|
+
if (isDuration(value)) return value.milliseconds;
|
|
468
|
+
return 0;
|
|
469
|
+
}
|
|
470
|
+
function comparableNumber(value) {
|
|
471
|
+
const date = dateValue(value);
|
|
472
|
+
return date ? date.valueOf() : numberValue(value);
|
|
473
|
+
}
|
|
474
|
+
function dateValue(value) {
|
|
475
|
+
if (value instanceof Date) return value;
|
|
476
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
477
|
+
const trimmed = value.trim();
|
|
478
|
+
const dateOnly = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/u);
|
|
479
|
+
if (dateOnly) return new Date(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3]));
|
|
480
|
+
const dateTime = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?)$/u);
|
|
481
|
+
if (dateTime) return new Date(Number(dateTime[1]), Number(dateTime[2]) - 1, Number(dateTime[3]), Number(dateTime[4] ?? 0), Number(dateTime[5] ?? 0), Number(dateTime[6] ?? 0), Number((dateTime[7] ?? "0").padEnd(3, "0")));
|
|
482
|
+
const parsed = new Date(value);
|
|
483
|
+
return Number.isNaN(parsed.valueOf()) ? null : parsed;
|
|
484
|
+
}
|
|
485
|
+
const DURATION_UNITS = [
|
|
486
|
+
{
|
|
487
|
+
pattern: "d",
|
|
488
|
+
toDays: 1,
|
|
489
|
+
field: "days"
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
pattern: "w",
|
|
493
|
+
toDays: 7,
|
|
494
|
+
field: "days"
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
pattern: "M",
|
|
498
|
+
toDays: 31,
|
|
499
|
+
field: "months"
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
pattern: "y",
|
|
503
|
+
toDays: 365,
|
|
504
|
+
field: "years"
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
pattern: "h",
|
|
508
|
+
toDays: 1 / 24,
|
|
509
|
+
field: "hours"
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
pattern: "m",
|
|
513
|
+
toDays: 1 / 1440,
|
|
514
|
+
field: "minutes"
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
pattern: "s",
|
|
518
|
+
toDays: 1 / 86400,
|
|
519
|
+
field: "seconds"
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
pattern: "day",
|
|
523
|
+
toDays: 1,
|
|
524
|
+
field: "days"
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
pattern: "days",
|
|
528
|
+
toDays: 1,
|
|
529
|
+
field: "days"
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
pattern: "week",
|
|
533
|
+
toDays: 7,
|
|
534
|
+
field: "days"
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
pattern: "weeks",
|
|
538
|
+
toDays: 7,
|
|
539
|
+
field: "days"
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
pattern: "month",
|
|
543
|
+
toDays: 31,
|
|
544
|
+
field: "months"
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
pattern: "months",
|
|
548
|
+
toDays: 31,
|
|
549
|
+
field: "months"
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
pattern: "year",
|
|
553
|
+
toDays: 365,
|
|
554
|
+
field: "years"
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
pattern: "years",
|
|
558
|
+
toDays: 365,
|
|
559
|
+
field: "years"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
pattern: "hour",
|
|
563
|
+
toDays: 1 / 24,
|
|
564
|
+
field: "hours"
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
pattern: "hours",
|
|
568
|
+
toDays: 1 / 24,
|
|
569
|
+
field: "hours"
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
pattern: "minute",
|
|
573
|
+
toDays: 1 / 1440,
|
|
574
|
+
field: "minutes"
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
pattern: "minutes",
|
|
578
|
+
toDays: 1 / 1440,
|
|
579
|
+
field: "minutes"
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
pattern: "second",
|
|
583
|
+
toDays: 1 / 86400,
|
|
584
|
+
field: "seconds"
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
pattern: "seconds",
|
|
588
|
+
toDays: 1 / 86400,
|
|
589
|
+
field: "seconds"
|
|
590
|
+
}
|
|
591
|
+
];
|
|
592
|
+
function parseDuration(value) {
|
|
593
|
+
const shortMatch = value.match(/^(\d+)([dwMmyhs])$/u);
|
|
594
|
+
if (shortMatch) {
|
|
595
|
+
const amount = Number(shortMatch[1]);
|
|
596
|
+
const unit = shortMatch[2];
|
|
597
|
+
const unitDef = DURATION_UNITS.find((u) => u.pattern === unit);
|
|
598
|
+
if (unitDef) return durationValue(amount * unitDef.toDays * 864e5, unitDef.field === "years" ? amount : 0, unitDef.field === "months" ? amount : 0, unitDef.field === "days" ? amount * unitDef.toDays : 0);
|
|
599
|
+
}
|
|
600
|
+
const namedMatch = value.match(/^(\d+)\s+(\S+)$/u);
|
|
601
|
+
if (namedMatch) {
|
|
602
|
+
const amount = Number(namedMatch[1]);
|
|
603
|
+
const unitName = namedMatch[2];
|
|
604
|
+
const unitDef = DURATION_UNITS.find((u) => u.pattern === unitName);
|
|
605
|
+
if (unitDef) return durationValue(amount * unitDef.toDays * 864e5, unitDef.field === "years" ? amount : 0, unitDef.field === "months" ? amount : 0, unitDef.field === "days" ? amount * unitDef.toDays : 0);
|
|
606
|
+
}
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
function durationValue(milliseconds, calendarYears = 0, calendarMonths = 0, calendarDays = 0) {
|
|
610
|
+
return {
|
|
611
|
+
milliseconds,
|
|
612
|
+
days: nearInteger(milliseconds / 864e5),
|
|
613
|
+
hours: nearInteger(milliseconds / 36e5),
|
|
614
|
+
minutes: nearInteger(milliseconds / 6e4),
|
|
615
|
+
seconds: nearInteger(milliseconds / 1e3),
|
|
616
|
+
months: nearInteger(milliseconds / (365.28478589915085 * 864e5 / 12)),
|
|
617
|
+
years: nearInteger(milliseconds / (365.28478589915085 * 864e5)),
|
|
618
|
+
calendarYears,
|
|
619
|
+
calendarMonths,
|
|
620
|
+
calendarDays
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function nearInteger(value) {
|
|
624
|
+
const rounded = Math.round(value);
|
|
625
|
+
return Math.abs(value - rounded) < 1e-9 ? rounded : value;
|
|
626
|
+
}
|
|
627
|
+
function addDurationToDate(date, duration, direction) {
|
|
628
|
+
const next = new Date(date.valueOf());
|
|
629
|
+
const calendarYears = duration.calendarYears ?? 0;
|
|
630
|
+
const calendarMonths = duration.calendarMonths ?? 0;
|
|
631
|
+
const calendarDays = duration.calendarDays ?? 0;
|
|
632
|
+
if (calendarYears !== 0) next.setFullYear(next.getFullYear() + direction * calendarYears);
|
|
633
|
+
if (calendarMonths !== 0) next.setMonth(next.getMonth() + direction * calendarMonths);
|
|
634
|
+
if (calendarDays !== 0) next.setDate(next.getDate() + direction * calendarDays);
|
|
635
|
+
const calendarMilliseconds = calendarYears * 365 * 864e5 + calendarMonths * 31 * 864e5 + calendarDays * 864e5;
|
|
636
|
+
const clockMilliseconds = duration.milliseconds - calendarMilliseconds;
|
|
637
|
+
if (clockMilliseconds !== 0) next.setTime(next.valueOf() + direction * clockMilliseconds);
|
|
638
|
+
return next;
|
|
639
|
+
}
|
|
640
|
+
function dateTimeString(value) {
|
|
641
|
+
if (!value) return;
|
|
642
|
+
const date = new Date(value);
|
|
643
|
+
if (Number.isNaN(date.valueOf())) return value;
|
|
644
|
+
return formatDateTimeForOutput(date);
|
|
645
|
+
}
|
|
646
|
+
function formatDateTimeForOutput(date) {
|
|
647
|
+
const pad = (part) => String(part).padStart(2, "0");
|
|
648
|
+
const datePart = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
649
|
+
if (date.getHours() === 0 && date.getMinutes() === 0 && date.getSeconds() === 0 && date.getMilliseconds() === 0) return datePart;
|
|
650
|
+
return `${datePart}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
651
|
+
}
|
|
652
|
+
function relativeTime(date) {
|
|
653
|
+
const diff = Date.now() - date.valueOf();
|
|
654
|
+
const absDiff = Math.abs(diff);
|
|
655
|
+
const isPast = diff > 0;
|
|
656
|
+
const seconds = Math.floor(absDiff / 1e3);
|
|
657
|
+
const minutes = Math.floor(absDiff / 6e4);
|
|
658
|
+
const hours = Math.floor(absDiff / 36e5);
|
|
659
|
+
const days = Math.floor(absDiff / 864e5);
|
|
660
|
+
const months = Math.floor(absDiff / (31 * 864e5));
|
|
661
|
+
const years = Math.floor(absDiff / (365 * 864e5));
|
|
662
|
+
const suffix = isPast ? "ago" : "from now";
|
|
663
|
+
if (years > 0) return `${years} year${years > 1 ? "s" : ""} ${suffix}`;
|
|
664
|
+
if (months > 0) return `${months} month${months > 1 ? "s" : ""} ${suffix}`;
|
|
665
|
+
if (days > 0) return `${days} day${days > 1 ? "s" : ""} ${suffix}`;
|
|
666
|
+
if (hours > 0) return `${hours} hour${hours > 1 ? "s" : ""} ${suffix}`;
|
|
667
|
+
if (minutes > 0) return `${minutes} minute${minutes > 1 ? "s" : ""} ${suffix}`;
|
|
668
|
+
if (seconds > 0) return `${seconds} second${seconds > 1 ? "s" : ""} ${suffix}`;
|
|
669
|
+
return "just now";
|
|
670
|
+
}
|
|
671
|
+
function uniqueValues(values) {
|
|
672
|
+
const seen = /* @__PURE__ */ new Set();
|
|
673
|
+
return values.filter((value) => {
|
|
674
|
+
const key = normalizeComparable$1(value);
|
|
675
|
+
if (seen.has(key)) return false;
|
|
676
|
+
seen.add(key);
|
|
677
|
+
return true;
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
function sameComparable(left, right) {
|
|
681
|
+
return normalizeComparable$1(left) === normalizeComparable$1(right);
|
|
682
|
+
}
|
|
683
|
+
function equalValues(left, right) {
|
|
684
|
+
if (typeof left === "string" && typeof right === "string") return left === right;
|
|
685
|
+
return sameComparable(left, right);
|
|
686
|
+
}
|
|
687
|
+
function normalizeComparable$1(value) {
|
|
688
|
+
if (isRecord$1(value) && typeof value.path === "string") return normalizeComparable$1(value.path);
|
|
689
|
+
if (isRecord$1(value) && isRecord$1(value.file) && typeof value.file.path === "string") return normalizeComparable$1(value.file.path);
|
|
690
|
+
return String(value).replace(/^#/u, "").replace(/^\[\[/u, "").replace(/\]\]$/u, "").replace(/\.md$/u, "").split("/").at(-1)?.toLowerCase() ?? "";
|
|
691
|
+
}
|
|
692
|
+
function tagMatches(tag, value) {
|
|
693
|
+
const normalizedTag = normalizeComparable$1(tag);
|
|
694
|
+
const normalizedValue = normalizeComparable$1(value);
|
|
695
|
+
return normalizedTag === normalizedValue || normalizedTag.startsWith(`${normalizedValue}/`);
|
|
696
|
+
}
|
|
697
|
+
function normalizeName(name) {
|
|
698
|
+
return name.replace(/-/gu, "").toLowerCase();
|
|
699
|
+
}
|
|
700
|
+
function isDuration(value) {
|
|
701
|
+
return isRecord$1(value) && typeof value.milliseconds === "number" && typeof value.days === "number";
|
|
702
|
+
}
|
|
703
|
+
function isRecord$1(value) {
|
|
704
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
705
|
+
}
|
|
706
|
+
function formatDate(date, pattern) {
|
|
707
|
+
const pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
708
|
+
const dayNames = [
|
|
709
|
+
"Sunday",
|
|
710
|
+
"Monday",
|
|
711
|
+
"Tuesday",
|
|
712
|
+
"Wednesday",
|
|
713
|
+
"Thursday",
|
|
714
|
+
"Friday",
|
|
715
|
+
"Saturday"
|
|
716
|
+
];
|
|
717
|
+
const dayNamesShort = [
|
|
718
|
+
"Sun",
|
|
719
|
+
"Mon",
|
|
720
|
+
"Tue",
|
|
721
|
+
"Wed",
|
|
722
|
+
"Thu",
|
|
723
|
+
"Fri",
|
|
724
|
+
"Sat"
|
|
725
|
+
];
|
|
726
|
+
const monthNames = [
|
|
727
|
+
"January",
|
|
728
|
+
"February",
|
|
729
|
+
"March",
|
|
730
|
+
"April",
|
|
731
|
+
"May",
|
|
732
|
+
"June",
|
|
733
|
+
"July",
|
|
734
|
+
"August",
|
|
735
|
+
"September",
|
|
736
|
+
"October",
|
|
737
|
+
"November",
|
|
738
|
+
"December"
|
|
739
|
+
];
|
|
740
|
+
const monthNamesShort = [
|
|
741
|
+
"Jan",
|
|
742
|
+
"Feb",
|
|
743
|
+
"Mar",
|
|
744
|
+
"Apr",
|
|
745
|
+
"May",
|
|
746
|
+
"Jun",
|
|
747
|
+
"Jul",
|
|
748
|
+
"Aug",
|
|
749
|
+
"Sep",
|
|
750
|
+
"Oct",
|
|
751
|
+
"Nov",
|
|
752
|
+
"Dec"
|
|
753
|
+
];
|
|
754
|
+
const day = date.getDate();
|
|
755
|
+
const ordinal = (n) => {
|
|
756
|
+
const suffix = [
|
|
757
|
+
"th",
|
|
758
|
+
"st",
|
|
759
|
+
"nd",
|
|
760
|
+
"rd"
|
|
761
|
+
];
|
|
762
|
+
const v = n % 100;
|
|
763
|
+
return n + (suffix[(v - 20) % 10] ?? suffix[v] ?? suffix[0]);
|
|
764
|
+
};
|
|
765
|
+
return pattern.replace(/dddd/gu, dayNames[date.getDay()] ?? "").replace(/ddd/gu, dayNamesShort[date.getDay()] ?? "").replace(/DD/gu, pad(date.getDate())).replace(/Do/gu, ordinal(day)).replace(/MMMM/gu, monthNames[date.getMonth()] ?? "").replace(/MMM/gu, monthNamesShort[date.getMonth()] ?? "").replace(/MM/gu, pad(date.getMonth() + 1)).replace(/YYYY/gu, String(date.getFullYear())).replace(/YY/gu, String(date.getFullYear()).slice(-2)).replace(/HH/gu, pad(date.getHours())).replace(/mm/gu, pad(date.getMinutes())).replace(/ss/gu, pad(date.getSeconds()));
|
|
766
|
+
}
|
|
767
|
+
function compareValuesForSort(a, b) {
|
|
768
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
769
|
+
return String(a ?? "").localeCompare(String(b ?? ""));
|
|
770
|
+
}
|
|
771
|
+
function escapeHtml(value) {
|
|
772
|
+
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """).replace(/'/gu, "'");
|
|
773
|
+
}
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/query.ts
|
|
776
|
+
function queryBase(base, inspections, options = {}) {
|
|
777
|
+
const byPath = new Map(inspections.map((inspection) => [inspection.file.path, inspection]));
|
|
778
|
+
const byBasename = new Map(inspections.map((inspection) => [normalizeComparable(inspection.file.basename), inspection]));
|
|
779
|
+
const context = options.context ? byPath.get(options.context) : void 0;
|
|
780
|
+
const view = selectView(base, options.view);
|
|
781
|
+
const columns = buildColumns(base, view);
|
|
782
|
+
const rows = [];
|
|
783
|
+
for (const inspection of inspections) {
|
|
784
|
+
const formulas = evaluateFormulas(base, inspection, {
|
|
785
|
+
row: inspection,
|
|
786
|
+
context,
|
|
787
|
+
formulas: {},
|
|
788
|
+
byPath,
|
|
789
|
+
byBasename
|
|
790
|
+
});
|
|
791
|
+
const evalContext = {
|
|
792
|
+
row: inspection,
|
|
793
|
+
context,
|
|
794
|
+
formulas,
|
|
795
|
+
byPath,
|
|
796
|
+
byBasename
|
|
797
|
+
};
|
|
798
|
+
if (inspection.file.kind !== "markdown" || !evaluateFilters(base.filters, evalContext) || !evaluateFilters(view?.filters, evalContext)) continue;
|
|
799
|
+
rows.push({
|
|
800
|
+
file: sanitizeRowFile(inspection.file),
|
|
801
|
+
values: Object.fromEntries(columns.map((column) => [column.id, readColumnValue(column.id, evalContext)])),
|
|
802
|
+
formulas,
|
|
803
|
+
sortValues: Object.fromEntries((view?.sort ?? []).map((item) => [item.property, readColumnValue(item.property, evalContext)]))
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
rows.sort((left, right) => compareRows(left, right, view?.sort, columns));
|
|
807
|
+
const limitedRows = view?.limit && view.limit > 0 ? rows.slice(0, view.limit) : rows;
|
|
808
|
+
const resolvedColumns = resolveColumnTypes(columns, limitedRows, inspections);
|
|
809
|
+
return {
|
|
810
|
+
base: base.path,
|
|
811
|
+
view: view?.name,
|
|
812
|
+
context: options.context,
|
|
813
|
+
meta: buildMeta(view),
|
|
814
|
+
columns: resolvedColumns,
|
|
815
|
+
rows: limitedRows.map((row) => projectRow(row, resolvedColumns)),
|
|
816
|
+
groups: buildGroups(limitedRows, view),
|
|
817
|
+
summaries: buildSummaries(limitedRows, view, base)
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function evaluateFormulas(base, _inspection, context) {
|
|
821
|
+
const formulas = {};
|
|
822
|
+
const evalContext = {
|
|
823
|
+
...context,
|
|
824
|
+
formulas
|
|
825
|
+
};
|
|
826
|
+
const evaluating = /* @__PURE__ */ new Set();
|
|
827
|
+
const evaluateFormula = (name) => {
|
|
828
|
+
if (Object.hasOwn(formulas, name)) return formulas[name];
|
|
829
|
+
const source = base.formulas[name];
|
|
830
|
+
if (source === void 0) return null;
|
|
831
|
+
if (evaluating.has(name)) throw new BaseEngineError("FORMULA_EVAL_ERROR", `Circular formula reference: ${name}`, { formula: name });
|
|
832
|
+
evaluating.add(name);
|
|
833
|
+
try {
|
|
834
|
+
formulas[name] = evaluateExpression(parseExpression(source), {
|
|
835
|
+
...evalContext,
|
|
836
|
+
formulas: new Proxy(formulas, { get: (target, property) => typeof property === "string" ? evaluateFormula(property) : Reflect.get(target, property) })
|
|
837
|
+
});
|
|
838
|
+
} catch (error) {
|
|
839
|
+
formulas[name] = error instanceof BaseEngineError ? error : new BaseEngineError("FORMULA_EVAL_ERROR", error instanceof Error ? error.message : String(error), { formula: name });
|
|
840
|
+
}
|
|
841
|
+
evaluating.delete(name);
|
|
842
|
+
return formulas[name];
|
|
843
|
+
};
|
|
844
|
+
for (const name of Object.keys(base.formulas)) evaluateFormula(name);
|
|
845
|
+
return formulas;
|
|
846
|
+
}
|
|
847
|
+
function evaluateFilters(filters, context) {
|
|
848
|
+
if (!filters) return true;
|
|
849
|
+
if (typeof filters === "string") return Boolean(evaluateExpression(parseExpression(filters), context));
|
|
850
|
+
if (Array.isArray(filters)) return filters.every((filter) => evaluateFilters(filter, context));
|
|
851
|
+
if (!isRecord(filters)) return true;
|
|
852
|
+
if (Array.isArray(filters.and)) return filters.and.every((filter) => evaluateFilters(filter, context));
|
|
853
|
+
if (Array.isArray(filters.or)) return filters.or.some((filter) => evaluateFilters(filter, context));
|
|
854
|
+
if (Array.isArray(filters.not)) return !filters.not.some((filter) => evaluateFilters(filter, context));
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
function readColumnValue(id, context) {
|
|
858
|
+
const value = evaluateExpression(parseExpression(id), context);
|
|
859
|
+
if (value !== null) return value;
|
|
860
|
+
for (const qualified of [`note.${id}`, `file.${id}`]) {
|
|
861
|
+
const resolved = evaluateExpression(parseExpression(qualified), context);
|
|
862
|
+
if (resolved !== null) return resolved;
|
|
863
|
+
}
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
function selectView(base, requested) {
|
|
867
|
+
if (!requested) return base.views[0];
|
|
868
|
+
const view = base.views.find((v) => v.name === requested);
|
|
869
|
+
if (!view) throw new BaseEngineError("BASE_VIEW_NOT_FOUND", `View not found: ${requested}`, {
|
|
870
|
+
requested,
|
|
871
|
+
availableViews: base.views.map((v) => v.name)
|
|
872
|
+
});
|
|
873
|
+
return view;
|
|
874
|
+
}
|
|
875
|
+
function buildColumns(base, view) {
|
|
876
|
+
return (view?.order && view.order.length > 0 ? view.order : Object.keys(base.properties)).map((id) => {
|
|
877
|
+
const property = findPropertyConfig(base.properties, id);
|
|
878
|
+
return {
|
|
879
|
+
id,
|
|
880
|
+
displayName: property?.displayName ?? defaultDisplayName(id),
|
|
881
|
+
type: valueType(property)
|
|
882
|
+
};
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
function defaultDisplayName(id) {
|
|
886
|
+
if (id === "file.name") return "name";
|
|
887
|
+
if (id === "file.mtime") return "modified time";
|
|
888
|
+
if (id === "file.ctime") return "created time";
|
|
889
|
+
if (id === "file.ext") return "extension";
|
|
890
|
+
if (id === "file.path") return "path";
|
|
891
|
+
return id.startsWith("formula.") ? id.slice(8) : id;
|
|
892
|
+
}
|
|
893
|
+
function findPropertyConfig(properties, id) {
|
|
894
|
+
return properties[id] ?? properties[`note.${id}`] ?? properties[`file.${id}`];
|
|
895
|
+
}
|
|
896
|
+
function valueType(_property) {
|
|
897
|
+
return "any";
|
|
898
|
+
}
|
|
899
|
+
function buildMeta(view) {
|
|
900
|
+
return {
|
|
901
|
+
type: view?.type,
|
|
902
|
+
name: view?.name,
|
|
903
|
+
filters: view?.filters,
|
|
904
|
+
order: view?.order ?? [],
|
|
905
|
+
sort: view?.sort,
|
|
906
|
+
limit: view?.limit,
|
|
907
|
+
groupBy: view?.groupBy,
|
|
908
|
+
summaries: view?.summaries
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function projectRow(row, columns) {
|
|
912
|
+
const data = Object.fromEntries(columns.map((column) => [column.id, Object.hasOwn(row.values, column.id) ? row.values[column.id] : null]));
|
|
913
|
+
const file = {
|
|
914
|
+
path: row.file.path,
|
|
915
|
+
name: row.file.name
|
|
916
|
+
};
|
|
917
|
+
Object.defineProperties(file, {
|
|
918
|
+
basename: {
|
|
919
|
+
value: row.file.basename,
|
|
920
|
+
enumerable: false
|
|
921
|
+
},
|
|
922
|
+
ext: {
|
|
923
|
+
value: row.file.ext,
|
|
924
|
+
enumerable: false
|
|
925
|
+
},
|
|
926
|
+
folder: {
|
|
927
|
+
value: row.file.folder,
|
|
928
|
+
enumerable: false
|
|
929
|
+
},
|
|
930
|
+
size: {
|
|
931
|
+
value: row.file.size,
|
|
932
|
+
enumerable: false
|
|
933
|
+
},
|
|
934
|
+
ctime: {
|
|
935
|
+
value: row.file.ctime,
|
|
936
|
+
enumerable: false
|
|
937
|
+
},
|
|
938
|
+
mtime: {
|
|
939
|
+
value: row.file.mtime,
|
|
940
|
+
enumerable: false
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
const projected = {
|
|
944
|
+
file,
|
|
945
|
+
data
|
|
946
|
+
};
|
|
947
|
+
Object.defineProperties(projected, {
|
|
948
|
+
values: {
|
|
949
|
+
value: row.values,
|
|
950
|
+
enumerable: false
|
|
951
|
+
},
|
|
952
|
+
formulas: {
|
|
953
|
+
value: row.formulas,
|
|
954
|
+
enumerable: false
|
|
955
|
+
},
|
|
956
|
+
sortValues: {
|
|
957
|
+
value: row.sortValues,
|
|
958
|
+
enumerable: false
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
return projected;
|
|
962
|
+
}
|
|
963
|
+
function resolveColumnTypes(columns, rows, inspections) {
|
|
964
|
+
return columns.map((column) => ({
|
|
965
|
+
...column,
|
|
966
|
+
type: resolveColumnType(column.id, rows, inspections)
|
|
967
|
+
}));
|
|
968
|
+
}
|
|
969
|
+
function resolveColumnType(id, rows, inspections) {
|
|
970
|
+
const propertyName = id.replace(/^note\./u, "");
|
|
971
|
+
for (const inspection of inspections) {
|
|
972
|
+
const property = inspection.properties.find((candidate) => candidate.name === propertyName);
|
|
973
|
+
if (property?.valueType) return normalizeColumnType(property.valueType);
|
|
974
|
+
}
|
|
975
|
+
if (id === "file.name" || id === "file.basename" || id === "file.path" || id === "file.folder" || id === "file.ext") return "text";
|
|
976
|
+
if (id === "file.size") return "number";
|
|
977
|
+
if (id === "file.ctime" || id === "file.mtime") return "datetime";
|
|
978
|
+
for (const row of rows) {
|
|
979
|
+
const value = row.values[id];
|
|
980
|
+
if (value !== null && value !== void 0) return inferColumnType(value);
|
|
981
|
+
}
|
|
982
|
+
return "empty";
|
|
983
|
+
}
|
|
984
|
+
function normalizeColumnType(type) {
|
|
985
|
+
if (type === "checkbox") return "boolean";
|
|
986
|
+
if (type === "multitext" || type === "aliases" || type === "tags") return "list";
|
|
987
|
+
if (type === "string") return "text";
|
|
988
|
+
return type;
|
|
989
|
+
}
|
|
990
|
+
function inferColumnType(value) {
|
|
991
|
+
if (value instanceof Date) return isMidnight(value) ? "date" : "datetime";
|
|
992
|
+
if (Array.isArray(value)) return "list";
|
|
993
|
+
if (value instanceof Error) return "error";
|
|
994
|
+
if (typeof value === "string") {
|
|
995
|
+
if (/^\d{4}-\d{2}-\d{2}$/u.test(value)) return "date";
|
|
996
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/u.test(value)) return "datetime";
|
|
997
|
+
if (/^\[\[.*\]\]$/u.test(value)) return "link";
|
|
998
|
+
return "text";
|
|
999
|
+
}
|
|
1000
|
+
if (typeof value === "number") return "number";
|
|
1001
|
+
if (typeof value === "boolean") return "boolean";
|
|
1002
|
+
if (value && typeof value === "object") return "object";
|
|
1003
|
+
return "empty";
|
|
1004
|
+
}
|
|
1005
|
+
function isMidnight(date) {
|
|
1006
|
+
return date.getHours() === 0 && date.getMinutes() === 0 && date.getSeconds() === 0 && date.getMilliseconds() === 0;
|
|
1007
|
+
}
|
|
1008
|
+
function compareRows(left, right, sort, columns = []) {
|
|
1009
|
+
for (const item of sort ?? []) {
|
|
1010
|
+
const result = compareValues(left.sortValues[item.property] ?? left.values[item.property] ?? left.formulas[formulaName(item.property)], right.sortValues[item.property] ?? right.values[item.property] ?? right.formulas[formulaName(item.property)]);
|
|
1011
|
+
if (result !== 0) return item.direction === "DESC" ? -result : result;
|
|
1012
|
+
}
|
|
1013
|
+
const direction = sort?.length ? sort[sort.length - 1]?.direction : void 0;
|
|
1014
|
+
for (const column of columns) {
|
|
1015
|
+
const leftValue = left.values[column.id];
|
|
1016
|
+
const rightValue = right.values[column.id];
|
|
1017
|
+
const result = compareValues(leftValue, rightValue);
|
|
1018
|
+
if (result !== 0) return direction === "DESC" ? -result : result;
|
|
1019
|
+
}
|
|
1020
|
+
return compareValues(left.file.basename, right.file.basename);
|
|
1021
|
+
}
|
|
1022
|
+
function buildGroups(rows, view) {
|
|
1023
|
+
const groupBy = view?.groupBy;
|
|
1024
|
+
return (groupBy ? [groupBy.property] : []).map((id) => {
|
|
1025
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1026
|
+
for (const row of rows) {
|
|
1027
|
+
const value = row.values[id] ?? row.formulas[formulaName(id)] ?? null;
|
|
1028
|
+
const key = comparableKey(value);
|
|
1029
|
+
const bucket = buckets.get(key) ?? {
|
|
1030
|
+
value,
|
|
1031
|
+
rows: []
|
|
1032
|
+
};
|
|
1033
|
+
bucket.rows.push(row);
|
|
1034
|
+
buckets.set(key, bucket);
|
|
1035
|
+
}
|
|
1036
|
+
const direction = groupBy?.property === id ? groupBy.direction : "ASC";
|
|
1037
|
+
return {
|
|
1038
|
+
property: id,
|
|
1039
|
+
direction,
|
|
1040
|
+
buckets: [...buckets.values()].sort((left, right) => {
|
|
1041
|
+
const result = compareValues(left.value, right.value);
|
|
1042
|
+
return direction === "DESC" ? -result : result;
|
|
1043
|
+
}).map((bucket) => ({
|
|
1044
|
+
value: bucket.value,
|
|
1045
|
+
count: bucket.rows.length,
|
|
1046
|
+
rows: bucket.rows.map((row) => row.file.path)
|
|
1047
|
+
}))
|
|
1048
|
+
};
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
function buildSummaries(rows, view, base) {
|
|
1052
|
+
const summaries = { count: rows.length };
|
|
1053
|
+
for (const [key, requestedSummary] of Object.entries(view?.summaries ?? {})) summaries[key] = summarizeValues(rows.map((row) => row.values[key] ?? row.formulas[formulaName(key)] ?? null), typeof requestedSummary === "string" ? requestedSummary : String(requestedSummary), base);
|
|
1054
|
+
return summaries;
|
|
1055
|
+
}
|
|
1056
|
+
function summarizeValues(values, requestedSummary, base) {
|
|
1057
|
+
const custom = base.summaries?.[requestedSummary];
|
|
1058
|
+
if (custom) return evaluateExpression(parseExpression(custom), {
|
|
1059
|
+
row: emptySummaryInspection(),
|
|
1060
|
+
formulas: {},
|
|
1061
|
+
byPath: /* @__PURE__ */ new Map(),
|
|
1062
|
+
byBasename: /* @__PURE__ */ new Map(),
|
|
1063
|
+
values
|
|
1064
|
+
});
|
|
1065
|
+
const normalized = requestedSummary.toLowerCase();
|
|
1066
|
+
const numbers = numericValues(values);
|
|
1067
|
+
const dates = values.map(dateFromValue).filter((value) => value instanceof Date);
|
|
1068
|
+
if (normalized === "sum") return sumNumbers(numbers);
|
|
1069
|
+
if (normalized === "average" || normalized === "mean") return meanNumbers(numbers);
|
|
1070
|
+
if (normalized === "min") return minNumber(numbers);
|
|
1071
|
+
if (normalized === "max") return maxNumber(numbers);
|
|
1072
|
+
if (normalized === "median") return medianNumbers(numbers);
|
|
1073
|
+
if (normalized === "stddev") return stddevNumbers(numbers);
|
|
1074
|
+
if (normalized === "checked") return values.filter((value) => value === true).length;
|
|
1075
|
+
if (normalized === "unchecked") return values.filter((value) => value === false).length;
|
|
1076
|
+
if (normalized === "empty") return values.filter(isEmptySummaryValue).length;
|
|
1077
|
+
if (normalized === "filled") return values.filter((value) => !isEmptySummaryValue(value)).length;
|
|
1078
|
+
if (normalized === "unique") return new Set(values.map(comparableKey)).size;
|
|
1079
|
+
if (normalized === "earliest") return dateSummary(minDate(dates));
|
|
1080
|
+
if (normalized === "latest") return dateSummary(maxDate(dates));
|
|
1081
|
+
if (normalized === "range") {
|
|
1082
|
+
if (numbers.length > 0) return Math.max(...numbers) - Math.min(...numbers);
|
|
1083
|
+
const earliest = minDate(dates);
|
|
1084
|
+
const latest = maxDate(dates);
|
|
1085
|
+
return earliest && latest ? latest.getTime() - earliest.getTime() : null;
|
|
1086
|
+
}
|
|
1087
|
+
return {
|
|
1088
|
+
count: numbers.length,
|
|
1089
|
+
sum: sumNumbers(numbers),
|
|
1090
|
+
average: meanNumbers(numbers)
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
function emptySummaryInspection() {
|
|
1094
|
+
return {
|
|
1095
|
+
file: {
|
|
1096
|
+
path: "",
|
|
1097
|
+
name: "",
|
|
1098
|
+
basename: "",
|
|
1099
|
+
ext: "",
|
|
1100
|
+
folder: "",
|
|
1101
|
+
kind: "markdown"
|
|
1102
|
+
},
|
|
1103
|
+
properties: [],
|
|
1104
|
+
tags: [],
|
|
1105
|
+
links: [],
|
|
1106
|
+
backlinks: [],
|
|
1107
|
+
embeds: []
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function dateFromValue(value) {
|
|
1111
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) return value;
|
|
1112
|
+
if (typeof value !== "string") return null;
|
|
1113
|
+
const date = new Date(value);
|
|
1114
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
1115
|
+
}
|
|
1116
|
+
function minDate(values) {
|
|
1117
|
+
return values.length > 0 ? new Date(Math.min(...values.map((value) => value.getTime()))) : null;
|
|
1118
|
+
}
|
|
1119
|
+
function maxDate(values) {
|
|
1120
|
+
return values.length > 0 ? new Date(Math.max(...values.map((value) => value.getTime()))) : null;
|
|
1121
|
+
}
|
|
1122
|
+
function dateSummary(value) {
|
|
1123
|
+
return value ? value.toISOString() : null;
|
|
1124
|
+
}
|
|
1125
|
+
function isEmptySummaryValue(value) {
|
|
1126
|
+
return value === null || value === void 0 || value === "" || Array.isArray(value) && value.length === 0;
|
|
1127
|
+
}
|
|
1128
|
+
function comparableKey(value) {
|
|
1129
|
+
return JSON.stringify(value) ?? String(value);
|
|
1130
|
+
}
|
|
1131
|
+
function sanitizeRowFile(file) {
|
|
1132
|
+
return {
|
|
1133
|
+
path: file.path,
|
|
1134
|
+
name: file.basename,
|
|
1135
|
+
basename: file.basename,
|
|
1136
|
+
ext: file.ext,
|
|
1137
|
+
folder: file.folder,
|
|
1138
|
+
size: file.size,
|
|
1139
|
+
ctime: file.ctime,
|
|
1140
|
+
mtime: file.mtime
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
function compareValues(left, right) {
|
|
1144
|
+
if (typeof left === "number" && typeof right === "number") return left - right;
|
|
1145
|
+
return String(left ?? "").localeCompare(String(right ?? ""));
|
|
1146
|
+
}
|
|
1147
|
+
function formulaName(id) {
|
|
1148
|
+
return id.startsWith("formula.") ? id.slice(8) : id;
|
|
1149
|
+
}
|
|
1150
|
+
function normalizeComparable(value) {
|
|
1151
|
+
return String(value).replace(/^\[\[/u, "").replace(/\]\]$/u, "").replace(/\.md$/u, "").toLowerCase();
|
|
1152
|
+
}
|
|
1153
|
+
function isRecord(value) {
|
|
1154
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1155
|
+
}
|
|
1156
|
+
//#endregion
|
|
1157
|
+
export { BaseEngineError, evaluateExpression, queryBase };
|
|
1158
|
+
|
|
1159
|
+
//# sourceMappingURL=index.mjs.map
|