Widget buttons for transclusions (#942) (#1013)

Widget buttons for transclusions
pull/1036/head
onespaceman 2024-08-08 14:19:41 -04:00 committed by GitHub
parent cafd001214
commit 7c23e8d622
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 144 additions and 51 deletions

View File

@ -172,7 +172,7 @@ export type CodeWidgetButton = {
widgetTarget?: boolean;
description: string;
svg: string;
invokeFunction: string;
invokeFunction: string[];
};
export type LintDiagnostic = {

View File

@ -82,19 +82,19 @@ export async function widget(
description: "Bake result",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-left"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg>`,
invokeFunction: "query.bakeButton",
invokeFunction: ["query.bakeButton", bodyText],
},
{
description: "Edit",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
invokeFunction: "query.editButton",
invokeFunction: ["query.editButton", bodyText],
},
{
description: "Reload",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
invokeFunction: "index.refreshWidgets",
invokeFunction: ["index.refreshWidgets"],
},
],
};

View File

@ -88,7 +88,7 @@ export async function renderTemplateWidgets(side: "top" | "bottom"): Promise<
description: "Reload",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
invokeFunction: "index.refreshWidgets",
invokeFunction: ["index.refreshWidgets"],
},
],
};

View File

@ -7,7 +7,6 @@ import {
import {
addParentPointers,
collectNodesOfType,
findNodeMatching,
findNodeOfType,
findParentMatching,
type ParseTree,
@ -20,6 +19,10 @@ 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";
import {
findNodeMatching,
nodeAtPos,
} from "@silverbulletmd/silverbullet/lib/tree";
export async function widget(
bodyText: string,
@ -50,19 +53,19 @@ export async function widget(
description: "Bake result",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-left"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg>`,
invokeFunction: "query.bakeButton",
invokeFunction: ["query.bakeButton", bodyText],
},
{
description: "Edit",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
invokeFunction: "query.editButton",
invokeFunction: ["query.editButton", bodyText],
},
{
description: "Reload",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
invokeFunction: "query.refreshAllWidgets",
invokeFunction: ["query.refreshAllWidgets"],
},
],
};
@ -94,13 +97,14 @@ export async function bakeButton(bodyText: string) {
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);
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) => n.type === "FencedCode" || n.type === "Image",
);
if (!blockNode) {
removeParentPointers(textNode);
@ -135,18 +139,25 @@ export async function bakeAllWidgets() {
/**
* 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)
* @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(
codeBlockNode: ParseTree,
nodeToReplace: ParseTree,
): Promise<ChangeSpec | null> {
const lang = renderToText(
findNodeOfType(codeBlockNode, "CodeInfo") ?? undefined,
);
const body = renderToText(
findNodeOfType(codeBlockNode, "CodeText") ?? undefined,
);
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;
}
@ -158,15 +169,15 @@ async function changeForBake(
);
if (
!content || !content.markdown === undefined ||
codeBlockNode.from === undefined ||
codeBlockNode.to === undefined
nodeToReplace.from === undefined ||
nodeToReplace.to === undefined
) { // Check attributes for undefined because 0 or empty string could be valid
return null;
}
return {
from: codeBlockNode.from,
to: codeBlockNode.to,
from: nodeToReplace.from,
to: nodeToReplace.to,
insert: content.markdown,
};
}

View File

@ -48,6 +48,14 @@ functions:
codeWidget: template
renderMode: markdown
transclusionWidget:
path: widget.ts:transclusionWidget
codeWidget: transclusion
renderMode: markdown
navigateButton:
path: widget.ts:navigateButton
# API invoked when a new page is created
newPage:
path: page.ts:newPage

View File

@ -1,4 +1,5 @@
import {
editor,
markdown,
space,
system,
@ -9,11 +10,14 @@ import type { CodeWidgetContent, PageMeta } from "../../plug-api/types.ts";
import { renderTemplate } from "./plug_api.ts";
import { renderToText } from "@silverbulletmd/silverbullet/lib/tree";
import {
isFederationPath,
resolvePath,
rewritePageRefs,
rewritePageRefsInString,
} from "@silverbulletmd/silverbullet/lib/resolve";
import { queryParsed } from "../query/api.ts";
import { parseQuery } from "../../plug-api/lib/parse_query.ts";
import { parsePageRef } from "@silverbulletmd/silverbullet/lib/page_ref";
type TemplateWidgetConfig = {
// Pull the template from a page
@ -99,19 +103,19 @@ export async function includeWidget(
description: "Bake result",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-left"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg>`,
invokeFunction: "query.bakeButton",
invokeFunction: ["query.bakeButton", bodyText],
},
{
description: "Edit",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
invokeFunction: "query.editButton",
invokeFunction: ["query.editButton", bodyText],
},
{
description: "Reload",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
invokeFunction: "query.refreshAllWidgets",
invokeFunction: ["query.refreshAllWidgets"],
},
],
};
@ -160,19 +164,19 @@ export async function templateWidget(
description: "Bake result",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-left"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg>`,
invokeFunction: "query.bakeButton",
invokeFunction: ["query.bakeButton", bodyText],
},
{
description: "Edit",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
invokeFunction: "query.editButton",
invokeFunction: ["query.editButton", bodyText],
},
{
description: "Reload",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
invokeFunction: "query.refreshAllWidgets",
invokeFunction: ["query.refreshAllWidgets"],
},
],
};
@ -182,3 +186,73 @@ export async function templateWidget(
};
}
}
export async function transclusionWidget(
bodyText: string,
pageName: string,
): Promise<CodeWidgetContent> {
const config = await system.getSpaceConfig();
const pageMeta: PageMeta = await loadPageObject(pageName);
let url: string | undefined = undefined;
let match: RegExpExecArray | null;
if ((match = /!?\[([^\]]*)\]\((.+)\)/g.exec(bodyText))) {
[/* fullMatch */, /* alias */ , url] = match;
} else if (
(match = /(!?\[\[)([^\]\|]+)(?:\|([^\]]+))?(\]\])/g.exec(bodyText))
) {
[/* fullMatch */, /* firstMark */ , url /* alias */] = match;
if (!isFederationPath(url)) {
url = "/" + url;
}
}
try {
if (!url) {
throw new Error("Could not parse link");
}
url = resolvePath(pageName, url, true);
const templateText =
`{{rewriteRefsAndFederationLinks([[${url}]], "${url}")}}`;
const { text: rendered } = await renderTemplate(
templateText,
pageMeta,
{ page: pageMeta, config },
);
return {
markdown: rendered,
buttons: [
{
description: "Bake result",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-left"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg>`,
invokeFunction: ["query.bakeButton", bodyText],
},
{
description: "Open Page",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
invokeFunction: ["template.navigateButton", url],
},
{
description: "Reload",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
invokeFunction: ["query.refreshAllWidgets", bodyText],
},
],
};
} catch (e: any) {
return {
markdown: `**Error:** ${e.message}`,
};
}
}
// Navigate to page in a transclusion widget
export async function navigateButton(url: string) {
const pageRef = parsePageRef(url);
await editor.navigate(pageRef, false, false);
}

