@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,232 @@
|
|
|
1
|
+
import { deepClone } from "@silverbulletmd/silverbullet/lib/json";
|
|
2
|
+
|
|
3
|
+
export type ParseTree = {
|
|
4
|
+
type?: string; // undefined === text node
|
|
5
|
+
from?: number;
|
|
6
|
+
to?: number;
|
|
7
|
+
text?: string;
|
|
8
|
+
children?: ParseTree[];
|
|
9
|
+
// Only present after running addParentPointers
|
|
10
|
+
parent?: ParseTree;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function addParentPointers(tree: ParseTree) {
|
|
14
|
+
if (!tree.children) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
for (const child of tree.children) {
|
|
18
|
+
child.parent = tree;
|
|
19
|
+
addParentPointers(child);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function removeParentPointers(tree: ParseTree) {
|
|
24
|
+
delete tree.parent;
|
|
25
|
+
if (!tree.children) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const child of tree.children) {
|
|
29
|
+
removeParentPointers(child);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function findParentMatching(
|
|
34
|
+
tree: ParseTree,
|
|
35
|
+
matchFn: (tree: ParseTree) => boolean,
|
|
36
|
+
): ParseTree | null {
|
|
37
|
+
let node = tree.parent;
|
|
38
|
+
while (node) {
|
|
39
|
+
if (matchFn(node)) {
|
|
40
|
+
return node;
|
|
41
|
+
}
|
|
42
|
+
node = node.parent;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function collectNodesOfType(
|
|
48
|
+
tree: ParseTree,
|
|
49
|
+
nodeType: string,
|
|
50
|
+
): ParseTree[] {
|
|
51
|
+
return collectNodesMatching(tree, (n) => n.type === nodeType);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function collectNodesMatching(
|
|
55
|
+
tree: ParseTree,
|
|
56
|
+
matchFn: (tree: ParseTree) => boolean,
|
|
57
|
+
): ParseTree[] {
|
|
58
|
+
if (matchFn(tree)) {
|
|
59
|
+
return [tree];
|
|
60
|
+
}
|
|
61
|
+
let results: ParseTree[] = [];
|
|
62
|
+
if (tree.children) {
|
|
63
|
+
for (const child of tree.children) {
|
|
64
|
+
results = [...results, ...collectNodesMatching(child, matchFn)];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function collectNodesMatchingAsync(
|
|
71
|
+
tree: ParseTree,
|
|
72
|
+
matchFn: (tree: ParseTree) => Promise<boolean>,
|
|
73
|
+
): Promise<ParseTree[]> {
|
|
74
|
+
if (await matchFn(tree)) {
|
|
75
|
+
return [tree];
|
|
76
|
+
}
|
|
77
|
+
let results: ParseTree[] = [];
|
|
78
|
+
if (tree.children) {
|
|
79
|
+
for (const child of tree.children) {
|
|
80
|
+
results = [
|
|
81
|
+
...results,
|
|
82
|
+
...await collectNodesMatchingAsync(child, matchFn),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// return value: returning undefined = not matched, continue, null = delete, new node = replace
|
|
90
|
+
export function replaceNodesMatching(
|
|
91
|
+
tree: ParseTree,
|
|
92
|
+
substituteFn: (tree: ParseTree) => ParseTree | null | undefined,
|
|
93
|
+
) {
|
|
94
|
+
if (tree && tree.children) {
|
|
95
|
+
const children = tree.children.slice();
|
|
96
|
+
for (const child of children) {
|
|
97
|
+
const subst = substituteFn(child);
|
|
98
|
+
if (subst !== undefined) {
|
|
99
|
+
const pos = tree.children.indexOf(child);
|
|
100
|
+
if (subst) {
|
|
101
|
+
tree.children.splice(pos, 1, subst);
|
|
102
|
+
} else {
|
|
103
|
+
// null = delete
|
|
104
|
+
tree.children.splice(pos, 1);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
replaceNodesMatching(child, substituteFn);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function replaceNodesMatchingAsync(
|
|
114
|
+
tree: ParseTree,
|
|
115
|
+
substituteFn: (tree: ParseTree) => Promise<ParseTree | null | undefined>,
|
|
116
|
+
) {
|
|
117
|
+
if (tree.children) {
|
|
118
|
+
const children = tree.children.slice();
|
|
119
|
+
for (const child of children) {
|
|
120
|
+
const subst = await substituteFn(child);
|
|
121
|
+
if (subst !== undefined) {
|
|
122
|
+
const pos = tree.children.indexOf(child);
|
|
123
|
+
if (subst) {
|
|
124
|
+
tree.children.splice(pos, 1, subst);
|
|
125
|
+
} else {
|
|
126
|
+
// null = delete
|
|
127
|
+
tree.children.splice(pos, 1);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
await replaceNodesMatchingAsync(child, substituteFn);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function findNodeMatching(
|
|
137
|
+
tree: ParseTree,
|
|
138
|
+
matchFn: (tree: ParseTree) => boolean,
|
|
139
|
+
): ParseTree | null {
|
|
140
|
+
return collectNodesMatching(tree, matchFn)[0];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function findNodeOfType(
|
|
144
|
+
tree: ParseTree,
|
|
145
|
+
nodeType: string,
|
|
146
|
+
): ParseTree | null {
|
|
147
|
+
return collectNodesMatching(tree, (n) => n.type === nodeType)[0];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function traverseTree(
|
|
151
|
+
tree: ParseTree,
|
|
152
|
+
// Return value = should stop traversal?
|
|
153
|
+
matchFn: (tree: ParseTree) => boolean,
|
|
154
|
+
): void {
|
|
155
|
+
// Do a collect, but ignore the result
|
|
156
|
+
collectNodesMatching(tree, matchFn);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function traverseTreeAsync(
|
|
160
|
+
tree: ParseTree,
|
|
161
|
+
// Return value = should stop traversal?
|
|
162
|
+
matchFn: (tree: ParseTree) => Promise<boolean>,
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
// Do a collect, but ignore the result
|
|
165
|
+
await collectNodesMatchingAsync(tree, matchFn);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function cloneTree(tree: ParseTree): ParseTree {
|
|
169
|
+
return deepClone(tree, ["parent"]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Finds non-text node at position
|
|
173
|
+
export function nodeAtPos(tree: ParseTree, pos: number): ParseTree | null {
|
|
174
|
+
if (pos < tree.from! || pos >= tree.to!) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if (!tree.children) {
|
|
178
|
+
return tree;
|
|
179
|
+
}
|
|
180
|
+
for (const child of tree.children) {
|
|
181
|
+
const n = nodeAtPos(child, pos);
|
|
182
|
+
if (n && n.text !== undefined) {
|
|
183
|
+
// Got a text node, let's return its parent
|
|
184
|
+
return tree;
|
|
185
|
+
} else if (n) {
|
|
186
|
+
// Got it
|
|
187
|
+
return n;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Turn ParseTree back into text
|
|
194
|
+
export function renderToText(tree?: ParseTree): string {
|
|
195
|
+
if (!tree) {
|
|
196
|
+
return "";
|
|
197
|
+
}
|
|
198
|
+
const pieces: string[] = [];
|
|
199
|
+
if (tree.text !== undefined) {
|
|
200
|
+
return tree.text;
|
|
201
|
+
}
|
|
202
|
+
for (const child of tree.children!) {
|
|
203
|
+
pieces.push(renderToText(child));
|
|
204
|
+
}
|
|
205
|
+
return pieces.join("");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function cleanTree(tree: ParseTree, omitTrimmable = true): ParseTree {
|
|
209
|
+
if (tree.type === "⚠") {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Parse error at pos ${tree.from}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (tree.text !== undefined) {
|
|
215
|
+
return tree;
|
|
216
|
+
}
|
|
217
|
+
const ast: ParseTree = {
|
|
218
|
+
type: tree.type,
|
|
219
|
+
children: [],
|
|
220
|
+
from: tree.from,
|
|
221
|
+
to: tree.to,
|
|
222
|
+
};
|
|
223
|
+
for (const node of tree.children!) {
|
|
224
|
+
if (node.type && node.type !== "Comment") {
|
|
225
|
+
ast.children!.push(cleanTree(node, omitTrimmable));
|
|
226
|
+
}
|
|
227
|
+
if (node.text && (omitTrimmable && node.text.trim() || !omitTrimmable)) {
|
|
228
|
+
ast.children!.push(node);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return ast;
|
|
232
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// Define the patch structure
|
|
2
|
+
export interface YamlPatch {
|
|
3
|
+
op: "set-key" | "delete-key";
|
|
4
|
+
path: string; // Still assuming simple, top-level key names
|
|
5
|
+
value?: any; // Required for set-key, not used for delete-key
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Helper function specifically for serializing scalar types
|
|
9
|
+
function serializeToYamlScalar(
|
|
10
|
+
value: string | number | boolean | null,
|
|
11
|
+
): string {
|
|
12
|
+
if (typeof value === "string") {
|
|
13
|
+
// Always quote empty strings and strings with special characters
|
|
14
|
+
if (
|
|
15
|
+
value === "" || // Empty string
|
|
16
|
+
value.match(/[:{#}[],&*!|>'"%@`]/) || // Special YAML characters (added % and ")
|
|
17
|
+
/^\d+(\.\d+)?([eE][+-]?\d+)?$/.test(value) || // Looks like a number
|
|
18
|
+
value.includes(":") || // Contains colons
|
|
19
|
+
["true", "false", "null", "yes", "no", "on", "off"].includes(
|
|
20
|
+
value.toLowerCase(),
|
|
21
|
+
)
|
|
22
|
+
) {
|
|
23
|
+
return JSON.stringify(value);
|
|
24
|
+
}
|
|
25
|
+
// Simple strings without special chars/meaning don't need quotes
|
|
26
|
+
return value;
|
|
27
|
+
} else if (
|
|
28
|
+
typeof value === "number" || typeof value === "boolean" || value === null
|
|
29
|
+
) {
|
|
30
|
+
return String(value);
|
|
31
|
+
}
|
|
32
|
+
// Default for unsupported scalar types (e.g., undefined)
|
|
33
|
+
return "null";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Updated helper function to serialize various JS types to YAML string representations
|
|
37
|
+
// Added baseIndentation parameter for handling nested lists correctly (though we only use it for top-level lists here)
|
|
38
|
+
function serializeToYamlValue(
|
|
39
|
+
value: any,
|
|
40
|
+
baseIndentation: string = "",
|
|
41
|
+
): string {
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
if (value.length === 0) {
|
|
44
|
+
return "[]"; // Use flow style for empty arrays for simplicity
|
|
45
|
+
}
|
|
46
|
+
// Determine indentation for list items (base + 2 spaces)
|
|
47
|
+
const itemIndentation = baseIndentation + " ";
|
|
48
|
+
// Format each item recursively, preceded by '- ' marker
|
|
49
|
+
return "\n" +
|
|
50
|
+
value.map((item) =>
|
|
51
|
+
`${itemIndentation}- ${serializeToYamlValue(item, itemIndentation)}`
|
|
52
|
+
).join("\n");
|
|
53
|
+
// Note: serializeToYamlValue is used recursively here to handle nested arrays/objects if needed in future
|
|
54
|
+
// However, the current `applyMinimalSetKeyPatches` only handles top-level keys.
|
|
55
|
+
} else if (typeof value === "object" && value !== null) {
|
|
56
|
+
// Basic object serialization (not requested, but good to consider)
|
|
57
|
+
// This is highly simplified and doesn't handle nesting well without more context
|
|
58
|
+
const itemIndentation = baseIndentation + " ";
|
|
59
|
+
const entries = Object.entries(value);
|
|
60
|
+
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");
|
|
67
|
+
} else {
|
|
68
|
+
// Handle scalars using the dedicated function
|
|
69
|
+
return serializeToYamlScalar(value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function applyPatches(
|
|
74
|
+
yamlString: string,
|
|
75
|
+
patches: YamlPatch[],
|
|
76
|
+
): string {
|
|
77
|
+
let currentYaml = yamlString;
|
|
78
|
+
|
|
79
|
+
for (const patch of patches) {
|
|
80
|
+
if (patch.op === "delete-key") {
|
|
81
|
+
// Handle delete operation
|
|
82
|
+
const key = patch.path;
|
|
83
|
+
const lines = currentYaml.split("\n");
|
|
84
|
+
let keyLineIndex = -1;
|
|
85
|
+
let startDeleteIndex = -1;
|
|
86
|
+
let endDeleteIndex = -1;
|
|
87
|
+
|
|
88
|
+
// Find the key line
|
|
89
|
+
for (let i = 0; i < lines.length; i++) {
|
|
90
|
+
const line = lines[i];
|
|
91
|
+
const trimmedLine = line.trim();
|
|
92
|
+
if (trimmedLine.startsWith(key + ":")) {
|
|
93
|
+
keyLineIndex = i;
|
|
94
|
+
startDeleteIndex = i;
|
|
95
|
+
endDeleteIndex = i;
|
|
96
|
+
|
|
97
|
+
// Look backwards for preceding comments (delete them too)
|
|
98
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
99
|
+
const prevLine = lines[j].trim();
|
|
100
|
+
if (prevLine.startsWith("#")) {
|
|
101
|
+
startDeleteIndex = j;
|
|
102
|
+
} else if (prevLine !== "") {
|
|
103
|
+
break;
|
|
104
|
+
} else {
|
|
105
|
+
// Empty line - include it in deletion if followed by comments
|
|
106
|
+
if (startDeleteIndex < i) {
|
|
107
|
+
startDeleteIndex = j;
|
|
108
|
+
} else {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Determine the indentation of the key
|
|
115
|
+
const keyIndent = lines[i].match(/^(\s*)/)?.[1] || "";
|
|
116
|
+
|
|
117
|
+
// Look forwards to find all content belonging to this key
|
|
118
|
+
// This includes list items and nested objects
|
|
119
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
120
|
+
const nextLine = lines[j];
|
|
121
|
+
const trimmedNextLine = nextLine.trim();
|
|
122
|
+
|
|
123
|
+
// Empty line - continue looking
|
|
124
|
+
if (trimmedNextLine === "") {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Comment at same or greater indentation - might be trailing comment
|
|
129
|
+
if (trimmedNextLine.startsWith("#")) {
|
|
130
|
+
const commentIndent = nextLine.match(/^(\s*)/)?.[1] || "";
|
|
131
|
+
if (commentIndent.length > keyIndent.length) {
|
|
132
|
+
endDeleteIndex = j;
|
|
133
|
+
continue;
|
|
134
|
+
} else {
|
|
135
|
+
// Comment at same or less indentation - not part of this key
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check indentation of next non-empty, non-comment line
|
|
141
|
+
const nextIndent = nextLine.match(/^(\s*)/)?.[1] || "";
|
|
142
|
+
if (nextIndent.length > keyIndent.length) {
|
|
143
|
+
// This line is indented more than the key, so it belongs to the key
|
|
144
|
+
endDeleteIndex = j;
|
|
145
|
+
} else {
|
|
146
|
+
// This line is at same or less indentation, so it's a new key
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (keyLineIndex !== -1) {
|
|
156
|
+
// Delete the lines from startDeleteIndex to endDeleteIndex (inclusive)
|
|
157
|
+
const beforeDelete = lines.slice(0, startDeleteIndex);
|
|
158
|
+
const afterDelete = lines.slice(endDeleteIndex + 1);
|
|
159
|
+
|
|
160
|
+
// Clean up excessive empty lines at the boundary
|
|
161
|
+
// Remove trailing empty lines from beforeDelete
|
|
162
|
+
while (
|
|
163
|
+
beforeDelete.length > 0 &&
|
|
164
|
+
beforeDelete[beforeDelete.length - 1].trim() === ""
|
|
165
|
+
) {
|
|
166
|
+
beforeDelete.pop();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Remove leading empty lines from afterDelete (but keep one if there's content after)
|
|
170
|
+
let leadingEmptyCount = 0;
|
|
171
|
+
for (const line of afterDelete) {
|
|
172
|
+
if (line.trim() === "") {
|
|
173
|
+
leadingEmptyCount++;
|
|
174
|
+
} else {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const cleanedAfterDelete = afterDelete.slice(
|
|
179
|
+
Math.min(leadingEmptyCount, 1),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
currentYaml = [...beforeDelete, ...cleanedAfterDelete].join("\n");
|
|
183
|
+
if (currentYaml && !currentYaml.endsWith("\n")) {
|
|
184
|
+
currentYaml += "\n";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// If key not found, do nothing
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (patch.op !== "set-key") continue;
|
|
192
|
+
|
|
193
|
+
const key = patch.path;
|
|
194
|
+
|
|
195
|
+
// Split the YAML into lines for easier processing
|
|
196
|
+
const lines = currentYaml.split("\n");
|
|
197
|
+
let keyLineIndex = -1;
|
|
198
|
+
let commentBlock = "";
|
|
199
|
+
let trailingComments = "";
|
|
200
|
+
let inlineComment = "";
|
|
201
|
+
|
|
202
|
+
// Find the key line and collect comments
|
|
203
|
+
for (let i = 0; i < lines.length; i++) {
|
|
204
|
+
const line = lines[i];
|
|
205
|
+
const trimmedLine = line.trim();
|
|
206
|
+
if (trimmedLine.startsWith(key + ":")) {
|
|
207
|
+
keyLineIndex = i;
|
|
208
|
+
// Extract inline comment if present
|
|
209
|
+
const commentMatch = line.match(/#.*$/);
|
|
210
|
+
if (commentMatch) {
|
|
211
|
+
inlineComment = commentMatch[0];
|
|
212
|
+
}
|
|
213
|
+
// Look backwards for comments
|
|
214
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
215
|
+
const prevLine = lines[j].trim();
|
|
216
|
+
if (prevLine.startsWith("#")) {
|
|
217
|
+
commentBlock = lines[j] + "\n" + commentBlock;
|
|
218
|
+
} else if (prevLine !== "") {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Look forwards for comments
|
|
223
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
224
|
+
const nextLine = lines[j].trim();
|
|
225
|
+
if (nextLine.startsWith("#")) {
|
|
226
|
+
trailingComments += lines[j] + "\n";
|
|
227
|
+
} else if (nextLine !== "") {
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Serialize the new value
|
|
236
|
+
const serializedNewValue = serializeToYamlValue(patch.value);
|
|
237
|
+
|
|
238
|
+
// Create the replacement line
|
|
239
|
+
let replacementLine: string;
|
|
240
|
+
if (serializedNewValue.startsWith("\n")) {
|
|
241
|
+
// For lists/objects, the key line ends with just ':'
|
|
242
|
+
replacementLine = `${key}:${inlineComment}${serializedNewValue}`;
|
|
243
|
+
} else {
|
|
244
|
+
// For scalars, format as key: value
|
|
245
|
+
replacementLine = `${key}: ${serializedNewValue}${
|
|
246
|
+
inlineComment ? " " + inlineComment : ""
|
|
247
|
+
}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (keyLineIndex !== -1) {
|
|
251
|
+
// Replace the existing line while preserving comments
|
|
252
|
+
const beforeKey = lines.slice(
|
|
253
|
+
0,
|
|
254
|
+
keyLineIndex - commentBlock.split("\n").filter(Boolean).length,
|
|
255
|
+
);
|
|
256
|
+
const afterKey = lines.slice(
|
|
257
|
+
keyLineIndex + 1 + trailingComments.split("\n").filter(Boolean).length,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Build the new content
|
|
261
|
+
const newContent = [
|
|
262
|
+
...beforeKey,
|
|
263
|
+
...commentBlock.split("\n").filter(Boolean),
|
|
264
|
+
replacementLine,
|
|
265
|
+
...trailingComments.split("\n").filter(Boolean),
|
|
266
|
+
...afterKey,
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
// Join lines and ensure proper newlines
|
|
270
|
+
currentYaml = newContent.join("\n").replace(/\n*$/, "\n") + "\n";
|
|
271
|
+
} else {
|
|
272
|
+
// Key not found: Add the new key-value pair to the end
|
|
273
|
+
const newLineBlock = replacementLine;
|
|
274
|
+
if (currentYaml.trim() === "") {
|
|
275
|
+
currentYaml = newLineBlock + "\n";
|
|
276
|
+
} else {
|
|
277
|
+
currentYaml = currentYaml.replace(/\n*$/, "\n") + newLineBlock + "\n";
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Ensure the result ends with a newline
|
|
283
|
+
return currentYaml.replace(/\n*$/, "\n");
|
|
284
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// declare global {
|
|
2
|
+
// function syscall(name: string, ...args: any[]): Promise<any>;
|
|
3
|
+
// }
|
|
4
|
+
|
|
5
|
+
// This is the case when running tests only, so giving it a dummy syscall function
|
|
6
|
+
if (typeof (globalThis as any).syscall === "undefined") {
|
|
7
|
+
(globalThis as any).syscall = () => {
|
|
8
|
+
throw new Error("Not implemented here");
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Late binding syscall
|
|
13
|
+
export function syscall(name: string, ...args: any[]): Promise<any> {
|
|
14
|
+
return (globalThis as any).syscall(name, ...args);
|
|
15
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { base64DecodeDataUrl } from "../lib/crypto.ts";
|
|
2
|
+
import { syscall } from "../syscall.ts";
|
|
3
|
+
|
|
4
|
+
import type { FileMeta } from "../../plug-api/types/index.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reads an asset embedded in a plug (via the `assets` field in the plug manifest).
|
|
8
|
+
* @param plugName name of the plug to read asset from
|
|
9
|
+
* @param name name of the asset to read
|
|
10
|
+
* @param encoding either "utf8" or "dataurl"
|
|
11
|
+
* @returns the content of the asset in the requested encoding
|
|
12
|
+
*/
|
|
13
|
+
export async function readAsset(
|
|
14
|
+
plugName: string,
|
|
15
|
+
name: string,
|
|
16
|
+
encoding: "utf8" | "dataurl" = "utf8",
|
|
17
|
+
): Promise<string> {
|
|
18
|
+
const dataUrl = await syscall("asset.readAsset", plugName, name) as string;
|
|
19
|
+
switch (encoding) {
|
|
20
|
+
case "utf8":
|
|
21
|
+
return new TextDecoder().decode(base64DecodeDataUrl(dataUrl));
|
|
22
|
+
case "dataurl":
|
|
23
|
+
return dataUrl;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function listFiles(plugName: string): Promise<FileMeta[]> {
|
|
28
|
+
return await syscall("asset.listFiles", plugName);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getFileMeta(
|
|
32
|
+
plugName: string,
|
|
33
|
+
name: string,
|
|
34
|
+
): Promise<FileMeta> {
|
|
35
|
+
return await syscall("asset.getFileMeta", plugName, name);
|
|
36
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { syscall } from "../syscall.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Implements a very simple (string) key value store for the client.
|
|
5
|
+
* Generally should only be used to set some client-specific states, such as preferences.
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sets a value in the client store.
|
|
11
|
+
* @param key the key to set
|
|
12
|
+
* @param value the value to set
|
|
13
|
+
*/
|
|
14
|
+
export function set(key: string, value: any): Promise<void> {
|
|
15
|
+
return syscall("clientStore.set", key, value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Gets a value from the client store.
|
|
20
|
+
* @param key the key to get
|
|
21
|
+
* @returns the value associated with the key
|
|
22
|
+
*/
|
|
23
|
+
export function get(key: string): Promise<any> {
|
|
24
|
+
return syscall("clientStore.get", key);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Deletes a value from the client store.
|
|
29
|
+
* @param key the key to delete
|
|
30
|
+
*/
|
|
31
|
+
export function del(key: string): Promise<void> {
|
|
32
|
+
return syscall("clientStore.delete", key);
|
|
33
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { syscall } from "../syscall.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gets a config value by path, with support for dot notation.
|
|
5
|
+
* @param path The path to get the value from
|
|
6
|
+
* @param defaultValue The default value to return if the path doesn't exist
|
|
7
|
+
* @returns The value at the path, or the default value
|
|
8
|
+
*/
|
|
9
|
+
export function get<T>(path: string | string[], defaultValue: T): Promise<T> {
|
|
10
|
+
return syscall("config.get", path, defaultValue);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Sets a config value by path, with support for dot notation.
|
|
15
|
+
* @param path The path to set the value at
|
|
16
|
+
* @param value The value to set
|
|
17
|
+
*/
|
|
18
|
+
export function set<T>(path: string, value: T): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Sets multiple config values at once.
|
|
21
|
+
* @param values An object containing key-value pairs to set
|
|
22
|
+
*/
|
|
23
|
+
export function set(values: Record<string, any>): Promise<void>;
|
|
24
|
+
export function set<T>(
|
|
25
|
+
pathOrValues: string | Record<string, any>,
|
|
26
|
+
value?: T,
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
return syscall("config.set", pathOrValues, value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Inserts a config value into an array
|
|
33
|
+
*/
|
|
34
|
+
export function insert<T>(
|
|
35
|
+
path: string | string[],
|
|
36
|
+
value: T,
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
return syscall("config.insert", path, value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Checks if a config path exists.
|
|
43
|
+
* @param path The path to check
|
|
44
|
+
* @returns True if the path exists, false otherwise
|
|
45
|
+
*/
|
|
46
|
+
export function has(path: string): Promise<boolean> {
|
|
47
|
+
return syscall("config.has", path);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Defines a JSON schema for a configuration key.
|
|
52
|
+
* The schema will be used to validate values when setting this key.
|
|
53
|
+
* @param key The configuration key to define a schema for
|
|
54
|
+
* @param schema The JSON schema to validate against
|
|
55
|
+
*/
|
|
56
|
+
export function define(key: string, schema: any): Promise<void> {
|
|
57
|
+
return syscall("config.define", key, schema);
|
|
58
|
+
}
|