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:
2026-01-30 09:26:18 -05:00
parent 1dc178f59f
commit 0f3739605c
2 changed files with 26 additions and 0 deletions

View File

@@ -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 {
if (!text) return "";
try {

View File

@@ -3,6 +3,23 @@ import ReactDOM from "react-dom/client";
import { App } from "./app.js";
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(
<React.StrictMode>
<App />