Adding a Copy Code Button

I just read David Bushell’s blog post about reading Salma Alam-Naylor’s blog post about adding copy code buttons, and decided to continue the chain.
By “copy code button”, we mean buttons added to code snippets that copy the text to your clipboard when clicked.
For example:
const x = 1 + 1;
If you click that “Copy Code” above, you should have
const x = 1 + 1;
in your copy/paste clipboard.
This is a feature that I’d seen on other blog posts, but it wasn’t until reading Alam-Naylor’s post that I understood what a help it is for people that use voice commands instead of a mouse.
Quoting from Alam-Naylor:
It is extremely difficult to highlight blocks of code and copy them with voice commands. At this point, for me at least, it is impossible. (Either it is actually impossible, or I haven’t had the brainpower to work out how to do it yet. Learning to navigate the web and code without your hands takes a huge mental toll.) For this reason, when you’re writing about code, especially when you’re creating tutorials that involve the reader copying and pasting code, it is absolutely imperative that you include a way for people with limited use of their hands to copy that code in a single action.
As somebody who also has a (less servere) form of Carpal Tunnel this hits particularly hard for me.
I want to make this site as easy as possible for people read and use, including those who can’t use a keyboard and mouse.
After all, everybody’s going to end up “disabled” at some point or another, if only just because of old age. We owe it to the people around us and our future selves to try to make our world accessible.
Technical Details
I write my musings and notes in Markdown, and use pandoc to convert them into HTML.
Pandoc has a concept of filters, a standardized way to write code against Pandoc’s Abstract Syntax Tree (AST). Basically, it lets you write code to change your HTML output at generation time.
I already use a Pandoc filter to turn inline links into footnotes, to allow for posting to Mastodon, and so I figured adding one more was the way to go!
Here’s the Python filter I ended up writing, which takes any
CodeBlocks
and adds a <button>
below
them.
#!/opt/homebrew/bin/python3
from hashlib import sha256
from pandocfilters import toJSONFilter, CodeBlock, RawBlock, Str, Plain
# Cache needed to prevent infinite recursion: https://github.com/jgm/pandocfilters/issues/72
= {}
alreadySeen
def addCopyCodeButton(key, value, _format, _meta):
if key == "CodeBlock" and not alreadySeen.get(value[1]):
= value
[id_, classes, namevals], contents 1]] = True
alreadySeen[value[
hash = sha256(contents.encode()).hexdigest()
= "<button class=\"copy-code-button\" onclick=\"navigator.clipboard.writeText(document.getElementById('" + hash + "').textContent).then(() => {this.textContent='Copied!';})\">"
buttonHTML
return [CodeBlock([hash, classes, namevals], contents), RawBlock("html", buttonHTML), Plain([Str("Copy Code")]), RawBlock("html", "</button>")]
if __name__ == "__main__":
toJSONFilter(addCopyCodeButton)
This is basically entirely cribbed from Bushell’s solution but it does it at HTML generation time instead of with Javascript on page load.
Each code block gets its own HTML id
, generated from the
sha256 hash of the content.1
Then when the copy button is clicked, it looks up the code block by
id, gets its textContent
, and puts it in your
clipboard.
The button’s own text is then changed to “Copied!”.
Only tricky thing to note is that Python pandocfilters
module has a
bug where your filter returns a new object, and then the filter gets
run again against that new object.
In my case, our filter takes in a CodeBlocks
and returns
a new CodeBlock
, and then the filter gets run against the
new CodeBlock
, so a new new CodeBlock
is
returned, and the filter gets run against that… on and on until we hit
the recursion limit.
I got around this in my filter by added a cache, and skipping over a
CodeBlock
if we’ve already seen its contents before.
I did look into fixing the bug in the module itself, and it turns out that somebody made a PR for it in 2023. Hopefully that gets merged in at some point!
This does mean that if I had two codeblocks with the same content, they’d end up with the same id. This would be a violation of the HTML contract, where ids need to be unique. It’d also break my pandoc filter recursion bug fix.
Not going to bother designing around it though – this be my blog after all!↩︎