Files
novaconium/novaconium/pages/_layout/code-copy.twig
T
code 672f997d8b Add copy-to-clipboard button to code blocks
Injects a hover-revealed copy button into every <pre><code> block
site-wide via a single layout partial, reading textContent so escaped
samples copy as literal text. Ships copy/check icons and matching Sass.

Passes icon markup to JS via <template> elements instead of Twig's
|escape('js'), which calls mb_ord() and fatals without mbstring — same
class of bug as the existing |slice/mb_substr gotcha, now documented in
AGENTS.md.

Closes the "Copy-to-clipboard button on code blocks" backlog item in
novaconium/ISSUES.md.
2026-07-14 03:03:29 +00:00

59 lines
2.5 KiB
Twig

{% import '_layout/icons.twig' as icons %}
{# Adds a hover-revealed copy button to every <pre><code> block on the
page, without touching any individual doc/blog page's markup. Reads
textContent (not innerHTML) when copying, so HTML-entity-escaped
samples (e.g. &lt;h1&gt; in the SEO starter template) come out as their
literal, unescaped characters rather than the escaped markup.
The icon markup is passed to JS via <template> elements (plain HTML
output, default autoescaping) rather than Twig's `|escape('js')`
filter — that filter calls Twig\Runtime\mb_ord() under the hood, which
hard-requires the mbstring extension and fatals
(`Call to undefined function Twig\Runtime\mb_ord()`) without it, the
same class of mbstring gotcha documented in AGENTS.md for `|slice` on
strings. #}
<template id="copy-code-icon-copy">{{ icons.copy() }}<span class="copy-code-label">Copy</span></template>
<template id="copy-code-icon-copied">{{ icons.check() }}<span class="copy-code-label">Copied!</span></template>
<script>
(function () {
var copyIconHtml = document.getElementById('copy-code-icon-copy').innerHTML;
var checkIconHtml = document.getElementById('copy-code-icon-copied').innerHTML;
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('pre').forEach(function (pre) {
if (!pre.querySelector('code')) {
return;
}
var button = document.createElement('button');
button.type = 'button';
button.className = 'copy-code-button icon-link';
button.setAttribute('aria-label', 'Copy code to clipboard');
button.innerHTML = copyIconHtml + '<span class="copy-code-label">Copy</span>';
pre.appendChild(button);
});
});
document.addEventListener('click', function (event) {
var button = event.target.closest('.copy-code-button');
if (!button) {
return;
}
var code = button.closest('pre').querySelector('code');
navigator.clipboard.writeText(code.textContent).then(function () {
var originalHtml = button.innerHTML;
button.innerHTML = checkIconHtml + '<span class="copy-code-label">Copied!</span>';
button.classList.add('copied');
setTimeout(function () {
button.innerHTML = originalHtml;
button.classList.remove('copied');
}, 1500);
});
});
})();
</script>