import { luaFormatNumber, LuaTable } from "../space_lua/runtime.ts"; import { isTaggedFloat } from "../space_lua/numeric.ts"; import { isSqlNull } from "../space_lua/query_collection.ts"; function escapeHtml(s: string): string { return s .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } // Render inline Markdown to HTML (caller provides the implementation) export type InlineRenderer = (text: string) => string; function defaultInlineRenderer(text: string): string { return escapeHtml(text); } function luaTypeName( v: any, ): "number" | "string" | "boolean" | "table" | "array" | undefined { if (v === undefined || v === null || isSqlNull(v)) return undefined; if (typeof v === "number" || isTaggedFloat(v)) return "number"; if (typeof v === "string") return "string"; if (typeof v === "boolean") return "boolean"; if (v instanceof LuaTable) { return hasStringKeys(v) ? "table" : "array"; } if (Array.isArray(v)) return "array"; if (isPlainObject(v)) return "table"; return "string"; } function isEmpty(v: any): boolean { return v === undefined || v === null || isSqlNull(v); } function isPlainObject(v: any): v is Record { return ( typeof v === "object" && v !== null && !Array.isArray(v) && v.constructor === Object ); } function hasStringKeys(tbl: LuaTable): boolean { for (const k of tbl.keys()) { if (typeof k === "string") return true; } return false; } function formatScalar(v: any): string { if (isEmpty(v)) return ""; if (isTaggedFloat(v)) return luaFormatNumber(v.value, "float"); if (typeof v === "number") return luaFormatNumber(v); if (typeof v === "boolean") return v ? "true" : "false"; return `${v}`; } /** * Render any Lua/JS value to an HTML string with data attributes. * Also sets data-type and data-empty on the outermost element when * used for widget display. */ export async function renderResultToHtml( result: any, renderInline: InlineRenderer = defaultInlineRenderer, ): Promise<{ html: string; dataType: string; }> { if (isEmpty(result)) { return { html: ``, dataType: "nil", }; } if (typeof result === "string") { return { html: renderInline(result), dataType: "string", }; } if (isTaggedFloat(result)) { return { html: escapeHtml(luaFormatNumber(result.value, "float")), dataType: "number", }; } if (typeof result === "number") { return { html: escapeHtml(luaFormatNumber(result)), dataType: "number", }; } if (typeof result === "boolean") { return { html: result ? "true" : "false", dataType: "boolean", }; } if (result instanceof LuaTable) { if (result.empty()) { return { html: `
`, dataType: "table", }; } const html = await renderLuaTableToHtml(result, renderInline); const type = hasStringKeys(result) ? "table" : "list"; return { html, dataType: type }; } if (Array.isArray(result)) { if (result.length === 0) { return { html: `
`, dataType: "table", }; } if (result.every(isPlainObject)) { return { html: await renderJsObjectArrayToHtml(result, renderInline), dataType: "table", }; } return { html: await renderJsArrayToHtml(result, renderInline), dataType: "list", }; } if (isPlainObject(result)) { if (Object.keys(result).length === 0) { return { html: `
`, dataType: "table", }; } return { html: await renderJsObjectArrayToHtml([result], renderInline), dataType: "table", }; } return { html: renderInline(`${result}`), dataType: "string", }; } async function renderLuaTableToHtml( tbl: LuaTable, renderInline: InlineRenderer, ): Promise { const keys = tbl.keys(); if (keys.length === 0) return `
`; const arrayLen = tbl.length; const stringKeys: string[] = []; for (const k of keys) { if (typeof k === "string") stringKeys.push(k); } const hasArrayPart = arrayLen > 0; const hasStrKeys = stringKeys.length > 0; // Pure array (render as `