From 55be05031cc3dc80651386e76c56cd809ad75e22 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Sat, 22 Mar 2025 08:40:26 +0100 Subject: [PATCH] Markdown widget removal --- common/common_system.ts | 2 - common/markdown.ts | 56 +----- common/syscalls/markdown.ts | 13 +- lib/manifest.ts | 7 +- plug-api/syscalls/code_widget.ts | 16 -- plug-api/syscalls/markdown.ts | 4 + plug-api/types.ts | 3 +- plugs/index/index.plug.yaml | 6 - plugs/index/toc.ts | 111 ------------ plugs/markdown/api.ts | 115 ------------- plugs/markdown/markdown.plug.yaml | 3 - plugs/markdown/preview.ts | 5 +- plugs/share/share.ts | 14 +- web/client_system.ts | 7 +- web/cm_plugins/code_widget.ts | 35 ++++ web/cm_plugins/fenced_code.ts | 29 +--- web/cm_plugins/inline_content.ts | 28 ++- web/cm_plugins/lua_widget.ts | 7 +- web/cm_plugins/markdown_widget.ts | 255 ---------------------------- web/cm_plugins/top_bottom_panels.ts | 39 ----- web/hooks/panel_widget.ts | 56 ------ web/syscalls/client_code_widget.ts | 2 +- 22 files changed, 85 insertions(+), 728 deletions(-) delete mode 100644 plugs/index/toc.ts create mode 100644 web/cm_plugins/code_widget.ts delete mode 100644 web/cm_plugins/markdown_widget.ts delete mode 100644 web/hooks/panel_widget.ts diff --git a/common/common_system.ts b/common/common_system.ts index 28ad322f..4de2a825 100644 --- a/common/common_system.ts +++ b/common/common_system.ts @@ -7,7 +7,6 @@ import type { EventHook } from "./hooks/event.ts"; import type { DataStore } from "$lib/data/datastore.ts"; import type { System } from "$lib/plugos/system.ts"; import type { CodeWidgetHook } from "../web/hooks/code_widget.ts"; -import type { PanelWidgetHook } from "../web/hooks/panel_widget.ts"; import type { SlashCommandHook } from "../web/hooks/slash_command.ts"; import type { DataStoreMQ } from "$lib/data/mq.datastore.ts"; import type { ParseTree } from "../plug-api/lib/tree.ts"; @@ -25,7 +24,6 @@ export abstract class CommonSystem { slashCommandHook?: SlashCommandHook; namespaceHook!: PlugNamespaceHook; codeWidgetHook!: CodeWidgetHook; - panelWidgetHook!: PanelWidgetHook; documentEditorHook!: DocumentEditorHook; readonly allKnownFiles = new Set(); diff --git a/common/markdown.ts b/common/markdown.ts index fc9380a9..7cc9cdd5 100644 --- a/common/markdown.ts +++ b/common/markdown.ts @@ -18,65 +18,18 @@ import { renderExpressionResult } from "$common/markdown_util.ts"; import type { Client } from "../web/client.ts"; /** - * Finds code widgets, runs their plug code to render and inlines their content in the parse tree + * Expands custom markdown Lua directives and transclusions into plain markdown * @param mdTree parsed markdown tree - * @param pageName name of the current page * @returns modified mdTree */ -export async function expandCodeWidgets( +export async function expandMarkdown( client: Client, mdTree: ParseTree, - pageName: string, env: LuaEnv, sf: LuaStackFrame, ): Promise { await replaceNodesMatchingAsync(mdTree, async (n) => { - if (n.type === "FencedCode") { - const codeInfo = findNodeOfType(n, "CodeInfo"); - if (!codeInfo) { - return; - } - const codeType = codeInfo.children![0].text!; - const codeTextNode = findNodeOfType(n, "CodeText"); - try { - // This will error out if this is not a code wiget, which is fine - const langCallback = client.clientSystem.codeWidgetHook - .codeWidgetCallbacks.get(codeType); - if (!langCallback) { - return { - text: "", - }; - } - const result = await langCallback( - renderToText(codeTextNode!), - pageName, - ); - if (!result) { - return { - text: "", - }; - } - // Only do this for "markdown" widgets, that is: that can render to markdown - if (result.markdown !== undefined) { - const parsedBody = parseMarkdown(result.markdown); - // Recursively process - return expandCodeWidgets( - client, - parsedBody, - pageName, - env, - sf, - ); - } - } catch (e: any) { - // 'not found' is to be expected (no code widget configured for this language) - // Every other error should probably be reported - if (!e.message.includes("not found")) { - console.trace(); - console.error("Error rendering code", e.message); - } - } - } else if (n.type === "Image") { + if (n.type === "Image") { // Let's scan for ![[embeds]] that are codified as Images, confusingly const wikiLinkMark = findNodeOfType(n, "WikiLinkMark"); if (!wikiLinkMark) { @@ -104,10 +57,9 @@ export async function expandCodeWidgets( const { text } = await client.space.readPage(ref.page); const parsedBody = parseMarkdown(text); // Recursively process - return expandCodeWidgets( + return expandMarkdown( client, parsedBody, - page, env, sf, ); diff --git a/common/syscalls/markdown.ts b/common/syscalls/markdown.ts index 5c5a5f65..fd25dd08 100644 --- a/common/syscalls/markdown.ts +++ b/common/syscalls/markdown.ts @@ -2,8 +2,11 @@ import type { SysCallMapping } from "../../lib/plugos/system.ts"; import { parse } from "../markdown_parser/parse_tree.ts"; import { type ParseTree, renderToText } from "../../plug-api/lib/tree.ts"; import { extendedMarkdownLanguage } from "../markdown_parser/parser.ts"; +import { expandMarkdown } from "$common/markdown.ts"; +import type { Client } from "../../web/client.ts"; +import { LuaStackFrame } from "$common/space_lua/runtime.ts"; -export function markdownSyscalls(): SysCallMapping { +export function markdownSyscalls(client: Client): SysCallMapping { return { "markdown.parseMarkdown": (_ctx, text: string): ParseTree => { return parse(extendedMarkdownLanguage, text); @@ -11,5 +14,13 @@ export function markdownSyscalls(): SysCallMapping { "markdown.renderParseTree": (_ctx, tree: ParseTree): string => { return renderToText(tree); }, + "markdown.expandMarkdown": (_ctx, tree: ParseTree): Promise => { + return expandMarkdown( + client, + tree, + client.clientSystem.spaceLuaEnv.env, + LuaStackFrame.lostFrame, + ); + }, }; } diff --git a/lib/manifest.ts b/lib/manifest.ts index 9eec2f61..93ea8983 100644 --- a/lib/manifest.ts +++ b/lib/manifest.ts @@ -4,7 +4,7 @@ import type { NamespaceOperation } from "./plugos/namespace.ts"; export type CodeWidgetT = { codeWidget?: string; - renderMode?: "markdown" | "iframe"; + renderMode?: "iframe"; }; export type CommandHookT = { @@ -37,10 +37,6 @@ export type MQHookT = { mqSubscriptions?: MQSubscription[]; }; -export type PanelWidgetT = { - panelWidget?: "top" | "bottom"; -}; - export type PlugNamespaceDef = { pattern: string; operation: NamespaceOperation; @@ -84,7 +80,6 @@ export type SilverBulletHooks = & MQHookT & EventHookT & CodeWidgetT - & PanelWidgetT & EndpointHookT & PlugNamespaceHookT & DocumentEditorT diff --git a/plug-api/syscalls/code_widget.ts b/plug-api/syscalls/code_widget.ts index 66fd7d28..4b27f2fc 100644 --- a/plug-api/syscalls/code_widget.ts +++ b/plug-api/syscalls/code_widget.ts @@ -1,21 +1,5 @@ -import type { CodeWidgetContent } from "../types.ts"; import { syscall } from "../syscall.ts"; -/** - * Renders a code widget. - * @param lang the language of the fenced code block - * @param body the body of the code to render - * @param pageName the name of the page the code widget appears on - * @returns the rendered code widget content - */ -export function render( - lang: string, - body: string, - pageName: string, -): Promise { - return syscall("codeWidget.render", lang, body, pageName); -} - /** * Refreshes all code widgets on the page that support it. */ diff --git a/plug-api/syscalls/markdown.ts b/plug-api/syscalls/markdown.ts index e5f4e5cf..972dbd41 100644 --- a/plug-api/syscalls/markdown.ts +++ b/plug-api/syscalls/markdown.ts @@ -18,3 +18,7 @@ export function parseMarkdown(text: string): Promise { export function renderParseTree(tree: ParseTree): Promise { return syscall("markdown.renderParseTree", tree); } + +export function expandMarkdown(tree: ParseTree): Promise { + return syscall("markdown.expandMarkdown", tree); +} diff --git a/plug-api/types.ts b/plug-api/types.ts index cb0971cd..c79f21ac 100644 --- a/plug-api/types.ts +++ b/plug-api/types.ts @@ -101,8 +101,7 @@ export type CodeWidgetCallback = ( ) => Promise; export type CodeWidgetContent = { - html?: string; - markdown?: string; + html: string; script?: string; buttons?: CodeWidgetButton[]; }; diff --git a/plugs/index/index.plug.yaml b/plugs/index/index.plug.yaml index 30d19aec..86f76712 100644 --- a/plugs/index/index.plug.yaml +++ b/plugs/index/index.plug.yaml @@ -169,12 +169,6 @@ functions: requireMode: rw requireEditor: page - # TOC - tocWidget: - path: toc.ts:widget - codeWidget: toc - renderMode: markdown - refreshWidgets: path: widget.ts:refreshWidgets diff --git a/plugs/index/toc.ts b/plugs/index/toc.ts deleted file mode 100644 index a4be77f4..00000000 --- a/plugs/index/toc.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { editor, markdown, YAML } from "@silverbulletmd/silverbullet/syscalls"; -import type { CodeWidgetContent } from "@silverbulletmd/silverbullet/types"; -import { stripMarkdown } from "@silverbulletmd/silverbullet/lib/markdown"; -import { traverseTree } from "@silverbulletmd/silverbullet/lib/tree"; - -type Header = { - name: string; - pos: number; - level: number; -}; - -type TocConfig = { - // Only show the TOC if there are at least this many headers - minHeaders?: number; - // Don't show the TOC if there are more than this many headers - maxHeaders?: number; - header?: boolean; - headerText?: string; - maxLevel?: number; - minLevel?: number; -}; - -export async function widget( - bodyText: string, -): Promise { - let config: TocConfig = {}; - if (bodyText.trim() !== "") { - config = await YAML.parse(bodyText); - } - - const page = await editor.getCurrentPage(); - const text = await editor.getText(); - const tree = await markdown.parseMarkdown(text); - const headers: Header[] = []; - traverseTree(tree, (n) => { - if (n.type?.startsWith("ATXHeading")) { - headers.push({ - name: n.children! - .slice(1) - .map(stripMarkdown) - .join("") - .trim(), - pos: n.from!, - level: +n.type[n.type.length - 1], - }); - - return true; - } - return false; - }); - - if (headers.length === 0) { - return null; - } - - if (config.minHeaders && headers.length < config.minHeaders) { - // Not enough headers, not showing TOC - return null; - } - if (config.maxHeaders && headers.length > config.maxHeaders) { - // Too many headers, not showing TOC - return null; - } - let headerText = (config.headerText ?? "# Table of Contents") + "\n"; - if (config.header === false) { - headerText = ""; - } - // Adjust level down if only sub-headers are used - let minLevel = headers.reduce( - (min, header) => Math.min(min, header.level), - 6, - ); - if (config.minLevel && config.minLevel > minLevel) minLevel = config.minLevel; - let renderedMd = headerText; - for (const header of headers) { - if ( - config.maxLevel && header.level > config.maxLevel || - (config.minLevel && header.level < config.minLevel) - ) { - continue; - } - renderedMd = renderedMd + " ".repeat((header.level - minLevel) * 2) + - "* [[" + - page + "@" + header.pos + "|" + header.name + "]]\n"; - } - - // console.log("Markdown", renderedMd); - return { - markdown: renderedMd, - buttons: [ - { - description: "Bake result", - svg: - ``, - invokeFunction: ["query.bakeButton", bodyText], - }, - { - description: "Edit", - svg: - ``, - invokeFunction: ["query.editButton", bodyText], - }, - { - description: "Reload", - svg: - ``, - invokeFunction: ["index.refreshWidgets"], - }, - ], - }; -} diff --git a/plugs/markdown/api.ts b/plugs/markdown/api.ts index 0f850653..9f90c7c8 100644 --- a/plugs/markdown/api.ts +++ b/plugs/markdown/api.ts @@ -1,123 +1,8 @@ -import { - findNodeOfType, - type ParseTree, - renderToText, - replaceNodesMatchingAsync, -} from "../../plug-api/lib/tree.ts"; -import { codeWidget, lua, space } from "@silverbulletmd/silverbullet/syscalls"; import { parseMarkdown } from "../../plug-api/syscalls/markdown.ts"; import { type MarkdownRenderOptions, renderMarkdownToHtml, } from "./markdown_render.ts"; -import { - type Ref, - validatePageName, -} from "@silverbulletmd/silverbullet/lib/page_ref"; -import { parseRef } from "@silverbulletmd/silverbullet/lib/page_ref"; -import type { LuaExpression } from "$common/space_lua/ast.ts"; -import { renderExpressionResult } from "$common/markdown_util.ts"; - -/** - * Finds code widgets, runs their plug code to render and inlines their content in the parse tree - * @param mdTree parsed markdown tree - * @param pageName name of the current page - * @returns modified mdTree - */ -export async function expandCodeWidgets( - mdTree: ParseTree, - pageName: string, -): Promise { - await replaceNodesMatchingAsync(mdTree, async (n) => { - if (n.type === "FencedCode") { - const codeInfo = findNodeOfType(n, "CodeInfo"); - if (!codeInfo) { - return; - } - const codeType = codeInfo.children![0].text!; - const codeTextNode = findNodeOfType(n, "CodeText"); - try { - // This will error out if this is not a code wiget, which is fine - const result = await codeWidget.render( - codeType, - renderToText(codeTextNode!), - pageName, - ); - if (!result) { - return { - text: "", - }; - } - // Only do this for "markdown" widgets, that is: that can render to markdown - if (result.markdown !== undefined) { - const parsedBody = await parseMarkdown(result.markdown); - // Recursively process - return expandCodeWidgets( - parsedBody, - pageName, - ); - } - } catch (e: any) { - // 'not found' is to be expected (no code widget configured for this language) - // Every other error should probably be reported - if (!e.message.includes("not found")) { - console.trace(); - console.error("Error rendering code", e.message); - } - } - } else if (n.type === "Image") { - // Let's scan for ![[embeds]] that are codified as Images, confusingly - const wikiLinkMark = findNodeOfType(n, "WikiLinkMark"); - if (!wikiLinkMark) { - return; - } - const wikiLinkPage = findNodeOfType(n, "WikiLinkPage"); - if (!wikiLinkPage) { - return; - } - - const page = wikiLinkPage.children![0].text!; - - // Check if this is likely a page link (based on the path format, e.g. if it contains an extension, it's probably not a page link) - let ref: Ref | undefined; - try { - ref = parseRef(page); - validatePageName(ref.page); - } catch { - // Not a valid page name, so not a page reference - return; - } - - const pageContent = await space.readPage(ref.page); - const parsedBody = await parseMarkdown(pageContent); - // Recursively process - return expandCodeWidgets( - parsedBody, - page, - ); - } else if (n.type === "LuaDirective") { - const expr = findNodeOfType(n, "LuaExpressionDirective") as - | LuaExpression - | null; - if (!expr) { - return; - } - const exprText = renderToText(expr); - - const result = await lua.evalExpression(exprText); - - if (result?._isWidget && result.markdown !== undefined) { - return await parseMarkdown(result.markdown); - } else if (result?._isWidget) { - return await parseMarkdown("**Widget did not return markdown**"); - } - - const markdown = await renderExpressionResult(result); - return await parseMarkdown(markdown); - } - }); - return mdTree; -} export async function markdownToHtml( markdown: string, diff --git a/plugs/markdown/markdown.plug.yaml b/plugs/markdown/markdown.plug.yaml index a00bf5d6..fc9f9b8f 100644 --- a/plugs/markdown/markdown.plug.yaml +++ b/plugs/markdown/markdown.plug.yaml @@ -3,9 +3,6 @@ assets: - "assets/*" functions: # API - expandCodeWidgets: - path: api.ts:expandCodeWidgets - syscall: markdown.expandCodeWidgets markdownToHtml: path: api.ts:markdownToHtml syscall: markdown.markdownToHtml diff --git a/plugs/markdown/preview.ts b/plugs/markdown/preview.ts index e470d0e1..0b42cc76 100644 --- a/plugs/markdown/preview.ts +++ b/plugs/markdown/preview.ts @@ -10,7 +10,6 @@ import { isLocalPath, resolvePath, } from "@silverbulletmd/silverbullet/lib/resolve"; -import { expandCodeWidgets } from "./api.ts"; export async function updateMarkdownPreview() { if (!(await clientStore.get("enableMarkdownPreview"))) { @@ -18,12 +17,12 @@ export async function updateMarkdownPreview() { } const currentPage = await editor.getCurrentPage(); const text = await editor.getText(); - const mdTree = await markdown.parseMarkdown(text); + let mdTree = await markdown.parseMarkdown(text); // const cleanMd = await cleanMarkdown(text); const css = await asset.readAsset("markdown", "assets/preview.css"); const js = await asset.readAsset("markdown", "assets/preview.js"); - await expandCodeWidgets(mdTree, currentPage); + mdTree = await markdown.expandMarkdown(mdTree); const html = renderMarkdownToHtml(mdTree, { smartHardBreak: true, annotationPositions: true, diff --git a/plugs/share/share.ts b/plugs/share/share.ts index f71a6fb7..cb25d09b 100644 --- a/plugs/share/share.ts +++ b/plugs/share/share.ts @@ -60,13 +60,8 @@ export function clipboardShareOptions() { } export async function clipboardMarkdownShare(text: string) { - const pageName = await editor.getCurrentPage(); const tree = await markdown.parseMarkdown(text); - let rendered = await system.invokeFunction( - "markdown.expandCodeWidgets", - tree, - pageName, - ); + let rendered = await markdown.expandMarkdown(tree); rendered = cleanMarkdown(rendered); await editor.copyToClipboard(renderToText(rendered).trim()); await editor.flashNotification("Copied to clipboard!"); @@ -100,13 +95,8 @@ export function cleanMarkdown(tree: ParseTree): ParseTree { } export async function clipboardRichTextShare(text: string) { - const pageName = await editor.getCurrentPage(); const tree = await markdown.parseMarkdown(text); - let rendered = await system.invokeFunction( - "markdown.expandCodeWidgets", - tree, - pageName, - ); + let rendered = await markdown.expandMarkdown(tree); rendered = cleanMarkdown(rendered); const html = await system.invokeFunction( "markdown.markdownToHtml", diff --git a/web/client_system.ts b/web/client_system.ts index 9921341f..8b365cb3 100644 --- a/web/client_system.ts +++ b/web/client_system.ts @@ -33,7 +33,6 @@ import { languageSyscalls } from "$common/syscalls/language.ts"; import { codeWidgetSyscalls } from "./syscalls/code_widget.ts"; import { clientCodeWidgetSyscalls } from "./syscalls/client_code_widget.ts"; import { KVPrimitivesManifestCache } from "$lib/plugos/manifest_cache.ts"; -import { PanelWidgetHook } from "./hooks/panel_widget.ts"; import { createKeyBindings } from "./editor_state.ts"; import { CommonSystem } from "$common/common_system.ts"; import type { DataStoreMQ } from "$lib/data/mq.datastore.ts"; @@ -83,10 +82,6 @@ export class ClientSystem extends CommonSystem { this.codeWidgetHook = new CodeWidgetHook(); this.system.addHook(this.codeWidgetHook); - // Panel widget hook - this.panelWidgetHook = new PanelWidgetHook(); - this.system.addHook(this.panelWidgetHook); - // Document editor hook this.documentEditorHook = new DocumentEditorHook(); this.system.addHook(this.documentEditorHook); @@ -152,7 +147,7 @@ export class ClientSystem extends CommonSystem { editorSyscalls(this.client), spaceReadSyscalls(this.client), systemSyscalls(client, false), - markdownSyscalls(), + markdownSyscalls(client), assetSyscalls(this.system), yamlSyscalls(), codeWidgetSyscalls(this.codeWidgetHook), diff --git a/web/cm_plugins/code_widget.ts b/web/cm_plugins/code_widget.ts new file mode 100644 index 00000000..0b4b6054 --- /dev/null +++ b/web/cm_plugins/code_widget.ts @@ -0,0 +1,35 @@ +export const activeWidgets = new Set(); + +export interface DomWidget { + dom?: HTMLElement; + renderContent( + div: HTMLElement, + cachedHtml: string | undefined, + ): Promise; +} + +export async function reloadAllWidgets() { + for (const widget of [...activeWidgets]) { + if (!widget.dom || !widget.dom.parentNode) { + activeWidgets.delete(widget); + continue; + } + // Create an empty widget DIV node + const newEl = document.createElement("div"); + await widget.renderContent(newEl, undefined); + // Replace the old widget with the new one + widget.dom.innerHTML = ""; + widget.dom.appendChild(newEl); + } +} + +function garbageCollectWidgets() { + for (const widget of activeWidgets) { + if (!widget.dom || !widget.dom.parentNode) { + // console.log("Garbage collecting widget", widget.bodyText); + activeWidgets.delete(widget); + } + } +} + +setInterval(garbageCollectWidgets, 5000); diff --git a/web/cm_plugins/fenced_code.ts b/web/cm_plugins/fenced_code.ts index 1cc4a84e..00d55bca 100644 --- a/web/cm_plugins/fenced_code.ts +++ b/web/cm_plugins/fenced_code.ts @@ -8,7 +8,6 @@ import { isCursorInRange, shouldRenderWidgets, } from "./util.ts"; -import { MarkdownWidget } from "./markdown_widget.ts"; import { IFrameWidget } from "./iframe_widget.ts"; export function fencedCodePlugin(client: Client) { @@ -26,10 +25,6 @@ export function fencedCodePlugin(client: Client) { const codeWidgetCallback = client.clientSystem.codeWidgetHook .codeWidgetCallbacks .get(lang); - const renderMode = client.clientSystem.codeWidgetHook.codeWidgetModes - .get( - lang, - ); // Only custom render when we have a custom renderer, and the current page is not a template if (codeWidgetCallback && shouldRenderWidgets(client)) { // We got a custom renderer! @@ -75,25 +70,13 @@ export function fencedCodePlugin(client: Client) { ); }); - const bodyText = lineStrings.slice(1, lineStrings.length - 1).join( - "\n", + const widget = new IFrameWidget( + from + lineStrings[0].length + 1, + to - lineStrings[lineStrings.length - 1].length - 1, + client, + lineStrings.slice(1, lineStrings.length - 1).join("\n"), + codeWidgetCallback, ); - const widget = renderMode === "markdown" - ? new MarkdownWidget( - from + lineStrings[0].length + 1, - client, - `widget:${client.currentPage}:${bodyText}`, - bodyText, - codeWidgetCallback, - "sb-markdown-widget", - ) - : new IFrameWidget( - from + lineStrings[0].length + 1, - to - lineStrings[lineStrings.length - 1].length - 1, - client, - lineStrings.slice(1, lineStrings.length - 1).join("\n"), - codeWidgetCallback, - ); widgets.push( Decoration.widget({ widget: widget, diff --git a/web/cm_plugins/inline_content.ts b/web/cm_plugins/inline_content.ts index d14a5da9..c4b53865 100644 --- a/web/cm_plugins/inline_content.ts +++ b/web/cm_plugins/inline_content.ts @@ -1,7 +1,6 @@ import type { EditorState, Range } from "@codemirror/state"; import { syntaxTree } from "@codemirror/language"; import { Decoration, WidgetType } from "@codemirror/view"; -import { MarkdownWidget } from "./markdown_widget.ts"; import { decoratorStateField, invisibleDecoration, @@ -15,6 +14,7 @@ import { } from "@silverbulletmd/silverbullet/lib/resolve"; import { parseRef } from "@silverbulletmd/silverbullet/lib/page_ref"; import { mime } from "mimetypes"; +import { LuaWidget } from "./lua_widget.ts"; type ContentDimensions = { width?: number; @@ -209,23 +209,21 @@ export function inlineContentPlugin(client: Client) { if ( client.clientSystem.allKnownFiles.has(pageRef.page + ".md") ) { - // This is a page reference, let's inline the content - const codeWidgetCallback = client.clientSystem.codeWidgetHook - .codeWidgetCallbacks.get("transclusion"); - - if (!codeWidgetCallback) { - return; - } - widgets.push( Decoration.widget({ - widget: new MarkdownWidget( - node.from, + widget: new LuaWidget( client, - `widget:${client.currentPage}:${text}`, - text, - codeWidgetCallback, - "sb-markdown-widget sb-markdown-widget-inline", + `widget:${client.currentPage}:${pageRef.page}`, + pageRef.page, + async (pageName) => { + const { text } = await client.space.readPage(pageName); + return { + _isWidget: true, + markdown: text, + }; + }, + true, + true, ), block: true, }).range(node.to), diff --git a/web/cm_plugins/lua_widget.ts b/web/cm_plugins/lua_widget.ts index 30e1f11f..4851bd27 100644 --- a/web/cm_plugins/lua_widget.ts +++ b/web/cm_plugins/lua_widget.ts @@ -8,14 +8,14 @@ import { import { parse } from "$common/markdown_parser/parse_tree.ts"; import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts"; import { renderToText } from "@silverbulletmd/silverbullet/lib/tree"; -import { activeWidgets } from "./markdown_widget.ts"; import { attachWidgetEventHandlers, moveCursorIntoText, } from "./widget_util.ts"; -import { expandCodeWidgets } from "$common/markdown.ts"; +import { expandMarkdown } from "$common/markdown.ts"; import { LuaStackFrame, LuaTable } from "$common/space_lua/runtime.ts"; import { jsonToMDTable } from "$common/markdown_util.ts"; +import { activeWidgets } from "./code_widget.ts"; export type LuaWidgetCallback = ( bodyText: string, @@ -126,10 +126,9 @@ export class LuaWidget extends WidgetType { const sf = LuaStackFrame.createWithGlobalEnv( client.clientSystem.spaceLuaEnv.env, ); - mdTree = await expandCodeWidgets( + mdTree = await expandMarkdown( client, mdTree, - this.client.currentPage, client.clientSystem.spaceLuaEnv.env, sf, ); diff --git a/web/cm_plugins/markdown_widget.ts b/web/cm_plugins/markdown_widget.ts deleted file mode 100644 index 64c3adfc..00000000 --- a/web/cm_plugins/markdown_widget.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { WidgetType } from "@codemirror/view"; -import type { Client } from "../client.ts"; -import type { - CodeWidgetButton, - CodeWidgetCallback, -} from "../../plug-api/types.ts"; -import { renderMarkdownToHtml } from "../../plugs/markdown/markdown_render.ts"; -import { - isLocalPath, - resolvePath, -} from "@silverbulletmd/silverbullet/lib/resolve"; -import { parse } from "$common/markdown_parser/parse_tree.ts"; -import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts"; -import { renderToText } from "@silverbulletmd/silverbullet/lib/tree"; -import { attachWidgetEventHandlers } from "./widget_util.ts"; - -export const activeWidgets = new Set(); - -export interface DomWidget { - dom?: HTMLElement; - renderContent( - div: HTMLElement, - cachedHtml: string | undefined, - ): Promise; -} - -export class MarkdownWidget extends WidgetType { - public dom?: HTMLElement; - - constructor( - readonly from: number | undefined, - readonly client: Client, - readonly cacheKey: string, - readonly bodyText: string, - readonly codeWidgetCallback: CodeWidgetCallback, - readonly className: string, - private tryInline = false, - ) { - super(); - } - - toDOM(): HTMLElement { - const wrapperSpan = document.createElement("span"); - wrapperSpan.className = "sb-markdown-wrapper"; - const innerDiv = document.createElement("div"); - wrapperSpan.appendChild(innerDiv); - const cacheItem = this.client.getWidgetCache(this.cacheKey); - if (cacheItem) { - innerDiv.innerHTML = this.wrapHtml(cacheItem.html, cacheItem.buttons); - } - - // Async kick-off of content renderer - this.renderContent(innerDiv, cacheItem?.html).catch(console.error); - this.dom = wrapperSpan; - return wrapperSpan; - } - - async renderContent( - div: HTMLElement, - cachedHtml: string | undefined, - ) { - div.className = this.className; - const widgetContent = await this.codeWidgetCallback( - this.bodyText, - this.client.currentPage, - ); - activeWidgets.add(this); - if (!widgetContent) { - div.innerHTML = ""; - this.client.setWidgetCache( - this.cacheKey, - { height: div.clientHeight, html: "" }, - ); - return; - } - let mdTree = parse( - extendedMarkdownLanguage, - widgetContent.markdown!, - ); - mdTree = await this.client.clientSystem.localSyscall( - "system.invokeFunction", - [ - "markdown.expandCodeWidgets", - mdTree, - this.client.currentPage, - ], - ); - const trimmedMarkdown = renderToText(mdTree).trim(); - - if (!trimmedMarkdown) { - // Net empty result after expansion - div.innerHTML = ""; - this.client.setWidgetCache( - this.cacheKey, - { height: div.clientHeight, html: "" }, - ); - return; - } - - if (this.tryInline) { - if (trimmedMarkdown.includes("\n")) { - // Heuristic that this is going to be a multi-line output and we should render this as a HTML block - div.style.display = "block"; - } else { - div.style.display = "inline"; - } - } - - // Parse the markdown again after trimming - mdTree = parse( - extendedMarkdownLanguage, - trimmedMarkdown, - ); - - const html = renderMarkdownToHtml(mdTree, { - // Annotate every element with its position so we can use it to put - // the cursor there when the user clicks on the table. - annotationPositions: true, - translateUrls: (url) => { - if (isLocalPath(url)) { - url = resolvePath( - this.client.currentPage, - decodeURI(url), - ); - } - - return url; - }, - preserveAttributes: true, - }, this.client.ui.viewState.allPages); - - if (cachedHtml !== html) { - // Only update the DOM if the content has changed - div.innerHTML = this.wrapHtml( - html, - widgetContent.buttons, - ); - } - if (html) { - this.attachListeners(div, widgetContent.buttons); - } - - // Let's give it a tick, then measure and cache - setTimeout(() => { - this.client.setWidgetCache( - this.cacheKey, - { - height: div.offsetHeight, - html, - buttons: widgetContent.buttons, - }, - ); - // Because of the rejiggering of the DOM, we need to do a no-op cursor move to make sure it's positioned correctly - this.client.editorView.dispatch({ - selection: { - anchor: this.client.editorView.state.selection.main.anchor, - }, - }); - }); - } - - private wrapHtml( - html: string, - buttons: CodeWidgetButton[] = [], - ) { - if (!html) { - return ""; - } - if (buttons.length === 0) { - return html; - } else { - return `
${ - buttons.filter((button) => !button.widgetTarget).map((button, idx) => - ` ` - ).join("") - }
${html}
`; - } - } - - private attachListeners(div: HTMLElement, buttons?: CodeWidgetButton[]) { - attachWidgetEventHandlers(div, this.client, this.bodyText); - - if (!buttons) { - buttons = []; - } - - for (let i = 0; i < buttons.length; i++) { - const button = buttons[i]; - if (button.widgetTarget) { - div.addEventListener("click", () => { - console.log("Widget clicked"); - this.client.clientSystem.localSyscall("system.invokeFunction", [ - button.invokeFunction[0], - this.from, - ]).catch(console.error); - }); - } else { - div.querySelector(`button[data-button="${i}"]`)!.addEventListener( - "click", - (e) => { - e.stopPropagation(); - this.client.clientSystem.localSyscall( - "system.invokeFunction", - button.invokeFunction, - ).then((newContent: string | undefined) => { - if (newContent) { - div.innerText = newContent; - } - this.client.focus(); - }).catch(console.error); - }, - ); - } - } - } - - override get estimatedHeight(): number { - const cacheItem = this.client.getWidgetCache(this.cacheKey); - return cacheItem ? cacheItem.height : -1; - } - - override eq(other: WidgetType): boolean { - return ( - other instanceof MarkdownWidget && - other.bodyText === this.bodyText && other.cacheKey === this.cacheKey && - this.from === other.from - ); - } -} - -export async function reloadAllWidgets() { - for (const widget of [...activeWidgets]) { - if (!widget.dom || !widget.dom.parentNode) { - activeWidgets.delete(widget); - continue; - } - // Create an empty widget DIV node - const newEl = document.createElement("div"); - await widget.renderContent(newEl, undefined); - // Replace the old widget with the new one - widget.dom.innerHTML = ""; - widget.dom.appendChild(newEl); - } -} - -function garbageCollectWidgets() { - for (const widget of activeWidgets) { - if (!widget.dom || !widget.dom.parentNode) { - // console.log("Garbage collecting widget", widget.bodyText); - activeWidgets.delete(widget); - } - } -} - -setInterval(garbageCollectWidgets, 5000); diff --git a/web/cm_plugins/top_bottom_panels.ts b/web/cm_plugins/top_bottom_panels.ts index 3d9598ee..af5c45ea 100644 --- a/web/cm_plugins/top_bottom_panels.ts +++ b/web/cm_plugins/top_bottom_panels.ts @@ -2,13 +2,11 @@ import type { EditorState } from "@codemirror/state"; import { Decoration } from "@codemirror/view"; import type { Client } from "../client.ts"; import { decoratorStateField } from "./util.ts"; -import { MarkdownWidget } from "./markdown_widget.ts"; import { LuaWidget, type LuaWidgetContent } from "./lua_widget.ts"; export function postScriptPrefacePlugin( editor: Client, ) { - const panelWidgetHook = editor.clientSystem.panelWidgetHook; return decoratorStateField((state: EditorState) => { if (!editor.clientSystem.scriptsLoaded) { console.info("System not yet ready, not rendering panel widgets."); @@ -16,24 +14,6 @@ export function postScriptPrefacePlugin( } const widgets: any[] = []; - // Plug based hooks - const topCallback = panelWidgetHook.callbacks.get("top"); - if (topCallback) { - widgets.push( - Decoration.widget({ - widget: new MarkdownWidget( - undefined, - editor, - `top:${editor.currentPage}`, - "top", - topCallback, - "sb-markdown-top-widget", - ), - side: -1, - block: true, - }).range(0), - ); - } // Event driven hooks widgets.push( Decoration.widget({ @@ -76,26 +56,7 @@ export function postScriptPrefacePlugin( ); // Bottom widgets - // Hook driven widgets - const bottomCallback = panelWidgetHook.callbacks.get("bottom"); - if (bottomCallback) { - widgets.push( - Decoration.widget({ - widget: new MarkdownWidget( - undefined, - editor, - `bottom:${editor.currentPage}`, - "bottom", - bottomCallback, - "sb-markdown-bottom-widget", - ), - side: 1, - block: true, - }).range(state.doc.length), - ); - } - // Event driven widgets widgets.push( Decoration.widget({ widget: new LuaWidget( diff --git a/web/hooks/panel_widget.ts b/web/hooks/panel_widget.ts deleted file mode 100644 index 89a5fc67..00000000 --- a/web/hooks/panel_widget.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { Hook, Manifest } from "../../lib/plugos/types.ts"; -import type { System } from "../../lib/plugos/system.ts"; -import type { CodeWidgetCallback } from "../../plug-api/types.ts"; -import type { PanelWidgetT } from "$lib/manifest.ts"; - -export class PanelWidgetHook implements Hook { - callbacks = new Map(); - - constructor() { - } - - collectAllPanelWidgets(system: System) { - this.callbacks.clear(); - for (const plug of system.loadedPlugs.values()) { - for ( - const [name, functionDef] of Object.entries( - plug.manifest!.functions, - ) - ) { - if (!functionDef.panelWidget) { - continue; - } - this.callbacks.set( - functionDef.panelWidget, - (bodyText, pageName) => { - return plug.invoke(name, [bodyText, pageName]); - }, - ); - } - } - } - - apply(system: System): void { - this.collectAllPanelWidgets(system); - system.on({ - plugLoaded: () => { - this.collectAllPanelWidgets(system); - }, - }); - } - - validateManifest(manifest: Manifest): string[] { - const errors = []; - for (const functionDef of Object.values(manifest.functions)) { - if (!functionDef.panelWidget) { - continue; - } - if (!["top", "bottom"].includes(functionDef.panelWidget)) { - errors.push( - `Panel widgets must be attached to either 'top' or 'bottom'.`, - ); - } - } - return errors; - } -} diff --git a/web/syscalls/client_code_widget.ts b/web/syscalls/client_code_widget.ts index d647a9fb..90ec2f94 100644 --- a/web/syscalls/client_code_widget.ts +++ b/web/syscalls/client_code_widget.ts @@ -1,5 +1,5 @@ import type { SysCallMapping } from "../../lib/plugos/system.ts"; -import { reloadAllWidgets } from "../cm_plugins/markdown_widget.ts"; +import { reloadAllWidgets } from "../cm_plugins/code_widget.ts"; import { broadcastReload } from "../components/widget_sandbox_iframe.ts"; export function clientCodeWidgetSyscalls(): SysCallMapping {