import type { Client } from "../client.ts"; import type { Range } from "@codemirror/state"; import { syntaxTree } from "@codemirror/language"; import { Decoration, type DecorationSet, type EditorView, ViewPlugin, type ViewUpdate, WidgetType, } from "@codemirror/view"; const ICON_SVG = ''; const EXCLUDE_LANGUAGES = ["template", "include", "query", "toc", "embed"]; class CodeCopyWidget extends WidgetType { constructor(readonly value: string, readonly client: Client) { super(); } override eq(other: CodeCopyWidget) { return other.value == this.value; } toDOM() { const wrap = document.createElement("span"); // wrap.setAttribute("aria-hidden", "true"); wrap.className = "sb-actions"; const button = wrap.appendChild(document.createElement("button")); button.type = "button"; button.title = "Copy to clipboard"; button.className = "sb-code-copy-button"; button.innerHTML = ICON_SVG; button.title = "Copy"; button.onclick = (e) => { e.stopPropagation(); e.preventDefault(); navigator.clipboard.writeText(this.value) .catch((err) => { this.client.flashNotification( `Error copying to clipboard: ${err}`, "error", ); }) .then(() => { this.client.flashNotification("Copied to clipboard", "info"); }); }; return wrap; } override ignoreEvent() { return true; } } function codeCopyDecoration( view: EditorView, client: Client, ) { const widgets: Range[] = []; for (const { from, to } of view.visibleRanges) { syntaxTree(view.state).iterate({ from, to, enter: (node) => { if (node.name == "FencedCode") { const textNodes = node.node.getChildren("CodeText"); const infoNode = node.node.getChild("CodeInfo"); if (textNodes.length === 0) { return; } const language = infoNode ? view.state.doc.sliceString( infoNode.from, infoNode.to, ) : undefined; if (language && EXCLUDE_LANGUAGES.includes(language)) { return; } // Accumulate the text content of the code block let text = ""; for (const textNode of textNodes) { text += view.state.doc.sliceString(textNode.from, textNode.to); } const deco = Decoration.widget({ widget: new CodeCopyWidget(text, client), side: 0, }); widgets.push(deco.range(node.from)); } }, }); } return Decoration.set(widgets); } export const codeCopyPlugin = (client: Client) => { return ViewPlugin.fromClass( class { decorations: DecorationSet; constructor(view: EditorView) { this.decorations = codeCopyDecoration(view, client); } update(update: ViewUpdate) { if ( update.docChanged || update.viewportChanged || syntaxTree(update.startState) != syntaxTree(update.state) ) { this.decorations = codeCopyDecoration(update.view, client); } } }, { decorations: (v) => v.decorations, }, ); };