@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
package/LICENSE.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Copyright 2022, Zef Hemel
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
5
|
+
the Software without restriction, including without limitation the rights to
|
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
7
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
8
|
+
subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
# SilverBullet
|
|
7
|
+
SilverBullet is a Programmable, Private, Browser-based, Open Source, Self Hosted, Personal Knowledge Management Platform.
|
|
8
|
+
|
|
9
|
+
_Yowza!_ That surely is a lot of adjectives to describe a browser-based Markdown editor programmable with Lua.
|
|
10
|
+
|
|
11
|
+
Let’s get more specific.
|
|
12
|
+
|
|
13
|
+
In SilverBullet you keep your content as a collection of Markdown Pages (called a Space). You navigate your space using the Page Picker like a traditional notes app, or through Links like a wiki (except they are bi-directional).
|
|
14
|
+
|
|
15
|
+
If you are the **writer** type, you’ll appreciate SilverBullet as a clean Markdown editor with Live Preview. If you have more of an **outliner** personality, SilverBullet has Outlining tools for you. Productivity freak? Have a look at Tasks. More of a **database** person? You will appreciate Objects and Queries.
|
|
16
|
+
|
|
17
|
+
And if you are comfortable **programming** a little bit — now we’re really talking. You will love _dynamically generating content_ with Space Lua (SilverBullet’s Lua dialect), or to use it to create custom Commands, Page Templates or Widgets.
|
|
18
|
+
|
|
19
|
+
[Much more detail can be found on silverbullet.md](https://silverbullet.md)
|
|
20
|
+
|
|
21
|
+
## Installing SilverBullet
|
|
22
|
+
Check out the [instructions](https://silverbullet.md/Install).
|
|
23
|
+
|
|
24
|
+
## Developing SilverBullet
|
|
25
|
+
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)) running through Deno is used to build both the front-end.
|
|
26
|
+
|
|
27
|
+
The server backend is written in Go.
|
|
28
|
+
|
|
29
|
+
If you're considering contributing changes, be aware of the [LLM use policy](https://silverbullet.md/LLM%20Use).
|
|
30
|
+
|
|
31
|
+
## Code structure
|
|
32
|
+
* `client/`: The SilverBullet client, implemented with TypeScript
|
|
33
|
+
* `server/`: The SilverBullet server, written in Go
|
|
34
|
+
* `plugs`: Set of built-in plugs that are distributed with SilverBullet
|
|
35
|
+
* `libraries`: A set of libraries (space scripts, page templates, slash templates) distributed with SilverBullet
|
|
36
|
+
* `plug-api/`: Useful APIs for use in plugs
|
|
37
|
+
* `lib/`: Useful libraries to be used in plugs
|
|
38
|
+
* `syscalls/`: TypeScript wrappers around syscalls
|
|
39
|
+
* `types/`: Various (client) types that can be references from plugs
|
|
40
|
+
* `bin`
|
|
41
|
+
* `plug_compile.ts` the plug compiler
|
|
42
|
+
* `scripts/`: Useful scripts
|
|
43
|
+
* `website/`: silverbullet.md website content
|
|
44
|
+
|
|
45
|
+
### Requirements
|
|
46
|
+
* [Deno](https://deno.com/): Used to build the frontend and plugs
|
|
47
|
+
* [Go](https://go.dev/): Used to build the backend
|
|
48
|
+
|
|
49
|
+
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:
|
|
50
|
+
|
|
51
|
+
```shell
|
|
52
|
+
go install github.com/air-verse/air@latest
|
|
53
|
+
```
|
|
54
|
+
Make sure your `$GOPATH/bin` is in your $PATH.
|
|
55
|
+
|
|
56
|
+
To build everything and run the server:
|
|
57
|
+
|
|
58
|
+
```shell
|
|
59
|
+
air <PATH-TO-YOUR-SPACE>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Alternatively, to build:
|
|
63
|
+
|
|
64
|
+
```shell
|
|
65
|
+
make
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
To run the resulting server:
|
|
69
|
+
|
|
70
|
+
```shell
|
|
71
|
+
./silverbullet <PATH-TO-YOUR-SPACE>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Useful development tasks
|
|
75
|
+
|
|
76
|
+
```shell
|
|
77
|
+
# Clean all generated files
|
|
78
|
+
make clean
|
|
79
|
+
# Typecheck and lint all code
|
|
80
|
+
make check
|
|
81
|
+
# Format all code
|
|
82
|
+
make fmt
|
|
83
|
+
# Run all tests
|
|
84
|
+
make test
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Build a docker container
|
|
88
|
+
Note, you do not need Deno nor Go locally installed for this to work:
|
|
89
|
+
|
|
90
|
+
```shell
|
|
91
|
+
docker build -t silverbullet .
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
To run:
|
|
95
|
+
|
|
96
|
+
```shell
|
|
97
|
+
docker run -p 3000:3000 -v <PATH-TO-YOUR-SPACE>:/space silverbullet
|
|
98
|
+
```
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
base64Decode,
|
|
3
|
+
base64EncodedDataUrl,
|
|
4
|
+
} from "../../plug-api/lib/crypto.ts";
|
|
5
|
+
|
|
6
|
+
type DataUrl = string;
|
|
7
|
+
|
|
8
|
+
// Mapping from path -> `data:mimetype;base64,base64-encoded-data` strings
|
|
9
|
+
export type AssetJson = Record<string, { data: DataUrl; mtime: number }>;
|
|
10
|
+
|
|
11
|
+
export class AssetBundle {
|
|
12
|
+
readonly bundle: AssetJson;
|
|
13
|
+
|
|
14
|
+
constructor(bundle: AssetJson = {}) {
|
|
15
|
+
this.bundle = bundle;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
has(path: string): boolean {
|
|
19
|
+
return path in this.bundle;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
listFiles(): string[] {
|
|
23
|
+
return Object.keys(this.bundle);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
readFileSync(
|
|
27
|
+
path: string,
|
|
28
|
+
): Uint8Array {
|
|
29
|
+
const content = this.bundle[path];
|
|
30
|
+
if (!content) {
|
|
31
|
+
throw new Error(`No such file ${path}`);
|
|
32
|
+
}
|
|
33
|
+
const data = content.data.split(",", 2)[1];
|
|
34
|
+
return base64Decode(data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
readFileAsDataUrl(path: string): string {
|
|
38
|
+
const content = this.bundle[path];
|
|
39
|
+
if (!content) {
|
|
40
|
+
throw new Error(`No such file ${path}`);
|
|
41
|
+
}
|
|
42
|
+
return content.data;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
readTextFileSync(
|
|
46
|
+
path: string,
|
|
47
|
+
): string {
|
|
48
|
+
return new TextDecoder().decode(this.readFileSync(path));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getMimeType(
|
|
52
|
+
path: string,
|
|
53
|
+
): string {
|
|
54
|
+
const entry = this.bundle[path];
|
|
55
|
+
if (!entry) {
|
|
56
|
+
throw new Error(`No such file ${path}`);
|
|
57
|
+
}
|
|
58
|
+
return entry.data.split(";")[0].split(":")[1];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getMtime(path: string): number {
|
|
62
|
+
const entry = this.bundle[path];
|
|
63
|
+
if (!entry) {
|
|
64
|
+
throw new Error(`No such file ${path}`);
|
|
65
|
+
}
|
|
66
|
+
return entry.mtime;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
writeFileSync(
|
|
70
|
+
path: string,
|
|
71
|
+
mimeType: string,
|
|
72
|
+
data: Uint8Array,
|
|
73
|
+
mtime: number = Date.now(),
|
|
74
|
+
) {
|
|
75
|
+
// Replace \ with / for windows
|
|
76
|
+
path = path.replaceAll("\\", "/");
|
|
77
|
+
this.bundle[path] = {
|
|
78
|
+
data: base64EncodedDataUrl(mimeType, data),
|
|
79
|
+
mtime,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
writeTextFileSync(
|
|
84
|
+
path: string,
|
|
85
|
+
mimeType: string,
|
|
86
|
+
s: string,
|
|
87
|
+
mtime: number = Date.now(),
|
|
88
|
+
) {
|
|
89
|
+
this.writeFileSync(path, mimeType, new TextEncoder().encode(s), mtime);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
toJSON(): AssetJson {
|
|
93
|
+
return this.bundle;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type LuaCollectionQuery,
|
|
3
|
+
queryLua,
|
|
4
|
+
} from "../space_lua/query_collection.ts";
|
|
5
|
+
import { LuaEnv, LuaStackFrame } from "../space_lua/runtime.ts";
|
|
6
|
+
import type { KvPrimitives, KvQueryOptions } from "./kv_primitives.ts";
|
|
7
|
+
|
|
8
|
+
import type { KV, KvKey } from "../../plug-api/types/datastore.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* This is the data store class you'll actually want to use, wrapping the primitives
|
|
12
|
+
* in a more user-friendly way
|
|
13
|
+
*/
|
|
14
|
+
export class DataStore {
|
|
15
|
+
constructor(
|
|
16
|
+
readonly kv: KvPrimitives,
|
|
17
|
+
) {
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async get<T = any>(key: KvKey): Promise<T | null> {
|
|
21
|
+
return (await this.batchGet([key]))[0];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
batchGet<T = any>(keys: KvKey[]): Promise<(T | null)[]> {
|
|
25
|
+
if (keys.length === 0) {
|
|
26
|
+
return Promise.resolve([]);
|
|
27
|
+
}
|
|
28
|
+
return this.kv.batchGet(keys);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
set(key: KvKey, value: any): Promise<void> {
|
|
32
|
+
return this.batchSet([{ key, value }]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
batchSet<T = any>(entries: KV<T>[]): Promise<void> {
|
|
36
|
+
if (entries.length === 0) {
|
|
37
|
+
return Promise.resolve();
|
|
38
|
+
}
|
|
39
|
+
const allKeyStrings = new Set<string>();
|
|
40
|
+
const uniqueEntries: KV[] = [];
|
|
41
|
+
for (const { key, value } of entries) {
|
|
42
|
+
const keyString = JSON.stringify(key);
|
|
43
|
+
if (allKeyStrings.has(keyString)) {
|
|
44
|
+
console.warn(`Duplicate key ${keyString} in batchSet, skipping`);
|
|
45
|
+
} else {
|
|
46
|
+
allKeyStrings.add(keyString);
|
|
47
|
+
uniqueEntries.push({ key, value });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return this.kv.batchSet(uniqueEntries);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
delete(key: KvKey): Promise<void> {
|
|
54
|
+
return this.batchDelete([key]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
batchDelete(keys: KvKey[]): Promise<void> {
|
|
58
|
+
if (keys.length === 0) {
|
|
59
|
+
return Promise.resolve();
|
|
60
|
+
}
|
|
61
|
+
return this.kv.batchDelete(keys);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async batchDeletePrefix(prefix: KvKey): Promise<void> {
|
|
65
|
+
const keys: KvKey[] = [];
|
|
66
|
+
for await (const { key } of this.kv.query({ prefix })) {
|
|
67
|
+
keys.push(key);
|
|
68
|
+
}
|
|
69
|
+
return this.batchDelete(keys);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
query<T = any>(options: KvQueryOptions): AsyncIterableIterator<KV<T>> {
|
|
73
|
+
return this.kv.query<T>(options);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
luaQuery<T = any>(
|
|
77
|
+
prefix: KvKey,
|
|
78
|
+
query: LuaCollectionQuery,
|
|
79
|
+
env: LuaEnv = new LuaEnv(),
|
|
80
|
+
sf: LuaStackFrame = LuaStackFrame.lostFrame,
|
|
81
|
+
enricher?: (key: KvKey, item: any) => any,
|
|
82
|
+
): Promise<T[]> {
|
|
83
|
+
return queryLua(this.kv, prefix, query, env, sf, enricher);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { KV, KvKey } from "../../plug-api/types/datastore.ts";
|
|
2
|
+
|
|
3
|
+
export type KvQueryOptions = {
|
|
4
|
+
prefix?: KvKey;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export interface KvPrimitives {
|
|
8
|
+
batchGet(keys: KvKey[]): Promise<(any | undefined)[]>;
|
|
9
|
+
|
|
10
|
+
batchSet(entries: KV[]): Promise<void>;
|
|
11
|
+
|
|
12
|
+
batchDelete(keys: KvKey[]): Promise<void>;
|
|
13
|
+
|
|
14
|
+
query<T = any>(options: KvQueryOptions): AsyncIterableIterator<KV<T>>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A more efficient way to do counts
|
|
18
|
+
*/
|
|
19
|
+
countQuery(options: KvQueryOptions): Promise<number>;
|
|
20
|
+
|
|
21
|
+
// Completely clear all data from this datastore
|
|
22
|
+
clear(): Promise<void>;
|
|
23
|
+
|
|
24
|
+
close(): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const wikiLinkRegex =
|
|
2
|
+
/(?<leadingTrivia>!?\[\[)(?<stringRef>.*?)(?:\|(?<alias>.*?))?(?<trailingTrivia>\]\])/g;
|
|
3
|
+
export const mdLinkRegex = /!?\[(?<title>[^\]]*)\]\((?<url>.+)\)/g;
|
|
4
|
+
export const tagRegex =
|
|
5
|
+
/#(?:(?:\d*[^\d\s!@#$%^&*(),.?":{}|<>\\][^\s!@#$%^&*(),.?":{}|<>\\]*)|(?:<[^>\n]+>))/;
|
|
6
|
+
export const nakedUrlRegex =
|
|
7
|
+
/(^https?:\/\/([-a-zA-Z0-9@:%_\+~#=]|(?:[.](?!(\s|$)))){1,256})(([-a-zA-Z0-9(@:%_\+~#?&=\/]|(?:[.,:;)](?!(\s|$))))*)/;
|
|
8
|
+
export const frontmatterQuotesRegex = /["'].*["']/g;
|
|
9
|
+
export const frontmatterUrlRegex = /([a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s"']+)/g;
|
|
10
|
+
export const frontmatterWikiLinkRegex =
|
|
11
|
+
/(?<leadingTrivia>!?\[\[)(?<stringRef>.*?)(?:\|(?<alias>.*?))?(?<trailingTrivia>\]\])/g;
|
|
12
|
+
export const frontmatterMailtoRegex = /(mailto:[^@\s]+@[^@\s"']+)/ig;
|
|
13
|
+
export const pWikiLinkRegex = new RegExp("^" + wikiLinkRegex.source); // Modified regex used only in parser
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventEmitter implementation, similar to the one used in CodeMirror
|
|
3
|
+
*/
|
|
4
|
+
export abstract class EventEmitter<HandlerT> {
|
|
5
|
+
private handlers: Partial<HandlerT>[] = [];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Subscribe to events
|
|
9
|
+
* @param handlers
|
|
10
|
+
*/
|
|
11
|
+
on(handlers: Partial<HandlerT>) {
|
|
12
|
+
this.handlers.push(handlers);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Unsubscribe from events
|
|
17
|
+
* @param handlers
|
|
18
|
+
*/
|
|
19
|
+
off(handlers: Partial<HandlerT>) {
|
|
20
|
+
this.handlers = this.handlers.filter((h) => h !== handlers);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Broadcast an event to all subscribers
|
|
25
|
+
* @param eventName
|
|
26
|
+
* @param args
|
|
27
|
+
*/
|
|
28
|
+
async emit(eventName: keyof HandlerT, ...args: any[]): Promise<void> {
|
|
29
|
+
for (const handler of this.handlers) {
|
|
30
|
+
const fn: any = handler[eventName];
|
|
31
|
+
if (fn) {
|
|
32
|
+
await Promise.resolve(fn(...args));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EventHookT } from "@silverbulletmd/silverbullet/type/manifest";
|
|
2
|
+
import type { Hook } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export interface EventHookI extends Hook<EventHookT> {
|
|
5
|
+
dispatchEvent(eventName: string, ...args: unknown[]): Promise<unknown[]>;
|
|
6
|
+
|
|
7
|
+
listEvents(): string[];
|
|
8
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Hook, Manifest } from "../types.ts";
|
|
2
|
+
import type { System } from "../system.ts";
|
|
3
|
+
import type { CodeWidgetT } from "@silverbulletmd/silverbullet/type/manifest";
|
|
4
|
+
import type { CodeWidgetCallback } from "@silverbulletmd/silverbullet/type/client";
|
|
5
|
+
|
|
6
|
+
export class CodeWidgetHook implements Hook<CodeWidgetT> {
|
|
7
|
+
codeWidgetCallbacks = new Map<string, CodeWidgetCallback>();
|
|
8
|
+
codeWidgetModes = new Map<string, "markdown" | "iframe">();
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
collectAllCodeWidgets(system: System<CodeWidgetT>) {
|
|
14
|
+
this.codeWidgetCallbacks.clear();
|
|
15
|
+
for (const plug of system.loadedPlugs.values()) {
|
|
16
|
+
for (
|
|
17
|
+
const [name, functionDef] of Object.entries(
|
|
18
|
+
plug.manifest!.functions,
|
|
19
|
+
)
|
|
20
|
+
) {
|
|
21
|
+
if (!functionDef.codeWidget) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
this.codeWidgetModes.set(
|
|
25
|
+
functionDef.codeWidget,
|
|
26
|
+
functionDef.renderMode || "iframe",
|
|
27
|
+
);
|
|
28
|
+
this.codeWidgetCallbacks.set(
|
|
29
|
+
functionDef.codeWidget,
|
|
30
|
+
(bodyText, pageName) => {
|
|
31
|
+
return plug.invoke(name, [bodyText, pageName]);
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
apply(system: System<CodeWidgetT>): void {
|
|
39
|
+
this.collectAllCodeWidgets(system);
|
|
40
|
+
system.on({
|
|
41
|
+
plugLoaded: () => {
|
|
42
|
+
this.collectAllCodeWidgets(system);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
validateManifest(manifest: Manifest<CodeWidgetT>): string[] {
|
|
48
|
+
const errors = [];
|
|
49
|
+
for (const functionDef of Object.values(manifest.functions)) {
|
|
50
|
+
if (!functionDef.codeWidget) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (typeof functionDef.codeWidget !== "string") {
|
|
54
|
+
errors.push(`Codewidgets require a string name.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return errors;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Hook, Manifest } from "../types.ts";
|
|
2
|
+
import type { System } from "../system.ts";
|
|
3
|
+
import { EventEmitter } from "../event.ts";
|
|
4
|
+
import { throttle } from "@silverbulletmd/silverbullet/lib/async";
|
|
5
|
+
import type { Command, CommandHookEvents } from "../../types/command.ts";
|
|
6
|
+
import type { CommandHookT } from "@silverbulletmd/silverbullet/type/manifest";
|
|
7
|
+
|
|
8
|
+
export class CommandHook extends EventEmitter<CommandHookEvents>
|
|
9
|
+
implements Hook<CommandHookT> {
|
|
10
|
+
system?: System<CommandHookT>;
|
|
11
|
+
public throttledBuildAllCommandsAndEmit = throttle(() => {
|
|
12
|
+
this.buildAllCommandsAndEmit();
|
|
13
|
+
}, 200);
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
private readOnly: boolean,
|
|
17
|
+
private additionalCommands: Map<string, Command>,
|
|
18
|
+
) {
|
|
19
|
+
super();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the command map
|
|
24
|
+
*/
|
|
25
|
+
buildAllCommands(): Map<string, Command> {
|
|
26
|
+
const commands = new Map<string, Command>();
|
|
27
|
+
// Add commands from plugs
|
|
28
|
+
if (!this.system) {
|
|
29
|
+
// Not initialized yet
|
|
30
|
+
return commands;
|
|
31
|
+
}
|
|
32
|
+
for (const plug of this.system.loadedPlugs.values()) {
|
|
33
|
+
for (
|
|
34
|
+
const [name, functionDef] of Object.entries(
|
|
35
|
+
plug.manifest!.functions,
|
|
36
|
+
)
|
|
37
|
+
) {
|
|
38
|
+
if (!functionDef.command) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const cmd = functionDef.command;
|
|
42
|
+
if (cmd.requireMode === "rw" && this.readOnly) {
|
|
43
|
+
// Bit hacky, but don't expose commands that require write mode in read-only mode
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
commands.set(cmd.name, {
|
|
47
|
+
...cmd,
|
|
48
|
+
run: (args?: string[]) => {
|
|
49
|
+
return plug.invoke(name, [cmd, ...args ?? []]);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const [name, cmd] of this.additionalCommands) {
|
|
55
|
+
if (commands.has(name)) {
|
|
56
|
+
// Existing command, let's do some inline patching
|
|
57
|
+
const existingCommand = commands.get(name)!;
|
|
58
|
+
const command: Command = {
|
|
59
|
+
...existingCommand,
|
|
60
|
+
...cmd,
|
|
61
|
+
};
|
|
62
|
+
if (cmd.run) {
|
|
63
|
+
command.run = cmd.run;
|
|
64
|
+
}
|
|
65
|
+
commands.set(name, command);
|
|
66
|
+
} else {
|
|
67
|
+
// New command, let's just set
|
|
68
|
+
commands.set(name, cmd);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return commands;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public buildAllCommandsAndEmit() {
|
|
75
|
+
this.emit("commandsUpdated", this.buildAllCommands());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
apply(system: System<CommandHookT>): void {
|
|
79
|
+
this.system = system;
|
|
80
|
+
system.on({
|
|
81
|
+
plugLoaded: () => {
|
|
82
|
+
this.throttledBuildAllCommandsAndEmit();
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
// On next tick
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
this.throttledBuildAllCommandsAndEmit();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
validateManifest(manifest: Manifest<CommandHookT>): string[] {
|
|
92
|
+
const errors = [];
|
|
93
|
+
for (const [name, functionDef] of Object.entries(manifest.functions)) {
|
|
94
|
+
if (!functionDef.command) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const cmd = functionDef.command;
|
|
98
|
+
if (!cmd.name) {
|
|
99
|
+
errors.push(`Function ${name} has a command but no name`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Hook, Manifest } from "../types.ts";
|
|
2
|
+
import type { DocumentEditorT } from "@silverbulletmd/silverbullet/type/manifest";
|
|
3
|
+
import type { System } from "../system.ts";
|
|
4
|
+
import type { DocumentEditorCallback } from "@silverbulletmd/silverbullet/type/client";
|
|
5
|
+
|
|
6
|
+
export class DocumentEditorHook implements Hook<DocumentEditorT> {
|
|
7
|
+
documentEditors = new Map<
|
|
8
|
+
string,
|
|
9
|
+
{ extensions: string[]; callback: DocumentEditorCallback }
|
|
10
|
+
>();
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
collectAllDocumentEditors(system: System<DocumentEditorT>) {
|
|
16
|
+
this.documentEditors.clear();
|
|
17
|
+
for (const plug of system.loadedPlugs.values()) {
|
|
18
|
+
for (
|
|
19
|
+
const [name, functionDef] of Object.entries(
|
|
20
|
+
plug.manifest!.functions,
|
|
21
|
+
)
|
|
22
|
+
) {
|
|
23
|
+
if (!functionDef.editor) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const keys = Array.isArray(functionDef.editor)
|
|
28
|
+
? functionDef.editor
|
|
29
|
+
: [functionDef.editor];
|
|
30
|
+
|
|
31
|
+
const conflict = Array.from(this.documentEditors.entries()).find((
|
|
32
|
+
[_, { extensions }],
|
|
33
|
+
) => keys.some((key) => extensions.includes(key)));
|
|
34
|
+
|
|
35
|
+
if (conflict) {
|
|
36
|
+
console.log(
|
|
37
|
+
`Extension definition of document editor ${name}: [${keys}] conflicts with the one from ${
|
|
38
|
+
conflict[0]
|
|
39
|
+
}: [${conflict[1].extensions}]! Using the latter.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.documentEditors.set(
|
|
44
|
+
name,
|
|
45
|
+
{ extensions: keys, callback: () => plug.invoke(name, []) },
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
apply(system: System<DocumentEditorT>): void {
|
|
52
|
+
this.collectAllDocumentEditors(system);
|
|
53
|
+
system.on({
|
|
54
|
+
plugLoaded: () => {
|
|
55
|
+
this.collectAllDocumentEditors(system);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
validateManifest(manifest: Manifest<DocumentEditorT>): string[] {
|
|
61
|
+
const errors = [];
|
|
62
|
+
for (const functionDef of Object.values(manifest.functions)) {
|
|
63
|
+
if (!functionDef.editor) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
typeof functionDef.editor !== "string" &&
|
|
68
|
+
!Array.isArray(functionDef.editor)
|
|
69
|
+
) {
|
|
70
|
+
errors.push(
|
|
71
|
+
`Document editors require a string name or an array of string names.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return errors;
|
|
76
|
+
}
|
|
77
|
+
}
|