Skip to content

Commit 82557ab

Browse files
committed
v1.0.0
- Initial release - Add a copy-to-clipboard button to every `code` block in posts and pages - Show the button on hover, with a timed confirmation state after copying - Config: confirmation text, display duration (ms), and scope (posts, pages, or both)
0 parents  commit 82557ab

9 files changed

Lines changed: 969 additions & 0 deletions

File tree

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules/
2+
.idea/
3+
.DS_Store
4+
*.log
5+
*.bak
6+
*.bak.*

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Changelog
2+
3+
### Version v1.0.0
4+
- Initial release
5+
- Add a copy-to-clipboard button to every `code` block in posts and pages
6+
- Show the button on hover, with a timed confirmation state after copying
7+
- Config: confirmation text, display duration (ms), and scope (posts, pages, or both)

DEV.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Developer Readme
2+
3+
## Development install
4+
5+
1. Clone:
6+
```
7+
git clone https://github.com/InPoint-Automation/CopyCodeButton.git
8+
```
9+
2. Install the plugin via Publii: **Tools -> Plugins -> Install plugin**, and select the
10+
packaged ZIP (see the Package section below).
11+
3. Enable in Publii, render the site, inspect output HTML.
12+
13+
## Package
14+
15+
Publii requires the files inside an `CopyCodeButton/` subfolder within the ZIP.
16+
17+
On Linux you can run a single command to make a clean .zip
18+
19+
```
20+
ln -s . CopyCodeButton && zip CopyCodeButton.zip CopyCodeButton/plugin.json CopyCodeButton/main.js CopyCodeButton/thumbnail.svg CopyCodeButton/README.md CopyCodeButton/LICENSE; rm CopyCodeButton
21+
```
22+
23+
Otherwise, you can just directly zip up the whole CopyCodeButton directory. You may consider to remove the screenshot, etc first.

