@silverbulletmd/silverbullet 2.4.1

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