@silverbulletmd/silverbullet 2.9.0 → 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.
Files changed (63) hide show
  1. package/README.md +36 -23
  2. package/client/plugos/hooks/syscall.ts +24 -10
  3. package/client/plugos/syscalls/asset.ts +61 -23
  4. package/client/plugos/syscalls/clientStore.ts +32 -6
  5. package/client/plugos/syscalls/client_code_widget.ts +9 -4
  6. package/client/plugos/syscalls/code_widget.ts +26 -11
  7. package/client/plugos/syscalls/config.ts +146 -30
  8. package/client/plugos/syscalls/datastore.ts +146 -40
  9. package/client/plugos/syscalls/editor.ts +1432 -711
  10. package/client/plugos/syscalls/event.ts +46 -13
  11. package/client/plugos/syscalls/fetch.ts +123 -82
  12. package/client/plugos/syscalls/index.ts +242 -116
  13. package/client/plugos/syscalls/jsonschema.ts +107 -8
  14. package/client/plugos/syscalls/language.ts +38 -12
  15. package/client/plugos/syscalls/markdown.ts +166 -50
  16. package/client/plugos/syscalls/mq.ts +93 -20
  17. package/client/plugos/syscalls/schema_introspection.ts +33 -0
  18. package/client/plugos/syscalls/service_registry.ts +74 -20
  19. package/client/plugos/syscalls/shell.ts +51 -20
  20. package/client/plugos/syscalls/space.ts +169 -113
  21. package/client/plugos/syscalls/sync.ts +50 -19
  22. package/client/plugos/syscalls/system.ts +236 -139
  23. package/client/plugos/system.ts +21 -5
  24. package/client/space_lua/api_documentation.ts +107 -0
  25. package/client/space_lua/ast.ts +13 -0
  26. package/client/space_lua/eval.ts +12 -4
  27. package/client/space_lua/parse.ts +243 -108
  28. package/client/space_lua/pretty_print.ts +128 -54
  29. package/client/space_lua/query_collection.ts +4 -4
  30. package/client/space_lua/render_widget.ts +156 -0
  31. package/client/space_lua/runtime.ts +83 -22
  32. package/client/space_lua/stdlib/crypto.ts +8 -3
  33. package/client/space_lua/stdlib/encoding.ts +27 -9
  34. package/client/space_lua/stdlib/js.ts +137 -22
  35. package/client/space_lua/stdlib/load.ts +24 -15
  36. package/client/space_lua/stdlib/math.ts +323 -118
  37. package/client/space_lua/stdlib/net.ts +57 -9
  38. package/client/space_lua/stdlib/os.ts +111 -44
  39. package/client/space_lua/stdlib/space_lua.ts +344 -20
  40. package/client/space_lua/stdlib/string.ts +282 -104
  41. package/client/space_lua/stdlib/string_pack.ts +58 -19
  42. package/client/space_lua/stdlib/table.ts +195 -40
  43. package/client/space_lua/stdlib.ts +335 -93
  44. package/client/space_lua/syscalls.ts +270 -0
  45. package/dist/plug-compile.js +1 -1
  46. package/package.json +3 -3
  47. package/plug-api/lib/ref.ts +96 -0
  48. package/plug-api/lib/shortcut.ts +2 -1
  49. package/plug-api/syscalls/config.ts +5 -3
  50. package/plug-api/syscalls/editor.ts +16 -0
  51. package/plug-api/syscalls/index.ts +17 -1
  52. package/plug-api/syscalls/jsonschema.ts +9 -0
  53. package/plug-api/syscalls/lua.ts +7 -0
  54. package/plug-api/syscalls/shell.ts +0 -1
  55. package/plug-api/syscalls/system.ts +7 -0
  56. package/plug-api/system_mock.ts +2 -2
  57. package/plug-api/types/index.ts +52 -1
  58. package/plug-api/types/manifest.ts +22 -4
  59. package/plug-api/ui/cx.ts +1 -3
  60. package/plug-api/ui/index.ts +3 -0
  61. package/plug-api/ui/panel_styles.ts +4 -3
  62. package/plug-api/ui/slugify.ts +44 -0
  63. package/client/plugos/syscalls/lua.ts +0 -81
@@ -29,9 +29,28 @@ function resolveOptions(opts?: PrintOptions): ResolvedOptions {
29
29
  }
