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 {