Skip to content

Commit

Permalink
Add copy button to code block
Browse files Browse the repository at this point in the history
  • Loading branch information
mengqi92 committed Apr 3, 2024
1 parent 0ee8cfd commit 48547e4
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 28 deletions.
29 changes: 16 additions & 13 deletions contentlayer.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import remarkToc from 'remark-toc'
import remarkMermaid from 'remark-mermaidjs'
import { preProcess, postProcess } from './src/lib/rehype-utils'

const Post = defineDocumentType(() => ({
name: 'Post',
Expand Down Expand Up @@ -81,25 +82,27 @@ export default makeSource({
remarkMermaid as any
],
rehypePlugins: [
preProcess,
rehypeSlug,
rehypeKatex as any,
[rehypeAutolinkHeadings, 'after'],
[rehypePrettyCode, {
theme: 'solarized-light',
onVisitLine(node: { children: string | any[] }) {
// Prevent lines from collapsing in `display: grid` mode, and allow empty
// lines to be copy/pasted
if (node.children.length === 0) {
node.children = [{ type: "text", value: " " }]
}
},
onVisitHighlightedLine(node: { properties: { className: string[] } }) {
node.properties.className.push("line--highlighted")
},
onVisitHighlightedWord(node: { properties: { className: string[] } }) {
node.properties.className = ["word--highlighted"]
},
// onVisitLine(node: { children: string | any[] }) {
// // Prevent lines from collapsing in `display: grid` mode, and allow empty
// // lines to be copy/pasted
// if (node.children.length === 0) {
// node.children = [{ type: "text", value: " " }]
// }
// },
// onVisitHighlightedLine(node: { properties: { className: string[] } }) {
// node.properties.className.push("line--highlighted")
// },
// onVisitHighlightedWord(node: { properties: { className: string[] } }) {
// node.properties.className = ["word--highlighted"]
// },
}],
postProcess
]
}
})
23 changes: 23 additions & 0 deletions src/app/_components/clipboard-copy.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.copyCode {
position: relative;
button {
z-index: 1;
position: absolute;
top: 13px;
right: -10px;
background-color: var(--code-highlight);
border-radius: 5px;
text-transform: uppercase;
font-size: 13px;
padding: .1rem .4rem .2rem;
color: var(--color-bg);
}
}

button.copyCode::after {
content: "📋";
}

.active button.copyCode::after {
content: "☑️";
}
60 changes: 60 additions & 0 deletions src/app/_components/clipboard-copy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use client';

import { useState } from "react";

function ClipboardCopy({ copyText }) {
const [isCopied, setIsCopied] = useState(false);
console.info('copyText passed in:', copyText);

// This is the function we wrote earlier
async function copyTextToClipboard(text) {
if ('clipboard' in navigator) {
return await navigator.clipboard.writeText(text);
} else {
return document.execCommand('copy', true, text);
}
}

// onClick handler function for the copy button
const handleCopyClick = () => {
// Asynchronously call copyTextToClipboard
copyTextToClipboard(copyText)
.then(() => {
console.info('Copied successfully: ', copyText);
// If successful, update the isCopied state value
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 1500);
})
.catch((err) => {
console.log(err);
});
}

return (
<div>
{/* Bind our handler function to the onClick button property */}
<button className="copyCode absolute top-3 right-3 text-slate-500 hover:ring-2 hover:ring-slate-300 rounded" onClick={handleCopyClick} aria-label="Copy code to clipboard">
{isCopied
?
<svg xmlns="http://www.w3.org/2000/svg" className="icon icon-tabler icon-tabler-clipboard-check stroke-slate-500" width="20" height="20" viewBox="0 0 24 24" strokeWidth="1.5" stroke="#000000" fill="none" strokeLinecap="round" strokeLinejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2" />
<path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z" />
<path d="M9 14l2 2l4 -4" />
</svg>
:
<svg xmlns="http://www.w3.org/2000/svg" className="icon icon-tabler icon-tabler-copy stroke-slate-500" width="20" height="20" viewBox="0 0 24 24" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z" />
<path d="M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1" />
</svg>
}
<span></span>
</button>
</div>
);
}

export default ClipboardCopy;
21 changes: 21 additions & 0 deletions src/app/_components/code-block.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client';

import clsx from "clsx";
import ClipboardCopy from "./clipboard-copy";
// import './clipboard-copy.css'

