Skip to content

Commit 827f580

Browse files
HeyItsGilbertclaude
andcommitted
feat(theme): add click-to-copy buttons to code blocks
Inject a copy button into Chroma `.highlight` blocks and `.ps-terminal` shortcode blocks. For line-numbered blocks the code is read from the last table cell so the line-number gutter is excluded. Uses the async Clipboard API with an execCommand fallback. Also fix `.ps-terminal` rendering a leading blank line: the shortcode now trims all surrounding whitespace via strings.TrimSpace, so CRLF content no longer leaves a stray carriage return before the first line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4ce8487 commit 827f580

4 files changed

Lines changed: 131 additions & 1 deletion

File tree

themes/powershell-community/layouts/_default/baseof.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
4141
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/themes/prism-tomorrow.min.css" rel="stylesheet">
4242
<link href="/css/alerts.css" rel="stylesheet">
43+
<link href="/css/code-copy.css" rel="stylesheet">
4344
{{ with .Site.Params.algolia }}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@algolia/algoliasearch-netlify-frontend@1/dist/algoliasearchNetlify.css" />{{ end }}
4445

4546
<!-- Custom CSS -->
@@ -235,6 +236,7 @@
235236
<script
236237
src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/plugins/autoloader/prism-autoloader.min.js"></script>
237238
<script src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.10.2/cdn.min.js" defer></script>
239+
<script src="/js/code-copy.js" defer></script>
238240

239241
<!-- Custom JavaScript -->
240242
<script>

themes/powershell-community/layouts/_shortcodes/terminal.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
Highlighting is handled client-side by Prism via the language-* class. */ -}}
77
{{- $lang := .Get "lang" | default "powershell" -}}
88
{{- $title := .Get "title" | default "PowerShell" -}}
9-
{{- $code := trim .Inner "\n" -}}
9+
{{- $code := .Inner | strings.TrimSpace -}}
1010
<div class="code-terminal ps-terminal rounded-xl overflow-hidden shadow-xl my-6">
1111
<div class="flex items-center px-4 py-3 border-b border-gray-700">
1212
<div class="flex space-x-2 mr-4">
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/* Click-to-copy buttons for code blocks.
2+
Injected by /js/code-copy.js into Hugo/Chroma `.highlight` blocks and the
3+
`.ps-terminal` shortcode. The button is positioned in the top-right corner
4+
of each block and fades in on hover (always visible on touch devices). */
5+
6+
.highlight,
7+
.ps-terminal {
8+
position: relative;
9+
}
10+
11+
.code-copy-btn {
12+
position: absolute;
13+
top: 0.5rem;
14+
right: 0.5rem;
15+
z-index: 10;
16+
display: inline-flex;
17+
align-items: center;
18+
gap: 0.35rem;
19+
padding: 0.3rem 0.55rem;
20+
font-size: 0.75rem;
21+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
22+
line-height: 1;
23+
color: #e5e7eb;
24+
background: rgba(31, 41, 55, 0.75);
25+
border: 1px solid rgba(255, 255, 255, 0.15);
26+
border-radius: 0.375rem;
27+
cursor: pointer;
28+
opacity: 0;
29+
transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
30+
}
31+
32+
.highlight:hover .code-copy-btn,
33+
.ps-terminal:hover .code-copy-btn,
34+
.code-copy-btn:focus-visible {
35+
opacity: 1;
36+
}
37+
38+
.code-copy-btn:hover {
39+
background: rgba(31, 41, 55, 0.95);
40+
}
41+
42+
.code-copy-btn:focus-visible {
43+
outline: 2px solid #00BCF2;
44+
outline-offset: 1px;
45+
}
46+
47+
.code-copy-btn.copied {
48+
color: #34d399;
49+
border-color: rgba(52, 211, 153, 0.5);
50+
}
51+
52+
/* No hover affordance on touch devices — keep the button visible. */
53+
@media (hover: none) {
54+
.code-copy-btn {
55+
opacity: 1;
56+
}
57+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* Adds a click-to-copy button to every code block on the page.
2+
Handles two block types:
3+
- Hugo/Chroma `.highlight` blocks. With lineNos enabled these render as a
4+
two-column table; the code lives in the LAST cell, so we read that and
5+
leave the line-number gutter out of the copied text.
6+
- `.ps-terminal` shortcode blocks, where the code is in `pre > code`.
7+
Styling lives in /css/code-copy.css. */
8+
(function () {
9+
'use strict';
10+
11+
function codeText(block) {
12+
var cell = block.querySelector('table tr > td:last-child');
13+
var source = cell || block.querySelector('pre code') || block.querySelector('pre');
14+
return source ? source.textContent.replace(/\n+$/, '') : '';
15+
}
16+
17+
function copyText(text) {
18+
if (navigator.clipboard && window.isSecureContext) {
19+
return navigator.clipboard.writeText(text);
20+
}
21+
return new Promise(function (resolve, reject) {
22+
var ta = document.createElement('textarea');
23+
ta.value = text;
24+
ta.style.position = 'fixed';
25+
ta.style.top = '-9999px';
26+
document.body.appendChild(ta);
27+
ta.select();
28+
try {
29+
document.execCommand('copy') ? resolve() : reject();
30+
} catch (err) {
31+
reject(err);
32+
} finally {
33+
document.body.removeChild(ta);
34+
}
35+
});
36+
}
37+
38+
var IDLE = '<i class="fas fa-copy" aria-hidden="true"></i><span>Copy</span>';
39+
var DONE = '<i class="fas fa-check" aria-hidden="true"></i><span>Copied!</span>';
40+
41+
function attach(block) {
42+
if (block.querySelector(':scope > .code-copy-btn')) {
43+
return;
44+
}
45+
var btn = document.createElement('button');
46+
btn.type = 'button';
47+
btn.className = 'code-copy-btn';
48+
btn.setAttribute('aria-label', 'Copy code to clipboard');
49+
btn.innerHTML = IDLE;
50+
block.appendChild(btn);
51+
52+
var reset;
53+
btn.addEventListener('click', function () {
54+
copyText(codeText(block)).then(function () {
55+
btn.classList.add('copied');
56+
btn.innerHTML = DONE;
57+
clearTimeout(reset);
58+
reset = setTimeout(function () {
59+
btn.classList.remove('copied');
60+
btn.innerHTML = IDLE;
61+
}, 2000);
62+
}).catch(function () {
63+
btn.innerHTML = '<span>Press Ctrl+C</span>';
64+
});
65+
});
66+
}
67+
68+
document.addEventListener('DOMContentLoaded', function () {
69+
document.querySelectorAll('.prose .highlight, .ps-terminal').forEach(attach);
70+
});
71+
})();

0 commit comments

Comments
 (0)