diff --git a/common/manifest.ts b/common/manifest.ts index c3a8d3f..278de77 100644 --- a/common/manifest.ts +++ b/common/manifest.ts @@ -7,6 +7,7 @@ import { PlugNamespaceHookT } from "./hooks/plug_namespace.ts"; import { CodeWidgetT } from "../web/hooks/code_widget.ts"; import { MQHookT } from "../plugos/hooks/mq.ts"; import { EndpointHookT } from "../plugos/hooks/endpoint.ts"; +import { PanelWidgetT } from "../web/hooks/panel_widget.ts"; /** Silverbullet hooks give plugs access to silverbullet core systems. * @@ -22,6 +23,7 @@ export type SilverBulletHooks = & MQHookT & EventHookT & CodeWidgetT + & PanelWidgetT & EndpointHookT & PlugNamespaceHookT; diff --git a/plug-api/silverbullet-syscall/editor.ts b/plug-api/silverbullet-syscall/editor.ts index 1de7baa..372e1b2 100644 --- a/plug-api/silverbullet-syscall/editor.ts +++ b/plug-api/silverbullet-syscall/editor.ts @@ -80,7 +80,7 @@ export function filterBox( } export function showPanel( - id: "lhs" | "rhs" | "bhs" | "modal" | "bottom" | "top", + id: "lhs" | "rhs" | "bhs" | "modal", mode: number, html: string, script = "", @@ -89,7 +89,7 @@ export function showPanel( } export function hidePanel( - id: "lhs" | "rhs" | "bhs" | "modal" | "bottom" | "top", + id: "lhs" | "rhs" | "bhs" | "modal", ): Promise { return syscall("editor.hidePanel", id); } diff --git a/plugs/index/asset/linked_mentions.js b/plugs/index/asset/linked_mentions.js deleted file mode 100644 index 8ef20d1..0000000 --- a/plugs/index/asset/linked_mentions.js +++ /dev/null @@ -1,19 +0,0 @@ -function processClick(e) { - const dataEl = e.target.closest("[data-ref]"); - syscall( - "system.invokeFunction", - "index.navigateToMention", - dataEl.getAttribute("data-ref"), - ).catch(console.error); -} - -document.getElementById("link-ul").addEventListener("click", processClick); -document.getElementById("hide-button").addEventListener("click", () => { - syscall("system.invokeFunction", "index.toggleMentions").catch(console.error); -}); - -document.getElementById("reload-button").addEventListener("click", () => { - syscall("system.invokeFunction", "index.renderMentions").catch( - console.error, - ); -}); diff --git a/plugs/index/asset/style.css b/plugs/index/asset/style.css deleted file mode 100644 index f25326f..0000000 --- a/plugs/index/asset/style.css +++ /dev/null @@ -1,76 +0,0 @@ -/* Reset SB styles */ -html, -body { - /*height: initial !important; - overflow-x: initial !important; - overflow-y: hidden !important;*/ - background-color: var(--root-background-color) !important; -} - -#sb-main { - height: initial !important; - display: initial !important; -} - -#sb-editor { - flex: initial !important; - height: initial !important; -} - -.cm-editor { - height: initial !important; -} - -body { - font-family: var(--ui-font); - - color: var(--root-color); -} - -.sb-line-h2 { - border-top-right-radius: 5px; - border-top-left-radius: 5px; - margin: 0; - padding: 10px !important; - background-color: rgba(233, 233, 233, 0.5); -} - -#button-bar { - position: absolute; - right: 10px; - top: 10px; - padding: 0 3px; -} - -#button-bar button { - border: none; - background: none; - cursor: pointer; - color: var(--root-color); -} - -#edit-button { - margin-left: -10px; -} - - -li code { - font-size: 80%; - color: #a5a4a4; -} - -li.toc-header-1 { - margin-left: 0; -} - -li.toc-header-2 { - margin-left: 2ch; -} - -li.toc-header-3 { - margin-left: 4ch; -} - -li.toc-header-4 { - margin-left: 6ch; -} \ No newline at end of file diff --git a/plugs/index/asset/toc.js b/plugs/index/asset/toc.js deleted file mode 100644 index 48e99f3..0000000 --- a/plugs/index/asset/toc.js +++ /dev/null @@ -1,26 +0,0 @@ -function processClick(e) { - const dataEl = e.target.closest("[data-ref]"); - syscall( - "system.invokeFunction", - "index.navigateToMention", - dataEl.getAttribute("data-ref"), - ).catch(console.error); -} - -document.getElementById("link-ul").addEventListener("click", processClick); -document.getElementById("hide-button").addEventListener("click", () => { - syscall("system.invokeFunction", "index.toggleTOC").catch(console.error); -}); - -document.body.addEventListener("mouseenter", () => { - console.log("Refreshing on focus"); - syscall("system.invokeFunction", "index.renderTOC").catch( - console.error, - ); -}); - -document.getElementById("reload-button").addEventListener("click", () => { - syscall("system.invokeFunction", "index.renderTOC").catch( - console.error, - ); -}); diff --git a/plugs/index/index.plug.yaml b/plugs/index/index.plug.yaml index b2e415b..fad311f 100644 --- a/plugs/index/index.plug.yaml +++ b/plugs/index/index.plug.yaml @@ -10,8 +10,6 @@ syntax: - "$" regex: "\\$[a-zA-Z\\.\\-\\/]+[\\w\\.\\-\\/]*" className: sb-named-anchor -assets: - - asset/* functions: loadBuiltinsIntoIndex: path: builtins.ts:loadBuiltinsIntoIndex @@ -159,20 +157,14 @@ functions: # Mentions panel (postscript) toggleMentions: - path: "./mentions_ps.ts:toggleMentions" + path: "./linked_mentions.ts:toggleMentions" command: name: "Mentions: Toggle" key: ctrl-alt-m - updateMentions: - path: "./mentions_ps.ts:updateMentions" - env: client - events: - - editor:pageLoaded - navigateToMention: - path: "./mentions_ps.ts:navigate" renderMentions: - path: "./mentions_ps.ts:renderMentions" + path: "./linked_mentions.ts:renderMentions" + panelWidget: bottom # TOC toggleTOC: @@ -184,8 +176,7 @@ functions: renderTOC: path: toc.ts:renderTOC env: client - events: - - editor:pageLoaded + panelWidget: top lintYAML: path: lint.ts:lintYAML diff --git a/plugs/index/linked_mentions.ts b/plugs/index/linked_mentions.ts new file mode 100644 index 0000000..9398ffc --- /dev/null +++ b/plugs/index/linked_mentions.ts @@ -0,0 +1,46 @@ +import { clientStore, editor, system } from "$sb/silverbullet-syscall/mod.ts"; +import { CodeWidgetContent } from "$sb/types.ts"; +import { queryObjects } from "./api.ts"; +import { LinkObject } from "./page_links.ts"; + +const hideMentionsKey = "hideMentions"; + +export async function toggleMentions() { + let hideMentions = await clientStore.get(hideMentionsKey); + hideMentions = !hideMentions; + await clientStore.set(hideMentionsKey, hideMentions); + if (!hideMentions) { + await renderMentions(); + } else { + await editor.dispatch({}); + } +} + +export async function renderMentions(): Promise { + const page = await editor.getCurrentPage(); + const linksResult = await queryObjects("link", { + // Query all links that point to this page, excluding those that are inside directives and self pointers. + filter: ["and", ["!=", ["attr", "page"], ["string", page]], ["and", ["=", [ + "attr", + "toPage", + ], ["string", page]], ["=", ["attr", "inDirective"], ["boolean", false]]]], + }); + if (linksResult.length === 0) { + // Don't show the panel if there are no links here. + return null; + } else { + let renderedMd = "# Linked Mentions\n"; + for (const link of linksResult) { + let snippet = await system.invokeFunction( + "markdown.markdownToHtml", + link.snippet, + ); + // strip HTML tags + snippet = snippet.replace(/<[^>]*>?/gm, ""); + renderedMd += `* [[${link.ref}]]: ...${snippet}...\n`; + } + return { + markdown: renderedMd, + }; + } +} diff --git a/plugs/index/mentions_ps.ts b/plugs/index/mentions_ps.ts deleted file mode 100644 index 4c86a68..0000000 --- a/plugs/index/mentions_ps.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { asset } from "$sb/plugos-syscall/mod.ts"; -import { clientStore, editor } from "$sb/silverbullet-syscall/mod.ts"; -import { queryObjects } from "./api.ts"; -import { LinkObject } from "./page_links.ts"; - -const hideMentionsKey = "hideMentions"; - -export async function toggleMentions() { - let hideMentions = await clientStore.get(hideMentionsKey); - hideMentions = !hideMentions; - await clientStore.set(hideMentionsKey, hideMentions); - if (!hideMentions) { - await renderMentions(); - } else { - await editor.hidePanel("bottom"); - } -} - -// Triggered when switching pages or upon first load -export async function updateMentions() { - if (await clientStore.get(hideMentionsKey)) { - return; - } - await renderMentions(); -} - -// use internal navigation via syscall to prevent reloading the full page. -export async function navigate(ref: string) { - const currentPage = await editor.getCurrentPage(); - const [page, pos] = ref.split(/[@$]/); - if (page === currentPage) { - await editor.moveCursor(+pos, true); - } else { - await editor.navigate(page, +pos); - } -} - -function escapeHtml(unsafe: string) { - return unsafe.replace(/&/g, "&").replace(//g, - ">", - ); -} - -export async function renderMentions() { - const page = await editor.getCurrentPage(); - const linksResult = await queryObjects("link", { - // Query all links that point to this page, excluding those that are inside directives and self pointers. - filter: ["and", ["!=", ["attr", "page"], ["string", page]], ["and", ["=", [ - "attr", - "toPage", - ], ["string", page]], ["=", ["attr", "inDirective"], ["boolean", false]]]], - }); - if (linksResult.length === 0) { - // Don't show the panel if there are no links here. - await editor.hidePanel("bottom"); - } else { - const css = await asset.readAsset("asset/style.css"); - const js = await asset.readAsset("asset/linked_mentions.js"); - - await editor.showPanel( - "bottom", - 1, - ` -
-
- - -
-
Linked Mentions
- -
- `, - js, - ); - } -} diff --git a/plugs/index/toc.ts b/plugs/index/toc.ts index c191593..9209d8a 100644 --- a/plugs/index/toc.ts +++ b/plugs/index/toc.ts @@ -5,7 +5,7 @@ import { system, } from "$sb/silverbullet-syscall/mod.ts"; import { renderToText, traverseTree, traverseTreeAsync } from "$sb/lib/tree.ts"; -import { asset } from "$sb/syscalls.ts"; +import { CodeWidgetContent } from "$sb/types.ts"; const hideTOCKey = "hideTOC"; const headerThreshold = 3; @@ -30,9 +30,11 @@ async function markdownToHtml(text: string): Promise { return system.invokeFunction("markdown.markdownToHtml", text); } -export async function renderTOC(reload = false) { +export async function renderTOC( + reload = false, +): Promise { if (await clientStore.get(hideTOCKey)) { - return editor.hidePanel("top"); + return null; } const page = await editor.getCurrentPage(); const text = await editor.getText(); @@ -41,9 +43,7 @@ export async function renderTOC(reload = false) { await traverseTreeAsync(tree, async (n) => { if (n.type?.startsWith("ATXHeading")) { headers.push({ - name: await markdownToHtml( - n.children!.slice(1).map(renderToText).join("").trim(), - ), + name: n.children!.slice(1).map(renderToText).join("").trim(), pos: n.from!, level: +n.type[n.type.length - 1], }); @@ -52,39 +52,19 @@ export async function renderTOC(reload = false) { } return false; }); - // console.log("All headers", headers); - if (!reload && cachedTOC === JSON.stringify(headers)) { - // TOC is the same, not updating - return; - } - cachedTOC = JSON.stringify(headers); if (headers.length < headerThreshold) { - // console.log("Not enough headers, not showing TOC", headers.length); - await editor.hidePanel("top"); - return; + console.log("Not enough headers, not showing TOC", headers.length); + return null; } - const css = await asset.readAsset("asset/style.css"); - const js = await asset.readAsset("asset/toc.js"); - - await editor.showPanel( - "top", - 1, - ` -
-
- - -
-
Table of Contents
- -
- `, - js, - ); + // console.log("Headers", headers); + const renderedMd = "# Table of Contents\n" + + headers.map((header) => + `${ + " ".repeat((header.level - 1) * 2) + }* [[${page}@${header.pos}|${header.name}]]` + ).join("\n"); + // console.log("Markdown", renderedMd); + return { + markdown: renderedMd, + }; } diff --git a/web/client_system.ts b/web/client_system.ts index be53e5f..b111fca 100644 --- a/web/client_system.ts +++ b/web/client_system.ts @@ -41,6 +41,7 @@ import { clientCodeWidgetSyscalls } from "./syscalls/client_code_widget.ts"; import { KVPrimitivesManifestCache } from "../plugos/manifest_cache.ts"; import { deepObjectMerge } from "$sb/lib/json.ts"; import { Query } from "$sb/types.ts"; +import { PanelWidgetHook } from "./hooks/panel_widget.ts"; const plugNameExtractRegex = /\/(.+)\.plug\.js$/; @@ -51,6 +52,7 @@ export class ClientSystem { codeWidgetHook: CodeWidgetHook; mdExtensions: MDExt[] = []; system: System; + panelWidgetHook: PanelWidgetHook; constructor( private client: Client, @@ -83,6 +85,10 @@ export class ClientSystem { this.codeWidgetHook = new CodeWidgetHook(); this.system.addHook(this.codeWidgetHook); + // Panel widget hook + this.panelWidgetHook = new PanelWidgetHook(); + this.system.addHook(this.panelWidgetHook); + // MQ hook if (client.syncMode) { // Process MQ messages locally diff --git a/web/cm_plugins/fenced_code.ts b/web/cm_plugins/fenced_code.ts index 89c711a..be218de 100644 --- a/web/cm_plugins/fenced_code.ts +++ b/web/cm_plugins/fenced_code.ts @@ -70,10 +70,10 @@ export function fencedCodePlugin(editor: Client) { const widget = renderMode === "markdown" ? new MarkdownWidget( from + lineStrings[0].length + 1, - to - lineStrings[lineStrings.length - 1].length - 1, editor, lineStrings.slice(1, lineStrings.length - 1).join("\n"), codeWidgetCallback, + "sb-markdown-widget", ) : new IFrameWidget( from + lineStrings[0].length + 1, diff --git a/web/cm_plugins/markdown_widget.ts b/web/cm_plugins/markdown_widget.ts index 0b89e5d..98c18b1 100644 --- a/web/cm_plugins/markdown_widget.ts +++ b/web/cm_plugins/markdown_widget.ts @@ -11,21 +11,25 @@ export class MarkdownWidget extends WidgetType { renderedMarkdown?: string; constructor( - readonly from: number, - readonly to: number, + readonly from: number | undefined, readonly client: Client, readonly bodyText: string, readonly codeWidgetCallback: CodeWidgetCallback, + readonly className: string, ) { super(); } toDOM(): HTMLElement { const div = document.createElement("div"); - div.className = "sb-markdown-widget"; + div.className = this.className; const cacheItem = this.client.getWidgetCache(this.bodyText); if (cacheItem) { - div.innerHTML = this.wrapHtml(cacheItem.html); + div.innerHTML = this.wrapHtml( + cacheItem.html, + this.from !== undefined, + this.from !== undefined, + ); this.attachListeners(div); } @@ -43,6 +47,11 @@ export class MarkdownWidget extends WidgetType { this.bodyText, this.client.currentPage!, ); + if (!widgetContent) { + div.innerHTML = ""; + // div.style.display = "none"; + return; + } const lang = buildMarkdown(this.client.system.mdExtensions); let mdTree = parse( lang, @@ -80,7 +89,11 @@ export class MarkdownWidget extends WidgetType { // HTML still same as in cache, no need to re-render return; } - div.innerHTML = this.wrapHtml(html); + div.innerHTML = this.wrapHtml( + html, + this.from !== undefined, + this.from !== undefined, + ); this.attachListeners(div); // Let's give it a tick, then measure and cache @@ -93,12 +106,20 @@ export class MarkdownWidget extends WidgetType { }); } - private wrapHtml(html: string) { + private wrapHtml(html: string, editButton = true, sourceButton = true) { return `
- + ${ + sourceButton + ? `` + : "" + } - + ${ + editButton + ? `` + : "" + }
${html}`; } @@ -109,7 +130,12 @@ export class MarkdownWidget extends WidgetType { // Override default click behavior with a local navigate (faster) el.addEventListener("click", (e) => { e.preventDefault(); - this.client.navigate(el.dataset.ref!); + const [pageName, pos] = el.dataset.ref!.split(/[$@]/); + if (pos && pos.match(/^\d+$/)) { + this.client.navigate(pageName, +pos); + } else { + this.client.navigate(pageName, pos); + } }); }); @@ -134,18 +160,20 @@ export class MarkdownWidget extends WidgetType { ); }); - div.querySelector(".edit-button")!.addEventListener("click", () => { - this.client.editorView.dispatch({ - selection: { anchor: this.from }, + if (this.from !== undefined) { + div.querySelector(".edit-button")!.addEventListener("click", () => { + this.client.editorView.dispatch({ + selection: { anchor: this.from! }, + }); + this.client.focus(); }); - this.client.focus(); - }); + div.querySelector(".source-button")!.addEventListener("click", () => { + div.innerText = this.renderedMarkdown!; + }); + } div.querySelector(".reload-button")!.addEventListener("click", () => { this.renderContent(div, undefined).catch(console.error); }); - div.querySelector(".source-button")!.addEventListener("click", () => { - div.innerText = this.renderedMarkdown!; - }); } get estimatedHeight(): number { diff --git a/web/cm_plugins/top_bottom_panels.ts b/web/cm_plugins/top_bottom_panels.ts index 824a802..ab18808 100644 --- a/web/cm_plugins/top_bottom_panels.ts +++ b/web/cm_plugins/top_bottom_panels.ts @@ -1,67 +1,40 @@ import { Decoration, EditorState, WidgetType } from "../deps.ts"; import type { Client } from "../client.ts"; import { decoratorStateField } from "./util.ts"; -import { PanelConfig } from "../types.ts"; -import { createWidgetSandboxIFrame } from "../components/widget_sandbox_iframe.ts"; +import { MarkdownWidget } from "./markdown_widget.ts"; -class IFrameWidget extends WidgetType { - widgetHeightCacheKey: string; - constructor( - readonly editor: Client, - readonly panel: PanelConfig, - readonly className: string, - ) { - super(); - this.widgetHeightCacheKey = `${this.editor.currentPage!}#${this.className}`; - } - - toDOM(): HTMLElement { - const iframe = createWidgetSandboxIFrame( - this.editor, - this.widgetHeightCacheKey, - this.panel, - ); - iframe.classList.add(this.className); - return iframe; - } - - get estimatedHeight(): number { - return this.editor.space.getCachedWidgetHeight( - this.widgetHeightCacheKey, - ); - } - - eq(other: WidgetType): boolean { - return this.panel.html === - (other as IFrameWidget).panel.html && - this.panel.script === - (other as IFrameWidget).panel.script; - } -} - -export function postScriptPrefacePlugin(editor: Client) { +export function postScriptPrefacePlugin( + editor: Client, +) { + const panelWidgetHook = editor.system.panelWidgetHook; return decoratorStateField((state: EditorState) => { const widgets: any[] = []; - if (editor.ui.viewState.panels.top.html) { + const topCallback = panelWidgetHook.callbacks.get("top"); + if (topCallback) { widgets.push( Decoration.widget({ - widget: new IFrameWidget( + widget: new MarkdownWidget( + undefined, editor, - editor.ui.viewState.panels.top, - "sb-top-iframe", + `top:${editor.currentPage}`, + topCallback, + "sb-markdown-top-widget", ), side: -1, block: true, }).range(0), ); } - if (editor.ui.viewState.panels.bottom.html) { + const bottomCallback = panelWidgetHook.callbacks.get("bottom"); + if (bottomCallback) { widgets.push( Decoration.widget({ - widget: new IFrameWidget( + widget: new MarkdownWidget( + undefined, editor, - editor.ui.viewState.panels.bottom, - "sb-bottom-iframe", + `bottom:${editor.currentPage}`, + bottomCallback, + "sb-markdown-bottom-widget", ), side: 1, block: true, diff --git a/web/hooks/panel_widget.ts b/web/hooks/panel_widget.ts new file mode 100644 index 0000000..3b7bd0c --- /dev/null +++ b/web/hooks/panel_widget.ts @@ -0,0 +1,59 @@ +import { Hook, Manifest } from "../../plugos/types.ts"; +import { System } from "../../plugos/system.ts"; +import { CodeWidgetCallback } from "$sb/types.ts"; + +export type PanelWidgetT = { + panelWidget?: "top" | "bottom"; +}; + +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/styles/editor.scss b/web/styles/editor.scss index 359807d..d9392e0 100644 --- a/web/styles/editor.scss +++ b/web/styles/editor.scss @@ -444,8 +444,31 @@ } .sb-markdown-widget { - overflow-y: scroll; margin: 0 0 -4ch 0; + } + + .sb-markdown-top-widget h1, + .sb-markdown-bottom-widget h1 { + border-top-right-radius: 5px; + border-top-left-radius: 5px; + margin: 0; + padding: 10px !important; + background-color: var(--editor-directive-background-color); + font-size: 1.2em; + } + + .sb-markdown-top-widget { + margin-bottom: 10px; + } + + .sb-markdown-bottom-widget { + margin-top: 10px; + } + + .sb-markdown-widget, + .sb-markdown-top-widget:has(*), + .sb-markdown-bottom-widget:has(*) { + overflow-y: scroll; border: 1px solid var(--editor-directive-background-color); border-radius: 5px; white-space: nowrap; diff --git a/web/types.ts b/web/types.ts index 908a301..989e8e5 100644 --- a/web/types.ts +++ b/web/types.ts @@ -98,8 +98,6 @@ export const initialViewState: AppViewState = { rhs: {}, bhs: {}, modal: {}, - top: {}, - bottom: {}, }, allPages: [], commands: new Map(),