@silverbulletmd/silverbullet 2.10.0 → 2.12.0
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 +6 -6
- package/client/asset_bundle/bundle.ts +0 -1
- package/client/config.ts +343 -0
- package/client/data/kv_primitives.ts +2 -2
- package/client/markdown_parser/constants.ts +5 -1
- package/client/plugos/hooks/command.ts +8 -6
- package/client/plugos/hooks/event.ts +60 -24
- package/client/plugos/hooks/mq.ts +0 -3
- package/client/plugos/hooks/slash_command.ts +3 -20
- package/client/plugos/hooks/syscall.ts +0 -4
- package/client/plugos/manifest_cache.ts +2 -34
- package/client/plugos/plug.ts +0 -3
- package/client/plugos/plug_compile.ts +3 -13
- package/client/plugos/sandboxes/sandbox.ts +2 -2
- package/client/plugos/sandboxes/worker_sandbox.ts +19 -9
- package/client/plugos/syscalls/capture.ts +104 -0
- package/client/plugos/syscalls/editor.ts +197 -56
- package/client/plugos/syscalls/event.ts +12 -9
- package/client/plugos/syscalls/fetch.ts +0 -4
- package/client/plugos/syscalls/icon.ts +46 -0
- package/client/plugos/syscalls/index.ts +33 -13
- package/client/plugos/syscalls/jsonschema.ts +0 -1
- package/client/plugos/syscalls/mq.ts +0 -1
- package/client/plugos/syscalls/navigator.ts +138 -0
- package/client/plugos/syscalls/search.ts +44 -0
- package/client/plugos/syscalls/shell.ts +0 -1
- package/client/plugos/syscalls/space.ts +250 -7
- package/client/plugos/syscalls/sync.ts +70 -4
- package/client/plugos/syscalls/system.ts +39 -0
- package/client/plugos/system.ts +9 -7
- package/client/plugos/types.ts +6 -6
- package/client/plugos/worker_runtime.ts +0 -2
- package/client/space_lua/aggregates.ts +6 -22
- package/client/space_lua/ast.ts +0 -5
- package/client/space_lua/ast_narrow.ts +0 -5
- package/client/space_lua/budget.ts +113 -0
- package/client/space_lua/budget_ui.ts +49 -0
- package/client/space_lua/eval.ts +113 -88
- package/client/space_lua/labels.ts +0 -4
- package/client/space_lua/numeric.ts +2 -7
- package/client/space_lua/parse.ts +0 -17
- package/client/space_lua/quarantine.ts +127 -0
- package/client/space_lua/query_collection.ts +18 -51
- package/client/space_lua/query_env.ts +0 -2
- package/client/space_lua/render_lua_markdown.ts +2 -7
- package/client/space_lua/render_widget.ts +18 -1
- package/client/space_lua/runtime.ts +64 -77
- package/client/space_lua/stdlib/format.ts +0 -12
- package/client/space_lua/stdlib/js.ts +0 -2
- package/client/space_lua/stdlib/load.ts +0 -1
- package/client/space_lua/stdlib/math.ts +0 -7
- package/client/space_lua/stdlib/net.ts +0 -4
- package/client/space_lua/stdlib/os.ts +0 -10
- package/client/space_lua/stdlib/pattern.ts +18 -23
- package/client/space_lua/stdlib/space_lua.ts +1 -2
- package/client/space_lua/stdlib/string_pack.ts +4 -7
- package/client/space_lua/stdlib/table.ts +0 -5
- package/client/space_lua/stdlib.ts +18 -31
- package/client/space_lua/tonumber.ts +0 -9
- package/dist/plug-compile.js +3 -4
- package/package.json +52 -41
- package/plug-api/lib/async.ts +1 -6
- package/plug-api/lib/collation.ts +22 -0
- package/plug-api/lib/crypto.ts +16 -15
- package/plug-api/lib/dates.ts +33 -0
- package/plug-api/lib/fuzzy.ts +282 -0
- package/plug-api/lib/json.ts +0 -6
- package/plug-api/lib/limited_map.ts +0 -2
- package/plug-api/lib/link_write.ts +27 -0
- package/plug-api/lib/ref.ts +28 -5
- package/plug-api/lib/resolve.ts +1 -9
- package/plug-api/lib/resolve_path.ts +290 -0
- package/plug-api/lib/shortcut.ts +15 -0
- package/plug-api/lib/tags.ts +0 -3
- package/plug-api/lib/transclusion.ts +28 -1
- package/plug-api/lib/tree.ts +0 -5
- package/plug-api/lib/yaml.ts +5 -44
- package/plug-api/syscall.ts +1 -5
- package/plug-api/syscalls/config.ts +2 -2
- package/plug-api/syscalls/editor.ts +75 -213
- package/plug-api/syscalls/icon.ts +18 -0
- package/plug-api/syscalls/index.ts +22 -4
- package/plug-api/syscalls/search.ts +11 -0
- package/plug-api/syscalls/space.ts +122 -5
- package/plug-api/syscalls/sync.ts +31 -2
- package/plug-api/syscalls/system.ts +46 -0
- package/plug-api/syscalls.ts +2 -0
- package/plug-api/system_mock.ts +13 -0
- package/plug-api/types/client.ts +20 -4
- package/plug-api/types/datastore.ts +11 -2
- package/plug-api/types/index.ts +10 -0
- package/plug-api/types/manifest.ts +7 -0
- package/plug-api/types/profile.ts +13 -0
- package/plug-api/types/revisions.ts +67 -0
- package/plug-api/ui/description.ts +64 -0
- package/plug-api/ui/hover.ts +69 -0
- package/plug-api/ui/index.ts +58 -2
- package/plug-api/ui/scroll.ts +30 -0
- package/plug-api/ui/tree_model.ts +285 -0
- package/plug-api/ui/tree_types.ts +67 -0
- package/plug-api/ui/use_fit_collapse.ts +67 -0
- package/plugs/builtin_plugs.ts +0 -1
- package/plugs/index/types.ts +9 -0
- package/plug-api/lib/memory_cache.ts +0 -21
- /package/plug-api/{ui → lib}/panel_styles.ts +0 -0
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ You were told there’s no such thing as a silver bullet. You were told wrong.
|
|
|
24
24
|
[Much more detail can be found on silverbullet.md](https://silverbullet.md)
|
|
25
25
|
|
|
26
26
|
## Installing SilverBullet
|
|
27
|
-
Check out the [instructions](https://silverbullet.md/Install).
|
|
27
|
+
Check out the [instructions](https://docs.silverbullet.md/Install).
|
|
28
28
|
|
|
29
29
|
## Developing SilverBullet
|
|
30
30
|
SilverBullet's frontend ("client") is written in [TypeScript](https://www.typescriptlang.org/) and built on top of the excellent [CodeMirror 6](https://codemirror.net/) editor component. Additional UI is built using [Preact](https://preactjs.com). [ESBuild](https://esbuild.github.io) is used to build the frontend.
|
|
@@ -46,7 +46,7 @@ The server backend is written in [Rust](https://www.rust-lang.org/) (a Cargo wor
|
|
|
46
46
|
* `syscalls/`: TypeScript wrappers around syscalls
|
|
47
47
|
* `types/`: Various (client) types that can be references from plugs
|
|
48
48
|
* `scripts/`: Useful scripts
|
|
49
|
-
* `docs/`: documentation (
|
|
49
|
+
* `docs/`: documentation (published as docs.silverbullet.md and bundled as Desktop's Help)
|
|
50
50
|
|
|
51
51
|
### Requirements
|
|
52
52
|
* [Node.js](https://nodejs.org/) 24+ and npm 10+: Used to build the frontend (client) and plugs
|
|
@@ -83,7 +83,7 @@ Run the server in development with `cargo run`. A **debug** build serves the cli
|
|
|
83
83
|
|
|
84
84
|
```shell
|
|
85
85
|
npm run build
|
|
86
|
-
cargo run <PATH-TO-YOUR-
|
|
86
|
+
cargo run <PATH-TO-YOUR-DATA-FOLDER>
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
**When you change only the client** (TypeScript in `client/`): you do **not** need to restart the server. Rebuild just the client bundle and reload the page in your browser — the debug server serves the freshly-built bundle straight from disk:
|
|
@@ -96,7 +96,7 @@ To build a self-contained **release** binary (with the client bundle embedded),
|
|
|
96
96
|
|
|
97
97
|
```shell
|
|
98
98
|
make
|
|
99
|
-
./target/release/silverbullet <PATH-TO-YOUR-
|
|
99
|
+
./target/release/silverbullet <PATH-TO-YOUR-DATA-FOLDER>
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
### Useful development tasks
|
|
@@ -115,7 +115,7 @@ make bench
|
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
### Build a docker container
|
|
118
|
-
Note, you do not need Node.js nor
|
|
118
|
+
Note, you do not need Node.js nor Rust locally installed for this to work:
|
|
119
119
|
|
|
120
120
|
```shell
|
|
121
121
|
docker build -t silverbullet .
|
|
@@ -123,5 +123,5 @@ docker build -t silverbullet .
|
|
|
123
123
|
|
|
124
124
|
To run:
|
|
125
125
|
```shell
|
|
126
|
-
docker run -p 3000:3000 -v <PATH-TO-YOUR-
|
|
126
|
+
docker run -p 3000:3000 -v <PATH-TO-YOUR-DATA-FOLDER>:/data silverbullet
|
|
127
127
|
```
|
package/client/config.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { type OutputUnit, Validator, format } from "@cfworker/json-schema";
|
|
2
|
+
import { stripFunctions } from "./plugos/util.ts";
|
|
3
|
+
|
|
4
|
+
format.email = (data: string) => data.includes("@");
|
|
5
|
+
format["page-ref"] = (data: string) =>
|
|
6
|
+
data.startsWith("[[") && data.endsWith("]]");
|
|
7
|
+
|
|
8
|
+
function cfwFormatErrors(errors: OutputUnit[]): string {
|
|
9
|
+
// Filter out "properties" wrapper errors, keep only the specific leaf errors
|
|
10
|
+
const leafErrors = errors.filter((e) => e.keyword !== "properties");
|
|
11
|
+
const errorsToUse = leafErrors.length > 0 ? leafErrors : errors;
|
|
12
|
+
|
|
13
|
+
return errorsToUse
|
|
14
|
+
.map((e) => {
|
|
15
|
+
// Convert instanceLocation from "#/foo/bar" to "foo.bar"
|
|
16
|
+
const path =
|
|
17
|
+
e.instanceLocation === "#"
|
|
18
|
+
? ""
|
|
19
|
+
: e.instanceLocation.slice(2).replaceAll("/", ".");
|
|
20
|
+
return path ? `${path}: ${e.error}` : e.error;
|
|
21
|
+
})
|
|
22
|
+
.join(", ");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validates that a value looks like a valid JSON schema.
|
|
27
|
+
* Uses a lightweight structural check rather than full meta-schema validation.
|
|
28
|
+
*/
|
|
29
|
+
function isValidJsonSchema(schema: any): { valid: boolean; error?: string } {
|
|
30
|
+
if (schema === null || schema === undefined) {
|
|
31
|
+
return { valid: false, error: "schema must not be null or undefined" };
|
|
32
|
+
}
|
|
33
|
+
if (typeof schema === "boolean") {
|
|
34
|
+
return { valid: true };
|
|
35
|
+
}
|
|
36
|
+
if (typeof schema !== "object" || Array.isArray(schema)) {
|
|
37
|
+
return { valid: false, error: "schema must be an object or boolean" };
|
|
38
|
+
}
|
|
39
|
+
if (schema.type !== undefined) {
|
|
40
|
+
const validTypes = [
|
|
41
|
+
"string",
|
|
42
|
+
"number",
|
|
43
|
+
"integer",
|
|
44
|
+
"boolean",
|
|
45
|
+
"object",
|
|
46
|
+
"array",
|
|
47
|
+
"null",
|
|
48
|
+
];
|
|
49
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
50
|
+
for (const t of types) {
|
|
51
|
+
if (!validTypes.includes(t)) {
|
|
52
|
+
return {
|
|
53
|
+
valid: false,
|
|
54
|
+
error: `schema.type must be one of ${validTypes.join(", ")}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { valid: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Configuration management (config.* APIs) for the client
|
|
64
|
+
*/
|
|
65
|
+
export type CategoryDefinition = {
|
|
66
|
+
name: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
priority?: number;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export class Config {
|
|
72
|
+
public schemas: Record<string, any> = {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Registered UI categories for the configuration manager. Categories appear
|
|
79
|
+
* in the UI in descending `priority` (higher first); categories referenced
|
|
80
|
+
* by a schema's `ui.category` but never registered fall to the bottom in
|
|
81
|
+
* alphabetical order.
|
|
82
|
+
*/
|
|
83
|
+
public categories: Record<string, CategoryDefinition> = {};
|
|
84
|
+
|
|
85
|
+
constructor(public values: Record<string, any> = {}) {}
|
|
86
|
+
|
|
87
|
+
public clear() {
|
|
88
|
+
this.schemas = {
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: {},
|
|
91
|
+
};
|
|
92
|
+
this.values = {};
|
|
93
|
+
this.categories = {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Defines (or updates) a UI category for the configuration manager. The
|
|
98
|
+
* category's `name` is the same string used in a schema field's
|
|
99
|
+
* `ui.category`.
|
|
100
|
+
*/
|
|
101
|
+
defineCategory(definition: CategoryDefinition): void {
|
|
102
|
+
if (typeof definition?.name !== "string" || !definition.name) {
|
|
103
|
+
throw new Error("defineCategory requires a non-empty name");
|
|
104
|
+
}
|
|
105
|
+
this.categories[definition.name] = { ...definition };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Defines a JSON schema for a configuration key
|
|
110
|
+
* @param key The configuration key to define a schema for
|
|
111
|
+
* @param schema The JSON schema to validate against
|
|
112
|
+
*/
|
|
113
|
+
define(key: string | string[], schema: any): void {
|
|
114
|
+
const result = isValidJsonSchema(schema);
|
|
115
|
+
if (!result.valid) {
|
|
116
|
+
throw new Error(`Invalid schema for key ${key}: ${result.error}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (typeof key === "string") {
|
|
120
|
+
key = key.split(".");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let current = this.schemas;
|
|
124
|
+
for (let i = 0; i < key.length - 1; i++) {
|
|
125
|
+
const part = key[i];
|
|
126
|
+
if (!current.properties[part]) {
|
|
127
|
+
current.properties[part] = {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
current = current.properties[part];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const finalKey = key[key.length - 1];
|
|
136
|
+
current.properties[finalKey] = schema;
|
|
137
|
+
|
|
138
|
+
this.applySchemaDefaults(key, schema);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Recursively applies default values from a schema definition.
|
|
143
|
+
* Only sets a value if none exists at that path.
|
|
144
|
+
*/
|
|
145
|
+
private applySchemaDefaults(path: string[], schema: any): void {
|
|
146
|
+
if ("default" in schema) {
|
|
147
|
+
if (!this.has(path)) {
|
|
148
|
+
this.set(path, structuredClone(schema.default));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (schema.properties) {
|
|
152
|
+
for (const [propKey, propSchema] of Object.entries(schema.properties)) {
|
|
153
|
+
this.applySchemaDefaults([...path, propKey], propSchema as any);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Gets a value from the config
|
|
160
|
+
* @param path The path to get, supports dot notation (e.g. "foo.bar.baz")
|
|
161
|
+
* @param defaultValue The default value to return if the path doesn't exist
|
|
162
|
+
* @returns The value at the path, or the default value
|
|
163
|
+
*/
|
|
164
|
+
get<T>(path: string | string[], defaultValue: T): T {
|
|
165
|
+
if (typeof path === "string") {
|
|
166
|
+
path = path.split(".");
|
|
167
|
+
}
|
|
168
|
+
const resolved = resolvePath(this.values, path);
|
|
169
|
+
if (!resolved) {
|
|
170
|
+
return defaultValue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return (resolved.obj[resolved.key] ?? defaultValue) as T;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Sets a value in the config
|
|
178
|
+
* @param path The path to set, supports dot notation (e.g. "foo.bar.baz")
|
|
179
|
+
* @param value The value to set
|
|
180
|
+
*/
|
|
181
|
+
set<T>(path: string, value: T): void;
|
|
182
|
+
/**
|
|
183
|
+
* Sets a value in the config
|
|
184
|
+
* @param path The path to set (one path element per array element)
|
|
185
|
+
* @param value The value to set
|
|
186
|
+
*/
|
|
187
|
+
set<T>(path: string[], value: T): void;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Sets multiple values in the config
|
|
191
|
+
* @param values An object containing key-value pairs to set
|
|
192
|
+
*/
|
|
193
|
+
set(values: Record<string, any>): void;
|
|
194
|
+
|
|
195
|
+
set<T>(
|
|
196
|
+
keyOrValues: string | string[] | Record<string, any>,
|
|
197
|
+
value?: T,
|
|
198
|
+
): void {
|
|
199
|
+
if (typeof keyOrValues === "string") {
|
|
200
|
+
keyOrValues = keyOrValues.split(".");
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(keyOrValues)) {
|
|
203
|
+
const key = keyOrValues as string[];
|
|
204
|
+
|
|
205
|
+
const resolved = resolvePath(this.values, key, true);
|
|
206
|
+
if (resolved) {
|
|
207
|
+
resolved.obj[resolved.key] = value;
|
|
208
|
+
} else {
|
|
209
|
+
throw new Error(`Invalid key ${key}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Re-apply nested schema defaults so partial object writes don't wipe
|
|
213
|
+
// out defaults for nested properties the user didn't specify.
|
|
214
|
+
const schema = this.getSchemaAtPath(key);
|
|
215
|
+
if (schema) {
|
|
216
|
+
this.applySchemaDefaults(key, schema);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.validatePath(key);
|
|
220
|
+
} else {
|
|
221
|
+
for (const [key, val] of Object.entries(keyOrValues)) {
|
|
222
|
+
this.set(key, val);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
insert<T>(key: string | string[], value?: T): void {
|
|
228
|
+
if (typeof key === "string") {
|
|
229
|
+
key = key.split(".");
|
|
230
|
+
}
|
|
231
|
+
const resolved = resolvePath(this.values, key, true);
|
|
232
|
+
if (resolved) {
|
|
233
|
+
if (!Array.isArray(resolved.obj[resolved.key])) {
|
|
234
|
+
resolved.obj[resolved.key] = [];
|
|
235
|
+
}
|
|
236
|
+
resolved.obj[resolved.key].push(value);
|
|
237
|
+
} else {
|
|
238
|
+
throw new Error(`Invalid key ${key}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.validatePath(key);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Checks if a path exists in the config
|
|
246
|
+
* @param path The path to check, supports dot notation (e.g. "foo.bar.baz")
|
|
247
|
+
* @returns True if the path exists, false otherwise
|
|
248
|
+
*/
|
|
249
|
+
has(path: string | string[]): boolean {
|
|
250
|
+
if (typeof path === "string") {
|
|
251
|
+
path = path.split(".");
|
|
252
|
+
}
|
|
253
|
+
const resolved = resolvePath(this.values, path);
|
|
254
|
+
if (!resolved) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return resolved.key in resolved.obj;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Lists all configuration keys in the config
|
|
263
|
+
* @returns An array of keys
|
|
264
|
+
*/
|
|
265
|
+
keys(): string[] {
|
|
266
|
+
return Object.keys(this.values);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Validates a specific path against its schema
|
|
271
|
+
*/
|
|
272
|
+
private validatePath(path: string[]): void {
|
|
273
|
+
for (let i = path.length; i > 0; i--) {
|
|
274
|
+
const schemaPath = path.slice(0, i);
|
|
275
|
+
const schema = this.getSchemaAtPath(schemaPath);
|
|
276
|
+
if (schema) {
|
|
277
|
+
const valueAtPath = this.get(schemaPath, undefined);
|
|
278
|
+
if (valueAtPath !== undefined) {
|
|
279
|
+
const validator = new Validator(schema, "7");
|
|
280
|
+
const result = validator.validate(stripFunctions(valueAtPath));
|
|
281
|
+
if (!result.valid) {
|
|
282
|
+
const errorText = cfwFormatErrors(result.errors);
|
|
283
|
+
throw new Error(
|
|
284
|
+
`Validation error for ${schemaPath.join(".")}:> ${errorText}`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
break; // Only validate the most specific schema
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Gets the schema at a specific path
|
|
295
|
+
*/
|
|
296
|
+
private getSchemaAtPath(path: string[]): any | null {
|
|
297
|
+
let current = this.schemas;
|
|
298
|
+
for (const part of path) {
|
|
299
|
+
if (!current.properties || !current.properties[part]) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
current = current.properties[part];
|
|
303
|
+
}
|
|
304
|
+
return current.type ? current : null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Resolves a configuration path to the containing object and final key
|
|
310
|
+
* @param path The path to resolve (e.g. ["foo", "bar", "baz"])
|
|
311
|
+
* @param create Whether to create objects along the path if they don't exist
|
|
312
|
+
* @returns The containing object and the final key, or null if the path cannot be resolved
|
|
313
|
+
*/
|
|
314
|
+
function resolvePath(
|
|
315
|
+
inObject: Record<string, any>,
|
|
316
|
+
path: string[],
|
|
317
|
+
create = false,
|
|
318
|
+
): { obj: any; key: string } | null {
|
|
319
|
+
path = [...path];
|
|
320
|
+
|
|
321
|
+
const lastKey = path.pop()!;
|
|
322
|
+
let current = inObject;
|
|
323
|
+
|
|
324
|
+
for (const part of path) {
|
|
325
|
+
if (current[part] === undefined) {
|
|
326
|
+
if (create) {
|
|
327
|
+
current[part] = {};
|
|
328
|
+
} else {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
} else if (typeof current[part] !== "object" || current[part] === null) {
|
|
332
|
+
if (create) {
|
|
333
|
+
current[part] = {};
|
|
334
|
+
} else {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
current = current[part];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return { obj: current, key: lastKey };
|
|
343
|
+
}
|
|
@@ -4,7 +4,7 @@ export type KvQueryOptions = {
|
|
|
4
4
|
prefix?: KvKey;
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export type KvPrimitives = {
|
|
8
8
|
batchGet(keys: KvKey[]): Promise<(any | undefined)[]>;
|
|
9
9
|
|
|
10
10
|
batchSet(entries: KV[]): Promise<void>;
|
|
@@ -22,4 +22,4 @@ export interface KvPrimitives {
|
|
|
22
22
|
clear(): Promise<void>;
|
|
23
23
|
|
|
24
24
|
close(): void;
|
|
25
|
-
}
|
|
25
|
+
};
|
|
@@ -3,7 +3,11 @@ export const wikiLinkRegex =
|
|
|
3
3
|
export const mdLinkRegex =
|
|
4
4
|
/!?\[(?<title>[^\]\\]*(?:\\.[^\]\\]*)*)\]\((?<url>.+)\)/g;
|
|
5
5
|
export const tagRegex =
|
|
6
|
-
/#(?:(?:\d*[^\d\s!@#$%^&*(),.?":{}|<>\\][^\s!@#$%^&*(),.?":{}|<>\\]*)|(?:<[^>\n]+>))/;
|
|
6
|
+
/#(?:(?:\d*[^\d\s!@#$%^&*(),.?":{}|<>\\';\/–—][^\s!@#$%^&*(),.?":{}|<>\\]*)|(?:<[^>\n]+>))(?<!-)/;
|
|
7
|
+
// Dots and apostrophes may only join name segments, so `@pete.` and `@Chris'`
|
|
8
|
+
// end before them; a possessive `'s` never joins, so `@Foo's` is `@Foo`.
|
|
9
|
+
export const atMentionRegex =
|
|
10
|
+
/@(?:\d*[^\d\s!@#$%^&*(),.?":{}|<>\\\/'’][^\s!@#$%^&*(),.?":{}|<>\\\/'’]*(?:(?:\.|['’](?,.?":{}|<>\\\/'’])))[^\s!@#$%^&*(),.?":{}|<>\\\/'’]+)*)/;
|
|
7
11
|
export const nakedUrlRegex =
|
|
8
12
|
/(^https?:\/\/([-a-zA-Z0-9@:%_+~#=]|(?:[.](?!(\s|$)))){1,256})(([-a-zA-Z0-9(@:%_+~#?&=/]|(?:[.,:;)](?!(\s|$))))*)/;
|
|
9
13
|
export const frontmatterQuotesRegex = /["'].*["']/g;
|
|
@@ -19,6 +19,7 @@ export class CommandHook
|
|
|
19
19
|
constructor(
|
|
20
20
|
private readOnly: boolean,
|
|
21
21
|
private additionalCommands: Map<string, Command>,
|
|
22
|
+
private disableServiceWorker = false,
|
|
22
23
|
) {
|
|
23
24
|
super();
|
|
24
25
|
}
|
|
@@ -39,7 +40,6 @@ export class CommandHook
|
|
|
39
40
|
cmd: Command,
|
|
40
41
|
) {
|
|
41
42
|
if (commands.has(name)) {
|
|
42
|
-
// Existing command, let's do some inline patching
|
|
43
43
|
const existingCommand = commands.get(name)!;
|
|
44
44
|
const command: Command = {
|
|
45
45
|
...existingCommand,
|
|
@@ -66,11 +66,12 @@ export class CommandHook
|
|
|
66
66
|
if (cmd.requireMode === "rw" && this.readOnly) {
|
|
67
67
|
continue;
|
|
68
68
|
}
|
|
69
|
+
if (cmd.requireServiceWorker && this.disableServiceWorker) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
69
72
|
commands.set(name, cmd);
|
|
70
73
|
}
|
|
71
|
-
// Add commands from plugs
|
|
72
74
|
if (!this.system) {
|
|
73
|
-
// Not initialized yet
|
|
74
75
|
return commands;
|
|
75
76
|
}
|
|
76
77
|
for (const plug of this.system.loadedPlugs.values()) {
|
|
@@ -82,7 +83,9 @@ export class CommandHook
|
|
|
82
83
|
}
|
|
83
84
|
const cmd = functionDef.command;
|
|
84
85
|
if (cmd.requireMode === "rw" && this.readOnly) {
|
|
85
|
-
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (cmd.requireServiceWorker && this.disableServiceWorker) {
|
|
86
89
|
continue;
|
|
87
90
|
}
|
|
88
91
|
this.mergeCommand(commands, cmd.name, {
|
|
@@ -102,7 +105,7 @@ export class CommandHook
|
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
public buildAllCommandsAndEmit() {
|
|
105
|
-
this.emit("commandsUpdated", this.buildAllCommands());
|
|
108
|
+
void this.emit("commandsUpdated", this.buildAllCommands());
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
apply(system: System<CommandHookT>): void {
|
|
@@ -112,7 +115,6 @@ export class CommandHook
|
|
|
112
115
|
this.throttledBuildAllCommandsAndEmit();
|
|
113
116
|
},
|
|
114
117
|
});
|
|
115
|
-
// On next tick
|
|
116
118
|
setTimeout(() => {
|
|
117
119
|
this.throttledBuildAllCommandsAndEmit();
|
|
118
120
|
});
|
|
@@ -3,6 +3,10 @@ import type { System } from "../system.ts";
|
|
|
3
3
|
import type { EventHookI } from "../eventhook.ts";
|
|
4
4
|
import type { EventHookT } from "@silverbulletmd/silverbullet/type/manifest";
|
|
5
5
|
import type { Config } from "../../config.ts";
|
|
6
|
+
import type { Ref } from "@silverbulletmd/silverbullet/lib/ref";
|
|
7
|
+
import { listenerDefinition, type DefinedListener } from "../syscalls/event.ts";
|
|
8
|
+
|
|
9
|
+
export type EventResultWithSource = { value: any; definition: Ref | null };
|
|
6
10
|
|
|
7
11
|
// System events:
|
|
8
12
|
// - plug:load (plugName: string)
|
|
@@ -66,10 +70,33 @@ export class EventHook implements EventHookI {
|
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
async dispatchEvent(eventName: string, ...args: any[]): Promise<any[]> {
|
|
73
|
+
return (await this.dispatch(eventName, args, false)).map(
|
|
74
|
+
({ value }) => value,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async dispatchEventStrict(eventName: string, ...args: any[]): Promise<any[]> {
|
|
79
|
+
return (await this.dispatch(eventName, args, true)).map(
|
|
80
|
+
({ value }) => value,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async dispatchEventWithSources(
|
|
85
|
+
eventName: string,
|
|
86
|
+
...args: any[]
|
|
87
|
+
): Promise<EventResultWithSource[]> {
|
|
88
|
+
return this.dispatch(eventName, args, false);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async dispatch(
|
|
92
|
+
eventName: string,
|
|
93
|
+
args: any[],
|
|
94
|
+
strict: boolean,
|
|
95
|
+
): Promise<EventResultWithSource[]> {
|
|
69
96
|
if (!this.system) {
|
|
70
97
|
throw new Error("Event hook is not initialized");
|
|
71
98
|
}
|
|
72
|
-
const promises: Promise<any
|
|
99
|
+
const promises: { promise: Promise<any>; definition: Ref | null }[] = [];
|
|
73
100
|
for (const plug of this.system.loadedPlugs.values()) {
|
|
74
101
|
const manifest = plug.manifest;
|
|
75
102
|
for (const [name, functionDef] of Object.entries(manifest!.functions)) {
|
|
@@ -79,11 +106,10 @@ export class EventHook implements EventHookI {
|
|
|
79
106
|
event === eventName ||
|
|
80
107
|
eventNameToRegex(event).test(eventName)
|
|
81
108
|
) {
|
|
82
|
-
// Only dispatch functions that can run in this environment
|
|
83
109
|
if (plug.canInvoke(name)) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
(async () => {
|
|
110
|
+
promises.push({
|
|
111
|
+
definition: null,
|
|
112
|
+
promise: (async () => {
|
|
87
113
|
try {
|
|
88
114
|
return await plug.invoke(name, args);
|
|
89
115
|
} catch (e: any) {
|
|
@@ -93,7 +119,7 @@ export class EventHook implements EventHookI {
|
|
|
93
119
|
throw e;
|
|
94
120
|
}
|
|
95
121
|
})(),
|
|
96
|
-
);
|
|
122
|
+
});
|
|
97
123
|
}
|
|
98
124
|
}
|
|
99
125
|
}
|
|
@@ -101,31 +127,28 @@ export class EventHook implements EventHookI {
|
|
|
101
127
|
}
|
|
102
128
|
}
|
|
103
129
|
|
|
104
|
-
// Local listeners
|
|
105
130
|
for (const [name, localListeners] of this.localListeners) {
|
|
106
131
|
if (eventNameToRegex(name).test(eventName)) {
|
|
107
132
|
for (const localListener of localListeners) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
(async () => {
|
|
133
|
+
promises.push({
|
|
134
|
+
definition: null,
|
|
135
|
+
promise: (async () => {
|
|
111
136
|
return await Promise.resolve(localListener(...args));
|
|
112
137
|
})(),
|
|
113
|
-
);
|
|
138
|
+
});
|
|
114
139
|
}
|
|
115
140
|
}
|
|
116
141
|
}
|
|
117
142
|
|
|
118
|
-
// Space Lua listeners
|
|
119
143
|
if (this.config) {
|
|
120
|
-
const configListeners: Record<string,
|
|
121
|
-
"eventListeners",
|
|
122
|
-
{},
|
|
123
|
-
);
|
|
144
|
+
const configListeners: Record<string, DefinedListener[]> =
|
|
145
|
+
this.config.get("eventListeners", {});
|
|
124
146
|
for (const [name, listeners] of Object.entries(configListeners)) {
|
|
125
147
|
if (eventNameToRegex(name).test(eventName)) {
|
|
126
148
|
for (const listener of listeners) {
|
|
127
|
-
promises.push(
|
|
128
|
-
|
|
149
|
+
promises.push({
|
|
150
|
+
definition: listener[listenerDefinition] ?? null,
|
|
151
|
+
promise: (async () => {
|
|
129
152
|
return await Promise.resolve(
|
|
130
153
|
listener({
|
|
131
154
|
name: eventName,
|
|
@@ -134,15 +157,25 @@ export class EventHook implements EventHookI {
|
|
|
134
157
|
}),
|
|
135
158
|
);
|
|
136
159
|
})(),
|
|
137
|
-
);
|
|
160
|
+
});
|
|
138
161
|
}
|
|
139
162
|
}
|
|
140
163
|
}
|
|
141
164
|
}
|
|
142
165
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
166
|
+
const settled = await Promise.allSettled(
|
|
167
|
+
promises.map(({ promise }) => promise),
|
|
168
|
+
);
|
|
169
|
+
if (strict) {
|
|
170
|
+
const failure = settled.find((result) => result.status === "rejected");
|
|
171
|
+
if (failure?.status === "rejected") throw failure.reason;
|
|
172
|
+
}
|
|
173
|
+
return settled
|
|
174
|
+
.map((result, index) => ({
|
|
175
|
+
result,
|
|
176
|
+
definition: promises[index].definition,
|
|
177
|
+
}))
|
|
178
|
+
.filter(({ result }) => {
|
|
146
179
|
if (result.status === "rejected") {
|
|
147
180
|
console.error(
|
|
148
181
|
"Error while dispatching event",
|
|
@@ -153,8 +186,11 @@ export class EventHook implements EventHookI {
|
|
|
153
186
|
}
|
|
154
187
|
return result.status === "fulfilled";
|
|
155
188
|
})
|
|
156
|
-
.map((result) =>
|
|
157
|
-
|
|
189
|
+
.map(({ result, definition }) => ({
|
|
190
|
+
value: result.status === "fulfilled" ? result.value : undefined,
|
|
191
|
+
definition,
|
|
192
|
+
}))
|
|
193
|
+
.filter(({ value }) => value != null); // This keeps non-null/undefined results
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
apply(system: System<EventHookT>): void {
|
|
@@ -48,7 +48,6 @@ export class MQHook implements Hook<MQHookT> {
|
|
|
48
48
|
|
|
49
49
|
reloadQueues() {
|
|
50
50
|
this.stop();
|
|
51
|
-
// Plug based subscriptions
|
|
52
51
|
for (const plug of this.system.loadedPlugs.values()) {
|
|
53
52
|
if (!plug.manifest) {
|
|
54
53
|
continue;
|
|
@@ -96,14 +95,12 @@ export class MQHook implements Hook<MQHookT> {
|
|
|
96
95
|
}
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
|
-
// Space Lua based subscriptions
|
|
100
98
|
const configListeners: Record<string, MQListenerSpec[]> = this.config.get(
|
|
101
99
|
"mqSubscriptions",
|
|
102
100
|
{},
|
|
103
101
|
);
|
|
104
102
|
for (const [queue, listeners] of Object.entries(configListeners)) {
|
|
105
103
|
for (const listener of listeners) {
|
|
106
|
-
// console.log("Subscribing to", queue, listener);
|
|
107
104
|
this.subscriptions.push(
|
|
108
105
|
this.mq.subscribe(
|
|
109
106
|
queue,
|