From 7c23e8d6229ab880da8e3bb54edbb820bbcae317 Mon Sep 17 00:00:00 2001 From: onespaceman Date: Thu, 8 Aug 2024 14:19:41 -0400 Subject: [PATCH] Widget buttons for transclusions (#942) (#1013) Widget buttons for transclusions --- plug-api/types.ts | 2 +- plugs/index/toc.ts | 6 +-- plugs/index/widget.ts | 2 +- plugs/query/widget.ts | 47 ++++++++++------- plugs/template/template.plug.yaml | 8 +++ plugs/template/widget.ts | 86 ++++++++++++++++++++++++++++--- web/cm_plugins/inline_content.ts | 32 +++++++----- web/cm_plugins/markdown_widget.ts | 8 +-- web/styles/editor.scss | 4 -- 9 files changed, 144 insertions(+), 51 deletions(-) diff --git a/plug-api/types.ts b/plug-api/types.ts index 5d78eaed..0e687c5d 100644 --- a/plug-api/types.ts +++ b/plug-api/types.ts @@ -172,7 +172,7 @@ export type CodeWidgetButton = { widgetTarget?: boolean; description: string; svg: string; - invokeFunction: string; + invokeFunction: string[]; }; export type LintDiagnostic = { diff --git a/plugs/index/toc.ts b/plugs/index/toc.ts index 054f2125..0817a018 100644 --- a/plugs/index/toc.ts +++ b/plugs/index/toc.ts @@ -82,19 +82,19 @@ export async function widget( description: "Bake result", svg: ``, - invokeFunction: "query.bakeButton", + invokeFunction: ["query.bakeButton", bodyText], }, { description: "Edit", svg: ``, - invokeFunction: "query.editButton", + invokeFunction: ["query.editButton", bodyText], }, { description: "Reload", svg: ``, - invokeFunction: "index.refreshWidgets", + invokeFunction: ["index.refreshWidgets"], }, ], }; diff --git a/plugs/index/widget.ts b/plugs/index/widget.ts index b92bf7b8..82f5129e 100644 --- a/plugs/index/widget.ts +++ b/plugs/index/widget.ts @@ -88,7 +88,7 @@ export async function renderTemplateWidgets(side: "top" | "bottom"): Promise< description: "Reload", svg: ``, - invokeFunction: "index.refreshWidgets", + invokeFunction: ["index.refreshWidgets"], }, ], }; diff --git a/plugs/query/widget.ts b/plugs/query/widget.ts index 21c485c1..f25d070f 100644 --- a/plugs/query/widget.ts +++ b/plugs/query/widget.ts @@ -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: ``, - invokeFunction: "query.bakeButton", + invokeFunction: ["query.bakeButton", bodyText], }, { description: "Edit", svg: ``, - invokeFunction: "query.editButton", + invokeFunction: ["query.editButton", bodyText], }, { description: "Reload", 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 { - 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, }; } diff --git a/plugs/template/template.plug.yaml b/plugs/template/template.plug.yaml index ec1b078e..1094ed74 100644 --- a/plugs/template/template.plug.yaml +++ b/plugs/template/template.plug.yaml @@ -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 diff --git a/plugs/template/widget.ts b/plugs/template/widget.ts index d8d020b3..546fa1ab 100644 --- a/plugs/template/widget.ts +++ b/plugs/template/widget.ts @@ -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: ``, - invokeFunction: "query.bakeButton", + invokeFunction: ["query.bakeButton", bodyText], }, { description: "Edit", svg: ``, - invokeFunction: "query.editButton", + invokeFunction: ["query.editButton", bodyText], }, { description: "Reload", svg: ``, - invokeFunction: "query.refreshAllWidgets", + invokeFunction: ["query.refreshAllWidgets"], }, ], }; @@ -160,19 +164,19 @@ export async function templateWidget( description: "Bake result", svg: ``, - invokeFunction: "query.bakeButton", + invokeFunction: ["query.bakeButton", bodyText], }, { description: "Edit", svg: ``, - invokeFunction: "query.editButton", + invokeFunction: ["query.editButton", bodyText], }, { description: "Reload", svg: ``, - invokeFunction: "query.refreshAllWidgets", + invokeFunction: ["query.refreshAllWidgets"], }, ], }; @@ -182,3 +186,73 @@ export async function templateWidget( }; } } + +export async function transclusionWidget( + bodyText: string, + pageName: string, +): Promise { + 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: + ``, + invokeFunction: ["query.bakeButton", bodyText], + }, + { + description: "Open Page", + svg: + ``, + invokeFunction: ["template.navigateButton", url], + }, + { + description: "Reload", + 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); +} diff --git a/web/cm_plugins/inline_content.ts b/web/cm_plugins/inline_content.ts index 765ee067..cb8fcb75 100644 --- a/web/cm_plugins/inline_content.ts +++ b/web/cm_plugins/inline_content.ts @@ -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), ); diff --git a/web/cm_plugins/markdown_widget.ts b/web/cm_plugins/markdown_widget.ts index ce9cdfdb..940eff70 100644 --- a/web/cm_plugins/markdown_widget.ts +++ b/web/cm_plugins/markdown_widget.ts @@ -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; } diff --git a/web/styles/editor.scss b/web/styles/editor.scss index 44748865..51e8b588 100644 --- a/web/styles/editor.scss +++ b/web/styles/editor.scss @@ -590,10 +590,6 @@ .sb-markdown-widget-inline { margin: 0 0 0 0; - - &:hover .button-bar { - display: none !important; - } } .sb-fenced-code-iframe {