collabmd 0.1.31 → 0.1.33

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.
Files changed (99) hide show
  1. package/dist/client/assets/{drawio-editor-B4sHHwWJ.js → drawio-editor-BMTwP5mD.js} +1 -1
  2. package/dist/client/assets/drawioEditor-D6clB5OH.js +2 -0
  3. package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Dzy_85Wp.js} +2 -2
  4. package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
  5. package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CAP9F8CB.js} +3 -3
  6. package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DD4AvzYP.js} +2 -2
  7. package/dist/client/assets/exportDocument-BbkN85c4.css +1 -0
  8. package/dist/client/assets/index-BIt5EZXz.css +1 -0
  9. package/dist/client/assets/{index-kPgEYvEX.js → index-Czg_KL-i.js} +2 -2
  10. package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  11. package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  12. package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  13. package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  14. package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  15. package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  16. package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  17. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  18. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  19. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  20. package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  21. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  22. package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  23. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  24. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  25. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  26. package/dist/client/assets/main-DeowJOSp.js +1368 -0
  27. package/dist/client/assets/vault-api-client-D7Ck0CvR.css +1 -0
  28. package/dist/client/assets/vault-api-client-p7h7cXKM.js +1 -0
  29. package/dist/client/drawio-editor.html +1 -1
  30. package/dist/client/excalidraw-editor.html +1 -1
  31. package/dist/client/export-document.html +2 -2
  32. package/dist/client/index.html +2 -2
  33. package/package.json +8 -5
  34. package/src/client/app/main-entry.js +2 -0
  35. package/src/client/application/app-shell/git-feature.js +28 -36
  36. package/src/client/application/app-shell/presence-feature.js +3 -1
  37. package/src/client/application/diagram-preview-hydrator.js +24 -15
  38. package/src/client/application/mermaid-preview-hydrator.js +121 -4
  39. package/src/client/application/plantuml-preview-hydrator.js +6 -16
  40. package/src/client/application/preview-renderer.js +11 -2
  41. package/src/client/bootstrap/collabmd-app-shell.js +182 -50
  42. package/src/client/domain/excalidraw-document-switch.js +3 -0
  43. package/src/client/excalidraw-editor.js +187 -84
  44. package/src/client/infrastructure/auth-client.js +4 -4
  45. package/src/client/infrastructure/backlinks-api-client.js +18 -0
  46. package/src/client/infrastructure/comment-thread-store.js +4 -0
  47. package/src/client/infrastructure/editor-session.js +6 -0
  48. package/src/client/infrastructure/editor-view-adapter.js +23 -0
  49. package/src/client/infrastructure/git-api-client.js +134 -0
  50. package/src/client/infrastructure/plantuml-api-client.js +21 -0
  51. package/src/client/infrastructure/vault-api-client.js +115 -0
  52. package/src/client/presentation/backlinks-panel.js +6 -9
  53. package/src/client/presentation/bases-preview-controller.js +1413 -34
  54. package/src/client/presentation/comments-panel.js +50 -5
  55. package/src/client/presentation/excalidraw-embed-controller.js +251 -15
  56. package/src/client/presentation/excalidraw-embed-reconciler.js +0 -1
  57. package/src/client/presentation/file-action-controller.js +97 -0
  58. package/src/client/presentation/file-explorer-controller.js +2 -0
  59. package/src/client/presentation/file-explorer-view.js +368 -0
  60. package/src/client/presentation/file-history-view-controller.js +41 -40
  61. package/src/client/presentation/git-diff-view-controller.js +70 -53
  62. package/src/client/presentation/git-panel-controller.js +12 -30
  63. package/src/client/styles/components/scrollbars.css +5 -0
  64. package/src/client/styles/export-document.css +45 -2
  65. package/src/client/styles/features/auth-gate.css +13 -24
  66. package/src/client/styles/features/comment-card.css +6 -0
  67. package/src/client/styles/features/comments-drawer.css +38 -18
  68. package/src/client/styles/features/diagram-preview.css +33 -0
  69. package/src/client/styles/features/embedded-preview.css +37 -11
  70. package/src/client/styles/features/preview-markdown.css +559 -39
  71. package/src/client/styles/foundation/themes.css +4 -0
  72. package/src/client/styles/foundation/tokens.css +4 -3
  73. package/src/client/styles/layout/sidebar.css +72 -0
  74. package/src/server/auth/auth-common.js +224 -0
  75. package/src/server/auth/auth-constants.js +30 -0
  76. package/src/server/auth/auth-strategies.js +320 -0
  77. package/src/server/auth/create-auth-service.js +24 -545
  78. package/src/server/domain/backlink-index.js +177 -5
  79. package/src/server/domain/bases/base-definition.js +382 -0
  80. package/src/server/domain/bases/base-expression-runtime.js +1196 -0
  81. package/src/server/domain/bases/base-index-snapshot-store.js +565 -0
  82. package/src/server/domain/bases/base-query-metadata.js +242 -0
  83. package/src/server/domain/bases/base-query-results.js +215 -0
  84. package/src/server/domain/bases/base-query-service.js +406 -1992
  85. package/src/server/domain/bases/base-transform.js +116 -0
  86. package/src/server/domain/collaboration/collaboration-room.js +36 -1
  87. package/src/server/domain/collaboration/room-client-state-store.js +1 -0
  88. package/src/server/domain/collaboration/room-persistence-controller.js +1 -1
  89. package/src/server/infrastructure/http/create-vault-api-query-handler.js +161 -1
  90. package/src/server/infrastructure/http/http-response.js +56 -0
  91. package/src/server/infrastructure/persistence/vault-file-store.js +110 -0
  92. package/dist/client/assets/drawioEditor-DGpo2hcx.js +0 -2
  93. package/dist/client/assets/embedded-editor-base-DgmRcsR8.css +0 -1
  94. package/dist/client/assets/exportDocument-DmV7Gngb.css +0 -1
  95. package/dist/client/assets/index-D69HOZHr.css +0 -1
  96. package/dist/client/assets/main-DSxNTtD-.js +0 -1243
  97. package/dist/client/assets/vault-api-client-BzHQseMN.js +0 -1
  98. package/dist/client/assets/vault-api-client-De7kMvrE.css +0 -1
  99. /package/dist/client/assets/{exportDocument-CtZKWE2c.js → exportDocument-Zfwq1XVx.js} +0 -0
