@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
@@ -14,7 +14,7 @@ import {
14
14
  LuaMultiRes,
15
15
  LuaRuntimeError,
16
16
  type LuaStackFrame,
17
- type LuaTable,
17
+ LuaTable,
18
18
  luaToString,
19
19
  luaTypeOf,
20
20
  type LuaValue,
@@ -37,35 +37,70 @@ import { isTaggedFloat, makeLuaFloat } from "./numeric.ts";
37
37
  import { isPromise } from "./rp.ts";
38
38
  import { isSqlNull } from "./sliq_null.ts";
39
39
 
40
- const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => {
41
- console.log("[Lua]", ...(await Promise.all(args.map((v) => luaToString(v)))));
40
+ const printFunction = new LuaBuiltinFunction({
41
+ callback: async (_sf, ...args) => {
42
+ console.log(
43
+ "[Lua]",
44
+ ...(await Promise.all(args.map((v) => luaToString(v)))),
45
+ );
46
+ },
47
+ description:
48
+ "Prints string representations of its arguments to the runtime log.",
49
+ signatures: ["print(...)"],
50
+ parameters: [{ name: "...", description: "Values to print." }],
51
+ examples: [{ code: 'print("Hello, world!")' }],
42
52
  });
43
53
 
44
- const assertFunction = new LuaBuiltinFunction(
45
- async (sf, value: any, message?: string) => {
54
+ const assertFunction = new LuaBuiltinFunction({
55
+ callback: async (sf, value: any, message?: string) => {
46
56
  if (!(await value)) {
47
57
  throw new LuaRuntimeError(`Assertion failed: ${message}`, sf);
48
58
  }
49
59
  },
50
- );
60
+ description:
61
+ "Raises an error when a value is falsy; otherwise completes successfully.",
62
+ parameters: [
63
+ { name: "value", description: "Condition to test." },
64
+ {
65
+ name: "message",
66
+ type: "string",
67
+ description: "Error detail.",
68
+ optional: true,
69
+ },
70
+ ],
71
+ examples: [{ code: 'assert(user ~= nil, "user is required")' }],
72
+ });
51
73
 
52
- const ipairsFunction = new LuaBuiltinFunction((sf, t: LuaTable | any[]) => {
53
- let i = 0;
74
+ const ipairsFunction = new LuaBuiltinFunction({
75
+ callback: (sf, t: LuaTable | any[]) => {
76
+ let i = 0;
54
77
 
55
- return async () => {
56
- i = i + 1;
78
+ return async () => {
79
+ i = i + 1;
57
80
 
58
- const v = await luaGet(t, i, sf.astCtx ?? null, sf);
59
- if (v === null || v === undefined) {
60
- return;
61
- }
81
+ const v = await luaGet(t, i, sf.astCtx ?? null, sf);
82
+ if (v === null || v === undefined) {
83
+ return;
84
+ }
62
85
 
63
- return new LuaMultiRes([i, v]);
64
- };
86
+ return new LuaMultiRes([i, v]);
87
+ };
88
+ },
89
+ description:
90
+ "Returns an iterator over consecutive integer keys starting at 1 and stopping at the first `nil`.",
91
+ parameters: [{ name: "table", type: "table" }],
92
+ returns: [
93
+ { type: "function", description: "Iterator yielding index and value." },
94
+ ],
95
+ examples: [
96
+ {
97
+ code: 'for i, fruit in ipairs({"apple", "banana"}) do\n print(i, fruit)\nend',
98
+ },
99
+ ],
65
100
  });
66
101
 
67
- const pairsFunction = new LuaBuiltinFunction(
68
- (sf, t: LuaTable | any[] | Record<string, any>) => {
102
+ const pairsFunction = new LuaBuiltinFunction({
103
+ callback: (sf, t: LuaTable | any[] | Record<string, any>) => {
69
104
  // Respect `__pairs` metamethod for Lua tables
70
105
  if (isLuaTable(t)) {
71
106
  const mt = (t as any).metatable as LuaTable | null | undefined;
@@ -105,10 +140,24 @@ const pairsFunction = new LuaBuiltinFunction(
105
140
  // Must return (iter, state, control) for generic for
106
141
  return new LuaMultiRes([iter, t, null]);
107
142
  },
108
- );
143
+ description:
144
+ "Returns an iterator over all table key-value pairs, respecting `__pairs`.",
145
+ parameters: [{ name: "table", type: "table" }],
146
+ returns: [
147
+ {
148
+ type: "function",
149
+ description: "Iterator plus its state and initial control value.",
150
+ },
151
+ ],
152
+ examples: [
153
+ {
154
+ code: 'for key, value in pairs({name = "Ada", age = 36}) do\n print(key, value)\nend',
155
+ },
156
+ ],
157
+ });
109
158
 
110
- export const eachFunction = new LuaBuiltinFunction(
111
- (sf, ar: LuaTable | any[]) => {
159
+ export const eachFunction = new LuaBuiltinFunction({
160
+ callback: (sf, ar: LuaTable | any[]) => {
112
161
  let i = 1;
113
162
  const length = (ar as any).length;
114
163
  return async () => {
@@ -120,18 +169,30 @@ export const eachFunction = new LuaBuiltinFunction(
120
169
  return result;
121
170
  };
122
171
  },
123
- );
172
+ description:
173
+ "Returns a Space Lua iterator over array-like values without yielding indices.",
174
+ parameters: [{ name: "table", type: "table" }],
175
+ returns: [{ type: "function", description: "Iterator yielding values." }],
176
+ examples: [
177
+ {
178
+ code: 'for fruit in each({"apple", "banana"}) do\n print(fruit)\nend',
179
+ },
180
+ ],
181
+ });
124
182
 
125
- const typeFunction = new LuaBuiltinFunction(
126
- (_sf, value: LuaValue): string | Promise<string> => {
183
+ const typeFunction = new LuaBuiltinFunction({
184
+ callback: (_sf, value: LuaValue): string | Promise<string> => {
127
185
  return luaTypeOf(value);
128
186
  },
129
- );
187
+ description: "Returns the Lua type name of a value.",
188
+ parameters: [{ name: "value" }],
189
+ returns: [{ type: "string" }],
190
+ });
130
191
 
131
192
  // tostring() checks `__tostring` metamethod first (with live SF), then
132
193
  // falls back to the default `luaToString` representation.
133
- const tostringFunction = new LuaBuiltinFunction(
134
- (sf, value: any): string | Promise<string> => {
194
+ const tostringFunction = new LuaBuiltinFunction({
195
+ callback: (sf, value: any): string | Promise<string> => {
135
196
  const mt = getMetatable(value, sf);
136
197
  if (mt) {
137
198
  const mm = mt.rawGet("__tostring");
@@ -153,10 +214,14 @@ const tostringFunction = new LuaBuiltinFunction(
153
214
  }
154
215
  return luaToString(value);
155
216
  },
156
- );
217
+ description:
218
+ "Converts a value to a string, respecting its `__tostring` metamethod.",
219
+ parameters: [{ name: "value" }],
220
+ returns: [{ type: "string" }],
221
+ });
157
222
 
158
- const tonumberFunction = new LuaBuiltinFunction(
159
- (sf, value: LuaValue, base?: number) => {
223
+ const tonumberFunction = new LuaBuiltinFunction({
224
+ callback: (sf, value: LuaValue, base?: number) => {
160
225
  if (base !== undefined) {
161
226
  if (!(typeof base === "number" && base >= 2 && base <= 36)) {
162
227
  throw new LuaRuntimeError(
@@ -188,10 +253,26 @@ const tonumberFunction = new LuaBuiltinFunction(
188
253
 
189
254
  return result.value;
190
255
  },
191
- );
256
+ description:
257
+ "Converts a number or numeric string to a Lua number, optionally in a base from 2 through 36.",
258
+ signatures: [
259
+ "tonumber(value): number|nil",
260
+ "tonumber(value, base): integer|nil",
261
+ ],
262
+ parameters: [
263
+ { name: "value", type: "number|string" },
264
+ { name: "base", type: "integer", optional: true },
265
+ ],
266
+ returns: [{ type: "number|nil" }],
267
+ examples: [{ code: 'print(tonumber("2a", 16)) -- 42' }],
268
+ });
192
269
 
193
- const errorFunction = new LuaBuiltinFunction((sf, message: string) => {
194
- throw new LuaRuntimeError(message, sf);
270
+ const errorFunction = new LuaBuiltinFunction({
271
+ callback: (sf, message: string) => {
272
+ throw new LuaRuntimeError(message, sf);
273
+ },
274
+ description: "Raises a Lua runtime error with the supplied message.",
275
+ parameters: [{ name: "message", type: "string" }],
195
276
  });
196
277
 
197
278
  async function pcallBoundary(
@@ -221,8 +302,8 @@ async function pcallBoundary(
221
302
  }
222
303
  }
223
304
 
224
- const pcallFunction = new LuaBuiltinFunction(
225
- async (sf, fn: ILuaFunction, ...args) => {
305
+ const pcallFunction = new LuaBuiltinFunction({
306
+ callback: async (sf, fn: ILuaFunction, ...args) => {
226
307
  // To-be-closed variables must be closed when unwinding to the
227
308
  // protected call boundary. Space Lua uses a per-thread close
228
309
  // stack, so we snapshot its length and close anything pushed
@@ -241,10 +322,29 @@ const pcallFunction = new LuaBuiltinFunction(
241
322
  }
242
323
  return new LuaMultiRes([false, res.message]);
243
324
  },
244
- );
325
+ description:
326
+ "Calls a function in protected mode and returns a success flag followed by results or an error message.",
327
+ signatures: ["pcall(function, ...): boolean, ..."],
328
+ parameters: [
329
+ { name: "function", type: "function" },
330
+ { name: "...", description: "Arguments passed to the function." },
331
+ ],
332
+ returns: [
333
+ { type: "boolean", description: "Whether the call succeeded." },
334
+ { description: "Call results or error message." },
335
+ ],
336
+ examples: [
337
+ { code: "local ok, result = pcall(function() return mightFail() end)" },
338
+ ],
339
+ });
245
340
 
246
- const xpcallFunction = new LuaBuiltinFunction(
247
- async (sf, fn: ILuaFunction, errorHandler: ILuaFunction, ...args) => {
341
+ const xpcallFunction = new LuaBuiltinFunction({
342
+ callback: async (
343
+ sf,
344
+ fn: ILuaFunction,
345
+ errorHandler: ILuaFunction,
346
+ ...args
347
+ ) => {
248
348
  // Same semantic as `pcall` (see comments there)
249
349
  const res = await pcallBoundary(sf, fn, args);
250
350
  if (res.ok) {
@@ -254,30 +354,69 @@ const xpcallFunction = new LuaBuiltinFunction(
254
354
  const outVals = hr instanceof LuaMultiRes ? hr.flatten().values : [hr];
255
355
  return new LuaMultiRes([false, ...outVals]);
256
356
  },
257
- );
357
+ description:
358
+ "Calls a function in protected mode and transforms any error with an error handler.",
359
+ signatures: ["xpcall(function, errorHandler, ...): boolean, ..."],
360
+ parameters: [
361
+ { name: "function", type: "function" },
362
+ { name: "errorHandler", type: "function" },
363
+ { name: "...", description: "Arguments passed to the function." },
364
+ ],
365
+ returns: [
366
+ { type: "boolean", description: "Whether the call succeeded." },
367
+ { description: "Call results or handler results." },
368
+ ],
369
+ examples: [
370
+ {
371
+ code: 'local ok, message = xpcall(riskyOperation, function(err)\n return "Operation failed: " .. tostring(err)\nend)',
372
+ },
373
+ ],
374
+ });
258
375
 
259
- const setmetatableFunction = new LuaBuiltinFunction(
260
- (sf, table: LuaTable, metatable: LuaTable) => {
376
+ const setmetatableFunction = new LuaBuiltinFunction({
377
+ callback: (sf, table: LuaTable, metatable: LuaTable) => {
261
378
  if (!metatable) {
262
379
  throw new LuaRuntimeError("metatable cannot be set to nil", sf);
263
380
  }
264
381
  table.metatable = metatable;
265
382
  return table;
266
383
  },
267
- );
384
+ description: "Sets a table's metatable and returns the table.",
385
+ parameters: [
386
+ { name: "table", type: "table" },
387
+ { name: "metatable", type: "table" },
388
+ ],
389
+ returns: [{ type: "table" }],
390
+ });
268
391
 
269
- const rawlenFunction = new LuaBuiltinFunction((_sf, value: LuaValue) => {
270
- return luaLen(value, _sf, true);
392
+ const rawlenFunction = new LuaBuiltinFunction({
393
+ callback: (_sf, value: LuaValue) => luaLen(value, _sf, true),
394
+ description: "Returns a string or table length without invoking `__len`.",
395
+ parameters: [{ name: "value", type: "string|table" }],
396
+ returns: [{ type: "integer" }],
271
397
  });
272
398
 
273
- const rawsetFunction = new LuaBuiltinFunction(
274
- (_sf, table: LuaTable, key: LuaValue, value: LuaValue) => {
399
+ const rawsetFunction = new LuaBuiltinFunction({
400
+ callback: (_sf, table: LuaTable, key: LuaValue, value: LuaValue) => {
275
401
  return (table as any).rawSet(key, value);
276
402
  },
277
- );
403
+ description:
404
+ "Sets a table key without invoking `__newindex` and returns the table.",
405
+ parameters: [
406
+ { name: "table", type: "table" },
407
+ { name: "key" },
408
+ { name: "value" },
409
+ ],
410
+ returns: [{ type: "table" }],
411
+ examples: [
412
+ {
413
+ code: 'local t = setmetatable({}, {__newindex = function() error("blocked") end})\nrawset(t, "name", "Ada")',
414
+ },
415
+ ],
416
+ });
278
417
 
279
- const rawgetFunction = new LuaBuiltinFunction(
280
- (_sf, table: any, key: LuaValue) => {
418
+ const rawgetFunction = new LuaBuiltinFunction({
419
+ callback: (_sf, table: any, key: LuaValue) => {
281
420
  const isArray = Array.isArray(table);
282
421
 
283
422
  const isPlainObj =
@@ -328,37 +467,58 @@ const rawgetFunction = new LuaBuiltinFunction(
328
467
  const v = (table as Record<string | number, any>)[k as any];
329
468
  return v === undefined ? null : v;
330
469
  },
331
- );
470
+ description: "Reads a table key without invoking `__index`.",
471
+ parameters: [{ name: "table", type: "table" }, { name: "key" }],
472
+ returns: [{ description: "Stored value or `nil`." }],
473
+ });
332
474
 
333
- const rawequalFunction = new LuaBuiltinFunction((_sf, a: any, b: any) => {
334
- const av = isTaggedFloat(a) ? a.value : a;
335
- const bv = isTaggedFloat(b) ? b.value : b;
336
- return av === bv;
475
+ const rawequalFunction = new LuaBuiltinFunction({
476
+ callback: (_sf, a: any, b: any) => {
477
+ const av = isTaggedFloat(a) ? a.value : a;
478
+ const bv = isTaggedFloat(b) ? b.value : b;
479
+ return av === bv;
480
+ },
481
+ description: "Tests two values for equality without invoking `__eq`.",
482
+ parameters: [{ name: "a" }, { name: "b" }],
483
+ returns: [{ type: "boolean" }],
337
484
  });
338
485
 
339
- const getmetatableFunction = new LuaBuiltinFunction((_sf, table: LuaTable) => {
340
- return (table as any).metatable;
486
+ const getmetatableFunction = new LuaBuiltinFunction({
487
+ callback: (_sf, table: LuaTable) => (table as any).metatable,
488
+ description: "Returns a table's metatable, or `nil` when none is set.",
489
+ parameters: [{ name: "table", type: "table" }],
490
+ returns: [{ type: "table|nil" }],
341
491
  });
342
492
 
343
- const dofileFunction = new LuaBuiltinFunction(async (sf, filename: string) => {
344
- const global = sf.threadLocal.get("_GLOBAL") as LuaEnv;
345
- const file = (await luaCall(
346
- (global.get("space") as any).get("readFile"),
347
- [filename],
348
- sf.astCtx!,
349
- sf,
350
- )) as Uint8Array;
351
- const code = new TextDecoder().decode(file);
352
- try {
353
- const parsedExpr = parseBlock(code);
354
- const env = new LuaEnv(global);
355
- await evalStatement(parsedExpr, env, sf.withCtx(parsedExpr.ctx));
356
- } catch (e: any) {
357
- throw new LuaRuntimeError(
358
- `Error evaluating "${filename}": ${e.message}`,
493
+ const dofileFunction = new LuaBuiltinFunction({
494
+ callback: async (sf, filename: string) => {
495
+ const global = sf.threadLocal.get("_GLOBAL") as LuaEnv;
496
+ const file = (await luaCall(
497
+ (global.get("space") as any).get("readFile"),
498
+ [filename],
499
+ sf.astCtx!,
359
500
  sf,
360
- );
361
- }
501
+ )) as Uint8Array;
502
+ const code = new TextDecoder().decode(file);
503
+ try {
504
+ const parsedExpr = parseBlock(code);
505
+ const env = new LuaEnv(global);
506
+ await evalStatement(parsedExpr, env, sf.withCtx(parsedExpr.ctx));
507
+ } catch (e: any) {
508
+ throw new LuaRuntimeError(
509
+ `Error evaluating "${filename}": ${e.message}`,
510
+ sf,
511
+ );
512
+ }
513
+ },
514
+ description: "Reads and executes a Lua source file from the current space.",
515
+ parameters: [
516
+ {
517
+ name: "path",
518
+ type: "string",
519
+ description: "Space-relative Lua file path.",
520
+ },
521
+ ],
362
522
  });
363
523
 
364
524
  /**
@@ -369,8 +529,8 @@ const dofileFunction = new LuaBuiltinFunction(async (sf, filename: string) => {
369
529
  * argument). Otherwise, index must be the string "#", and select
370
530
  * returns the total number of extra arguments it received.
371
531
  */
372
- const selectFunction = new LuaBuiltinFunction(
373
- (_sf, index: number | "#", ...args: LuaValue[]) => {
532
+ const selectFunction = new LuaBuiltinFunction({
533
+ callback: (_sf, index: number | "#", ...args: LuaValue[]) => {
374
534
  if (index === "#") {
375
535
  return args.length;
376
536
  }
@@ -381,7 +541,19 @@ const selectFunction = new LuaBuiltinFunction(
381
541
  return new LuaMultiRes(args.slice(args.length + index));
382
542
  }
383
543
  },
384
- );
544
+ description:
545
+ "Returns the count of extra arguments or all arguments from a selected position onward.",
546
+ signatures: ['select("#", ...): integer', "select(index, ...): ..."],
547
+ parameters: [
548
+ {
549
+ name: "index",
550
+ type: "integer|string",
551
+ description: "One-based index, negative index from the end, or `#`.",
552
+ },
553
+ { name: "..." },
554
+ ],
555
+ returns: [{ description: "Argument count or selected argument values." }],
556
+ });
385
557
 
386
558
  /**
387
559
  * From the Lua docs:
@@ -403,8 +575,12 @@ const selectFunction = new LuaBuiltinFunction(
403
575
  * during its traversal. You may however modify existing fields. In
404
576
  * particular, you may set existing fields to nil.
405
577
  */
406
- const nextFunction = new LuaBuiltinFunction(
407
- (sf, table: LuaTable | Record<string, any>, index: number | null = null) => {
578
+ const nextFunction = new LuaBuiltinFunction({
579
+ callback: (
580
+ sf,
581
+ table: LuaTable | Record<string, any>,
582
+ index: number | null = null,
583
+ ) => {
408
584
  if (!table) {
409
585
  // When nil value
410
586
  return null;
@@ -434,24 +610,84 @@ const nextFunction = new LuaBuiltinFunction(
434
610
  }
435
611
  return new LuaMultiRes([key, luaGet(table, key, sf.astCtx ?? null, sf)]);
436
612
  },
437
- );
613
+ description:
614
+ "Returns the next table key and value after a given key, or the first pair when the key is omitted.",
615
+ parameters: [
616
+ { name: "table", type: "table" },
617
+ { name: "index", description: "Previous key.", optional: true },
618
+ ],
619
+ returns: [
620
+ { description: "Next key or `nil`." },
621
+ { description: "Value at the next key." },
622
+ ],
623
+ });
438
624
 
439
625
  // Non-standard, but useful
440
- const someFunction = new LuaBuiltinFunction(async (_sf, value: any) => {
441
- switch (await luaTypeOf(value)) {
442
- case "number":
443
- if (!Number.isFinite(value)) return null;
444
- break;
445
- case "string":
446
- if (value.trim() === "") return null;
447
- break;
448
- case "table":
449
- if (luaKeys(value).length === 0) return null;
450
- }
451
- return value;
626
+ const someFunction = new LuaBuiltinFunction({
627
+ callback: async (_sf, value: any) => {
628
+ switch (await luaTypeOf(value)) {
629
+ case "number":
630
+ if (!Number.isFinite(value)) return null;
631
+ break;
632
+ case "string":
633
+ if (value.trim() === "") return null;
634
+ break;
635
+ case "table":
636
+ if (luaKeys(value).length === 0) return null;
637
+ }
638
+ return value;
639
+ },
640
+ description:
641
+ "Returns `nil` for empty Space Lua values and otherwise returns the value unchanged.",
642
+ parameters: [
643
+ {
644
+ name: "value",
645
+ description:
646
+ "Value to normalize; blank strings, empty tables, infinities, and NaN are empty.",
647
+ },
648
+ ],
649
+ returns: [{ description: "Original value or `nil`." }],
650
+ examples: [
651
+ {
652
+ code: 'print(some(" ") or "empty")\nprint(some({}) or "empty")\nprint(some(0))',
653
+ },
654
+ ],
452
655
  });
453
656
 
454
- const loadFunction = new LuaBuiltinFunction((sf, s) => luaLoad(s, sf));
657
+ const loadFunction = new LuaBuiltinFunction({
658
+ callback: (sf, s) => luaLoad(s, sf),
659
+ description:
660
+ "Compiles Lua source into a callable chunk without executing it.",
661
+ parameters: [
662
+ { name: "chunk", type: "string", description: "Lua source code." },
663
+ ],
664
+ returns: [
665
+ { type: "function|nil", description: "Compiled chunk or `nil`." },
666
+ { type: "string", description: "Compilation error when unsuccessful." },
667
+ ],
668
+ });
669
+
670
+ function annotateBuiltinApi(
671
+ value: unknown,
672
+ path: string,
673
+ page: string,
674
+ seen = new WeakSet<object>(),
675
+ ): void {
676
+ if (!value || typeof value !== "object" || seen.has(value)) return;
677
+ seen.add(value);
678
+ if (isILuaFunction(value)) {
679
+ value.info ??= { kind: "builtin" };
680
+ value.info.name ??= path;
681
+ value.info.see ??= page;
682
+ return;
683
+ }
684
+ if (value instanceof LuaTable) {
685
+ for (const key of value.keys()) {
686
+ if (typeof key !== "string") continue;
687
+ annotateBuiltinApi(value.rawGet(key), `${path}.${key}`, page, seen);
688
+ }
689
+ }
690
+ }
455
691
 
456
692
  export function luaBuildStandardEnv() {
457
693
  const env = new LuaEnv();
@@ -499,5 +735,11 @@ export function luaBuildStandardEnv() {
499
735
  env.set("crypto", cryptoApi);
500
736
  env.set("net", netApi);
501
737
  env.set("some", someFunction);
738
+
739
+ for (const name of env.keys()) {
740
+ const value = env.get(name);
741
+ const page = value instanceof LuaTable ? `API/${name}` : "API/global";
742
+ annotateBuiltinApi(value, name, page);
743
+ }
502
744
  return env;
503
745
  }