Add language labels and copy-to-clipboard for code blocks
Custom marked renderer wraps fenced code blocks in a .code-block-wrapper div containing a language label badge (top-right) and a copy button. The language label shows the fenced language identifier in uppercase. A delegated click handler on the document root intercepts clicks on [data-copy-code] buttons, reads the sibling <code> element's text content, writes it to the clipboard, and shows a "Copied!" / "Failed" confirmation that auto-reverts after 1.5 seconds. Delegated handling is necessary because code blocks are rendered via dangerouslySetInnerHTML and don't participate in React's synthetic event system. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,15 @@ marked.use(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Custom renderer to wrap code blocks with copy button
|
||||||
|
const renderer = new marked.Renderer();
|
||||||
|
renderer.code = function ({ text, lang }: { text: string; lang?: string | undefined; escaped?: boolean }) {
|
||||||
|
const langLabel = lang ? `<span class="code-lang-label">${escapeHtml(lang)}</span>` : "";
|
||||||
|
return `<div class="code-block-wrapper">${langLabel}<button class="code-copy-btn" data-copy-code type="button">Copy</button><pre class="hljs"><code>${text}</code></pre></div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
marked.use({ renderer });
|
||||||
|
|
||||||
export function renderMarkdown(text: string): string {
|
export function renderMarkdown(text: string): string {
|
||||||
if (!text) return "";
|
if (!text) return "";
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,6 +3,23 @@ import ReactDOM from "react-dom/client";
|
|||||||
import { App } from "./app.js";
|
import { App } from "./app.js";
|
||||||
import "./styles/main.css";
|
import "./styles/main.css";
|
||||||
|
|
||||||
|
// Delegated click handler for code block copy buttons
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
const btn = (e.target as HTMLElement).closest("[data-copy-code]") as HTMLButtonElement | null;
|
||||||
|
if (!btn) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
const wrapper = btn.closest(".code-block-wrapper");
|
||||||
|
const code = wrapper?.querySelector("code");
|
||||||
|
if (!code) return;
|
||||||
|
navigator.clipboard.writeText(code.textContent || "").then(() => {
|
||||||
|
btn.textContent = "Copied!";
|
||||||
|
setTimeout(() => { btn.textContent = "Copy"; }, 1500);
|
||||||
|
}).catch(() => {
|
||||||
|
btn.textContent = "Failed";
|
||||||
|
setTimeout(() => { btn.textContent = "Copy"; }, 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
Reference in New Issue
Block a user