From 2aaa797bda01bd602b22f565b29ece48efc88266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Jan=20Fialka?= Date: Mon, 9 Mar 2026 11:45:04 +0100 Subject: [PATCH] Improve Lua Widget Rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lua `${...}` expression results are rendered using two parallel paths: - **HTML path** (display path) for visual display in the editor and - **Markdown path** (copy path) for the *copy button* output. Every Lua value is classified as one of: - **Scalar:** number, string, boolean, or `nil` - **Empty table:** `{}` - **Array:** a `LuaTable` with only sequential integer keys (no string keys) - **Array of tables:** an array where every element is a `LuaTable` - **Record:** a `LuaTable` with any string key (may also have integer keys) Scalars are rendered as inline Markdown - wiki links, tags, bold, and other inline syntax are resolved, `nil` renders as ``. Empty tables render as ``. An array of tables renders as a multi-row `
`. Column headers are the union of all element keys. Each cell value recurses through the same classification. An array of scalars (or a mix of scalars and tables) renders as `
` with one column per key. Cell values recurse. Every ``; +} + +async function renderLiHtml( + v: any, + renderInline: InlineRenderer, +): Promise { + const type = luaTypeName(v); + const attrs: string[] = []; + if (type) { + attrs.push(`data-list-item-type="${type}"`); + } + if (isEmpty(v)) { + attrs.push("data-list-item-empty"); + } + const attrStr = attrs.length > 0 ? " " + attrs.join(" ") : ""; + const content = await renderCellContent(v, renderInline); + return `${content}`; +} + +// Render a value content (recurse for nested tables/arrays) +function renderCellContent( + v: any, + renderInline: InlineRenderer, +): Promise { + if (isEmpty(v)) return Promise.resolve(""); + if (v instanceof LuaTable) { + if (v.empty()) return Promise.resolve(`
` carries `data-table-cell-type` and optionally `data-table-cell-empty`. Every `
  • ` carries `data-list-item-type` and optionally `data-list-item-empty`. The outermost element carries `data-type`. These attributes allow CSS styling of rendered widgets. Nesting is **unlimited**. Tables inside cells, lists inside cells, tables inside list items all render correctly in HTML. Scalars render as plain text. Lua `nil` renders as `nil`. Empty tables render as `*(empty table)*`. An array of tables renders as a pipe-delimited Markdown table. Nested values in cells are rendered as Lua literals via `toStringAsync()` (e.g. `{1, 2, 3}` or `{a = 1}`), since Markdown table cells cannot contain block-level content. An array of scalars renders as a Markdown list (`- item`). When a list item is itself multi-line (a sublist) it uses an empty bullet with two-space indented content on the next line: ``` - 1 - 2 - - nested 1 - nested 2 ``` Nested tables or arrays **beyond the first list nesting** level are rendered as Lua literals. A record renders as a single-row Markdown table. Column headers follow the order returned by `LuaTable.keys`: string keys in their insertion order, followed by integer keys in ascending order. For example, `{a = 1, b = 2, 3, d = 4}` produces columns `a | b | d | 1`. This is consequence of `LuaTable` storing string keys and the array part separately (for performance). Arrays mixing plain scalars and record-like tables (e.g. `{1, 2, 3, {a=3}}`) were previously rendered as a single flat table due to an internal JS conversion (`toJS`). They now render as a list, with record elements shown as nested tables - which more accurately reflects their Lua structure. This is the only divergence from the original presentation. To restore the old tabular layout, wrap all elements as records with a consistent key: ```lua -- Before (was a flat table, now a list with nested table): ${{1, 2, 3, {a=3}}} -- After (explicit records, renders as a table): ${{{value=1}, {value=2}, {value=3}, {a=3}}} ``` Signed-off-by: Matouš Jan Fialka --- client/codemirror/lua_directive.ts | 7 + client/codemirror/lua_widget.ts | 109 ++++--- client/markdown_renderer/result_render.ts | 172 ++++++++++- client/space_lua/render_lua_html.ts | 332 ++++++++++++++++++++++ 4 files changed, 557 insertions(+), 63 deletions(-) create mode 100644 client/space_lua/render_lua_html.ts diff --git a/client/codemirror/lua_directive.ts b/client/codemirror/lua_directive.ts index 2a677d28..5c5320d8 100644 --- a/client/codemirror/lua_directive.ts +++ b/client/codemirror/lua_directive.ts @@ -14,6 +14,7 @@ import { LuaEnv, LuaRuntimeError, LuaStackFrame, + LuaTable, luaValueToJS, singleResult, } from "../space_lua/runtime.ts"; @@ -123,6 +124,12 @@ export function luaDirectivePlugin(client: Client) { ) { return rawResult; } + if (rawResult instanceof LuaTable) { + if (rawResult.rawGet("_isWidget")) { + return luaValueToJS(rawResult, sf); + } + return rawResult; + } // everything else needs luaValueToJS for widget support return luaValueToJS(rawResult, sf); } catch (e: any) { diff --git a/client/codemirror/lua_widget.ts b/client/codemirror/lua_widget.ts index eff36158..1e9ddb5c 100644 --- a/client/codemirror/lua_widget.ts +++ b/client/codemirror/lua_widget.ts @@ -13,15 +13,13 @@ import { moveCursorIntoText, } from "./widget_util.ts"; import { expandMarkdown } from "../markdown_renderer/inline.ts"; -import { luaFormatNumber, LuaTable } from "../space_lua/runtime.ts"; -import { isTaggedFloat } from "../space_lua/numeric.ts"; import { isBlockMarkdown, - jsonToMDTable, - refCellTransformer, + renderExpressionResult, } from "../markdown_renderer/result_render.ts"; import { activeWidgets } from "./code_widget.ts"; import type { Ref } from "@silverbulletmd/silverbullet/lib/ref"; +import { renderResultToHtml } from "../space_lua/render_lua_html.ts"; export type LuaWidgetCallback = ( bodyText: string, @@ -95,6 +93,26 @@ export class LuaWidget extends WidgetType { return wrapperSpan; } + // Build an inline Markdown renderer bound to the current client context + private buildInlineRenderer(): (text: string) => string { + return (text: string): string => { + const mdTree = parse(extendedMarkdownLanguage, text); + return renderMarkdownToHtml(mdTree, { + shortWikiLinks: this.client.config.get("shortWikiLinks", false), + translateUrls: (url) => { + if (isLocalURL(url)) { + url = resolveMarkdownLink( + this.client.currentName(), + decodeURI(url), + ); + } + return url; + }, + preserveAttributes: true, + }, this.client.ui.viewState.allPages); + }; + } + async renderContent( div: HTMLElement, ) { @@ -121,38 +139,53 @@ export class LuaWidget extends WidgetType { let block = false; let copyContent: string | undefined = undefined; - // Normalization + // Normalization (non-widget results go through dual-path rendering) if (typeof widgetContent === "string" || !widgetContent._isWidget) { - // Apply heuristic to render the object as a markdown table + // HTML path for display (with data attributes, nested structures) + const inlineRenderer = this.buildInlineRenderer(); + const { html: displayHtml, dataType } = await renderResultToHtml( + widgetContent, + inlineRenderer, + ); + // Markdown path for copy button (flat tables, `{...}` for nested) + const markdownCopy = await renderExpressionResult(widgetContent); + widgetContent = { _isWidget: true, - markdown: await renderExpressionResult(widgetContent), + html: `${displayHtml}`, + display: (dataType === "table" || dataType === "list") + ? "block" + : "inline", }; + copyContent = markdownCopy; } - if (widgetContent.cssClasses) { - div.className = widgetContent.cssClasses.join(" "); + // After normalization `widgetContent` is always the object form + const wc = widgetContent as Exclude; + + if (wc.cssClasses) { + div.className = wc.cssClasses.join(" "); } - if (widgetContent.html) { - if (typeof widgetContent.html === "string") { - html = parseHtmlString(widgetContent.html); - copyContent = widgetContent.html; + if (wc.html) { + if (typeof wc.html === "string") { + html = parseHtmlString(wc.html); + if (!copyContent) copyContent = wc.html; } else { - html = widgetContent.html; - copyContent = widgetContent.html.outerHTML; + html = wc.html; + if (!copyContent) copyContent = wc.html.outerHTML; } - block = widgetContent.display === "block"; + block = wc.display === "block"; if (block) { div.className += " sb-lua-directive-block"; } else { div.className += " sb-lua-directive-inline"; } } - if (widgetContent.markdown) { + if (wc.markdown) { let mdTree = parse( extendedMarkdownLanguage, - widgetContent.markdown || "", + wc.markdown || "", ); mdTree = await expandMarkdown( @@ -176,7 +209,7 @@ export class LuaWidget extends WidgetType { return; } - block = widgetContent._isWidget && widgetContent.display === "block" || + block = wc._isWidget && wc.display === "block" || isBlockMarkdown(trimmedMarkdown); if (block) { div.className += " sb-lua-directive-block"; @@ -211,7 +244,7 @@ export class LuaWidget extends WidgetType { div, this.client, this.inPage ? this.codeText : undefined, - widgetContent._isWidget && widgetContent.events, + wc._isWidget && wc.events, ); } @@ -343,42 +376,6 @@ export class LuaWidget extends WidgetType { } } -export function renderExpressionResult(result: any): Promise { - if (result instanceof LuaTable) { - result = result.toJS(); - } - // Must check before object/array checks — tagged floats are plain objects - if (isTaggedFloat(result)) { - return Promise.resolve(luaFormatNumber(result.value, "float")); - } - if (typeof result === "number") { - return Promise.resolve(luaFormatNumber(result)); - } - if ( - Array.isArray(result) && result.length > 0 && typeof result[0] === "object" - ) { - // If result is an array of objects, render as a Markdown table - try { - return jsonToMDTable(result, refCellTransformer); - } catch (e: any) { - console.error( - `Error rendering expression directive: ${e.message} for value ${ - JSON.stringify(result) - }`, - ); - return Promise.resolve(JSON.stringify(result)); - } - } else if (typeof result === "object" && result.constructor === Object) { - // If result is a plain object, render as a Markdown table - return jsonToMDTable([result], refCellTransformer); - } else if (Array.isArray(result)) { - // Not-object array, let's render it as a Markdown list - return Promise.resolve(result.map((item) => `- ${item}`).join("\n")); - } else { - return Promise.resolve("" + result); - } -} - export function parseHtmlString(html: string): HTMLElement { const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); diff --git a/client/markdown_renderer/result_render.ts b/client/markdown_renderer/result_render.ts index 36496f86..ae828b99 100644 --- a/client/markdown_renderer/result_render.ts +++ b/client/markdown_renderer/result_render.ts @@ -13,6 +13,10 @@ export function defaultTransformer(v: any, _k: string): Promise { if (typeof v === "string") { return Promise.resolve(escapeRegularPipes(v.replaceAll("\n", " "))); } + if (v instanceof LuaTable) { + // Render nested `LuaTables` as literal `{...}` for copy path + return v.toStringAsync().then((s) => escapeRegularPipes(s)); + } if (v && typeof v === "object") { return Promise.resolve(luaToString(v)); } @@ -110,12 +114,18 @@ export async function jsonToMDTable( return lines.join("\n"); } -export function renderExpressionResult(result: any): Promise { +// Render an expression result as Markdown (for copy path) +// LuaTables are rendered natively, JS objects/arrays use `jsonToMDTable` +export function renderExpressionResult( + result: any, + cellTransformer: (v: any, k: string) => Promise = refCellTransformer, +): Promise { if (result === undefined || result === null) { return Promise.resolve("nil"); } + // LuaTable: render natively without `.toJS` if (result instanceof LuaTable) { - result = result.toJS(); + return renderLuaTableToMarkdown(result, cellTransformer); } // Must check before object/array checks — tagged floats are plain objects if (isTaggedFloat(result)) { @@ -129,7 +139,7 @@ export function renderExpressionResult(result: any): Promise { ) { // If result is an array of objects, render as a Markdown table try { - return jsonToMDTable(result); + return jsonToMDTable(result, cellTransformer); } catch (e: any) { console.error( `Error rendering expression directive: ${e.message} for value ${ @@ -139,16 +149,164 @@ export function renderExpressionResult(result: any): Promise { return Promise.resolve(JSON.stringify(result)); } } else if (typeof result === "object" && result.constructor === Object) { - // If result is a plain object, render as a Markdown table - return jsonToMDTable([result]); + if (Object.keys(result).length === 0) { + return Promise.resolve("*(empty table)*"); + } + return jsonToMDTable([result], cellTransformer); } else if (Array.isArray(result)) { - // Not-object array, let's render it as a Markdown list - return Promise.resolve(result.map((item) => `- ${item}`).join("\n")); + if (result.length === 0) { + return Promise.resolve("*(empty table)*"); + } + return renderListItems(result, cellTransformer, false); } else { return Promise.resolve("" + result); } } +// Render a `LuaTable` to Markdown (table for records, list for arrays) +async function renderLuaTableToMarkdown( + tbl: LuaTable, + cellTransformer: (v: any, k: string) => Promise, + nested = false, +): Promise { + const keys = tbl.keys(); + if (keys.length === 0) return "*(empty table)*"; + + 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 + if (hasArrayPart && !hasStrKeys) { + const elements: any[] = []; + for (let i = 1; i <= arrayLen; i++) elements.push(tbl.rawGet(i)); + // If all elements are `LuaTables` then to Markdown table + if (elements.every((el) => el instanceof LuaTable)) { + return renderLuaTableArrayToMarkdown( + elements as LuaTable[], + cellTransformer, + ); + } + // Flat list + return renderListItems(elements, cellTransformer, nested); + } + + // Record or mixed - use `keys` order directly (preserve order) + const allHeaders: string[] = keys.map(String); + + const lines: string[] = [ + "|" + allHeaders.join("|") + "|", + "|" + allHeaders.map(() => "--").join("|") + "|", + ]; + const cells: string[] = []; + for (const k of keys) { + const v = tbl.rawGet(k); + cells.push(await cellTransformer(v, String(k))); + } + lines.push("|" + cells.join("|") + "|"); + return lines.join("\n"); +} + +// Render a list of items as indented Markdown list +async function renderListItems( + items: any[], + cellTransformer: (v: any, k: string) => Promise, + nested: boolean, +): Promise { + const rendered: string[] = []; + for (const item of items) { + const md = await renderItemToMarkdown(item, cellTransformer, nested); + if (md.includes("\n")) { + const lines = md.split("\n"); + rendered.push( + "-\n" + + lines.map((l) => ` ${l}`).join("\n"), + ); + } else { + rendered.push(`- ${md}`); + } + } + return rendered.join("\n"); +} + +// Render a single item for use inside a list +function renderItemToMarkdown( + item: any, + cellTransformer: (v: any, k: string) => Promise, + nested: boolean, +): Promise { + if (item === undefined || item === null) return Promise.resolve(""); + if (item instanceof LuaTable) { + if (item.empty()) return Promise.resolve("*(empty table)*"); + if (nested) { + return item.toStringAsync(); + } + return renderLuaTableToMarkdown(item, cellTransformer, true); + } + if (Array.isArray(item)) { + if (item.length === 0) return Promise.resolve("*(empty table)*"); + if (isPlainObjectHelper(item)) { + return jsonToMDTable(item, cellTransformer); + } + if (nested) { + return Promise.resolve(JSON.stringify(item)); + } + return renderListItems(item, cellTransformer, true); + } + if (isPlainObjectHelper(item)) { + if (Object.keys(item).length === 0) { + return Promise.resolve("*(empty table)*"); + } + if (nested) { + return Promise.resolve(JSON.stringify(item)); + } + return jsonToMDTable([item], cellTransformer); + } + if (isTaggedFloat(item)) { + return Promise.resolve(luaFormatNumber(item.value, "float")); + } + if (typeof item === "number") return Promise.resolve(luaFormatNumber(item)); + return Promise.resolve("" + item); +} + +function isPlainObjectHelper(v: any): v is Record { + return typeof v === "object" && v !== null && !Array.isArray(v) && + v.constructor === Object; +} + +// Array of record-like `LuaTables` to multi-row Markdown table +async function renderLuaTableArrayToMarkdown( + tables: LuaTable[], + cellTransformer: (v: any, k: string) => Promise, +): Promise { + const headerSet = new Set(); + for (const tbl of tables) { + for (const k of tbl.keys()) headerSet.add(String(k)); + } + if (headerSet.size === 0) return "*(empty table)*"; + const headers = [...headerSet]; + + const lines: string[] = [ + "|" + headers.join("|") + "|", + "|" + headers.map(() => "--").join("|") + "|", + ]; + for (const tbl of tables) { + const cells: string[] = []; + for (const h of headers) { + const key = /^\d+$/.test(h) ? Number(h) : h; + const v = tbl.rawGet(key); + cells.push(await cellTransformer(v, h)); + } + lines.push("|" + cells.join("|") + "|"); + } + return lines.join("\n"); +} + /** * Applies some heuristics to figure out if a string should be rendered as a markdown block or inline markdown * @param s markdown string diff --git a/client/space_lua/render_lua_html.ts b/client/space_lua/render_lua_html.ts new file mode 100644 index 00000000..cb712b04 --- /dev/null +++ b/client/space_lua/render_lua_html.ts @@ -0,0 +1,332 @@ +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 `
      ` or multi-row ``) + if (hasArrayPart && !hasStrKeys) { + const elements: any[] = []; + for (let i = 1; i <= arrayLen; i++) elements.push(tbl.rawGet(i)); + // If all elements are `LuaTables` then render as multi-row table + if (elements.every((el) => el instanceof LuaTable)) { + return renderLuaTableArrayToHtml(elements as LuaTable[], renderInline); + } + return renderArrayToHtml(elements, renderInline); + } + + // Has string keys (record or mixed) - use `keys` order directly + const lines: string[] = []; + lines.push("
      "); + lines.push(""); + for (const k of keys) lines.push(``); + lines.push(""); + lines.push(""); + for (const k of keys) { + const v = tbl.rawGet(k); + lines.push(await renderTdHtml(v, renderInline)); + } + lines.push(""); + lines.push("
      ${escapeHtml(String(k))}
      "); + return lines.join(""); +} + +// Array of record-like LuaTables to multi-row `` +async function renderLuaTableArrayToHtml( + tables: LuaTable[], + renderInline: InlineRenderer, +): Promise { + const headerSet = new Set(); + for (const tbl of tables) { + for (const k of tbl.keys()) headerSet.add(String(k)); + } + if (headerSet.size === 0) return `
      `; + const headers = [...headerSet]; + + const lines: string[] = []; + lines.push(""); + lines.push(""); + for (const h of headers) lines.push(``); + lines.push(""); + lines.push(""); + for (const tbl of tables) { + lines.push(""); + for (const h of headers) { + const key = /^\d+$/.test(h) ? Number(h) : h; + const v = tbl.rawGet(key); + lines.push(await renderTdHtml(v, renderInline)); + } + lines.push(""); + } + lines.push(""); + lines.push("
      ${escapeHtml(h)}
      "); + return lines.join(""); +} + +async function renderJsObjectArrayToHtml( + jsonArray: Record[], + renderInline: InlineRenderer, +): Promise { + const headerSet = new Set(); + for (const entry of jsonArray) { + for (const k of Object.keys(entry)) headerSet.add(k); + } + if (headerSet.size === 0) return `
      `; + const headers = [...headerSet]; + + const lines: string[] = []; + lines.push(""); + lines.push(""); + for (const h of headers) lines.push(``); + lines.push(""); + lines.push(""); + for (const row of jsonArray) { + lines.push(""); + for (const h of headers) { + lines.push(await renderTdHtml(row[h], renderInline)); + } + lines.push(""); + } + lines.push(""); + lines.push("
      ${escapeHtml(h)}
      "); + return lines.join(""); +} + +async function renderArrayToHtml( + items: any[], + renderInline: InlineRenderer, +): Promise { + const lines: string[] = []; + lines.push("
        "); + for (const item of items) { + lines.push(await renderLiHtml(item, renderInline)); + } + lines.push("
      "); + return lines.join(""); +} + +function renderJsArrayToHtml( + items: any[], + renderInline: InlineRenderer, +): Promise { + return renderArrayToHtml(items, renderInline); +} + +async function renderTdHtml( + v: any, + renderInline: InlineRenderer, +): Promise { + const type = luaTypeName(v); + const attrs: string[] = []; + if (type) { + attrs.push(`data-table-cell-type="${type}"`); + } + if (isEmpty(v)) { + attrs.push("data-table-cell-empty"); + } + const attrStr = attrs.length > 0 ? " " + attrs.join(" ") : ""; + const content = await renderCellContent(v, renderInline); + return `${content}
  • `); + return renderLuaTableToHtml(v, renderInline); + } + if (Array.isArray(v)) { + if (v.length === 0) { + return Promise.resolve(`
    `); + } + if (v.every(isPlainObject)) { + return renderJsObjectArrayToHtml(v, renderInline); + } + return renderArrayToHtml(v, renderInline); + } + if (isPlainObject(v)) { + if (Object.keys(v).length === 0) { + return Promise.resolve(`
    `); + } + return renderJsObjectArrayToHtml([v], renderInline); + } + return Promise.resolve(renderInline(formatScalar(v))); +}