30
30
 
31
31
  const RESERVED = new Set([
32
- "and", "break", "do", "else", "elseif", "end", "false", "for", "function",
33
- "goto", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then",
34
- "true", "until", "while",
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",
35
54
  ]);
36
55
 
37
56
  function isLuaIdentifier(s: string): boolean {
@@ -39,16 +58,26 @@ function isLuaIdentifier(s: string): boolean {
39
58
  }
40
59
 
41
60
  const BINARY_PREC: Record<string, number> = {
42
- "or": 1,
43
- "and": 2,
44
- "<": 3, ">": 3, "<=": 3, ">=": 3, "~=": 3, "==": 3,
61
+ or: 1,
62
+ and: 2,
63
+ "<": 3,
64
+ ">": 3,
65
+ "<=": 3,
66
+ ">=": 3,
67
+ "~=": 3,
68
+ "==": 3,
45
69
  "|": 4,
46
70
  "~": 5, // binary bitwise xor
47
71
  "&": 6,
48
- "<<": 7, ">>": 7,
72
+ "<<": 7,
73
+ ">>": 7,
49
74
  "..": 8,
50
- "+": 9, "-": 9,
51
- "*": 10, "/": 10, "//": 10, "%": 10,
75
+ "+": 9,
76
+ "-": 9,
77
+ "*": 10,
78
+ "/": 10,
79
+ "//": 10,
80
+ "%": 10,
52
81
  "^": 12,
53
82
  };
54
83
  const UNARY_PREC = 11;
@@ -62,7 +91,7 @@ function exprPrec(e: LuaExpression): number {
62
91
  }
63
92
 
64
93
  class Printer {
65
- constructor(private opts: ResolvedOptions) { }
94
+ constructor(private opts: ResolvedOptions) {}
66
95
 
67
96
  private indent(depth: number): string {
68
97
  return " ".repeat(depth * this.opts.indentWidth);
@@ -129,22 +158,29 @@ class Printer {
129
158
  case "TableConstructor":
130
159
  return this.tableConstructor(e.fields, depth);
131
160
  case "FunctionDefinition":
132
- return "function" +
133
- this.functionRest(e.body.parameters, e.body.block, depth);
161
+ return `function${this.functionRest(
162
+ e.body.parameters,
163
+ e.body.block,
164
+ depth,
165
+ )}`;
134
166
  case "FunctionCall":
135
167
  return this.functionCall(e, depth);
136
168
  case "Query": {
137
- const lines = e.clauses.map((c) =>
138
- this.indent(depth + 1) + this.queryClause(c, depth + 1)
169
+ const lines = e.clauses.map(
170
+ (c) => this.indent(depth + 1) + this.queryClause(c, depth + 1),
139
171
  );
140
172
  return `query[[\n${lines.join("\n")}\n${this.indent(depth)}]]`;
141
173
  }
142
174
  case "FilteredCall":
143
- return `${this.expression(e.call, depth)} filter ${this.expression(e.filter, depth)
144
- }`;
175
+ return `${this.expression(e.call, depth)} filter ${this.expression(
176
+ e.filter,
177
+ depth,
178
+ )}`;
145
179
  case "AggregateCall":
146
- return `${this.expression(e.call, depth)} order by ${this.orderByList(e.orderBy, depth)
147
- }`;
180
+ return `${this.expression(e.call, depth)} order by ${this.orderByList(
181
+ e.orderBy,
182
+ depth,
183
+ )}`;
148
184
  default:
149
185
  throw new Error(
150
186
  `pretty_print: unsupported expression type: ${(e as any).type}`,
@@ -193,8 +229,8 @@ class Printer {
193
229
  private tableConstructor(fields: LuaTableField[], depth: number): string {
194
230
  if (fields.length === 0) return "{}";
195
231
  if (fields.length === 1) return `{${this.field(fields[0], depth)}}`;
196
- const lines = fields.map((f) =>
197
- this.indent(depth + 1) + this.field(f, depth + 1)
232
+ const lines = fields.map(
233
+ (f) => this.indent(depth + 1) + this.field(f, depth + 1),
198
234
  );
199
235
  const tail = this.opts.trailingComma ? "," : "";
200
236
  return `{\n${lines.join(",\n")}${tail}\n${this.indent(depth)}}`;
@@ -224,7 +260,8 @@ class Printer {
224
260
  ? ` order by ${this.orderByList(e.orderBy, depth)}`
225
261
  : "";
226
262
  if (
227
- !e.orderBy && args.length === 1 &&
263
+ !e.orderBy &&
264
+ args.length === 1 &&
228
265
  (args[0].type === "TableConstructor" || args[0].type === "String")
229
266
  ) {
230
267
  return `${base} ${this.expression(args[0], depth)}`;
@@ -254,8 +291,10 @@ class Printer {
254
291
  case "Where":
255
292
  return `where ${this.expression(c.expression, depth)}`;
256
293
  case "Limit":
257
- return `limit ${this.expression(c.limit, depth)}` +
258
- (c.offset ? ` offset ${this.expression(c.offset, depth)}` : "");
294
+ return (
295
+ `limit ${this.expression(c.limit, depth)}` +
296
+ (c.offset ? ` offset ${this.expression(c.offset, depth)}` : "")
297
+ );
259
298
  case "Offset":
260
299
  return `offset ${this.expression(c.offset, depth)}`;
261
300
  case "OrderBy":
@@ -271,16 +310,32 @@ class Printer {
271
310
 
272
311
  block(b: LuaBlock, depth: number): string {
273
312
  const stmts = b.statements.filter((s) => s.type !== "Semicolon");
274
- const isDef = (s: LuaStatement) =>
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]) =>
275
318
  s.type === "Function" || s.type === "LocalFunction";
276
319
  let out = "";
277
- for (let i = 0; i < stmts.length; i++) {
320
+ for (let i = 0; i < items.length; i++) {
278
321
  if (i > 0) {
279
322
  out += "\n";
280
323
  // blank line separating function definitions from neighbours
281
- if (isDef(stmts[i - 1]) || isDef(stmts[i])) out += "\n";
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
+ }
282
332
  }
283
- out += this.statement(stmts[i], depth);
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);
284
339
  }
285
340
  return out;
286
341
  }
@@ -290,11 +345,14 @@ class Printer {
290
345
  switch (s.type) {
291
346
  case "Local": {
292
347
  const names = s.names
293
- .map((n) =>
294
- n.name +
295
- (n.attribute
296
- ? ` <${n.attribute}>`
297
- : (n.attributes?.length ? ` <${n.attributes[0]}>` : ""))
348
+ .map(
349
+ (n) =>
350
+ n.name +
351
+ (n.attribute
352
+ ? ` <${n.attribute}>`
353
+ : n.attributes?.length
354
+ ? ` <${n.attributes[0]}>`
355
+ : ""),
298
356
  )
299
357
  .join(", ");
300
358
  const exprs = s.expressions?.length
@@ -303,12 +361,16 @@ class Printer {
303
361
  return `${ind}local ${names}${exprs}`;
304
362
  }
305
363
  case "Assignment":
306
- return `${ind}${s.variables.map((v) => this.expression(v, depth)).join(", ")
307
- } = ${s.expressions.map((e) => this.expression(e, depth)).join(", ")}`;
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(", ")}`;
308
369
  case "Return":
309
370
  return s.expressions.length
310
- ? `${ind}return ${s.expressions.map((e) => this.expression(e, depth)).join(", ")
311
- }`
371
+ ? `${ind}return ${s.expressions
372
+ .map((e) => this.expression(e, depth))
373
+ .join(", ")}`
312
374
  : `${ind}return`;
313
375
  case "FunctionCallStatement":
314
376
  return ind + this.expression(s.call, depth);
@@ -328,7 +390,8 @@ class Printer {
328
390
  const kw = i === 0 ? "if" : "elseif";
329
391
  const head = i === 0 ? `${ind}${kw} ` : `\n${ind}${kw} `;
330
392
  const body = this.block(c.block, depth + 1);
331
- out += `${head}${this.expression(c.condition, depth)} then` +
393
+ out +=
394
+ `${head}${this.expression(c.condition, depth)} then` +
332
395
  (body ? `\n${body}` : "");
333
396
  });
334
397
  if (s.elseBlock) {
@@ -340,34 +403,48 @@ class Printer {
340
403
  }
341
404
  case "While": {
342
405
  const body = this.block(s.block, depth + 1);
343
- return `${ind}while ${this.expression(s.condition, depth)} do` +
344
- (body ? `\n${body}` : "") + `\n${ind}end`;
406
+ return (
407
+ `${ind}while ${this.expression(s.condition, depth)} do` +
408
+ (body ? `\n${body}` : "") +
409
+ `\n${ind}end`
410
+ );
345
411
  }
346
412
  case "Repeat": {
347
413
  const body = this.block(s.block, depth + 1);
348
- return `${ind}repeat` + (body ? `\n${body}` : "") +
349
- `\n${ind}until ${this.expression(s.condition, depth)}`;
414
+ return (
415
+ `${ind}repeat` +
416
+ (body ? `\n${body}` : "") +
417
+ `\n${ind}until ${this.expression(s.condition, depth)}`
418
+ );
350
419
  }
351
420
  case "For": {
352
421
  const step = s.step ? `, ${this.expression(s.step, depth)}` : "";
353
422
  const body = this.block(s.block, depth + 1);
354
- return `${ind}for ${s.name} = ${this.expression(s.start, depth)}, ${this.expression(s.end, depth)
355
- }${step} do${body ? `\n${body}` : ""}\n${ind}end`;
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`;
356
427
  }
357
428
  case "ForIn": {
358
429
  const body = this.block(s.block, depth + 1);
359
- return `${ind}for ${s.names.join(", ")} in ${s.expressions.map((e) => this.expression(e, depth)).join(", ")
360
- } do${body ? `\n${body}` : ""}\n${ind}end`;
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`;
361
433
  }
362
434
  case "Function": {
363
- const name = s.name.propNames.join(".") +
435
+ const name =
436
+ s.name.propNames.join(".") +
364
437
  (s.name.colonName ? `:${s.name.colonName}` : "");
365
- return `${ind}function ${name}` +
366
- this.functionRest(s.body.parameters, s.body.block, depth);
438
+ return (
439
+ `${ind}function ${name}` +
440
+ this.functionRest(s.body.parameters, s.body.block, depth)
441
+ );
367
442
  }
368
443
  case "LocalFunction":
369
- return `${ind}local function ${s.name}` +
370
- this.functionRest(s.body.parameters, s.body.block, depth);
444
+ return (
445
+ `${ind}local function ${s.name}` +
446
+ this.functionRest(s.body.parameters, s.body.block, depth)
447
+ );
371
448
  default:
372
449
  throw new Error(
373
450
  `pretty_print: unsupported statement type: ${(s as any).type}`,
@@ -383,9 +460,6 @@ export function prettyPrintExpression(
383
460
  return new Printer(resolveOptions(opts)).expression(expr, 0);
384
461
  }
385
462
 
386
- export function prettyPrintBlock(
387
- block: LuaBlock,
388
- opts?: PrintOptions,
389
- ): string {
463
+ export function prettyPrintBlock(block: LuaBlock, opts?: PrintOptions): string {
390
464
  return new Printer(resolveOptions(opts)).block(block, 0);
391
465
  }
@@ -193,7 +193,7 @@ export interface LuaQueryCollection {
193
193
  * Implements a query collection for a regular JavaScript array
194
194
  */
195
195
  export class ArrayQueryCollection<T> implements LuaQueryCollection {
196
- constructor(private readonly array: T[]) { }
196
+ constructor(private readonly array: T[]) {}
197
197
  query(
198
198
  query: LuaCollectionQuery,
199
199
  env: LuaEnv,
@@ -842,7 +842,7 @@ export async function applyQuery(
842
842
 
843
843
  const mkEnv = grouped
844
844
  ? (ov: string | undefined, item: any, e: LuaEnv, s: LuaStackFrame) =>
845
- buildGroupItemEnv(ov, groupByNames, item, e, s)
845
+ buildGroupItemEnv(ov, groupByNames, item, e, s)
846
846
  : buildItemEnvLocal;
847
847
 
848
848
  let selectResults: any[] | undefined;
@@ -1002,7 +1002,7 @@ export async function queryLua<T = any>(
1002
1002
  for await (let { key, value } of kv.query({ prefix })) {
1003
1003
  if (enricher) {
1004
1004
  value = enricher(key, value);
1005
- // Enrichers may return undefined to filter a row out
1005
+ // Enrichers may return undefined to filter a row out
1006
1006
  if (value === undefined) continue;
1007
1007
  }
1008
1008
  results.push(value);
@@ -1056,7 +1056,7 @@ export class DataStoreQueryCollection implements LuaQueryCollection {
1056
1056
  constructor(
1057
1057
  private readonly dataStore: DataStore,
1058
1058
  readonly prefix: string[],
1059
- ) { }
1059
+ ) {}
1060
1060
  query(
1061
1061
  query: LuaCollectionQuery,
1062
1062
  env: LuaEnv,
@@ -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
+ }
@@ -5,6 +5,10 @@ import { isPromise, rpAll } from "./rp.ts";
5
5
  import { isNegativeZero, isTaggedFloat } from "./numeric.ts";
6
6
  import { isSqlNull } from "./sliq_null.ts";
7
7
  import { luaFormat } from "./stdlib/format.ts";
8
+ import type {
9
+ LuaFunctionDocumentation,
10
+ LuaFunctionInfo,
11
+ } from "../../plug-api/types/index.ts";
8
12
 
9
13
  export type LuaType =
10
14
  | "nil"
@@ -20,10 +24,19 @@ export type LuaType =
20
24
  export type LuaValue = any;
21
25
  export type JSValue = any;
22
26
 
27
+ type LuaFunctionDefinitionMetadata = LuaFunctionDocumentation &
28
+ Partial<Pick<LuaFunctionInfo, "kind" | "name" | "source">>;
29
+
30
+ export type LuaFunctionDefinition<Callback> = {
31
+ callback: Callback;
32
+ } & LuaFunctionDefinitionMetadata;
33
+
23
34
  export interface ILuaFunction {
24
35
  call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue;
25
36
 
26
37
  asString(): string;
38
+
39
+ info?: LuaFunctionInfo;
27
40
  }
28
41
 
29
42
  export interface ILuaSettable {
@@ -453,12 +466,25 @@ export function singleResult(value: any): any {
453
466
  export class LuaFunction implements ILuaFunction {
454
467
  private capturedEnv: LuaEnv;
455
468
  funcHasGotos?: boolean;
469
+ info: LuaFunctionInfo;
456
470
 
457
471
  constructor(
458
472
  readonly body: LuaFunctionBody,
459
473
  closure: LuaEnv,
474
+ info: Partial<LuaFunctionInfo> = {},
460
475
  ) {
461
476
  this.capturedEnv = closure;
477
+ const documentation = body.documentation ?? {};
478
+ this.info = {
479
+ kind: "lua",
480
+ source: { ...body.ctx },
481
+ ...documentation,
482
+ ...info,
483
+ parameters:
484
+ info.parameters ??
485
+ documentation.parameters ??
486
+ body.parameters.map((name) => ({ name })),
487
+ };
462
488
  }
463
489
 
464
490
  call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
@@ -482,10 +508,7 @@ export class LuaFunction implements ILuaFunction {
482
508
  for (let i = 0; i < varargStart; i++) {
483
509
  env.setLocal(params[i], resolvedArgs[i] ?? null);
484
510
  }
485
- env.setLocal(
486
- "...",
487
- new LuaMultiRes(resolvedArgs.slice(varargStart)),
488
- );
511
+ env.setLocal("...", new LuaMultiRes(resolvedArgs.slice(varargStart)));
489
512
  } else {
490
513
  // Non-variadic: bind all named params directly
491
514
  for (let i = 0; i < params.length; i++) {
@@ -565,7 +588,26 @@ function mapFunctionReturnValue(values: any[]): any {
565
588
  }
566
589
 
567
590
  export class LuaNativeJSFunction implements ILuaFunction {
568
- constructor(readonly fn: (...args: JSValue[]) => JSValue) {}
591
+ readonly fn: (...args: JSValue[]) => JSValue;
592
+ public info: LuaFunctionInfo;
593
+
594
+ constructor(
595
+ definition:
596
+ | ((...args: JSValue[]) => JSValue)
597
+ | LuaFunctionDefinition<(...args: JSValue[]) => JSValue>,
598
+ ) {
599
+ if (typeof definition === "function") {
600
+ this.fn = definition;
601
+ this.info = { kind: "builtin" };
602
+ } else {
603
+ const { callback, ...info } = definition;
604
+ this.fn = callback;
605
+ this.info = {
606
+ kind: "builtin",
607
+ ...info,
608
+ };
609
+ }
610
+ }
569
611
 
570
612
  // Performs automatic conversion between Lua and JS values for arguments, but not for return values
571
613
  call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
@@ -595,9 +637,28 @@ export class LuaNativeJSFunction implements ILuaFunction {
595
637
  }
596
638
 
597
639
  export class LuaBuiltinFunction implements ILuaFunction {
640
+ readonly fn: (sf: LuaStackFrame, ...args: LuaValue[]) => LuaValue;
641
+ public info: LuaFunctionInfo;
642
+
598
643
  constructor(
599
- readonly fn: (sf: LuaStackFrame, ...args: LuaValue[]) => LuaValue,
600
- ) {}
644
+ definition:
645
+ | ((sf: LuaStackFrame, ...args: LuaValue[]) => LuaValue)
646
+ | LuaFunctionDefinition<
647
+ (sf: LuaStackFrame, ...args: LuaValue[]) => LuaValue
648
+ >,
649
+ ) {
650
+ if (typeof definition === "function") {
651
+ this.fn = definition;
652
+ this.info = { kind: "builtin" };
653
+ } else {
654
+ const { callback, ...info } = definition;
655
+ this.fn = callback;
656
+ this.info = {
657
+ kind: "builtin",
658
+ ...info,
659
+ };
660
+ }
661
+ }
601
662
 
602
663
  call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
603
664
  // _CTX is already available via the stack frame
@@ -1313,11 +1374,9 @@ export function luaSet(
1313
1374
  const k = toNumKey(normKey);
1314
1375
  const jsVal = luaValueToJS(value, sf);
1315
1376
  if (isPromise(jsVal)) {
1316
- return (jsVal as Promise<any>).then(
1317
- (v) => {
1318
- (obj as Record<string | number, any>)[k] = v;
1319
- },
1320
- );
1377
+ return (jsVal as Promise<any>).then((v) => {
1378
+ (obj as Record<string | number, any>)[k] = v;
1379
+ });
1321
1380
  }
1322
1381
  (obj as Record<string | number, any>)[k] = jsVal;
1323
1382
  }
@@ -1492,16 +1551,18 @@ export function luaCall(
1492
1551
 
1493
1552
  export function luaEquals(a: any, b: any): boolean {
1494
1553
  // Normalize nil variants (null, undefined, SQL NULL) to null
1495
- const an = (a === null || a === undefined || isSqlNull(a))
1496
- ? null
1497
- : isTaggedFloat(a)
1498
- ? a.value
1499
- : a;
1500
- const bn = (b === null || b === undefined || isSqlNull(b))
1501
- ? null
1502
- : isTaggedFloat(b)
1503
- ? b.value
1504
- : b;
1554
+ const an =
1555
+ a === null || a === undefined || isSqlNull(a)
1556
+ ? null
1557
+ : isTaggedFloat(a)
1558
+ ? a.value
1559
+ : a;
1560
+ const bn =
1561
+ b === null || b === undefined || isSqlNull(b)
1562
+ ? null
1563
+ : isTaggedFloat(b)
1564
+ ? b.value
1565
+ : b;
1505
1566
  return an === bn;
1506
1567
  }
1507
1568
 
@@ -2,9 +2,14 @@ import { LuaBuiltinFunction, LuaTable } from "../runtime.ts";
2
2
  import { hashSHA256 } from "../../../plug-api/lib/crypto.ts";
3
3
 
4
4
  export const cryptoApi = new LuaTable({
5
- sha256: new LuaBuiltinFunction(
6
- (_sf, s: string | Uint8Array): Promise<string> => {
5
+ sha256: new LuaBuiltinFunction({
6
+ callback: (_sf, s: string | Uint8Array): Promise<string> => {
7
7
  return hashSHA256(s);
8
8
  },
9
- ),
9
+ description: "Computes the SHA-256 digest of a string or byte buffer.",
10
+ parameters: [
11
+ { name: "data", type: "string|bytes", description: "Data to hash." },
12
+ ],
13
+ returns: [{ type: "string", description: "Hexadecimal SHA-256 digest." }],
14
+ }),
10
15
  });