@silverbulletmd/silverbullet 2.9.0 → 2.11.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.
Files changed (123) hide show
  1. package/README.md +38 -25
  2. package/client/asset_bundle/bundle.ts +0 -1
  3. package/client/config.ts +343 -0
  4. package/client/markdown_parser/constants.ts +3 -1
  5. package/client/plugos/hooks/command.ts +0 -5
  6. package/client/plugos/hooks/event.ts +0 -6
  7. package/client/plugos/hooks/mq.ts +0 -3
  8. package/client/plugos/hooks/slash_command.ts +1 -7
  9. package/client/plugos/hooks/syscall.ts +24 -14
  10. package/client/plugos/manifest_cache.ts +0 -32
  11. package/client/plugos/plug.ts +0 -3
  12. package/client/plugos/plug_compile.ts +3 -13
  13. package/client/plugos/sandboxes/worker_sandbox.ts +19 -9
  14. package/client/plugos/syscalls/asset.ts +61 -23
  15. package/client/plugos/syscalls/clientStore.ts +32 -6
  16. package/client/plugos/syscalls/client_code_widget.ts +9 -4
  17. package/client/plugos/syscalls/code_widget.ts +26 -11
  18. package/client/plugos/syscalls/config.ts +146 -30
  19. package/client/plugos/syscalls/datastore.ts +146 -40
  20. package/client/plugos/syscalls/editor.ts +1518 -722
  21. package/client/plugos/syscalls/event.ts +44 -13
  22. package/client/plugos/syscalls/fetch.ts +120 -83
  23. package/client/plugos/syscalls/icon.ts +46 -0
  24. package/client/plugos/syscalls/index.ts +267 -121
  25. package/client/plugos/syscalls/jsonschema.ts +107 -9
  26. package/client/plugos/syscalls/language.ts +38 -12
  27. package/client/plugos/syscalls/markdown.ts +166 -50
  28. package/client/plugos/syscalls/mq.ts +92 -20
  29. package/client/plugos/syscalls/navigator.ts +143 -0
  30. package/client/plugos/syscalls/schema_introspection.ts +33 -0
  31. package/client/plugos/syscalls/search.ts +44 -0
  32. package/client/plugos/syscalls/service_registry.ts +74 -20
  33. package/client/plugos/syscalls/shell.ts +51 -21
  34. package/client/plugos/syscalls/space.ts +412 -113
  35. package/client/plugos/syscalls/sync.ts +103 -20
  36. package/client/plugos/syscalls/system.ts +274 -139
  37. package/client/plugos/system.ts +28 -10
  38. package/client/plugos/worker_runtime.ts +0 -2
  39. package/client/space_lua/aggregates.ts +0 -16
  40. package/client/space_lua/api_documentation.ts +107 -0
  41. package/client/space_lua/ast.ts +13 -5
  42. package/client/space_lua/ast_narrow.ts +0 -5
  43. package/client/space_lua/budget.ts +113 -0
  44. package/client/space_lua/budget_ui.ts +49 -0
  45. package/client/space_lua/eval.ts +125 -92
  46. package/client/space_lua/labels.ts +0 -4
  47. package/client/space_lua/numeric.ts +0 -5
  48. package/client/space_lua/parse.ts +243 -125
  49. package/client/space_lua/pretty_print.ts +128 -54
  50. package/client/space_lua/quarantine.ts +127 -0
  51. package/client/space_lua/query_collection.ts +7 -51
  52. package/client/space_lua/query_env.ts +0 -2
  53. package/client/space_lua/render_lua_markdown.ts +2 -7
  54. package/client/space_lua/render_widget.ts +173 -0
  55. package/client/space_lua/runtime.ts +133 -92
  56. package/client/space_lua/stdlib/crypto.ts +8 -3
  57. package/client/space_lua/stdlib/encoding.ts +27 -9
  58. package/client/space_lua/stdlib/format.ts +0 -12
  59. package/client/space_lua/stdlib/js.ts +136 -23
  60. package/client/space_lua/stdlib/load.ts +24 -16
  61. package/client/space_lua/stdlib/math.ts +331 -133
  62. package/client/space_lua/stdlib/net.ts +57 -13
  63. package/client/space_lua/stdlib/os.ts +111 -54
  64. package/client/space_lua/stdlib/pattern.ts +10 -15
  65. package/client/space_lua/stdlib/space_lua.ts +344 -21
  66. package/client/space_lua/stdlib/string.ts +282 -104
  67. package/client/space_lua/stdlib/string_pack.ts +58 -22
  68. package/client/space_lua/stdlib/table.ts +195 -45
  69. package/client/space_lua/stdlib.ts +354 -125
  70. package/client/space_lua/syscalls.ts +270 -0
  71. package/client/space_lua/tonumber.ts +0 -9
  72. package/dist/plug-compile.js +2 -3
  73. package/package.json +15 -5
  74. package/plug-api/lib/async.ts +1 -6
  75. package/plug-api/lib/collation.ts +22 -0
  76. package/plug-api/lib/crypto.ts +16 -15
  77. package/plug-api/lib/dates.ts +33 -0
  78. package/plug-api/lib/fuzzy.ts +282 -0
  79. package/plug-api/lib/json.ts +0 -6
  80. package/plug-api/lib/limited_map.ts +0 -2
  81. package/plug-api/lib/link_write.ts +27 -0
  82. package/plug-api/{ui → lib}/panel_styles.ts +4 -3
  83. package/plug-api/lib/ref.ts +124 -5
  84. package/plug-api/lib/resolve.ts +0 -1
  85. package/plug-api/lib/resolve_path.ts +290 -0
  86. package/plug-api/lib/shortcut.ts +17 -1
  87. package/plug-api/lib/tags.ts +0 -3
  88. package/plug-api/lib/transclusion.ts +28 -1
  89. package/plug-api/lib/tree.ts +0 -5
  90. package/plug-api/lib/yaml.ts +3 -42
  91. package/plug-api/syscall.ts +1 -5
  92. package/plug-api/syscalls/config.ts +7 -5
  93. package/plug-api/syscalls/editor.ts +70 -208
  94. package/plug-api/syscalls/icon.ts +18 -0
  95. package/plug-api/syscalls/index.ts +39 -5
  96. package/plug-api/syscalls/jsonschema.ts +9 -0
  97. package/plug-api/syscalls/lua.ts +7 -0
  98. package/plug-api/syscalls/search.ts +11 -0
  99. package/plug-api/syscalls/shell.ts +0 -1
  100. package/plug-api/syscalls/space.ts +122 -5
  101. package/plug-api/syscalls/sync.ts +23 -2
  102. package/plug-api/syscalls/system.ts +53 -0
  103. package/plug-api/syscalls.ts +2 -0
  104. package/plug-api/system_mock.ts +15 -2
  105. package/plug-api/types/client.ts +4 -3
  106. package/plug-api/types/datastore.ts +11 -2
  107. package/plug-api/types/index.ts +62 -1
  108. package/plug-api/types/manifest.ts +22 -4
  109. package/plug-api/types/profile.ts +13 -0
  110. package/plug-api/types/revisions.ts +67 -0
  111. package/plug-api/ui/cx.ts +1 -3
  112. package/plug-api/ui/description.ts +64 -0
  113. package/plug-api/ui/hover.ts +69 -0
  114. package/plug-api/ui/index.ts +61 -2
  115. package/plug-api/ui/scroll.ts +30 -0
  116. package/plug-api/ui/slugify.ts +44 -0
  117. package/plug-api/ui/tree_model.ts +263 -0
  118. package/plug-api/ui/tree_types.ts +66 -0
  119. package/plug-api/ui/use_fit_collapse.ts +67 -0
  120. package/plugs/builtin_plugs.ts +0 -1
  121. package/plugs/index/types.ts +9 -0
  122. package/client/plugos/syscalls/lua.ts +0 -81
  123. package/plug-api/lib/memory_cache.ts +0 -21
