@silverbulletmd/silverbullet 2.5.3 → 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.
Files changed (90) hide show
  1. package/README.md +4 -5
  2. package/client/asset_bundle/bundle.ts +3 -9
  3. package/client/data/datastore.ts +4 -5
  4. package/client/markdown_parser/constants.ts +3 -2
  5. package/client/plugos/hooks/code_widget.ts +3 -5
  6. package/client/plugos/hooks/command.ts +8 -8
  7. package/client/plugos/hooks/document_editor.ts +10 -12
  8. package/client/plugos/hooks/event.ts +33 -36
  9. package/client/plugos/hooks/mq.ts +17 -17
  10. package/client/plugos/hooks/plug_namespace.ts +3 -5
  11. package/client/plugos/hooks/slash_command.ts +12 -27
  12. package/client/plugos/hooks/syscall.ts +3 -3
  13. package/client/plugos/manifest_cache.ts +22 -15
  14. package/client/plugos/plug.ts +2 -5
  15. package/client/plugos/plug_compile.ts +67 -65
  16. package/client/plugos/protocol.ts +28 -28
  17. package/client/plugos/proxy_fetch.ts +7 -6
  18. package/client/plugos/sandboxes/worker_sandbox.ts +16 -15
  19. package/client/plugos/syscalls/asset.ts +1 -3
  20. package/client/plugos/syscalls/code_widget.ts +1 -3
  21. package/client/plugos/syscalls/config.ts +1 -5
  22. package/client/plugos/syscalls/datastore.ts +1 -1
  23. package/client/plugos/syscalls/editor.ts +63 -60
  24. package/client/plugos/syscalls/event.ts +9 -12
  25. package/client/plugos/syscalls/fetch.ts +30 -22
  26. package/client/plugos/syscalls/index.ts +10 -1
  27. package/client/plugos/syscalls/jsonschema.ts +72 -32
  28. package/client/plugos/syscalls/language.ts +9 -5
  29. package/client/plugos/syscalls/markdown.ts +29 -7
  30. package/client/plugos/syscalls/mq.ts +3 -11
  31. package/client/plugos/syscalls/service_registry.ts +1 -4
  32. package/client/plugos/syscalls/shell.ts +2 -5
  33. package/client/plugos/syscalls/sync.ts +69 -60
  34. package/client/plugos/syscalls/system.ts +2 -3
  35. package/client/plugos/system.ts +4 -10
  36. package/client/plugos/worker_runtime.ts +4 -3
  37. package/client/space_lua/aggregates.ts +632 -59
  38. package/client/space_lua/ast.ts +21 -9
  39. package/client/space_lua/ast_narrow.ts +4 -2
  40. package/client/space_lua/eval.ts +842 -536
  41. package/client/space_lua/labels.ts +6 -11
  42. package/client/space_lua/liq_null.ts +6 -0
  43. package/client/space_lua/numeric.ts +5 -8
  44. package/client/space_lua/parse.ts +290 -169
  45. package/client/space_lua/query_collection.ts +213 -149
  46. package/client/space_lua/render_lua_markdown.ts +369 -0
  47. package/client/space_lua/rp.ts +5 -4
  48. package/client/space_lua/runtime.ts +245 -142
  49. package/client/space_lua/stdlib/format.ts +34 -20
  50. package/client/space_lua/stdlib/js.ts +3 -7
  51. package/client/space_lua/stdlib/load.ts +1 -3
  52. package/client/space_lua/stdlib/math.ts +15 -14
  53. package/client/space_lua/stdlib/net.ts +25 -15
  54. package/client/space_lua/stdlib/os.ts +76 -85
  55. package/client/space_lua/stdlib/pattern.ts +28 -35
  56. package/client/space_lua/stdlib/prng.ts +15 -12
  57. package/client/space_lua/stdlib/space_lua.ts +16 -17
  58. package/client/space_lua/stdlib/string.ts +7 -17
  59. package/client/space_lua/stdlib/string_pack.ts +23 -19
  60. package/client/space_lua/stdlib/table.ts +5 -9
  61. package/client/space_lua/stdlib.ts +20 -30
  62. package/client/space_lua/tonumber.ts +79 -40
  63. package/client/space_lua/util.ts +14 -10
  64. package/dist/plug-compile.js +44 -41
  65. package/package.json +24 -22
  66. package/plug-api/lib/async.ts +19 -6
  67. package/plug-api/lib/crypto.ts +5 -6
  68. package/plug-api/lib/dates.ts +15 -7
  69. package/plug-api/lib/json.ts +10 -4
  70. package/plug-api/lib/ref.ts +18 -18
  71. package/plug-api/lib/resolve.ts +7 -11
  72. package/plug-api/lib/tags.ts +13 -4
  73. package/plug-api/lib/transclusion.ts +6 -17
  74. package/plug-api/lib/tree.ts +115 -43
  75. package/plug-api/lib/yaml.ts +25 -15
  76. package/plug-api/syscalls/asset.ts +1 -1
  77. package/plug-api/syscalls/config.ts +1 -4
  78. package/plug-api/syscalls/editor.ts +14 -14
  79. package/plug-api/syscalls/jsonschema.ts +1 -3
  80. package/plug-api/syscalls/lua.ts +3 -9
  81. package/plug-api/syscalls/mq.ts +1 -4
  82. package/plug-api/syscalls/shell.ts +4 -1
  83. package/plug-api/syscalls/space.ts +3 -10
  84. package/plug-api/syscalls/system.ts +1 -4
  85. package/plug-api/syscalls/yaml.ts +2 -6
  86. package/plug-api/types/client.ts +16 -1
  87. package/plug-api/types/event.ts +6 -4
  88. package/plug-api/types/manifest.ts +8 -9
  89. package/plugs/builtin_plugs.ts +2 -2
  90. package/dist/worker_runtime_bundle.js +0 -233
