@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.
- package/README.md +4 -4
- package/client/asset_bundle/bundle.ts +0 -1
- package/client/config.ts +343 -0
- package/client/markdown_parser/constants.ts +3 -1
- package/client/plugos/hooks/command.ts +0 -5
- package/client/plugos/hooks/event.ts +0 -6
- package/client/plugos/hooks/mq.ts +0 -3
- package/client/plugos/hooks/slash_command.ts +1 -7
- package/client/plugos/hooks/syscall.ts +0 -4
- package/client/plugos/manifest_cache.ts +0 -32
- package/client/plugos/plug.ts +0 -3
- package/client/plugos/plug_compile.ts +3 -13
- package/client/plugos/sandboxes/worker_sandbox.ts +19 -9
- package/client/plugos/syscalls/editor.ts +128 -53
- package/client/plugos/syscalls/event.ts +0 -2
- package/client/plugos/syscalls/fetch.ts +0 -4
- package/client/plugos/syscalls/icon.ts +46 -0
- package/client/plugos/syscalls/index.ts +33 -13
- package/client/plugos/syscalls/jsonschema.ts +0 -1
- package/client/plugos/syscalls/mq.ts +0 -1
- package/client/plugos/syscalls/navigator.ts +143 -0
- package/client/plugos/syscalls/search.ts +44 -0
- package/client/plugos/syscalls/shell.ts +0 -1
- package/client/plugos/syscalls/space.ts +250 -7
- package/client/plugos/syscalls/sync.ts +56 -4
- package/client/plugos/syscalls/system.ts +38 -0
- package/client/plugos/system.ts +7 -5
- package/client/plugos/worker_runtime.ts +0 -2
- package/client/space_lua/aggregates.ts +0 -16
- package/client/space_lua/ast.ts +0 -5
- package/client/space_lua/ast_narrow.ts +0 -5
- package/client/space_lua/budget.ts +113 -0
- package/client/space_lua/budget_ui.ts +49 -0
- package/client/space_lua/eval.ts +113 -88
- package/client/space_lua/labels.ts +0 -4
- package/client/space_lua/numeric.ts +0 -5
- package/client/space_lua/parse.ts +0 -17
- package/client/space_lua/quarantine.ts +127 -0
- package/client/space_lua/query_collection.ts +3 -47
- package/client/space_lua/query_env.ts +0 -2
- package/client/space_lua/render_lua_markdown.ts +2 -7
- package/client/space_lua/render_widget.ts +18 -1
- package/client/space_lua/runtime.ts +50 -70
- package/client/space_lua/stdlib/format.ts +0 -12
- package/client/space_lua/stdlib/js.ts +0 -2
- package/client/space_lua/stdlib/load.ts +0 -1
- package/client/space_lua/stdlib/math.ts +0 -7
- package/client/space_lua/stdlib/net.ts +0 -4
- package/client/space_lua/stdlib/os.ts +0 -10
- package/client/space_lua/stdlib/pattern.ts +10 -15
- package/client/space_lua/stdlib/space_lua.ts +1 -2
- package/client/space_lua/stdlib/string_pack.ts +0 -3
- package/client/space_lua/stdlib/table.ts +0 -5
- package/client/space_lua/stdlib.ts +18 -31
- package/client/space_lua/tonumber.ts +0 -9
- package/dist/plug-compile.js +2 -3
- package/package.json +13 -3
- package/plug-api/lib/async.ts +1 -6
- package/plug-api/lib/collation.ts +22 -0
- package/plug-api/lib/crypto.ts +16 -15
- package/plug-api/lib/dates.ts +33 -0
- package/plug-api/lib/fuzzy.ts +282 -0
- package/plug-api/lib/json.ts +0 -6
- package/plug-api/lib/limited_map.ts +0 -2
- package/plug-api/lib/link_write.ts +27 -0
- package/plug-api/lib/ref.ts +28 -5
- package/plug-api/lib/resolve.ts +0 -1
- package/plug-api/lib/resolve_path.ts +290 -0
- package/plug-api/lib/shortcut.ts +15 -0
- package/plug-api/lib/tags.ts +0 -3
- package/plug-api/lib/transclusion.ts +28 -1
- package/plug-api/lib/tree.ts +0 -5
- package/plug-api/lib/yaml.ts +3 -42
- package/plug-api/syscall.ts +1 -5
- package/plug-api/syscalls/config.ts +2 -2
- package/plug-api/syscalls/editor.ts +59 -213
- package/plug-api/syscalls/icon.ts +18 -0
- package/plug-api/syscalls/index.ts +22 -4
- package/plug-api/syscalls/search.ts +11 -0
- package/plug-api/syscalls/space.ts +122 -5
- package/plug-api/syscalls/sync.ts +23 -2
- package/plug-api/syscalls/system.ts +46 -0
- package/plug-api/syscalls.ts +2 -0
- package/plug-api/system_mock.ts +13 -0
- package/plug-api/types/client.ts +4 -3
- package/plug-api/types/datastore.ts +11 -2
- package/plug-api/types/index.ts +10 -0
- package/plug-api/types/profile.ts +13 -0
- package/plug-api/types/revisions.ts +67 -0
- package/plug-api/ui/description.ts +64 -0
- package/plug-api/ui/hover.ts +69 -0
- package/plug-api/ui/index.ts +58 -2
- package/plug-api/ui/scroll.ts +30 -0
- package/plug-api/ui/tree_model.ts +263 -0
- package/plug-api/ui/tree_types.ts +66 -0
- package/plug-api/ui/use_fit_collapse.ts +67 -0
- package/plugs/builtin_plugs.ts +0 -1
- package/plugs/index/types.ts +9 -0
- package/plug-api/lib/memory_cache.ts +0 -21
- /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
|
|
650
|
-
collation
|
|
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
|
|
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
|
|
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),
|