2024-07-30 23:33:33 +08:00
|
|
|
import type { Client } from "../client.ts";
|
|
|
|
import type { Range } from "@codemirror/state";
|
2024-03-16 22:29:24 +08:00
|
|
|
import { syntaxTree } from "@codemirror/language";
|
2024-02-23 17:12:48 +08:00
|
|
|
import {
|
|
|
|
Decoration,
|
2024-07-30 23:33:33 +08:00
|
|
|
type DecorationSet,
|
|
|
|
type EditorView,
|
2024-02-23 17:12:48 +08:00
|
|
|
ViewPlugin,
|
2024-07-30 23:33:33 +08:00
|
|
|
type ViewUpdate,
|
2024-02-23 17:12:48 +08:00
|
|
|
WidgetType,
|
2024-03-16 22:29:24 +08:00
|
|
|
} from "@codemirror/view";
|
2024-02-23 17:12:48 +08:00
|
|
|
|
|
|
|
const ICON_SVG =
|
|
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-copy"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
|
|
|
|
|
2024-02-24 22:52:22 +08:00
|
|
|
const EXCLUDE_LANGUAGES = ["template", "include", "query", "toc", "embed"];
|
2024-02-23 17:12:48 +08:00
|
|
|
|
|
|
|
class CodeCopyWidget extends WidgetType {
|
|
|
|
constructor(readonly value: string, readonly client: Client) {
|
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
2024-10-10 18:52:28 +08:00
|
|
|
override eq(other: CodeCopyWidget) {
|
2024-02-23 17:12:48 +08:00
|
|
|
return other.value == this.value;
|
|
|
|
}
|
|
|
|
|
|
|
|
toDOM() {
|
|
|
|
const wrap = document.createElement("span");
|
2024-08-20 16:52:08 +08:00
|
|
|
// wrap.setAttribute("aria-hidden", "true");
|
2024-02-23 17:12:48 +08:00
|
|
|
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(() => {
|
2024-02-23 20:54:51 +08:00
|
|
|
this.client.flashNotification("Copied to clipboard", "info");
|
2024-02-23 17:12:48 +08:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
return wrap;
|
|
|
|
}
|
|
|
|
|
2024-10-10 18:52:28 +08:00
|
|
|
override ignoreEvent() {
|
2024-02-23 17:12:48 +08:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function codeCopyDecoration(
|
|
|
|
view: EditorView,
|
|
|
|
client: Client,
|
|
|
|
) {
|
|
|
|
const widgets: Range<Decoration>[] = [];
|
|
|
|
for (const { from, to } of view.visibleRanges) {
|
|
|
|
syntaxTree(view.state).iterate({
|
|
|
|
from,
|
|
|
|
to,
|
|
|
|
enter: (node) => {
|
|
|
|
if (node.name == "FencedCode") {
|
2024-08-20 16:52:08 +08:00
|
|
|
const textNodes = node.node.getChildren("CodeText");
|
2024-02-23 17:12:48 +08:00
|
|
|
const infoNode = node.node.getChild("CodeInfo");
|
|
|
|
|
2024-08-20 16:52:08 +08:00
|
|
|
if (textNodes.length === 0) {
|
2024-02-23 17:12:48 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const language = infoNode
|
|
|
|
? view.state.doc.sliceString(
|
|
|
|
infoNode.from,
|
|
|
|
infoNode.to,
|
|
|
|
)
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
if (language && EXCLUDE_LANGUAGES.includes(language)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-08-20 16:52:08 +08:00
|
|
|
// Accumulate the text content of the code block
|
|
|
|
let text = "";
|
|
|
|
for (const textNode of textNodes) {
|
|
|
|
text += view.state.doc.sliceString(textNode.from, textNode.to);
|
|
|
|
}
|
2024-02-23 17:12:48 +08:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
};
|