@silverbulletmd/silverbullet 2.4.1
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/LICENSE.md +18 -0
- package/README.md +98 -0
- package/client/asset_bundle/bundle.ts +95 -0
- package/client/data/datastore.ts +85 -0
- package/client/data/kv_primitives.ts +25 -0
- package/client/markdown_parser/constants.ts +13 -0
- package/client/plugos/event.ts +36 -0
- package/client/plugos/eventhook.ts +8 -0
- package/client/plugos/hooks/code_widget.ts +59 -0
- package/client/plugos/hooks/command.ts +104 -0
- package/client/plugos/hooks/document_editor.ts +77 -0
- package/client/plugos/hooks/event.ts +187 -0
- package/client/plugos/hooks/mq.ts +154 -0
- package/client/plugos/hooks/plug_namespace.ts +85 -0
- package/client/plugos/hooks/slash_command.ts +192 -0
- package/client/plugos/hooks/syscall.ts +66 -0
- package/client/plugos/manifest_cache.ts +67 -0
- package/client/plugos/plug.ts +99 -0
- package/client/plugos/plug_compile.ts +202 -0
- package/client/plugos/protocol.ts +40 -0
- package/client/plugos/proxy_fetch.ts +53 -0
- package/client/plugos/sandboxes/deno_worker_sandbox.ts +6 -0
- package/client/plugos/sandboxes/sandbox.ts +14 -0
- package/client/plugos/sandboxes/web_worker_sandbox.ts +17 -0
- package/client/plugos/sandboxes/worker_sandbox.ts +132 -0
- package/client/plugos/syscalls/asset.ts +35 -0
- package/client/plugos/syscalls/clientStore.ts +21 -0
- package/client/plugos/syscalls/client_code_widget.ts +12 -0
- package/client/plugos/syscalls/code_widget.ts +24 -0
- package/client/plugos/syscalls/config.ts +46 -0
- package/client/plugos/syscalls/datastore.ts +89 -0
- package/client/plugos/syscalls/editor.ts +673 -0
- package/client/plugos/syscalls/event.ts +36 -0
- package/client/plugos/syscalls/fetch.ts +128 -0
- package/client/plugos/syscalls/index.ts +102 -0
- package/client/plugos/syscalls/jsonschema.ts +69 -0
- package/client/plugos/syscalls/language.ts +23 -0
- package/client/plugos/syscalls/lua.ts +58 -0
- package/client/plugos/syscalls/markdown.ts +84 -0
- package/client/plugos/syscalls/mq.ts +52 -0
- package/client/plugos/syscalls/service_registry.ts +43 -0
- package/client/plugos/syscalls/shell.ts +39 -0
- package/client/plugos/syscalls/space.ts +139 -0
- package/client/plugos/syscalls/sync.ts +77 -0
- package/client/plugos/syscalls/system.ts +150 -0
- package/client/plugos/system.ts +201 -0
- package/client/plugos/types.ts +60 -0
- package/client/plugos/util.ts +14 -0
- package/client/plugos/worker_runtime.ts +195 -0
- package/client/space_lua/ast.ts +328 -0
- package/client/space_lua/ast_narrow.ts +81 -0
- package/client/space_lua/eval.ts +2478 -0
- package/client/space_lua/labels.ts +416 -0
- package/client/space_lua/numeric.ts +240 -0
- package/client/space_lua/parse.ts +1522 -0
- package/client/space_lua/query_collection.ts +232 -0
- package/client/space_lua/rp.ts +27 -0
- package/client/space_lua/runtime.ts +1702 -0
- package/client/space_lua/stdlib/crypto.ts +10 -0
- package/client/space_lua/stdlib/encoding.ts +19 -0
- package/client/space_lua/stdlib/format.ts +770 -0
- package/client/space_lua/stdlib/js.ts +73 -0
- package/client/space_lua/stdlib/load.ts +52 -0
- package/client/space_lua/stdlib/math.ts +193 -0
- package/client/space_lua/stdlib/net.ts +113 -0
- package/client/space_lua/stdlib/os.ts +368 -0
- package/client/space_lua/stdlib/space_lua.ts +153 -0
- package/client/space_lua/stdlib/string.ts +286 -0
- package/client/space_lua/stdlib/table.ts +401 -0
- package/client/space_lua/stdlib.ts +489 -0
- package/client/space_lua/tonumber.ts +501 -0
- package/client/space_lua/util.ts +96 -0
- package/dist/plug-compile.js +1513 -0
- package/package.json +120 -0
- package/plug-api/constants.ts +42 -0
- package/plug-api/lib/async.ts +162 -0
- package/plug-api/lib/crypto.ts +202 -0
- package/plug-api/lib/dates.ts +13 -0
- package/plug-api/lib/json.ts +136 -0
- package/plug-api/lib/limited_map.ts +72 -0
- package/plug-api/lib/memory_cache.ts +21 -0
- package/plug-api/lib/native_fetch.ts +6 -0
- package/plug-api/lib/ref.ts +275 -0
- package/plug-api/lib/resolve.ts +90 -0
- package/plug-api/lib/tags.ts +15 -0
- package/plug-api/lib/transclusion.ts +122 -0
- package/plug-api/lib/tree.ts +232 -0
- package/plug-api/lib/yaml.ts +284 -0
- package/plug-api/syscall.ts +15 -0
- package/plug-api/syscalls/asset.ts +36 -0
- package/plug-api/syscalls/client_store.ts +33 -0
- package/plug-api/syscalls/code_widget.ts +8 -0
- package/plug-api/syscalls/config.ts +58 -0
- package/plug-api/syscalls/datastore.ts +96 -0
- package/plug-api/syscalls/editor.ts +517 -0
- package/plug-api/syscalls/event.ts +47 -0
- package/plug-api/syscalls/index.ts +77 -0
- package/plug-api/syscalls/jsonschema.ts +25 -0
- package/plug-api/syscalls/language.ts +23 -0
- package/plug-api/syscalls/lua.ts +20 -0
- package/plug-api/syscalls/markdown.ts +38 -0
- package/plug-api/syscalls/mq.ts +79 -0
- package/plug-api/syscalls/shell.ts +14 -0
- package/plug-api/syscalls/space.ts +212 -0
- package/plug-api/syscalls/sync.ts +28 -0
- package/plug-api/syscalls/system.ts +102 -0
- package/plug-api/syscalls/yaml.ts +28 -0
- package/plug-api/syscalls.ts +21 -0
- package/plug-api/system_mock.ts +89 -0
- package/plug-api/types/client.ts +116 -0
- package/plug-api/types/config.ts +22 -0
- package/plug-api/types/datastore.ts +28 -0
- package/plug-api/types/event.ts +27 -0
- package/plug-api/types/index.ts +56 -0
- package/plug-api/types/manifest.ts +98 -0
- package/plug-api/types/namespace.ts +6 -0
- package/plugs/builtin_plugs.ts +14 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// deno-lint-ignore-file ban-types
|
|
2
|
+
import type { Manifest } from "../types.ts";
|
|
3
|
+
import type { System } from "../system.ts";
|
|
4
|
+
import type { EventHookI } from "../eventhook.ts";
|
|
5
|
+
import type { EventHookT } from "@silverbulletmd/silverbullet/type/manifest";
|
|
6
|
+
import type { Config } from "../../config.ts";
|
|
7
|
+
|
|
8
|
+
// System events:
|
|
9
|
+
// - plug:load (plugName: string)
|
|
10
|
+
|
|
11
|
+
export class EventHook implements EventHookI {
|
|
12
|
+
private system?: System<EventHookT>;
|
|
13
|
+
private localListeners: Map<string, ((...args: any[]) => any)[]> = new Map();
|
|
14
|
+
|
|
15
|
+
constructor(readonly config?: Config) {
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
addLocalListener(eventName: string, callback: (...args: any[]) => any) {
|
|
19
|
+
if (!this.localListeners.has(eventName)) {
|
|
20
|
+
this.localListeners.set(eventName, []);
|
|
21
|
+
}
|
|
22
|
+
this.localListeners.get(eventName)!.push(callback);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
removeLocalListener(eventName: string, callback: (...args: any[]) => any) {
|
|
26
|
+
if (!this.localListeners.has(eventName)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const listeners = this.localListeners.get(eventName)!;
|
|
30
|
+
const index = listeners.indexOf(callback);
|
|
31
|
+
if (index !== -1) {
|
|
32
|
+
listeners.splice(index, 1);
|
|
33
|
+
}
|
|
34
|
+
if (listeners.length === 0) {
|
|
35
|
+
this.localListeners.delete(eventName);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Pull all events listened to
|
|
40
|
+
listEvents(): string[] {
|
|
41
|
+
if (!this.system) {
|
|
42
|
+
throw new Error("Event hook is not initialized");
|
|
43
|
+
}
|
|
44
|
+
const eventNames = new Set<string>();
|
|
45
|
+
for (const plug of this.system.loadedPlugs.values()) {
|
|
46
|
+
for (const functionDef of Object.values(plug.manifest!.functions)) {
|
|
47
|
+
if (functionDef.events) {
|
|
48
|
+
for (const eventName of functionDef.events) {
|
|
49
|
+
eventNames.add(eventName);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const eventName of this.localListeners.keys()) {
|
|
55
|
+
eventNames.add(eventName);
|
|
56
|
+
}
|
|
57
|
+
if (this.config) {
|
|
58
|
+
const configListeners: Record<string, Function[]> = this.config.get(
|
|
59
|
+
"eventListeners",
|
|
60
|
+
{},
|
|
61
|
+
);
|
|
62
|
+
for (const name of Object.keys(configListeners)) {
|
|
63
|
+
eventNames.add(name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return [...eventNames];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async dispatchEvent(eventName: string, ...args: any[]): Promise<any[]> {
|
|
71
|
+
if (!this.system) {
|
|
72
|
+
throw new Error("Event hook is not initialized");
|
|
73
|
+
}
|
|
74
|
+
const promises: Promise<any>[] = [];
|
|
75
|
+
for (const plug of this.system.loadedPlugs.values()) {
|
|
76
|
+
const manifest = plug.manifest;
|
|
77
|
+
for (
|
|
78
|
+
const [name, functionDef] of Object.entries(
|
|
79
|
+
manifest!.functions,
|
|
80
|
+
)
|
|
81
|
+
) {
|
|
82
|
+
if (functionDef.events) {
|
|
83
|
+
for (const event of functionDef.events) {
|
|
84
|
+
if (
|
|
85
|
+
event === eventName || eventNameToRegex(event).test(eventName)
|
|
86
|
+
) {
|
|
87
|
+
// Only dispatch functions that can run in this environment
|
|
88
|
+
if (plug.canInvoke(name)) {
|
|
89
|
+
// Queue the promise
|
|
90
|
+
promises.push((async () => {
|
|
91
|
+
try {
|
|
92
|
+
return await plug.invoke(name, args);
|
|
93
|
+
} catch (e: any) {
|
|
94
|
+
console.error(
|
|
95
|
+
`Error dispatching event ${eventName} to ${plug.manifest.name}.${name}: ${e.message}`,
|
|
96
|
+
);
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
})());
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Local listeners
|
|
108
|
+
for (const [name, localListeners] of this.localListeners) {
|
|
109
|
+
if (eventNameToRegex(name).test(eventName)) {
|
|
110
|
+
for (const localListener of localListeners) {
|
|
111
|
+
// Queue the promise
|
|
112
|
+
promises.push((async () => {
|
|
113
|
+
return await Promise.resolve(localListener(...args));
|
|
114
|
+
})());
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Space Lua listeners
|
|
120
|
+
if (this.config) {
|
|
121
|
+
const configListeners: Record<string, Function[]> = this.config.get(
|
|
122
|
+
"eventListeners",
|
|
123
|
+
{},
|
|
124
|
+
);
|
|
125
|
+
for (const [name, listeners] of Object.entries(configListeners)) {
|
|
126
|
+
if (eventNameToRegex(name).test(eventName)) {
|
|
127
|
+
for (const listener of listeners) {
|
|
128
|
+
promises.push((async () => {
|
|
129
|
+
return await Promise.resolve(
|
|
130
|
+
listener({
|
|
131
|
+
name: eventName,
|
|
132
|
+
// Most events have a single argument, so let's optimize for that, otherwise pass all arguments as an array
|
|
133
|
+
data: args.length === 1 ? args[0] : args,
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
})());
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Wait for all promises to resolve
|
|
143
|
+
return (await Promise.allSettled(promises))
|
|
144
|
+
.filter((result) => {
|
|
145
|
+
if (result.status === "rejected") {
|
|
146
|
+
console.error(
|
|
147
|
+
"Error while dispatching event",
|
|
148
|
+
eventName,
|
|
149
|
+
":",
|
|
150
|
+
result.reason,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return result.status === "fulfilled";
|
|
154
|
+
})
|
|
155
|
+
.map((result) => result.value)
|
|
156
|
+
.filter((result) => result != null); // This keeps non-null/undefined results
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
apply(system: System<EventHookT>): void {
|
|
160
|
+
this.system = system;
|
|
161
|
+
this.system.on({
|
|
162
|
+
plugLoaded: async (plug) => {
|
|
163
|
+
await this.dispatchEvent("plug:load", plug.manifest.name);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
validateManifest(manifest: Manifest<EventHookT>): string[] {
|
|
169
|
+
const errors = [];
|
|
170
|
+
for (
|
|
171
|
+
const [_, functionDef] of Object.entries(
|
|
172
|
+
manifest.functions || {},
|
|
173
|
+
)
|
|
174
|
+
) {
|
|
175
|
+
if (functionDef.events && !Array.isArray(functionDef.events)) {
|
|
176
|
+
errors.push("'events' key must be an array of strings");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return errors;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function eventNameToRegex(eventName: string): RegExp {
|
|
184
|
+
return new RegExp(
|
|
185
|
+
`^${eventName.replace(/\*/g, ".*").replace(/\//g, "\\/")}$`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// deno-lint-ignore-file ban-types
|
|
2
|
+
import type { Hook, Manifest } from "../types.ts";
|
|
3
|
+
import type { System } from "../system.ts";
|
|
4
|
+
import { throttle } from "@silverbulletmd/silverbullet/lib/async";
|
|
5
|
+
import type { MQHookT } from "@silverbulletmd/silverbullet/type/manifest";
|
|
6
|
+
import type { DataStoreMQ, QueueWorker } from "../../data/mq.datastore.ts";
|
|
7
|
+
import type {
|
|
8
|
+
MQMessage,
|
|
9
|
+
MQSubscribeOptions,
|
|
10
|
+
} from "@silverbulletmd/silverbullet/type/datastore";
|
|
11
|
+
import type { Config } from "../../config.ts";
|
|
12
|
+
|
|
13
|
+
export type MQListenerSpec =
|
|
14
|
+
& MQSubscribeOptions
|
|
15
|
+
& {
|
|
16
|
+
queue: string;
|
|
17
|
+
autoAck?: boolean;
|
|
18
|
+
run: Function;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class MQHook implements Hook<MQHookT> {
|
|
22
|
+
subscriptions: QueueWorker[] = [];
|
|
23
|
+
throttledReloadQueues = throttle(() => {
|
|
24
|
+
this.reloadQueues();
|
|
25
|
+
}, 1000);
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private system: System<MQHookT>,
|
|
29
|
+
readonly mq: DataStoreMQ,
|
|
30
|
+
readonly config: Config,
|
|
31
|
+
) {
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
apply(system: System<MQHookT>): void {
|
|
35
|
+
this.system = system;
|
|
36
|
+
system.on({
|
|
37
|
+
plugLoaded: () => {
|
|
38
|
+
this.throttledReloadQueues();
|
|
39
|
+
},
|
|
40
|
+
plugUnloaded: () => {
|
|
41
|
+
this.throttledReloadQueues();
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
this.throttledReloadQueues();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
stop() {
|
|
49
|
+
this.subscriptions.forEach((worker) => worker.stop());
|
|
50
|
+
this.subscriptions = [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
reloadQueues() {
|
|
54
|
+
this.stop();
|
|
55
|
+
// Plug based subscriptions
|
|
56
|
+
for (const plug of this.system.loadedPlugs.values()) {
|
|
57
|
+
if (!plug.manifest) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
for (
|
|
61
|
+
const [name, functionDef] of Object.entries(
|
|
62
|
+
plug.manifest.functions,
|
|
63
|
+
)
|
|
64
|
+
) {
|
|
65
|
+
if (!functionDef.mqSubscriptions) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const subscriptions = functionDef.mqSubscriptions;
|
|
69
|
+
for (const subscriptionDef of subscriptions) {
|
|
70
|
+
const queue = subscriptionDef.queue;
|
|
71
|
+
this.subscriptions.push(
|
|
72
|
+
this.mq.subscribe(
|
|
73
|
+
queue,
|
|
74
|
+
{
|
|
75
|
+
batchSize: subscriptionDef.batchSize,
|
|
76
|
+
pollInterval: subscriptionDef.pollInterval,
|
|
77
|
+
},
|
|
78
|
+
async (messages: MQMessage[]) => {
|
|
79
|
+
try {
|
|
80
|
+
await plug.invoke(name, [messages]);
|
|
81
|
+
if (subscriptionDef.autoAck) {
|
|
82
|
+
await this.mq.batchAck(queue, messages.map((m) => m.id));
|
|
83
|
+
}
|
|
84
|
+
} catch (e: any) {
|
|
85
|
+
console.error(
|
|
86
|
+
"Execution of mqSubscription for queue",
|
|
87
|
+
queue,
|
|
88
|
+
"invoking",
|
|
89
|
+
name,
|
|
90
|
+
"with messages",
|
|
91
|
+
messages,
|
|
92
|
+
"failed:",
|
|
93
|
+
e,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Space Lua based subscriptions
|
|
103
|
+
const configListeners: Record<string, MQListenerSpec[]> = this.config.get(
|
|
104
|
+
"mqSubscriptions",
|
|
105
|
+
{},
|
|
106
|
+
);
|
|
107
|
+
for (const [queue, listeners] of Object.entries(configListeners)) {
|
|
108
|
+
for (const listener of listeners) {
|
|
109
|
+
// console.log("Subscribing to", queue, listener);
|
|
110
|
+
this.subscriptions.push(
|
|
111
|
+
this.mq.subscribe(
|
|
112
|
+
queue,
|
|
113
|
+
{
|
|
114
|
+
batchSize: listener.batchSize,
|
|
115
|
+
pollInterval: listener.pollInterval,
|
|
116
|
+
},
|
|
117
|
+
async (messages: MQMessage[]) => {
|
|
118
|
+
try {
|
|
119
|
+
await listener.run(messages);
|
|
120
|
+
if (listener.autoAck) {
|
|
121
|
+
await this.mq.batchAck(queue, messages.map((m) => m.id));
|
|
122
|
+
}
|
|
123
|
+
} catch (e: any) {
|
|
124
|
+
console.error(
|
|
125
|
+
"Execution of mqSubscription for queue",
|
|
126
|
+
queue,
|
|
127
|
+
"with messages",
|
|
128
|
+
messages,
|
|
129
|
+
"failed:",
|
|
130
|
+
e,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
validateManifest(manifest: Manifest<MQHookT>): string[] {
|
|
141
|
+
const errors: string[] = [];
|
|
142
|
+
for (const functionDef of Object.values(manifest.functions)) {
|
|
143
|
+
if (!functionDef.mqSubscriptions) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
for (const subscriptionDef of functionDef.mqSubscriptions) {
|
|
147
|
+
if (!subscriptionDef.queue) {
|
|
148
|
+
errors.push("Missing queue name for mqSubscription");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return errors;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { NamespaceOperation } from "@silverbulletmd/silverbullet/type/namespace";
|
|
2
|
+
import type { PlugNamespaceHookT } from "@silverbulletmd/silverbullet/type/manifest";
|
|
3
|
+
import type { Plug } from "../plug.ts";
|
|
4
|
+
import type { System } from "../system.ts";
|
|
5
|
+
import type { Hook, Manifest } from "../types.ts";
|
|
6
|
+
|
|
7
|
+
type SpaceFunction = {
|
|
8
|
+
operation: NamespaceOperation;
|
|
9
|
+
pattern: RegExp;
|
|
10
|
+
plug: Plug<PlugNamespaceHookT>;
|
|
11
|
+
name: string;
|
|
12
|
+
env?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class PlugNamespaceHook implements Hook<PlugNamespaceHookT> {
|
|
16
|
+
spaceFunctions: SpaceFunction[] = [];
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
apply(system: System<PlugNamespaceHookT>): void {
|
|
22
|
+
system.on({
|
|
23
|
+
plugLoaded: () => {
|
|
24
|
+
this.updateCache(system);
|
|
25
|
+
},
|
|
26
|
+
plugUnloaded: () => {
|
|
27
|
+
this.updateCache(system);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
updateCache(system: System<PlugNamespaceHookT>) {
|
|
33
|
+
this.spaceFunctions = [];
|
|
34
|
+
for (const plug of system.loadedPlugs.values()) {
|
|
35
|
+
if (plug.manifest?.functions) {
|
|
36
|
+
for (
|
|
37
|
+
const [funcName, funcDef] of Object.entries(
|
|
38
|
+
plug.manifest.functions,
|
|
39
|
+
)
|
|
40
|
+
) {
|
|
41
|
+
if (funcDef.pageNamespace) {
|
|
42
|
+
this.spaceFunctions.push({
|
|
43
|
+
operation: funcDef.pageNamespace.operation,
|
|
44
|
+
pattern: new RegExp(funcDef.pageNamespace.pattern),
|
|
45
|
+
plug,
|
|
46
|
+
name: funcName,
|
|
47
|
+
env: funcDef.env,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
validateManifest(manifest: Manifest<PlugNamespaceHookT>): string[] {
|
|
56
|
+
const errors: string[] = [];
|
|
57
|
+
if (!manifest.functions) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
for (const [funcName, funcDef] of Object.entries(manifest.functions)) {
|
|
61
|
+
if (funcDef.pageNamespace) {
|
|
62
|
+
if (!funcDef.pageNamespace.pattern) {
|
|
63
|
+
errors.push(`Function ${funcName} has a namespace but no pattern`);
|
|
64
|
+
}
|
|
65
|
+
if (!funcDef.pageNamespace.operation) {
|
|
66
|
+
errors.push(`Function ${funcName} has a namespace but no operation`);
|
|
67
|
+
}
|
|
68
|
+
if (
|
|
69
|
+
![
|
|
70
|
+
"readFile",
|
|
71
|
+
"writeFile",
|
|
72
|
+
"getFileMeta",
|
|
73
|
+
"listFiles",
|
|
74
|
+
"deleteFile",
|
|
75
|
+
].includes(funcDef.pageNamespace.operation)
|
|
76
|
+
) {
|
|
77
|
+
errors.push(
|
|
78
|
+
`Function ${funcName} has an invalid operation ${funcDef.pageNamespace.operation}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return errors;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { Hook, Manifest } from "../types.ts";
|
|
2
|
+
import type { System } from "../system.ts";
|
|
3
|
+
import type {
|
|
4
|
+
Completion,
|
|
5
|
+
CompletionContext,
|
|
6
|
+
CompletionResult,
|
|
7
|
+
} from "@codemirror/autocomplete";
|
|
8
|
+
import type { Client } from "../../client.ts";
|
|
9
|
+
import { syntaxTree } from "@codemirror/language";
|
|
10
|
+
import { safeRun, throttle } from "@silverbulletmd/silverbullet/lib/async";
|
|
11
|
+
import type { SlashCommandHookT } from "@silverbulletmd/silverbullet/type/manifest";
|
|
12
|
+
import type { SlashCommand } from "../../types/command.ts";
|
|
13
|
+
import type {
|
|
14
|
+
SlashCompletionOption,
|
|
15
|
+
SlashCompletions,
|
|
16
|
+
} from "@silverbulletmd/silverbullet/type/client";
|
|
17
|
+
|
|
18
|
+
const slashCommandRegexp = /([^\w:]|^)\/[\w#\-]*/;
|
|
19
|
+
|
|
20
|
+
export class SlashCommandHook implements Hook<SlashCommandHookT> {
|
|
21
|
+
slashCommands: SlashCommand[] = [];
|
|
22
|
+
throttledBuildAllCommands = throttle(() => {
|
|
23
|
+
this.buildAllCommands();
|
|
24
|
+
}, 200);
|
|
25
|
+
|
|
26
|
+
constructor(private client: Client) {
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
buildAllCommands() {
|
|
30
|
+
const clientSystem = this.client.clientSystem;
|
|
31
|
+
const system = clientSystem.system;
|
|
32
|
+
|
|
33
|
+
this.slashCommands = [];
|
|
34
|
+
for (const plug of system.loadedPlugs.values()) {
|
|
35
|
+
for (
|
|
36
|
+
const [name, functionDef] of Object.entries(
|
|
37
|
+
plug.manifest!.functions,
|
|
38
|
+
)
|
|
39
|
+
) {
|
|
40
|
+
if (!functionDef.slashCommand) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const cmd = functionDef.slashCommand;
|
|
44
|
+
this.slashCommands.push({
|
|
45
|
+
...cmd,
|
|
46
|
+
run: () => {
|
|
47
|
+
return plug.invoke(name, [cmd]);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Iterate over script defined slash commands
|
|
53
|
+
for (
|
|
54
|
+
const command of Object.values(
|
|
55
|
+
this.client.config.get<Record<string, SlashCommand>>(
|
|
56
|
+
"slashCommands",
|
|
57
|
+
{},
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
) {
|
|
61
|
+
this.slashCommands.push(command);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Completer for CodeMirror
|
|
66
|
+
public async slashCommandCompleter(
|
|
67
|
+
ctx: CompletionContext,
|
|
68
|
+
): Promise<CompletionResult | null> {
|
|
69
|
+
const prefix = ctx.matchBefore(slashCommandRegexp);
|
|
70
|
+
if (!prefix) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const prefixText = prefix.text;
|
|
74
|
+
const options: Completion[] = [];
|
|
75
|
+
|
|
76
|
+
// No slash commands in comment blocks (queries and such) or links
|
|
77
|
+
const currentNode = syntaxTree(ctx.state).resolveInner(ctx.pos);
|
|
78
|
+
if (
|
|
79
|
+
currentNode.type.name === "CommentBlock" ||
|
|
80
|
+
currentNode.type.name === "Link"
|
|
81
|
+
) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if the slash command is available in the current context
|
|
86
|
+
const parentNodes = this.client.extractParentNodes(ctx.state, currentNode);
|
|
87
|
+
for (const def of this.slashCommands) {
|
|
88
|
+
if (
|
|
89
|
+
def.onlyContexts &&
|
|
90
|
+
!def.onlyContexts.some((context) =>
|
|
91
|
+
parentNodes.some((node) => node.startsWith(context))
|
|
92
|
+
)
|
|
93
|
+
) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (
|
|
97
|
+
def.exceptContexts && def.exceptContexts.some(
|
|
98
|
+
(context) => parentNodes.some((node) => node.startsWith(context)),
|
|
99
|
+
)
|
|
100
|
+
) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
options.push({
|
|
104
|
+
label: def.name,
|
|
105
|
+
detail: def.description,
|
|
106
|
+
boost: def.priority,
|
|
107
|
+
apply: () => {
|
|
108
|
+
// Delete slash command part
|
|
109
|
+
this.client.editorView.dispatch({
|
|
110
|
+
changes: {
|
|
111
|
+
from: prefix!.from + prefixText.indexOf("/"),
|
|
112
|
+
to: ctx.pos,
|
|
113
|
+
insert: "",
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
// Replace with whatever the completion is
|
|
117
|
+
safeRun(async () => {
|
|
118
|
+
await def.run!();
|
|
119
|
+
this.client.focus();
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const slashCompletions: CompletionResult | SlashCompletions | null =
|
|
126
|
+
await this.client
|
|
127
|
+
.completeWithEvent(
|
|
128
|
+
ctx,
|
|
129
|
+
"slash:complete",
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (slashCompletions) {
|
|
133
|
+
for (
|
|
134
|
+
const slashCompletion of slashCompletions
|
|
135
|
+
.options as SlashCompletionOption[]
|
|
136
|
+
) {
|
|
137
|
+
options.push({
|
|
138
|
+
label: slashCompletion.label,
|
|
139
|
+
detail: slashCompletion.detail,
|
|
140
|
+
boost: slashCompletion.order && -slashCompletion.order,
|
|
141
|
+
apply: () => {
|
|
142
|
+
// Delete slash command part
|
|
143
|
+
this.client.editorView.dispatch({
|
|
144
|
+
changes: {
|
|
145
|
+
from: prefix!.from + prefixText.indexOf("/"),
|
|
146
|
+
to: ctx.pos,
|
|
147
|
+
insert: "",
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
// Replace with whatever the completion is
|
|
151
|
+
safeRun(async () => {
|
|
152
|
+
await this.client.clientSystem.system.invokeFunction(
|
|
153
|
+
slashCompletion.invoke,
|
|
154
|
+
[slashCompletion],
|
|
155
|
+
);
|
|
156
|
+
this.client.focus();
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
// + 1 because of the '/'
|
|
165
|
+
from: prefix.from + prefixText.indexOf("/") + 1,
|
|
166
|
+
options: options,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
apply(system: System<SlashCommandHookT>): void {
|
|
171
|
+
this.buildAllCommands();
|
|
172
|
+
system.on({
|
|
173
|
+
plugLoaded: () => {
|
|
174
|
+
this.buildAllCommands();
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
validateManifest(manifest: Manifest<SlashCommandHookT>): string[] {
|
|
180
|
+
const errors = [];
|
|
181
|
+
for (const [name, functionDef] of Object.entries(manifest.functions)) {
|
|
182
|
+
if (!functionDef.slashCommand) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const cmd = functionDef.slashCommand;
|
|
186
|
+
if (!cmd.name) {
|
|
187
|
+
errors.push(`Function ${name} has a command but no name`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Hook, Manifest } from "../types.ts";
|
|
2
|
+
import type { SysCallMapping, System } from "../system.ts";
|
|
3
|
+
import type { SyscallHookT } from "@silverbulletmd/silverbullet/type/manifest";
|
|
4
|
+
|
|
5
|
+
export class SyscallHook implements Hook<SyscallHookT> {
|
|
6
|
+
apply(system: System<SyscallHookT>): void {
|
|
7
|
+
this.registerSyscalls(system);
|
|
8
|
+
system.on({
|
|
9
|
+
plugLoaded: () => {
|
|
10
|
+
this.registerSyscalls(system);
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
registerSyscalls(system: System<SyscallHookT>) {
|
|
16
|
+
// Register syscalls from all loaded plugs
|
|
17
|
+
for (const plug of system.loadedPlugs.values()) {
|
|
18
|
+
const syscalls: SysCallMapping = {};
|
|
19
|
+
|
|
20
|
+
for (
|
|
21
|
+
const [name, functionDef] of Object.entries(plug.manifest!.functions)
|
|
22
|
+
) {
|
|
23
|
+
if (!functionDef.syscall) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const syscallName = functionDef.syscall;
|
|
28
|
+
|
|
29
|
+
// Add the syscall to our mapping
|
|
30
|
+
syscalls[syscallName] = (ctx, ...args) => {
|
|
31
|
+
// Delegate to the system to invoke the function
|
|
32
|
+
return system.syscall(ctx, "system.invokeFunction", [
|
|
33
|
+
`${plug.manifest!.name}.${name}`,
|
|
34
|
+
...args,
|
|
35
|
+
]);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Register the syscalls with no required permissions
|
|
39
|
+
system.registerSyscalls([], syscalls);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
validateManifest(manifest: Manifest<SyscallHookT>): string[] {
|
|
45
|
+
const errors: string[] = [];
|
|
46
|
+
for (const [name, functionDef] of Object.entries(manifest.functions)) {
|
|
47
|
+
if (!functionDef.syscall) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Validate syscall name is provided
|
|
52
|
+
if (!functionDef.syscall) {
|
|
53
|
+
errors.push(`Function ${name} has a syscall but no name`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Validate syscall name format (should be namespaced)
|
|
58
|
+
if (!functionDef.syscall.includes(".")) {
|
|
59
|
+
errors.push(
|
|
60
|
+
`Function ${name} has invalid syscall name "${functionDef.syscall}" - must be in format "namespace.name"`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return errors;
|
|
65
|
+
}
|
|
66
|
+
}
|