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