import { WidgetType } from "../deps.ts"; import type { Client } from "../client.ts"; import type { CodeWidgetCallback } from "$sb/types.ts"; import { renderMarkdownToHtml } from "../../plugs/markdown/markdown_render.ts"; import { resolveAttachmentPath } from "$sb/lib/resolve.ts"; import { parse } from "../../common/markdown_parser/parse_tree.ts"; import buildMarkdown from "../../common/markdown_parser/parser.ts"; import { renderToText } from "$sb/lib/tree.ts"; export class MarkdownWidget extends WidgetType { renderedMarkdown?: string; constructor( readonly from: number, readonly to: number, readonly client: Client, readonly bodyText: string, readonly codeWidgetCallback: CodeWidgetCallback, ) { super(); } toDOM(): HTMLElement { const div = document.createElement("div"); div.className = "sb-markdown-widget"; const cacheItem = this.client.getWidgetCache(this.bodyText); if (cacheItem) { div.innerHTML = this.wrapHtml(cacheItem.html); this.attachListeners(div); } // Async kick-off of content renderer this.renderContent(div, cacheItem?.html).catch(console.error); return div; } private async renderContent( div: HTMLElement, cachedHtml: string | undefined, ) { const widgetContent = await this.codeWidgetCallback( this.bodyText, this.client.currentPage!, ); const lang = buildMarkdown(this.client.system.mdExtensions); let mdTree = parse( lang, widgetContent.markdown!, ); mdTree = await this.client.system.localSyscall( "system.invokeFunction", [ "markdown.expandCodeWidgets", mdTree, this.client.currentPage, ], ); // Used for the source button this.renderedMarkdown = renderToText(mdTree); 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 (!url.includes("://")) { url = resolveAttachmentPath( this.client.currentPage!, decodeURI(url), ); } return url; }, preserveAttributes: true, }); if (cachedHtml === html) { // HTML still same as in cache, no need to re-render return; } div.innerHTML = this.wrapHtml(html); this.attachListeners(div); // Let's give it a tick, then measure and cache setTimeout(() => { this.client.setWidgetCache( this.bodyText, div.clientHeight, html, ); }); } private wrapHtml(html: string) { return `
${html}`; } private attachListeners(div: HTMLElement) { div.querySelectorAll("a[data-ref]").forEach((el_) => { const el = el_ as HTMLElement; // Override default click behavior with a local navigate (faster) el.addEventListener("click", (e) => { e.preventDefault(); this.client.navigate(el.dataset.ref!); }); }); // Implement task toggling div.querySelectorAll("span[data-external-task-ref]").forEach((el: any) => { const taskRef = el.dataset.externalTaskRef; el.querySelector("input[type=checkbox]").addEventListener( "change", (e: any) => { const oldState = e.target.dataset.state; const newState = oldState === " " ? "x" : " "; // Update state in DOM as well for future toggles e.target.dataset.state = newState; console.log("Toggling task", taskRef); this.client.system.localSyscall( "system.invokeFunction", ["tasks.updateTaskState", taskRef, oldState, newState], ).catch( console.error, ); }, ); }); div.querySelector(".edit-button")!.addEventListener("click", () => { this.client.editorView.dispatch({ selection: { anchor: this.from }, }); this.client.focus(); }); 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 { const cacheItem = this.client.getWidgetCache(this.bodyText); // console.log("Calling estimated height", cacheItem); return cacheItem ? cacheItem.height : -1; } eq(other: WidgetType): boolean { return ( other instanceof MarkdownWidget && other.bodyText === this.bodyText ); } }