const CodeBlock = ({ className, children, raw, ...props }) => {
console.log('raw: ', raw);
return (
<>
<div className="copyCode">
<pre className={clsx("p-0 my-4 px-4 py-4 bg-transparent rounded overflow-x-auto relative", className)} {...props}>
{children}
<ClipboardCopy copyText={raw}/>
</pre>
</div>
</>
)
}

export default CodeBlock;
24 changes: 9 additions & 15 deletions src/app/_components/markdown-post.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMDXComponent } from "next-contentlayer/hooks";
import { MDXComponents } from "mdx/types";
import clsx from "clsx";
import CodeBlock from "./code-block";

const components: MDXComponents = {
// Allows customizing built-in components, e.g. to add styling.
Expand All @@ -12,24 +12,18 @@ const components: MDXComponents = {
blockquote: ({ children }) => <blockquote className="mt-6 bg-slate-50 py-2 border-l-2 pl-6 italic">{children}</blockquote>,
ul: ({ children }) => <ul className="my-6 ml-6 list-disc [&>li]:mt-2">{children}</ul>,
li: ({ children }) => <li className="leading-7 list-outside list-decimal">{children}</li>,
pre: ({ className, children, ...props }) => {
return (
<pre className={clsx("p-0 my-4 px-4 py-4 bg-transparent rounded overflow-x-auto", className)} {...props}>
{children}
</pre>
)
},
code: ({children}) => <code className="relative my-4 px-[0.3rem] py-[0.2rem] font-mono text-sm">{children}</code>,
pre: ({ className, children, raw, ...props}) => <CodeBlock children={children} raw={raw} className={className} {...props}/>,
code: ({ children }) => <code className="relative my-4 px-[0.3rem] py-[0.2rem] font-mono text-sm">{children}</code>,
lead: ({ children }) => <p className="text-xl text-muted-foreground">{children}</p>,
a: ({ children, href }) => (
<a href={href} className="text-red-500 underline decoration-red-400 underline-offset-4 hover:text-red-700 hover:decoration-red-700 hover:decoration-2">{children}</a>
),
table: ({children}) => <table className="table-auto my-2 w-full text-sm text-right rtl:text-left text-gray-500 dark:text-gray-400">{children}</table>,
tbody: ({children}) => <tbody className="bg-slate-100 dark:bg-white-700 text-gray-700 dark:text-gray-400">{children}</tbody>,
thead: ({children}) => <thead className="text-gray-700 uppercase bg-sky-50 dark:bg-gray-700 dark:text-gray-400 border-b-2 border-b-gray-500">{children}</thead>,
th: ({children}) => <th className="px-4 py-3">{children}</th>,
td: ({children}) => <td className="px-4 py-4">{children}</td>,
tr: ({children}) => <tr className="divide-y [&:not(:last-child)]:border-b-2 border-b-slate-200">{children}</tr>
table: ({ children }) => <table className="table-auto my-2 w-full text-sm text-right rtl:text-left text-gray-500 dark:text-gray-400">{children}</table>,
tbody: ({ children }) => <tbody className="bg-slate-100 dark:bg-white-700 text-gray-700 dark:text-gray-400">{children}</tbody>,
thead: ({ children }) => <thead className="text-gray-700 uppercase bg-sky-50 dark:bg-gray-700 dark:text-gray-400 border-b-2 border-b-gray-500">{children}</thead>,
th: ({ children }) => <th className="px-4 py-3">{children}</th>,
td: ({ children }) => <td className="px-4 py-4">{children}</td>,
tr: ({ children }) => <tr className="divide-y [&:not(:last-child)]:border-b-2 border-b-slate-200">{children}</tr>
};

interface MarkdownPostProps {
Expand Down
32 changes: 32 additions & 0 deletions src/lib/rehype-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { visit } from 'unist-util-visit'

export const preProcess = () => (tree) => {
visit(tree, (node) => {
if (node?.type === 'element' && node?.tagName === 'pre') {
const [codeEl] = node.children

if (codeEl.tagName !== 'code') return

node.raw = codeEl.children?.[0].value
// console.info('node preProcessed: ', node);
}
})
}

export const postProcess = () => (tree) => {
visit(tree, (node) => {
if (node?.type === 'element' && node?.tagName === 'figure') {
if (!("data-rehype-pretty-code-figure" in node.properties)) {
return;
}

for (const child of node.children) {
if (child.tagName === "pre") {
child.properties["raw"] = node.raw;
}
}
node.properties['raw'] = node.raw
console.log('PostProcessed: ', node); // here to see if you're getting the raw text
}
})
}

0 comments on commit 48547e4

Please sign in to comment.