@3sln/trove 0.0.2
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/README.md +1227 -0
- package/package.json +75 -0
- package/packages/core/src/collections/index.js +249 -0
- package/packages/core/src/errors.js +186 -0
- package/packages/core/src/identity/discovery.js +210 -0
- package/packages/core/src/identity/index.js +188 -0
- package/packages/core/src/identity/jwt.js +199 -0
- package/packages/core/src/index.js +104 -0
- package/packages/core/src/indexers/contribution.js +115 -0
- package/packages/core/src/indexers/registry.js +162 -0
- package/packages/core/src/indexing.js +340 -0
- package/packages/core/src/issues.js +150 -0
- package/packages/core/src/kv.js +0 -0
- package/packages/core/src/links.js +141 -0
- package/packages/core/src/metadata/cursor.js +73 -0
- package/packages/core/src/metadata/interface.js +244 -0
- package/packages/core/src/metadata/memory.js +270 -0
- package/packages/core/src/metadata/sqlite.js +412 -0
- package/packages/core/src/notifications/index.js +139 -0
- package/packages/core/src/notifications/webpush.js +217 -0
- package/packages/core/src/plugins/contributions.js +177 -0
- package/packages/core/src/plugins/identity.js +98 -0
- package/packages/core/src/plugins/index.js +225 -0
- package/packages/core/src/plugins/indexers.js +142 -0
- package/packages/core/src/plugins/installStore.js +134 -0
- package/packages/core/src/plugins/package.js +102 -0
- package/packages/core/src/plugins/packageStore.js +61 -0
- package/packages/core/src/plugins/runtime.js +101 -0
- package/packages/core/src/plugins/sql.js +52 -0
- package/packages/core/src/retry.js +74 -0
- package/packages/core/src/scan.js +302 -0
- package/packages/core/src/search/embeddings.js +128 -0
- package/packages/core/src/search/index.js +200 -0
- package/packages/core/src/search/keywordStore.js +107 -0
- package/packages/core/src/search/sqliteStores.js +455 -0
- package/packages/core/src/search/tagMatch.js +59 -0
- package/packages/core/src/search/transformer.js +195 -0
- package/packages/core/src/search/vectorStore.js +274 -0
- package/packages/core/src/search/vectorize.js +249 -0
- package/packages/core/src/sidecar/document.js +213 -0
- package/packages/core/src/sidecar/index.js +174 -0
- package/packages/core/src/sidecar/manager.js +239 -0
- package/packages/core/src/sidecar/store.js +46 -0
- package/packages/core/src/signedUrls.js +170 -0
- package/packages/core/src/sqlite-d1.js +162 -0
- package/packages/core/src/sqlite-driver.js +42 -0
- package/packages/core/src/sqlite.js +162 -0
- package/packages/core/src/storage/filesystem.js +283 -0
- package/packages/core/src/storage/interface.js +222 -0
- package/packages/core/src/storage/memory.js +113 -0
- package/packages/core/src/storage/prefixed.js +75 -0
- package/packages/core/src/storage/s3.js +316 -0
- package/packages/core/src/storage/s3sigv4.js +185 -0
- package/packages/core/src/tasks.js +228 -0
- package/packages/core/src/uploads.js +386 -0
- package/packages/core/src/util.js +125 -0
- package/packages/core/src/vfs.js +666 -0
- package/packages/plugin-sdk/src/browser.js +316 -0
- package/packages/plugin-sdk/src/index.js +32 -0
- package/packages/plugin-sdk/src/protocol.js +59 -0
- package/packages/plugin-sdk/src/rpc.js +95 -0
- package/packages/server/src/adapters/bun.js +78 -0
- package/packages/server/src/adapters/node.js +115 -0
- package/packages/server/src/adapters/staticAssets.js +123 -0
- package/packages/server/src/adapters/webDist.js +70 -0
- package/packages/server/src/adapters/worker-tasks.js +206 -0
- package/packages/server/src/adapters/worker.js +159 -0
- package/packages/server/src/cachePolicy.js +34 -0
- package/packages/server/src/engine/README.md +88 -0
- package/packages/server/src/engine/actions/scanCollection.js +114 -0
- package/packages/server/src/engine/index.js +95 -0
- package/packages/server/src/engine/lazy.js +25 -0
- package/packages/server/src/engine/providers/access.js +363 -0
- package/packages/server/src/engine/providers/core.js +405 -0
- package/packages/server/src/engine/providers/scan.js +67 -0
- package/packages/server/src/index.js +698 -0
- package/packages/server/src/manifest.js +98 -0
- package/packages/server/src/mcp/auth.js +40 -0
- package/packages/server/src/mcp/index.js +213 -0
- package/packages/server/src/mcp/protocol.js +181 -0
- package/packages/server/src/mcp/tools.js +351 -0
- package/packages/server/src/router.js +229 -0
- package/packages/server/src/routes.js +1066 -0
- package/packages/server/src/scope.js +43 -0
- package/packages/web/dist/assets/chunk-4xqbzebh.js +5 -0
- package/packages/web/dist/assets/chunk-4xqbzebh.js.map +9 -0
- package/packages/web/dist/assets/chunk-h05bxfbs.js +5 -0
- package/packages/web/dist/assets/chunk-h05bxfbs.js.map +10 -0
- package/packages/web/dist/assets/main-4cxs7prw.js +356 -0
- package/packages/web/dist/assets/main-4cxs7prw.js.map +103 -0
- package/packages/web/dist/assets/styles-kcx1x337.css +1 -0
- package/packages/web/dist/icon.svg +11 -0
- package/packages/web/dist/index.html +16 -0
- package/packages/web/dist/sql-wasm.wasm +0 -0
- package/packages/web/dist/sw.js +186 -0
- package/packages/web/src/bl/actions.js +410 -0
- package/packages/web/src/bl/activity.js +306 -0
- package/packages/web/src/bl/commands.js +274 -0
- package/packages/web/src/bl/fileType.js +49 -0
- package/packages/web/src/bl/index.js +70 -0
- package/packages/web/src/bl/links.js +54 -0
- package/packages/web/src/bl/offline.js +268 -0
- package/packages/web/src/bl/openers.js +71 -0
- package/packages/web/src/bl/pluginInstall.js +59 -0
- package/packages/web/src/bl/services.js +143 -0
- package/packages/web/src/bl/social.js +234 -0
- package/packages/web/src/bl/tagQuery.js +44 -0
- package/packages/web/src/main.js +10 -0
- package/packages/web/src/platform/api.js +529 -0
- package/packages/web/src/platform/commands.js +89 -0
- package/packages/web/src/platform/context.js +77 -0
- package/packages/web/src/platform/contributions.js +156 -0
- package/packages/web/src/platform/index.js +150 -0
- package/packages/web/src/platform/keybindings.js +199 -0
- package/packages/web/src/platform/mediaUrls.js +137 -0
- package/packages/web/src/platform/navigation.js +131 -0
- package/packages/web/src/platform/notifications.js +50 -0
- package/packages/web/src/platform/overlay.js +81 -0
- package/packages/web/src/platform/pluginClientDb.js +132 -0
- package/packages/web/src/platform/pluginDock.js +141 -0
- package/packages/web/src/platform/pluginFrames.js +194 -0
- package/packages/web/src/platform/pluginHost.js +648 -0
- package/packages/web/src/platform/pluginMedia.js +62 -0
- package/packages/web/src/platform/pluginModules.js +90 -0
- package/packages/web/src/platform/pluginNet.js +71 -0
- package/packages/web/src/platform/pluginPackage.js +247 -0
- package/packages/web/src/platform/pluginRpc.js +377 -0
- package/packages/web/src/platform/pluginSigning.js +168 -0
- package/packages/web/src/platform/pluginStore.js +67 -0
- package/packages/web/src/platform/settings.js +101 -0
- package/packages/web/src/platform/spatialNav.js +286 -0
- package/packages/web/src/platform/viewport.js +123 -0
- package/packages/web/src/platform/voice.js +133 -0
- package/packages/web/src/platform/voiceSearch.js +155 -0
- package/packages/web/src/platform/whenclause.js +162 -0
- package/packages/web/src/platform/workbench.js +156 -0
- package/packages/web/src/runtime.js +73 -0
- package/packages/web/src/styles.css +1382 -0
- package/packages/web/src/ui/components/activityBar.js +35 -0
- package/packages/web/src/ui/components/activityPanel.js +132 -0
- package/packages/web/src/ui/components/commandPalette.js +154 -0
- package/packages/web/src/ui/components/editorArea.js +75 -0
- package/packages/web/src/ui/components/launcher.js +392 -0
- package/packages/web/src/ui/components/openers/index.js +212 -0
- package/packages/web/src/ui/components/openers/markdown.js +222 -0
- package/packages/web/src/ui/components/overlays.js +255 -0
- package/packages/web/src/ui/components/phoneChrome.js +188 -0
- package/packages/web/src/ui/components/pluginReview.js +151 -0
- package/packages/web/src/ui/components/pluginsView.js +120 -0
- package/packages/web/src/ui/components/settingsView.js +258 -0
- package/packages/web/src/ui/components/social.js +290 -0
- package/packages/web/src/ui/components/statusBar.js +198 -0
- package/packages/web/src/ui/components/views/grid.js +115 -0
- package/packages/web/src/ui/components/views/index.js +155 -0
- package/packages/web/src/ui/components/views/list.js +50 -0
- package/packages/web/src/ui/components/views/parts.js +58 -0
- package/packages/web/src/ui/compositions/workbench.js +125 -0
- package/packages/web/src/ui/format.js +33 -0
- package/packages/web/src/ui/icon.js +81 -0
- package/packages/web/src/ui/media.js +114 -0
- package/packages/web/src/ui/sanitize.js +86 -0
- package/packages/web/src/workbench.js +205 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// SocialService — the client side of identity, conversations, tags, and mention
|
|
2
|
+
// notifications. Reactive (the workbench `watch`es it): it holds the current
|
|
3
|
+
// principal, the notification inbox (polled + refreshed on demand), and the
|
|
4
|
+
// active file's sidecar (tags + threaded comments). It also owns the web-push
|
|
5
|
+
// subscription flow (register the service worker, subscribe with the server's
|
|
6
|
+
// VAPID key). All mutations optimistically reload the affected sidecar.
|
|
7
|
+
|
|
8
|
+
import { cell } from '../runtime.js';
|
|
9
|
+
|
|
10
|
+
export class SocialService {
|
|
11
|
+
constructor(platform) {
|
|
12
|
+
this.platform = platform;
|
|
13
|
+
this.api = platform.api;
|
|
14
|
+
this.state = {
|
|
15
|
+
me: null,
|
|
16
|
+
notifications: { items: [], unread: 0 },
|
|
17
|
+
inboxOpen: false,
|
|
18
|
+
pushSupported: 'serviceWorker' in navigator && 'PushManager' in window,
|
|
19
|
+
pushEnabled: false,
|
|
20
|
+
sidecar: null, // { nodeId, tags, comments, subscribers, loading, error }
|
|
21
|
+
backlinks: null, // { nodeId, items, loading, error } — what links to the open item
|
|
22
|
+
posting: false,
|
|
23
|
+
replyTo: null, // { id, author } when composing a reply
|
|
24
|
+
};
|
|
25
|
+
this.cell = cell(this.state);
|
|
26
|
+
this._pollTimer = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
observe() {
|
|
30
|
+
return this.cell;
|
|
31
|
+
}
|
|
32
|
+
#set(patch) {
|
|
33
|
+
this.state = { ...this.state, ...patch };
|
|
34
|
+
this.cell.setValue(this.state);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async init() {
|
|
38
|
+
try {
|
|
39
|
+
const me = await this.api.me();
|
|
40
|
+
this.#set({ me: me.principal, admin: !!me.admin });
|
|
41
|
+
} catch { /* anonymous / offline */ }
|
|
42
|
+
await this.refreshNotifications();
|
|
43
|
+
// Light polling; the service worker also nudges on push.
|
|
44
|
+
this._pollTimer = setInterval(() => this.refreshNotifications(), 45_000);
|
|
45
|
+
if (this._pollTimer.unref) this._pollTimer.unref();
|
|
46
|
+
this.#detectPush();
|
|
47
|
+
window.addEventListener('focus', () => this.refreshNotifications());
|
|
48
|
+
navigator.serviceWorker?.addEventListener?.('message', (e) => {
|
|
49
|
+
if (e.data?.type === 'trove-push') this.refreshNotifications();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async refreshNotifications() {
|
|
54
|
+
try {
|
|
55
|
+
const n = await this.api.notifications();
|
|
56
|
+
this.#set({ notifications: n });
|
|
57
|
+
} catch { /* not enabled / offline */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
toggleInbox(open) {
|
|
61
|
+
const next = open ?? !this.state.inboxOpen;
|
|
62
|
+
this.#set({ inboxOpen: next });
|
|
63
|
+
if (next && this.state.notifications.unread) this.markAllRead();
|
|
64
|
+
}
|
|
65
|
+
async markAllRead() {
|
|
66
|
+
try {
|
|
67
|
+
await this.api.markNotificationsRead();
|
|
68
|
+
this.#set({ notifications: { ...this.state.notifications, unread: 0, items: this.state.notifications.items.map((i) => ({ ...i, read: true })) } });
|
|
69
|
+
} catch { /* ignore */ }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --- sidecar (per active file) ---------------------------------------------
|
|
73
|
+
|
|
74
|
+
async loadSidecar(nodeId) {
|
|
75
|
+
if (!nodeId) return this.#set({ sidecar: null, backlinks: null });
|
|
76
|
+
this.#set({ sidecar: { nodeId, loading: true, tags: [], comments: [], subscribers: [] } });
|
|
77
|
+
this.loadBacklinks(nodeId);
|
|
78
|
+
try {
|
|
79
|
+
const view = await this.api.sidecar(nodeId);
|
|
80
|
+
// A slower request for a previously-open item must not land on this one — the
|
|
81
|
+
// same guard loadBacklinks already had. Without it, opening A then quickly opening
|
|
82
|
+
// B let A resolve last, so `state.sidecar.nodeId` became A while B was on screen
|
|
83
|
+
// and comment()/addTag()/removeTag() all wrote to A.
|
|
84
|
+
if (this.state.sidecar?.nodeId !== nodeId) return;
|
|
85
|
+
this.#set({ sidecar: { ...view, loading: false } });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (this.state.sidecar?.nodeId !== nodeId) return;
|
|
88
|
+
this.#set({ sidecar: { nodeId, loading: false, error: err.message, tags: [], comments: [] } });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* What links to this item. With no folders, this is how you find out where an item
|
|
94
|
+
* "lives" — which documents gather it up — so a failure has to be visible rather
|
|
95
|
+
* than rendering as "nothing links here", which would be a claim about the drive.
|
|
96
|
+
*/
|
|
97
|
+
async loadBacklinks(nodeId) {
|
|
98
|
+
if (!nodeId) return this.#set({ backlinks: null });
|
|
99
|
+
this.#set({ backlinks: { nodeId, items: [], loading: true, error: null } });
|
|
100
|
+
try {
|
|
101
|
+
const res = await this.api.backlinks(nodeId);
|
|
102
|
+
// A slower request for a previously-open item must not land on this one.
|
|
103
|
+
if (this.state.backlinks?.nodeId !== nodeId) return;
|
|
104
|
+
this.#set({ backlinks: { nodeId, items: res.items || [], loading: false, error: null } });
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (this.state.backlinks?.nodeId !== nodeId) return;
|
|
107
|
+
this.#set({ backlinks: { nodeId, items: [], loading: false, error: err.message } });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async #reload() {
|
|
111
|
+
if (this.state.sidecar?.nodeId) await this.loadSidecar(this.state.sidecar.nodeId);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setReplyTo(target) {
|
|
115
|
+
this.#set({ replyTo: target });
|
|
116
|
+
}
|
|
117
|
+
async comment(body) {
|
|
118
|
+
const nodeId = this.state.sidecar?.nodeId;
|
|
119
|
+
if (!nodeId || !body.trim()) return;
|
|
120
|
+
const parentId = this.state.replyTo?.id || null;
|
|
121
|
+
// Offline → queue the op; it replays (CRDT-merges) on reconnect.
|
|
122
|
+
if (this.offline && !this.offline.state.online) {
|
|
123
|
+
await this.offline.queueOp({ method: 'POST', path: `/api/items/${encodeURIComponent(nodeId)}/comments`, body: { body, parentId } });
|
|
124
|
+
this.#set({ replyTo: null });
|
|
125
|
+
this.platform.notifications.info('Offline — your comment will post when you reconnect.');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.#set({ posting: true });
|
|
129
|
+
try {
|
|
130
|
+
await this.api.addComment(nodeId, { body, parentId });
|
|
131
|
+
this.#set({ replyTo: null });
|
|
132
|
+
await this.#reload();
|
|
133
|
+
} catch (err) {
|
|
134
|
+
this.platform.notifications.error(`Couldn't post comment: ${err.message}`);
|
|
135
|
+
} finally {
|
|
136
|
+
this.#set({ posting: false });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async deleteComment(cid) {
|
|
140
|
+
const nodeId = this.state.sidecar?.nodeId;
|
|
141
|
+
await this.api.deleteComment(nodeId, cid).catch((e) => this.platform.notifications.error(e.message));
|
|
142
|
+
await this.#reload();
|
|
143
|
+
}
|
|
144
|
+
async react(cid, emoji) {
|
|
145
|
+
const nodeId = this.state.sidecar?.nodeId;
|
|
146
|
+
const comment = findComment(this.state.sidecar?.comments || [], cid);
|
|
147
|
+
const on = !(comment?.reactions?.[emoji] || []).includes(this.state.me?.id);
|
|
148
|
+
try {
|
|
149
|
+
await this.api.reactComment(nodeId, cid, emoji, on);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
this.platform.notifications.error(`Couldn't react: ${err.message}`);
|
|
152
|
+
}
|
|
153
|
+
await this.#reload();
|
|
154
|
+
}
|
|
155
|
+
async addTag(name, value) {
|
|
156
|
+
const nodeId = this.state.sidecar?.nodeId;
|
|
157
|
+
if (!name.trim()) return;
|
|
158
|
+
if (this.offline && !this.offline.state.online) {
|
|
159
|
+
await this.offline.queueOp({ method: 'POST', path: `/api/items/${encodeURIComponent(nodeId)}/tags`, body: { name: name.trim(), value } });
|
|
160
|
+
this.platform.notifications.info('Offline — tag will sync when you reconnect.');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
await this.api.setTag(nodeId, name.trim(), value).catch((e) => this.platform.notifications.error(e.message));
|
|
164
|
+
await this.#reload();
|
|
165
|
+
}
|
|
166
|
+
async removeTag(name) {
|
|
167
|
+
const nodeId = this.state.sidecar?.nodeId;
|
|
168
|
+
// Offline → queue like addTag so the removal isn't silently lost.
|
|
169
|
+
if (this.offline && !this.offline.state.online) {
|
|
170
|
+
await this.offline.queueOp({ method: 'DELETE', path: `/api/items/${encodeURIComponent(nodeId)}/tags/${encodeURIComponent(name)}` });
|
|
171
|
+
this.platform.notifications.info('Offline — tag removal will sync when you reconnect.');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
await this.api.removeTag(nodeId, name);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
this.platform.notifications.error(`Couldn't remove tag: ${err.message}`);
|
|
178
|
+
}
|
|
179
|
+
await this.#reload();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --- web push --------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
async #detectPush() {
|
|
185
|
+
if (!this.state.pushSupported) return;
|
|
186
|
+
try {
|
|
187
|
+
const reg = await navigator.serviceWorker.getRegistration();
|
|
188
|
+
const sub = await reg?.pushManager?.getSubscription();
|
|
189
|
+
this.#set({ pushEnabled: !!sub });
|
|
190
|
+
} catch { /* ignore */ }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async enablePush() {
|
|
194
|
+
if (!this.state.pushSupported) {
|
|
195
|
+
this.platform.notifications.warn('Push notifications are not supported in this browser.');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const { publicKey } = await this.api.vapidKey();
|
|
200
|
+
if (!publicKey) {
|
|
201
|
+
this.platform.notifications.warn('This server has no VAPID key configured for web push.');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const reg = await navigator.serviceWorker.register('/sw.js');
|
|
205
|
+
await navigator.serviceWorker.ready;
|
|
206
|
+
const perm = await Notification.requestPermission();
|
|
207
|
+
if (perm !== 'granted') return this.platform.notifications.info('Notifications permission was denied.');
|
|
208
|
+
const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(publicKey) });
|
|
209
|
+
await this.api.subscribePush(sub.toJSON());
|
|
210
|
+
this.#set({ pushEnabled: true });
|
|
211
|
+
this.platform.notifications.success('Notifications enabled — you’ll be pinged when someone @mentions you.');
|
|
212
|
+
} catch (err) {
|
|
213
|
+
this.platform.notifications.error(`Couldn't enable notifications: ${err.message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function findComment(list, cid) {
|
|
219
|
+
for (const c of list) {
|
|
220
|
+
if (c.id === cid) return c;
|
|
221
|
+
const inner = findComment(c.replies || [], cid);
|
|
222
|
+
if (inner) return inner;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function urlBase64ToUint8Array(base64) {
|
|
228
|
+
const padding = '='.repeat((4 - (base64.length % 4)) % 4);
|
|
229
|
+
const b64 = (base64 + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
230
|
+
const raw = atob(b64);
|
|
231
|
+
const out = new Uint8Array(raw.length);
|
|
232
|
+
for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Parse `#tag` / `#key:op:value` filter tokens out of a launcher query and match
|
|
2
|
+
// nodes against them. Grammar (each token, space-separated):
|
|
3
|
+
// #tag → the property/tag is present
|
|
4
|
+
// #key:=v #key:!=v #key:<v #key:<=v #key:>v #key:>=v → compare
|
|
5
|
+
// #key:v → equality shorthand (same as #key:=v)
|
|
6
|
+
// Values may be "quoted" to include spaces. Comparisons are numeric when both
|
|
7
|
+
// sides parse as numbers, otherwise case-insensitive string comparisons.
|
|
8
|
+
|
|
9
|
+
import { matchTagFilters } from '@3sln/trove/core/search/tagMatch.js';
|
|
10
|
+
|
|
11
|
+
const TOKEN = /#([\w.-]+)(?::(<=|>=|!=|=|<|>)?("[^"]*"|[^#\s]+)?)?/g;
|
|
12
|
+
|
|
13
|
+
/** @returns {{ text: string, filters: {key,op,value,present}[] }} */
|
|
14
|
+
export function parseTagQuery(query) {
|
|
15
|
+
const filters = [];
|
|
16
|
+
let text = query;
|
|
17
|
+
for (const m of query.matchAll(TOKEN)) {
|
|
18
|
+
const [, key, op, rawValue] = m;
|
|
19
|
+
const value = rawValue && rawValue.startsWith('"') ? rawValue.slice(1, -1) : rawValue;
|
|
20
|
+
filters.push(value == null || value === ''
|
|
21
|
+
? { key, present: true }
|
|
22
|
+
: { key, op: op || '=', value, present: false });
|
|
23
|
+
text = text.replace(m[0], ' ');
|
|
24
|
+
}
|
|
25
|
+
return { text: text.replace(/\s+/g, ' ').trim(), filters };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* True if `node` satisfies ALL filters (AND).
|
|
30
|
+
*
|
|
31
|
+
* Delegates to core rather than re-implementing. This file had its own copy, and the
|
|
32
|
+
* two drifted on the ordering operators — the client lowercased both operands, the
|
|
33
|
+
* server did not — so `#author:>Bob` against `author: "alice"` answered differently
|
|
34
|
+
* online and offline. Same query, same drive, different files.
|
|
35
|
+
*/
|
|
36
|
+
export function matchesTagFilters(node, filters) {
|
|
37
|
+
if (!filters || !filters.length) return true;
|
|
38
|
+
return matchTagFilters(node, filters);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Short human label for a filter (for the results heading). */
|
|
42
|
+
export function filterLabel(f) {
|
|
43
|
+
return f.present ? `#${f.key}` : `#${f.key}:${f.op}${f.value}`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The open-source build's entry point: a workbench with nothing added to it.
|
|
2
|
+
//
|
|
3
|
+
// Everything that used to live here is `createWorkbench` (./workbench.js), which takes
|
|
4
|
+
// the openers and views a build wants to ship. A hosted or bespoke build imports that
|
|
5
|
+
// instead of this file and passes its own — no fork, and the same registry the plugins
|
|
6
|
+
// use.
|
|
7
|
+
|
|
8
|
+
import { createWorkbench } from './workbench.js';
|
|
9
|
+
|
|
10
|
+
createWorkbench();
|