Skip to content

Commit 98c08ca

Browse files
committed
feat: add dc html export pipeline
1 parent 7d3059e commit 98c08ca

13 files changed

Lines changed: 1538 additions & 0 deletions

src/lib/dc/clipboard.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
export type DcClipboardPayload = {
2+
"text/html": Blob;
3+
"text/plain": Blob;
4+
};
5+
6+
type ClipboardItemConstructor = new (items: {
7+
"text/html": Blob;
8+
"text/plain": Blob;
9+
}) => ClipboardItem;
10+
11+
export function createDcClipboardPayload(html: string, plainText: string): DcClipboardPayload {
12+
return {
13+
"text/html": new Blob([html], { type: "text/html" }),
14+
"text/plain": new Blob([plainText], { type: "text/plain" }),
15+
};
16+
}
17+
18+
function getClipboardItemConstructor(): ClipboardItemConstructor | undefined {
19+
if (typeof ClipboardItem !== "undefined") {
20+
return ClipboardItem as ClipboardItemConstructor;
21+
}
22+
23+
return undefined;
24+
}
25+
26+
export async function copyDcHtml(html: string, plainText: string): Promise<void> {
27+
const ClipboardItemCtor = getClipboardItemConstructor();
28+
29+
if (ClipboardItemCtor && typeof navigator !== "undefined" && navigator.clipboard?.write) {
30+
const item = new ClipboardItemCtor(createDcClipboardPayload(html, plainText));
31+
32+
await navigator.clipboard.write([item]);
33+
return;
34+
}
35+
36+
if (typeof window === "undefined" || typeof document === "undefined") {
37+
throw new Error("Clipboard is available only in the browser.");
38+
}
39+
40+
const target = document.createElement("div");
41+
target.setAttribute("contenteditable", "true");
42+
target.style.position = "fixed";
43+
target.style.left = "-10000px";
44+
target.style.top = "0";
45+
target.innerHTML = html;
46+
document.body.append(target);
47+
48+
const selection = window.getSelection();
49+
const range = document.createRange();
50+
range.selectNodeContents(target);
51+
selection?.removeAllRanges();
52+
selection?.addRange(range);
53+
54+
try {
55+
const copied = document.execCommand("copy");
56+
if (!copied) {
57+
throw new Error("Copy command was rejected.");
58+
}
59+
} finally {
60+
selection?.removeAllRanges();
61+
target.remove();
62+
}
63+
}
64+
65+
export async function copyPlainText(text: string): Promise<void> {
66+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
67+
await navigator.clipboard.writeText(text);
68+
return;
69+
}
70+
71+
if (typeof window === "undefined" || typeof document === "undefined") {
72+
throw new Error("Clipboard is available only in the browser.");
73+
}
74+
75+
const target = document.createElement("textarea");
76+
target.value = text;
77+
target.setAttribute("readonly", "true");
78+
target.style.position = "fixed";
79+
target.style.left = "-10000px";
80+
target.style.top = "0";
81+
document.body.append(target);
82+
target.select();
83+
84+
try {
85+
const copied = document.execCommand("copy");
86+
if (!copied) {
87+
throw new Error("Copy command was rejected.");
88+
}
89+
} finally {
90+
target.remove();
91+
}
92+
}

src/lib/dc/escape-html.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const htmlEscapeMap: Record<string, string> = {
2+
"&": "&amp;",
3+
"<": "&lt;",
4+
">": "&gt;",
5+
'"': "&quot;",
6+
"'": "&#39;",
7+
};
8+
9+
export function escapeHtml(value: string): string {
10+
return value.replace(/[&<>"']/g, (char) => htmlEscapeMap[char] ?? char);
11+
}

0 commit comments

Comments
 (0)