From cf81594b027bb9b80399d6fca133158d0d87885c Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Wed, 12 Feb 2025 20:09:02 +0100 Subject: [PATCH] Fixes #1236 --- common/markdown.ts | 143 +++++++++++++++++++++++++++++++++++ common/space_lua/runtime.ts | 9 +++ common/syscalls/lua.ts | 15 ++-- web/cm_plugins/lua_widget.ts | 19 +++-- 4 files changed, 174 insertions(+), 12 deletions(-) create mode 100644 common/markdown.ts diff --git a/common/markdown.ts b/common/markdown.ts new file mode 100644 index 00000000..3dec6c43 --- /dev/null +++ b/common/markdown.ts @@ -0,0 +1,143 @@ +import { + findNodeOfType, + type ParseTree, + renderToText, + replaceNodesMatchingAsync, +} from "@silverbulletmd/silverbullet/lib/tree"; +import { + parsePageRef, + validatePageName, +} from "@silverbulletmd/silverbullet/lib/page_ref"; +import { renderExpressionResult } from "../plugs/template/util.ts"; +import { parseMarkdown } from "$common/markdown_parser/parser.ts"; +import type { LuaExpression } from "$common/space_lua/ast.ts"; +import { evalExpression } from "$common/space_lua/eval.ts"; +import type { LuaEnv, LuaStackFrame } from "$common/space_lua/runtime.ts"; +import { parseExpressionString } from "$common/space_lua/parse.ts"; +import type { CodeWidgetHook } from "../web/hooks/code_widget.ts"; + +/** + * Finds code widgets, runs their plug code to render and inlines their content in the parse tree + * @param mdTree parsed markdown tree + * @param pageName name of the current page + * @returns modified mdTree + */ +export async function expandCodeWidgets( + codeWidgetHook: CodeWidgetHook, + mdTree: ParseTree, + pageName: string, + env: LuaEnv, + sf: LuaStackFrame, +): Promise { + await replaceNodesMatchingAsync(mdTree, async (n) => { + if (n.type === "FencedCode") { + const codeInfo = findNodeOfType(n, "CodeInfo"); + if (!codeInfo) { + return; + } + const codeType = codeInfo.children![0].text!; + const codeTextNode = findNodeOfType(n, "CodeText"); + try { + // This will error out if this is not a code wiget, which is fine + const langCallback = codeWidgetHook.codeWidgetCallbacks.get(codeType); + if (!langCallback) { + return { + text: "", + }; + } + const result = await langCallback( + renderToText(codeTextNode!), + pageName, + ); + if (!result) { + return { + text: "", + }; + } + // Only do this for "markdown" widgets, that is: that can render to markdown + if (result.markdown !== undefined) { + const parsedBody = parseMarkdown(result.markdown); + // Recursively process + return expandCodeWidgets( + codeWidgetHook, + parsedBody, + pageName, + env, + sf, + ); + } + } catch (e: any) { + // 'not found' is to be expected (no code widget configured for this language) + // Every other error should probably be reported + if (!e.message.includes("not found")) { + console.trace(); + console.error("Error rendering code", e.message); + } + } + } else if (n.type === "Image") { + // Let's scan for ![[embeds]] that are codified as Images, confusingly + const wikiLinkMark = findNodeOfType(n, "WikiLinkMark"); + if (!wikiLinkMark) { + return; + } + const wikiLinkPage = findNodeOfType(n, "WikiLinkPage"); + if (!wikiLinkPage) { + return; + } + + const page = wikiLinkPage.children![0].text!; + + // Check if this is likely a page link (based on the path format, e.g. if it contains an extension, it's probably not a page link) + try { + const ref = parsePageRef(page); + validatePageName(ref.page); + } catch { + // Not a valid page name, so not a page reference + return; + } + + // Internally translate this to a template that inlines a page, then render that + const langCallback = codeWidgetHook.codeWidgetCallbacks.get("template")!; + const result = await langCallback(`{{[[${page}]]}}`, pageName); + if (!result) { + return { + text: "", + }; + } + // Only do this for "markdown" widgets, that is: that can render to markdown + if (result.markdown !== undefined) { + const parsedBody = await parseMarkdown(result.markdown); + // Recursively process + return expandCodeWidgets( + codeWidgetHook, + parsedBody, + page, + env, + sf, + ); + } + } else if (n.type === "LuaDirective") { + const expr = findNodeOfType(n, "LuaExpressionDirective") as + | LuaExpression + | null; + if (!expr) { + return; + } + const exprText = renderToText(expr); + + let result = await evalExpression( + parseExpressionString(exprText), + env, + sf, + ); + + if (result?.markdown) { + result = result.markdown; + } + + const markdown = await renderExpressionResult(result); + return parseMarkdown(markdown); + } + }); + return mdTree; +} diff --git a/common/space_lua/runtime.ts b/common/space_lua/runtime.ts index 578c77a1..298b54dc 100644 --- a/common/space_lua/runtime.ts +++ b/common/space_lua/runtime.ts @@ -106,6 +106,15 @@ export class LuaStackFrame { } static lostFrame = new LuaStackFrame(new LuaEnv(), null); + + static createWithGlobalEnv( + globalEnv: any, + ctx: ASTCtx | null = null, + ): LuaStackFrame { + const env = new LuaEnv(); + env.setLocal("_GLOBAL", globalEnv); + return new LuaStackFrame(env, ctx); + } } export class LuaMultiRes { diff --git a/common/syscalls/lua.ts b/common/syscalls/lua.ts index d83831ce..51ed6728 100644 --- a/common/syscalls/lua.ts +++ b/common/syscalls/lua.ts @@ -29,16 +29,21 @@ export function luaSyscalls(commonSystem: CommonSystem): SysCallMapping { commonSystem.spaceLuaEnv.env, ); const sf = new LuaStackFrame(env, null); - const result = evalExpression(ast, commonSystem.spaceLuaEnv.env, sf); - if (isSendable(result)) { - return luaValueToJS(result); + const luaResult = await evalExpression( + ast, + commonSystem.spaceLuaEnv.env, + sf, + ); + const jsResult = luaValueToJS(luaResult); + if (isSendable(jsResult)) { + return jsResult; } else { // This may evaluate to e.g. a function, which is not sendable, in this case we'll console.warn and return a stringified version of the result console.warn( "Lua eval result is not sendable, returning stringified version", - result, + jsResult, ); - return luaToString(result); + return luaToString(luaResult); } } catch (e: any) { console.error("Lua eval error: ", e.message, e.sf?.astCtx); diff --git a/web/cm_plugins/lua_widget.ts b/web/cm_plugins/lua_widget.ts index 6306aaa9..14ea1f98 100644 --- a/web/cm_plugins/lua_widget.ts +++ b/web/cm_plugins/lua_widget.ts @@ -11,6 +11,8 @@ import { renderToText } from "@silverbulletmd/silverbullet/lib/tree"; import { activeWidgets } from "./markdown_widget.ts"; import { attachWidgetEventHandlers } from "./widget_util.ts"; import { renderExpressionResult } from "../../plugs/template/util.ts"; +import { expandCodeWidgets } from "$common/markdown.ts"; +import { LuaEnv, LuaStackFrame } from "$common/space_lua/runtime.ts"; export type LuaWidgetCallback = ( bodyText: string, @@ -101,13 +103,16 @@ export class LuaWidget extends WidgetType { extendedMarkdownLanguage, mdContent, ); - mdTree = await this.client.clientSystem.localSyscall( - "system.invokeFunction", - [ - "markdown.expandCodeWidgets", - mdTree, - this.client.currentPage, - ], + + const tl = new LuaEnv(); + const sf = new LuaStackFrame(tl, null, LuaStackFrame.lostFrame); + tl.setLocal("_GLOBAL", client.clientSystem.spaceLuaEnv.env); + mdTree = await expandCodeWidgets( + client.clientSystem.codeWidgetHook, + mdTree, + this.client.currentPage, + client.clientSystem.spaceLuaEnv.env, + sf, ); const trimmedMarkdown = renderToText(mdTree).trim();