@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.
Files changed (117) hide show
  1. package/LICENSE.md +18 -0
  2. package/README.md +98 -0
  3. package/client/asset_bundle/bundle.ts +95 -0
  4. package/client/data/datastore.ts +85 -0
  5. package/client/data/kv_primitives.ts +25 -0
  6. package/client/markdown_parser/constants.ts +13 -0
  7. package/client/plugos/event.ts +36 -0
  8. package/client/plugos/eventhook.ts +8 -0
  9. package/client/plugos/hooks/code_widget.ts +59 -0
  10. package/client/plugos/hooks/command.ts +104 -0
  11. package/client/plugos/hooks/document_editor.ts +77 -0
  12. package/client/plugos/hooks/event.ts +187 -0
  13. package/client/plugos/hooks/mq.ts +154 -0
  14. package/client/plugos/hooks/plug_namespace.ts +85 -0
  15. package/client/plugos/hooks/slash_command.ts +192 -0
  16. package/client/plugos/hooks/syscall.ts +66 -0
  17. package/client/plugos/manifest_cache.ts +67 -0
  18. package/client/plugos/plug.ts +99 -0
  19. package/client/plugos/plug_compile.ts +202 -0
  20. package/client/plugos/protocol.ts +40 -0
  21. package/client/plugos/proxy_fetch.ts +53 -0
  22. package/client/plugos/sandboxes/deno_worker_sandbox.ts +6 -0
  23. package/client/plugos/sandboxes/sandbox.ts +14 -0
  24. package/client/plugos/sandboxes/web_worker_sandbox.ts +17 -0
  25. package/client/plugos/sandboxes/worker_sandbox.ts +132 -0
  26. package/client/plugos/syscalls/asset.ts +35 -0
  27. package/client/plugos/syscalls/clientStore.ts +21 -0
  28. package/client/plugos/syscalls/client_code_widget.ts +12 -0
  29. package/client/plugos/syscalls/code_widget.ts +24 -0
  30. package/client/plugos/syscalls/config.ts +46 -0
  31. package/client/plugos/syscalls/datastore.ts +89 -0
  32. package/client/plugos/syscalls/editor.ts +673 -0
  33. package/client/plugos/syscalls/event.ts +36 -0
  34. package/client/plugos/syscalls/fetch.ts +128 -0
  35. package/client/plugos/syscalls/index.ts +102 -0
  36. package/client/plugos/syscalls/jsonschema.ts +69 -0
  37. package/client/plugos/syscalls/language.ts +23 -0
  38. package/client/plugos/syscalls/lua.ts +58 -0
  39. package/client/plugos/syscalls/markdown.ts +84 -0
  40. package/client/plugos/syscalls/mq.ts +52 -0
  41. package/client/plugos/syscalls/service_registry.ts +43 -0
  42. package/client/plugos/syscalls/shell.ts +39 -0
  43. package/client/plugos/syscalls/space.ts +139 -0
  44. package/client/plugos/syscalls/sync.ts +77 -0
  45. package/client/plugos/syscalls/system.ts +150 -0
  46. package/client/plugos/system.ts +201 -0
  47. package/client/plugos/types.ts +60 -0
  48. package/client/plugos/util.ts +14 -0
  49. package/client/plugos/worker_runtime.ts +195 -0
  50. package/client/space_lua/ast.ts +328 -0
  51. package/client/space_lua/ast_narrow.ts +81 -0
  52. package/client/space_lua/eval.ts +2478 -0
  53. package/client/space_lua/labels.ts +416 -0
  54. package/client/space_lua/numeric.ts +240 -0
  55. package/client/space_lua/parse.ts +1522 -0
  56. package/client/space_lua/query_collection.ts +232 -0
  57. package/client/space_lua/rp.ts +27 -0
  58. package/client/space_lua/runtime.ts +1702 -0
  59. package/client/space_lua/stdlib/crypto.ts +10 -0
  60. package/client/space_lua/stdlib/encoding.ts +19 -0
  61. package/client/space_lua/stdlib/format.ts +770 -0
  62. package/client/space_lua/stdlib/js.ts +73 -0
  63. package/client/space_lua/stdlib/load.ts +52 -0
  64. package/client/space_lua/stdlib/math.ts +193 -0
  65. package/client/space_lua/stdlib/net.ts +113 -0
  66. package/client/space_lua/stdlib/os.ts +368 -0
  67. package/client/space_lua/stdlib/space_lua.ts +153 -0
  68. package/client/space_lua/stdlib/string.ts +286 -0
  69. package/client/space_lua/stdlib/table.ts +401 -0
  70. package/client/space_lua/stdlib.ts +489 -0
  71. package/client/space_lua/tonumber.ts +501 -0
  72. package/client/space_lua/util.ts +96 -0
  73. package/dist/plug-compile.js +1513 -0
  74. package/package.json +120 -0
  75. package/plug-api/constants.ts +42 -0
  76. package/plug-api/lib/async.ts +162 -0
  77. package/plug-api/lib/crypto.ts +202 -0
  78. package/plug-api/lib/dates.ts +13 -0
  79. package/plug-api/lib/json.ts +136 -0
  80. package/plug-api/lib/limited_map.ts +72 -0
  81. package/plug-api/lib/memory_cache.ts +21 -0
  82. package/plug-api/lib/native_fetch.ts +6 -0
  83. package/plug-api/lib/ref.ts +275 -0
  84. package/plug-api/lib/resolve.ts +90 -0
  85. package/plug-api/lib/tags.ts +15 -0
  86. package/plug-api/lib/transclusion.ts +122 -0
  87. package/plug-api/lib/tree.ts +232 -0
  88. package/plug-api/lib/yaml.ts +284 -0
  89. package/plug-api/syscall.ts +15 -0
  90. package/plug-api/syscalls/asset.ts +36 -0
  91. package/plug-api/syscalls/client_store.ts +33 -0
  92. package/plug-api/syscalls/code_widget.ts +8 -0
  93. package/plug-api/syscalls/config.ts +58 -0
  94. package/plug-api/syscalls/datastore.ts +96 -0
  95. package/plug-api/syscalls/editor.ts +517 -0
  96. package/plug-api/syscalls/event.ts +47 -0
  97. package/plug-api/syscalls/index.ts +77 -0
  98. package/plug-api/syscalls/jsonschema.ts +25 -0
  99. package/plug-api/syscalls/language.ts +23 -0
  100. package/plug-api/syscalls/lua.ts +20 -0
  101. package/plug-api/syscalls/markdown.ts +38 -0
  102. package/plug-api/syscalls/mq.ts +79 -0
  103. package/plug-api/syscalls/shell.ts +14 -0
  104. package/plug-api/syscalls/space.ts +212 -0
  105. package/plug-api/syscalls/sync.ts +28 -0
  106. package/plug-api/syscalls/system.ts +102 -0
  107. package/plug-api/syscalls/yaml.ts +28 -0
  108. package/plug-api/syscalls.ts +21 -0
  109. package/plug-api/system_mock.ts +89 -0
  110. package/plug-api/types/client.ts +116 -0
  111. package/plug-api/types/config.ts +22 -0
  112. package/plug-api/types/datastore.ts +28 -0
  113. package/plug-api/types/event.ts +27 -0
  114. package/plug-api/types/index.ts +56 -0
  115. package/plug-api/types/manifest.ts +98 -0
  116. package/plug-api/types/namespace.ts +6 -0
  117. 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
+ }