@@ -0,0 +1,1196 @@
1
+ import { isImageAttachmentFilePath } from '../../../domain/file-kind.js';
2
+
3
+ function isPlainObject(value) {
4
+ return Object.prototype.toString.call(value) === '[object Object]';
5
+ }
6
+
7
+ function normalizeVaultPathSegments(pathValue = '') {
8
+ return String(pathValue ?? '')
9
+ .replace(/\\/g, '/')
10
+ .split('/')
11
+ .filter(Boolean);
12
+ }
13
+
14
+ function resolveVaultRelativePath(fromFilePath, relativePath) {
15
+ const sourceSegments = normalizeVaultPathSegments(fromFilePath);
16
+ const targetSegments = String(relativePath ?? '')
17
+ .split('/')
18
+ .map((segment) => {
19
+ try {
20
+ return decodeURIComponent(segment);
21
+ } catch {
22
+ return segment;
23
+ }
24
+ });
25
+
26
+ if (targetSegments.length === 0) {
27
+ return '';
28
+ }
29
+
30
+ sourceSegments.pop();
31
+ const resolvedSegments = [...sourceSegments];
32
+
33
+ for (const rawSegment of targetSegments) {
34
+ const segment = String(rawSegment ?? '').trim();
35
+ if (!segment || segment === '.') {
36
+ continue;
37
+ }
38
+
39
+ if (segment === '..') {
40
+ if (resolvedSegments.length === 0) {
41
+ return '';
42
+ }
43
+ resolvedSegments.pop();
44
+ continue;
45
+ }
46
+
47
+ resolvedSegments.push(segment);
48
+ }
49
+
50
+ return resolvedSegments.join('/');
51
+ }
52
+
53
+ export function createLinkValue(target, {
54
+ display = '',
55
+ exists = false,
56
+ external = false,
57
+ rawTarget = '',
58
+ } = {}) {
59
+ return {
60
+ __baseType: 'link',
61
+ display: display ? String(display) : '',
62
+ exists: Boolean(exists),
63
+ external: Boolean(external),
64
+ path: external ? '' : String(target ?? ''),
65
+ rawTarget: String(rawTarget || target || ''),
66
+ url: external ? String(target ?? '') : '',
67
+ };
68
+ }
69
+
70
+ export function dedupeLinkValues(values = []) {
71
+ const seen = new Set();
72
+ return values.filter((value) => {
73
+ const key = value?.external
74
+ ? `external:${value.url || value.rawTarget || value.display || ''}`
75
+ : `path:${value.path || value.rawTarget || value.display || ''}`;
76
+ if (!key || seen.has(key)) {
77
+ return false;
78
+ }
79
+ seen.add(key);
80
+ return true;
81
+ });
82
+ }
83
+
84
+ function createFormulaNamespace(row, evaluationState) {
85
+ return {
86
+ __baseType: 'formula-namespace',
87
+ evaluationState,
88
+ row,
89
+ };
90
+ }
91
+
92
+ function createContextFileValue(fileValue) {
93
+ return fileValue ?? null;
94
+ }
95
+
96
+ function normalizeDuration(duration) {
97
+ const match = String(duration ?? '').trim().match(/^(-?\d+)\s*([smhdw])$/iu);
98
+ if (!match) {
99
+ return null;
100
+ }
101
+
102
+ const amount = Number(match[1]);
103
+ const unit = match[2].toLowerCase();
104
+ const multiplier = {
105
+ d: 86_400_000,
106
+ h: 3_600_000,
107
+ m: 60_000,
108
+ s: 1_000,
109
+ w: 604_800_000,
110
+ }[unit];
111
+ return Number.isFinite(amount) && multiplier ? amount * multiplier : null;
112
+ }
113
+
114
+ function hasFormulaDefinition(definition, propertyId = '') {
115
+ return Boolean(definition?.formulas?.[propertyId]);
116
+ }
117
+
118
+ export function parseDateValue(value) {
119
+ if (value instanceof Date) {
120
+ return Number.isFinite(value.getTime()) ? value : null;
121
+ }
122
+ if (typeof value === 'number') {
123
+ const date = new Date(value);
124
+ return Number.isFinite(date.getTime()) ? date : null;
125
+ }
126
+ if (typeof value !== 'string') {
127
+ return null;
128
+ }
129
+
130
+ const normalized = value.trim();
131
+ if (!normalized) {
132
+ return null;
133
+ }
134
+
135
+ if (/^\d{8}$/u.test(normalized)) {
136
+ return new Date(`${normalized.slice(0, 4)}-${normalized.slice(4, 6)}-${normalized.slice(6, 8)}T00:00:00`);
137
+ }
138
+
139
+ if (/^\d{8}\d{4}$/u.test(normalized)) {
140
+ return new Date(`${normalized.slice(0, 4)}-${normalized.slice(4, 6)}-${normalized.slice(6, 8)}T${normalized.slice(8, 10)}:${normalized.slice(10, 12)}:00`);
141
+ }
142
+
143
+ const date = new Date(normalized);
144
+ return Number.isFinite(date.getTime()) ? date : null;
145
+ }
146
+
147
+ function isDateLikeString(value) {
148
+ if (typeof value !== 'string') {
149
+ return false;
150
+ }
151
+
152
+ const normalized = value.trim();
153
+ if (!normalized) {
154
+ return false;
155
+ }
156
+
157
+ return (
158
+ /^\d{8}(?:\d{4})?$/u.test(normalized)
159
+ || /^\d{4}-\d{2}-\d{2}(?:[T\s]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})?)?$/u.test(normalized)
160
+ );
161
+ }
162
+
163
+ function coerceDateComparable(value) {
164
+ if (!isDateLikeString(value)) {
165
+ return value;
166
+ }
167
+
168
+ return parseDateValue(value) ?? value;
169
+ }
170
+
171
+ function normalizeComparablePair(left, right) {
172
+ if (left instanceof Date || right instanceof Date) {
173
+ return [
174
+ left instanceof Date ? left : coerceDateComparable(left),
175
+ right instanceof Date ? right : coerceDateComparable(right),
176
+ ];
177
+ }
178
+
179
+ if (typeof left === 'string' && typeof right === 'string' && isDateLikeString(left) && isDateLikeString(right)) {
180
+ return [
181
+ parseDateValue(left) ?? left,
182
+ parseDateValue(right) ?? right,
183
+ ];
184
+ }
185
+
186
+ return [left, right];
187
+ }
188
+
189
+ function formatDateValue(value, format = 'YYYY-MM-DD') {
190
+ const date = parseDateValue(value);
191
+ if (!date) {
192
+ return '';
193
+ }
194
+
195
+ const padded = (number) => String(number).padStart(2, '0');
196
+ return String(format ?? 'YYYY-MM-DD')
197
+ .replace(/YYYY/g, String(date.getFullYear()))
198
+ .replace(/MM/g, padded(date.getMonth() + 1))
199
+ .replace(/DD/g, padded(date.getDate()))
200
+ .replace(/HH/g, padded(date.getHours()))
201
+ .replace(/mm/g, padded(date.getMinutes()))
202
+ .replace(/ss/g, padded(date.getSeconds()));
203
+ }
204
+
205
+ function toComparableValue(value) {
206
+ if (value instanceof Date) {
207
+ return value.getTime();
208
+ }
209
+ return value;
210
+ }
211
+
212
+ export function toDisplayText(value) {
213
+ if (value == null) {
214
+ return '';
215
+ }
216
+ if (value instanceof Date) {
217
+ return formatDateValue(value, 'YYYY-MM-DD HH:mm:ss');
218
+ }
219
+ if (Array.isArray(value)) {
220
+ return value.map((entry) => toDisplayText(entry)).join(', ');
221
+ }
222
+ if (typeof value === 'boolean') {
223
+ return value ? 'true' : 'false';
224
+ }
225
+ if (typeof value === 'number') {
226
+ return Number.isFinite(value) ? String(value) : '';
227
+ }
228
+ if (value?.__baseType === 'file') {
229
+ return value.path || value.name || '';
230
+ }
231
+ if (value?.__baseType === 'link') {
232
+ return value.display || value.path || value.url || '';
233
+ }
234
+ if (isPlainObject(value)) {
235
+ try {
236
+ return JSON.stringify(value);
237
+ } catch {
238
+ return '[object Object]';
239
+ }
240
+ }
241
+ return String(value);
242
+ }
243
+
244
+ export function valuesEqual(left, right) {
245
+ const [normalizedLeft, normalizedRight] = normalizeComparablePair(left, right);
246
+
247
+ left = normalizedLeft;
248
+ right = normalizedRight;
249
+
250
+ if (left?.__baseType === 'file' && right?.__baseType === 'file') {
251
+ return left.path === right.path;
252
+ }
253
+ if (left?.__baseType === 'link' && right?.__baseType === 'link') {
254
+ return left.url === right.url && left.path === right.path;
255
+ }
256
+ if (left instanceof Date || right instanceof Date) {
257
+ return toComparableValue(left) === toComparableValue(right);
258
+ }
259
+ return left === right;
260
+ }
261
+
262
+ function resolveImageAttachmentPath(value, {
263
+ rowFilePath = '',
264
+ snapshot = null,
265
+ sourcePath = '',
266
+ } = {}) {
267
+ const text = String(value ?? '').trim();
268
+ const rowsByPath = snapshot?.rowsByPath;
269
+ if (!text || !rowsByPath) {
270
+ return null;
271
+ }
272
+
273
+ const candidates = [];
274
+ const seen = new Set();
275
+ const appendCandidate = (candidate) => {
276
+ const normalized = String(candidate ?? '').trim().replace(/\\/g, '/');
277
+ if (!normalized || seen.has(normalized)) {
278
+ return;
279
+ }
280
+ seen.add(normalized);
281
+ candidates.push(normalized);
282
+ };
283
+
284
+ if (rowFilePath) {
285
+ appendCandidate(resolveVaultRelativePath(rowFilePath, text));
286
+ }
287
+ if (sourcePath) {
288
+ appendCandidate(resolveVaultRelativePath(sourcePath, text));
289
+ }
290
+ appendCandidate(text);
291
+
292
+ return candidates.find((candidate) => (
293
+ isImageAttachmentFilePath(candidate)
294
+ && rowsByPath.has(candidate)
295
+ )) ?? null;
296
+ }
297
+
298
+ export function serializeBaseValue(value, options = {}) {
299
+ if (value == null || value === '') {
300
+ return { text: '', type: 'empty', value: null };
301
+ }
302
+ if (value instanceof Date) {
303
+ return {
304
+ iso: value.toISOString(),
305
+ text: formatDateValue(value, 'YYYY-MM-DD HH:mm:ss'),
306
+ type: 'date',
307
+ value: value.toISOString(),
308
+ };
309
+ }
310
+ if (Array.isArray(value)) {
311
+ return {
312
+ items: value.map((entry) => serializeBaseValue(entry, options)),
313
+ text: toDisplayText(value),
314
+ type: 'list',
315
+ value: value.map((entry) => toDisplayText(entry)),
316
+ };
317
+ }
318
+ if (value?.__baseType === 'file') {
319
+ return {
320
+ path: value.path,
321
+ text: value.path,
322
+ type: 'file',
323
+ value: value.path,
324
+ };
325
+ }
326
+ if (value?.__baseType === 'link') {
327
+ return {
328
+ external: value.external,
329
+ path: value.path,
330
+ text: value.display || value.path || value.url,
331
+ type: 'link',
332
+ url: value.url,
333
+ value: value.display || value.path || value.url,
334
+ };
335
+ }
336
+ if (typeof value === 'boolean') {
337
+ return { text: value ? 'true' : 'false', type: 'boolean', value };
338
+ }
339
+ if (typeof value === 'number') {
340
+ return { text: Number.isFinite(value) ? String(value) : '', type: 'number', value };
341
+ }
342
+ if (typeof value === 'string') {
343
+ const imagePath = resolveImageAttachmentPath(value, options);
344
+ if (imagePath) {
345
+ return {
346
+ path: imagePath,
347
+ text: value,
348
+ type: 'image',
349
+ value: imagePath,
350
+ };
351
+ }
352
+ return { text: value, type: 'string', value };
353
+ }
354
+ return {
355
+ text: toDisplayText(value),
356
+ type: 'object',
357
+ value,
358
+ };
359
+ }
360
+
361
+ export function compareValues(left, right) {
362
+ const [normalizedLeft, normalizedRight] = normalizeComparablePair(left, right);
363
+ left = normalizedLeft;
364
+ right = normalizedRight;
365
+
366
+ const comparableLeft = toComparableValue(left);
367
+ const comparableRight = toComparableValue(right);
368
+ if (comparableLeft == null && comparableRight == null) return 0;
369
+ if (comparableLeft == null) return 1;
370
+ if (comparableRight == null) return -1;
371
+ if (typeof comparableLeft === 'number' && typeof comparableRight === 'number') {
372
+ return comparableLeft - comparableRight;
373
+ }
374
+ return String(toDisplayText(comparableLeft)).localeCompare(String(toDisplayText(comparableRight)), undefined, { sensitivity: 'base' });
375
+ }
376
+
377
+ function applyBinaryOperator(operator, left, right) {
378
+ if (operator === '&&') {
379
+ return Boolean(left) && Boolean(right);
380
+ }
381
+ if (operator === '||') {
382
+ return Boolean(left) || Boolean(right);
383
+ }
384
+ if (operator === '==') {
385
+ return valuesEqual(left, right);
386
+ }
387
+ if (operator === '!=') {
388
+ return !valuesEqual(left, right);
389
+ }
390
+ if (operator === '<') {
391
+ return compareValues(left, right) < 0;
392
+ }
393
+ if (operator === '<=') {
394
+ return compareValues(left, right) <= 0;
395
+ }
396
+ if (operator === '>') {
397
+ return compareValues(left, right) > 0;
398
+ }
399
+ if (operator === '>=') {
400
+ return compareValues(left, right) >= 0;
401
+ }
402
+ if (operator === '+') {
403
+ if (left instanceof Date && typeof right === 'string') {
404
+ const duration = normalizeDuration(right);
405
+ return duration == null ? left : new Date(left.getTime() + duration);
406
+ }
407
+ if (typeof left === 'string' || typeof right === 'string') {
408
+ return `${toDisplayText(left)}${toDisplayText(right)}`;
409
+ }
410
+ return Number(left ?? 0) + Number(right ?? 0);
411
+ }
412
+ if (operator === '-') {
413
+ if (left instanceof Date && typeof right === 'string') {
414
+ const duration = normalizeDuration(right);
415
+ return duration == null ? left : new Date(left.getTime() - duration);
416
+ }
417
+ if (left instanceof Date && right instanceof Date) {
418
+ return left.getTime() - right.getTime();
419
+ }
420
+ return Number(left ?? 0) - Number(right ?? 0);
421
+ }
422
+ if (operator === '*') {
423
+ return Number(left ?? 0) * Number(right ?? 0);
424
+ }
425
+ if (operator === '/') {
426
+ return Number(right ?? 0) === 0 ? null : Number(left ?? 0) / Number(right ?? 0);
427
+ }
428
+ if (operator === '%') {
429
+ return Number(right ?? 0) === 0 ? null : Number(left ?? 0) % Number(right ?? 0);
430
+ }
431
+
432
+ throw new Error(`Unsupported operator: ${operator}`);
433
+ }
434
+
435
+ function invokeGlobalFunction(name, argNodes, scope, evaluate) {
436
+ if (name === 'if') {
437
+ const condition = evaluate(argNodes[0], scope);
438
+ return condition ? evaluate(argNodes[1], scope) : evaluate(argNodes[2], scope);
439
+ }
440
+
441
+ const args = argNodes.map((node) => evaluate(node, scope));
442
+ switch (name) {
443
+ case 'date':
444
+ return parseDateValue(args[0]);
445
+ case 'link': {
446
+ const target = String(args[0] ?? '').trim();
447
+ if (!target) {
448
+ return null;
449
+ }
450
+ const external = /^[a-z]+:/iu.test(target);
451
+ return createLinkValue(target, {
452
+ display: typeof args[1] === 'string' ? args[1] : '',
453
+ exists: external,
454
+ external,
455
+ });
456
+ }
457
+ case 'list':
458
+ return args;
459
+ case 'max':
460
+ return args.flat().reduce((acc, value) => (acc == null || compareValues(value, acc) > 0 ? value : acc), null);
461
+ case 'min':
462
+ return args.flat().reduce((acc, value) => (acc == null || compareValues(value, acc) < 0 ? value : acc), null);
463
+ case 'now':
464
+ return new Date();
465
+ case 'today': {
466
+ const date = new Date();
467
+ date.setHours(0, 0, 0, 0);
468
+ return date;
469
+ }
470
+ default:
471
+ throw new Error(`Unknown function: ${name}`);
472
+ }
473
+ }
474
+
475
+ function invokeMethod(target, name, argNodes, scope, evaluate, rootContext) {
476
+ if (target?.__baseType === 'formula-namespace') {
477
+ if (argNodes.length > 0) {
478
+ throw new Error(`Formula namespace property "${name}" is not callable`);
479
+ }
480
+ return rootContext.resolveFormulaValue(name, target.row, target.evaluationState);
481
+ }
482
+
483
+ if (target == null) {
484
+ switch (name) {
485
+ case 'contains':
486
+ case 'containsAll':
487
+ case 'containsAny':
488
+ case 'endsWith':
489
+ case 'hasLink':
490
+ case 'hasProperty':
491
+ case 'hasTag':
492
+ case 'inFolder':
493
+ case 'linksTo':
494
+ case 'startsWith':
495
+ return false;
496
+ case 'isEmpty':
497
+ return true;
498
+ default:
499
+ break;
500
+ }
501
+ }
502
+
503
+ if (Array.isArray(target)) {
504
+ if (name === 'contains') {
505
+ const needle = evaluate(argNodes[0], scope);
506
+ return target.some((entry) => valuesEqual(entry, needle));
507
+ }
508
+ if (name === 'containsAll') {
509
+ return argNodes.every((node) => target.some((entry) => valuesEqual(entry, evaluate(node, scope))));
510
+ }
511
+ if (name === 'containsAny') {
512
+ return argNodes.some((node) => target.some((entry) => valuesEqual(entry, evaluate(node, scope))));
513
+ }
514
+ if (name === 'filter') {
515
+ const predicate = argNodes[0];
516
+ return target.filter((value, index) => Boolean(evaluate(predicate, scope.child({ index, value }))));
517
+ }
518
+ if (name === 'flat') {
519
+ return target.flat();
520
+ }
521
+ if (name === 'isEmpty') {
522
+ return target.length === 0;
523
+ }
524
+ if (name === 'join') {
525
+ return target.map((entry) => toDisplayText(entry)).join(String(evaluate(argNodes[0], scope) ?? ''));
526
+ }
527
+ if (name === 'map') {
528
+ const mapper = argNodes[0];
529
+ return target.map((value, index) => evaluate(mapper, scope.child({ index, value })));
530
+ }
531
+ if (name === 'reduce') {
532
+ const reducer = argNodes[0];
533
+ let accumulator = argNodes.length > 1 ? evaluate(argNodes[1], scope) : null;
534
+ target.forEach((value, index) => {
535
+ accumulator = evaluate(reducer, scope.child({ acc: accumulator, index, value }));
536
+ });
537
+ return accumulator;
538
+ }
539
+ if (name === 'reverse') {
540
+ return [...target].reverse();
541
+ }
542
+ if (name === 'slice') {
543
+ return target.slice(Number(evaluate(argNodes[0], scope) ?? 0), argNodes[1] ? Number(evaluate(argNodes[1], scope)) : undefined);
544
+ }
545
+ if (name === 'sort') {
546
+ return [...target].sort(compareValues);
547
+ }
548
+ if (name === 'unique') {
549
+ const uniqueValues = [];
550
+ target.forEach((value) => {
551
+ if (!uniqueValues.some((entry) => valuesEqual(entry, value))) {
552
+ uniqueValues.push(value);
553
+ }
554
+ });
555
+ return uniqueValues;
556
+ }
557
+ }
558
+
559
+ if (typeof target === 'string') {
560
+ switch (name) {
561
+ case 'contains':
562
+ return target.toLowerCase().includes(String(evaluate(argNodes[0], scope) ?? '').toLowerCase());
563
+ case 'endsWith':
564
+ return target.endsWith(String(evaluate(argNodes[0], scope) ?? ''));
565
+ case 'isEmpty':
566
+ return target.length === 0;
567
+ case 'lower':
568
+ return target.toLowerCase();
569
+ case 'replace':
570
+ return target.replaceAll(String(evaluate(argNodes[0], scope) ?? ''), String(evaluate(argNodes[1], scope) ?? ''));
571
+ case 'split':
572
+ return target.split(String(evaluate(argNodes[0], scope) ?? ''));
573
+ case 'startsWith':
574
+ return target.startsWith(String(evaluate(argNodes[0], scope) ?? ''));
575
+ case 'title':
576
+ return target.replace(/\b\p{L}/gu, (match) => match.toUpperCase());
577
+ case 'trim':
578
+ return target.trim();
579
+ case 'upper':
580
+ return target.toUpperCase();
581
+ default:
582
+ break;
583
+ }
584
+ }
585
+
586
+ if (typeof target === 'number') {
587
+ switch (name) {
588
+ case 'ceil':
589
+ return Math.ceil(target);
590
+ case 'floor':
591
+ return Math.floor(target);
592
+ case 'isEmpty':
593
+ return false;
594
+ case 'round':
595
+ return Number.isFinite(evaluate(argNodes[0], scope))
596
+ ? Number(target.toFixed(Number(evaluate(argNodes[0], scope))))
597
+ : Math.round(target);
598
+ case 'toFixed':
599
+ return target.toFixed(Number(evaluate(argNodes[0], scope) ?? 0));
600
+ default:
601
+ break;
602
+ }
603
+ }
604
+
605
+ const asDate = parseDateValue(target);
606
+ if (asDate) {
607
+ switch (name) {
608
+ case 'format':
609
+ return formatDateValue(asDate, String(evaluate(argNodes[0], scope) ?? 'YYYY-MM-DD'));
610
+ case 'isEmpty':
611
+ return false;
612
+ default:
613
+ break;
614
+ }
615
+ }
616
+
617
+ if (target?.__baseType === 'file') {
618
+ switch (name) {
619
+ case 'asLink':
620
+ return createLinkValue(target.path, {
621
+ display: typeof evaluate(argNodes[0], scope) === 'string' ? evaluate(argNodes[0], scope) : '',
622
+ exists: true,
623
+ });
624
+ case 'hasLink': {
625
+ const needle = evaluate(argNodes[0], scope);
626
+ const needlePath = needle?.__baseType === 'file'
627
+ ? needle.path
628
+ : (needle?.__baseType === 'link' ? needle.path : String(needle ?? ''));
629
+ return target.links.some((link) => link.path === needlePath || link.rawTarget === needlePath);
630
+ }
631
+ case 'hasProperty':
632
+ return Object.hasOwn(target.properties, String(evaluate(argNodes[0], scope) ?? ''));
633
+ case 'hasTag':
634
+ return argNodes.some((node) => {
635
+ const value = String(evaluate(node, scope) ?? '').replace(/^#/u, '');
636
+ return target.tags.some((tag) => tag === value || tag.startsWith(`${value}/`));
637
+ });
638
+ case 'inFolder': {
639
+ const folder = String(evaluate(argNodes[0], scope) ?? '').replace(/\/+$/u, '');
640
+ return !folder || target.folder === folder || target.folder.startsWith(`${folder}/`);
641
+ }
642
+ default:
643
+ break;
644
+ }
645
+ }
646
+
647
+ if (target?.__baseType === 'link') {
648
+ switch (name) {
649
+ case 'asFile':
650
+ return target.path ? rootContext.snapshot.rowsByPath.get(target.path)?.file ?? null : null;
651
+ case 'linksTo': {
652
+ const other = evaluate(argNodes[0], scope);
653
+ const otherPath = other?.__baseType === 'file'
654
+ ? other.path
655
+ : (other?.__baseType === 'link' ? other.path : String(other ?? ''));
656
+ return target.path === otherPath;
657
+ }
658
+ default:
659
+ break;
660
+ }
661
+ }
662
+
663
+ if (isPlainObject(target)) {
664
+ switch (name) {
665
+ case 'isEmpty':
666
+ return Object.keys(target).length === 0;
667
+ case 'keys':
668
+ return Object.keys(target);
669
+ case 'values':
670
+ return Object.values(target);
671
+ default:
672
+ break;
673
+ }
674
+ }
675
+
676
+ throw new Error(`Unsupported method "${name}"`);
677
+ }
678
+
679
+ function tokenizeExpression(source = '') {
680
+ const tokens = [];
681
+ const text = String(source ?? '');
682
+ let index = 0;
683
+
684
+ while (index < text.length) {
685
+ const character = text[index];
686
+ if (/\s/u.test(character)) {
687
+ index += 1;
688
+ continue;
689
+ }
690
+
691
+ const twoChar = text.slice(index, index + 2);
692
+ const threeChar = text.slice(index, index + 3);
693
+ if (['&&', '||', '==', '!=', '<=', '>='].includes(twoChar)) {
694
+ tokens.push({ type: 'operator', value: twoChar });
695
+ index += 2;
696
+ continue;
697
+ }
698
+ if (threeChar === '...') {
699
+ tokens.push({ type: 'operator', value: threeChar });
700
+ index += 3;
701
+ continue;
702
+ }
703
+ if ('+-*/%!<>()[]{}.,:'.includes(character)) {
704
+ tokens.push({
705
+ type: '()[]{}.,:'.includes(character) ? 'punctuation' : 'operator',
706
+ value: character,
707
+ });
708
+ index += 1;
709
+ continue;
710
+ }
711
+ if (character === '"' || character === '\'') {
712
+ let value = '';
713
+ let cursor = index + 1;
714
+ while (cursor < text.length) {
715
+ const nextCharacter = text[cursor];
716
+ if (nextCharacter === '\\') {
717
+ const escaped = text[cursor + 1] ?? '';
718
+ value += escaped;
719
+ cursor += 2;
720
+ continue;
721
+ }
722
+ if (nextCharacter === character) {
723
+ break;
724
+ }
725
+ value += nextCharacter;
726
+ cursor += 1;
727
+ }
728
+ if (text[cursor] !== character) {
729
+ throw new Error(`Unterminated string in expression: ${source}`);
730
+ }
731
+ tokens.push({ type: 'string', value });
732
+ index = cursor + 1;
733
+ continue;
734
+ }
735
+ if (/\d/u.test(character)) {
736
+ let cursor = index + 1;
737
+ while (cursor < text.length && /[\d.]/u.test(text[cursor])) {
738
+ cursor += 1;
739
+ }
740
+ tokens.push({ type: 'number', value: text.slice(index, cursor) });
741
+ index = cursor;
742
+ continue;
743
+ }
744
+ if (/[A-Za-z_$]/u.test(character)) {
745
+ let cursor = index + 1;
746
+ while (cursor < text.length && /[A-Za-z0-9_$]/u.test(text[cursor])) {
747
+ cursor += 1;
748
+ }
749
+ tokens.push({ type: 'identifier', value: text.slice(index, cursor) });
750
+ index = cursor;
751
+ continue;
752
+ }
753
+
754
+ throw new Error(`Unexpected token "${character}" in expression: ${source}`);
755
+ }
756
+
757
+ return tokens;
758
+ }
759
+
760
+ const BINARY_PRECEDENCE = {
761
+ '||': 1,
762
+ '&&': 2,
763
+ '==': 3,
764
+ '!=': 3,
765
+ '<': 4,
766
+ '<=': 4,
767
+ '>': 4,
768
+ '>=': 4,
769
+ '+': 5,
770
+ '-': 5,
771
+ '*': 6,
772
+ '/': 6,
773
+ '%': 6,
774
+ };
775
+
776
+ function parseExpression(source = '') {
777
+ const tokens = tokenizeExpression(source);
778
+ let index = 0;
779
+
780
+ const peek = () => tokens[index] ?? null;
781
+ const consume = (expectedValue = null) => {
782
+ const token = tokens[index] ?? null;
783
+ if (!token) {
784
+ throw new Error(`Unexpected end of expression: ${source}`);
785
+ }
786
+ if (expectedValue !== null && token.value !== expectedValue) {
787
+ throw new Error(`Expected "${expectedValue}" in expression: ${source}`);
788
+ }
789
+ index += 1;
790
+ return token;
791
+ };
792
+
793
+ const parsePrimary = () => {
794
+ const token = peek();
795
+ if (!token) {
796
+ throw new Error(`Unexpected end of expression: ${source}`);
797
+ }
798
+
799
+ if (token.type === 'number') {
800
+ consume();
801
+ return { type: 'Literal', value: Number(token.value) };
802
+ }
803
+
804
+ if (token.type === 'string') {
805
+ consume();
806
+ return { type: 'Literal', value: token.value };
807
+ }
808
+
809
+ if (token.type === 'identifier') {
810
+ consume();
811
+ if (token.value === 'true') return { type: 'Literal', value: true };
812
+ if (token.value === 'false') return { type: 'Literal', value: false };
813
+ if (token.value === 'null') return { type: 'Literal', value: null };
814
+ return { name: token.value, type: 'Identifier' };
815
+ }
816
+
817
+ if (token.value === '(') {
818
+ consume('(');
819
+ const expression = parseBinaryExpression(0);
820
+ consume(')');
821
+ return expression;
822
+ }
823
+
824
+ if (token.value === '[') {
825
+ consume('[');
826
+ const elements = [];
827
+ while (peek() && peek().value !== ']') {
828
+ elements.push(parseBinaryExpression(0));
829
+ if (peek()?.value === ',') {
830
+ consume(',');
831
+ }
832
+ }
833
+ consume(']');
834
+ return { elements, type: 'ArrayExpression' };
835
+ }
836
+
837
+ if (token.value === '{') {
838
+ consume('{');
839
+ const properties = [];
840
+ while (peek() && peek().value !== '}') {
841
+ const keyToken = consume();
842
+ const key = keyToken.type === 'identifier' ? keyToken.value : String(keyToken.value ?? '');
843
+ consume(':');
844
+ properties.push({ key, value: parseBinaryExpression(0) });
845
+ if (peek()?.value === ',') {
846
+ consume(',');
847
+ }
848
+ }
849
+ consume('}');
850
+ return { properties, type: 'ObjectExpression' };
851
+ }
852
+
853
+ throw new Error(`Unexpected token "${token.value}" in expression: ${source}`);
854
+ };
855
+
856
+ const parsePostfix = (baseNode) => {
857
+ let node = baseNode;
858
+ while (peek()) {
859
+ if (peek().value === '.') {
860
+ consume('.');
861
+ const property = consume();
862
+ if (property.type !== 'identifier') {
863
+ throw new Error(`Expected property name in expression: ${source}`);
864
+ }
865
+ node = {
866
+ computed: false,
867
+ object: node,
868
+ property: { name: property.value, type: 'Identifier' },
869
+ type: 'MemberExpression',
870
+ };
871
+ continue;
872
+ }
873
+
874
+ if (peek().value === '[') {
875
+ consume('[');
876
+ const property = parseBinaryExpression(0);
877
+ consume(']');
878
+ node = {
879
+ computed: true,
880
+ object: node,
881
+ property,
882
+ type: 'MemberExpression',
883
+ };
884
+ continue;
885
+ }
886
+
887
+ if (peek().value === '(') {
888
+ consume('(');
889
+ const args = [];
890
+ while (peek() && peek().value !== ')') {
891
+ args.push(parseBinaryExpression(0));
892
+ if (peek()?.value === ',') {
893
+ consume(',');
894
+ }
895
+ }
896
+ consume(')');
897
+ node = {
898
+ arguments: args,
899
+ callee: node,
900
+ type: 'CallExpression',
901
+ };
902
+ continue;
903
+ }
904
+
905
+ break;
906
+ }
907
+ return node;
908
+ };
909
+
910
+ const parseUnary = () => {
911
+ const token = peek();
912
+ if (token?.type === 'operator' && ['!', '-'].includes(token.value)) {
913
+ consume();
914
+ return {
915
+ argument: parseUnary(),
916
+ operator: token.value,
917
+ type: 'UnaryExpression',
918
+ };
919
+ }
920
+
921
+ return parsePostfix(parsePrimary());
922
+ };
923
+
924
+ const parseBinaryExpression = (minimumPrecedence) => {
925
+ let left = parseUnary();
926
+ while (true) {
927
+ const operator = peek();
928
+ const precedence = operator ? BINARY_PRECEDENCE[operator.value] : null;
929
+ if (!precedence || precedence < minimumPrecedence) {
930
+ break;
931
+ }
932
+
933
+ consume();
934
+ const right = parseBinaryExpression(precedence + 1);
935
+ left = {
936
+ left,
937
+ operator: operator.value,
938
+ right,
939
+ type: 'BinaryExpression',
940
+ };
941
+ }
942
+ return left;
943
+ };
944
+
945
+ const ast = parseBinaryExpression(0);
946
+ if (index < tokens.length) {
947
+ throw new Error(`Unexpected token "${tokens[index].value}" in expression: ${source}`);
948
+ }
949
+ return ast;
950
+ }
951
+
952
+ class ExpressionScope {
953
+ constructor({ locals = {}, parent = null, rootContext }) {
954
+ this.locals = locals;
955
+ this.parent = parent;
956
+ this.rootContext = rootContext;
957
+ }
958
+
959
+ child(locals = {}) {
960
+ return new ExpressionScope({
961
+ locals,
962
+ parent: this,
963
+ rootContext: this.rootContext,
964
+ });
965
+ }
966
+
967
+ get(name) {
968
+ if (Object.hasOwn(this.locals, name)) {
969
+ return this.locals[name];
970
+ }
971
+ if (this.parent) {
972
+ return this.parent.get(name);
973
+ }
974
+ return this.rootContext.resolveIdentifier(name);
975
+ }
976
+ }
977
+
978
+ function evaluateAst(ast, scope) {
979
+ const rootContext = scope.rootContext;
980
+ const evaluate = (node, nextScope = scope) => evaluateAst(node, nextScope);
981
+
982
+ switch (ast?.type) {
983
+ case 'ArrayExpression':
984
+ return ast.elements.map((element) => evaluate(element, scope));
985
+ case 'BinaryExpression':
986
+ return applyBinaryOperator(ast.operator, evaluate(ast.left, scope), evaluate(ast.right, scope));
987
+ case 'CallExpression': {
988
+ if (ast.callee?.type === 'Identifier') {
989
+ return invokeGlobalFunction(ast.callee.name, ast.arguments, scope, evaluate);
990
+ }
991
+ if (ast.callee?.type === 'MemberExpression') {
992
+ const target = evaluate(ast.callee.object, scope);
993
+ const property = ast.callee.computed
994
+ ? evaluate(ast.callee.property, scope)
995
+ : ast.callee.property.name;
996
+ return invokeMethod(target, String(property ?? ''), ast.arguments, scope, evaluate, rootContext);
997
+ }
998
+ throw new Error('Unsupported call target');
999
+ }
1000
+ case 'Identifier':
1001
+ return scope.get(ast.name);
1002
+ case 'Literal':
1003
+ return ast.value;
1004
+ case 'MemberExpression': {
1005
+ const target = evaluate(ast.object, scope);
1006
+ const property = ast.computed ? evaluate(ast.property, scope) : ast.property.name;
1007
+ if (target?.__baseType === 'formula-namespace') {
1008
+ return rootContext.resolveFormulaValue(String(property ?? ''), target.row, target.evaluationState);
1009
+ }
1010
+ if (target == null) {
1011
+ return null;
1012
+ }
1013
+ return target[property];
1014
+ }
1015
+ case 'ObjectExpression':
1016
+ return ast.properties.reduce((acc, property) => {
1017
+ acc[property.key] = evaluate(property.value, scope);
1018
+ return acc;
1019
+ }, {});
1020
+ case 'UnaryExpression': {
1021
+ const value = evaluate(ast.argument, scope);
1022
+ return ast.operator === '!' ? !value : -Number(value ?? 0);
1023
+ }
1024
+ default:
1025
+ throw new Error(`Unsupported expression node "${ast?.type}"`);
1026
+ }
1027
+ }
1028
+
1029
+ export function evaluateExpression(expressionSource, rootContext, locals = {}) {
1030
+ const cache = rootContext.astCache;
1031
+ let ast = cache.get(expressionSource);
1032
+ if (!ast) {
1033
+ ast = parseExpression(expressionSource);
1034
+ cache.set(expressionSource, ast);
1035
+ }
1036
+ const scope = new ExpressionScope({ locals, rootContext });
1037
+ return evaluateAst(ast, scope);
1038
+ }
1039
+
1040
+ export function evaluateFilterNode(filterNode, rootContext) {
1041
+ if (!filterNode) {
1042
+ return true;
1043
+ }
1044
+ if (typeof filterNode === 'string') {
1045
+ return Boolean(evaluateExpression(filterNode, rootContext));
1046
+ }
1047
+ if (!isPlainObject(filterNode)) {
1048
+ return true;
1049
+ }
1050
+
1051
+ if (Array.isArray(filterNode.and)) {
1052
+ return filterNode.and.every((entry) => evaluateFilterNode(entry, rootContext));
1053
+ }
1054
+ if (Array.isArray(filterNode.or)) {
1055
+ return filterNode.or.some((entry) => evaluateFilterNode(entry, rootContext));
1056
+ }
1057
+ if (filterNode.not != null) {
1058
+ if (Array.isArray(filterNode.not)) {
1059
+ return !filterNode.not.some((entry) => evaluateFilterNode(entry, rootContext));
1060
+ }
1061
+ return !evaluateFilterNode(filterNode.not, rootContext);
1062
+ }
1063
+
1064
+ return true;
1065
+ }
1066
+
1067
+ function normalizeFormulaLookupName(name = '') {
1068
+ return String(name ?? '').replace(/^formula\./u, '');
1069
+ }
1070
+
1071
+ function resolveFormulaPropertyId(definition, name = '') {
1072
+ const formulaName = normalizeFormulaLookupName(name);
1073
+ if (!formulaName) {
1074
+ return null;
1075
+ }
1076
+
1077
+ const precomputedLookup = definition?.formulaLookup;
1078
+ if (precomputedLookup instanceof Map) {
1079
+ return precomputedLookup.get(formulaName) ?? precomputedLookup.get(`formula.${formulaName}`) ?? null;
1080
+ }
1081
+
1082
+ return Object.keys(definition?.formulas ?? {}).find((propertyId) => (
1083
+ normalizeFormulaLookupName(propertyId) === formulaName
1084
+ )) ?? null;
1085
+ }
1086
+
1087
+ export function createEvaluationRootContext({
1088
+ astCache = new Map(),
1089
+ currentRow,
1090
+ definition,
1091
+ evaluationState = null,
1092
+ identifierResolver = null,
1093
+ snapshot,
1094
+ thisFile,
1095
+ }) {
1096
+ const nextEvaluationState = evaluationState ?? {
1097
+ cache: new Map(),
1098
+ stack: new Set(),
1099
+ };
1100
+
1101
+ return {
1102
+ astCache,
1103
+ currentRow,
1104
+ definition,
1105
+ evaluationState: nextEvaluationState,
1106
+ resolveFormulaValue: (name, row, state = nextEvaluationState) => {
1107
+ const formulaPropertyId = resolveFormulaPropertyId(definition, name);
1108
+ if (!formulaPropertyId) {
1109
+ return null;
1110
+ }
1111
+
1112
+ const cacheKey = `${row.file.path}::${formulaPropertyId}`;
1113
+ if (state.cache.has(cacheKey)) {
1114
+ return state.cache.get(cacheKey);
1115
+ }
1116
+ if (state.stack.has(cacheKey)) {
1117
+ throw new Error(`Circular formula reference: ${formulaPropertyId}`);
1118
+ }
1119
+
1120
+ state.stack.add(cacheKey);
1121
+ const nextContext = createEvaluationRootContext({
1122
+ astCache,
1123
+ currentRow: row,
1124
+ definition,
1125
+ evaluationState: state,
1126
+ snapshot,
1127
+ thisFile,
1128
+ });
1129
+ const result = evaluateExpression(definition.formulas[formulaPropertyId].formula, nextContext);
1130
+ state.stack.delete(cacheKey);
1131
+ state.cache.set(cacheKey, result);
1132
+ return result;
1133
+ },
1134
+ resolveIdentifier: (name) => {
1135
+ if (typeof identifierResolver === 'function') {
1136
+ const resolved = identifierResolver(name);
1137
+ if (resolved !== undefined) {
1138
+ return resolved;
1139
+ }
1140
+ }
1141
+ if (name === 'file') {
1142
+ return currentRow.file;
1143
+ }
1144
+ if (name === 'formula') {
1145
+ return createFormulaNamespace(currentRow, nextEvaluationState);
1146
+ }
1147
+ if (name === 'note') {
1148
+ return currentRow.noteProperties;
1149
+ }
1150
+ if (name === 'this') {
1151
+ return createContextFileValue(thisFile);
1152
+ }
1153
+ if (name === 'values') {
1154
+ return [];
1155
+ }
1156
+ if (Object.hasOwn(currentRow.noteProperties, name)) {
1157
+ return currentRow.noteProperties[name];
1158
+ }
1159
+ return null;
1160
+ },
1161
+ snapshot,
1162
+ thisFile,
1163
+ };
1164
+ }
1165
+
1166
+ export function getPropertyValue(propertyId, row, definition, snapshot, thisFile, rootContext = null) {
1167
+ if (propertyId.startsWith('file.')) {
1168
+ return propertyId.split('.').slice(1).reduce((acc, segment) => acc?.[segment], row.file);
1169
+ }
1170
+
1171
+ if (hasFormulaDefinition(definition, propertyId)) {
1172
+ const context = rootContext ?? createEvaluationRootContext({
1173
+ currentRow: row,
1174
+ definition,
1175
+ snapshot,
1176
+ thisFile,
1177
+ });
1178
+ return context.resolveFormulaValue(propertyId, row, context.evaluationState);
1179
+ }
1180
+
1181
+ if (propertyId.startsWith('note.')) {
1182
+ return row.noteProperties[propertyId.slice(5)] ?? null;
1183
+ }
1184
+
1185
+ if (hasFormulaDefinition(definition, `formula.${propertyId}`)) {
1186
+ const context = rootContext ?? createEvaluationRootContext({
1187
+ currentRow: row,
1188
+ definition,
1189
+ snapshot,
1190
+ thisFile,
1191
+ });
1192
+ return context.resolveFormulaValue(propertyId, row, context.evaluationState);
1193
+ }
1194
+
1195
+ return row.noteProperties[propertyId] ?? null;
1196
+ }