collabmd 0.1.32 → 0.1.34
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/client/assets/{drawio-editor-Br4qyh3r.js → drawio-editor-BMTwP5mD.js} +1 -1
- package/dist/client/assets/{drawioEditor-ClmnTx5J.js → drawioEditor-D6clB5OH.js} +2 -2
- package/dist/client/assets/{editor-session-Cf-iV5-3.js → editor-session-Dzy_85Wp.js} +1 -1
- package/dist/client/assets/{excalidraw-editor-CXAxVhlw.js → excalidraw-editor-CAP9F8CB.js} +3 -3
- package/dist/client/assets/{excalidrawEditor-DoH_kMdu.js → excalidrawEditor-DD4AvzYP.js} +2 -2
- package/dist/client/assets/{exportDocument-9wTrBZNS.css → exportDocument-BbkN85c4.css} +1 -1
- package/dist/client/assets/index-BIt5EZXz.css +1 -0
- package/dist/client/assets/{index-C8QZYHvV.js → index-BLrtj9YM.js} +2 -2
- package/dist/client/assets/main-CfSHaGes.js +1368 -0
- package/dist/client/assets/{vault-api-client-Bf9tB_GW.js → vault-api-client-p7h7cXKM.js} +1 -1
- package/dist/client/drawio-editor.html +1 -1
- package/dist/client/excalidraw-editor.html +1 -1
- package/dist/client/export-document.html +2 -2
- package/dist/client/index.html +2 -2
- package/package.json +6 -5
- package/src/client/application/app-shell/git-feature.js +44 -36
- package/src/client/application/app-shell/ui-feature-shell.js +84 -63
- package/src/client/bootstrap/collabmd-app-shell.js +6 -0
- package/src/client/infrastructure/editor-session.js +4 -0
- package/src/client/infrastructure/editor-view-adapter.js +23 -0
- package/src/client/infrastructure/vault-api-client.js +48 -0
- package/src/client/presentation/bases-preview-controller.js +1388 -28
- package/src/client/presentation/comments-panel.js +50 -5
- package/src/client/presentation/excalidraw-embed-controller.js +4 -1
- package/src/client/presentation/file-history-view-controller.js +9 -1
- package/src/client/presentation/git-panel-controller.js +101 -91
- package/src/client/styles/components/scrollbars.css +5 -0
- package/src/client/styles/features/preview-markdown.css +468 -23
- package/src/server/domain/backlink-index.js +202 -11
- package/src/server/domain/bases/base-definition.js +138 -11
- package/src/server/domain/bases/base-expression-runtime.js +54 -17
- package/src/server/domain/bases/base-index-snapshot-store.js +167 -20
- package/src/server/domain/bases/base-query-metadata.js +242 -0
- package/src/server/domain/bases/base-query-results.js +18 -8
- package/src/server/domain/bases/base-query-service.js +360 -49
- package/src/server/domain/bases/base-transform.js +116 -0
- package/src/server/infrastructure/http/create-request-handler.js +63 -28
- package/src/server/infrastructure/http/create-vault-api-command-handler.js +275 -256
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +258 -184
- package/src/server/infrastructure/persistence/vault-file-store.js +68 -12
- package/dist/client/assets/index-CxSbUTs2.css +0 -1
- package/dist/client/assets/main-BjHHMlv0.js +0 -1249
- /package/dist/client/assets/{exportDocument-Bia2hI25.js → exportDocument-Zfwq1XVx.js} +0 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { createWikiTargetIndex, resolveWikiTargetWithIndex } from '../../domain/wiki-link-resolver.js';
|
|
14
|
-
import { isMarkdownFilePath } from '../../domain/file-kind.js';
|
|
14
|
+
import { getVaultFileExtension, isMarkdownFilePath } from '../../domain/file-kind.js';
|
|
15
15
|
import { mapWithConcurrency } from '../shared/async-utils.js';
|
|
16
16
|
|
|
17
17
|
const WIKI_LINK_RE = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
|
|
@@ -27,6 +27,39 @@ function createDeferred() {
|
|
|
27
27
|
return { promise, reject, resolve };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function normalizeWikiTargetKey(target = '') {
|
|
31
|
+
const normalizedTarget = String(target ?? '').trim();
|
|
32
|
+
if (!normalizedTarget) {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return getVaultFileExtension(normalizedTarget)
|
|
37
|
+
? normalizedTarget
|
|
38
|
+
: `${normalizedTarget}.md`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function collectWikiTargetKeysForFilePath(filePath = '') {
|
|
42
|
+
const normalizedPath = String(filePath ?? '').trim();
|
|
43
|
+
if (!normalizedPath) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const segments = normalizedPath.split('/').filter(Boolean);
|
|
48
|
+
const keys = [];
|
|
49
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
50
|
+
keys.push(segments.slice(index).join('/'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (isMarkdownFilePath(normalizedPath)) {
|
|
54
|
+
const rawSegments = normalizedPath.replace(/\.md$/i, '').split('/').filter(Boolean);
|
|
55
|
+
for (let index = 0; index < rawSegments.length; index += 1) {
|
|
56
|
+
keys.push(rawSegments.slice(index).join('/'));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return [...new Set(keys)];
|
|
61
|
+
}
|
|
62
|
+
|
|
30
63
|
export class BacklinkIndex {
|
|
31
64
|
constructor({
|
|
32
65
|
rebuildDelayMs = 150,
|
|
@@ -44,6 +77,10 @@ export class BacklinkIndex {
|
|
|
44
77
|
this.reverse = new Map();
|
|
45
78
|
/** @type {Map<string, Map<string, string[]>>} sourcePath → targetPath → contexts[] */
|
|
46
79
|
this.contextsBySource = new Map();
|
|
80
|
+
/** @type {Map<string, Set<string>>} sourcePath → normalized raw target keys */
|
|
81
|
+
this.rawTargetKeysBySource = new Map();
|
|
82
|
+
/** @type {Map<string, Set<string>>} normalized raw target key → source paths */
|
|
83
|
+
this.rawTargetSources = new Map();
|
|
47
84
|
/** @type {string[]} cached flat target file list for link resolution */
|
|
48
85
|
this._fileList = [];
|
|
49
86
|
/** @type {string[]} cached markdown source file list for content scans */
|
|
@@ -140,13 +177,16 @@ export class BacklinkIndex {
|
|
|
140
177
|
this.forward.clear();
|
|
141
178
|
this.reverse.clear();
|
|
142
179
|
this.contextsBySource.clear();
|
|
180
|
+
this.rawTargetKeysBySource.clear();
|
|
181
|
+
this.rawTargetSources.clear();
|
|
143
182
|
|
|
144
183
|
const snapshot = workspaceState ?? await this._resolveWorkspaceState();
|
|
145
184
|
this._fileList = Array.from(snapshot?.filePaths ?? snapshot?.markdownPaths ?? []);
|
|
146
|
-
this._sourceFileList = Array.from(
|
|
147
|
-
snapshot?.markdownPaths ?? this._fileList.filter((filePath) => isMarkdownFilePath(filePath)),
|
|
148
|
-
).filter((filePath) => this._fileList.includes(filePath));
|
|
149
185
|
this._fileSet = new Set(this._fileList);
|
|
186
|
+
this._sourceFileList = Array.from(
|
|
187
|
+
snapshot?.markdownPaths
|
|
188
|
+
?? this._fileList.filter((filePath) => isMarkdownFilePath(filePath)),
|
|
189
|
+
).filter((filePath) => this._fileSet.has(filePath));
|
|
150
190
|
this._refreshWikiTargetIndex();
|
|
151
191
|
|
|
152
192
|
const fileContents = await mapWithConcurrency(
|
|
@@ -256,6 +296,21 @@ export class BacklinkIndex {
|
|
|
256
296
|
this.contextsBySource.set(newPath, sourceContexts);
|
|
257
297
|
}
|
|
258
298
|
|
|
299
|
+
if (this.rawTargetKeysBySource.has(oldPath)) {
|
|
300
|
+
const rawTargetKeys = this.rawTargetKeysBySource.get(oldPath);
|
|
301
|
+
this.rawTargetKeysBySource.delete(oldPath);
|
|
302
|
+
this.rawTargetKeysBySource.set(newPath, rawTargetKeys);
|
|
303
|
+
rawTargetKeys.forEach((rawTargetKey) => {
|
|
304
|
+
const sources = this.rawTargetSources.get(rawTargetKey);
|
|
305
|
+
if (!sources) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
sources.delete(oldPath);
|
|
310
|
+
sources.add(newPath);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
259
314
|
// Move forward links
|
|
260
315
|
const oldForward = this.forward.get(oldPath);
|
|
261
316
|
if (oldForward) {
|
|
@@ -316,22 +371,64 @@ export class BacklinkIndex {
|
|
|
316
371
|
previousState = null,
|
|
317
372
|
nextState = null,
|
|
318
373
|
} = {}) {
|
|
319
|
-
let refreshIndex = false;
|
|
320
374
|
const previousEntries = previousState?.entries ?? new Map();
|
|
321
375
|
const nextEntries = nextState?.entries ?? new Map();
|
|
376
|
+
const changedPaths = Array.from(new Set(workspaceChange.changedPaths ?? []));
|
|
377
|
+
const { impactedSources, renameMap } = this._computeWorkspaceChangeMeta(workspaceChange, changedPaths, previousEntries, nextEntries);
|
|
378
|
+
|
|
379
|
+
let refreshIndex = this._applyDeletedPaths(workspaceChange.deletedPaths);
|
|
380
|
+
refreshIndex = this._applyRenamedPaths(workspaceChange.renamedPaths, refreshIndex);
|
|
381
|
+
refreshIndex = await this._applyChangedPaths(changedPaths, previousEntries, nextEntries, refreshIndex);
|
|
322
382
|
|
|
323
|
-
|
|
383
|
+
if (refreshIndex) {
|
|
384
|
+
const sourcesToRefresh = this._collectRefreshSources(impactedSources, changedPaths, workspaceChange.renamedPaths, nextEntries);
|
|
385
|
+
this._refreshWikiTargetIndex();
|
|
386
|
+
await this._refreshImpactedSources(sourcesToRefresh, { nextEntries, renameMap });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
_computeWorkspaceChangeMeta(workspaceChange, changedPaths, previousEntries, nextEntries) {
|
|
391
|
+
const createdPaths = changedPaths.filter((pathValue) => (
|
|
392
|
+
nextEntries.has(pathValue) && !previousEntries.has(pathValue)
|
|
393
|
+
));
|
|
394
|
+
const removedChangedPaths = changedPaths.filter((pathValue) => (
|
|
395
|
+
!nextEntries.has(pathValue) && previousEntries.has(pathValue)
|
|
396
|
+
));
|
|
397
|
+
const membershipAffectedPaths = [
|
|
398
|
+
...(workspaceChange.deletedPaths ?? []),
|
|
399
|
+
...removedChangedPaths,
|
|
400
|
+
...createdPaths,
|
|
401
|
+
...(workspaceChange.renamedPaths ?? []).flatMap((entry) => [entry?.oldPath, entry?.newPath]).filter(Boolean),
|
|
402
|
+
];
|
|
403
|
+
const impactedSources = this._collectImpactedSourcesForMembershipChanges(membershipAffectedPaths);
|
|
404
|
+
const renameMap = new Map(
|
|
405
|
+
(workspaceChange.renamedPaths ?? [])
|
|
406
|
+
.filter((entry) => entry?.oldPath && entry?.newPath)
|
|
407
|
+
.map((entry) => [entry.oldPath, entry.newPath]),
|
|
408
|
+
);
|
|
409
|
+
return { impactedSources, renameMap };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
_applyDeletedPaths(deletedPaths) {
|
|
413
|
+
let refreshIndex = false;
|
|
414
|
+
for (const pathValue of deletedPaths ?? []) {
|
|
324
415
|
refreshIndex = this.onFileDeleted(pathValue, { refreshIndex: false }) || refreshIndex;
|
|
325
416
|
}
|
|
417
|
+
return refreshIndex;
|
|
418
|
+
}
|
|
326
419
|
|
|
327
|
-
|
|
420
|
+
_applyRenamedPaths(renamedPaths, refreshIndex) {
|
|
421
|
+
for (const entry of renamedPaths ?? []) {
|
|
328
422
|
if (!entry?.oldPath || !entry?.newPath) {
|
|
329
423
|
continue;
|
|
330
424
|
}
|
|
331
425
|
refreshIndex = this.onFileRenamed(entry.oldPath, entry.newPath, { refreshIndex: false }) || refreshIndex;
|
|
332
426
|
}
|
|
427
|
+
return refreshIndex;
|
|
428
|
+
}
|
|
333
429
|
|
|
334
|
-
|
|
430
|
+
async _applyChangedPaths(changedPaths, previousEntries, nextEntries, refreshIndex) {
|
|
431
|
+
for (const pathValue of changedPaths) {
|
|
335
432
|
const existsNow = nextEntries.has(pathValue);
|
|
336
433
|
const existedBefore = previousEntries.has(pathValue);
|
|
337
434
|
if (!existsNow) {
|
|
@@ -359,10 +456,22 @@ export class BacklinkIndex {
|
|
|
359
456
|
refreshIndex = this.onFileCreated(pathValue, content, { refreshIndex: false }) || refreshIndex;
|
|
360
457
|
}
|
|
361
458
|
}
|
|
459
|
+
return refreshIndex;
|
|
460
|
+
}
|
|
362
461
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
462
|
+
_collectRefreshSources(impactedSources, changedPaths, renamedPaths, nextEntries) {
|
|
463
|
+
const sourcesToRefresh = new Set(impactedSources);
|
|
464
|
+
changedPaths.forEach((pathValue) => {
|
|
465
|
+
if (nextEntries.has(pathValue) && isMarkdownFilePath(pathValue)) {
|
|
466
|
+
sourcesToRefresh.add(pathValue);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
(renamedPaths ?? []).forEach((entry) => {
|
|
470
|
+
if (entry?.newPath && isMarkdownFilePath(entry.newPath)) {
|
|
471
|
+
sourcesToRefresh.add(entry.newPath);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
return sourcesToRefresh;
|
|
366
475
|
}
|
|
367
476
|
|
|
368
477
|
/**
|
|
@@ -408,6 +517,7 @@ export class BacklinkIndex {
|
|
|
408
517
|
_indexFile(filePath, content) {
|
|
409
518
|
const resolvedTargets = new Set();
|
|
410
519
|
const contextsByTarget = new Map();
|
|
520
|
+
const rawTargetKeys = new Set();
|
|
411
521
|
const lines = content.split('\n');
|
|
412
522
|
|
|
413
523
|
for (const line of lines) {
|
|
@@ -419,6 +529,11 @@ export class BacklinkIndex {
|
|
|
419
529
|
continue;
|
|
420
530
|
}
|
|
421
531
|
|
|
532
|
+
const rawTargetKey = normalizeWikiTargetKey(target);
|
|
533
|
+
if (rawTargetKey) {
|
|
534
|
+
rawTargetKeys.add(rawTargetKey);
|
|
535
|
+
}
|
|
536
|
+
|
|
422
537
|
const resolved = this._resolveTarget(target);
|
|
423
538
|
if (!resolved || resolved === filePath) {
|
|
424
539
|
continue;
|
|
@@ -434,6 +549,17 @@ export class BacklinkIndex {
|
|
|
434
549
|
}
|
|
435
550
|
|
|
436
551
|
this.contextsBySource.delete(filePath);
|
|
552
|
+
this._removeRawTargetSourceContributions(filePath);
|
|
553
|
+
|
|
554
|
+
if (rawTargetKeys.size > 0) {
|
|
555
|
+
this.rawTargetKeysBySource.set(filePath, rawTargetKeys);
|
|
556
|
+
rawTargetKeys.forEach((rawTargetKey) => {
|
|
557
|
+
if (!this.rawTargetSources.has(rawTargetKey)) {
|
|
558
|
+
this.rawTargetSources.set(rawTargetKey, new Set());
|
|
559
|
+
}
|
|
560
|
+
this.rawTargetSources.get(rawTargetKey).add(filePath);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
437
563
|
|
|
438
564
|
if (resolvedTargets.size > 0) {
|
|
439
565
|
this.contextsBySource.set(filePath, contextsByTarget);
|
|
@@ -450,6 +576,7 @@ export class BacklinkIndex {
|
|
|
450
576
|
|
|
451
577
|
_removeForwardLinks(filePath) {
|
|
452
578
|
this.contextsBySource.delete(filePath);
|
|
579
|
+
this._removeRawTargetSourceContributions(filePath);
|
|
453
580
|
|
|
454
581
|
const oldTargets = this.forward.get(filePath);
|
|
455
582
|
if (!oldTargets) return;
|
|
@@ -478,6 +605,70 @@ export class BacklinkIndex {
|
|
|
478
605
|
_refreshWikiTargetIndex() {
|
|
479
606
|
this._wikiTargetIndex = createWikiTargetIndex(this._fileList);
|
|
480
607
|
}
|
|
608
|
+
|
|
609
|
+
_removeRawTargetSourceContributions(filePath) {
|
|
610
|
+
const rawTargetKeys = this.rawTargetKeysBySource.get(filePath);
|
|
611
|
+
if (!rawTargetKeys) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
rawTargetKeys.forEach((rawTargetKey) => {
|
|
616
|
+
const sources = this.rawTargetSources.get(rawTargetKey);
|
|
617
|
+
if (!sources) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
sources.delete(filePath);
|
|
622
|
+
if (sources.size === 0) {
|
|
623
|
+
this.rawTargetSources.delete(rawTargetKey);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
this.rawTargetKeysBySource.delete(filePath);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
_collectImpactedSourcesForMembershipChanges(pathValues = []) {
|
|
630
|
+
const affectedTargetKeys = new Set();
|
|
631
|
+
pathValues.forEach((pathValue) => {
|
|
632
|
+
collectWikiTargetKeysForFilePath(pathValue).forEach((targetKey) => {
|
|
633
|
+
affectedTargetKeys.add(targetKey);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
const impactedSources = new Set();
|
|
638
|
+
affectedTargetKeys.forEach((targetKey) => {
|
|
639
|
+
this.rawTargetSources.get(targetKey)?.forEach((sourcePath) => {
|
|
640
|
+
impactedSources.add(sourcePath);
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
return impactedSources;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async _refreshImpactedSources(impactedSources = new Set(), {
|
|
647
|
+
nextEntries = new Map(),
|
|
648
|
+
renameMap = new Map(),
|
|
649
|
+
} = {}) {
|
|
650
|
+
const refreshedSources = new Set();
|
|
651
|
+
|
|
652
|
+
for (const sourcePath of impactedSources) {
|
|
653
|
+
const livePath = renameMap.get(sourcePath) ?? sourcePath;
|
|
654
|
+
if (
|
|
655
|
+
refreshedSources.has(livePath)
|
|
656
|
+
|| !livePath
|
|
657
|
+
|| !isMarkdownFilePath(livePath)
|
|
658
|
+
|| !nextEntries.has(livePath)
|
|
659
|
+
) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const content = await this.vaultFileStore.readMarkdownFile(livePath);
|
|
664
|
+
if (content === null) {
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
refreshedSources.add(livePath);
|
|
669
|
+
this.updateFile(livePath, content, { refreshIndex: false });
|
|
670
|
+
}
|
|
671
|
+
}
|
|
481
672
|
}
|
|
482
673
|
|
|
483
674
|
/** Flatten a vault tree into an array of file paths. */
|
|
@@ -15,6 +15,25 @@ function capitalize(value = '') {
|
|
|
15
15
|
return `${normalized.charAt(0).toUpperCase()}${normalized.slice(1)}`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function normalizeDirection(value, fallback = 'asc') {
|
|
19
|
+
return String(value ?? fallback).toLowerCase() === 'desc' ? 'desc' : 'asc';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeFormulaPropertyId(value = '') {
|
|
23
|
+
const normalized = String(value ?? '').trim();
|
|
24
|
+
if (!normalized) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return normalized.startsWith('formula.')
|
|
29
|
+
? normalized
|
|
30
|
+
: `formula.${normalized}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function bareFormulaName(value = '') {
|
|
34
|
+
return normalizeFormulaPropertyId(value).replace(/^formula\./u, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
18
37
|
function normalizeBaseView(rawView, index) {
|
|
19
38
|
const view = isPlainObject(rawView) ? rawView : {};
|
|
20
39
|
const type = typeof view.type === 'string' ? view.type : 'table';
|
|
@@ -43,9 +62,7 @@ function normalizeBaseView(rawView, index) {
|
|
|
43
62
|
return acc;
|
|
44
63
|
}, {}),
|
|
45
64
|
filters: view.filters ?? null,
|
|
46
|
-
groupBy:
|
|
47
|
-
? view.groupBy
|
|
48
|
-
: (typeof view.group_by === 'string' ? view.group_by : null),
|
|
65
|
+
groupBy: normalizeViewGroupBy(view.groupBy ?? view.group_by),
|
|
49
66
|
id: `view-${index}`,
|
|
50
67
|
image: typeof view.image === 'string' ? view.image : null,
|
|
51
68
|
limit: Number.isFinite(Number(view.limit)) ? Math.max(0, Number(view.limit)) : null,
|
|
@@ -80,6 +97,31 @@ function normalizeViewOrder(value) {
|
|
|
80
97
|
return [];
|
|
81
98
|
}
|
|
82
99
|
|
|
100
|
+
function normalizeViewGroupBy(value) {
|
|
101
|
+
if (typeof value === 'string' && value.trim()) {
|
|
102
|
+
return {
|
|
103
|
+
direction: 'asc',
|
|
104
|
+
explicitDirection: false,
|
|
105
|
+
property: value.trim(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!isPlainObject(value)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const property = value.property ?? value.id ?? value.name ?? null;
|
|
114
|
+
if (typeof property !== 'string' || !property.trim()) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
direction: normalizeDirection(value.direction ?? value.order, 'asc'),
|
|
120
|
+
explicitDirection: Object.hasOwn(value, 'direction') || Object.hasOwn(value, 'order'),
|
|
121
|
+
property: property.trim(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
83
125
|
function normalizeViewSort(value) {
|
|
84
126
|
if (typeof value === 'string' && value.trim()) {
|
|
85
127
|
return [{ direction: 'asc', property: value.trim() }];
|
|
@@ -103,9 +145,7 @@ function normalizeViewSort(value) {
|
|
|
103
145
|
return null;
|
|
104
146
|
}
|
|
105
147
|
|
|
106
|
-
const direction =
|
|
107
|
-
? 'desc'
|
|
108
|
-
: 'asc';
|
|
148
|
+
const direction = normalizeDirection(entry.direction ?? entry.order, 'asc');
|
|
109
149
|
return { direction, property: property.trim() };
|
|
110
150
|
})
|
|
111
151
|
.filter(Boolean);
|
|
@@ -183,20 +223,70 @@ function resolvePropertyLabel(propertyId, definition) {
|
|
|
183
223
|
if (config?.displayName) {
|
|
184
224
|
return config.displayName;
|
|
185
225
|
}
|
|
226
|
+
|
|
227
|
+
if (propertyId.startsWith('formula.')) {
|
|
228
|
+
return bareFormulaName(propertyId);
|
|
229
|
+
}
|
|
230
|
+
|
|
186
231
|
return propertyId.startsWith('file.')
|
|
187
232
|
? propertyId.slice(5)
|
|
188
233
|
: propertyId.replace(/^note\./u, '');
|
|
189
234
|
}
|
|
190
235
|
|
|
236
|
+
function normalizeFormulaEntries(rawFormulas, rawProperties) {
|
|
237
|
+
const formulas = {};
|
|
238
|
+
const appendFormula = (propertyId, formula, { legacy = false } = {}) => {
|
|
239
|
+
const normalizedId = normalizeFormulaPropertyId(propertyId);
|
|
240
|
+
if (!normalizedId || typeof formula !== 'string') {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
formulas[normalizedId] = {
|
|
245
|
+
formula,
|
|
246
|
+
id: normalizedId,
|
|
247
|
+
legacy,
|
|
248
|
+
name: bareFormulaName(normalizedId),
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (isPlainObject(rawFormulas)) {
|
|
253
|
+
Object.entries(rawFormulas).forEach(([name, formula]) => {
|
|
254
|
+
appendFormula(name, formula);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
Object.entries(rawProperties).forEach(([id, config]) => {
|
|
259
|
+
if (typeof config?.formula === 'string') {
|
|
260
|
+
appendFormula(id, config.formula, { legacy: true });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return formulas;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function createFormulaLookup(formulas = {}) {
|
|
268
|
+
const lookup = new Map();
|
|
269
|
+
|
|
270
|
+
Object.keys(formulas).forEach((propertyId) => {
|
|
271
|
+
const bareName = bareFormulaName(propertyId);
|
|
272
|
+
if (bareName) {
|
|
273
|
+
lookup.set(bareName, propertyId);
|
|
274
|
+
}
|
|
275
|
+
lookup.set(propertyId, propertyId);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return lookup;
|
|
279
|
+
}
|
|
280
|
+
|
|
191
281
|
export function normalizeBaseDefinition(source = '') {
|
|
192
282
|
const raw = yaml.load(String(source ?? '')) ?? {};
|
|
193
283
|
const rawObject = isPlainObject(raw) ? raw : {};
|
|
194
284
|
const rawProperties = isPlainObject(rawObject.properties) ? rawObject.properties : {};
|
|
285
|
+
const formulas = normalizeFormulaEntries(rawObject.formulas, rawProperties);
|
|
195
286
|
const properties = Object.entries(rawProperties).reduce((acc, [id, config]) => {
|
|
196
287
|
const normalizedConfig = isPlainObject(config) ? { ...config } : {};
|
|
197
288
|
acc[id] = {
|
|
198
289
|
displayName: typeof normalizedConfig.displayName === 'string' ? normalizedConfig.displayName : null,
|
|
199
|
-
formula: typeof normalizedConfig.formula === 'string' ? normalizedConfig.formula : null,
|
|
200
290
|
id,
|
|
201
291
|
raw: normalizedConfig,
|
|
202
292
|
};
|
|
@@ -211,6 +301,8 @@ export function normalizeBaseDefinition(source = '') {
|
|
|
211
301
|
|
|
212
302
|
return {
|
|
213
303
|
filters: rawObject.filters ?? null,
|
|
304
|
+
formulas,
|
|
305
|
+
formulaLookup: createFormulaLookup(formulas),
|
|
214
306
|
properties,
|
|
215
307
|
raw: rawObject,
|
|
216
308
|
views,
|
|
@@ -220,7 +312,10 @@ export function normalizeBaseDefinition(source = '') {
|
|
|
220
312
|
export function buildColumns(definition, view) {
|
|
221
313
|
const order = view.order.length > 0
|
|
222
314
|
? view.order
|
|
223
|
-
: ['file.name', ...
|
|
315
|
+
: ['file.name', ...new Set([
|
|
316
|
+
...Object.keys(definition.properties),
|
|
317
|
+
...Object.keys(definition.formulas),
|
|
318
|
+
])];
|
|
224
319
|
return order.map((propertyId) => ({
|
|
225
320
|
id: propertyId,
|
|
226
321
|
label: resolvePropertyLabel(propertyId, definition),
|
|
@@ -230,8 +325,8 @@ export function buildColumns(definition, view) {
|
|
|
230
325
|
export function collectEvaluatedPropertyIds(columns, view) {
|
|
231
326
|
const propertyIds = new Set(columns.map((column) => column.id));
|
|
232
327
|
|
|
233
|
-
if (view.groupBy) {
|
|
234
|
-
propertyIds.add(view.groupBy);
|
|
328
|
+
if (view.groupBy?.property) {
|
|
329
|
+
propertyIds.add(view.groupBy.property);
|
|
235
330
|
}
|
|
236
331
|
|
|
237
332
|
view.sort.forEach((sortConfig) => {
|
|
@@ -249,7 +344,39 @@ export function collectEvaluatedPropertyIds(columns, view) {
|
|
|
249
344
|
return [...propertyIds];
|
|
250
345
|
}
|
|
251
346
|
|
|
347
|
+
export function findView(definition, requestedView = '') {
|
|
348
|
+
return definition.views.find((entry) => entry.name === requestedView || entry.id === requestedView) ?? definition.views[0];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function normalizeRawDefinitionForWrite(definition) {
|
|
352
|
+
const rawDefinition = isPlainObject(definition?.raw)
|
|
353
|
+
? structuredClone(definition.raw)
|
|
354
|
+
: structuredClone(isPlainObject(definition) ? definition : {});
|
|
355
|
+
const rawProperties = isPlainObject(rawDefinition.properties) ? rawDefinition.properties : {};
|
|
356
|
+
const formulas = isPlainObject(rawDefinition.formulas)
|
|
357
|
+
? { ...rawDefinition.formulas }
|
|
358
|
+
: {};
|
|
359
|
+
|
|
360
|
+
Object.entries(rawProperties).forEach(([propertyId, config]) => {
|
|
361
|
+
if (!isPlainObject(config) || typeof config.formula !== 'string') {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
formulas[bareFormulaName(propertyId)] = config.formula;
|
|
366
|
+
delete config.formula;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
rawDefinition.properties = rawProperties;
|
|
370
|
+
if (Object.keys(formulas).length > 0) {
|
|
371
|
+
rawDefinition.formulas = formulas;
|
|
372
|
+
} else {
|
|
373
|
+
delete rawDefinition.formulas;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return rawDefinition;
|
|
377
|
+
}
|
|
378
|
+
|
|
252
379
|
export function serializeBaseDefinition(definition) {
|
|
253
|
-
const raw = definition
|
|
380
|
+
const raw = normalizeRawDefinitionForWrite(definition);
|
|
254
381
|
return `${yaml.dump(raw, { lineWidth: -1, noRefs: true }).trim()}\n`;
|
|
255
382
|
}
|
|
@@ -111,6 +111,10 @@ function normalizeDuration(duration) {
|
|
|
111
111
|
return Number.isFinite(amount) && multiplier ? amount * multiplier : null;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
function hasFormulaDefinition(definition, propertyId = '') {
|
|
115
|
+
return Boolean(definition?.formulas?.[propertyId]);
|
|
116
|
+
}
|
|
117
|
+
|
|
114
118
|
export function parseDateValue(value) {
|
|
115
119
|
if (value instanceof Date) {
|
|
116
120
|
return Number.isFinite(value.getTime()) ? value : null;
|
|
@@ -476,6 +480,26 @@ function invokeMethod(target, name, argNodes, scope, evaluate, rootContext) {
|
|
|
476
480
|
return rootContext.resolveFormulaValue(name, target.row, target.evaluationState);
|
|
477
481
|
}
|
|
478
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
|
+
|
|
479
503
|
if (Array.isArray(target)) {
|
|
480
504
|
if (name === 'contains') {
|
|
481
505
|
const needle = evaluate(argNodes[0], scope);
|
|
@@ -1031,6 +1055,9 @@ export function evaluateFilterNode(filterNode, rootContext) {
|
|
|
1031
1055
|
return filterNode.or.some((entry) => evaluateFilterNode(entry, rootContext));
|
|
1032
1056
|
}
|
|
1033
1057
|
if (filterNode.not != null) {
|
|
1058
|
+
if (Array.isArray(filterNode.not)) {
|
|
1059
|
+
return !filterNode.not.some((entry) => evaluateFilterNode(entry, rootContext));
|
|
1060
|
+
}
|
|
1034
1061
|
return !evaluateFilterNode(filterNode.not, rootContext);
|
|
1035
1062
|
}
|
|
1036
1063
|
|
|
@@ -1041,6 +1068,22 @@ function normalizeFormulaLookupName(name = '') {
|
|
|
1041
1068
|
return String(name ?? '').replace(/^formula\./u, '');
|
|
1042
1069
|
}
|
|
1043
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
|
+
|
|
1044
1087
|
export function createEvaluationRootContext({
|
|
1045
1088
|
astCache = new Map(),
|
|
1046
1089
|
currentRow,
|
|
@@ -1061,11 +1104,7 @@ export function createEvaluationRootContext({
|
|
|
1061
1104
|
definition,
|
|
1062
1105
|
evaluationState: nextEvaluationState,
|
|
1063
1106
|
resolveFormulaValue: (name, row, state = nextEvaluationState) => {
|
|
1064
|
-
const
|
|
1065
|
-
const formulaPropertyId = Object.keys(definition.properties).find((propertyId) => {
|
|
1066
|
-
const config = definition.properties[propertyId];
|
|
1067
|
-
return config?.formula && normalizeFormulaLookupName(propertyId) === formulaName;
|
|
1068
|
-
});
|
|
1107
|
+
const formulaPropertyId = resolveFormulaPropertyId(definition, name);
|
|
1069
1108
|
if (!formulaPropertyId) {
|
|
1070
1109
|
return null;
|
|
1071
1110
|
}
|
|
@@ -1087,7 +1126,7 @@ export function createEvaluationRootContext({
|
|
|
1087
1126
|
snapshot,
|
|
1088
1127
|
thisFile,
|
|
1089
1128
|
});
|
|
1090
|
-
const result = evaluateExpression(definition.
|
|
1129
|
+
const result = evaluateExpression(definition.formulas[formulaPropertyId].formula, nextContext);
|
|
1091
1130
|
state.stack.delete(cacheKey);
|
|
1092
1131
|
state.cache.set(cacheKey, result);
|
|
1093
1132
|
return result;
|
|
@@ -1124,18 +1163,13 @@ export function createEvaluationRootContext({
|
|
|
1124
1163
|
};
|
|
1125
1164
|
}
|
|
1126
1165
|
|
|
1127
|
-
export function getPropertyValue(propertyId, row, definition, snapshot, thisFile) {
|
|
1166
|
+
export function getPropertyValue(propertyId, row, definition, snapshot, thisFile, rootContext = null) {
|
|
1128
1167
|
if (propertyId.startsWith('file.')) {
|
|
1129
1168
|
return propertyId.split('.').slice(1).reduce((acc, segment) => acc?.[segment], row.file);
|
|
1130
1169
|
}
|
|
1131
1170
|
|
|
1132
|
-
if (propertyId
|
|
1133
|
-
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
const propertyConfig = definition.properties[propertyId];
|
|
1137
|
-
if (propertyConfig?.formula) {
|
|
1138
|
-
const context = createEvaluationRootContext({
|
|
1171
|
+
if (hasFormulaDefinition(definition, propertyId)) {
|
|
1172
|
+
const context = rootContext ?? createEvaluationRootContext({
|
|
1139
1173
|
currentRow: row,
|
|
1140
1174
|
definition,
|
|
1141
1175
|
snapshot,
|
|
@@ -1144,9 +1178,12 @@ export function getPropertyValue(propertyId, row, definition, snapshot, thisFile
|
|
|
1144
1178
|
return context.resolveFormulaValue(propertyId, row, context.evaluationState);
|
|
1145
1179
|
}
|
|
1146
1180
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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({
|
|
1150
1187
|
currentRow: row,
|
|
1151
1188
|
definition,
|
|
1152
1189
|
snapshot,
|