import type { EditorState } from "@codemirror/state"; import { syntaxTree } from "@codemirror/language"; import { Decoration, WidgetType } from "@codemirror/view"; import { decoratorStateField, invisibleDecoration, isCursorInRange, } from "./util.ts"; import { renderMarkdownToHtml } from "../markdown_renderer/markdown_render.ts"; import { type ParseTree, renderToText, } from "@silverbulletmd/silverbullet/lib/tree"; import { lezerToParseTree } from "../markdown_parser/parse_tree.ts"; import type { Client } from "../client.ts"; import { isLocalURL, resolveMarkdownLink, } from "@silverbulletmd/silverbullet/lib/resolve"; import { expandMarkdown } from "../markdown_renderer/inline.ts"; import { attachWidgetEventHandlers } from "./widget_util.ts"; class TableViewWidget extends WidgetType { tableBodyText: string; constructor( readonly client: Client, readonly t: ParseTree, ) { super(); this.tableBodyText = renderToText(t); } override get estimatedHeight(): number { return this.client.widgetCache.getCachedWidgetHeight( `table:${this.tableBodyText}`, ); } toDOM(): HTMLElement { const dom = document.createElement("span"); dom.classList.add("sb-table-widget"); dom.addEventListener("click", (e) => { // Pulling data-pos to put the cursor in the right place, falling back // to the start of the table. const dataAttributes = (e.target as any).dataset; const fallbackPos = this.client.editorView.posAtDOM(dom, 0); this.client.editorView.dispatch({ selection: { anchor: dataAttributes.pos ? +dataAttributes.pos : fallbackPos, }, }); }); void expandMarkdown( this.client.space, this.client.currentName(), this.t, this.client.clientSystem.spaceLuaEnv, ).then((t) => { dom.innerHTML = renderMarkdownToHtml(t, { // 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, shortWikiLinks: this.client.config.get("shortWikiLinks", false), translateUrls: (url) => { if (isLocalURL(url)) { url = resolveMarkdownLink( this.client.currentName(), decodeURI(url), ); } return url; }, }); setTimeout(() => { // Give it a tick to render attachWidgetEventHandlers(dom, this.client, this.tableBodyText); this.client.widgetCache.setCachedWidgetHeight( `table:${this.tableBodyText}`, dom.clientHeight, ); }); }); return dom; } override eq(other: WidgetType): boolean { return ( other instanceof TableViewWidget && other.tableBodyText === this.tableBodyText ); } } export function tablePlugin(editor: Client) { return decoratorStateField((state: EditorState) => { const widgets: any[] = []; syntaxTree(state).iterate({ enter: (node) => { const { from, to, name } = node; if (name !== "Table") return; if (isCursorInRange(state, [from, to])) return; const tableText = state.sliceDoc(from, to); const lineStrings = tableText.split("\n"); const lines: { from: number; to: number }[] = []; let fromIt = from; for (const line of lineStrings) { lines.push({ from: fromIt, to: fromIt + line.length, }); fromIt += line.length + 1; } const firstLine = lines[0], lastLine = lines[lines.length - 1]; // In case of doubt, back out if (!firstLine || !lastLine) return; widgets.push(invisibleDecoration.range(firstLine.from, firstLine.to)); widgets.push(invisibleDecoration.range(lastLine.from, lastLine.to)); lines.slice(1, lines.length - 1).forEach((line) => { widgets.push( Decoration.line({ class: "sb-line-table-outside" }).range( line.from, ), ); }); const text = state.sliceDoc(0, to); widgets.push( Decoration.widget({ widget: new TableViewWidget( editor, lezerToParseTree(text, node.node), ), }).range(from), ); }, }); return Decoration.set(widgets, true); }); }