import { codeWidget, editor, markdown, system, } from "@silverbulletmd/silverbullet/syscalls"; import { addParentPointers, collectNodesOfType, findNodeOfType, findParentMatching, type ParseTree, removeParentPointers, renderToText, } from "@silverbulletmd/silverbullet/lib/tree"; import { parseQuery } from "../../plug-api/lib/parse_query.ts"; import { loadPageObject, replaceTemplateVars } from "../template/page.ts"; import type { CodeWidgetContent } from "../../plug-api/types.ts"; import { jsonToMDTable } from "../template/util.ts"; import type { ChangeSpec } from "@codemirror/state"; import { findNodeMatching, nodeAtPos, } from "@silverbulletmd/silverbullet/lib/tree"; export async function widget( bodyText: string, pageName: string, ): Promise { const config = await system.getSpaceConfig(); const pageObject = await loadPageObject(pageName); try { let resultMarkdown = ""; const parsedQuery = await parseQuery( await replaceTemplateVars(bodyText, pageObject, config), ); const results = await system.invokeFunction( "query.renderQuery", parsedQuery, { page: pageObject, config, }, ); if (Array.isArray(results)) { if (results.length === 0) { resultMarkdown = "No results"; } else { resultMarkdown = jsonToMDTable(results); } } else { resultMarkdown = results; } return { markdown: resultMarkdown, buttons: [ { description: "Bake result", svg: ``, invokeFunction: ["query.bakeButton", bodyText], }, { description: "Edit", svg: ``, invokeFunction: ["query.editButton", bodyText], }, { description: "Reload", svg: ``, invokeFunction: ["query.refreshAllWidgets"], }, ], }; } catch (e: any) { return { markdown: `**Error:** ${e.message}` }; } } export function refreshAllWidgets() { codeWidget.refreshAll(); } export async function editButton(bodyText: string) { const text = await editor.getText(); // This is a bit of a heuristic and will point to the wrong place if the same body text appears in multiple places, which is easy to replicate but unlikely to happen in the real world // A more accurate fix would be to update the widget (and therefore the index of where this widget appears) on every change, but this would be rather expensive. I think this is good enough. const bodyPos = text.indexOf("\n" + bodyText + "\n"); if (bodyPos === -1) { await editor.flashNotification("Could not find widget to edit", "error"); return; } await editor.moveCursor(bodyPos + 1); } export async function bakeButton(bodyText: string) { try { const text = await editor.getText(); const tree = await markdown.parseMarkdown(text); addParentPointers(tree); // Need to find it in page to make the replacement, see editButton for comment about finding by content const textNode = findNodeMatching(tree, (n) => n.text === bodyText) || nodeAtPos(tree, text.indexOf(bodyText)); if (!textNode) { throw new Error(`Could not find node to bake`); } const blockNode = findParentMatching( textNode, (n) => n.type === "FencedCode" || n.type === "Image", ); if (!blockNode) { removeParentPointers(textNode); console.error("baked node", textNode); throw new Error("Could not find FencedCode above baked node"); } const changes = await changeForBake(blockNode); if (changes) { await editor.dispatch({ changes }); } else { // Either something failed, or this widget does not meet requirements for baking and shouldn't show the button at all throw new Error("Baking with button didn't produce any changes"); } } catch (error) { console.error(error); await editor.flashNotification("Could not bake widget", "error"); } } export async function bakeAllWidgets() { const text = await editor.getText(); const tree = await markdown.parseMarkdown(text); const changes = (await Promise.all( collectNodesOfType(tree, "FencedCode").map(changeForBake), )).filter((c): c is ChangeSpec => c !== null); await editor.dispatch({ changes }); await editor.flashNotification(`Baked ${changes.length} live blocks`); } /** * Create change description to replace a widget source with its markdown output * @param nodeToReplace node of type FencedCode or Image for a markdown widget (eg. query, template, toc) * @returns single replacement for the editor, or null if the widget didn't render to markdown */ async function changeForBake( nodeToReplace: ParseTree, ): Promise { const lang = nodeToReplace.type === "FencedCode" ? renderToText(findNodeOfType(nodeToReplace, "CodeInfo") ?? undefined) : nodeToReplace.type === "Image" ? "transclusion" : undefined; let body: string | undefined = undefined; if (nodeToReplace.type === "FencedCode") { body = renderToText(findNodeOfType(nodeToReplace, "CodeText") ?? undefined); } else if (nodeToReplace.type === "Image") { body = renderToText(nodeToReplace); } if (!lang || body === undefined) { return null; } const content = await codeWidget.render( lang, body, await editor.getCurrentPage(), ); if ( !content || !content.markdown === undefined || nodeToReplace.from === undefined || nodeToReplace.to === undefined ) { // Check attributes for undefined because 0 or empty string could be valid return null; } return { from: nodeToReplace.from, to: nodeToReplace.to, insert: content.markdown, }; }