| assets/ananke/css/_code.css | ●●●●● patch | view | raw | blame | history | |
| config/_default/params.toml | ●●●●● patch | view | raw | blame | history | |
| layouts/_markup/render-codeblock.html | ●●●●● patch | view | raw | blame | history | |
| layouts/_partials/site-scripts.html | ●●●●● patch | view | raw | blame | history |
assets/ananke/css/_code.css
@@ -19,6 +19,62 @@ } p code { font-size: 0.9em; } } /* ------------------------------------------------------------------ * * Code block copy button (see layouts/_markup/render-codeblock.html) * * Progressive enhancement: the button is hidden until site-scripts.html * reveals it, so no inert button appears when JavaScript is disabled. * * ------------------------------------------------------------------ */ .code-block { position: relative; } .code-block .code-copy { position: absolute; top: 0.5rem; right: 0.5rem; z-index: 2; padding: 0.25rem 0.6rem; font-family: inherit; font-size: 0.75rem; line-height: 1.4; color: #ddd; background-color: rgba(255, 255, 255, 0.12); border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 4px; cursor: pointer; opacity: 0; transition: opacity 0.15s ease-in-out, background-color 0.15s ease-in-out; } /* Reveal on hover and when focused for keyboard users. */ .code-block:hover .code-copy, .code-block .code-copy:focus-visible { opacity: 1; } .code-block .code-copy:hover { background-color: rgba(255, 255, 255, 0.22); } .code-block .code-copy.is-copied { color: #fff; background-color: #19a974; border-color: #19a974; } /* Touch devices have no hover; keep the button visible but subtle. */ @media (hover: none) { .code-block .code-copy { opacity: 0.7; } } @media (prefers-reduced-motion: reduce) { .code-block .code-copy { transition: none; } } config/_default/params.toml
@@ -1,6 +1,7 @@ [ananke] show_recent_posts = true # show recent posts on the homepage show_categories = true # show categories terms on single pages copy_code = true # show a copy-to-clipboard button on code blocks (progressive enhancement) [ananke.home] content_alignment = "center" # options: left, center, right layouts/_markup/render-codeblock.html
New file @@ -0,0 +1,27 @@ {{- /* Code block render hook. Wraps Hugo's standard syntax-highlighted output in a container with a copy-to-clipboard button. Highlighting still honours the site's [markup.highlight] configuration because we delegate to transform.HighlightCodeBlock, which reads both the per-fence options and the global config. Progressive enhancement: the button is rendered with the `hidden` attribute and is only revealed by site-scripts.html when JavaScript runs, so sites without JS show no inert button. The copy behaviour itself is wired by the same script (a single delegated listener). Disable site-wide with `[ananke] copy_code = false` (see params.toml). */ -}} {{- $result := transform.HighlightCodeBlock . -}} {{- if eq false site.Params.ananke.copy_code -}} {{- $result.Wrapped -}} {{- else -}} <div class="code-block"> <button class="code-copy" type="button" hidden aria-label="Copy code to clipboard"> <span class="code-copy-label" aria-hidden="true">Copy</span> </button> {{ $result.Wrapped }} </div> {{- end -}} layouts/_partials/site-scripts.html
@@ -1 +1,76 @@ {{/* For Users's overwrite */}} {{- /* Copy-to-clipboard behaviour for code blocks rendered by layouts/_markup/render-codeblock.html. Progressive enhancement: the buttons are hidden in the markup and only revealed here, so a site with JavaScript disabled never shows an inert button. A single delegated listener handles every code block on the page. Override this partial in your own project to add or replace site scripts; disable the copy buttons entirely with `[ananke] copy_code = false`. */ -}} {{- if ne false site.Params.ananke.copy_code -}} <script> (function () { "use strict"; // Reveal the copy buttons now that JavaScript is available. var buttons = document.querySelectorAll(".code-block .code-copy[hidden]"); for (var i = 0; i < buttons.length; i++) { buttons[i].hidden = false; } function flash(button) { var label = button.querySelector(".code-copy-label"); var previous = label ? label.textContent : null; button.classList.add("is-copied"); if (label) label.textContent = "Copied"; window.setTimeout(function () { button.classList.remove("is-copied"); if (label && previous !== null) label.textContent = previous; }, 2000); } function legacyCopy(text) { var area = document.createElement("textarea"); area.value = text; area.setAttribute("readonly", ""); area.style.position = "absolute"; area.style.left = "-9999px"; document.body.appendChild(area); area.select(); var ok = false; try { ok = document.execCommand("copy"); } catch (e) { ok = false; } document.body.removeChild(area); return ok; } function copyFrom(button) { var container = button.closest(".code-block"); if (!container) return; var pre = container.querySelector("pre"); if (!pre) return; var source = pre.querySelector("code") || pre; var text = source.innerText; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then( function () { flash(button); }, function () { if (legacyCopy(text)) flash(button); } ); } else if (legacyCopy(text)) { flash(button); } } document.addEventListener("click", function (event) { var button = event.target.closest(".code-copy"); if (button) copyFrom(button); }); })(); </script> {{- end -}}