import { codeWidget, editor, markdown, system } from "$sb/syscalls.ts"; import { addParentPointers, collectNodesOfType, findNodeMatching, findNodeOfType, findParentMatching, type ParseTree, removeParentPointers, renderToText, } from "$sb/lib/tree.ts"; import { parseQuery } from "$sb/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 { renderQuery } from "./api.ts"; import type { ChangeSpec } from "@codemirror/state"; 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 renderQuery(parsedQuery, { page: pageObject, config, }); if (Array.isArray(results)) { resultMarkdown = jsonToMDTable(results); } else { resultMarkdown = results; } return { markdown: resultMarkdown, buttons: [ { description: "Bake result", svg: ``, invokeFunction: "query.bakeButton", }, { description: "Edit", svg: ``, invokeFunction: "query.editButton", }, { 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); if (!textNode) { throw new Error(`Could not find node to bake`); } const blockNode = findParentMatching( textNode, (n) => n.type === "FencedCode", ); 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 codeBlockNode node of type FencedCode 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( codeBlockNode: ParseTree, ): Promise { const lang = renderToText( findNodeOfType(codeBlockNode, "CodeInfo") ?? undefined, ); const body = renderToText( findNodeOfType(codeBlockNode, "CodeText") ?? undefined, ); if (!lang || body === undefined) { return null; } const content = await codeWidget.render( lang, body, await editor.getCurrentPage(), ); if ( !content || !content.markdown === undefined || codeBlockNode.from === undefined || codeBlockNode.to === undefined ) { // Check attributes for undefined because 0 or empty string could be valid return null; } return { from: codeBlockNode.from, to: codeBlockNode.to, insert: content.markdown, }; }