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.
- package/dist/client/assets/{drawio-editor-B4sHHwWJ.js → drawio-editor-BMTwP5mD.js} +1 -1
- package/dist/client/assets/drawioEditor-D6clB5OH.js +2 -0
- package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Dzy_85Wp.js} +2 -2
- package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
- package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CAP9F8CB.js} +3 -3
- package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DD4AvzYP.js} +2 -2
- package/dist/client/assets/exportDocument-BbkN85c4.css +1 -0
- package/dist/client/assets/index-BIt5EZXz.css +1 -0
- package/dist/client/assets/{index-kPgEYvEX.js → index-Czg_KL-i.js} +2 -2
- package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/client/assets/main-DeowJOSp.js +1368 -0
- package/dist/client/assets/vault-api-client-D7Ck0CvR.css +1 -0
- package/dist/client/assets/vault-api-client-p7h7cXKM.js +1 -0
- 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 +8 -5
- package/src/client/app/main-entry.js +2 -0
- package/src/client/application/app-shell/git-feature.js +28 -36
- package/src/client/application/app-shell/presence-feature.js +3 -1
- package/src/client/application/diagram-preview-hydrator.js +24 -15
- package/src/client/application/mermaid-preview-hydrator.js +121 -4
- package/src/client/application/plantuml-preview-hydrator.js +6 -16
- package/src/client/application/preview-renderer.js +11 -2
- package/src/client/bootstrap/collabmd-app-shell.js +182 -50
- package/src/client/domain/excalidraw-document-switch.js +3 -0
- package/src/client/excalidraw-editor.js +187 -84
- package/src/client/infrastructure/auth-client.js +4 -4
- package/src/client/infrastructure/backlinks-api-client.js +18 -0
- package/src/client/infrastructure/comment-thread-store.js +4 -0
- package/src/client/infrastructure/editor-session.js +6 -0
- package/src/client/infrastructure/editor-view-adapter.js +23 -0
- package/src/client/infrastructure/git-api-client.js +134 -0
- package/src/client/infrastructure/plantuml-api-client.js +21 -0
- package/src/client/infrastructure/vault-api-client.js +115 -0
- package/src/client/presentation/backlinks-panel.js +6 -9
- package/src/client/presentation/bases-preview-controller.js +1413 -34
- package/src/client/presentation/comments-panel.js +50 -5
- package/src/client/presentation/excalidraw-embed-controller.js +251 -15
- package/src/client/presentation/excalidraw-embed-reconciler.js +0 -1
- package/src/client/presentation/file-action-controller.js +97 -0
- package/src/client/presentation/file-explorer-controller.js +2 -0
- package/src/client/presentation/file-explorer-view.js +368 -0
- package/src/client/presentation/file-history-view-controller.js +41 -40
- package/src/client/presentation/git-diff-view-controller.js +70 -53
- package/src/client/presentation/git-panel-controller.js +12 -30
- package/src/client/styles/components/scrollbars.css +5 -0
- package/src/client/styles/export-document.css +45 -2
- package/src/client/styles/features/auth-gate.css +13 -24
- package/src/client/styles/features/comment-card.css +6 -0
- package/src/client/styles/features/comments-drawer.css +38 -18
- package/src/client/styles/features/diagram-preview.css +33 -0
- package/src/client/styles/features/embedded-preview.css +37 -11
- package/src/client/styles/features/preview-markdown.css +559 -39
- package/src/client/styles/foundation/themes.css +4 -0
- package/src/client/styles/foundation/tokens.css +4 -3
- package/src/client/styles/layout/sidebar.css +72 -0
- package/src/server/auth/auth-common.js +224 -0
- package/src/server/auth/auth-constants.js +30 -0
- package/src/server/auth/auth-strategies.js +320 -0
- package/src/server/auth/create-auth-service.js +24 -545
- package/src/server/domain/backlink-index.js +177 -5
- package/src/server/domain/bases/base-definition.js +382 -0
- package/src/server/domain/bases/base-expression-runtime.js +1196 -0
- package/src/server/domain/bases/base-index-snapshot-store.js +565 -0
- package/src/server/domain/bases/base-query-metadata.js +242 -0
- package/src/server/domain/bases/base-query-results.js +215 -0
- package/src/server/domain/bases/base-query-service.js +406 -1992
- package/src/server/domain/bases/base-transform.js +116 -0
- package/src/server/domain/collaboration/collaboration-room.js +36 -1
- package/src/server/domain/collaboration/room-client-state-store.js +1 -0
- package/src/server/domain/collaboration/room-persistence-controller.js +1 -1
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +161 -1
- package/src/server/infrastructure/http/http-response.js +56 -0
- package/src/server/infrastructure/persistence/vault-file-store.js +110 -0
- package/dist/client/assets/drawioEditor-DGpo2hcx.js +0 -2
- package/dist/client/assets/embedded-editor-base-DgmRcsR8.css +0 -1
- package/dist/client/assets/exportDocument-DmV7Gngb.css +0 -1
- package/dist/client/assets/index-D69HOZHr.css +0 -1
- package/dist/client/assets/main-DSxNTtD-.js +0 -1243
- package/dist/client/assets/vault-api-client-BzHQseMN.js +0 -1
- package/dist/client/assets/vault-api-client-De7kMvrE.css +0 -1
- /package/dist/client/assets/{exportDocument-CtZKWE2c.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) {
|
|
@@ -319,6 +374,25 @@ export class BacklinkIndex {
|
|
|
319
374
|
let refreshIndex = false;
|
|
320
375
|
const previousEntries = previousState?.entries ?? new Map();
|
|
321
376
|
const nextEntries = nextState?.entries ?? new Map();
|
|
377
|
+
const changedPaths = Array.from(new Set(workspaceChange.changedPaths ?? []));
|
|
378
|
+
const createdPaths = changedPaths.filter((pathValue) => (
|
|
379
|
+
nextEntries.has(pathValue) && !previousEntries.has(pathValue)
|
|
380
|
+
));
|
|
381
|
+
const removedChangedPaths = changedPaths.filter((pathValue) => (
|
|
382
|
+
!nextEntries.has(pathValue) && previousEntries.has(pathValue)
|
|
383
|
+
));
|
|
384
|
+
const membershipAffectedPaths = [
|
|
385
|
+
...(workspaceChange.deletedPaths ?? []),
|
|
386
|
+
...removedChangedPaths,
|
|
387
|
+
...createdPaths,
|
|
388
|
+
...(workspaceChange.renamedPaths ?? []).flatMap((entry) => [entry?.oldPath, entry?.newPath]).filter(Boolean),
|
|
389
|
+
];
|
|
390
|
+
const impactedSources = this._collectImpactedSourcesForMembershipChanges(membershipAffectedPaths);
|
|
391
|
+
const renameMap = new Map(
|
|
392
|
+
(workspaceChange.renamedPaths ?? [])
|
|
393
|
+
.filter((entry) => entry?.oldPath && entry?.newPath)
|
|
394
|
+
.map((entry) => [entry.oldPath, entry.newPath]),
|
|
395
|
+
);
|
|
322
396
|
|
|
323
397
|
for (const pathValue of workspaceChange.deletedPaths ?? []) {
|
|
324
398
|
refreshIndex = this.onFileDeleted(pathValue, { refreshIndex: false }) || refreshIndex;
|
|
@@ -331,7 +405,7 @@ export class BacklinkIndex {
|
|
|
331
405
|
refreshIndex = this.onFileRenamed(entry.oldPath, entry.newPath, { refreshIndex: false }) || refreshIndex;
|
|
332
406
|
}
|
|
333
407
|
|
|
334
|
-
for (const pathValue of
|
|
408
|
+
for (const pathValue of changedPaths) {
|
|
335
409
|
const existsNow = nextEntries.has(pathValue);
|
|
336
410
|
const existedBefore = previousEntries.has(pathValue);
|
|
337
411
|
if (!existsNow) {
|
|
@@ -361,7 +435,23 @@ export class BacklinkIndex {
|
|
|
361
435
|
}
|
|
362
436
|
|
|
363
437
|
if (refreshIndex) {
|
|
438
|
+
const sourcesToRefresh = new Set(impactedSources);
|
|
439
|
+
changedPaths.forEach((pathValue) => {
|
|
440
|
+
if (nextEntries.has(pathValue) && isMarkdownFilePath(pathValue)) {
|
|
441
|
+
sourcesToRefresh.add(pathValue);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
(workspaceChange.renamedPaths ?? []).forEach((entry) => {
|
|
445
|
+
if (entry?.newPath && isMarkdownFilePath(entry.newPath)) {
|
|
446
|
+
sourcesToRefresh.add(entry.newPath);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
364
450
|
this._refreshWikiTargetIndex();
|
|
451
|
+
await this._refreshImpactedSources(sourcesToRefresh, {
|
|
452
|
+
nextEntries,
|
|
453
|
+
renameMap,
|
|
454
|
+
});
|
|
365
455
|
}
|
|
366
456
|
}
|
|
367
457
|
|
|
@@ -408,6 +498,7 @@ export class BacklinkIndex {
|
|
|
408
498
|
_indexFile(filePath, content) {
|
|
409
499
|
const resolvedTargets = new Set();
|
|
410
500
|
const contextsByTarget = new Map();
|
|
501
|
+
const rawTargetKeys = new Set();
|
|
411
502
|
const lines = content.split('\n');
|
|
412
503
|
|
|
413
504
|
for (const line of lines) {
|
|
@@ -419,6 +510,11 @@ export class BacklinkIndex {
|
|
|
419
510
|
continue;
|
|
420
511
|
}
|
|
421
512
|
|
|
513
|
+
const rawTargetKey = normalizeWikiTargetKey(target);
|
|
514
|
+
if (rawTargetKey) {
|
|
515
|
+
rawTargetKeys.add(rawTargetKey);
|
|
516
|
+
}
|
|
517
|
+
|
|
422
518
|
const resolved = this._resolveTarget(target);
|
|
423
519
|
if (!resolved || resolved === filePath) {
|
|
424
520
|
continue;
|
|
@@ -434,6 +530,17 @@ export class BacklinkIndex {
|
|
|
434
530
|
}
|
|
435
531
|
|
|
436
532
|
this.contextsBySource.delete(filePath);
|
|
533
|
+
this._removeRawTargetSourceContributions(filePath);
|
|
534
|
+
|
|
535
|
+
if (rawTargetKeys.size > 0) {
|
|
536
|
+
this.rawTargetKeysBySource.set(filePath, rawTargetKeys);
|
|
537
|
+
rawTargetKeys.forEach((rawTargetKey) => {
|
|
538
|
+
if (!this.rawTargetSources.has(rawTargetKey)) {
|
|
539
|
+
this.rawTargetSources.set(rawTargetKey, new Set());
|
|
540
|
+
}
|
|
541
|
+
this.rawTargetSources.get(rawTargetKey).add(filePath);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
437
544
|
|
|
438
545
|
if (resolvedTargets.size > 0) {
|
|
439
546
|
this.contextsBySource.set(filePath, contextsByTarget);
|
|
@@ -450,6 +557,7 @@ export class BacklinkIndex {
|
|
|
450
557
|
|
|
451
558
|
_removeForwardLinks(filePath) {
|
|
452
559
|
this.contextsBySource.delete(filePath);
|
|
560
|
+
this._removeRawTargetSourceContributions(filePath);
|
|
453
561
|
|
|
454
562
|
const oldTargets = this.forward.get(filePath);
|
|
455
563
|
if (!oldTargets) return;
|
|
@@ -478,6 +586,70 @@ export class BacklinkIndex {
|
|
|
478
586
|
_refreshWikiTargetIndex() {
|
|
479
587
|
this._wikiTargetIndex = createWikiTargetIndex(this._fileList);
|
|
480
588
|
}
|
|
589
|
+
|
|
590
|
+
_removeRawTargetSourceContributions(filePath) {
|
|
591
|
+
const rawTargetKeys = this.rawTargetKeysBySource.get(filePath);
|
|
592
|
+
if (!rawTargetKeys) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
rawTargetKeys.forEach((rawTargetKey) => {
|
|
597
|
+
const sources = this.rawTargetSources.get(rawTargetKey);
|
|
598
|
+
if (!sources) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
sources.delete(filePath);
|
|
603
|
+
if (sources.size === 0) {
|
|
604
|
+
this.rawTargetSources.delete(rawTargetKey);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
this.rawTargetKeysBySource.delete(filePath);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
_collectImpactedSourcesForMembershipChanges(pathValues = []) {
|
|
611
|
+
const affectedTargetKeys = new Set();
|
|
612
|
+
pathValues.forEach((pathValue) => {
|
|
613
|
+
collectWikiTargetKeysForFilePath(pathValue).forEach((targetKey) => {
|
|
614
|
+
affectedTargetKeys.add(targetKey);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const impactedSources = new Set();
|
|
619
|
+
affectedTargetKeys.forEach((targetKey) => {
|
|
620
|
+
this.rawTargetSources.get(targetKey)?.forEach((sourcePath) => {
|
|
621
|
+
impactedSources.add(sourcePath);
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
return impactedSources;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async _refreshImpactedSources(impactedSources = new Set(), {
|
|
628
|
+
nextEntries = new Map(),
|
|
629
|
+
renameMap = new Map(),
|
|
630
|
+
} = {}) {
|
|
631
|
+
const refreshedSources = new Set();
|
|
632
|
+
|
|
633
|
+
for (const sourcePath of impactedSources) {
|
|
634
|
+
const livePath = renameMap.get(sourcePath) ?? sourcePath;
|
|
635
|
+
if (
|
|
636
|
+
refreshedSources.has(livePath)
|
|
637
|
+
|| !livePath
|
|
638
|
+
|| !isMarkdownFilePath(livePath)
|
|
639
|
+
|| !nextEntries.has(livePath)
|
|
640
|
+
) {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const content = await this.vaultFileStore.readMarkdownFile(livePath);
|
|
645
|
+
if (content === null) {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
refreshedSources.add(livePath);
|
|
650
|
+
this.updateFile(livePath, content, { refreshIndex: false });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
481
653
|
}
|
|
482
654
|
|
|
483
655
|
/** Flatten a vault tree into an array of file paths. */
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_VIEW_TYPES = new Set(['cards', 'list', 'table']);
|
|
4
|
+
|
|
5
|
+
function isPlainObject(value) {
|
|
6
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function capitalize(value = '') {
|
|
10
|
+
const normalized = String(value ?? '').trim();
|
|
11
|
+
if (!normalized) {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return `${normalized.charAt(0).toUpperCase()}${normalized.slice(1)}`;
|
|
16
|
+
}
|
|
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
|
+
|
|
37
|
+
function normalizeBaseView(rawView, index) {
|
|
38
|
+
const view = isPlainObject(rawView) ? rawView : {};
|
|
39
|
+
const type = typeof view.type === 'string' ? view.type : 'table';
|
|
40
|
+
const name = typeof view.name === 'string' && view.name.trim()
|
|
41
|
+
? view.name.trim()
|
|
42
|
+
: `${capitalize(type)} ${index + 1}`;
|
|
43
|
+
return {
|
|
44
|
+
extra: Object.entries(view).reduce((acc, [key, value]) => {
|
|
45
|
+
if ([
|
|
46
|
+
'filters',
|
|
47
|
+
'groupBy',
|
|
48
|
+
'group_by',
|
|
49
|
+
'image',
|
|
50
|
+
'limit',
|
|
51
|
+
'name',
|
|
52
|
+
'order',
|
|
53
|
+
'properties',
|
|
54
|
+
'sort',
|
|
55
|
+
'sorts',
|
|
56
|
+
'summaries',
|
|
57
|
+
'type',
|
|
58
|
+
].includes(key)) {
|
|
59
|
+
return acc;
|
|
60
|
+
}
|
|
61
|
+
acc[key] = value;
|
|
62
|
+
return acc;
|
|
63
|
+
}, {}),
|
|
64
|
+
filters: view.filters ?? null,
|
|
65
|
+
groupBy: normalizeViewGroupBy(view.groupBy ?? view.group_by),
|
|
66
|
+
id: `view-${index}`,
|
|
67
|
+
image: typeof view.image === 'string' ? view.image : null,
|
|
68
|
+
limit: Number.isFinite(Number(view.limit)) ? Math.max(0, Number(view.limit)) : null,
|
|
69
|
+
name,
|
|
70
|
+
order: normalizeViewOrder(view.order ?? view.properties),
|
|
71
|
+
sort: normalizeViewSort(view.sort ?? view.sorts),
|
|
72
|
+
summaries: normalizeSummaries(view.summaries),
|
|
73
|
+
supported: SUPPORTED_VIEW_TYPES.has(type),
|
|
74
|
+
type,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeViewOrder(value) {
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
return value
|
|
81
|
+
.map((entry) => {
|
|
82
|
+
if (typeof entry === 'string') {
|
|
83
|
+
return entry;
|
|
84
|
+
}
|
|
85
|
+
if (isPlainObject(entry)) {
|
|
86
|
+
return entry.id ?? entry.property ?? entry.name ?? null;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
})
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof value === 'string' && value.trim()) {
|
|
94
|
+
return [value.trim()];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
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
|
+
|
|
125
|
+
function normalizeViewSort(value) {
|
|
126
|
+
if (typeof value === 'string' && value.trim()) {
|
|
127
|
+
return [{ direction: 'asc', property: value.trim() }];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!Array.isArray(value)) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return value
|
|
135
|
+
.map((entry) => {
|
|
136
|
+
if (typeof entry === 'string') {
|
|
137
|
+
return { direction: 'asc', property: entry };
|
|
138
|
+
}
|
|
139
|
+
if (!isPlainObject(entry)) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const property = entry.property ?? entry.id ?? entry.name ?? null;
|
|
144
|
+
if (typeof property !== 'string' || !property.trim()) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const direction = normalizeDirection(entry.direction ?? entry.order, 'asc');
|
|
149
|
+
return { direction, property: property.trim() };
|
|
150
|
+
})
|
|
151
|
+
.filter(Boolean);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeSummaries(value) {
|
|
155
|
+
if (!value) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (Array.isArray(value)) {
|
|
160
|
+
return value
|
|
161
|
+
.map((entry, index) => normalizeSummary(entry, index))
|
|
162
|
+
.filter(Boolean);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (isPlainObject(value)) {
|
|
166
|
+
const entries = [];
|
|
167
|
+
Object.entries(value).forEach(([property, propertySummaries]) => {
|
|
168
|
+
if (Array.isArray(propertySummaries)) {
|
|
169
|
+
propertySummaries.forEach((entry, index) => {
|
|
170
|
+
const normalized = normalizeSummary(entry, index, property);
|
|
171
|
+
if (normalized) {
|
|
172
|
+
entries.push(normalized);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const normalized = normalizeSummary(propertySummaries, entries.length, property);
|
|
179
|
+
if (normalized) {
|
|
180
|
+
entries.push(normalized);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return entries;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeSummary(entry, index, property = null) {
|
|
190
|
+
if (typeof entry === 'string') {
|
|
191
|
+
return {
|
|
192
|
+
id: `summary-${index}`,
|
|
193
|
+
label: capitalize(entry),
|
|
194
|
+
property,
|
|
195
|
+
type: entry.toLowerCase(),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!isPlainObject(entry)) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const summaryProperty = typeof (entry.property ?? property) === 'string'
|
|
204
|
+
? String(entry.property ?? property).trim()
|
|
205
|
+
: null;
|
|
206
|
+
const formula = typeof entry.formula === 'string' ? entry.formula : null;
|
|
207
|
+
const type = typeof entry.type === 'string' ? entry.type.toLowerCase() : (formula ? 'custom' : null);
|
|
208
|
+
if (!summaryProperty) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
formula,
|
|
214
|
+
id: `summary-${index}`,
|
|
215
|
+
label: typeof entry.name === 'string' ? entry.name : capitalize(type || 'Summary'),
|
|
216
|
+
property: summaryProperty,
|
|
217
|
+
type,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resolvePropertyLabel(propertyId, definition) {
|
|
222
|
+
const config = definition.properties[propertyId];
|
|
223
|
+
if (config?.displayName) {
|
|
224
|
+
return config.displayName;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (propertyId.startsWith('formula.')) {
|
|
228
|
+
return bareFormulaName(propertyId);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return propertyId.startsWith('file.')
|
|
232
|
+
? propertyId.slice(5)
|
|
233
|
+
: propertyId.replace(/^note\./u, '');
|
|
234
|
+
}
|
|
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
|
+
|
|
281
|
+
export function normalizeBaseDefinition(source = '') {
|
|
282
|
+
const raw = yaml.load(String(source ?? '')) ?? {};
|
|
283
|
+
const rawObject = isPlainObject(raw) ? raw : {};
|
|
284
|
+
const rawProperties = isPlainObject(rawObject.properties) ? rawObject.properties : {};
|
|
285
|
+
const formulas = normalizeFormulaEntries(rawObject.formulas, rawProperties);
|
|
286
|
+
const properties = Object.entries(rawProperties).reduce((acc, [id, config]) => {
|
|
287
|
+
const normalizedConfig = isPlainObject(config) ? { ...config } : {};
|
|
288
|
+
acc[id] = {
|
|
289
|
+
displayName: typeof normalizedConfig.displayName === 'string' ? normalizedConfig.displayName : null,
|
|
290
|
+
id,
|
|
291
|
+
raw: normalizedConfig,
|
|
292
|
+
};
|
|
293
|
+
return acc;
|
|
294
|
+
}, {});
|
|
295
|
+
|
|
296
|
+
const rawViews = Array.isArray(rawObject.views) ? rawObject.views : [];
|
|
297
|
+
const views = rawViews.map((rawView, index) => normalizeBaseView(rawView, index));
|
|
298
|
+
if (views.length === 0) {
|
|
299
|
+
views.push(normalizeBaseView({ name: 'Table', type: 'table' }, 0));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
filters: rawObject.filters ?? null,
|
|
304
|
+
formulas,
|
|
305
|
+
formulaLookup: createFormulaLookup(formulas),
|
|
306
|
+
properties,
|
|
307
|
+
raw: rawObject,
|
|
308
|
+
views,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function buildColumns(definition, view) {
|
|
313
|
+
const order = view.order.length > 0
|
|
314
|
+
? view.order
|
|
315
|
+
: ['file.name', ...new Set([
|
|
316
|
+
...Object.keys(definition.properties),
|
|
317
|
+
...Object.keys(definition.formulas),
|
|
318
|
+
])];
|
|
319
|
+
return order.map((propertyId) => ({
|
|
320
|
+
id: propertyId,
|
|
321
|
+
label: resolvePropertyLabel(propertyId, definition),
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function collectEvaluatedPropertyIds(columns, view) {
|
|
326
|
+
const propertyIds = new Set(columns.map((column) => column.id));
|
|
327
|
+
|
|
328
|
+
if (view.groupBy?.property) {
|
|
329
|
+
propertyIds.add(view.groupBy.property);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
view.sort.forEach((sortConfig) => {
|
|
333
|
+
if (sortConfig?.property) {
|
|
334
|
+
propertyIds.add(sortConfig.property);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
view.summaries.forEach((summary) => {
|
|
339
|
+
if (summary?.property) {
|
|
340
|
+
propertyIds.add(summary.property);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return [...propertyIds];
|
|
345
|
+
}
|
|
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
|
+
|
|
379
|
+
export function serializeBaseDefinition(definition) {
|
|
380
|
+
const raw = normalizeRawDefinitionForWrite(definition);
|
|
381
|
+
return `${yaml.dump(raw, { lineWidth: -1, noRefs: true }).trim()}\n`;
|
|
382
|
+
}
|