@@ -1,6 +1,9 @@
1
- export function throttle(func: () => void, limit: number): () => void {
1
+ export function throttle(
2
+ func: () => void,
3
+ limit: number,
4
+ ): (() => void) & { flush(): void } {
2
5
  let timer: any = null;
3
- return () => {
6
+ const throttled = () => {
4
7
  if (!timer) {
5
8
  timer = setTimeout(() => {
6
9
  func();
@@ -8,6 +11,15 @@ export function throttle(func: () => void, limit: number): () => void {
8
11
  }, limit);
9
12
  }
10
13
  };
14
+ // Immediately execute any pending call and cancel the timer
15
+ throttled.flush = () => {
16
+ if (timer) {
17
+ clearTimeout(timer);
18
+ timer = null;
19
+ func();
20
+ }
21
+ };
22
+ return throttled;
11
23
  }
12
24
 
13
25
  export function throttleImmediately(
@@ -38,7 +50,7 @@ export function timeout(ms: number): Promise<never> {
38
50
  return new Promise((_resolve, reject) =>
39
51
  setTimeout(() => {
40
52
  reject(new Error("timeout"));
41
- }, ms)
53
+ }, ms),
42
54
  );
43
55
  }
44
56
 
@@ -58,7 +70,7 @@ export class PromiseQueue {
58
70
  return new Promise((resolve, reject) => {
59
71
  this.queue.push({ fn, resolve, reject });
60
72
  if (!this.processing) {
61
- this.process();
73
+ void this.process();
62
74
  }
63
75
  });
64
76
  }
@@ -79,7 +91,7 @@ export class PromiseQueue {
79
91
  reject(error);
80
92
  }
81
93
 
82
- this.process(); // Continue processing the next promise in the queue
94
+ void this.process(); // Continue processing the next promise in the queue
83
95
  }
84
96
  }
85
97
 
