@silverbulletmd/silverbullet 2.4.2 → 2.6.1
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 +19 -4
- package/client/asset_bundle/bundle.ts +3 -9
- package/client/data/datastore.ts +4 -5
- package/client/markdown_parser/constants.ts +5 -4
- package/client/plugos/hooks/code_widget.ts +3 -8
- package/client/plugos/hooks/command.ts +8 -8
- package/client/plugos/hooks/document_editor.ts +10 -15
- package/client/plugos/hooks/event.ts +33 -36
- package/client/plugos/hooks/mq.ts +17 -17
- package/client/plugos/hooks/plug_namespace.ts +3 -8
- package/client/plugos/hooks/slash_command.ts +13 -28
- package/client/plugos/hooks/syscall.ts +3 -3
- package/client/plugos/manifest_cache.ts +22 -15
- package/client/plugos/plug.ts +2 -6
- package/client/plugos/plug_compile.ts +79 -78
- package/client/plugos/protocol.ts +28 -28
- package/client/plugos/proxy_fetch.ts +7 -6
- package/client/plugos/sandboxes/web_worker_sandbox.ts +1 -1
- package/client/plugos/sandboxes/worker_sandbox.ts +18 -18
- package/client/plugos/syscalls/asset.ts +1 -3
- package/client/plugos/syscalls/code_widget.ts +1 -3
- package/client/plugos/syscalls/config.ts +1 -5
- package/client/plugos/syscalls/datastore.ts +1 -1
- package/client/plugos/syscalls/editor.ts +72 -69
- package/client/plugos/syscalls/event.ts +9 -12
- package/client/plugos/syscalls/fetch.ts +31 -23
- package/client/plugos/syscalls/index.ts +10 -1
- package/client/plugos/syscalls/jsonschema.ts +72 -32
- package/client/plugos/syscalls/language.ts +9 -5
- package/client/plugos/syscalls/markdown.ts +29 -7
- package/client/plugos/syscalls/mq.ts +4 -12
- package/client/plugos/syscalls/service_registry.ts +1 -4
- package/client/plugos/syscalls/shell.ts +2 -5
- package/client/plugos/syscalls/space.ts +1 -1
- package/client/plugos/syscalls/sync.ts +69 -60
- package/client/plugos/syscalls/system.ts +2 -3
- package/client/plugos/system.ts +6 -12
- package/client/plugos/worker_runtime.ts +12 -33
- package/client/space_lua/aggregates.ts +782 -0
- package/client/space_lua/ast.ts +42 -8
- package/client/space_lua/ast_narrow.ts +4 -2
- package/client/space_lua/eval.ts +886 -575
- package/client/space_lua/labels.ts +7 -12
- package/client/space_lua/liq_null.ts +6 -0
- package/client/space_lua/numeric.ts +5 -8
- package/client/space_lua/parse.ts +346 -120
- package/client/space_lua/query_collection.ts +926 -82
- package/client/space_lua/query_env.ts +26 -0
- package/client/space_lua/render_lua_markdown.ts +369 -0
- package/client/space_lua/rp.ts +5 -4
- package/client/space_lua/runtime.ts +288 -155
- package/client/space_lua/stdlib/format.ts +53 -39
- package/client/space_lua/stdlib/js.ts +3 -7
- package/client/space_lua/stdlib/load.ts +1 -3
- package/client/space_lua/stdlib/math.ts +84 -58
- package/client/space_lua/stdlib/net.ts +27 -17
- package/client/space_lua/stdlib/os.ts +81 -85
- package/client/space_lua/stdlib/pattern.ts +695 -0
- package/client/space_lua/stdlib/prng.ts +148 -0
- package/client/space_lua/stdlib/space_lua.ts +17 -23
- package/client/space_lua/stdlib/string.ts +102 -190
- package/client/space_lua/stdlib/string_pack.ts +490 -0
- package/client/space_lua/stdlib/table.ts +76 -16
- package/client/space_lua/stdlib.ts +53 -39
- package/client/space_lua/tonumber.ts +82 -42
- package/client/space_lua/util.ts +53 -15
- package/dist/plug-compile.js +55 -98
- package/package.json +27 -20
- package/plug-api/constants.ts +0 -32
- package/plug-api/lib/async.ts +20 -7
- package/plug-api/lib/crypto.ts +16 -17
- package/plug-api/lib/dates.ts +15 -7
- package/plug-api/lib/json.ts +11 -5
- package/plug-api/lib/limited_map.ts +1 -1
- package/plug-api/lib/native_fetch.ts +2 -0
- package/plug-api/lib/ref.ts +23 -23
- package/plug-api/lib/resolve.ts +7 -11
- package/plug-api/lib/tags.ts +13 -4
- package/plug-api/lib/transclusion.ts +10 -21
- package/plug-api/lib/tree.ts +165 -45
- package/plug-api/lib/yaml.ts +35 -25
- package/plug-api/syscalls/asset.ts +1 -1
- package/plug-api/syscalls/config.ts +1 -4
- package/plug-api/syscalls/editor.ts +15 -15
- package/plug-api/syscalls/jsonschema.ts +1 -3
- package/plug-api/syscalls/lua.ts +3 -9
- package/plug-api/syscalls/mq.ts +1 -4
- package/plug-api/syscalls/shell.ts +4 -1
- package/plug-api/syscalls/space.ts +3 -10
- package/plug-api/syscalls/system.ts +1 -4
- package/plug-api/syscalls/yaml.ts +2 -6
- package/plug-api/system_mock.ts +0 -1
- package/plug-api/types/client.ts +16 -1
- package/plug-api/types/event.ts +6 -4
- package/plug-api/types/manifest.ts +8 -9
- package/plugs/builtin_plugs.ts +2 -2
- package/client/plugos/sandboxes/deno_worker_sandbox.ts +0 -6
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { LuaEnv, luaGet, luaKeys, type LuaStackFrame } from "./runtime.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build an environment for evaluating per-item expressions in queries.
|
|
5
|
+
* Extracted to its own module to avoid circular imports between
|
|
6
|
+
* query_collection.ts and aggregates.ts.
|
|
7
|
+
*/
|
|
8
|
+
export function buildItemEnv(
|
|
9
|
+
objectVariable: string | undefined,
|
|
10
|
+
item: any,
|
|
11
|
+
env: LuaEnv,
|
|
12
|
+
sf: LuaStackFrame,
|
|
13
|
+
): LuaEnv {
|
|
14
|
+
const itemEnv = new LuaEnv(env);
|
|
15
|
+
if (!objectVariable) {
|
|
16
|
+
// Inject all item keys as variables
|
|
17
|
+
for (const key of luaKeys(item)) {
|
|
18
|
+
itemEnv.setLocal(key, luaGet(item, key, sf.astCtx ?? null, sf));
|
|
19
|
+
}
|
|
20
|
+
// As well as _
|
|
21
|
+
itemEnv.setLocal("_", item);
|
|
22
|
+
} else {
|
|
23
|
+
itemEnv.setLocal(objectVariable, item);
|
|
24
|
+
}
|
|
25
|
+
return itemEnv;
|
|
26
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultTransformer,
|
|
3
|
+
escapeRegularPipes,
|
|
4
|
+
jsonToMDTable,
|
|
5
|
+
} from "../markdown_renderer/result_render.ts";
|
|
6
|
+
import { isSqlNull } from "../space_lua/liq_null.ts";
|
|
7
|
+
import { isTaggedFloat } from "../space_lua/numeric.ts";
|
|
8
|
+
import { LuaTable, luaFormatNumber } from "../space_lua/runtime.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Applies some heuristics to figure out if a string should be rendered
|
|
12
|
+
* as a markdown block or inline markdown.
|
|
13
|
+
*/
|
|
14
|
+
export function isBlockMarkdown(s: string) {
|
|
15
|
+
if (s.includes("\n")) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return !!s.match(/[-*]\s+/);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isEmpty(v: any): boolean {
|
|
22
|
+
return v === undefined || v === null || isSqlNull(v);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isPlainObject(v: any): v is Record<string, any> {
|
|
26
|
+
return (
|
|
27
|
+
typeof v === "object" &&
|
|
28
|
+
v !== null &&
|
|
29
|
+
!Array.isArray(v) &&
|
|
30
|
+
v.constructor === Object
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatScalar(v: any): string {
|
|
35
|
+
if (isEmpty(v)) return "";
|
|
36
|
+
if (isTaggedFloat(v)) return luaFormatNumber(v.value, "float");
|
|
37
|
+
if (typeof v === "number") return luaFormatNumber(v);
|
|
38
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
39
|
+
return `${v}`.trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const emptyTable = "<table data-table-empty></table>";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build an HTML table from string headers and a row accessor.
|
|
46
|
+
* Each row is rendered by calling `getCell(rowIndex, header)`.
|
|
47
|
+
*/
|
|
48
|
+
function buildHtmlTable(
|
|
49
|
+
headers: string[],
|
|
50
|
+
rowCount: number,
|
|
51
|
+
getCell: (rowIndex: number, header: string) => any,
|
|
52
|
+
): string {
|
|
53
|
+
if (headers.length === 0) return emptyTable;
|
|
54
|
+
const parts: string[] = ["<table><thead><tr>"];
|
|
55
|
+
for (const h of headers) parts.push(`<th>${h}</th>`);
|
|
56
|
+
parts.push("</tr></thead><tbody>");
|
|
57
|
+
for (let i = 0; i < rowCount; i++) {
|
|
58
|
+
parts.push("<tr>");
|
|
59
|
+
for (const h of headers) parts.push(renderTd(getCell(i, h)));
|
|
60
|
+
parts.push("</tr>");
|
|
61
|
+
}
|
|
62
|
+
parts.push("</tbody></table>");
|
|
63
|
+
return parts.join("");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Collect the union of string keys across items. */
|
|
67
|
+
function collectHeaders<T>(
|
|
68
|
+
items: T[],
|
|
69
|
+
getKeys: (item: T) => Iterable<string>,
|
|
70
|
+
): string[] {
|
|
71
|
+
const set = new Set<string>();
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
for (const k of getKeys(item)) set.add(k);
|
|
74
|
+
}
|
|
75
|
+
return [...set];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Classification of a JS/Lua value that drives both the HTML and the
|
|
80
|
+
* clean-markdown renderers. Keeping this in one place ensures the two
|
|
81
|
+
* output paths stay in lock-step when new cases are added.
|
|
82
|
+
*/
|
|
83
|
+
export type Classified =
|
|
84
|
+
| { kind: "nil"; dataType: "nil" }
|
|
85
|
+
| {
|
|
86
|
+
kind: "scalar";
|
|
87
|
+
text: string;
|
|
88
|
+
dataType: "string" | "number" | "boolean";
|
|
89
|
+
}
|
|
90
|
+
| { kind: "emptyTable"; dataType: "table" }
|
|
91
|
+
| {
|
|
92
|
+
kind: "record";
|
|
93
|
+
headers: string[];
|
|
94
|
+
getCell: (header: string) => any;
|
|
95
|
+
dataType: "table";
|
|
96
|
+
}
|
|
97
|
+
| {
|
|
98
|
+
kind: "recordArray";
|
|
99
|
+
headers: string[];
|
|
100
|
+
rowCount: number;
|
|
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.
|
|
104
|
+
dataType: "table" | "list";
|
|
105
|
+
}
|
|
106
|
+
| { kind: "scalarArray"; items: any[]; dataType: "list" };
|
|
107
|
+
|
|
108
|
+
export function classifyResult(result: any): Classified {
|
|
109
|
+
if (isEmpty(result)) return { kind: "nil", dataType: "nil" };
|
|
110
|
+
if (typeof result === "string") {
|
|
111
|
+
return { kind: "scalar", text: result, dataType: "string" };
|
|
112
|
+
}
|
|
113
|
+
if (isTaggedFloat(result)) {
|
|
114
|
+
return {
|
|
115
|
+
kind: "scalar",
|
|
116
|
+
text: luaFormatNumber(result.value, "float"),
|
|
117
|
+
dataType: "number",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (typeof result === "number") {
|
|
121
|
+
return {
|
|
122
|
+
kind: "scalar",
|
|
123
|
+
text: luaFormatNumber(result),
|
|
124
|
+
dataType: "number",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (typeof result === "boolean") {
|
|
128
|
+
return {
|
|
129
|
+
kind: "scalar",
|
|
130
|
+
text: result ? "true" : "false",
|
|
131
|
+
dataType: "boolean",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (result instanceof LuaTable) {
|
|
136
|
+
if (result.empty()) return { kind: "emptyTable", dataType: "table" };
|
|
137
|
+
const keys = result.keys();
|
|
138
|
+
const arrayLen = result.length;
|
|
139
|
+
const hasStrKeys = keys.some((k) => typeof k === "string");
|
|
140
|
+
|
|
141
|
+
// Pure array
|
|
142
|
+
if (arrayLen > 0 && !hasStrKeys) {
|
|
143
|
+
const elements: any[] = [];
|
|
144
|
+
for (let i = 1; i <= arrayLen; i++) elements.push(result.rawGet(i));
|
|
145
|
+
if (elements.every((el) => el instanceof LuaTable)) {
|
|
146
|
+
const tables = elements as LuaTable[];
|
|
147
|
+
const headers = collectHeaders(tables, (t) => t.keys().map(String));
|
|
148
|
+
if (headers.length === 0) {
|
|
149
|
+
return { kind: "emptyTable", dataType: "table" };
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
kind: "recordArray",
|
|
153
|
+
headers,
|
|
154
|
+
rowCount: tables.length,
|
|
155
|
+
getCell: (i, h) => {
|
|
156
|
+
const key = /^\d+$/.test(h) ? Number(h) : h;
|
|
157
|
+
return tables[i].rawGet(key);
|
|
158
|
+
},
|
|
159
|
+
dataType: "list",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return { kind: "scalarArray", items: elements, dataType: "list" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Has string keys (record or mixed) — single-row table
|
|
166
|
+
const headers = keys.map(String);
|
|
167
|
+
return {
|
|
168
|
+
kind: "record",
|
|
169
|
+
headers,
|
|
170
|
+
getCell: (h) => {
|
|
171
|
+
const key = keys[headers.indexOf(h)];
|
|
172
|
+
return result.rawGet(key);
|
|
173
|
+
},
|
|
174
|
+
dataType: "table",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (Array.isArray(result)) {
|
|
179
|
+
if (result.length === 0) return { kind: "emptyTable", dataType: "table" };
|
|
180
|
+
if (result.every(isPlainObject)) {
|
|
181
|
+
const headers = collectHeaders(result, Object.keys);
|
|
182
|
+
return {
|
|
183
|
+
kind: "recordArray",
|
|
184
|
+
headers,
|
|
185
|
+
rowCount: result.length,
|
|
186
|
+
getCell: (i, h) => result[i][h],
|
|
187
|
+
dataType: "table",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return { kind: "scalarArray", items: result, dataType: "list" };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (isPlainObject(result)) {
|
|
194
|
+
if (Object.keys(result).length === 0) {
|
|
195
|
+
return { kind: "emptyTable", dataType: "table" };
|
|
196
|
+
}
|
|
197
|
+
const headers = Object.keys(result);
|
|
198
|
+
return {
|
|
199
|
+
kind: "record",
|
|
200
|
+
headers,
|
|
201
|
+
getCell: (h) => result[h],
|
|
202
|
+
dataType: "table",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { kind: "scalar", text: `${result}`, dataType: "string" };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Render any Lua/JS value to a markdown string (with embedded HTML
|
|
211
|
+
* for structured data like tables).
|
|
212
|
+
*
|
|
213
|
+
* Scalar lists are rendered as plain lines. Tables use HTML for
|
|
214
|
+
* full nesting support.
|
|
215
|
+
*
|
|
216
|
+
* The returned markdown can be fed through the markdown parser and
|
|
217
|
+
* renderer to produce final HTML, getting wiki links, hashtags,
|
|
218
|
+
* formatting etc. for free.
|
|
219
|
+
*/
|
|
220
|
+
export function renderResultToMarkdown(
|
|
221
|
+
result: any,
|
|
222
|
+
classified: Classified = classifyResult(result),
|
|
223
|
+
): {
|
|
224
|
+
markdown: string;
|
|
225
|
+
dataType: string;
|
|
226
|
+
} {
|
|
227
|
+
switch (classified.kind) {
|
|
228
|
+
case "nil":
|
|
229
|
+
return { markdown: "", dataType: "nil" };
|
|
230
|
+
case "scalar":
|
|
231
|
+
return { markdown: classified.text, dataType: classified.dataType };
|
|
232
|
+
case "emptyTable":
|
|
233
|
+
return { markdown: emptyTable, dataType: "table" };
|
|
234
|
+
case "record":
|
|
235
|
+
return {
|
|
236
|
+
markdown: buildHtmlTable(classified.headers, 1, (_i, h) =>
|
|
237
|
+
classified.getCell(h),
|
|
238
|
+
),
|
|
239
|
+
dataType: "table",
|
|
240
|
+
};
|
|
241
|
+
case "recordArray":
|
|
242
|
+
return {
|
|
243
|
+
markdown: buildHtmlTable(
|
|
244
|
+
classified.headers,
|
|
245
|
+
classified.rowCount,
|
|
246
|
+
classified.getCell,
|
|
247
|
+
),
|
|
248
|
+
dataType: classified.dataType,
|
|
249
|
+
};
|
|
250
|
+
case "scalarArray":
|
|
251
|
+
return {
|
|
252
|
+
markdown: renderArrayToMarkdown(classified.items),
|
|
253
|
+
dataType: "list",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Render any Lua/JS value as "clean" GFM-style markdown suitable for
|
|
260
|
+
* the Copy button. Tables render as pipe tables, scalar arrays as
|
|
261
|
+
* newline-joined lines, scalars as their plain text.
|
|
262
|
+
*
|
|
263
|
+
* Nested structures inside a table cell degrade to their Lua literal
|
|
264
|
+
* form (via `LuaTable.toStringAsync()` in `defaultTransformer`), since
|
|
265
|
+
* GFM table cells cannot contain block-level content.
|
|
266
|
+
*/
|
|
267
|
+
/**
|
|
268
|
+
* Cell transformer for the clean-markdown (copy) path. Renders:
|
|
269
|
+
* - `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),
|
|
273
|
+
* - everything else via `defaultTransformer` (which Lua-encodes nested
|
|
274
|
+
* tables and escapes pipes for scalars).
|
|
275
|
+
*/
|
|
276
|
+
function cleanCellTransformer(v: any, k: string): Promise<string> {
|
|
277
|
+
if (k === "ref") return Promise.resolve(`[[${v}]]`);
|
|
278
|
+
const c = classifyResult(v);
|
|
279
|
+
if (c.kind === "scalarArray") {
|
|
280
|
+
return Promise.resolve(
|
|
281
|
+
c.items
|
|
282
|
+
.map(formatScalar)
|
|
283
|
+
.map((s) => escapeRegularPipes(s.replaceAll("\n", " ")))
|
|
284
|
+
.join("<br/>"),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return defaultTransformer(v, k);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function renderResultToCleanMarkdown(
|
|
291
|
+
result: any,
|
|
292
|
+
classified: Classified = classifyResult(result),
|
|
293
|
+
): Promise<string> {
|
|
294
|
+
switch (classified.kind) {
|
|
295
|
+
case "nil":
|
|
296
|
+
return "";
|
|
297
|
+
case "scalar":
|
|
298
|
+
return classified.text;
|
|
299
|
+
case "emptyTable":
|
|
300
|
+
return "*(empty table)*";
|
|
301
|
+
case "record": {
|
|
302
|
+
const row: Record<string, any> = {};
|
|
303
|
+
for (const h of classified.headers) row[h] = classified.getCell(h);
|
|
304
|
+
return jsonToMDTable([row], cleanCellTransformer);
|
|
305
|
+
}
|
|
306
|
+
case "recordArray": {
|
|
307
|
+
const rows: Record<string, any>[] = [];
|
|
308
|
+
for (let i = 0; i < classified.rowCount; i++) {
|
|
309
|
+
const row: Record<string, any> = {};
|
|
310
|
+
for (const h of classified.headers) row[h] = classified.getCell(i, h);
|
|
311
|
+
rows.push(row);
|
|
312
|
+
}
|
|
313
|
+
return jsonToMDTable(rows, cleanCellTransformer);
|
|
314
|
+
}
|
|
315
|
+
case "scalarArray":
|
|
316
|
+
return classified.items.map(formatScalar).join("\n");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function luaTypeName(v: any): string | undefined {
|
|
321
|
+
if (isEmpty(v)) return undefined;
|
|
322
|
+
if (typeof v === "number" || isTaggedFloat(v)) return "number";
|
|
323
|
+
if (typeof v === "string") return "string";
|
|
324
|
+
if (typeof v === "boolean") return "boolean";
|
|
325
|
+
if (v instanceof LuaTable) {
|
|
326
|
+
return v.keys().some((k) => typeof k === "string") ? "table" : "array";
|
|
327
|
+
}
|
|
328
|
+
if (Array.isArray(v)) return "array";
|
|
329
|
+
if (isPlainObject(v)) return "table";
|
|
330
|
+
return "string";
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function renderTd(v: any): string {
|
|
334
|
+
if (isEmpty(v)) return "<td data-table-cell-empty></td>";
|
|
335
|
+
const type = luaTypeName(v);
|
|
336
|
+
const attr = type ? ` data-table-cell-type="${type}"` : "";
|
|
337
|
+
return `<td${attr}>${renderCellContent(v)}</td>`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function renderArrayToMarkdown(items: any[]): string {
|
|
341
|
+
return items.map((item) => renderCellContent(item)).join("\n");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function renderArrayToHtmlLines(items: any[]): string {
|
|
345
|
+
return items.map((item) => renderCellContent(item)).join("<br/>");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Render a value as cell content. Used inside HTML contexts (table
|
|
350
|
+
* cells, nested structures) where markdown block syntax won't be
|
|
351
|
+
* parsed, so scalar arrays are joined with `<br/>` rather than newlines.
|
|
352
|
+
*/
|
|
353
|
+
function renderCellContent(v: any): string {
|
|
354
|
+
const c = classifyResult(v);
|
|
355
|
+
switch (c.kind) {
|
|
356
|
+
case "nil":
|
|
357
|
+
return "";
|
|
358
|
+
case "scalar":
|
|
359
|
+
return c.text;
|
|
360
|
+
case "emptyTable":
|
|
361
|
+
return emptyTable;
|
|
362
|
+
case "record":
|
|
363
|
+
return buildHtmlTable(c.headers, 1, (_i, h) => c.getCell(h));
|
|
364
|
+
case "recordArray":
|
|
365
|
+
return buildHtmlTable(c.headers, c.rowCount, c.getCell);
|
|
366
|
+
case "scalarArray":
|
|
367
|
+
return renderArrayToHtmlLines(c.items);
|
|
368
|
+
}
|
|
369
|
+
}
|
package/client/space_lua/rp.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
export type RP<T> = T | Promise<T>;
|
|
4
4
|
|
|
5
|
-
// Returns true when v is a Promise or
|
|
5
|
+
// Returns true when v is a Promise or has a then function.
|
|
6
|
+
// Optimized: skip the property access for primitives (number, string, boolean, null, undefined).
|
|
6
7
|
export function isPromise<T>(v: RP<T>): v is Promise<T> {
|
|
7
|
-
return
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
return (
|
|
9
|
+
typeof v === "object" && v !== null && typeof (v as any).then === "function"
|
|
10
|
+
);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function rpThen<A, B>(v: RP<A>, f: (a: A) => RP<B>): RP<B> {
|