@silverbulletmd/silverbullet 2.10.0 → 2.11.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 (100) hide show
  1. package/README.md +4 -4
  2. package/client/asset_bundle/bundle.ts +0 -1
  3. package/client/config.ts +343 -0
  4. package/client/markdown_parser/constants.ts +3 -1
  5. package/client/plugos/hooks/command.ts +0 -5
  6. package/client/plugos/hooks/event.ts +0 -6
  7. package/client/plugos/hooks/mq.ts +0 -3
  8. package/client/plugos/hooks/slash_command.ts +1 -7
  9. package/client/plugos/hooks/syscall.ts +0 -4
  10. package/client/plugos/manifest_cache.ts +0 -32
  11. package/client/plugos/plug.ts +0 -3
  12. package/client/plugos/plug_compile.ts +3 -13
  13. package/client/plugos/sandboxes/worker_sandbox.ts +19 -9
  14. package/client/plugos/syscalls/editor.ts +128 -53
  15. package/client/plugos/syscalls/event.ts +0 -2
  16. package/client/plugos/syscalls/fetch.ts +0 -4
  17. package/client/plugos/syscalls/icon.ts +46 -0
  18. package/client/plugos/syscalls/index.ts +33 -13
  19. package/client/plugos/syscalls/jsonschema.ts +0 -1
  20. package/client/plugos/syscalls/mq.ts +0 -1
  21. package/client/plugos/syscalls/navigator.ts +143 -0
  22. package/client/plugos/syscalls/search.ts +44 -0
  23. package/client/plugos/syscalls/shell.ts +0 -1
  24. package/client/plugos/syscalls/space.ts +250 -7
  25. package/client/plugos/syscalls/sync.ts +56 -4
  26. package/client/plugos/syscalls/system.ts +38 -0
  27. package/client/plugos/system.ts +7 -5
  28. package/client/plugos/worker_runtime.ts +0 -2
  29. package/client/space_lua/aggregates.ts +0 -16
  30. package/client/space_lua/ast.ts +0 -5
  31. package/client/space_lua/ast_narrow.ts +0 -5
  32. package/client/space_lua/budget.ts +113 -0
  33. package/client/space_lua/budget_ui.ts +49 -0
  34. package/client/space_lua/eval.ts +113 -88
  35. package/client/space_lua/labels.ts +0 -4
  36. package/client/space_lua/numeric.ts +0 -5
  37. package/client/space_lua/parse.ts +0 -17
  38. package/client/space_lua/quarantine.ts +127 -0
  39. package/client/space_lua/query_collection.ts +3 -47
  40. package/client/space_lua/query_env.ts +0 -2
  41. package/client/space_lua/render_lua_markdown.ts +2 -7
  42. package/client/space_lua/render_widget.ts +18 -1
  43. package/client/space_lua/runtime.ts +50 -70
  44. package/client/space_lua/stdlib/format.ts +0 -12
  45. package/client/space_lua/stdlib/js.ts +0 -2
  46. package/client/space_lua/stdlib/load.ts +0 -1
  47. package/client/space_lua/stdlib/math.ts +0 -7
  48. package/client/space_lua/stdlib/net.ts +0 -4
  49. package/client/space_lua/stdlib/os.ts +0 -10
  50. package/client/space_lua/stdlib/pattern.ts +10 -15
  51. package/client/space_lua/stdlib/space_lua.ts +1 -2
  52. package/client/space_lua/stdlib/string_pack.ts +0 -3
  53. package/client/space_lua/stdlib/table.ts +0 -5
  54. package/client/space_lua/stdlib.ts +18 -31
  55. package/client/space_lua/tonumber.ts +0 -9
  56. package/dist/plug-compile.js +2 -3
  57. package/package.json +13 -3
  58. package/plug-api/lib/async.ts +1 -6
  59. package/plug-api/lib/collation.ts +22 -0
  60. package/plug-api/lib/crypto.ts +16 -15
  61. package/plug-api/lib/dates.ts +33 -0
  62. package/plug-api/lib/fuzzy.ts +282 -0
  63. package/plug-api/lib/json.ts +0 -6
  64. package/plug-api/lib/limited_map.ts +0 -2
  65. package/plug-api/lib/link_write.ts +27 -0
  66. package/plug-api/lib/ref.ts +28 -5
  67. package/plug-api/lib/resolve.ts +0 -1
  68. package/plug-api/lib/resolve_path.ts +290 -0
  69. package/plug-api/lib/shortcut.ts +15 -0
  70. package/plug-api/lib/tags.ts +0 -3
  71. package/plug-api/lib/transclusion.ts +28 -1
  72. package/plug-api/lib/tree.ts +0 -5
  73. package/plug-api/lib/yaml.ts +3 -42
  74. package/plug-api/syscall.ts +1 -5
  75. package/plug-api/syscalls/config.ts +2 -2
  76. package/plug-api/syscalls/editor.ts +59 -213
  77. package/plug-api/syscalls/icon.ts +18 -0
  78. package/plug-api/syscalls/index.ts +22 -4
  79. package/plug-api/syscalls/search.ts +11 -0
  80. package/plug-api/syscalls/space.ts +122 -5
  81. package/plug-api/syscalls/sync.ts +23 -2
  82. package/plug-api/syscalls/system.ts +46 -0
  83. package/plug-api/syscalls.ts +2 -0
  84. package/plug-api/system_mock.ts +13 -0
  85. package/plug-api/types/client.ts +4 -3
  86. package/plug-api/types/datastore.ts +11 -2
  87. package/plug-api/types/index.ts +10 -0
  88. package/plug-api/types/profile.ts +13 -0
  89. package/plug-api/types/revisions.ts +67 -0
  90. package/plug-api/ui/description.ts +64 -0
  91. package/plug-api/ui/hover.ts +69 -0
  92. package/plug-api/ui/index.ts +58 -2
  93. package/plug-api/ui/scroll.ts +30 -0
  94. package/plug-api/ui/tree_model.ts +263 -0
  95. package/plug-api/ui/tree_types.ts +66 -0
  96. package/plug-api/ui/use_fit_collapse.ts +67 -0
  97. package/plugs/builtin_plugs.ts +0 -1
  98. package/plugs/index/types.ts +9 -0
  99. package/plug-api/lib/memory_cache.ts +0 -21
  100. /package/plug-api/{ui → lib}/panel_styles.ts +0 -0
