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]].