@silverbulletmd/silverbullet 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +18 -0
- package/README.md +98 -0
- package/client/asset_bundle/bundle.ts +95 -0
- package/client/data/datastore.ts +85 -0
- package/client/data/kv_primitives.ts +25 -0
- package/client/markdown_parser/constants.ts +13 -0
- package/client/plugos/event.ts +36 -0
- package/client/plugos/eventhook.ts +8 -0
- package/client/plugos/hooks/code_widget.ts +59 -0
- package/client/plugos/hooks/command.ts +104 -0
- package/client/plugos/hooks/document_editor.ts +77 -0
- package/client/plugos/hooks/event.ts +187 -0
- package/client/plugos/hooks/mq.ts +154 -0
- package/client/plugos/hooks/plug_namespace.ts +85 -0
- package/client/plugos/hooks/slash_command.ts +192 -0
- package/client/plugos/hooks/syscall.ts +66 -0
- package/client/plugos/manifest_cache.ts +67 -0
- package/client/plugos/plug.ts +99 -0
- package/client/plugos/plug_compile.ts +202 -0
- package/client/plugos/protocol.ts +40 -0
- package/client/plugos/proxy_fetch.ts +53 -0
- package/client/plugos/sandboxes/deno_worker_sandbox.ts +6 -0
- package/client/plugos/sandboxes/sandbox.ts +14 -0
- package/client/plugos/sandboxes/web_worker_sandbox.ts +17 -0
- package/client/plugos/sandboxes/worker_sandbox.ts +132 -0
- package/client/plugos/syscalls/asset.ts +35 -0
- package/client/plugos/syscalls/clientStore.ts +21 -0
- package/client/plugos/syscalls/client_code_widget.ts +12 -0
- package/client/plugos/syscalls/code_widget.ts +24 -0
- package/client/plugos/syscalls/config.ts +46 -0
- package/client/plugos/syscalls/datastore.ts +89 -0
- package/client/plugos/syscalls/editor.ts +673 -0
- package/client/plugos/syscalls/event.ts +36 -0
- package/client/plugos/syscalls/fetch.ts +128 -0
- package/client/plugos/syscalls/index.ts +102 -0
- package/client/plugos/syscalls/jsonschema.ts +69 -0
- package/client/plugos/syscalls/language.ts +23 -0
- package/client/plugos/syscalls/lua.ts +58 -0
- package/client/plugos/syscalls/markdown.ts +84 -0
- package/client/plugos/syscalls/mq.ts +52 -0
- package/client/plugos/syscalls/service_registry.ts +43 -0
- package/client/plugos/syscalls/shell.ts +39 -0
- package/client/plugos/syscalls/space.ts +139 -0
- package/client/plugos/syscalls/sync.ts +77 -0
- package/client/plugos/syscalls/system.ts +150 -0
- package/client/plugos/system.ts +201 -0
- package/client/plugos/types.ts +60 -0
- package/client/plugos/util.ts +14 -0
- package/client/plugos/worker_runtime.ts +195 -0
- package/client/space_lua/ast.ts +328 -0
- package/client/space_lua/ast_narrow.ts +81 -0
- package/client/space_lua/eval.ts +2478 -0
- package/client/space_lua/labels.ts +416 -0
- package/client/space_lua/numeric.ts +240 -0
- package/client/space_lua/parse.ts +1522 -0
- package/client/space_lua/query_collection.ts +232 -0
- package/client/space_lua/rp.ts +27 -0
- package/client/space_lua/runtime.ts +1702 -0
- package/client/space_lua/stdlib/crypto.ts +10 -0
- package/client/space_lua/stdlib/encoding.ts +19 -0
- package/client/space_lua/stdlib/format.ts +770 -0
- package/client/space_lua/stdlib/js.ts +73 -0
- package/client/space_lua/stdlib/load.ts +52 -0
- package/client/space_lua/stdlib/math.ts +193 -0
- package/client/space_lua/stdlib/net.ts +113 -0
- package/client/space_lua/stdlib/os.ts +368 -0
- package/client/space_lua/stdlib/space_lua.ts +153 -0
- package/client/space_lua/stdlib/string.ts +286 -0
- package/client/space_lua/stdlib/table.ts +401 -0
- package/client/space_lua/stdlib.ts +489 -0
- package/client/space_lua/tonumber.ts +501 -0
- package/client/space_lua/util.ts +96 -0
- package/dist/plug-compile.js +1513 -0
- package/package.json +120 -0
- package/plug-api/constants.ts +42 -0
- package/plug-api/lib/async.ts +162 -0
- package/plug-api/lib/crypto.ts +202 -0
- package/plug-api/lib/dates.ts +13 -0
- package/plug-api/lib/json.ts +136 -0
- package/plug-api/lib/limited_map.ts +72 -0
- package/plug-api/lib/memory_cache.ts +21 -0
- package/plug-api/lib/native_fetch.ts +6 -0
- package/plug-api/lib/ref.ts +275 -0
- package/plug-api/lib/resolve.ts +90 -0
- package/plug-api/lib/tags.ts +15 -0
- package/plug-api/lib/transclusion.ts +122 -0
- package/plug-api/lib/tree.ts +232 -0
- package/plug-api/lib/yaml.ts +284 -0
- package/plug-api/syscall.ts +15 -0
- package/plug-api/syscalls/asset.ts +36 -0
- package/plug-api/syscalls/client_store.ts +33 -0
- package/plug-api/syscalls/code_widget.ts +8 -0
- package/plug-api/syscalls/config.ts +58 -0
- package/plug-api/syscalls/datastore.ts +96 -0
- package/plug-api/syscalls/editor.ts +517 -0
- package/plug-api/syscalls/event.ts +47 -0
- package/plug-api/syscalls/index.ts +77 -0
- package/plug-api/syscalls/jsonschema.ts +25 -0
- package/plug-api/syscalls/language.ts +23 -0
- package/plug-api/syscalls/lua.ts +20 -0
- package/plug-api/syscalls/markdown.ts +38 -0
- package/plug-api/syscalls/mq.ts +79 -0
- package/plug-api/syscalls/shell.ts +14 -0
- package/plug-api/syscalls/space.ts +212 -0
- package/plug-api/syscalls/sync.ts +28 -0
- package/plug-api/syscalls/system.ts +102 -0
- package/plug-api/syscalls/yaml.ts +28 -0
- package/plug-api/syscalls.ts +21 -0
- package/plug-api/system_mock.ts +89 -0
- package/plug-api/types/client.ts +116 -0
- package/plug-api/types/config.ts +22 -0
- package/plug-api/types/datastore.ts +28 -0
- package/plug-api/types/event.ts +27 -0
- package/plug-api/types/index.ts +56 -0
- package/plug-api/types/manifest.ts +98 -0
- package/plug-api/types/namespace.ts +6 -0
- package/plugs/builtin_plugs.ts +14 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
type LimitedMapRecord<V> = {
|
|
2
|
+
value: V;
|
|
3
|
+
la: number;
|
|
4
|
+
expTimer?: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export class LimitedMap<V> {
|
|
8
|
+
private map: Map<string, LimitedMapRecord<V>>;
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
private maxSize: number,
|
|
12
|
+
initialJson: Record<string, LimitedMapRecord<V>> = {},
|
|
13
|
+
) {
|
|
14
|
+
this.map = new Map(Object.entries(initialJson));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param key
|
|
19
|
+
* @param value
|
|
20
|
+
* @param ttl time to live (in ms)
|
|
21
|
+
*/
|
|
22
|
+
set(key: string, value: V, ttl?: number) {
|
|
23
|
+
const entry: LimitedMapRecord<V> = { value, la: Date.now() };
|
|
24
|
+
if (ttl) {
|
|
25
|
+
const existingEntry = this.map.get(key);
|
|
26
|
+
if (existingEntry?.expTimer) {
|
|
27
|
+
clearTimeout(existingEntry.expTimer);
|
|
28
|
+
}
|
|
29
|
+
entry.expTimer = setTimeout(() => {
|
|
30
|
+
this.map.delete(key);
|
|
31
|
+
}, ttl);
|
|
32
|
+
}
|
|
33
|
+
if (this.map.size >= this.maxSize) {
|
|
34
|
+
// Remove the oldest key before adding a new one
|
|
35
|
+
const oldestKey = this.getOldestKey();
|
|
36
|
+
this.map.delete(oldestKey!);
|
|
37
|
+
}
|
|
38
|
+
this.map.set(key, entry);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get(key: string): V | undefined {
|
|
42
|
+
const entry = this.map.get(key);
|
|
43
|
+
if (entry) {
|
|
44
|
+
// Update the last accessed timestamp
|
|
45
|
+
entry.la = Date.now();
|
|
46
|
+
return entry.value;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
remove(key: string) {
|
|
52
|
+
this.map.delete(key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
toJSON(): Record<string, any> {
|
|
56
|
+
return Object.fromEntries(this.map.entries());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private getOldestKey(): string | undefined {
|
|
60
|
+
let oldestKey: string | undefined;
|
|
61
|
+
let oldestTimestamp: number | undefined;
|
|
62
|
+
|
|
63
|
+
for (const [key, entry] of this.map.entries()) {
|
|
64
|
+
if (!oldestTimestamp || entry.la < oldestTimestamp) {
|
|
65
|
+
oldestKey = key;
|
|
66
|
+
oldestTimestamp = entry.la;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return oldestKey;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LimitedMap } from "./limited_map.ts";
|
|
2
|
+
|
|
3
|
+
const cache = new LimitedMap<any>(50);
|
|
4
|
+
|
|
5
|
+
export async function ttlCache<K, V>(
|
|
6
|
+
key: K,
|
|
7
|
+
fn: (key: K) => Promise<V>,
|
|
8
|
+
ttlSecs?: number,
|
|
9
|
+
): Promise<V> {
|
|
10
|
+
if (!ttlSecs) {
|
|
11
|
+
return fn(key);
|
|
12
|
+
}
|
|
13
|
+
const serializedKey = JSON.stringify(key);
|
|
14
|
+
const cached = cache.get(serializedKey);
|
|
15
|
+
if (cached) {
|
|
16
|
+
return cached;
|
|
17
|
+
}
|
|
18
|
+
const result = await fn(key);
|
|
19
|
+
cache.set(serializedKey, result, ttlSecs * 1000);
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findNodeMatching,
|
|
3
|
+
findNodeOfType,
|
|
4
|
+
type ParseTree,
|
|
5
|
+
renderToText,
|
|
6
|
+
} from "@silverbulletmd/silverbullet/lib/tree";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents a path with an extension. This is a little cursed, but enforces
|
|
10
|
+
* that people check the path before setting it. For navigation logic the empty
|
|
11
|
+
* path will point to the index page. This could differ for e.g. for the
|
|
12
|
+
* wikilink logic where it points to the currentPage
|
|
13
|
+
*/
|
|
14
|
+
export type Path = `${string}.${string}` | "";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Represents a reference to a page or document, with optional position, anchor and header
|
|
18
|
+
*/
|
|
19
|
+
export type Ref = {
|
|
20
|
+
path: Path;
|
|
21
|
+
meta?: boolean;
|
|
22
|
+
|
|
23
|
+
details?:
|
|
24
|
+
| { type: "position"; pos: number }
|
|
25
|
+
| { type: "linecolumn"; line: number; column: number }
|
|
26
|
+
| { type: "header"; header: string };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Determines the file extension of a ref. It will only return the last
|
|
31
|
+
* extension, so `foo.tar.gz` resolves to `gz`
|
|
32
|
+
* @returns The file extension WITHOUT the dot
|
|
33
|
+
*/
|
|
34
|
+
export function getPathExtension(path: Path): string {
|
|
35
|
+
// If the ref links to the the file it's on (i.e. path === ""), it's safe to assume it's a link to a "md" page
|
|
36
|
+
return path !== "" ? path.split(".").pop()!.toLowerCase() : "md";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Renders a path into a "name". This means it removes the extension for `.md` path
|
|
41
|
+
*/
|
|
42
|
+
export function getNameFromPath(path: Path): string {
|
|
43
|
+
return encodeRef({ path });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Determines there a ref points to a markdown file
|
|
48
|
+
*/
|
|
49
|
+
export function isMarkdownPath(path: Path): boolean {
|
|
50
|
+
return getPathExtension(path) === "md";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Adds an `md` extension to any path without an extension or a path ending in
|
|
55
|
+
* `.conflicted`, except to the empty path
|
|
56
|
+
* @param path The path to normalize. Cannot contain any position or header
|
|
57
|
+
* addons
|
|
58
|
+
*/
|
|
59
|
+
function normalizePath(path: string): Path {
|
|
60
|
+
if (path.startsWith("/")) {
|
|
61
|
+
path = path.slice(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (/.+\.[a-zA-Z0-9]+$/.test(path) || path === "") {
|
|
65
|
+
return path as Path;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return `${path}.md`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Determines wether a name conforms to all the requirments.
|
|
73
|
+
*/
|
|
74
|
+
export function isValidName(name: string): boolean {
|
|
75
|
+
const ref = parseToRef(name);
|
|
76
|
+
|
|
77
|
+
// If the name, parses as a link and doesn't provide any other info we can be
|
|
78
|
+
// sure it was only parsed as a path and that the path then conforms to all
|
|
79
|
+
// the requirements
|
|
80
|
+
return !!ref && !ref.details && !ref.meta && name !== "" &&
|
|
81
|
+
encodeRef(ref) === name;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Determines wether a path conforms to all the requirments.
|
|
86
|
+
*/
|
|
87
|
+
export function isValidPath(path: string): path is Path {
|
|
88
|
+
const ref = parseToRef(path);
|
|
89
|
+
|
|
90
|
+
return !!ref && ref.path === path;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* ONLY TOUCH THIS IF YOU REALLY KNOW WHAT YOU ARE DOING. THIS REGEX IS INTEGRAL
|
|
95
|
+
* TO THE INNER WORKINGS OF SILVERBULLET AND CHANGES COULD INTRODUCE MAJOR BUGS
|
|
96
|
+
*/
|
|
97
|
+
const refRegex =
|
|
98
|
+
/^(?<meta>\^)?(?<path>(?!.*\.[a-zA-Z0-9]+\.md$)(?!\/?(\.|\^))(?!.*(?:\/|^)\.{1,2}(?:\/|$)|.*\/{2})(?!.*(?:\]\]|\[\[))[^@#\|<>]*)(@(?<pos>\d+)|@[Ll](?<line>\d+)(?:[Cc](?<col>\d+))?|#\s*(?<header>.*))?$/;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parses a reference string into a ref object.
|
|
102
|
+
* @returns A ref or if the parsing fails null
|
|
103
|
+
*/
|
|
104
|
+
export function parseToRef(stringRef: string): Ref | null {
|
|
105
|
+
const match = stringRef.match(refRegex);
|
|
106
|
+
if (!match || !match.groups) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const groups = match.groups;
|
|
111
|
+
|
|
112
|
+
const ref: Ref = { path: normalizePath(groups.path) };
|
|
113
|
+
|
|
114
|
+
if (groups.meta) {
|
|
115
|
+
ref.meta = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (groups.pos !== undefined) {
|
|
119
|
+
ref.details = {
|
|
120
|
+
type: "position",
|
|
121
|
+
pos: parseInt(groups.pos),
|
|
122
|
+
};
|
|
123
|
+
} else if (groups.line !== undefined) {
|
|
124
|
+
ref.details = {
|
|
125
|
+
type: "linecolumn",
|
|
126
|
+
line: parseInt(groups.line),
|
|
127
|
+
column: groups.col !== undefined ? parseInt(groups.col) : 1,
|
|
128
|
+
};
|
|
129
|
+
} else if (groups.header !== undefined) {
|
|
130
|
+
ref.details = {
|
|
131
|
+
type: "header",
|
|
132
|
+
header: groups.header,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return ref;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The inverse of {@link parseToRef}, encodes a ref object into a reference string.
|
|
141
|
+
* It tries to produce the shortest valid representation
|
|
142
|
+
*/
|
|
143
|
+
export function encodeRef(ref: Ref): string {
|
|
144
|
+
let stringRef: string = ref.path;
|
|
145
|
+
|
|
146
|
+
if (isMarkdownPath(ref.path)) {
|
|
147
|
+
stringRef = stringRef.slice(0, -3);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (ref.details?.type === "linecolumn") {
|
|
151
|
+
stringRef += `@L${ref.details.line}`;
|
|
152
|
+
|
|
153
|
+
if (ref.details.column !== 1) {
|
|
154
|
+
stringRef += `C${ref.details.column}`;
|
|
155
|
+
}
|
|
156
|
+
} else if (ref.details?.type === "position") {
|
|
157
|
+
stringRef += `@${ref.details.pos}`;
|
|
158
|
+
} else if (ref.details?.type === "header") {
|
|
159
|
+
stringRef += `#${ref.details.header}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return stringRef;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Uses a parseTree and a ref pointing to a position inside it to determine the
|
|
167
|
+
* offset from the start inside it, using {@link getOffsetFromHeader} and
|
|
168
|
+
* {@link getOffsetFromLineColumn}
|
|
169
|
+
* @param text If provided the parseTree won't be rendered back to text
|
|
170
|
+
* @returns The offset in the file if it's able to determine it, otherwise -1
|
|
171
|
+
*/
|
|
172
|
+
export function getOffsetFromRef(
|
|
173
|
+
parseTree: ParseTree,
|
|
174
|
+
ref: Ref,
|
|
175
|
+
text?: string,
|
|
176
|
+
): number {
|
|
177
|
+
if (!ref.details) {
|
|
178
|
+
return -1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
switch (ref.details.type) {
|
|
182
|
+
case "position":
|
|
183
|
+
return ref.details.pos;
|
|
184
|
+
case "linecolumn":
|
|
185
|
+
return getOffsetFromLineColumn(
|
|
186
|
+
text ?? renderToText(parseTree),
|
|
187
|
+
ref.details.line,
|
|
188
|
+
ref.details.column,
|
|
189
|
+
);
|
|
190
|
+
case "header": {
|
|
191
|
+
return getOffsetFromHeader(parseTree, ref.details.header);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Find the header inside a {@link ParseTree} and returns the position the end
|
|
198
|
+
* of the header
|
|
199
|
+
* @param parseTree The parse tree. Can e.g. be generate using
|
|
200
|
+
* `markdown.parseMarkdown`
|
|
201
|
+
* @param header The header, spaces at the start or end are ignored
|
|
202
|
+
* @returns The position of the header inside the document, if it can't be found
|
|
203
|
+
* -1
|
|
204
|
+
*/
|
|
205
|
+
export function getOffsetFromHeader(
|
|
206
|
+
parseTree: ParseTree,
|
|
207
|
+
header: string,
|
|
208
|
+
): number {
|
|
209
|
+
const node = findNodeMatching(
|
|
210
|
+
parseTree,
|
|
211
|
+
(subTree) => {
|
|
212
|
+
if (!subTree.type || !subTree.type.startsWith("ATXHeading")) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const mark = findNodeOfType(subTree, "HeaderMark");
|
|
217
|
+
if (!mark || mark.from === undefined || mark.to === undefined) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return renderToText(subTree)
|
|
222
|
+
.slice(mark.to - mark.from)
|
|
223
|
+
.trimStart() === header.trim();
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (!node) {
|
|
228
|
+
return -1;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return node.to ?? -1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Calculates the character offset from a line and column position. If the
|
|
236
|
+
* position is out of bounds, it does a best-effort job returning a position.
|
|
237
|
+
* @param text The text which is used to determine the offset. Only `\n` are
|
|
238
|
+
* considered line breaks.
|
|
239
|
+
* @param line The line number of the described position. Starts at 1
|
|
240
|
+
* @param column The column number of the described position. Starts at 0
|
|
241
|
+
*/
|
|
242
|
+
export function getOffsetFromLineColumn(
|
|
243
|
+
text: string,
|
|
244
|
+
line: number,
|
|
245
|
+
column: number,
|
|
246
|
+
): number {
|
|
247
|
+
const lines = text.split("\n");
|
|
248
|
+
|
|
249
|
+
const linePos = lines
|
|
250
|
+
.slice(0, Math.max(line - 1, 0))
|
|
251
|
+
.map((l) => l.length)
|
|
252
|
+
.reduce((totalLen, len) => totalLen + len, 0);
|
|
253
|
+
|
|
254
|
+
const columnPos = Math.max(
|
|
255
|
+
0,
|
|
256
|
+
Math.min(lines[line - 1]?.length ?? 0, column - 1),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
return linePos + columnPos;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Encodes a page name for use in a URI. Basically does
|
|
264
|
+
* {@link encodeURIComponent}, but puts slashes back in place.
|
|
265
|
+
*/
|
|
266
|
+
export function encodePageURI(page: string): string {
|
|
267
|
+
return encodeURIComponent(page).replace(/%2F/g, "/");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Decodes a page name from a URI.
|
|
272
|
+
*/
|
|
273
|
+
export function decodePageURI(page: string): string {
|
|
274
|
+
return decodeURIComponent(page);
|
|
275
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Path } from "@silverbulletmd/silverbullet/lib/ref";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Determines wether a url points into the world wide web or to the local SB instance
|
|
5
|
+
*/
|
|
6
|
+
export function isLocalURL(url: string): boolean {
|
|
7
|
+
return !url.includes("://") &&
|
|
8
|
+
!url.startsWith("mailto:") &&
|
|
9
|
+
!url.startsWith("tel:");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extracts the folder name from a page or document name or a path
|
|
14
|
+
*/
|
|
15
|
+
export function folderName(name: string | Path): string {
|
|
16
|
+
return name.split("/").slice(0, -1).join("/");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function fileName(path: Path): Path;
|
|
20
|
+
export function fileName(name: string): string;
|
|
21
|
+
export function fileName(name: string | Path): string | Path {
|
|
22
|
+
return name.split("/").pop()!;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const builtinPrefixes = [
|
|
26
|
+
"tag:",
|
|
27
|
+
"search:",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Builtin pages are pages which SB should automatically consider as existing
|
|
32
|
+
*/
|
|
33
|
+
export function isBuiltinPath(path: Path): boolean {
|
|
34
|
+
return builtinPrefixes.some((prefix) => path.startsWith(prefix));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolves a markdown link url relative to an absolute url. It won't resolve
|
|
39
|
+
* above the base of the absolute path in the file tree. This means excess `..`
|
|
40
|
+
* will be dropped. It will also only resolve leading `..`
|
|
41
|
+
*/
|
|
42
|
+
export function resolveMarkdownLink(
|
|
43
|
+
absolute: string,
|
|
44
|
+
relative: string,
|
|
45
|
+
): string {
|
|
46
|
+
// These are part of the commonmark spec for urls with spaces inbetween.
|
|
47
|
+
if (relative.startsWith("<") && relative.endsWith(">")) {
|
|
48
|
+
relative = relative.slice(1, -1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (relative.startsWith("/")) {
|
|
52
|
+
return relative.slice(1);
|
|
53
|
+
} else {
|
|
54
|
+
const splitAbsolute = absolute
|
|
55
|
+
.split("/")
|
|
56
|
+
.slice(0, -1);
|
|
57
|
+
const splitRelative = relative
|
|
58
|
+
.split("/");
|
|
59
|
+
|
|
60
|
+
while (splitRelative && splitRelative[0] === "..") {
|
|
61
|
+
splitAbsolute.pop();
|
|
62
|
+
splitRelative.shift();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return [...splitAbsolute, ...splitRelative].join("/") as Path;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Turns an absolute path into a relative path, relative to some base directory. USE WITH CAUTION, definitely buggy
|
|
71
|
+
*/
|
|
72
|
+
export function absoluteToRelativePath(base: string, absolute: string): string {
|
|
73
|
+
// Remove leading /
|
|
74
|
+
base = base.startsWith("/") ? base.slice(1) : base;
|
|
75
|
+
absolute = absolute.startsWith("/") ? absolute.slice(1) : absolute;
|
|
76
|
+
|
|
77
|
+
const splitAbsolute = absolute.split("/");
|
|
78
|
+
const splitBase = base.split("/");
|
|
79
|
+
splitBase.pop();
|
|
80
|
+
|
|
81
|
+
// TODO: This is definitely not robust
|
|
82
|
+
while (splitBase && splitBase[0] === splitAbsolute[0]) {
|
|
83
|
+
splitBase.shift();
|
|
84
|
+
splitAbsolute.shift();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
splitBase.fill("..");
|
|
88
|
+
|
|
89
|
+
return [...splitBase, ...splitAbsolute].join("/");
|
|
90
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Extract the name from hashtag text, removing # prefix and <angle brackets> if necessary */
|
|
2
|
+
export function extractHashtag(text: string): string {
|
|
3
|
+
if (text[0] !== "#") { // you shouldn't call this function at all
|
|
4
|
+
console.error("extractHashtag called on already clean string", text);
|
|
5
|
+
return text;
|
|
6
|
+
} else if (text[1] === "<") {
|
|
7
|
+
if (text.slice(-1) !== ">") { // this is malformed: #<name but maybe we're trying to autocomplete
|
|
8
|
+
return text.slice(2);
|
|
9
|
+
} else { // this is correct #<name>
|
|
10
|
+
return text.slice(2, -1);
|
|
11
|
+
}
|
|
12
|
+
} else { // this is just #name
|
|
13
|
+
return text.slice(1);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isLocalURL,
|
|
3
|
+
resolveMarkdownLink,
|
|
4
|
+
} from "@silverbulletmd/silverbullet/lib/resolve";
|
|
5
|
+
import {
|
|
6
|
+
mdLinkRegex,
|
|
7
|
+
wikiLinkRegex,
|
|
8
|
+
} from "../../client/markdown_parser/constants.ts";
|
|
9
|
+
import {
|
|
10
|
+
getNameFromPath,
|
|
11
|
+
parseToRef,
|
|
12
|
+
} from "@silverbulletmd/silverbullet/lib/ref";
|
|
13
|
+
|
|
14
|
+
export type LinkType = "wikilink" | "markdownlink";
|
|
15
|
+
/**
|
|
16
|
+
* Represents a transclusion
|
|
17
|
+
*/
|
|
18
|
+
export type Transclusion = {
|
|
19
|
+
url: string;
|
|
20
|
+
alias: string;
|
|
21
|
+
dimension?: ContentDimensions;
|
|
22
|
+
linktype: LinkType;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Describes the dimensions of a transclusion, if provided through the alias.
|
|
26
|
+
* Can be parsed from the alias using {@link parseDimensionFromAlias}
|
|
27
|
+
*/
|
|
28
|
+
export type ContentDimensions = {
|
|
29
|
+
width?: number;
|
|
30
|
+
height?: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse an alias, possibly containing dimensions into an object
|
|
35
|
+
* @example "alias", "alias|100", "alias|100x200", "100", "100x200"
|
|
36
|
+
*/
|
|
37
|
+
export function parseDimensionFromAlias(
|
|
38
|
+
text: string,
|
|
39
|
+
): { alias: string; dimension?: ContentDimensions } {
|
|
40
|
+
let alias: string;
|
|
41
|
+
let dim: ContentDimensions | undefined;
|
|
42
|
+
if (text.includes("|")) {
|
|
43
|
+
const [aliasPart, dimPart] = text.split("|");
|
|
44
|
+
alias = aliasPart;
|
|
45
|
+
const [width, height] = dimPart.split("x");
|
|
46
|
+
dim = {};
|
|
47
|
+
if (width) {
|
|
48
|
+
dim.width = parseInt(width);
|
|
49
|
+
}
|
|
50
|
+
if (height) {
|
|
51
|
+
dim.height = parseInt(height);
|
|
52
|
+
}
|
|
53
|
+
} else if (/^[x\d]/.test(text)) {
|
|
54
|
+
const [width, height] = text.split("x");
|
|
55
|
+
dim = {};
|
|
56
|
+
if (width) {
|
|
57
|
+
dim.width = parseInt(width);
|
|
58
|
+
}
|
|
59
|
+
if (height) {
|
|
60
|
+
dim.height = parseInt(height);
|
|
61
|
+
}
|
|
62
|
+
alias = "";
|
|
63
|
+
} else {
|
|
64
|
+
alias = text;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { alias, dimension: dim };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parses a transclusion of the type `![[]]` or `![]()`
|
|
72
|
+
* @param text
|
|
73
|
+
*/
|
|
74
|
+
export function parseTransclusion(
|
|
75
|
+
text: string,
|
|
76
|
+
): Transclusion | null {
|
|
77
|
+
let url, alias = undefined;
|
|
78
|
+
let linktype: LinkType = "markdownlink";
|
|
79
|
+
// TODO: Take in the tree and use tree nodes to get url and alias (Applies to all regex uses)
|
|
80
|
+
mdLinkRegex.lastIndex = 0;
|
|
81
|
+
wikiLinkRegex.lastIndex = 0;
|
|
82
|
+
let match: RegExpMatchArray | null = null;
|
|
83
|
+
if ((match = mdLinkRegex.exec(text)) && match.groups) {
|
|
84
|
+
({ url, title: alias } = match.groups);
|
|
85
|
+
|
|
86
|
+
if (isLocalURL(url)) {
|
|
87
|
+
url = resolveMarkdownLink(
|
|
88
|
+
client.currentName(),
|
|
89
|
+
decodeURI(url),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
linktype = "markdownlink";
|
|
93
|
+
} else if ((match = wikiLinkRegex.exec(text)) && match.groups) {
|
|
94
|
+
({ stringRef: url, alias } = match.groups);
|
|
95
|
+
linktype = "wikilink";
|
|
96
|
+
} else {
|
|
97
|
+
// We found no match
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let dimension: ContentDimensions | undefined;
|
|
102
|
+
if (alias) {
|
|
103
|
+
({ alias, dimension: dimension } = parseDimensionFromAlias(alias));
|
|
104
|
+
} else {
|
|
105
|
+
alias = "";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
url,
|
|
110
|
+
alias,
|
|
111
|
+
dimension,
|
|
112
|
+
linktype,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function nameFromTransclusion(t: Transclusion): string {
|
|
117
|
+
const ref = parseToRef(t.url);
|
|
118
|
+
if (!ref) {
|
|
119
|
+
throw new Error(`Cannot extract name from transclusion: ${t.url}`);
|
|
120
|
+
}
|
|
121
|
+
return getNameFromPath(ref.path);
|
|
122
|
+
}
|