From 1d9673468505eaad912ebb7b9b6d601ac2b5bf60 Mon Sep 17 00:00:00 2001
From: Patrick Kollitsch <davidsneighbourdev+gh@gmail.com>
Date: Sun, 07 Jun 2026 03:47:20 +0000
Subject: [PATCH] feat: copy-to-clipboard buttons for code blocks (#986)
---
config/_default/params.toml | 1
layouts/_markup/render-codeblock.html | 27 +++++++++
layouts/_partials/site-scripts.html | 77 +++++++++++++++++++++++++
assets/ananke/css/_code.css | 60 +++++++++++++++++++
4 files changed, 162 insertions(+), 3 deletions(-)
diff --git a/assets/ananke/css/_code.css b/assets/ananke/css/_code.css
index 11fd170..a41ef5d 100644
--- a/assets/ananke/css/_code.css
+++ b/assets/ananke/css/_code.css
@@ -19,6 +19,62 @@
}
p code {
-
font-size: 0.9em;
- }
\ No newline at end of file
+}
+
+/* ------------------------------------------------------------------ *
+ * 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;
+ }
+}
\ No newline at end of file
diff --git a/config/_default/params.toml b/config/_default/params.toml
index 56d0b77..9c0a4e0 100644
--- a/config/_default/params.toml
+++ b/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
diff --git a/layouts/_markup/render-codeblock.html b/layouts/_markup/render-codeblock.html
new file mode 100644
index 0000000..94b2077
--- /dev/null
+++ b/layouts/_markup/render-codeblock.html
@@ -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 -}}
diff --git a/layouts/_partials/site-scripts.html b/layouts/_partials/site-scripts.html
index e6462a9..9569ead 100644
--- a/layouts/_partials/site-scripts.html
+++ b/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 -}}
--
Gitblit v1.10.0