View File

@ -160,8 +160,7 @@ export function inlineContentPlugin(client: Client) {
}
const text = state.sliceDoc(node.from, node.to);
let [url, alias]: (string | null)[] = [null, null];
let dim: ContentDimensions | undefined;
let [url, alias]: (string | undefined)[] = [undefined, undefined];
let match: RegExpExecArray | null;
if ((match = /!?\[([^\]]*)\]\((.+)\)/g.exec(text))) {
[/* fullMatch */, alias, url] = match;
@ -172,10 +171,12 @@ export function inlineContentPlugin(client: Client) {
if (!isFederationPath(url)) {
url = "/" + url;
}
} else {
}
if (!url) {
return;
}
let dim: ContentDimensions | undefined;
if (alias) {
const { alias: parsedAlias, dim: parsedDim } = parseAlias(alias);
if (parsedAlias) {
@ -187,7 +188,11 @@ export function inlineContentPlugin(client: Client) {
}
if (isLocalPath(url)) {
url = resolvePath(client.currentPage, decodeURI(url), true);
url = resolvePath(
client.currentPage,
decodeURI(url),
true,
);
const pageRef = parsePageRef(url);
if (
isFederationPath(pageRef.page) ||
@ -195,30 +200,24 @@ export function inlineContentPlugin(client: Client) {
) {
// This is a page reference, let's inline the content
const codeWidgetCallback = client.clientSystem.codeWidgetHook
.codeWidgetCallbacks.get("template");
.codeWidgetCallbacks.get("transclusion");
if (!codeWidgetCallback) {
return;
}
widgets.push(
Decoration.line({
class: "sb-fenced-code-iframe",
}).range(node.to),
);
widgets.push(
Decoration.widget({
widget: new MarkdownWidget(
node.from,
client,
`widget:${client.currentPage}:${text}`,
`{{rewriteRefsAndFederationLinks([[${url}]], "${url}")}}`,
text,
codeWidgetCallback,
"sb-markdown-widget sb-markdown-widget-inline",
),
block: true,
}).range(node.to),
}).range(node.to + 1),
);
return;
}
@ -226,7 +225,12 @@ export function inlineContentPlugin(client: Client) {
widgets.push(
Decoration.widget({
widget: new InlineContentWidget(url, alias, dim, client),
widget: new InlineContentWidget(
url,
alias,
dim,
client,
),
block: true,
}).range(node.to + 1),
);

View File

@ -257,7 +257,7 @@ export class MarkdownWidget extends WidgetType {
div.addEventListener("click", () => {
console.log("Widget clicked");
this.client.clientSystem.localSyscall("system.invokeFunction", [
button.invokeFunction,
button.invokeFunction[0],
this.from,
]).catch(console.error);
});
@ -266,10 +266,10 @@ export class MarkdownWidget extends WidgetType {
"click",
(e) => {
e.stopPropagation();
this.client.clientSystem.localSyscall("system.invokeFunction", [
this.client.clientSystem.localSyscall(
"system.invokeFunction",
button.invokeFunction,
this.bodyText,
]).then((newContent: string | undefined) => {
).then((newContent: string | undefined) => {
if (newContent) {
div.innerText = newContent;
}

View File

@ -590,10 +590,6 @@
.sb-markdown-widget-inline {
margin: 0 0 0 0;
&:hover .button-bar {
display: none !important;
}
}
.sb-fenced-code-iframe {