@@ -7,7 +7,6 @@ export interface LuaTaggedFloat {
7
7
  readonly isFloat: true;
8
8
  }
9
9
 
10
- // Pre-allocated singletons for float zeros
11
10
  const FLOAT_POS_ZERO: LuaTaggedFloat = { value: 0, isFloat: true };
12
11
  const FLOAT_NEG_ZERO: LuaTaggedFloat = { value: -0, isFloat: true };
13
12
 
@@ -159,7 +158,6 @@ export function coerceNumericPair(
159
158
  ): CoerceNumericPairResult {
160
159
  const forceFloat = op === "/" || op === "^";
161
160
 
162
- // Both plain numbers
163
161
  if (typeof a === "number" && typeof b === "number") {
164
162
  const lt = leftType ?? inferNumericType(a);
165
163
  const rt = rightType ?? inferNumericType(b);
@@ -175,7 +173,6 @@ export function coerceNumericPair(
175
173
  };
176
174
  }
177
175
 
178
- // One tagged float, one plain number
179
176
  if (typeof a === "number" && isTaggedFloat(b)) {
180
177
  return {
181
178
  left: a,
@@ -192,7 +189,6 @@ export function coerceNumericPair(
192
189
  };
193
190
  }
194
191
 
195
- // Both tagged floats
196
192
  if (isTaggedFloat(a) && isTaggedFloat(b)) {
197
193
  return {
198
194
  left: a.value,
@@ -201,7 +197,6 @@ export function coerceNumericPair(
201
197
  };
202
198
  }
203
199
 
204
- // General fallback
205
200
  const A = coerceNumeric(a, leftType);
206
201
  const B = coerceNumeric(b, rightType);
207
202
 
@@ -50,7 +50,6 @@ const customIndent = indentNodeProp.add({
50
50
  },
51
51
  });
52
52
 
53
- // Use the customIndent in your language support
54
53
  export const luaLanguage = LRLanguage.define({
55
54
  name: "space-lua",
56
55
  parser: parser.configure({
@@ -547,7 +546,6 @@ function functionBodyCapturesNames(
547
546
  return blockReferencesNames(body.block, check);
548
547
  }
549
548
 
550
- // Walk block using `exprReferencesNames` (inside a function body).
551
549
  function blockReferencesNames(block: LuaBlock, names: Set<string>): boolean {
552
550
  for (let i = 0; i < block.statements.length; i++) {
553
551
  if (statementReferencesNames(block.statements[i], names)) return true;
@@ -650,7 +648,6 @@ function statementReferencesNames(
650
648
  }
651
649
  }
652
650
 
653
- // Walk block looking for `FunctionDefinition` nodes that capture `names`.
654
651
  function blockCapturesNames(block: LuaBlock, names: Set<string>): boolean {
655
652
  for (let i = 0; i < block.statements.length; i++) {
656
653
  if (statementCapturesNames(block.statements[i], names)) return true;
@@ -743,8 +740,6 @@ function statementCapturesNames(s: LuaStatement, names: Set<string>): boolean {
743
740
  }
744
741
  }
745
742
 
746
- // At loop block level find `FunctionDefinition` and check if it
747
- // captures `names`.
748
743
  function exprCapturesNames(e: LuaExpression, names: Set<string>): boolean {
749
744
  if (!e) return false;
750
745
  switch (e.type) {
@@ -871,7 +866,6 @@ function parseBlockNode(t: ParseTree, ctx: ASTCtx): LuaBlock {
871
866
  case "Label": {
872
867
  hasLabel = true;
873
868
  hasLabelHere = true;
874
- // Duplicate labels in the same block are illegal
875
869
  const name = (s as any).name as string;
876
870
  if (!dup) {
877
871
  if (seen.has(name)) {
@@ -1207,7 +1201,6 @@ function parseStatement(t: ParseTree, ctx: ASTCtx): LuaStatement {
1207
1201
  case "break":
1208
1202
  return { type: "Break", ctx: context(t, ctx) };
1209
1203
  default:
1210
- // Gracefully ignore unknown empty nodes
1211
1204
  if (!t.children || t.children.length === 0) {
1212
1205
  return {
1213
1206
  type: "Semicolon",
@@ -1363,10 +1356,7 @@ function parseExpList(t: ParseTree, ctx: ASTCtx): LuaExpression[] {
1363
1356
 
1364
1357
  const delimiterRegex = /^(\[=*\[)([\s\S]*)(\]=*\])$/;
1365
1358
 
1366
- // In case of quoted strings, remove the quotes and unescape the string
1367
- // In case of a [[ type ]] literal string, remove the brackets
1368
1359
  function parseString(s: string): string {
1369
- // Handle long strings with delimiters
1370
1360
  const delimiterMatch = s.match(delimiterRegex);
1371
1361
  if (delimiterMatch) {
1372
1362
  let text = delimiterMatch[2];
@@ -1403,11 +1393,9 @@ function parseString(s: string): string {
1403
1393
  case "'":
1404
1394
  return "'"; // Single quote
1405
1395
  default:
1406
- // Handle hexadecimal \x00
1407
1396
  if (capture.startsWith("x")) {
1408
1397
  return String.fromCharCode(parseInt(capture.slice(1), 16));
1409
1398
  }
1410
- // Handle unicode \u{XXXX}
1411
1399
  if (capture.startsWith("u{")) {
1412
1400
  const codePoint = parseInt(capture.slice(2, -1), 16);
1413
1401
  return String.fromCodePoint(codePoint);
@@ -1435,7 +1423,6 @@ function parseExpression(t: ParseTree, ctx: ASTCtx): LuaExpression {
1435
1423
  const text = t.children![0].text!.toLowerCase();
1436
1424
  return {
1437
1425
  type: "Number",
1438
- // Use the integer parser fox 0x literals
1439
1426
  // biome-ignore lint/correctness/useParseIntRadix: hex strings need auto-detect radix
1440
1427
  value: text.includes("x") ? parseInt(text) : parseFloat(text),
1441
1428
  numericType: /[.eEpP]/.test(text) ? "float" : "int",
@@ -1651,7 +1638,6 @@ function parseQueryClause(t: ParseTree, ctx: ASTCtx): LuaQueryClause {
1651
1638
  }
1652
1639
  }
1653
1640
 
1654
- // Parse a single OrderBy node (shared by query OrderByClause and AggOrderBy)
1655
1641
  function parseOrderByNode(child: ParseTree, ctx: ASTCtx): LuaOrderBy {
1656
1642
  const kids = child.children!;
1657
1643
  let direction: "asc" | "desc" = "asc";
@@ -1685,7 +1671,6 @@ function parseOrderByNode(child: ParseTree, ctx: ASTCtx): LuaOrderBy {
1685
1671
  return ob;
1686
1672
  }
1687
1673
 
1688
- // Parse an AggOrderBy node into LuaOrderBy[]
1689
1674
  function parseAggOrderBy(t: ParseTree, ctx: ASTCtx): LuaOrderBy[] {
1690
1675
  if (t.type !== "AggOrderBy") {
1691
1676
  throw new Error(`Expected AggOrderBy, got ${t.type}`);
@@ -1699,7 +1684,6 @@ function parseAggOrderBy(t: ParseTree, ctx: ASTCtx): LuaOrderBy[] {
1699
1684
  return orderBy;
1700
1685
  }
1701
1686
 
1702
- // Parse function args, extracting AggOrderBy if present inside funcParams
1703
1687
  function parseFunctionArgsWithOrderBy(
1704
1688
  ts: ParseTree[],
1705
1689
  ctx: ASTCtx,
@@ -1808,7 +1792,6 @@ export function parseBlock(s: string, ctx: ASTCtx = {}): LuaBlock {
1808
1792
  const result = parseChunk(withoutComments(concreteTree), ctx);
1809
1793
  assignCommentsToBlocks(result, comments, s);
1810
1794
  attachFunctionDocumentation(result, comments, s);
1811
- // console.log("Parsed AST", JSON.stringify(result, null, 2));
1812
1795
  getBlockGotoMeta(result);
1813
1796
  return result;
1814
1797
  } catch (e: any) {
@@ -0,0 +1,127 @@
1
+ export const QUARANTINE_KEY = "spaceLuaQuarantine";
2
+
3
+ export type QuarantineStore = {
4
+ getItem(k: string): string | null;
5
+ setItem(k: string, v: string): void;
6
+ };
7
+
8
+ function defaultStore(): QuarantineStore {
9
+ return localStorage;
10
+ }
11
+
12
+ /**
13
+ * Dashboard can host multiple spaces on the same origin, sharing
14
+ * localStorage. Namespace by document.baseURI the same way boot.ts's
15
+ * cachedFetch does (and client.ts's deriveDbName), so one space's
16
+ * quarantine entries never collide with or get pruned by another's.
17
+ */
18
+ export function quarantineStorageKey(): string {
19
+ const namespace = typeof document !== "undefined" ? document.baseURI : "";
20
+ return `silverbullet.${namespace}.${QUARANTINE_KEY}`;
21
+ }
22
+
23
+ export function hashScript(source: string): string {
24
+ let h = 2166136261;
25
+ for (let i = 0; i < source.length; i++) {
26
+ h ^= source.charCodeAt(i);
27
+ h = Math.imul(h, 16777619);
28
+ }
29
+ return (h >>> 0).toString(36);
30
+ }
31
+
32
+ function read(store: QuarantineStore): Record<string, string> {
33
+ let raw: string | null;
34
+ try {
35
+ raw = store.getItem(quarantineStorageKey());
36
+ } catch {
37
+ return {};
38
+ }
39
+ if (!raw) {
40
+ return {};
41
+ }
42
+ try {
43
+ const parsed = JSON.parse(raw);
44
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
45
+ ? parsed
46
+ : {};
47
+ } catch {
48
+ return {};
49
+ }
50
+ }
51
+
52
+ function write(store: QuarantineStore, v: Record<string, string>): void {
53
+ try {
54
+ store.setItem(quarantineStorageKey(), JSON.stringify(v));
55
+ } catch {
56
+ // Quarantine is a convenience: a store that refuses to persist must never
57
+ // prevent scripts from loading.
58
+ }
59
+ }
60
+
61
+ export function isQuarantined(
62
+ ref: string,
63
+ source: string,
64
+ store: QuarantineStore = defaultStore(),
65
+ ): boolean {
66
+ const entries = read(store);
67
+ const stored = entries[ref];
68
+ if (stored === undefined) {
69
+ return false;
70
+ }
71
+ if (stored === hashScript(source)) {
72
+ return true;
73
+ }
74
+ delete entries[ref];
75
+ write(store, entries);
76
+ return false;
77
+ }
78
+
79
+ export function quarantine(
80
+ ref: string,
81
+ source: string,
82
+ store: QuarantineStore = defaultStore(),
83
+ ): void {
84
+ const entries = read(store);
85
+ entries[ref] = hashScript(source);
86
+ write(store, entries);
87
+ }
88
+
89
+ export function unquarantine(
90
+ ref: string,
91
+ store: QuarantineStore = defaultStore(),
92
+ ): void {
93
+ const entries = read(store);
94
+ delete entries[ref];
95
+ write(store, entries);
96
+ }
97
+
98
+ export function listQuarantined(
99
+ store: QuarantineStore = defaultStore(),
100
+ ): string[] {
101
+ return Object.keys(read(store));
102
+ }
103
+
104
+ /**
105
+ * Drops quarantine entries whose ref no longer appears among the live
106
+ * scripts. A ref changes whenever the script it belongs to moves (e.g. an
107
+ * edit shifts its page@charOffset), so the hash-mismatch self-heal in
108
+ * isQuarantined never revisits the old ref to prune it. Call this with the
109
+ * full live ref list before quarantine state is read for display.
110
+ */
111
+ export function reconcileQuarantine(
112
+ liveRefs: string[],
113
+ store: QuarantineStore = defaultStore(),
114
+ ): void {
115
+ const entries = read(store);
116
+ const live = new Set(liveRefs);
117
+ let changed = false;
118
+ for (const ref of Object.keys(entries)) {
119
+ if (!live.has(ref)) {
120
+ delete entries[ref];
121
+ changed = true;
122
+ }
123
+ }
124
+ if (changed) {
125
+ write(store, entries);
126
+ }
127
+ }
@@ -33,6 +33,7 @@ import type { DataStore } from "../data/datastore.ts";
33
33
  import type { KvPrimitives } from "../data/kv_primitives.ts";
34
34
 
35
35
  import type { QueryCollationConfig } from "../../plug-api/types/config.ts";
36
+ import { compareCollated } from "../../plug-api/lib/collation.ts";
36
37
 
37
38
  import type { KvKey } from "../../plug-api/types/datastore.ts";
38
39
 
@@ -77,7 +78,6 @@ function buildGroupItemEnv(
77
78
  itemEnv.setLocal("group", groupVal);
78
79
  }
79
80
 
80
- // Unpack named fields from multi-key LuaTable keys
81
81
  if (keyVal instanceof LuaTable) {
82
82
  for (const k of luaKeys(keyVal)) {
83
83
  if (typeof k !== "string") continue;
@@ -85,12 +85,8 @@ function buildGroupItemEnv(
85
85
  }
86
86
  }
87
87
 
88
- // Bind all `group by` aliases/names to their key values. For
89
- // single key bind the name to the scalar `keyVal`. For multi-key
90
- // bind each name to the field from the key table.
91
88
  if (groupByNames && groupByNames.length > 0) {
92
89
  if (!(keyVal instanceof LuaTable)) {
93
- // Bind all names to scalar
94
90
  for (const gbn of groupByNames) {
95
91
  itemEnv.setLocal(gbn, keyVal);
96
92
  }
@@ -109,8 +105,6 @@ function buildGroupItemEnv(
109
105
  }
110
106
 
111
107
  /**
112
- * Build an environment for evaluating per-item expressions in queries.
113
- *
114
108
  * When `objectVariable` is NOT set: item fields are unpacked as locals
115
109
  * and shadow any globals. The item is also bound to `_`.
116
110
  *
@@ -128,7 +122,6 @@ function buildItemEnvLocal(
128
122
  if (objectVariable) {
129
123
  itemEnv.setLocal(objectVariable, item);
130
124
  } else {
131
- // Unpack item fields as locals so unqualified access works
132
125
  itemEnv.setLocal("_", item);
133
126
  if (item instanceof LuaTable) {
134
127
  for (const key of luaKeys(item)) {
@@ -157,26 +150,15 @@ export type LuaGroupByEntry = {
157
150
  alias?: string;
158
151
  };
159
152
 
160
- /**
161
- * Represents a query for a collection
162
- */
163
153
  export type LuaCollectionQuery = {
164
154
  objectVariable?: string;
165
- // The filter expression evaluated with Lua
166
155
  where?: LuaExpression;
167
- // The order by expression evaluated with Lua
168
156
  orderBy?: LuaOrderBy[];
169
- // The select expression evaluated with Lua
170
157
  select?: LuaExpression;
171
- // The limit of the query
172
158
  limit?: number;
173
- // The offset of the query
174
159
  offset?: number;
175
- // Whether to return only distinct values
176
160
  distinct?: boolean;
177
- // The group by entries evaluated with Lua
178
161
  groupBy?: LuaGroupByEntry[];
179
- // The having expression evaluated with Lua
180
162
  having?: LuaExpression;
181
163
  };
182
164
 
@@ -189,9 +171,6 @@ export interface LuaQueryCollection {
189
171
  ): Promise<any[]>;
190
172
  }
191
173
 
192
- /**
193
- * Implements a query collection for a regular JavaScript array
194
- */
195
174
  export class ArrayQueryCollection<T> implements LuaQueryCollection {
196
175
  constructor(private readonly array: T[]) {}
197
176
  query(
@@ -204,7 +183,6 @@ export class ArrayQueryCollection<T> implements LuaQueryCollection {
204
183
  }
205
184
  }
206
185
 
207
- // Wrap any object, array, or LuaQueryCollection as a queryable collection
208
186
  export function toCollection(obj: any): LuaQueryCollection {
209
187
  if (
210
188
  obj instanceof ArrayQueryCollection ||
@@ -515,7 +493,6 @@ function resolveUsing(
515
493
  return new LuaFunction(using, env);
516
494
  }
517
495
 
518
- // Compare values using a custom comparator with SWO violation detection
519
496
  async function usingCompare(
520
497
  luaCmp: LuaValue,
521
498
  aVal: any,
@@ -621,7 +598,6 @@ async function sortKeyCompare(
621
598
  const aVal = aKeys[idx];
622
599
  const bVal = bKeys[idx];
623
600
 
624
- // Handle nulls positioning
625
601
  const aIsNull = aVal === null || aVal === undefined || isSqlNull(aVal);
626
602
  const bIsNull = bVal === null || bVal === undefined || isSqlNull(bVal);
627
603
  if (aIsNull || bIsNull) {
@@ -646,25 +622,16 @@ async function sortKeyCompare(
646
622
  idx,
647
623
  );
648
624
  if (cmp !== 0) return cmp;
649
- } else if (
650
- collation?.enabled &&
651
- typeof aVal === "string" &&
652
- typeof bVal === "string"
653
- ) {
654
- const order = collator.compare(aVal, bVal);
625
+ } else {
626
+ const order = compareCollated(aVal, bVal, collation, collator);
655
627
  if (order !== 0) {
656
628
  return desc ? -order : order;
657
629
  }
658
- } else if (aVal < bVal) {
659
- return desc ? 1 : -1;
660
- } else if (aVal > bVal) {
661
- return desc ? -1 : 1;
662
630
  }
663
631
  }
664
632
  return 0;
665
633
  }
666
634
 
667
- // Build a select-result table from a non-aggregate select expression
668
635
  async function evalSelectExpression(
669
636
  selectExpr: LuaExpression,
670
637
  itemEnv: LuaEnv,
@@ -711,14 +678,11 @@ export async function applyQuery(
711
678
 
712
679
  const grouped = !!query.groupBy;
713
680
 
714
- // Collect group-by key names for unpacking into the post-group environment.
715
681
  let groupByNames: string[] | undefined;
716
682
 
717
683
  if (query.groupBy) {
718
- // Extract expressions and names from `group by` entries
719
684
  const groupByEntries = query.groupBy;
720
685
 
721
- // Derive canonical name (explicit alias first, or from expression)
722
686
  groupByNames = groupByEntries
723
687
  .map((entry) => {
724
688
  if (entry.alias) return entry.alias;
@@ -740,7 +704,6 @@ export async function applyQuery(
740
704
  const entry = groupByEntries[ei];
741
705
  const v = await evalExpression(entry.expr, itemEnv, sf);
742
706
  keyParts.push(v);
743
- // Use alias if provided, or from expression
744
707
  const name =
745
708
  entry.alias ??
746
709
  (entry.expr.type === "Variable" ? entry.expr.name : undefined) ??
@@ -769,11 +732,9 @@ export async function applyQuery(
769
732
  keyVal = keyParts[0];
770
733
  } else {
771
734
  const kt = new LuaTable();
772
- // Always populate array indices from keyParts
773
735
  for (let i = 0; i < keyParts.length; i++) {
774
736
  kt.rawSetArrayIndex(i + 1, keyParts[i]);
775
737
  }
776
- // Additionally set named fields for Variable/PropertyAccess exprs
777
738
  for (const name in keyRecord) {
778
739
  void kt.rawSet(name, keyRecord[name]);
779
740
  }
@@ -847,7 +808,6 @@ export async function applyQuery(
847
808
 
848
809
  let selectResults: any[] | undefined;
849
810
 
850
- // Pre-compute select for grouped + ordered queries
851
811
  if (grouped && query.select && query.orderBy) {
852
812
  const selectExpr = query.select;
853
813
  selectResults = [];
@@ -879,7 +839,6 @@ export async function applyQuery(
879
839
  violated.push(false);
880
840
  }
881
841
 
882
- // Decorate: pre-compute all sort keys once (Schwartzian transform)
883
842
  const sortKeys = await precomputeSortKeys(
884
843
  results,
885
844
  query.orderBy,
@@ -892,10 +851,8 @@ export async function applyQuery(
892
851
  config,
893
852
  );
894
853
 
895
- // Tag each result with its original index for stable sorting
896
854
  const tagged = results.map((val, idx) => ({ val, idx }));
897
855
 
898
- // Sort: compare cached keys only, no Lua eval in comparator
899
856
  await asyncMergeSort(tagged, (a, b) =>
900
857
  sortKeyCompare(
901
858
  a,
@@ -911,7 +868,6 @@ export async function applyQuery(
911
868
  ),
912
869
  );
913
870
 
914
- // Check for SWO violations in comparators
915
871
  for (let i = 0; i < violated.length; i++) {
916
872
  if (violated[i]) {
917
873
  throw new LuaRuntimeError(
@@ -13,11 +13,9 @@ export function buildItemEnv(
13
13
  ): LuaEnv {
14
14
  const itemEnv = new LuaEnv(env);
15
15
  if (!objectVariable) {
16
- // Inject all item keys as variables
17
16
  for (const key of luaKeys(item)) {
18
17
  itemEnv.setLocal(key, luaGet(item, key, sf.astCtx ?? null, sf));
19
18
  }
20
- // As well as _
21
19
  itemEnv.setLocal("_", item);
22
20
  } else {
23
21
  itemEnv.setLocal(objectVariable, item);
@@ -99,8 +99,7 @@ export type Classified =
99
99
  headers: string[];
100
100
  rowCount: number;
101
101
  getCell: (rowIndex: number, header: string) => any;
102
- // LuaTable record-arrays are historically tagged as "list", while
103
- // JS record-arrays are tagged as "table". Preserved for compatibility.
102
+ // Compatibility contract: LuaTable record-arrays use "list"; JS arrays use "table".
104
103
  dataType: "table" | "list";
105
104
  }
106
105
  | { kind: "scalarArray"; items: any[]; dataType: "list" };
@@ -138,7 +137,6 @@ export function classifyResult(result: any): Classified {
138
137
  const arrayLen = result.length;
139
138
  const hasStrKeys = keys.some((k) => typeof k === "string");
140
139
 
141
- // Pure array
142
140
  if (arrayLen > 0 && !hasStrKeys) {
143
141
  const elements: any[] = [];
144
142
  for (let i = 1; i <= arrayLen; i++) elements.push(result.rawGet(i));
@@ -162,7 +160,6 @@ export function classifyResult(result: any): Classified {
162
160
  return { kind: "scalarArray", items: elements, dataType: "list" };
163
161
  }
164
162
 
165
- // Has string keys (record or mixed) — single-row table
166
163
  const headers = keys.map(String);
167
164
  return {
168
165
  kind: "record",
@@ -267,9 +264,7 @@ export function renderResultToMarkdown(
267
264
  /**
268
265
  * Cell transformer for the clean-markdown (copy) path. Renders:
269
266
  * - `ref` columns as wiki links,
270
- * - scalar arrays as `<br/>`-joined lines (mirrors the HTML display
271
- * path, and relies on the markdown renderer now handling self-closing
272
- * `<br/>` inside GFM table cells),
267
+ * - scalar arrays as `<br/>`-joined lines inside GFM table cells,
273
268
  * - everything else via `defaultTransformer` (which Lua-encodes nested
274
269
  * tables and escapes pipes for scalars).
275
270
  */
@@ -11,6 +11,11 @@ import {
11
11
  luaValueToJS,
12
12
  singleResult,
13
13
  } from "./runtime.ts";
14
+ import {
15
+ BUSY_LIMIT_DEFAULT_MS,
16
+ LuaBudgetStopped,
17
+ makeLuaBudget,
18
+ } from "./budget.ts";
14
19
  import { isTaggedFloat } from "./numeric.ts";
15
20
  import {
16
21
  encodeRef,
@@ -18,6 +23,7 @@ import {
18
23
  } from "@silverbulletmd/silverbullet/lib/ref";
19
24
  import { resolveASTReference } from "../space_lua.ts";
20
25
  import type { Client } from "../client.ts";
26
+ import { isViewValue } from "../navigator/view_value.ts";
21
27
  import {
22
28
  classifyResult,
23
29
  renderResultToCleanMarkdown,
@@ -48,6 +54,12 @@ export async function renderLuaWidgetResult(
48
54
  client.clientSystem.spaceLuaEnv.env,
49
55
  ctx,
50
56
  );
57
+ sf.threadState.budget = makeLuaBudget({
58
+ busyLimitMs: BUSY_LIMIT_DEFAULT_MS,
59
+ onLimit: (b) => {
60
+ b.stopped = true;
61
+ },
62
+ });
51
63
  const env = new LuaEnv(client.clientSystem.spaceLuaEnv.env);
52
64
  env.setLocal("_CTX", tl);
53
65
  const rawResult = singleResult(await compute(env, sf));
@@ -62,6 +74,9 @@ export async function renderLuaWidgetResult(
62
74
  }
63
75
  return luaValueToJS(rawResult, sf);
64
76
  } catch (e: any) {
77
+ if (e instanceof LuaBudgetStopped) {
78
+ return `**Lua timeout:** this widget took too long to render and was stopped. Reload the page to try again.`;
79
+ }
65
80
  if (e instanceof LuaRuntimeError && e.sf?.astCtx) {
66
81
  const source = resolveASTReference(e.sf.astCtx);
67
82
  if (source) {
@@ -128,6 +143,9 @@ export async function expressionToPortableMarkdown(
128
143
  expressionText,
129
144
  currentPageMeta,
130
145
  );
146
+ if (isViewValue(rawResult)) {
147
+ return { ok: false, reason: "A view has no portable Markdown rendering" };
148
+ }
131
149
  if (rawResult === null || rawResult === undefined) {
132
150
  return { ok: true, markdown: "" };
133
151
  }
@@ -147,7 +165,6 @@ export async function expressionToPortableMarkdown(
147
165
  }
148
166
  return { ok: false, reason: "html-only widget (no markdown rendering)" };
149
167
  }
150
- // Scalars, tables, arrays, plain strings → clean GFM markdown.
151
168
  const markdown = await renderResultToCleanMarkdown(
152
169
  rawResult,
153
170
  classifyResult(rawResult),