collabmd 0.1.31 → 0.1.32
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-Br4qyh3r.js} +1 -1
- package/dist/client/assets/drawioEditor-ClmnTx5J.js +2 -0
- package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Cf-iV5-3.js} +2 -2
- package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
- package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CXAxVhlw.js} +2 -2
- package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DoH_kMdu.js} +2 -2
- package/dist/client/assets/exportDocument-9wTrBZNS.css +1 -0
- package/dist/client/assets/{index-kPgEYvEX.js → index-C8QZYHvV.js} +2 -2
- package/dist/client/assets/index-CxSbUTs2.css +1 -0
- 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-BjHHMlv0.js +1249 -0
- package/dist/client/assets/vault-api-client-Bf9tB_GW.js +1 -0
- package/dist/client/assets/vault-api-client-D7Ck0CvR.css +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 +4 -1
- 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 +176 -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 +2 -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 +67 -0
- package/src/client/presentation/backlinks-panel.js +6 -9
- package/src/client/presentation/bases-preview-controller.js +26 -7
- 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/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 +103 -28
- 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/bases/base-definition.js +255 -0
- package/src/server/domain/bases/base-expression-runtime.js +1159 -0
- package/src/server/domain/bases/base-index-snapshot-store.js +418 -0
- package/src/server/domain/bases/base-query-results.js +205 -0
- package/src/server/domain/bases/base-query-service.js +68 -1965
- 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 +109 -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-Bia2hI25.js} +0 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { basename, dirname, extname } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { createWikiTargetIndex, resolveWikiTargetWithIndex } from '../../../domain/wiki-link-resolver.js';
|
|
4
|
+
import {
|
|
5
|
+
getVaultFileExtension,
|
|
6
|
+
isMarkdownFilePath,
|
|
7
|
+
stripVaultFileExtension,
|
|
8
|
+
} from '../../../domain/file-kind.js';
|
|
9
|
+
import { extractYamlFrontmatter } from '../../../domain/yaml-frontmatter.js';
|
|
10
|
+
import { mapWithConcurrency } from '../../shared/async-utils.js';
|
|
11
|
+
import { createLinkValue, dedupeLinkValues } from './base-expression-runtime.js';
|
|
12
|
+
|
|
13
|
+
const INTERNAL_LINK_RE = /(!)?\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]/g;
|
|
14
|
+
const INLINE_TAG_RE = /(^|[\s(])#([A-Za-z0-9/_-]+)/g;
|
|
15
|
+
const INDEX_READ_CONCURRENCY = 8;
|
|
16
|
+
|
|
17
|
+
function isPlainObject(value) {
|
|
18
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeTags(value) {
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
return value
|
|
24
|
+
.map((entry) => String(entry ?? '').trim().replace(/^#/u, ''))
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const normalized = String(value ?? '').trim().replace(/^#/u, '');
|
|
29
|
+
return normalized ? [normalized] : [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractInlineTags(markdownText = '') {
|
|
33
|
+
const tags = new Set();
|
|
34
|
+
let match;
|
|
35
|
+
while ((match = INLINE_TAG_RE.exec(String(markdownText ?? ''))) !== null) {
|
|
36
|
+
tags.add(match[2]);
|
|
37
|
+
}
|
|
38
|
+
return [...tags];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function compareVaultPaths(left = '', right = '') {
|
|
42
|
+
return String(left).localeCompare(String(right), undefined, { sensitivity: 'base' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function extractReferences(markdownText = '', wikiTargetIndex) {
|
|
46
|
+
const links = [];
|
|
47
|
+
const embeds = [];
|
|
48
|
+
let match;
|
|
49
|
+
while ((match = INTERNAL_LINK_RE.exec(String(markdownText ?? ''))) !== null) {
|
|
50
|
+
const isEmbed = Boolean(match[1]);
|
|
51
|
+
const rawTarget = String(match[2] ?? '').trim();
|
|
52
|
+
if (!rawTarget) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const resolvedPath = resolveWikiTargetWithIndex(rawTarget, wikiTargetIndex);
|
|
57
|
+
const linkValue = createLinkValue(resolvedPath || rawTarget, {
|
|
58
|
+
exists: Boolean(resolvedPath),
|
|
59
|
+
rawTarget,
|
|
60
|
+
});
|
|
61
|
+
links.push(linkValue);
|
|
62
|
+
if (isEmbed) {
|
|
63
|
+
embeds.push(linkValue);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { embeds, links };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isWorkspaceFileEntry(entry = {}) {
|
|
70
|
+
return entry?.nodeType === 'file' || entry?.type !== 'directory';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function listWorkspaceFilePaths(workspaceState = {}) {
|
|
74
|
+
if (Array.isArray(workspaceState?.filePaths)) {
|
|
75
|
+
return [...workspaceState.filePaths].sort(compareVaultPaths);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return Array.from(workspaceState?.entries?.values?.() ?? [])
|
|
79
|
+
.filter((entry) => isWorkspaceFileEntry(entry))
|
|
80
|
+
.map((entry) => entry.path)
|
|
81
|
+
.sort(compareVaultPaths);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeWikiTargetKey(target = '') {
|
|
85
|
+
const normalizedTarget = String(target ?? '').trim();
|
|
86
|
+
if (!normalizedTarget) {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return getVaultFileExtension(normalizedTarget)
|
|
91
|
+
? normalizedTarget
|
|
92
|
+
: `${normalizedTarget}.md`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function collectWikiTargetKeysForFilePath(filePath = '') {
|
|
96
|
+
const normalizedPath = String(filePath ?? '').trim();
|
|
97
|
+
if (!normalizedPath) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const segments = normalizedPath.split('/').filter(Boolean);
|
|
102
|
+
const keys = [];
|
|
103
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
104
|
+
keys.push(segments.slice(index).join('/'));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (isMarkdownFilePath(normalizedPath)) {
|
|
108
|
+
const rawSegments = normalizedPath.replace(/\.md$/i, '').split('/').filter(Boolean);
|
|
109
|
+
for (let index = 0; index < rawSegments.length; index += 1) {
|
|
110
|
+
keys.push(rawSegments.slice(index).join('/'));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return [...new Set(keys)];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export class BaseIndexSnapshotStore {
|
|
118
|
+
constructor({
|
|
119
|
+
vaultFileStore,
|
|
120
|
+
workspaceStateProvider = null,
|
|
121
|
+
workspaceStateSynchronizer = null,
|
|
122
|
+
}) {
|
|
123
|
+
this.vaultFileStore = vaultFileStore;
|
|
124
|
+
this.workspaceStateProvider = workspaceStateProvider;
|
|
125
|
+
this.workspaceStateSynchronizer = workspaceStateSynchronizer;
|
|
126
|
+
this.indexSnapshot = null;
|
|
127
|
+
this.lastWorkspaceState = null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async getWorkspaceState() {
|
|
131
|
+
const workspaceState = await this.workspaceStateProvider?.();
|
|
132
|
+
if (workspaceState) {
|
|
133
|
+
this.lastWorkspaceState = workspaceState;
|
|
134
|
+
return workspaceState;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (this.lastWorkspaceState) {
|
|
138
|
+
return this.lastWorkspaceState;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const scannedWorkspaceState = await this.vaultFileStore.scanWorkspaceState();
|
|
142
|
+
this.lastWorkspaceState = scannedWorkspaceState;
|
|
143
|
+
return scannedWorkspaceState;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
createSnapshotRow(filePath, workspaceState, wikiTargetIndex, markdownContent = null) {
|
|
147
|
+
const metadata = workspaceState.metadata.get(filePath) ?? null;
|
|
148
|
+
const frontmatter = markdownContent ? extractYamlFrontmatter(markdownContent) : null;
|
|
149
|
+
const noteProperties = isPlainObject(frontmatter?.data) ? frontmatter.data : {};
|
|
150
|
+
const bodyMarkdown = frontmatter?.bodyMarkdown ?? markdownContent ?? '';
|
|
151
|
+
const references = extractReferences(bodyMarkdown, wikiTargetIndex);
|
|
152
|
+
const tags = [...new Set([
|
|
153
|
+
...normalizeTags(noteProperties.tags),
|
|
154
|
+
...extractInlineTags(bodyMarkdown),
|
|
155
|
+
])];
|
|
156
|
+
const fileValue = {
|
|
157
|
+
__baseType: 'file',
|
|
158
|
+
backlinks: [],
|
|
159
|
+
basename: stripVaultFileExtension(basename(filePath)),
|
|
160
|
+
ctime: Number.isFinite(metadata?.ctimeMs) ? new Date(metadata.ctimeMs) : null,
|
|
161
|
+
embeds: references.embeds,
|
|
162
|
+
ext: extname(filePath).replace(/^\./u, ''),
|
|
163
|
+
folder: dirname(filePath) === '.' ? '' : dirname(filePath).replace(/\\/g, '/'),
|
|
164
|
+
links: references.links,
|
|
165
|
+
mtime: Number.isFinite(metadata?.mtimeMs) ? new Date(metadata.mtimeMs) : null,
|
|
166
|
+
name: basename(filePath),
|
|
167
|
+
path: filePath,
|
|
168
|
+
properties: noteProperties,
|
|
169
|
+
size: Number(metadata?.size ?? 0) || 0,
|
|
170
|
+
tags,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
forwardLinks: new Set(
|
|
175
|
+
references.links
|
|
176
|
+
.map((link) => link?.path)
|
|
177
|
+
.filter((targetPath) => targetPath && targetPath !== filePath),
|
|
178
|
+
),
|
|
179
|
+
row: {
|
|
180
|
+
file: fileValue,
|
|
181
|
+
noteProperties,
|
|
182
|
+
path: filePath,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
rebuildBacklinks(snapshot) {
|
|
188
|
+
const backlinksByTarget = new Map(snapshot.filePaths.map((filePath) => [filePath, []]));
|
|
189
|
+
|
|
190
|
+
snapshot.rowsByPath.forEach((row, filePath) => {
|
|
191
|
+
const forwardLinks = snapshot.forwardLinksByPath.get(filePath) ?? new Set();
|
|
192
|
+
forwardLinks.forEach((targetPath) => {
|
|
193
|
+
if (!backlinksByTarget.has(targetPath) || targetPath === row.file.path) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
backlinksByTarget.get(targetPath).push(createLinkValue(row.file.path, {
|
|
198
|
+
display: basename(row.file.path),
|
|
199
|
+
exists: true,
|
|
200
|
+
}));
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
snapshot.backlinksByPath = backlinksByTarget;
|
|
205
|
+
snapshot.rowsByPath.forEach((row, filePath) => {
|
|
206
|
+
row.file.backlinks = dedupeLinkValues(backlinksByTarget.get(filePath) ?? []);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async buildIndexSnapshot(workspaceState = null) {
|
|
211
|
+
const resolvedWorkspaceState = workspaceState ?? await this.getWorkspaceState();
|
|
212
|
+
const fileEntries = listWorkspaceFilePaths(resolvedWorkspaceState);
|
|
213
|
+
const wikiTargetIndex = createWikiTargetIndex(fileEntries);
|
|
214
|
+
const markdownContents = new Map();
|
|
215
|
+
|
|
216
|
+
await mapWithConcurrency(resolvedWorkspaceState.markdownPaths ?? [], INDEX_READ_CONCURRENCY, async (filePath) => {
|
|
217
|
+
markdownContents.set(filePath, await this.vaultFileStore.readMarkdownFile(filePath));
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const rowsByPath = new Map();
|
|
221
|
+
const forwardLinksByPath = new Map();
|
|
222
|
+
fileEntries.forEach((filePath) => {
|
|
223
|
+
const markdownContent = markdownContents.get(filePath) ?? null;
|
|
224
|
+
const rowRecord = this.createSnapshotRow(filePath, resolvedWorkspaceState, wikiTargetIndex, markdownContent);
|
|
225
|
+
rowsByPath.set(filePath, rowRecord.row);
|
|
226
|
+
forwardLinksByPath.set(filePath, rowRecord.forwardLinks);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const snapshot = {
|
|
230
|
+
backlinksByPath: new Map(),
|
|
231
|
+
filePaths: fileEntries,
|
|
232
|
+
forwardLinksByPath,
|
|
233
|
+
rowsByPath,
|
|
234
|
+
scannedAt: resolvedWorkspaceState.scannedAt,
|
|
235
|
+
wikiTargetIndex,
|
|
236
|
+
workspaceState: resolvedWorkspaceState,
|
|
237
|
+
};
|
|
238
|
+
this.rebuildBacklinks(snapshot);
|
|
239
|
+
this.indexSnapshot = snapshot;
|
|
240
|
+
this.lastWorkspaceState = resolvedWorkspaceState;
|
|
241
|
+
return snapshot;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async synchronizeWorkspaceState() {
|
|
245
|
+
await this.workspaceStateSynchronizer?.();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
removeSnapshotPath(snapshot, filePath) {
|
|
249
|
+
snapshot.rowsByPath.delete(filePath);
|
|
250
|
+
snapshot.forwardLinksByPath.delete(filePath);
|
|
251
|
+
snapshot.backlinksByPath.delete(filePath);
|
|
252
|
+
snapshot.filePaths = snapshot.filePaths.filter((candidatePath) => candidatePath !== filePath);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
upsertSnapshotPath(snapshot, filePath) {
|
|
256
|
+
if (snapshot.filePaths.includes(filePath)) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
snapshot.filePaths.push(filePath);
|
|
261
|
+
snapshot.filePaths.sort(compareVaultPaths);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
collectImpactedSourcesForMembershipChanges(snapshot, pathValues = []) {
|
|
265
|
+
const affectedTargetKeys = new Set();
|
|
266
|
+
pathValues.forEach((pathValue) => {
|
|
267
|
+
collectWikiTargetKeysForFilePath(pathValue).forEach((targetKey) => {
|
|
268
|
+
affectedTargetKeys.add(targetKey);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (affectedTargetKeys.size === 0) {
|
|
273
|
+
return new Set();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const impactedPaths = new Set();
|
|
277
|
+
snapshot.rowsByPath.forEach((row, filePath) => {
|
|
278
|
+
const links = row?.file?.links ?? [];
|
|
279
|
+
if (links.some((link) => affectedTargetKeys.has(normalizeWikiTargetKey(link?.rawTarget)))) {
|
|
280
|
+
impactedPaths.add(filePath);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return impactedPaths;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async refreshSnapshotRows(snapshot, workspaceState, pathValues = []) {
|
|
288
|
+
const filePathsToRefresh = Array.from(new Set(pathValues))
|
|
289
|
+
.filter((pathValue) => {
|
|
290
|
+
const entry = workspaceState?.entries?.get(pathValue);
|
|
291
|
+
return pathValue && isWorkspaceFileEntry(entry);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
await mapWithConcurrency(filePathsToRefresh, INDEX_READ_CONCURRENCY, async (filePath) => {
|
|
295
|
+
const markdownContent = isMarkdownFilePath(filePath)
|
|
296
|
+
? await this.vaultFileStore.readMarkdownFile(filePath)
|
|
297
|
+
: null;
|
|
298
|
+
const rowRecord = this.createSnapshotRow(filePath, workspaceState, snapshot.wikiTargetIndex, markdownContent);
|
|
299
|
+
snapshot.rowsByPath.set(filePath, rowRecord.row);
|
|
300
|
+
snapshot.forwardLinksByPath.set(filePath, rowRecord.forwardLinks);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async ensureIndexSnapshot({ basePath = '', sourcePath = '' } = {}) {
|
|
305
|
+
await this.synchronizeWorkspaceState();
|
|
306
|
+
let workspaceState = await this.getWorkspaceState();
|
|
307
|
+
const requiresFreshScan = [basePath, sourcePath]
|
|
308
|
+
.filter(Boolean)
|
|
309
|
+
.some((pathValue) => !workspaceState?.entries?.has?.(pathValue));
|
|
310
|
+
if (requiresFreshScan) {
|
|
311
|
+
workspaceState = await this.vaultFileStore.scanWorkspaceState();
|
|
312
|
+
this.lastWorkspaceState = workspaceState;
|
|
313
|
+
if (this.indexSnapshot?.scannedAt !== workspaceState?.scannedAt) {
|
|
314
|
+
this.indexSnapshot = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (this.indexSnapshot?.scannedAt === workspaceState?.scannedAt) {
|
|
319
|
+
return this.indexSnapshot;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return this.buildIndexSnapshot(workspaceState);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async initializeFromWorkspaceState(workspaceState = null) {
|
|
326
|
+
this.lastWorkspaceState = workspaceState ?? null;
|
|
327
|
+
if (this.indexSnapshot && workspaceState && this.indexSnapshot.scannedAt !== workspaceState.scannedAt) {
|
|
328
|
+
this.indexSnapshot = null;
|
|
329
|
+
}
|
|
330
|
+
return this.indexSnapshot;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async applyWorkspaceChange(workspaceChange = {}, {
|
|
334
|
+
previousState = null,
|
|
335
|
+
nextState = null,
|
|
336
|
+
} = {}) {
|
|
337
|
+
this.lastWorkspaceState = nextState ?? this.lastWorkspaceState;
|
|
338
|
+
if (!this.indexSnapshot) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!previousState || this.indexSnapshot.scannedAt !== previousState.scannedAt) {
|
|
343
|
+
this.indexSnapshot = null;
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const changedPaths = Array.from(new Set(workspaceChange.changedPaths ?? []));
|
|
348
|
+
const deletedFilePaths = (workspaceChange.deletedPaths ?? []).filter((pathValue) => (
|
|
349
|
+
isWorkspaceFileEntry(previousState?.entries?.get(pathValue))
|
|
350
|
+
));
|
|
351
|
+
const renamedFileEntries = (workspaceChange.renamedPaths ?? []).filter((entry) => (
|
|
352
|
+
isWorkspaceFileEntry(previousState?.entries?.get(entry?.oldPath))
|
|
353
|
+
|| isWorkspaceFileEntry(nextState?.entries?.get(entry?.newPath))
|
|
354
|
+
));
|
|
355
|
+
const createdFilePaths = changedPaths.filter((pathValue) => (
|
|
356
|
+
isWorkspaceFileEntry(nextState?.entries?.get(pathValue))
|
|
357
|
+
&& !isWorkspaceFileEntry(previousState?.entries?.get(pathValue))
|
|
358
|
+
));
|
|
359
|
+
const removedChangedFilePaths = changedPaths.filter((pathValue) => (
|
|
360
|
+
!isWorkspaceFileEntry(nextState?.entries?.get(pathValue))
|
|
361
|
+
&& isWorkspaceFileEntry(previousState?.entries?.get(pathValue))
|
|
362
|
+
));
|
|
363
|
+
const membershipChanged = deletedFilePaths.length > 0
|
|
364
|
+
|| renamedFileEntries.length > 0
|
|
365
|
+
|| createdFilePaths.length > 0
|
|
366
|
+
|| removedChangedFilePaths.length > 0;
|
|
367
|
+
|
|
368
|
+
const markdownPathsToRefresh = changedPaths.filter((pathValue) => (
|
|
369
|
+
isWorkspaceFileEntry(previousState?.entries?.get(pathValue))
|
|
370
|
+
&& isWorkspaceFileEntry(nextState?.entries?.get(pathValue))
|
|
371
|
+
));
|
|
372
|
+
|
|
373
|
+
if (!membershipChanged && markdownPathsToRefresh.length === 0) {
|
|
374
|
+
this.indexSnapshot.workspaceState = nextState ?? this.indexSnapshot.workspaceState;
|
|
375
|
+
this.indexSnapshot.scannedAt = nextState?.scannedAt ?? this.indexSnapshot.scannedAt;
|
|
376
|
+
return this.indexSnapshot;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const snapshot = this.indexSnapshot;
|
|
380
|
+
if (membershipChanged) {
|
|
381
|
+
const membershipAffectedPaths = [
|
|
382
|
+
...deletedFilePaths,
|
|
383
|
+
...removedChangedFilePaths,
|
|
384
|
+
...createdFilePaths,
|
|
385
|
+
...renamedFileEntries.flatMap((entry) => [entry.oldPath, entry.newPath]),
|
|
386
|
+
];
|
|
387
|
+
const impactedSourcePaths = this.collectImpactedSourcesForMembershipChanges(snapshot, membershipAffectedPaths);
|
|
388
|
+
|
|
389
|
+
[...deletedFilePaths, ...removedChangedFilePaths].forEach((pathValue) => {
|
|
390
|
+
this.removeSnapshotPath(snapshot, pathValue);
|
|
391
|
+
});
|
|
392
|
+
renamedFileEntries.forEach((entry) => {
|
|
393
|
+
this.removeSnapshotPath(snapshot, entry.oldPath);
|
|
394
|
+
});
|
|
395
|
+
[...createdFilePaths, ...renamedFileEntries.map((entry) => entry.newPath)].forEach((pathValue) => {
|
|
396
|
+
const nextEntry = nextState?.entries?.get(pathValue);
|
|
397
|
+
if (isWorkspaceFileEntry(nextEntry)) {
|
|
398
|
+
this.upsertSnapshotPath(snapshot, pathValue);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
snapshot.wikiTargetIndex = createWikiTargetIndex(snapshot.filePaths);
|
|
403
|
+
await this.refreshSnapshotRows(snapshot, nextState, [
|
|
404
|
+
...markdownPathsToRefresh,
|
|
405
|
+
...createdFilePaths,
|
|
406
|
+
...renamedFileEntries.map((entry) => entry.newPath),
|
|
407
|
+
...impactedSourcePaths,
|
|
408
|
+
]);
|
|
409
|
+
} else {
|
|
410
|
+
await this.refreshSnapshotRows(snapshot, nextState, markdownPathsToRefresh);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
snapshot.workspaceState = nextState;
|
|
414
|
+
snapshot.scannedAt = nextState?.scannedAt ?? snapshot.scannedAt;
|
|
415
|
+
this.rebuildBacklinks(snapshot);
|
|
416
|
+
return snapshot;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compareValues,
|
|
3
|
+
createEvaluationRootContext,
|
|
4
|
+
evaluateExpression,
|
|
5
|
+
serializeBaseValue,
|
|
6
|
+
toDisplayText,
|
|
7
|
+
valuesEqual,
|
|
8
|
+
} from './base-expression-runtime.js';
|
|
9
|
+
|
|
10
|
+
function summarizeValues(values = [], summary, definition, snapshot, thisFile) {
|
|
11
|
+
const meaningfulValues = values.filter((value) => value != null && value !== '');
|
|
12
|
+
switch (summary.type) {
|
|
13
|
+
case 'average': {
|
|
14
|
+
const numbers = meaningfulValues.map(Number).filter(Number.isFinite);
|
|
15
|
+
return numbers.length > 0 ? numbers.reduce((sum, value) => sum + value, 0) / numbers.length : null;
|
|
16
|
+
}
|
|
17
|
+
case 'checked':
|
|
18
|
+
return values.filter(Boolean).length;
|
|
19
|
+
case 'custom': {
|
|
20
|
+
const context = createEvaluationRootContext({
|
|
21
|
+
astCache: new Map(),
|
|
22
|
+
currentRow: {
|
|
23
|
+
file: thisFile,
|
|
24
|
+
noteProperties: thisFile?.properties ?? {},
|
|
25
|
+
},
|
|
26
|
+
definition,
|
|
27
|
+
identifierResolver: (name) => (name === 'values' ? values : undefined),
|
|
28
|
+
snapshot,
|
|
29
|
+
thisFile,
|
|
30
|
+
});
|
|
31
|
+
return evaluateExpression(summary.formula, context);
|
|
32
|
+
}
|
|
33
|
+
case 'earliest':
|
|
34
|
+
return meaningfulValues.reduce((acc, value) => (acc == null || compareValues(value, acc) < 0 ? value : acc), null);
|
|
35
|
+
case 'empty':
|
|
36
|
+
return values.filter((value) => value == null || value === '').length;
|
|
37
|
+
case 'filled':
|
|
38
|
+
return meaningfulValues.length;
|
|
39
|
+
case 'latest':
|
|
40
|
+
return meaningfulValues.reduce((acc, value) => (acc == null || compareValues(value, acc) > 0 ? value : acc), null);
|
|
41
|
+
case 'max':
|
|
42
|
+
return meaningfulValues.reduce((acc, value) => (acc == null || compareValues(value, acc) > 0 ? value : acc), null);
|
|
43
|
+
case 'median': {
|
|
44
|
+
const numbers = meaningfulValues.map(Number).filter(Number.isFinite).sort((left, right) => left - right);
|
|
45
|
+
if (numbers.length === 0) return null;
|
|
46
|
+
const middle = Math.floor(numbers.length / 2);
|
|
47
|
+
return numbers.length % 2 === 0 ? (numbers[middle - 1] + numbers[middle]) / 2 : numbers[middle];
|
|
48
|
+
}
|
|
49
|
+
case 'min':
|
|
50
|
+
return meaningfulValues.reduce((acc, value) => (acc == null || compareValues(value, acc) < 0 ? value : acc), null);
|
|
51
|
+
case 'range': {
|
|
52
|
+
const sorted = meaningfulValues.slice().sort(compareValues);
|
|
53
|
+
if (sorted.length === 0) return null;
|
|
54
|
+
const first = sorted[0];
|
|
55
|
+
const last = sorted[sorted.length - 1];
|
|
56
|
+
if (first instanceof Date && last instanceof Date) {
|
|
57
|
+
return last.getTime() - first.getTime();
|
|
58
|
+
}
|
|
59
|
+
return Number(last ?? 0) - Number(first ?? 0);
|
|
60
|
+
}
|
|
61
|
+
case 'stddev': {
|
|
62
|
+
const numbers = meaningfulValues.map(Number).filter(Number.isFinite);
|
|
63
|
+
if (numbers.length === 0) return null;
|
|
64
|
+
const average = numbers.reduce((sum, value) => sum + value, 0) / numbers.length;
|
|
65
|
+
const variance = numbers.reduce((sum, value) => sum + ((value - average) ** 2), 0) / numbers.length;
|
|
66
|
+
return Math.sqrt(variance);
|
|
67
|
+
}
|
|
68
|
+
case 'sum':
|
|
69
|
+
return meaningfulValues.map(Number).filter(Number.isFinite).reduce((sum, value) => sum + value, 0);
|
|
70
|
+
case 'unchecked':
|
|
71
|
+
return values.filter((value) => value === false || value == null).length;
|
|
72
|
+
case 'unique': {
|
|
73
|
+
const uniqueValues = [];
|
|
74
|
+
meaningfulValues.forEach((value) => {
|
|
75
|
+
if (!uniqueValues.some((entry) => valuesEqual(entry, value))) {
|
|
76
|
+
uniqueValues.push(value);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return uniqueValues.length;
|
|
80
|
+
}
|
|
81
|
+
default:
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createSummaryPayload(columns, rows, summaries, definition, snapshot, thisFile) {
|
|
87
|
+
return summaries.map((summary) => {
|
|
88
|
+
const column = columns.find((entry) => entry.id === summary.property);
|
|
89
|
+
const values = rows.map((row) => row.rawCells[summary.property]);
|
|
90
|
+
const value = summarizeValues(values, summary, definition, snapshot, thisFile);
|
|
91
|
+
return {
|
|
92
|
+
id: summary.id,
|
|
93
|
+
label: summary.label,
|
|
94
|
+
property: summary.property,
|
|
95
|
+
propertyLabel: column?.label ?? summary.property,
|
|
96
|
+
value: serializeBaseValue(value, {
|
|
97
|
+
snapshot,
|
|
98
|
+
sourcePath: thisFile?.path ?? '',
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createCsv(rows, columns) {
|
|
105
|
+
const escapeCell = (value) => {
|
|
106
|
+
const text = String(value ?? '');
|
|
107
|
+
if (/[,"\n]/u.test(text)) {
|
|
108
|
+
return `"${text.replace(/"/g, '""')}"`;
|
|
109
|
+
}
|
|
110
|
+
return text;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const lines = [
|
|
114
|
+
columns.map((column) => escapeCell(column.label)).join(','),
|
|
115
|
+
...rows.map((row) => columns.map((column) => escapeCell(toDisplayText(row.rawCells[column.id]))).join(',')),
|
|
116
|
+
];
|
|
117
|
+
return `${lines.join('\n')}\n`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function rowMatchesSearch(row, columns, query) {
|
|
121
|
+
const normalizedQuery = String(query ?? '').trim().toLowerCase();
|
|
122
|
+
if (!normalizedQuery) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const haystack = [
|
|
127
|
+
row.file.path,
|
|
128
|
+
...columns.map((column) => toDisplayText(row.rawCells[column.id])),
|
|
129
|
+
].join('\n').toLowerCase();
|
|
130
|
+
return haystack.includes(normalizedQuery);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function buildQueryResultPayload({
|
|
134
|
+
activeView,
|
|
135
|
+
columns,
|
|
136
|
+
definition,
|
|
137
|
+
rows,
|
|
138
|
+
snapshot,
|
|
139
|
+
thisFile,
|
|
140
|
+
}) {
|
|
141
|
+
const groupsByKey = new Map();
|
|
142
|
+
if (activeView.groupBy) {
|
|
143
|
+
rows.forEach((row) => {
|
|
144
|
+
const groupValue = row.rawCells[activeView.groupBy];
|
|
145
|
+
const groupKey = toDisplayText(groupValue) || 'Empty';
|
|
146
|
+
const entry = groupsByKey.get(groupKey) ?? {
|
|
147
|
+
key: groupKey,
|
|
148
|
+
label: groupKey,
|
|
149
|
+
rows: [],
|
|
150
|
+
value: serializeBaseValue(groupValue, {
|
|
151
|
+
rowFilePath: row.file.path,
|
|
152
|
+
snapshot,
|
|
153
|
+
sourcePath: thisFile?.path ?? '',
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
entry.rows.push(row);
|
|
157
|
+
groupsByKey.set(groupKey, entry);
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
groupsByKey.set('All', {
|
|
161
|
+
key: 'All',
|
|
162
|
+
label: 'All',
|
|
163
|
+
rows,
|
|
164
|
+
value: serializeBaseValue(null),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const serializedRows = rows.map((row) => ({
|
|
169
|
+
cells: columns.reduce((acc, column) => {
|
|
170
|
+
acc[column.id] = serializeBaseValue(row.rawCells[column.id], {
|
|
171
|
+
rowFilePath: row.file.path,
|
|
172
|
+
snapshot,
|
|
173
|
+
sourcePath: thisFile?.path ?? '',
|
|
174
|
+
});
|
|
175
|
+
return acc;
|
|
176
|
+
}, {}),
|
|
177
|
+
file: serializeBaseValue(row.file, {
|
|
178
|
+
rowFilePath: row.file.path,
|
|
179
|
+
snapshot,
|
|
180
|
+
sourcePath: thisFile?.path ?? '',
|
|
181
|
+
}),
|
|
182
|
+
path: row.file.path,
|
|
183
|
+
}));
|
|
184
|
+
const rowsByPath = new Map(serializedRows.map((row) => [row.path, row]));
|
|
185
|
+
|
|
186
|
+
const groups = Array.from(groupsByKey.values()).map((group) => ({
|
|
187
|
+
key: group.key,
|
|
188
|
+
label: group.label,
|
|
189
|
+
summaries: createSummaryPayload(columns, group.rows, activeView.summaries, definition, snapshot, thisFile),
|
|
190
|
+
value: group.value,
|
|
191
|
+
rows: group.rows.map((row) => rowsByPath.get(row.file.path)),
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
csv: createCsv(rows, columns),
|
|
196
|
+
groups,
|
|
197
|
+
rows: serializedRows,
|
|
198
|
+
summaries: createSummaryPayload(columns, rows, activeView.summaries, definition, snapshot, thisFile),
|
|
199
|
+
thisFile: serializeBaseValue(thisFile, {
|
|
200
|
+
snapshot,
|
|
201
|
+
sourcePath: thisFile?.path ?? '',
|
|
202
|
+
}),
|
|
203
|
+
totalRows: rows.length,
|
|
204
|
+
};
|
|
205
|
+
}
|