package/README.md CHANGED
@@ -27,27 +27,30 @@ You were told there’s no such thing as a silver bullet. You were told wrong.
27
27
  Check out the [instructions](https://silverbullet.md/Install).
28
28
 
29
29
  ## Developing SilverBullet
30
- SilverBullet's frontend 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.
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.
31
31
 
32
- The server backend is written in Go.
32
+ The server backend is written in [Rust](https://www.rust-lang.org/) (a Cargo workspace). It serves the pre-built client bundle, the file/space HTTP API, authentication, and a headless-Chrome runtime for server-side Lua.
33
33
 
34
34
  ## Code structure
35
- * `client/`: The SilverBullet client, implemented with TypeScript
36
- * `server/`: The SilverBullet server, written in Go
37
- * `plugs`: Set of built-in plugs that are distributed with SilverBullet
38
- * `libraries`: A set of libraries (space scripts, page templates, slash templates) distributed with SilverBullet
39
- * `plug-api/`: Useful APIs for use in plugs
35
+ * `bin/silverbullet/`: The standalone `silverbullet` server binary (Rust) (this is the one you generally run)
36
+ * `bin/sb/`: The `sb` CLI client (Rust)
37
+ * `bin/plug-compile.ts`: the plug compiler
38
+ * `client/`: The SilverBullet client (browser, TypeScript)
39
+ * `server/`: The SilverBullet server library (Rust): HTTP router, handlers, auth, runtime seam
40
+ * `server-common/`: Shared Rust crate (space primitives, shared types)
41
+ * `server-runtime-chrome/`: Headless-Chrome runtime backend (Rust)
42
+ * `plugs`: Set of built-in plugs (TypeScript) that are distributed with SilverBullet
43
+ * `libraries`: A set of libraries (space scripts, page templates, slash templates) distributed with SilverBullet (Space Lua)
44
+ * `plug-api/`: Useful APIs for use in plugs and published to NPM
40
45
  * `lib/`: Useful libraries to be used in plugs
41
46
  * `syscalls/`: TypeScript wrappers around syscalls
42
47
  * `types/`: Various (client) types that can be references from plugs
43
- * `bin`
44
- * `plug_compile.ts` the plug compiler
45
48
  * `scripts/`: Useful scripts
46
- * `website/`: silverbullet.md website content
49
+ * `docs/`: documentation (also serves a silverbullet.md website content)
47
50
 
48
51
  ### Requirements
49
- * [Node.js](https://nodejs.org/) 24+ and npm 10+: Used to build the frontend and plugs
50
- * [Go](https://go.dev/): Used to build the backend
52
+ * [Node.js](https://nodejs.org/) 24+ and npm 10+: Used to build the frontend (client) and plugs
53
+ * [Rust](https://www.rust-lang.org/tools/install) (stable, via `rustup`): Used to build the server
51
54
 
52
55
  The project includes `.nvmrc` and `.node-version` files. If you use [nvm](https://github.com/nvm-sh/nvm) or another Node version manager, it will automatically use the correct Node.js version:
53
56
 
@@ -55,35 +58,45 @@ The project includes `.nvmrc` and `.node-version` files. If you use [nvm](https:
55
58
  nvm use # If using nvm
56
59
  ```
57
60
 
58
- It's convenient to also install [air](https://github.com/air-verse/air) for development, this will automatically rebuild both the frontend and backend when changes are made:
61
+ First, install dependencies:
59
62
 
60
63
  ```shell
61
- go install github.com/air-verse/air@latest
64
+ make setup
62
65
  ```
63
- Make sure your `$GOPATH/bin` is in your $PATH.
64
66
 
65
- First, install dependencies:
67
+ To create a (release) build:
66
68
 
67
69
  ```shell
68
- make setup
70
+ make
69
71
  ```
70
72
 
71
- To build everything and run the server (which automatically restarts upon file changing):
73
+ This will produce `silverbullet` and `cli` binaries in `./target/release/`, run `./target/release/silverbullet` to run the server.
74
+
75
+ ### Development workflow
76
+
77
+ SilverBullet has two halves you rebuild **independently**:
78
+
79
+ * The **client** (TypeScript: `client/`) is built by ESBuild into `client_bundle/`, which the server then serves.
80
+ * The **server** (Rust: `server/`, `server-common/`, `server-runtime-chrome/`, `bin/silverbullet/`) is a compiled binary.
81
+
82
+ Run the server in development with `cargo run`. A **debug** build serves the client bundle **live from `client_bundle/` on disk** (a release build embeds it instead):
72
83
 
73
84
  ```shell
74
- air <PATH-TO-YOUR-SPACE>
85
+ npm run build
86
+ cargo run <PATH-TO-YOUR-DATA-FOLDER>
75
87
  ```
76
88
 
77
- Alternatively, to build:
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:
78
90
 
79
91
  ```shell
80
- make
92
+ npm run build
81
93
  ```
82
94
 
83
- To run the resulting server:
95
+ To build a self-contained **release** binary (with the client bundle embedded), and run it:
84
96
 
85
97
  ```shell
86
- ./silverbullet <PATH-TO-YOUR-SPACE>
98
+ make
99
+ ./target/release/silverbullet <PATH-TO-YOUR-DATA-FOLDER>
87
100
  ```
88
101
 
89
102
  ### Useful development tasks
@@ -102,7 +115,7 @@ make bench
102
115
  ```
103
116
 
104
117
  ### Build a docker container
105
- Note, you do not need Node.js nor Go locally installed for this to work:
118
+ Note, you do not need Node.js nor Rust locally installed for this to work:
106
119
 
107
120
  ```shell
108
121
  docker build -t silverbullet .
@@ -110,5 +123,5 @@ docker build -t silverbullet .
110
123
 
111
124
  To run:
112
125
  ```shell
113
- docker run -p 3000:3000 -v <PATH-TO-YOUR-SPACE>:/space silverbullet
126
+ docker run -p 3000:3000 -v <PATH-TO-YOUR-DATA-FOLDER>:/data silverbullet
114
127
  ```
@@ -66,7 +66,6 @@ export class AssetBundle {
66
66
  data: Uint8Array,
67
67
  mtime: number = Date.now(),
68
68
  ) {
69
- // Replace \ with / for windows
70
69
  path = path.replaceAll("\\", "/");
71
70
  this.bundle[path] = {
72
71
  data: base64EncodedDataUrl(mimeType, data),
@@ -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
+ }
@@ -3,7 +3,9 @@ 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
+ export const atMentionRegex =
8
+ /@(?:\d*[^\d\s!@#$%^&*(),.?":{}|<>\\\/][^\s!@#$%^&*(),.?":{}|<>\\\/]*(?:\.[^\s!@#$%^&*(),.?":{}|<>\\\/]+)*)/;
7
9
  export const nakedUrlRegex =
8
10
  /(^https?:\/\/([-a-zA-Z0-9@:%_+~#=]|(?:[.](?!(\s|$)))){1,256})(([-a-zA-Z0-9(@:%_+~#?&=/]|(?:[.,:;)](?!(\s|$))))*)/;
9
11
  export const frontmatterQuotesRegex = /["'].*["']/g;
@@ -39,7 +39,6 @@ export class CommandHook
39
39
  cmd: Command,
40
40
  ) {
41
41
  if (commands.has(name)) {
42
- // Existing command, let's do some inline patching
43
42
  const existingCommand = commands.get(name)!;
44
43
  const command: Command = {
45
44
  ...existingCommand,
@@ -68,9 +67,7 @@ export class CommandHook
68
67
  }
69
68
  commands.set(name, cmd);
70
69
  }
71
- // Add commands from plugs
72
70
  if (!this.system) {
73
- // Not initialized yet
74
71
  return commands;
75
72
  }
76
73
  for (const plug of this.system.loadedPlugs.values()) {
@@ -82,7 +79,6 @@ export class CommandHook
82
79
  }
83
80
  const cmd = functionDef.command;
84
81
  if (cmd.requireMode === "rw" && this.readOnly) {
85
- // Bit hacky, but don't expose commands that require write mode in read-only mode
86
82
  continue;
87
83
  }
88
84
  this.mergeCommand(commands, cmd.name, {
@@ -112,7 +108,6 @@ export class CommandHook
112
108
  this.throttledBuildAllCommandsAndEmit();
113
109
  },
114
110
  });
115
- // On next tick
116
111
  setTimeout(() => {
117
112
  this.throttledBuildAllCommandsAndEmit();
118
113
  });
@@ -79,9 +79,7 @@ export class EventHook implements EventHookI {
79
79
  event === eventName ||
80
80
  eventNameToRegex(event).test(eventName)
81
81
  ) {
82
- // Only dispatch functions that can run in this environment
83
82
  if (plug.canInvoke(name)) {
84
- // Queue the promise
85
83
  promises.push(
86
84
  (async () => {
87
85
  try {
@@ -101,11 +99,9 @@ export class EventHook implements EventHookI {
101
99
  }
102
100
  }
103
101
 
104
- // Local listeners
105
102
  for (const [name, localListeners] of this.localListeners) {
106
103
  if (eventNameToRegex(name).test(eventName)) {
107
104
  for (const localListener of localListeners) {
108
- // Queue the promise
109
105
  promises.push(
110
106
  (async () => {
111
107
  return await Promise.resolve(localListener(...args));
@@ -115,7 +111,6 @@ export class EventHook implements EventHookI {
115
111
  }
116
112
  }
117
113
 
118
- // Space Lua listeners
119
114
  if (this.config) {
120
115
  const configListeners: Record<string, Function[]> = this.config.get(
121
116
  "eventListeners",
@@ -140,7 +135,6 @@ export class EventHook implements EventHookI {
140
135
  }
141
136
  }
142
137
 
143
- // Wait for all promises to resolve
144
138
  return (await Promise.allSettled(promises))
145
139
  .filter((result) => {
146
140
  if (result.status === "rejected") {
@@ -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,
@@ -46,7 +46,6 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
46
46
  });
47
47
  }
48
48
  }
49
- // Iterate over script defined slash commands
50
49
  for (const command of Object.values(
51
50
  this.client.config.get<Record<string, SlashCommand>>("slashCommands", {}),
52
51
  )) {
@@ -70,13 +69,12 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
70
69
  // the user is referring to preceding text, not what comes after.
71
70
  const currentNode = syntaxTree(ctx.state).resolveInner(ctx.pos, -1);
72
71
  if (
73
- currentNode.type.name === "CommentBlock" ||
72
+ currentNode.type.name === "CommentMarkerBlock" ||
74
73
  currentNode.type.name === "Link"
75
74
  ) {
76
75
  return null;
77
76
  }
78
77
 
79
- // Check if the slash command is available in the current context
80
78
  const parentNodes = this.client.extractParentNodes(ctx.state, currentNode);
81
79
  for (const def of this.slashCommands) {
82
80
  if (
@@ -99,7 +97,6 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
99
97
  detail: def.description,
100
98
  boost: def.priority,
101
99
  apply: () => {
102
- // Delete slash command part
103
100
  this.client.editorView.dispatch({
104
101
  changes: {
105
102
  from: prefix!.from + prefixText.indexOf("/"),
@@ -107,7 +104,6 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
107
104
  insert: "",
108
105
  },
109
106
  });
110
- // Replace with whatever the completion is
111
107
  safeRun(async () => {
112
108
  await def.run!();
113
109
  this.client.focus();
@@ -126,7 +122,6 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
126
122
  detail: slashCompletion.detail,
127
123
  boost: slashCompletion.order && -slashCompletion.order,
128
124
  apply: () => {
129
- // Delete slash command part
130
125
  this.client.editorView.dispatch({
131
126
  changes: {
132
127
  from: prefix!.from + prefixText.indexOf("/"),
@@ -134,7 +129,6 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
134
129
  insert: "",
135
130
  },
136
131
  });
137
- // Replace with whatever the completion is
138
132
  safeRun(async () => {
139
133
  await this.client.clientSystem.system.invokeFunction(
140
134
  slashCompletion.invoke,