@silverbulletmd/silverbullet 2.4.2 → 2.6.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/README.md +19 -4
- package/client/asset_bundle/bundle.ts +3 -9
- package/client/data/datastore.ts +4 -5
- package/client/markdown_parser/constants.ts +5 -4
- package/client/plugos/hooks/code_widget.ts +3 -8
- package/client/plugos/hooks/command.ts +8 -8
- package/client/plugos/hooks/document_editor.ts +10 -15
- package/client/plugos/hooks/event.ts +33 -36
- package/client/plugos/hooks/mq.ts +17 -17
- package/client/plugos/hooks/plug_namespace.ts +3 -8
- package/client/plugos/hooks/slash_command.ts +13 -28
- package/client/plugos/hooks/syscall.ts +3 -3
- package/client/plugos/manifest_cache.ts +22 -15
- package/client/plugos/plug.ts +2 -6
- package/client/plugos/plug_compile.ts +79 -78
- package/client/plugos/protocol.ts +28 -28
- package/client/plugos/proxy_fetch.ts +7 -6
- package/client/plugos/sandboxes/web_worker_sandbox.ts +1 -1
- package/client/plugos/sandboxes/worker_sandbox.ts +18 -18
- package/client/plugos/syscalls/asset.ts +1 -3
- package/client/plugos/syscalls/code_widget.ts +1 -3
- package/client/plugos/syscalls/config.ts +1 -5
- package/client/plugos/syscalls/datastore.ts +1 -1
- package/client/plugos/syscalls/editor.ts +72 -69
- package/client/plugos/syscalls/event.ts +9 -12
- package/client/plugos/syscalls/fetch.ts +31 -23
- package/client/plugos/syscalls/index.ts +10 -1
- package/client/plugos/syscalls/jsonschema.ts +72 -32
- package/client/plugos/syscalls/language.ts +9 -5
- package/client/plugos/syscalls/markdown.ts +29 -7
- package/client/plugos/syscalls/mq.ts +4 -12
- package/client/plugos/syscalls/service_registry.ts +1 -4
- package/client/plugos/syscalls/shell.ts +2 -5
- package/client/plugos/syscalls/space.ts +1 -1
- package/client/plugos/syscalls/sync.ts +69 -60
- package/client/plugos/syscalls/system.ts +2 -3
- package/client/plugos/system.ts +6 -12
- package/client/plugos/worker_runtime.ts +12 -33
- package/client/space_lua/aggregates.ts +782 -0
- package/client/space_lua/ast.ts +42 -8
- package/client/space_lua/ast_narrow.ts +4 -2
- package/client/space_lua/eval.ts +886 -575
- package/client/space_lua/labels.ts +7 -12
- package/client/space_lua/liq_null.ts +6 -0
- package/client/space_lua/numeric.ts +5 -8
- package/client/space_lua/parse.ts +346 -120
- package/client/space_lua/query_collection.ts +926 -82
- package/client/space_lua/query_env.ts +26 -0
- package/client/space_lua/render_lua_markdown.ts +369 -0
- package/client/space_lua/rp.ts +5 -4
- package/client/space_lua/runtime.ts +288 -155
- package/client/space_lua/stdlib/format.ts +53 -39
- package/client/space_lua/stdlib/js.ts +3 -7
- package/client/space_lua/stdlib/load.ts +1 -3
- package/client/space_lua/stdlib/math.ts +84 -58
- package/client/space_lua/stdlib/net.ts +27 -17
- package/client/space_lua/stdlib/os.ts +81 -85
- package/client/space_lua/stdlib/pattern.ts +695 -0
- package/client/space_lua/stdlib/prng.ts +148 -0
- package/client/space_lua/stdlib/space_lua.ts +17 -23
- package/client/space_lua/stdlib/string.ts +102 -190
- package/client/space_lua/stdlib/string_pack.ts +490 -0
- package/client/space_lua/stdlib/table.ts +76 -16
- package/client/space_lua/stdlib.ts +53 -39
- package/client/space_lua/tonumber.ts +82 -42
- package/client/space_lua/util.ts +53 -15
- package/dist/plug-compile.js +55 -98
- package/package.json +27 -20
- package/plug-api/constants.ts +0 -32
- package/plug-api/lib/async.ts +20 -7
- package/plug-api/lib/crypto.ts +16 -17
- package/plug-api/lib/dates.ts +15 -7
- package/plug-api/lib/json.ts +11 -5
- package/plug-api/lib/limited_map.ts +1 -1
- package/plug-api/lib/native_fetch.ts +2 -0
- package/plug-api/lib/ref.ts +23 -23
- package/plug-api/lib/resolve.ts +7 -11
- package/plug-api/lib/tags.ts +13 -4
- package/plug-api/lib/transclusion.ts +10 -21
- package/plug-api/lib/tree.ts +165 -45
- package/plug-api/lib/yaml.ts +35 -25
- package/plug-api/syscalls/asset.ts +1 -1
- package/plug-api/syscalls/config.ts +1 -4
- package/plug-api/syscalls/editor.ts +15 -15
- package/plug-api/syscalls/jsonschema.ts +1 -3
- package/plug-api/syscalls/lua.ts +3 -9
- package/plug-api/syscalls/mq.ts +1 -4
- package/plug-api/syscalls/shell.ts +4 -1
- package/plug-api/syscalls/space.ts +3 -10
- package/plug-api/syscalls/system.ts +1 -4
- package/plug-api/syscalls/yaml.ts +2 -6
- package/plug-api/system_mock.ts +0 -1
- package/plug-api/types/client.ts +16 -1
- package/plug-api/types/event.ts +6 -4
- package/plug-api/types/manifest.ts +8 -9
- package/plugs/builtin_plugs.ts +2 -2
- package/client/plugos/sandboxes/deno_worker_sandbox.ts +0 -6
package/plug-api/lib/ref.ts
CHANGED
|
@@ -77,8 +77,9 @@ export function isValidName(name: string): boolean {
|
|
|
77
77
|
// If the name, parses as a link and doesn't provide any other info we can be
|
|
78
78
|
// sure it was only parsed as a path and that the path then conforms to all
|
|
79
79
|
// the requirements
|
|
80
|
-
return
|
|
81
|
-
encodeRef(ref) === name
|
|
80
|
+
return (
|
|
81
|
+
!!ref && !ref.details && !ref.meta && name !== "" && encodeRef(ref) === name
|
|
82
|
+
);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
/**
|
|
@@ -87,7 +88,7 @@ export function isValidName(name: string): boolean {
|
|
|
87
88
|
export function isValidPath(path: string): path is Path {
|
|
88
89
|
const ref = parseToRef(path);
|
|
89
90
|
|
|
90
|
-
return !!ref && ref.path === path;
|
|
91
|
+
return !!ref && ref.path === path && path !== "";
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/**
|
|
@@ -95,7 +96,7 @@ export function isValidPath(path: string): path is Path {
|
|
|
95
96
|
* TO THE INNER WORKINGS OF SILVERBULLET AND CHANGES COULD INTRODUCE MAJOR BUGS
|
|
96
97
|
*/
|
|
97
98
|
const refRegex =
|
|
98
|
-
/^(?<meta>\^)?(?<path>(?!.*\.[a-zA-Z0-9]+\.md$)(?!\/?(\.|\^))(?!.*(?:\/|^)\.{1,2}(?:\/|$)|.*\/{2})(?!.*(?:\]\]|\[\[))[
|
|
99
|
+
/^(?<meta>\^)?(?<path>(?!.*\.[a-zA-Z0-9]+\.md$)(?!\/?(\.|\^))(?!.*(?:\/|^)\.{1,2}(?:\/|$)|.*\/{2})(?!.*(?:\]\]|\[\[))[^@#|<>]*)(@(?<pos>\d+)|@[Ll](?<line>\d+)(?:[Cc](?<col>\d+))?|#\s*(?<header>.*))?$/;
|
|
99
100
|
|
|
100
101
|
/**
|
|
101
102
|
* Parses a reference string into a ref object.
|
|
@@ -118,13 +119,13 @@ export function parseToRef(stringRef: string): Ref | null {
|
|
|
118
119
|
if (groups.pos !== undefined) {
|
|
119
120
|
ref.details = {
|
|
120
121
|
type: "position",
|
|
121
|
-
pos: parseInt(groups.pos),
|
|
122
|
+
pos: parseInt(groups.pos, 10),
|
|
122
123
|
};
|
|
123
124
|
} else if (groups.line !== undefined) {
|
|
124
125
|
ref.details = {
|
|
125
126
|
type: "linecolumn",
|
|
126
|
-
line: parseInt(groups.line),
|
|
127
|
-
column: groups.col !== undefined ? parseInt(groups.col) : 1,
|
|
127
|
+
line: parseInt(groups.line, 10),
|
|
128
|
+
column: groups.col !== undefined ? parseInt(groups.col, 10) : 1,
|
|
128
129
|
};
|
|
129
130
|
} else if (groups.header !== undefined) {
|
|
130
131
|
ref.details = {
|
|
@@ -206,23 +207,22 @@ export function getOffsetFromHeader(
|
|
|
206
207
|
parseTree: ParseTree,
|
|
207
208
|
header: string,
|
|
208
209
|
): number {
|
|
209
|
-
const node = findNodeMatching(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return renderToText(subTree)
|
|
210
|
+
const node = findNodeMatching(parseTree, (subTree) => {
|
|
211
|
+
if (!subTree.type || !subTree.type.startsWith("ATXHeading")) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const mark = findNodeOfType(subTree, "HeaderMark");
|
|
216
|
+
if (!mark || mark.from === undefined || mark.to === undefined) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
renderToText(subTree)
|
|
222
222
|
.slice(mark.to - mark.from)
|
|
223
|
-
.trimStart() === header.trim()
|
|
224
|
-
|
|
225
|
-
);
|
|
223
|
+
.trimStart() === header.trim()
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
226
|
|
|
227
227
|
if (!node) {
|
|
228
228
|
return -1;
|
package/plug-api/lib/resolve.ts
CHANGED
|
@@ -4,9 +4,11 @@ import type { Path } from "@silverbulletmd/silverbullet/lib/ref";
|
|
|
4
4
|
* Determines wether a url points into the world wide web or to the local SB instance
|
|
5
5
|
*/
|
|
6
6
|
export function isLocalURL(url: string): boolean {
|
|
7
|
-
return
|
|
7
|
+
return (
|
|
8
|
+
!url.includes("://") &&
|
|
8
9
|
!url.startsWith("mailto:") &&
|
|
9
|
-
!url.startsWith("tel:")
|
|
10
|
+
!url.startsWith("tel:")
|
|
11
|
+
);
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -22,10 +24,7 @@ export function fileName(name: string | Path): string | Path {
|
|
|
22
24
|
return name.split("/").pop()!;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
const builtinPrefixes = [
|
|
26
|
-
"tag:",
|
|
27
|
-
"search:",
|
|
28
|
-
];
|
|
27
|
+
const builtinPrefixes = ["tag:", "search:"];
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
30
|
* Builtin pages are pages which SB should automatically consider as existing
|
|
@@ -51,11 +50,8 @@ export function resolveMarkdownLink(
|
|
|
51
50
|
if (relative.startsWith("/")) {
|
|
52
51
|
return relative.slice(1);
|
|
53
52
|
} else {
|
|
54
|
-
const splitAbsolute = absolute
|
|
55
|
-
|
|
56
|
-
.slice(0, -1);
|
|
57
|
-
const splitRelative = relative
|
|
58
|
-
.split("/");
|
|
53
|
+
const splitAbsolute = absolute.split("/").slice(0, -1);
|
|
54
|
+
const splitRelative = relative.split("/");
|
|
59
55
|
|
|
60
56
|
while (splitRelative && splitRelative[0] === "..") {
|
|
61
57
|
splitAbsolute.pop();
|
package/plug-api/lib/tags.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
|
+
/** Check if a tag marks a page as a meta page (exact "meta" or "meta/..." subtag) */
|
|
2
|
+
export function isMetaTag(tag: string): boolean {
|
|
3
|
+
return tag === "meta" || tag.startsWith("meta/");
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
/** Extract the name from hashtag text, removing # prefix and <angle brackets> if necessary */
|
|
2
7
|
export function extractHashtag(text: string): string {
|
|
3
|
-
if (text[0] !== "#") {
|
|
8
|
+
if (text[0] !== "#") {
|
|
9
|
+
// you shouldn't call this function at all
|
|
4
10
|
console.error("extractHashtag called on already clean string", text);
|
|
5
11
|
return text;
|
|
6
12
|
} else if (text[1] === "<") {
|
|
7
|
-
if (text.slice(-1) !== ">") {
|
|
13
|
+
if (text.slice(-1) !== ">") {
|
|
14
|
+
// this is malformed: #<name but maybe we're trying to autocomplete
|
|
8
15
|
return text.slice(2);
|
|
9
|
-
} else {
|
|
16
|
+
} else {
|
|
17
|
+
// this is correct #<name>
|
|
10
18
|
return text.slice(2, -1);
|
|
11
19
|
}
|
|
12
|
-
} else {
|
|
20
|
+
} else {
|
|
21
|
+
// this is just #name
|
|
13
22
|
return text.slice(1);
|
|
14
23
|
}
|
|
15
24
|
}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isLocalURL,
|
|
3
|
-
resolveMarkdownLink,
|
|
4
|
-
} from "@silverbulletmd/silverbullet/lib/resolve";
|
|
5
1
|
import {
|
|
6
2
|
mdLinkRegex,
|
|
7
3
|
wikiLinkRegex,
|
|
@@ -34,9 +30,10 @@ export type ContentDimensions = {
|
|
|
34
30
|
* Parse an alias, possibly containing dimensions into an object
|
|
35
31
|
* @example "alias", "alias|100", "alias|100x200", "100", "100x200"
|
|
36
32
|
*/
|
|
37
|
-
export function parseDimensionFromAlias(
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
export function parseDimensionFromAlias(text: string): {
|
|
34
|
+
alias: string;
|
|
35
|
+
dimension?: ContentDimensions;
|
|
36
|
+
} {
|
|
40
37
|
let alias: string;
|
|
41
38
|
let dim: ContentDimensions | undefined;
|
|
42
39
|
if (text.includes("|")) {
|
|
@@ -45,19 +42,19 @@ export function parseDimensionFromAlias(
|
|
|
45
42
|
const [width, height] = dimPart.split("x");
|
|
46
43
|
dim = {};
|
|
47
44
|
if (width) {
|
|
48
|
-
dim.width = parseInt(width);
|
|
45
|
+
dim.width = parseInt(width, 10);
|
|
49
46
|
}
|
|
50
47
|
if (height) {
|
|
51
|
-
dim.height = parseInt(height);
|
|
48
|
+
dim.height = parseInt(height, 10);
|
|
52
49
|
}
|
|
53
50
|
} else if (/^[x\d]/.test(text)) {
|
|
54
51
|
const [width, height] = text.split("x");
|
|
55
52
|
dim = {};
|
|
56
53
|
if (width) {
|
|
57
|
-
dim.width = parseInt(width);
|
|
54
|
+
dim.width = parseInt(width, 10);
|
|
58
55
|
}
|
|
59
56
|
if (height) {
|
|
60
|
-
dim.height = parseInt(height);
|
|
57
|
+
dim.height = parseInt(height, 10);
|
|
61
58
|
}
|
|
62
59
|
alias = "";
|
|
63
60
|
} else {
|
|
@@ -71,10 +68,8 @@ export function parseDimensionFromAlias(
|
|
|
71
68
|
* Parses a transclusion of the type `![[]]` or `![]()`
|
|
72
69
|
* @param text
|
|
73
70
|
*/
|
|
74
|
-
export function parseTransclusion(
|
|
75
|
-
|
|
76
|
-
): Transclusion | null {
|
|
77
|
-
let url, alias = undefined;
|
|
71
|
+
export function parseTransclusion(text: string): Transclusion | null {
|
|
72
|
+
let url, alias;
|
|
78
73
|
let linktype: LinkType = "markdownlink";
|
|
79
74
|
// TODO: Take in the tree and use tree nodes to get url and alias (Applies to all regex uses)
|
|
80
75
|
mdLinkRegex.lastIndex = 0;
|
|
@@ -83,12 +78,6 @@ export function parseTransclusion(
|
|
|
83
78
|
if ((match = mdLinkRegex.exec(text)) && match.groups) {
|
|
84
79
|
({ url, title: alias } = match.groups);
|
|
85
80
|
|
|
86
|
-
if (isLocalURL(url)) {
|
|
87
|
-
url = resolveMarkdownLink(
|
|
88
|
-
client.currentName(),
|
|
89
|
-
decodeURI(url),
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
81
|
linktype = "markdownlink";
|
|
93
82
|
} else if ((match = wikiLinkRegex.exec(text)) && match.groups) {
|
|
94
83
|
({ stringRef: url, alias } = match.groups);
|
package/plug-api/lib/tree.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { deepClone } from "@silverbulletmd/silverbullet/lib/json";
|
|
2
|
-
|
|
3
1
|
export type ParseTree = {
|
|
4
2
|
type?: string; // undefined === text node
|
|
5
3
|
from?: number;
|
|
@@ -21,7 +19,7 @@ export function addParentPointers(tree: ParseTree) {
|
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
export function removeParentPointers(tree: ParseTree) {
|
|
24
|
-
|
|
22
|
+
tree.parent = undefined;
|
|
25
23
|
if (!tree.children) {
|
|
26
24
|
return;
|
|
27
25
|
}
|
|
@@ -51,38 +49,53 @@ export function collectNodesOfType(
|
|
|
51
49
|
return collectNodesMatching(tree, (n) => n.type === nodeType);
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
|
|
52
|
+
function collectNodesMatchingInternal(
|
|
55
53
|
tree: ParseTree,
|
|
56
54
|
matchFn: (tree: ParseTree) => boolean,
|
|
57
|
-
|
|
55
|
+
results: ParseTree[],
|
|
56
|
+
): void {
|
|
58
57
|
if (matchFn(tree)) {
|
|
59
|
-
|
|
58
|
+
results.push(tree);
|
|
59
|
+
return;
|
|
60
60
|
}
|
|
61
|
-
let results: ParseTree[] = [];
|
|
62
61
|
if (tree.children) {
|
|
63
62
|
for (const child of tree.children) {
|
|
64
|
-
|
|
63
|
+
collectNodesMatchingInternal(child, matchFn, results);
|
|
65
64
|
}
|
|
66
65
|
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function collectNodesMatching(
|
|
69
|
+
tree: ParseTree,
|
|
70
|
+
matchFn: (tree: ParseTree) => boolean,
|
|
71
|
+
): ParseTree[] {
|
|
72
|
+
const results: ParseTree[] = [];
|
|
73
|
+
collectNodesMatchingInternal(tree, matchFn, results);
|
|
67
74
|
return results;
|
|
68
75
|
}
|
|
69
76
|
|
|
70
|
-
|
|
77
|
+
async function collectNodesMatchingAsyncInternal(
|
|
71
78
|
tree: ParseTree,
|
|
72
79
|
matchFn: (tree: ParseTree) => Promise<boolean>,
|
|
73
|
-
|
|
80
|
+
results: ParseTree[],
|
|
81
|
+
): Promise<void> {
|
|
74
82
|
if (await matchFn(tree)) {
|
|
75
|
-
|
|
83
|
+
results.push(tree);
|
|
84
|
+
return;
|
|
76
85
|
}
|
|
77
|
-
let results: ParseTree[] = [];
|
|
78
86
|
if (tree.children) {
|
|
79
87
|
for (const child of tree.children) {
|
|
80
|
-
|
|
81
|
-
...results,
|
|
82
|
-
...await collectNodesMatchingAsync(child, matchFn),
|
|
83
|
-
];
|
|
88
|
+
await collectNodesMatchingAsyncInternal(child, matchFn, results);
|
|
84
89
|
}
|
|
85
90
|
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function collectNodesMatchingAsync(
|
|
94
|
+
tree: ParseTree,
|
|
95
|
+
matchFn: (tree: ParseTree) => Promise<boolean>,
|
|
96
|
+
): Promise<ParseTree[]> {
|
|
97
|
+
const results: ParseTree[] = [];
|
|
98
|
+
await collectNodesMatchingAsyncInternal(tree, matchFn, results);
|
|
86
99
|
return results;
|
|
87
100
|
}
|
|
88
101
|
|
|
@@ -91,20 +104,23 @@ export function replaceNodesMatching(
|
|
|
91
104
|
tree: ParseTree,
|
|
92
105
|
substituteFn: (tree: ParseTree) => ParseTree | null | undefined,
|
|
93
106
|
) {
|
|
94
|
-
if (tree
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
if (tree?.children) {
|
|
108
|
+
let i = 0;
|
|
109
|
+
while (i < tree.children.length) {
|
|
110
|
+
const child = tree.children[i];
|
|
97
111
|
const subst = substituteFn(child);
|
|
98
112
|
if (subst !== undefined) {
|
|
99
|
-
const pos = tree.children.indexOf(child);
|
|
100
113
|
if (subst) {
|
|
101
|
-
tree.children
|
|
114
|
+
tree.children[i] = subst;
|
|
115
|
+
i++;
|
|
102
116
|
} else {
|
|
103
117
|
// null = delete
|
|
104
|
-
tree.children.splice(
|
|
118
|
+
tree.children.splice(i, 1);
|
|
119
|
+
// don't increment i — next child shifted into this position
|
|
105
120
|
}
|
|
106
121
|
} else {
|
|
107
122
|
replaceNodesMatching(child, substituteFn);
|
|
123
|
+
i++;
|
|
108
124
|
}
|
|
109
125
|
}
|
|
110
126
|
}
|
|
@@ -115,19 +131,21 @@ export async function replaceNodesMatchingAsync(
|
|
|
115
131
|
substituteFn: (tree: ParseTree) => Promise<ParseTree | null | undefined>,
|
|
116
132
|
) {
|
|
117
133
|
if (tree.children) {
|
|
118
|
-
|
|
119
|
-
|
|
134
|
+
let i = 0;
|
|
135
|
+
while (i < tree.children.length) {
|
|
136
|
+
const child = tree.children[i];
|
|
120
137
|
const subst = await substituteFn(child);
|
|
121
138
|
if (subst !== undefined) {
|
|
122
|
-
const pos = tree.children.indexOf(child);
|
|
123
139
|
if (subst) {
|
|
124
|
-
tree.children
|
|
140
|
+
tree.children[i] = subst;
|
|
141
|
+
i++;
|
|
125
142
|
} else {
|
|
126
143
|
// null = delete
|
|
127
|
-
tree.children.splice(
|
|
144
|
+
tree.children.splice(i, 1);
|
|
128
145
|
}
|
|
129
146
|
} else {
|
|
130
147
|
await replaceNodesMatchingAsync(child, substituteFn);
|
|
148
|
+
i++;
|
|
131
149
|
}
|
|
132
150
|
}
|
|
133
151
|
}
|
|
@@ -137,36 +155,88 @@ export function findNodeMatching(
|
|
|
137
155
|
tree: ParseTree,
|
|
138
156
|
matchFn: (tree: ParseTree) => boolean,
|
|
139
157
|
): ParseTree | null {
|
|
140
|
-
|
|
158
|
+
if (matchFn(tree)) {
|
|
159
|
+
return tree;
|
|
160
|
+
}
|
|
161
|
+
if (tree.children) {
|
|
162
|
+
for (const child of tree.children) {
|
|
163
|
+
const result = findNodeMatching(child, matchFn);
|
|
164
|
+
if (result) {
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
141
170
|
}
|
|
142
171
|
|
|
143
172
|
export function findNodeOfType(
|
|
144
173
|
tree: ParseTree,
|
|
145
174
|
nodeType: string,
|
|
146
175
|
): ParseTree | null {
|
|
147
|
-
|
|
176
|
+
if (tree.type === nodeType) {
|
|
177
|
+
return tree;
|
|
178
|
+
}
|
|
179
|
+
if (tree.children) {
|
|
180
|
+
for (const child of tree.children) {
|
|
181
|
+
const result = findNodeOfType(child, nodeType);
|
|
182
|
+
if (result) {
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
148
188
|
}
|
|
149
189
|
|
|
150
190
|
export function traverseTree(
|
|
151
191
|
tree: ParseTree,
|
|
152
|
-
// Return value = should stop traversal?
|
|
192
|
+
// Return value = should stop traversal into children?
|
|
153
193
|
matchFn: (tree: ParseTree) => boolean,
|
|
154
194
|
): void {
|
|
155
|
-
|
|
156
|
-
|
|
195
|
+
if (matchFn(tree)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (tree.children) {
|
|
199
|
+
for (const child of tree.children) {
|
|
200
|
+
traverseTree(child, matchFn);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
157
203
|
}
|
|
158
204
|
|
|
159
205
|
export async function traverseTreeAsync(
|
|
160
206
|
tree: ParseTree,
|
|
161
|
-
// Return value = should stop traversal?
|
|
207
|
+
// Return value = should stop traversal into children?
|
|
162
208
|
matchFn: (tree: ParseTree) => Promise<boolean>,
|
|
163
209
|
): Promise<void> {
|
|
164
|
-
|
|
165
|
-
|
|
210
|
+
if (await matchFn(tree)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (tree.children) {
|
|
214
|
+
for (const child of tree.children) {
|
|
215
|
+
await traverseTreeAsync(child, matchFn);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
166
218
|
}
|
|
167
219
|
|
|
168
220
|
export function cloneTree(tree: ParseTree): ParseTree {
|
|
169
|
-
|
|
221
|
+
if (tree.text !== undefined) {
|
|
222
|
+
return {
|
|
223
|
+
from: tree.from,
|
|
224
|
+
to: tree.to,
|
|
225
|
+
text: tree.text,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const clone: ParseTree = {
|
|
229
|
+
type: tree.type,
|
|
230
|
+
from: tree.from,
|
|
231
|
+
to: tree.to,
|
|
232
|
+
};
|
|
233
|
+
if (tree.children) {
|
|
234
|
+
clone.children = new Array(tree.children.length);
|
|
235
|
+
for (let i = 0; i < tree.children.length; i++) {
|
|
236
|
+
clone.children[i] = cloneTree(tree.children[i]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return clone;
|
|
170
240
|
}
|
|
171
241
|
|
|
172
242
|
// Finds non-text node at position
|
|
@@ -182,7 +252,8 @@ export function nodeAtPos(tree: ParseTree, pos: number): ParseTree | null {
|
|
|
182
252
|
if (n && n.text !== undefined) {
|
|
183
253
|
// Got a text node, let's return its parent
|
|
184
254
|
return tree;
|
|
185
|
-
}
|
|
255
|
+
}
|
|
256
|
+
if (n) {
|
|
186
257
|
// Got it
|
|
187
258
|
return n;
|
|
188
259
|
}
|
|
@@ -190,26 +261,75 @@ export function nodeAtPos(tree: ParseTree, pos: number): ParseTree | null {
|
|
|
190
261
|
return null;
|
|
191
262
|
}
|
|
192
263
|
|
|
264
|
+
// Ensure a TableRow/TableHeader has a TableCell between every pair of
|
|
265
|
+
// TableDelimiters, and optionally pad to match columnCount.
|
|
266
|
+
// headerHasLeadingDelim indicates whether the header starts with a delimiter.
|
|
267
|
+
export function normalizeTableRow(
|
|
268
|
+
row: ParseTree,
|
|
269
|
+
columnCount?: number,
|
|
270
|
+
headerHasLeadingDelim?: boolean,
|
|
271
|
+
): void {
|
|
272
|
+
const children = row.children;
|
|
273
|
+
if (!children) return;
|
|
274
|
+
const normalized: ParseTree[] = [];
|
|
275
|
+
let lookingForCell = false;
|
|
276
|
+
for (const child of children) {
|
|
277
|
+
if (child.type === "TableDelimiter" && lookingForCell) {
|
|
278
|
+
normalized.push({ type: "TableCell", children: [] });
|
|
279
|
+
}
|
|
280
|
+
if (child.type === "TableDelimiter") {
|
|
281
|
+
lookingForCell = true;
|
|
282
|
+
}
|
|
283
|
+
if (child.type === "TableCell") {
|
|
284
|
+
lookingForCell = false;
|
|
285
|
+
}
|
|
286
|
+
normalized.push(child);
|
|
287
|
+
}
|
|
288
|
+
row.children = normalized;
|
|
289
|
+
|
|
290
|
+
// Fix leading-pipe mismatch: row has leading delimiter but header doesn't
|
|
291
|
+
if (headerHasLeadingDelim === false) {
|
|
292
|
+
if (row.children.length > 0 && row.children[0].type === "TableDelimiter") {
|
|
293
|
+
// Insert empty cell after the leading delimiter
|
|
294
|
+
row.children.splice(1, 0, { type: "TableCell", children: [] });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Pad trailing empty cells to match header column count
|
|
299
|
+
if (columnCount !== undefined) {
|
|
300
|
+
let cellCount = 0;
|
|
301
|
+
for (const child of row.children) {
|
|
302
|
+
if (child.type === "TableCell") cellCount++;
|
|
303
|
+
}
|
|
304
|
+
while (cellCount < columnCount) {
|
|
305
|
+
row.children.push({ type: "TableCell", children: [] });
|
|
306
|
+
cellCount++;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
193
311
|
// Turn ParseTree back into text
|
|
194
312
|
export function renderToText(tree?: ParseTree): string {
|
|
195
313
|
if (!tree) {
|
|
196
314
|
return "";
|
|
197
315
|
}
|
|
198
|
-
const pieces: string[] = [];
|
|
199
316
|
if (tree.text !== undefined) {
|
|
200
317
|
return tree.text;
|
|
201
318
|
}
|
|
202
|
-
|
|
203
|
-
|
|
319
|
+
const children = tree.children!;
|
|
320
|
+
if (children.length === 1) {
|
|
321
|
+
return renderToText(children[0]);
|
|
322
|
+
}
|
|
323
|
+
let result = "";
|
|
324
|
+
for (const child of children) {
|
|
325
|
+
result += renderToText(child);
|
|
204
326
|
}
|
|
205
|
-
return
|
|
327
|
+
return result;
|
|
206
328
|
}
|
|
207
329
|
|
|
208
330
|
export function cleanTree(tree: ParseTree, omitTrimmable = true): ParseTree {
|
|
209
331
|
if (tree.type === "⚠") {
|
|
210
|
-
throw new Error(
|
|
211
|
-
`Parse error at pos ${tree.from}`,
|
|
212
|
-
);
|
|
332
|
+
throw new Error(`Parse error at pos ${tree.from}`);
|
|
213
333
|
}
|
|
214
334
|
if (tree.text !== undefined) {
|
|
215
335
|
return tree;
|
|
@@ -224,7 +344,7 @@ export function cleanTree(tree: ParseTree, omitTrimmable = true): ParseTree {
|
|
|
224
344
|
if (node.type && node.type !== "Comment") {
|
|
225
345
|
ast.children!.push(cleanTree(node, omitTrimmable));
|
|
226
346
|
}
|
|
227
|
-
if (node.text && (omitTrimmable && node.text.trim() || !omitTrimmable)) {
|
|
347
|
+
if (node.text && ((omitTrimmable && node.text.trim()) || !omitTrimmable)) {
|
|
228
348
|
ast.children!.push(node);
|
|
229
349
|
}
|
|
230
350
|
}
|