LICENSE

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copy Code Button
2+
3+
<p align="center" width="100%">
4+
<img alt="Icon" src="./thumbnail.svg" width="40%" />
5+
</p>
6+
7+
## Description
8+
9+
Copy Code Button is a Publii plugin that adds a copy-to-clipboard button to every `code`
10+
block in your posts and pages. The button appears on hover and shows a brief
11+
confirmation after copying. It uses the browser's `navigator.clipboard` API.
12+
13+
## Features
14+
- Copy-to-clipboard button on every code block in posts and pages
15+
- Button appears on hover
16+
- Confirmation after a successful copy
17+
- Configurable confirmation text (default `Copied!`)
18+
- Configurable confirmation duration in milliseconds (default `2000`)
19+
- Configurable scope: posts and pages, posts only, or pages only
20+
21+
## Requirements
22+
Publii 0.43.0 or newer.
23+
24+
## Installation
25+
26+
1. Download the release ZIP.
27+
2. Publii -> Tools -> Plugins -> Install plugin -> pick the ZIP.
28+
3. Enable the plugin for the site.
29+
30+
## Usage
31+
Open Plugins -> Copy Code Button -> settings icon. Set the confirmation text and
32+
duration, and choose whether the button applies to posts and pages, posts only,
33+
or pages only. Rebuild the site; hover any code block to reveal the copy button.
34+
35+
<p align="center" width="80%">
36+
<img alt="screenshot" src="./screenshot.png" width="80%" />
37+
</p>
38+
39+
## Contributing
40+
41+
Feel free to fork this project or contribute a PR.
42+
Build instructions can be found in the developer readme [DEV.md](DEV.md).
43+
44+
## Licenses
45+
46+
### Main code
47+
48+
This project is licensed under the GPLv3+.
49+
See [LICENSE](LICENSE) file for details.
50+
51+
### Third-Party Components
52+
53+
None, the plugin is self-contained plain JavaScript with no dependencies.
54+
55+
## Credits
56+
57+
This project was developed by InPoint Automation Sp. z o.o.
58+
https://inpointautomation.com/
59+
60+
Thanks to the [Publii](https://getpublii.com/) project for the CMS and plugin API.
61+
62+
## Changelog
63+
64+
See [CHANGELOG.md](CHANGELOG.md) for the full version history.

main.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copy Code Button - Copyright (C) 2026 InPoint Automation Sp. z o.o.
2+
// Licensed under the GNU General Public License v3 or later; see LICENSE.
3+
//
4+
// Adds copy-to-clipboard button to every code block in posts and pages.
5+
6+
class CopyCodeButton {
7+
constructor(API, name, config) {
8+
this.API = API;
9+
this.name = name;
10+
this.config = config;
11+
}
12+
13+
addModifiers() {
14+
const isPost = this.config.applyTo === 'post' || this.config.applyTo === 'both';
15+
const isPage = this.config.applyTo === 'page' || this.config.applyTo === 'both';
16+
17+
if (isPost) {
18+
this.API.addModifier('postText', this.addCopyButtons, 2, this);
19+
}
20+
if (isPage) {
21+
this.API.addModifier('pageText', this.addCopyButtons, 2, this);
22+
}
23+
}
24+
25+
addInsertions() {
26+
this.API.addInsertion('customHeadCode', this.addStyles, 1, this);
27+
this.API.addInsertion('customFooterCode', this.addScript, 1, this);
28+
}
29+
30+
addCopyButtons(rendererInstance, text) {
31+
const buttonHtml = '<button class="copy-code-btn" type="button" aria-label="Copy code">'
32+
+ '<svg class="copy-code-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'
33+
+ '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>'
34+
+ '<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>'
35+
+ '</svg>'
36+
+ '<span class="copy-code-text"></span>'
37+
+ '</button>';
38+
39+
return text.replace(
40+
/(<pre(?:\s[^>]*)?>)\s*(<code(?:\s[^>]*)?>)([\s\S]*?)<\/code>\s*<\/pre>/gi,
41+
function (match, preTag, codeTag, codeContent) {
42+
return '<div class="code-block-wrapper">'
43+
+ buttonHtml
44+
+ preTag + codeTag + codeContent + '</code></pre>'
45+
+ '</div>';
46+
}
47+
);
48+
}
49+
50+
addStyles(rendererInstance, context) {
51+
if (!context.post && !context.page) return '';
52+
53+
return '<style>'
54+
+ '.code-block-wrapper {'
55+
+ ' position: relative;'
56+
+ '}'
57+
+ '.copy-code-btn {'
58+
+ ' position: absolute !important;'
59+
+ ' top: 8px !important;'
60+
+ ' right: 12px !important;'
61+
+ ' z-index: 10;'
62+
+ ' display: flex !important;'
63+
+ ' align-items: center !important;'
64+
+ ' gap: 4px;'
65+
+ ' padding: 6px !important;'
66+
+ ' height: auto !important;'
67+
+ ' min-height: 0 !important;'
68+
+ ' border-radius: 4px;'
69+
+ ' cursor: pointer;'
70+
+ ' opacity: 0;'
71+
+ ' transition: opacity 0.2s ease;'
72+
+ ' line-height: 1 !important;'
73+
+ '}'
74+
+ '.code-block-wrapper:hover .copy-code-btn {'
75+
+ ' opacity: 1;'
76+
+ '}'
77+
+ '.copy-code-btn.is-copied {'
78+
+ ' opacity: 1;'
79+
+ ' padding: 6px 10px !important;'
80+
+ '}'
81+
+ '.copy-code-icon {'
82+
+ ' flex-shrink: 0;'
83+
+ '}'
84+
+ '.copy-code-text {'
85+
+ ' color: inherit !important;'
86+
+ '}'
87+
+ '.copy-code-text:empty {'
88+
+ ' display: none;'
89+
+ '}'
90+
+ '</style>';
91+
}
92+
93+
addScript(rendererInstance, context) {
94+
if (!context.post && !context.page) return '';
95+
96+
const copiedText = this.config.copiedText || 'Copied!';
97+
const copiedDuration = parseInt(this.config.copiedDuration, 10) || 2000;
98+
99+
return '<script>'
100+
+ 'document.addEventListener("DOMContentLoaded", function() {'
101+
+ ' document.querySelectorAll(".copy-code-btn").forEach(function(btn) {'
102+
+ ' btn.addEventListener("click", function() {'
103+
+ ' var wrapper = this.closest(".code-block-wrapper");'
104+
+ ' var code = wrapper.querySelector("code");'
105+
+ ' if (!code) return;'
106+
+ ' var text = code.innerText || code.textContent;'
107+
+ ' navigator.clipboard.writeText(text).then(function() {'
108+
+ ' var textSpan = btn.querySelector(".copy-code-text");'
109+
+ ' textSpan.textContent = "' + this.escJs(copiedText) + '";'
110+
+ ' btn.classList.add("is-copied");'
111+
+ ' setTimeout(function() {'
112+
+ ' textSpan.textContent = "";'
113+
+ ' btn.classList.remove("is-copied");'
114+
+ ' }, ' + copiedDuration + ');'
115+
+ ' }).catch(function(err) {'
116+
+ ' console.error("[Copy Code Button] Copy failed:", err);'
117+
+ ' });'
118+
+ ' });'
119+
+ ' });'
120+
+ '});'
121+
+ '</script>';
122+
}
123+
124+
escJs(str) {
125+
return String(str)
126+
.replace(/\\/g, '\\\\')
127+
.replace(/"/g, '\\"')
128+
.replace(/\n/g, '\\n');
129+
}
130+
}
131+
132+
module.exports = CopyCodeButton;

plugin.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "Copy Code Button",
3+
"description": "Adds a copy-to-clipboard button to all code blocks in posts and pages.",
4+
"license": "GPL-3.0",
5+
"author": "InPoint Automation",
6+
"version": "1.0.0",
7+
"scope": "site",
8+
"minimumPubliiVersion": "0.43.0",
9+
"config": [
10+
{
11+
"name": "copiedText",
12+
"label": "Copied confirmation text",
13+
"group": "Display",
14+
"type": "text",
15+
"value": "Copied!"
16+
},
17+
{
18+
"name": "copiedDuration",
19+
"label": "Confirmation display duration (ms)",
20+
"group": "Display",
21+
"type": "number",
22+
"value": 2000
23+
},
24+
{
25+
"name": "applyTo",
26+
"label": "Apply to",
27+
"group": "Advanced",
28+
"type": "radio",
29+
"options": [
30+
{ "label": "Posts and pages", "value": "both" },
31+
{ "label": "Posts only", "value": "post" },
32+
{ "label": "Pages only", "value": "page" }
33+
],
34+
"value": "both"
35+
}
36+
]
37+
}

screenshot.png

20.7 KB
Loading

thumbnail.svg

Lines changed: 26 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)