mirror of
https://github.com/quartz-community/syntax-highlighting.git
synced 2026-07-22 02:50:27 +00:00
feat: add clipboard copy button for code blocks
Move clipboard script and styles from Quartz core Body.tsx into the syntax-highlighting plugin. Injected via externalResources() hook with a new clipboard option (defaults to true). Adds esbuild text-loader for .inline.ts and .scss files.
This commit is contained in:
parent
7255e37de4
commit
fada634ccd
7 changed files with 135 additions and 4 deletions
3
src/globals.d.ts
vendored
Normal file
3
src/globals.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
interface Window {
|
||||
addCleanup(callback: () => void): void;
|
||||
}
|
||||
39
src/scripts/clipboard.inline.ts
Normal file
39
src/scripts/clipboard.inline.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
const svgCopy =
|
||||
'<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true"><path fill-rule="evenodd" d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path><path fill-rule="evenodd" d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"></path></svg>';
|
||||
const svgCheck =
|
||||
'<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true"><path fill-rule="evenodd" fill="rgb(63, 185, 80)" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path></svg>';
|
||||
|
||||
document.addEventListener("nav", () => {
|
||||
const els = document.getElementsByTagName("pre");
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
const el = els[i];
|
||||
if (!el) continue;
|
||||
const codeBlock = el.getElementsByTagName("code")[0];
|
||||
if (codeBlock) {
|
||||
const source = (
|
||||
codeBlock.dataset.clipboard ? JSON.parse(codeBlock.dataset.clipboard) : codeBlock.innerText
|
||||
).replace(/\n\n/g, "\n");
|
||||
const button = document.createElement("button");
|
||||
button.className = "clipboard-button";
|
||||
button.type = "button";
|
||||
button.innerHTML = svgCopy;
|
||||
button.ariaLabel = "Copy source";
|
||||
function onClick() {
|
||||
navigator.clipboard.writeText(source).then(
|
||||
() => {
|
||||
button.blur();
|
||||
button.innerHTML = svgCheck;
|
||||
setTimeout(() => {
|
||||
button.innerHTML = svgCopy;
|
||||
button.style.borderColor = "";
|
||||
}, 2000);
|
||||
},
|
||||
(error) => console.error(error),
|
||||
);
|
||||
}
|
||||
button.addEventListener("click", onClick);
|
||||
window.addCleanup(() => button.removeEventListener("click", onClick));
|
||||
el.prepend(button);
|
||||
}
|
||||
}
|
||||
});
|
||||
4
src/styles.d.ts
vendored
Normal file
4
src/styles.d.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
declare module "*.scss" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
36
src/styles/clipboard.scss
Normal file
36
src/styles/clipboard.scss
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
.clipboard-button {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
float: right;
|
||||
right: 0;
|
||||
padding: 0.4rem;
|
||||
margin: 0.3rem;
|
||||
color: var(--gray);
|
||||
border-color: var(--dark);
|
||||
background-color: var(--light);
|
||||
border: 1px solid;
|
||||
border-radius: 5px;
|
||||
opacity: 0;
|
||||
transition: 0.2s;
|
||||
|
||||
& > svg {
|
||||
fill: var(--light);
|
||||
filter: contrast(0.3);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
border-color: var(--secondary);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
&:hover > .clipboard-button {
|
||||
opacity: 1;
|
||||
transition: 0.2s;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
import rehypePrettyCode from "rehype-pretty-code";
|
||||
import type { Options as CodeOptions, Theme as CodeTheme } from "rehype-pretty-code";
|
||||
import type { QuartzTransformerPlugin } from "@quartz-community/types";
|
||||
import type { QuartzTransformerPlugin, JSResource, CSSResource } from "@quartz-community/types";
|
||||
// @ts-ignore
|
||||
import clipboardScript from "./scripts/clipboard.inline";
|
||||
import clipboardStyle from "./styles/clipboard.scss";
|
||||
|
||||
interface Theme extends Record<string, CodeTheme> {
|
||||
light: CodeTheme;
|
||||
|
|
@ -10,6 +13,7 @@ interface Theme extends Record<string, CodeTheme> {
|
|||
export interface SyntaxHighlightingOptions {
|
||||
theme?: Theme;
|
||||
keepBackground?: boolean;
|
||||
clipboard?: boolean;
|
||||
}
|
||||
|
||||
const defaultOptions: SyntaxHighlightingOptions = {
|
||||
|
|
@ -18,16 +22,37 @@ const defaultOptions: SyntaxHighlightingOptions = {
|
|||
dark: "github-dark",
|
||||
},
|
||||
keepBackground: false,
|
||||
clipboard: true,
|
||||
};
|
||||
|
||||
export const SyntaxHighlighting: QuartzTransformerPlugin<Partial<SyntaxHighlightingOptions>> = (
|
||||
userOpts,
|
||||
) => {
|
||||
const opts: CodeOptions = { ...defaultOptions, ...userOpts };
|
||||
const opts = { ...defaultOptions, ...userOpts };
|
||||
const { clipboard, ...codeOpts } = opts;
|
||||
return {
|
||||
name: "SyntaxHighlighting",
|
||||
htmlPlugins() {
|
||||
return [[rehypePrettyCode, opts]];
|
||||
return [[rehypePrettyCode, codeOpts as CodeOptions]];
|
||||
},
|
||||
externalResources() {
|
||||
const js: JSResource[] = [];
|
||||
const css: CSSResource[] = [];
|
||||
|
||||
if (clipboard) {
|
||||
js.push({
|
||||
script: clipboardScript,
|
||||
loadTime: "afterDOMReady",
|
||||
contentType: "inline",
|
||||
});
|
||||
|
||||
css.push({
|
||||
content: clipboardStyle,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
return { js, css };
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"rootDir": ".",
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
|
|
|
|||
|
|
@ -12,4 +12,28 @@ export default defineConfig({
|
|||
target: "es2022",
|
||||
splitting: false,
|
||||
outDir: "dist",
|
||||
esbuildPlugins: [
|
||||
{
|
||||
name: "text-loader",
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /\.scss$/ }, async (args) => {
|
||||
const fs = await import("fs");
|
||||
const text = await fs.promises.readFile(args.path, "utf8");
|
||||
return {
|
||||
contents: text,
|
||||
loader: "text",
|
||||
};
|
||||
});
|
||||
|
||||
build.onLoad({ filter: /\.inline\.ts$/ }, async (args) => {
|
||||
const fs = await import("fs");
|
||||
const text = await fs.promises.readFile(args.path, "utf8");
|
||||
return {
|
||||
contents: text,
|
||||
loader: "text",
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue