@silverbulletmd/silverbullet 2.8.1 → 2.10.0
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 +42 -25
- package/client/plugos/hooks/syscall.ts +24 -10
- package/client/plugos/syscalls/asset.ts +61 -23
- package/client/plugos/syscalls/clientStore.ts +32 -6
- package/client/plugos/syscalls/client_code_widget.ts +9 -4
- package/client/plugos/syscalls/code_widget.ts +26 -11
- package/client/plugos/syscalls/config.ts +146 -30
- package/client/plugos/syscalls/datastore.ts +146 -40
- package/client/plugos/syscalls/editor.ts +1432 -700
- package/client/plugos/syscalls/event.ts +46 -13
- package/client/plugos/syscalls/fetch.ts +123 -82
- package/client/plugos/syscalls/index.ts +243 -114
- package/client/plugos/syscalls/jsonschema.ts +107 -8
- package/client/plugos/syscalls/language.ts +38 -12
- package/client/plugos/syscalls/markdown.ts +166 -50
- package/client/plugos/syscalls/mq.ts +93 -20
- package/client/plugos/syscalls/schema_introspection.ts +33 -0
- package/client/plugos/syscalls/service_registry.ts +74 -20
- package/client/plugos/syscalls/shell.ts +51 -20
- package/client/plugos/syscalls/space.ts +169 -113
- package/client/plugos/syscalls/sync.ts +50 -19
- package/client/plugos/syscalls/system.ts +236 -139
- package/client/plugos/system.ts +21 -5
- package/client/space_lua/api_documentation.ts +107 -0
- package/client/space_lua/ast.ts +13 -0
- package/client/space_lua/eval.ts +12 -4
- package/client/space_lua/parse.ts +254 -119
- package/client/space_lua/pretty_print.ts +465 -0
- package/client/space_lua/query_collection.ts +3 -3
- package/client/space_lua/render_widget.ts +156 -0
- package/client/space_lua/runtime.ts +83 -22
- package/client/space_lua/stdlib/crypto.ts +8 -3
- package/client/space_lua/stdlib/encoding.ts +27 -9
- package/client/space_lua/stdlib/js.ts +137 -22
- package/client/space_lua/stdlib/load.ts +26 -17
- package/client/space_lua/stdlib/math.ts +323 -118
- package/client/space_lua/stdlib/net.ts +57 -9
- package/client/space_lua/stdlib/os.ts +111 -44
- package/client/space_lua/stdlib/space_lua.ts +397 -13
- package/client/space_lua/stdlib/string.ts +282 -104
- package/client/space_lua/stdlib/string_pack.ts +58 -19
- package/client/space_lua/stdlib/table.ts +195 -40
- package/client/space_lua/stdlib.ts +336 -94
- package/client/space_lua/syscalls.ts +270 -0
- package/dist/plug-compile.js +1 -1
- package/package.json +12 -4
- package/plug-api/lib/ref.ts +96 -0
- package/plug-api/lib/shortcut.ts +2 -1
- package/plug-api/syscalls/config.ts +5 -3
- package/plug-api/syscalls/editor.ts +23 -2
- package/plug-api/syscalls/index.ts +24 -1
- package/plug-api/syscalls/jsonschema.ts +9 -0
- package/plug-api/syscalls/lua.ts +28 -1
- package/plug-api/syscalls/shell.ts +0 -1
- package/plug-api/syscalls/system.ts +7 -0
- package/plug-api/system_mock.ts +2 -2
- package/plug-api/types/config.ts +1 -1
- package/plug-api/types/index.ts +52 -1
- package/plug-api/types/manifest.ts +22 -4
- package/plug-api/ui/cx.ts +4 -0
- package/plug-api/ui/index.ts +22 -0
- package/plug-api/ui/panel_styles.ts +33 -0
- package/plug-api/ui/slugify.ts +44 -0
- package/plugs/builtin_plugs.ts +1 -0
- package/client/plugos/syscalls/lua.ts +0 -58
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LuaBlock,
|
|
3
|
+
LuaExpression,
|
|
4
|
+
LuaOrderBy,
|
|
5
|
+
LuaQueryClause,
|
|
6
|
+
LuaStatement,
|
|
7
|
+
LuaTableField,
|
|
8
|
+
} from "./ast.ts";
|
|
9
|
+
import { luaFormatNumber } from "./runtime.ts";
|
|
10
|
+
|
|
11
|
+
export type PrintOptions = {
|
|
12
|
+
indentWidth?: number;
|
|
13
|
+
quote?: "double" | "single";
|
|
14
|
+
trailingComma?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type ResolvedOptions = {
|
|
18
|
+
indentWidth: number;
|
|
19
|
+
quote: "double" | "single";
|
|
20
|
+
trailingComma: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function resolveOptions(opts?: PrintOptions): ResolvedOptions {
|
|
24
|
+
return {
|
|
25
|
+
indentWidth: opts?.indentWidth ?? 2,
|
|
26
|
+
quote: opts?.quote ?? "double",
|
|
27
|
+
trailingComma: opts?.trailingComma ?? true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const RESERVED = new Set([
|
|
32
|
+
"and",
|
|
33
|
+
"break",
|
|
34
|
+
"do",
|
|
35
|
+
"else",
|
|
36
|
+
"elseif",
|
|
37
|
+
"end",
|
|
38
|
+
"false",
|
|
39
|
+
"for",
|
|
40
|
+
"function",
|
|
41
|
+
"goto",
|
|
42
|
+
"if",
|
|
43
|
+
"in",
|
|
44
|
+
"local",
|
|
45
|
+
"nil",
|
|
46
|
+
"not",
|
|
47
|
+
"or",
|
|
48
|
+
"repeat",
|
|
49
|
+
"return",
|
|
50
|
+
"then",
|
|
51
|
+
"true",
|
|
52
|
+
"until",
|
|
53
|
+
"while",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
function isLuaIdentifier(s: string): boolean {
|
|
57
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(s) && !RESERVED.has(s);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const BINARY_PREC: Record<string, number> = {
|
|
61
|
+
or: 1,
|
|
62
|
+
and: 2,
|
|
63
|
+
"<": 3,
|
|
64
|
+
">": 3,
|
|
65
|
+
"<=": 3,
|
|
66
|
+
">=": 3,
|
|
67
|
+
"~=": 3,
|
|
68
|
+
"==": 3,
|
|
69
|
+
"|": 4,
|
|
70
|
+
"~": 5, // binary bitwise xor
|
|
71
|
+
"&": 6,
|
|
72
|
+
"<<": 7,
|
|
73
|
+
">>": 7,
|
|
74
|
+
"..": 8,
|
|
75
|
+
"+": 9,
|
|
76
|
+
"-": 9,
|
|
77
|
+
"*": 10,
|
|
78
|
+
"/": 10,
|
|
79
|
+
"//": 10,
|
|
80
|
+
"%": 10,
|
|
81
|
+
"^": 12,
|
|
82
|
+
};
|
|
83
|
+
const UNARY_PREC = 11;
|
|
84
|
+
const RIGHT_ASSOC = new Set(["..", "^"]);
|
|
85
|
+
const ATOM_PREC = 99;
|
|
86
|
+
|
|
87
|
+
function exprPrec(e: LuaExpression): number {
|
|
88
|
+
if (e.type === "Binary") return BINARY_PREC[e.operator] ?? ATOM_PREC;
|
|
89
|
+
if (e.type === "Unary") return UNARY_PREC;
|
|
90
|
+
return ATOM_PREC;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class Printer {
|
|
94
|
+
constructor(private opts: ResolvedOptions) {}
|
|
95
|
+
|
|
96
|
+
private indent(depth: number): string {
|
|
97
|
+
return " ".repeat(depth * this.opts.indentWidth);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private quoteString(s: string): string {
|
|
101
|
+
const q = this.opts.quote === "double" ? '"' : "'";
|
|
102
|
+
let out = q;
|
|
103
|
+
for (const ch of s) {
|
|
104
|
+
switch (ch) {
|
|
105
|
+
case "\\":
|
|
106
|
+
out += "\\\\";
|
|
107
|
+
break;
|
|
108
|
+
case "\n":
|
|
109
|
+
out += "\\n";
|
|
110
|
+
break;
|
|
111
|
+
case "\r":
|
|
112
|
+
out += "\\r";
|
|
113
|
+
break;
|
|
114
|
+
case "\t":
|
|
115
|
+
out += "\\t";
|
|
116
|
+
break;
|
|
117
|
+
case "\0":
|
|
118
|
+
out += "\\0";
|
|
119
|
+
break;
|
|
120
|
+
default:
|
|
121
|
+
out += ch === q ? `\\${ch}` : ch;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out + q;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
expression(e: LuaExpression, depth = 0): string {
|
|
128
|
+
switch (e.type) {
|
|
129
|
+
case "Nil":
|
|
130
|
+
return "nil";
|
|
131
|
+
case "Boolean":
|
|
132
|
+
return e.value ? "true" : "false";
|
|
133
|
+
case "Number":
|
|
134
|
+
return luaFormatNumber(e.value, e.numericType);
|
|
135
|
+
case "String":
|
|
136
|
+
return this.quoteString(e.value);
|
|
137
|
+
case "Variable":
|
|
138
|
+
return e.name;
|
|
139
|
+
case "PropertyAccess":
|
|
140
|
+
return `${this.expression(e.object, depth)}.${e.property}`;
|
|
141
|
+
case "TableAccess":
|
|
142
|
+
return `${this.expression(e.object, depth)}[${this.expression(e.key, depth)}]`;
|
|
143
|
+
case "Parenthesized":
|
|
144
|
+
return `(${this.expression(e.expression, depth)})`;
|
|
145
|
+
case "Binary": {
|
|
146
|
+
const p = BINARY_PREC[e.operator] ?? ATOM_PREC;
|
|
147
|
+
const rightAssoc = RIGHT_ASSOC.has(e.operator);
|
|
148
|
+
const left = this.operand(e.left, p, false, rightAssoc, depth);
|
|
149
|
+
const right = this.operand(e.right, p, true, rightAssoc, depth);
|
|
150
|
+
return `${left} ${e.operator} ${right}`;
|
|
151
|
+
}
|
|
152
|
+
case "Unary": {
|
|
153
|
+
const arg = this.unaryOperand(e.argument, depth);
|
|
154
|
+
return /^[a-z]+$/.test(e.operator)
|
|
155
|
+
? `${e.operator} ${arg}`
|
|
156
|
+
: `${e.operator}${arg}`;
|
|
157
|
+
}
|
|
158
|
+
case "TableConstructor":
|
|
159
|
+
return this.tableConstructor(e.fields, depth);
|
|
160
|
+
case "FunctionDefinition":
|
|
161
|
+
return `function${this.functionRest(
|
|
162
|
+
e.body.parameters,
|
|
163
|
+
e.body.block,
|
|
164
|
+
depth,
|
|
165
|
+
)}`;
|
|
166
|
+
case "FunctionCall":
|
|
167
|
+
return this.functionCall(e, depth);
|
|
168
|
+
case "Query": {
|
|
169
|
+
const lines = e.clauses.map(
|
|
170
|
+
(c) => this.indent(depth + 1) + this.queryClause(c, depth + 1),
|
|
171
|
+
);
|
|
172
|
+
return `query[[\n${lines.join("\n")}\n${this.indent(depth)}]]`;
|
|
173
|
+
}
|
|
174
|
+
case "FilteredCall":
|
|
175
|
+
return `${this.expression(e.call, depth)} filter ${this.expression(
|
|
176
|
+
e.filter,
|
|
177
|
+
depth,
|
|
178
|
+
)}`;
|
|
179
|
+
case "AggregateCall":
|
|
180
|
+
return `${this.expression(e.call, depth)} order by ${this.orderByList(
|
|
181
|
+
e.orderBy,
|
|
182
|
+
depth,
|
|
183
|
+
)}`;
|
|
184
|
+
default:
|
|
185
|
+
throw new Error(
|
|
186
|
+
`pretty_print: unsupported expression type: ${(e as any).type}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private operand(
|
|
192
|
+
e: LuaExpression,
|
|
193
|
+
parentPrec: number,
|
|
194
|
+
isRight: boolean,
|
|
195
|
+
parentRightAssoc: boolean,
|
|
196
|
+
depth: number,
|
|
197
|
+
): string {
|
|
198
|
+
const cp = exprPrec(e);
|
|
199
|
+
let parens = false;
|
|
200
|
+
if (cp < parentPrec) {
|
|
201
|
+
parens = true;
|
|
202
|
+
} else if (cp === parentPrec) {
|
|
203
|
+
// left-assoc parent: the right child of equal precedence needs parens.
|
|
204
|
+
// right-assoc parent: the left child of equal precedence needs parens.
|
|
205
|
+
parens = parentRightAssoc ? !isRight : isRight;
|
|
206
|
+
}
|
|
207
|
+
const s = this.expression(e, depth);
|
|
208
|
+
return parens ? `(${s})` : s;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private unaryOperand(e: LuaExpression, depth: number): string {
|
|
212
|
+
const s = this.expression(e, depth);
|
|
213
|
+
return exprPrec(e) < UNARY_PREC ? `(${s})` : s;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private field(f: LuaTableField, depth: number): string {
|
|
217
|
+
switch (f.type) {
|
|
218
|
+
case "PropField":
|
|
219
|
+
return isLuaIdentifier(f.key)
|
|
220
|
+
? `${f.key} = ${this.expression(f.value, depth)}`
|
|
221
|
+
: `[${this.quoteString(f.key)}] = ${this.expression(f.value, depth)}`;
|
|
222
|
+
case "DynamicField":
|
|
223
|
+
return `[${this.expression(f.key, depth)}] = ${this.expression(f.value, depth)}`;
|
|
224
|
+
case "ExpressionField":
|
|
225
|
+
return this.expression(f.value, depth);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private tableConstructor(fields: LuaTableField[], depth: number): string {
|
|
230
|
+
if (fields.length === 0) return "{}";
|
|
231
|
+
if (fields.length === 1) return `{${this.field(fields[0], depth)}}`;
|
|
232
|
+
const lines = fields.map(
|
|
233
|
+
(f) => this.indent(depth + 1) + this.field(f, depth + 1),
|
|
234
|
+
);
|
|
235
|
+
const tail = this.opts.trailingComma ? "," : "";
|
|
236
|
+
return `{\n${lines.join(",\n")}${tail}\n${this.indent(depth)}}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// returns the part of a function after the `function` keyword (+ optional
|
|
240
|
+
// name): "(params) end" or "(params)\n <body>\nend"
|
|
241
|
+
private functionRest(
|
|
242
|
+
params: string[],
|
|
243
|
+
body: LuaBlock,
|
|
244
|
+
depth: number,
|
|
245
|
+
): string {
|
|
246
|
+
const head = `(${params.join(", ")})`;
|
|
247
|
+
const rendered = this.block(body, depth + 1);
|
|
248
|
+
if (!rendered) return `${head} end`;
|
|
249
|
+
return `${head}\n${rendered}\n${this.indent(depth)}end`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private functionCall(
|
|
253
|
+
e: Extract<LuaExpression, { type: "FunctionCall" }>,
|
|
254
|
+
depth: number,
|
|
255
|
+
): string {
|
|
256
|
+
let base = this.expression(e.prefix, depth);
|
|
257
|
+
if (e.name) base += `:${e.name}`;
|
|
258
|
+
const args = e.args;
|
|
259
|
+
const orderBy = e.orderBy
|
|
260
|
+
? ` order by ${this.orderByList(e.orderBy, depth)}`
|
|
261
|
+
: "";
|
|
262
|
+
if (
|
|
263
|
+
!e.orderBy &&
|
|
264
|
+
args.length === 1 &&
|
|
265
|
+
(args[0].type === "TableConstructor" || args[0].type === "String")
|
|
266
|
+
) {
|
|
267
|
+
return `${base} ${this.expression(args[0], depth)}`;
|
|
268
|
+
}
|
|
269
|
+
return `${base}(${args.map((a) => this.expression(a, depth)).join(", ")})${orderBy}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private fieldList(fields: LuaTableField[], depth: number): string {
|
|
273
|
+
return fields.map((f) => this.field(f, depth)).join(", ");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private orderByList(orders: LuaOrderBy[], depth: number): string {
|
|
277
|
+
return orders
|
|
278
|
+
.map((o) => {
|
|
279
|
+
let s = this.expression(o.expression, depth);
|
|
280
|
+
if (o.direction === "desc") s += " desc";
|
|
281
|
+
if (o.nulls) s += ` nulls ${o.nulls}`;
|
|
282
|
+
return s;
|
|
283
|
+
})
|
|
284
|
+
.join(", ");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private queryClause(c: LuaQueryClause, depth: number): string {
|
|
288
|
+
switch (c.type) {
|
|
289
|
+
case "From":
|
|
290
|
+
return `from ${this.fieldList(c.fields, depth)}`;
|
|
291
|
+
case "Where":
|
|
292
|
+
return `where ${this.expression(c.expression, depth)}`;
|
|
293
|
+
case "Limit":
|
|
294
|
+
return (
|
|
295
|
+
`limit ${this.expression(c.limit, depth)}` +
|
|
296
|
+
(c.offset ? ` offset ${this.expression(c.offset, depth)}` : "")
|
|
297
|
+
);
|
|
298
|
+
case "Offset":
|
|
299
|
+
return `offset ${this.expression(c.offset, depth)}`;
|
|
300
|
+
case "OrderBy":
|
|
301
|
+
return `order by ${this.orderByList(c.orderBy, depth)}`;
|
|
302
|
+
case "Select":
|
|
303
|
+
return `select ${this.fieldList(c.fields, depth)}`;
|
|
304
|
+
case "GroupBy":
|
|
305
|
+
return `group by ${this.fieldList(c.fields, depth)}`;
|
|
306
|
+
case "Having":
|
|
307
|
+
return `having ${this.expression(c.expression, depth)}`;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
block(b: LuaBlock, depth: number): string {
|
|
312
|
+
const stmts = b.statements.filter((s) => s.type !== "Semicolon");
|
|
313
|
+
const items: (LuaStatement | NonNullable<LuaBlock["comments"]>[number])[] =
|
|
314
|
+
[...stmts, ...(b.comments ?? [])].sort(
|
|
315
|
+
(a, z) => (a.ctx.from ?? 0) - (z.ctx.from ?? 0),
|
|
316
|
+
);
|
|
317
|
+
const isDef = (s: (typeof items)[number]) =>
|
|
318
|
+
s.type === "Function" || s.type === "LocalFunction";
|
|
319
|
+
let out = "";
|
|
320
|
+
for (let i = 0; i < items.length; i++) {
|
|
321
|
+
if (i > 0) {
|
|
322
|
+
out += "\n";
|
|
323
|
+
// blank line separating function definitions from neighbours
|
|
324
|
+
const previous = items[i - 1];
|
|
325
|
+
const isDocumentationPair =
|
|
326
|
+
previous.type === "Comment" &&
|
|
327
|
+
previous.text.startsWith("---") &&
|
|
328
|
+
isDef(items[i]);
|
|
329
|
+
if (!isDocumentationPair && (isDef(previous) || isDef(items[i]))) {
|
|
330
|
+
out += "\n";
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const item = items[i];
|
|
334
|
+
out +=
|
|
335
|
+
item.type === "Comment"
|
|
336
|
+
? this.indent(depth) +
|
|
337
|
+
item.text.replaceAll("\n", `\n${this.indent(depth)}`)
|
|
338
|
+
: this.statement(item, depth);
|
|
339
|
+
}
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private statement(s: LuaStatement, depth: number): string {
|
|
344
|
+
const ind = this.indent(depth);
|
|
345
|
+
switch (s.type) {
|
|
346
|
+
case "Local": {
|
|
347
|
+
const names = s.names
|
|
348
|
+
.map(
|
|
349
|
+
(n) =>
|
|
350
|
+
n.name +
|
|
351
|
+
(n.attribute
|
|
352
|
+
? ` <${n.attribute}>`
|
|
353
|
+
: n.attributes?.length
|
|
354
|
+
? ` <${n.attributes[0]}>`
|
|
355
|
+
: ""),
|
|
356
|
+
)
|
|
357
|
+
.join(", ");
|
|
358
|
+
const exprs = s.expressions?.length
|
|
359
|
+
? ` = ${s.expressions.map((e) => this.expression(e, depth)).join(", ")}`
|
|
360
|
+
: "";
|
|
361
|
+
return `${ind}local ${names}${exprs}`;
|
|
362
|
+
}
|
|
363
|
+
case "Assignment":
|
|
364
|
+
return `${ind}${s.variables
|
|
365
|
+
.map((v) => this.expression(v, depth))
|
|
366
|
+
.join(
|
|
367
|
+
", ",
|
|
368
|
+
)} = ${s.expressions.map((e) => this.expression(e, depth)).join(", ")}`;
|
|
369
|
+
case "Return":
|
|
370
|
+
return s.expressions.length
|
|
371
|
+
? `${ind}return ${s.expressions
|
|
372
|
+
.map((e) => this.expression(e, depth))
|
|
373
|
+
.join(", ")}`
|
|
374
|
+
: `${ind}return`;
|
|
375
|
+
case "FunctionCallStatement":
|
|
376
|
+
return ind + this.expression(s.call, depth);
|
|
377
|
+
case "Break":
|
|
378
|
+
return `${ind}break`;
|
|
379
|
+
case "Goto":
|
|
380
|
+
return `${ind}goto ${s.name}`;
|
|
381
|
+
case "Label":
|
|
382
|
+
return `${ind}::${s.name}::`;
|
|
383
|
+
case "Block": {
|
|
384
|
+
const body = this.block(s, depth + 1);
|
|
385
|
+
return body ? `${ind}do\n${body}\n${ind}end` : `${ind}do end`;
|
|
386
|
+
}
|
|
387
|
+
case "If": {
|
|
388
|
+
let out = "";
|
|
389
|
+
s.conditions.forEach((c, i) => {
|
|
390
|
+
const kw = i === 0 ? "if" : "elseif";
|
|
391
|
+
const head = i === 0 ? `${ind}${kw} ` : `\n${ind}${kw} `;
|
|
392
|
+
const body = this.block(c.block, depth + 1);
|
|
393
|
+
out +=
|
|
394
|
+
`${head}${this.expression(c.condition, depth)} then` +
|
|
395
|
+
(body ? `\n${body}` : "");
|
|
396
|
+
});
|
|
397
|
+
if (s.elseBlock) {
|
|
398
|
+
const body = this.block(s.elseBlock, depth + 1);
|
|
399
|
+
out += `\n${ind}else${body ? `\n${body}` : ""}`;
|
|
400
|
+
}
|
|
401
|
+
out += `\n${ind}end`;
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
case "While": {
|
|
405
|
+
const body = this.block(s.block, depth + 1);
|
|
406
|
+
return (
|
|
407
|
+
`${ind}while ${this.expression(s.condition, depth)} do` +
|
|
408
|
+
(body ? `\n${body}` : "") +
|
|
409
|
+
`\n${ind}end`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
case "Repeat": {
|
|
413
|
+
const body = this.block(s.block, depth + 1);
|
|
414
|
+
return (
|
|
415
|
+
`${ind}repeat` +
|
|
416
|
+
(body ? `\n${body}` : "") +
|
|
417
|
+
`\n${ind}until ${this.expression(s.condition, depth)}`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
case "For": {
|
|
421
|
+
const step = s.step ? `, ${this.expression(s.step, depth)}` : "";
|
|
422
|
+
const body = this.block(s.block, depth + 1);
|
|
423
|
+
return `${ind}for ${s.name} = ${this.expression(s.start, depth)}, ${this.expression(
|
|
424
|
+
s.end,
|
|
425
|
+
depth,
|
|
426
|
+
)}${step} do${body ? `\n${body}` : ""}\n${ind}end`;
|
|
427
|
+
}
|
|
428
|
+
case "ForIn": {
|
|
429
|
+
const body = this.block(s.block, depth + 1);
|
|
430
|
+
return `${ind}for ${s.names.join(", ")} in ${s.expressions
|
|
431
|
+
.map((e) => this.expression(e, depth))
|
|
432
|
+
.join(", ")} do${body ? `\n${body}` : ""}\n${ind}end`;
|
|
433
|
+
}
|
|
434
|
+
case "Function": {
|
|
435
|
+
const name =
|
|
436
|
+
s.name.propNames.join(".") +
|
|
437
|
+
(s.name.colonName ? `:${s.name.colonName}` : "");
|
|
438
|
+
return (
|
|
439
|
+
`${ind}function ${name}` +
|
|
440
|
+
this.functionRest(s.body.parameters, s.body.block, depth)
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
case "LocalFunction":
|
|
444
|
+
return (
|
|
445
|
+
`${ind}local function ${s.name}` +
|
|
446
|
+
this.functionRest(s.body.parameters, s.body.block, depth)
|
|
447
|
+
);
|
|
448
|
+
default:
|
|
449
|
+
throw new Error(
|
|
450
|
+
`pretty_print: unsupported statement type: ${(s as any).type}`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function prettyPrintExpression(
|
|
457
|
+
expr: LuaExpression,
|
|
458
|
+
opts?: PrintOptions,
|
|
459
|
+
): string {
|
|
460
|
+
return new Printer(resolveOptions(opts)).expression(expr, 0);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function prettyPrintBlock(block: LuaBlock, opts?: PrintOptions): string {
|
|
464
|
+
return new Printer(resolveOptions(opts)).block(block, 0);
|
|
465
|
+
}
|
|
@@ -915,9 +915,7 @@ export async function applyQuery(
|
|
|
915
915
|
for (let i = 0; i < violated.length; i++) {
|
|
916
916
|
if (violated[i]) {
|
|
917
917
|
throw new LuaRuntimeError(
|
|
918
|
-
`order by #${
|
|
919
|
-
i + 1
|
|
920
|
-
}: 'using' comparator violates strict weak ordering`,
|
|
918
|
+
`order by #${i + 1}: 'using' comparator violates strict weak ordering`,
|
|
921
919
|
sf,
|
|
922
920
|
);
|
|
923
921
|
}
|
|
@@ -1004,6 +1002,8 @@ export async function queryLua<T = any>(
|
|
|
1004
1002
|
for await (let { key, value } of kv.query({ prefix })) {
|
|
1005
1003
|
if (enricher) {
|
|
1006
1004
|
value = enricher(key, value);
|
|
1005
|
+
// Enrichers may return undefined to filter a row out
|
|
1006
|
+
if (value === undefined) continue;
|
|
1007
1007
|
}
|
|
1008
1008
|
results.push(value);
|
|
1009
1009
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { ASTCtx } from "./ast.ts";
|
|
2
|
+
import { evalExpression } from "./eval.ts";
|
|
3
|
+
import { parseExpressionString } from "./parse.ts";
|
|
4
|
+
import {
|
|
5
|
+
type ILuaFunction,
|
|
6
|
+
LuaEnv,
|
|
7
|
+
LuaRuntimeError,
|
|
8
|
+
LuaStackFrame,
|
|
9
|
+
LuaTable,
|
|
10
|
+
type LuaValue,
|
|
11
|
+
luaValueToJS,
|
|
12
|
+
singleResult,
|
|
13
|
+
} from "./runtime.ts";
|
|
14
|
+
import { isTaggedFloat } from "./numeric.ts";
|
|
15
|
+
import {
|
|
16
|
+
encodeRef,
|
|
17
|
+
getNameFromPath,
|
|
18
|
+
} from "@silverbulletmd/silverbullet/lib/ref";
|
|
19
|
+
import { resolveASTReference } from "../space_lua.ts";
|
|
20
|
+
import type { Client } from "../client.ts";
|
|
21
|
+
import {
|
|
22
|
+
classifyResult,
|
|
23
|
+
renderResultToCleanMarkdown,
|
|
24
|
+
} from "./render_lua_markdown.ts";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Run a Space Lua computation and convert its result into something the
|
|
28
|
+
* LuaWidget renderer understands (a widget table, markdown string, or number).
|
|
29
|
+
* Shared by the `${...}` directive and Lua code widgets so the conversion +
|
|
30
|
+
* error formatting live in one place.
|
|
31
|
+
*/
|
|
32
|
+
export async function renderLuaWidgetResult(
|
|
33
|
+
client: Client,
|
|
34
|
+
compute: (env: LuaEnv, sf: LuaStackFrame) => Promise<LuaValue> | LuaValue,
|
|
35
|
+
ctx: ASTCtx,
|
|
36
|
+
currentPageMeta?: { name: string } | undefined,
|
|
37
|
+
): Promise<any> {
|
|
38
|
+
try {
|
|
39
|
+
const tl = new LuaEnv();
|
|
40
|
+
tl.setLocal(
|
|
41
|
+
"currentPage",
|
|
42
|
+
currentPageMeta ||
|
|
43
|
+
(client.ui.viewState.current
|
|
44
|
+
? { name: getNameFromPath(client.ui.viewState.current.path) }
|
|
45
|
+
: undefined),
|
|
46
|
+
);
|
|
47
|
+
const sf = LuaStackFrame.createWithGlobalEnv(
|
|
48
|
+
client.clientSystem.spaceLuaEnv.env,
|
|
49
|
+
ctx,
|
|
50
|
+
);
|
|
51
|
+
const env = new LuaEnv(client.clientSystem.spaceLuaEnv.env);
|
|
52
|
+
env.setLocal("_CTX", tl);
|
|
53
|
+
const rawResult = singleResult(await compute(env, sf));
|
|
54
|
+
if (isTaggedFloat(rawResult) || typeof rawResult === "number") {
|
|
55
|
+
return rawResult;
|
|
56
|
+
}
|
|
57
|
+
if (rawResult instanceof LuaTable) {
|
|
58
|
+
if (rawResult.rawGet("_isWidget")) {
|
|
59
|
+
return luaValueToJS(rawResult, sf);
|
|
60
|
+
}
|
|
61
|
+
return rawResult;
|
|
62
|
+
}
|
|
63
|
+
return luaValueToJS(rawResult, sf);
|
|
64
|
+
} catch (e: any) {
|
|
65
|
+
if (e instanceof LuaRuntimeError && e.sf?.astCtx) {
|
|
66
|
+
const source = resolveASTReference(e.sf.astCtx);
|
|
67
|
+
if (source) {
|
|
68
|
+
return `**Lua error:** ${e.message} (Origin: [[${encodeRef(source)}]])`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return `**Lua error:** ${e.message}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Evaluate a `${...}`-style expression string and render it as a widget. */
|
|
76
|
+
export async function renderLuaExpression(
|
|
77
|
+
client: Client,
|
|
78
|
+
expressionText: string,
|
|
79
|
+
currentPageMeta?: { name: string } | undefined,
|
|
80
|
+
): Promise<any> {
|
|
81
|
+
if (expressionText.trim().length === 0) {
|
|
82
|
+
return "**Error:** Empty Lua expression";
|
|
83
|
+
}
|
|
84
|
+
const expr = parseExpressionString(expressionText);
|
|
85
|
+
return renderLuaWidgetResult(
|
|
86
|
+
client,
|
|
87
|
+
(env, sf) => evalExpression(expr, env, sf),
|
|
88
|
+
expr.ctx,
|
|
89
|
+
currentPageMeta,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Call a stored Lua render function (e.g. a code widget) and render it. */
|
|
94
|
+
export async function renderLuaCallback(
|
|
95
|
+
client: Client,
|
|
96
|
+
fn: ILuaFunction,
|
|
97
|
+
args: LuaValue[],
|
|
98
|
+
currentPageMeta?: { name: string } | undefined,
|
|
99
|
+
): Promise<any> {
|
|
100
|
+
return renderLuaWidgetResult(
|
|
101
|
+
client,
|
|
102
|
+
(_env, sf) => fn.call(sf, ...args),
|
|
103
|
+
{} as ASTCtx,
|
|
104
|
+
currentPageMeta,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type PortableMarkdownResult =
|
|
109
|
+
| { ok: true; markdown: string }
|
|
110
|
+
| { ok: false; reason: string };
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Evaluate a `${...}`-style expression and produce portable GFM markdown — the
|
|
114
|
+
* same clean rendering the Copy button uses — or report why it can't be baked.
|
|
115
|
+
* - nil/empty → ok with empty markdown
|
|
116
|
+
* - Lua error → not ok (the eval error string)
|
|
117
|
+
* - widget object: portable only if it exposes a `markdown` rendering;
|
|
118
|
+
* html-only widgets are not bakeable
|
|
119
|
+
* - scalars / tables / arrays / strings → clean GFM
|
|
120
|
+
*/
|
|
121
|
+
export async function expressionToPortableMarkdown(
|
|
122
|
+
client: Client,
|
|
123
|
+
expressionText: string,
|
|
124
|
+
currentPageMeta?: { name: string } | undefined,
|
|
125
|
+
): Promise<PortableMarkdownResult> {
|
|
126
|
+
const rawResult = await renderLuaExpression(
|
|
127
|
+
client,
|
|
128
|
+
expressionText,
|
|
129
|
+
currentPageMeta,
|
|
130
|
+
);
|
|
131
|
+
if (rawResult === null || rawResult === undefined) {
|
|
132
|
+
return { ok: true, markdown: "" };
|
|
133
|
+
}
|
|
134
|
+
// renderLuaExpression returns eval failures as markdown error strings.
|
|
135
|
+
if (
|
|
136
|
+
typeof rawResult === "string" &&
|
|
137
|
+
(rawResult.startsWith("**Lua error:**") ||
|
|
138
|
+
rawResult.startsWith("**Error:**"))
|
|
139
|
+
) {
|
|
140
|
+
return { ok: false, reason: rawResult };
|
|
141
|
+
}
|
|
142
|
+
// Widget objects: portable only when they expose a `markdown` rendering.
|
|
143
|
+
if (typeof rawResult === "object" && (rawResult as any)._isWidget) {
|
|
144
|
+
const md = (rawResult as any).markdown;
|
|
145
|
+
if (typeof md === "string") {
|
|
146
|
+
return { ok: true, markdown: md.trim() };
|
|
147
|
+
}
|
|
148
|
+
return { ok: false, reason: "html-only widget (no markdown rendering)" };
|
|
149
|
+
}
|
|
150
|
+
// Scalars, tables, arrays, plain strings → clean GFM markdown.
|
|
151
|
+
const markdown = await renderResultToCleanMarkdown(
|
|
152
|
+
rawResult,
|
|
153
|
+
classifyResult(rawResult),
|
|
154
|
+
);
|
|
155
|
+
return { ok: true, markdown };
|
|
156
|
+
}
|