@@ -105,7 +117,8 @@ export async function batchRequests<I, O>(
105
117
  const batchResults = await Promise.all(batches.map(fn));
106
118
  // Flatten the results
107
119
  for (const batchResult of batchResults) {
108
- if (Array.isArray(batchResult)) { // If fn returns an array, collect them
120
+ if (Array.isArray(batchResult)) {
121
+ // If fn returns an array, collect them
109
122
  results.push(...batchResult);
110
123
  }
111
124
  }
@@ -42,9 +42,8 @@ export async function hashSHA256(
42
42
  ): Promise<string> {
43
43
  // Transform the string into an ArrayBuffer
44
44
  const encoder = new TextEncoder();
45
- const data: Uint8Array = typeof message === "string"
46
- ? encoder.encode(message)
47
- : message;
45
+ const data: Uint8Array =
46
+ typeof message === "string" ? encoder.encode(message) : message;
48
47
 
49
48
  // Generate the hash
50
49
  const hashBuffer = await globalThis.crypto.subtle.digest(
@@ -53,9 +52,9 @@ export async function hashSHA256(
53
52
  );
54
53
 
55
54
  // Transform the hash into a hex string
56
- return Array.from(new Uint8Array(hashBuffer)).map((b) =>
57
- b.toString(16).padStart(2, "0")
58
- ).join("");
55
+ return Array.from(new Uint8Array(hashBuffer))
56
+ .map((b) => b.toString(16).padStart(2, "0"))
57
+ .join("");
59
58
  }
60
59
 
61
60
  /**
@@ -3,11 +3,19 @@ export function niceDate(d: Date): string {
3
3
  }
4
4
 
5
5
  export function localDateString(d: Date): string {
6
- return d.getFullYear() +
7
- "-" + String(d.getMonth() + 1).padStart(2, "0") +
8
- "-" + String(d.getDate()).padStart(2, "0") +
9
- "T" + String(d.getHours()).padStart(2, "0") +
10
- ":" + String(d.getMinutes()).padStart(2, "0") +
11
- ":" + String(d.getSeconds()).padStart(2, "0") +
12
- "." + String(d.getMilliseconds()).padStart(3, "0");
6
+ return (
7
+ d.getFullYear() +
8
+ "-" +
9
+ String(d.getMonth() + 1).padStart(2, "0") +
10
+ "-" +
11
+ String(d.getDate()).padStart(2, "0") +
12
+ "T" +
13
+ String(d.getHours()).padStart(2, "0") +
14
+ ":" +
15
+ String(d.getMinutes()).padStart(2, "0") +
16
+ ":" +
17
+ String(d.getSeconds()).padStart(2, "0") +
18
+ "." +
19
+ String(d.getMilliseconds()).padStart(3, "0")
20
+ );
13
21
  }
@@ -52,11 +52,17 @@ export function deepEqual(a: any, b: any): boolean {
52
52
  export function cleanStringDate(d: Date): string {
53
53
  // If no significant time, return a date string only
54
54
  if (
55
- d.getUTCHours() === 0 && d.getUTCMinutes() === 0 && d.getUTCSeconds() === 0
55
+ d.getUTCHours() === 0 &&
56
+ d.getUTCMinutes() === 0 &&
57
+ d.getUTCSeconds() === 0
56
58
  ) {
57
- return d.getUTCFullYear() + "-" +
58
- String(d.getUTCMonth() + 1).padStart(2, "0") + "-" +
59
- String(d.getUTCDate()).padStart(2, "0");
59
+ return (
60
+ d.getUTCFullYear() +
61
+ "-" +
62
+ String(d.getUTCMonth() + 1).padStart(2, "0") +
63
+ "-" +
64
+ String(d.getUTCDate()).padStart(2, "0")
65
+ );
60
66
  } else {
61
67
  return d.toISOString();
62
68
  }
@@ -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 !!ref && !ref.details && !ref.meta && name !== "" &&
81
- encodeRef(ref) === name;
80
+ return (
81
+ !!ref && !ref.details && !ref.meta && name !== "" && encodeRef(ref) === name
82
+ );
82
83
  }
83
84
 
84
85
  /**
@@ -206,23 +207,22 @@ export function getOffsetFromHeader(
206
207
  parseTree: ParseTree,
207
208
  header: string,
208
209
  ): 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)
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;
@@ -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 !url.includes("://") &&
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
- .split("/")
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();
@@ -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] !== "#") { // you shouldn't call this function at all
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) !== ">") { // this is malformed: #<name but maybe we're trying to autocomplete
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 { // this is correct #<name>
16
+ } else {
17
+ // this is correct #<name>
10
18
  return text.slice(2, -1);
11
19
  }
12
- } else { // this is just #name
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
- text: string,
39
- ): { alias: string; dimension?: ContentDimensions } {
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("|")) {
@@ -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
- text: string,
76
- ): Transclusion | null {
77
- let url, alias ;
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);
@@ -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
- delete tree.parent;
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
- export function collectNodesMatching(
52
+ function collectNodesMatchingInternal(
55
53
  tree: ParseTree,
56
54
  matchFn: (tree: ParseTree) => boolean,
57
- ): ParseTree[] {
55
+ results: ParseTree[],
56
+ ): void {
58
57
  if (matchFn(tree)) {
59
- return [tree];
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
- results = [...results, ...collectNodesMatching(child, matchFn)];
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
- export async function collectNodesMatchingAsync(
77
+ async function collectNodesMatchingAsyncInternal(
71
78
  tree: ParseTree,
72
79
  matchFn: (tree: ParseTree) => Promise<boolean>,
73
- ): Promise<ParseTree[]> {
80
+ results: ParseTree[],
81
+ ): Promise<void> {
74
82
  if (await matchFn(tree)) {
75
- return [tree];
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
- results = [
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
 
@@ -92,19 +105,22 @@ export function replaceNodesMatching(
92
105
  substituteFn: (tree: ParseTree) => ParseTree | null | undefined,
93
106
  ) {
94
107
  if (tree?.children) {
95
- const children = tree.children.slice();
96
- for (const child of 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.splice(pos, 1, subst);
114
+ tree.children[i] = subst;
115
+ i++;
102
116
  } else {
103
117
  // null = delete
104
- tree.children.splice(pos, 1);
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
- const children = tree.children.slice();
119
- for (const child of children) {
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.splice(pos, 1, subst);
140
+ tree.children[i] = subst;
141
+ i++;
125
142
  } else {
126
143
  // null = delete
127
- tree.children.splice(pos, 1);
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
- return collectNodesMatching(tree, matchFn)[0];
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
- return collectNodesMatching(tree, (n) => n.type === nodeType)[0];
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
- // Do a collect, but ignore the result
156
- collectNodesMatching(tree, matchFn);
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
- // Do a collect, but ignore the result
165
- await collectNodesMatchingAsync(tree, matchFn);
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
- return deepClone(tree, ["parent"]);
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
@@ -243,21 +313,23 @@ export function renderToText(tree?: ParseTree): string {
243
313
  if (!tree) {
244
314
  return "";
245
315
  }
246
- const pieces: string[] = [];
247
316
  if (tree.text !== undefined) {
248
317
  return tree.text;
249
318
  }
250
- for (const child of tree.children!) {
251
- pieces.push(renderToText(child));
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);
252
326
  }
253
- return pieces.join("");
327
+ return result;
254
328
  }
255
329
 
256
330
  export function cleanTree(tree: ParseTree, omitTrimmable = true): ParseTree {
257
331
  if (tree.type === "⚠") {
258
- throw new Error(
259
- `Parse error at pos ${tree.from}`,
260
- );
332
+ throw new Error(`Parse error at pos ${tree.from}`);
261
333
  }
262
334
  if (tree.text !== undefined) {
263
335
  return tree;
@@ -272,7 +344,7 @@ export function cleanTree(tree: ParseTree, omitTrimmable = true): ParseTree {
272
344
  if (node.type && node.type !== "Comment") {
273
345
  ast.children!.push(cleanTree(node, omitTrimmable));
274
346
  }
275
- if (node.text && (omitTrimmable && node.text.trim() || !omitTrimmable)) {
347
+ if (node.text && ((omitTrimmable && node.text.trim()) || !omitTrimmable)) {
276
348
  ast.children!.push(node);
277
349
  }
278
350
  }
@@ -25,7 +25,9 @@ function serializeToYamlScalar(
25
25
  // Simple strings without special chars/meaning don't need quotes
26
26
  return value;
27
27
  } else if (
28
- typeof value === "number" || typeof value === "boolean" || value === null
28
+ typeof value === "number" ||
29
+ typeof value === "boolean" ||
30
+ value === null
29
31
  ) {
30
32
  return String(value);
31
33
  }
@@ -46,10 +48,15 @@ function serializeToYamlValue(
46
48
  // Determine indentation for list items (base + 2 spaces)
47
49
  const itemIndentation = `${baseIndentation} `;
48
50
  // Format each item recursively, preceded by '- ' marker
49
- return "\n" +
50
- value.map((item) =>
51
- `${itemIndentation}- ${serializeToYamlValue(item, itemIndentation)}`
52
- ).join("\n");
51
+ return (
52
+ "\n" +
53
+ value
54
+ .map(
55
+ (item) =>
56
+ `${itemIndentation}- ${serializeToYamlValue(item, itemIndentation)}`,
57
+ )
58
+ .join("\n")
59
+ );
53
60
  // Note: serializeToYamlValue is used recursively here to handle nested arrays/objects if needed in future
54
61
  // However, the current `applyMinimalSetKeyPatches` only handles top-level keys.
55
62
  } else if (typeof value === "object" && value !== null) {
@@ -58,22 +65,25 @@ function serializeToYamlValue(
58
65
  const itemIndentation = `${baseIndentation} `;
59
66
  const entries = Object.entries(value);
60
67
  if (entries.length === 0) return "{}"; // Flow style empty objects
61
- return "\n" +
62
- entries.map(([key, val]) =>
63
- `${itemIndentation}${key}: ${
64
- serializeToYamlValue(val, itemIndentation)
65
- }`
66
- ).join("\n");
68
+ return (
69
+ "\n" +
70
+ entries
71
+ .map(
72
+ ([key, val]) =>
73
+ `${itemIndentation}${key}: ${serializeToYamlValue(
74
+ val,
75
+ itemIndentation,
76
+ )}`,
77
+ )
78
+ .join("\n")
79
+ );
67
80
  } else {
68
81
  // Handle scalars using the dedicated function
69
82
  return serializeToYamlScalar(value);
70
83
  }
71
84
  }
72
85
 
73
- export function applyPatches(
74
- yamlString: string,
75
- patches: YamlPatch[],
76
- ): string {
86
+ export function applyPatches(yamlString: string, patches: YamlPatch[]): string {
77
87
  let currentYaml = yamlString;
78
88
 
79
89
  for (const patch of patches) {
@@ -15,7 +15,7 @@ export async function readAsset(
15
15
  name: string,
16
16
  encoding: "utf8" | "dataurl" = "utf8",
17
17
  ): Promise<string> {
18
- const dataUrl = await syscall("asset.readAsset", plugName, name) as string;
18
+ const dataUrl = (await syscall("asset.readAsset", plugName, name)) as string;
19
19
  switch (encoding) {
20
20
  case "utf8":
21
21
  return new TextDecoder().decode(base64DecodeDataUrl(dataUrl));
@@ -31,10 +31,7 @@ export function set<T>(
31
31
  /**
32
32
  * Inserts a config value into an array
33
33
  */
34
- export function insert<T>(
35
- path: string | string[],
36
- value: T,
37
- ): Promise<void> {
34
+ export function insert<T>(path: string | string[], value: T): Promise<void> {
38
35
  return syscall("config.insert", path, value);
39
36
  }
40
37