diff --git a/plugs/index/toc.ts b/plugs/index/toc.ts index 30315bb6..3273153c 100644 --- a/plugs/index/toc.ts +++ b/plugs/index/toc.ts @@ -73,6 +73,12 @@ export async function widget( return { markdown: renderedMd, buttons: [ + { + description: "Bake result", + svg: + ``, + invokeFunction: "query.bakeButton", + }, { description: "Edit", svg: diff --git a/plugs/query/query.plug.yaml b/plugs/query/query.plug.yaml index fd2aaf85..b483d7c1 100644 --- a/plugs/query/query.plug.yaml +++ b/plugs/query/query.plug.yaml @@ -17,25 +17,28 @@ functions: editButton: path: widget.ts:editButton + bakeButton: + path: widget.ts:bakeButton + lintQuery: path: lint.ts:lintQuery events: - - editor:lint + - editor:lint queryComplete: path: complete.ts:queryComplete events: - - editor:complete + - editor:complete queryAttributeComplete: path: complete.ts:queryAttributeComplete events: - - editor:complete + - editor:complete languageComplete: path: complete.ts:languageComplete events: - - editor:complete + - editor:complete refreshAllWidgets: path: widget.ts:refreshAllWidgets @@ -43,3 +46,7 @@ functions: name: "Live Queries and Templates: Refresh All" key: "Alt-q" + bakeAllWidgets: + path: widget.ts:bakeAllWidgets + command: + name: "Page: Bake live blocks" diff --git a/plugs/query/widget.ts b/plugs/query/widget.ts index 94afa7fa..e56cf47d 100644 --- a/plugs/query/widget.ts +++ b/plugs/query/widget.ts @@ -1,10 +1,20 @@ -import { codeWidget, editor, events } from "$sb/syscalls.ts"; +import { codeWidget, editor, markdown } from "$sb/syscalls.ts"; +import { + addParentPointers, + collectNodesOfType, + findNodeMatching, + findNodeOfType, + findParentMatching, + ParseTree, + removeParentPointers, + renderToText, +} from "$lib/tree.ts"; import { parseQuery } from "$sb/lib/parse-query.ts"; import { loadPageObject, replaceTemplateVars } from "../template/page.ts"; -import { resolvePath } from "$sb/lib/resolve.ts"; -import { CodeWidgetContent } from "../../type/types.ts"; -import { jsonToMDTable, renderQueryTemplate } from "../template/util.ts"; +import { CodeWidgetContent } from "$type/types.ts"; +import { jsonToMDTable } from "../template/util.ts"; import { renderQuery } from "./api.ts"; +import { ChangeSpec } from "../../web/deps.ts"; export async function widget( bodyText: string, @@ -27,6 +37,12 @@ export async function widget( return { markdown: resultMarkdown, buttons: [ + { + description: "Bake result", + svg: + ``, + invokeFunction: "query.bakeButton", + }, { description: "Edit", svg: @@ -52,7 +68,7 @@ export function refreshAllWidgets() { export async function editButton(bodyText: string) { const text = await editor.getText(); - // This is a it 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 + // 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) { @@ -61,3 +77,87 @@ export async function editButton(bodyText: string) { } 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, + }; +} diff --git a/plugs/template/widget.ts b/plugs/template/widget.ts index e466e369..d3c3b670 100644 --- a/plugs/template/widget.ts +++ b/plugs/template/widget.ts @@ -81,17 +81,26 @@ export async function includeWidget( return { markdown: rendered, - buttons: [{ - description: "Edit", - svg: - ``, - invokeFunction: "query.editButton", - }, { - description: "Reload", - svg: - ``, - invokeFunction: "query.refreshAllWidgets", - }], + buttons: [ + { + description: "Bake result", + svg: + ``, + invokeFunction: "query.bakeButton", + }, + { + description: "Edit", + svg: + ``, + invokeFunction: "query.editButton", + }, + { + description: "Reload", + svg: + ``, + invokeFunction: "query.refreshAllWidgets", + }, + ], }; } catch (e: any) { return { @@ -132,17 +141,26 @@ export async function templateWidget( return { markdown: rendered, - buttons: [{ - description: "Edit", - svg: - ``, - invokeFunction: "query.editButton", - }, { - description: "Reload", - svg: - ``, - invokeFunction: "query.refreshAllWidgets", - }], + buttons: [ + { + description: "Bake result", + svg: + ``, + invokeFunction: "query.bakeButton", + }, + { + description: "Edit", + svg: + ``, + invokeFunction: "query.editButton", + }, + { + description: "Reload", + svg: + ``, + invokeFunction: "query.refreshAllWidgets", + }, + ], }; } catch (e: any) { return { diff --git a/website/Live Queries.md b/website/Live Queries.md index 0f2d4056..4a3cb2a8 100644 --- a/website/Live Queries.md +++ b/website/Live Queries.md @@ -6,4 +6,9 @@ The syntax used is: page limit 3 ``` -Queries are written using SilverBullet’s [[Query Language]]. \ No newline at end of file +Queries are written using SilverBullet’s [[Query Language]]. + +# Baking +A query block can be replaced with its current output by clicking the "Bake result" button in the top right corner. This will make it a regular part of page, which will not respond to changes in the data source anymore. + +You can also use the {[Page: Bake live blocks]} command to do it in-place for every [[Blocks|block]] in this page which is rendered as Markdown (but not [[Live Template Widgets]]). You can undo the replacement with `Ctrl+Z`/`Cmd+Z` as usual, but if you want to keep your queries you shoud make a copy of the page first. diff --git a/website/Live Templates.md b/website/Live Templates.md index 3e003481..42f4345c 100644 --- a/website/Live Templates.md +++ b/website/Live Templates.md @@ -1,11 +1,11 @@ -Live Templates are a type of [[Blocks|block]] that render [[Templates]] written in [[Template Language]] inline in a page. +Live Templates are a type of [[Blocks|block]] that render [[Templates]] written in [[Template Language]] inline in a page. There are two variants of Live Templates: * `template`: where the template is specified inline in a code block. * `include`: where an external page (template) is _included_ and rendered. -Template blocks are specified using [[Markdown]]‘s fenced code block notation using either `template` or `include` as its language. +Template blocks are specified using [[Markdown]]‘s fenced code block notation using either `template` or `include` as its language. They can also be [[Live Queries#Baking|baked]]. # Template To specify a template to render inline, you can use the `template` block. The body is written in [[Template Language]].