Starling Cybernetics

Copy This Code

Starling Cybernetics believes in open source software.

While some validator operators believe that secrecy is the key to maintaining a competitive edge, we think that fostering a community based on positive-sum collaboration has long-term rewards far in excess of any short-term gains that could be won by keeping our operational know-how to ourselves.

As such, every code sample on this blog is released under the MIT License unless otherwise specified. And to put our money where our mouth is, I spent a couple hours yesterday hacking together a little “copy this code” widget for the blog you are presently reading.

In the upper right-hand corner of every code sample you'll now see a little clipboard icon which will copy the code sample for you. Check out this example showing you how to use the pcli command line wallet for Penumbra to delegate testnet tokens to Starling Staking:

# Delegate to Starling Staking on Penumbra testnet using this command:
pcli tx delegate 1000penumbra \
  --to penumbravalid1gjdvn0u85rgldqk5adfexn6n4y8d2m3tfla54sc4gu95xwpzssxsjutk7u

Home-made tastes better

To do this, I could have used a library like ClipboardJS, which advertises the slogan: “No Flash. No frameworks. Just 3kb gzipped.” But using even a minimal third-party Javascript library just to copy some text to the clipboard seemed unnecessarily elaborate, and serving the library in a reasonable way would require linking to a content-delivery network which might collect analytics about this site's readers – something we are committed to avoiding.

In this post, we will do ClipboardJS one better. To adapt their phrasing:

And it will be tooled specifically to our purpose: automatically copying from code blocks in blog posts.

How it works

This site is hosted by Bear Blog, “a privacy-first, no-nonsense, super-fast blogging platform.” Bear renders posts written as Markdown into HTML pages, so I don't have to.1 In Bear's case, a Markdown fenced code block (bracketed by triple-backticks ```) gets rendered like this:

<div class="highlight">
    <pre>
    <!-- a bunch of <span> tags annotating code with syntax highlighting -->
    </pre>
</div>

We want to inject a <button> inside the outer <div> of each highlighted code block, so that the DOM would then look like this:

<div class="highlight">
    <pre>
    <!-- ... -->
    </pre>
    <button type="button" title="Copy">📋</button>
</div>

And of course, the button should actually do what it says on the tin!

Script in the <footer>

Bear lets paid users like Starling Cybernetics insert arbitrary content into the <head>/<footer> of the page to further customize it, so I added the following script to the footer of each page.2

<!-- This script adds a "copy this code" button to every code block -->
<script>
// Parameters for the behavior/appearance of the button:
const reset = 750; // milliseconds
const message = {
    copy: '📋',    // What does it look like initially?
    confirm: '✅', // What does it look like on success?
    error: '❌',   // What does it look like on error?
};

// For every highlighted code block...
document.querySelectorAll('div.highlight').forEach((div) => {
    // Make a clipboard button
    let button = document.createElement('button');
    button.type = 'button';
    button.title = 'Copy';
    button.innerText = message.copy;
    // When the button is clicked...
    button.addEventListener("click", () => {
        // Get the contents of the code as text
        let code = div.querySelector('pre').innerText;
        // Copy the code to the clipboard, and then ...
        navigator.clipboard.writeText(code).then(
            // ... if successful, change the button to a check mark
            () => button.innerText = message.confirm,
            // ... if something went wrong, change the button to an X
            () => button.innerText = message.error,
        ).then(() => {
            // Wait for the user to move the mouse or keyboard focus away
            ["mouseout", "focusout"].forEach((e) => {
                button.addEventListener(e, () => {
                    // Remove the :active pseudoclass from the button
                    document.activeElement && document.activeElement.blur();
                    // Then some timeout later, reset the button text
                    window.setTimeout(() => {
                        button.innerText = message.copy
                    }, reset);
                }, { once: true }); // Remove listener after it fires
            });
        });
    });
    // Finally, insert the prepared button into the document
    div.appendChild(button);         
});
</script>

Digression: minimalism vs. comprehensiveness

Just for fun, here's the script with all the unnecessary spacing and comments stripped out:

const reset=750;const message={copy:'📋',confirm:'✅',error:'❌',};document.querySelectorAll('div.highlight').forEach((div)=>{let button=document.createElement('button');button.type='button';button.title='Copy';button.innerText=message.copy;button.addEventListener("click",()=>{let code=div.querySelector('pre').innerText;navigator.clipboard.writeText(code).then(()=>button.innerText=message.confirm,()=>button.innerText=message.error).then(()=>{["mouseout","focusout"].forEach((e)=>{button.addEventListener(e,()=>{document.activeElement&&document.activeElement.blur();window.setTimeout(()=>{button.innerText=message.copy},reset);},{once:true});});});});div.appendChild(button);});

This is 685 bytes (374 bytes gzipped), and accomplishes precisely our goal. By contrast, merely including ClipboardJS would cost more than 8x this amount of bandwidth,3 and it doesn't even include the logic about traversing the DOM to insert our buttons.

Admittedly, ClipboardJS and similar have polyfills to make things work in browsers that don't support the clipboard.writeText API, but since as of this writing, 95% of global web traffic comes from supported browsers, I think this is all right.

Making it pretty

If you were following along and stopped now, you would have a <button> at the bottom of each code block which functionally accomplished the desired purpose. What remains is to make it pretty: locate it in the right place, and give it a nice hover effect.

This site's CSS is a little bit of a tangle, but the most important takeaway is that the <div class="highlight"> needs to be position: relative and the <button> needs to be position: absolute, so that the <button> can be positioned exactly at the upper right corner of the containing <div>.4 Having set those properties, the right and top properties can be used to precisely align the button.

I then styled it similarly to the links on the rest of the website, and added a hover effect using a CSS transition that made use of a box-shadow to give it that inviting golden glow.

If you want to see the exact details, just right-click on this very page and select “View Source”: it's all there... as it should be.

Till next time,

—finch

Want to catch the next post?

  1. Different Markdown rendering strategies might produce different HTML, so you might need to adapt this approach a little if you're using a different host or static site generator.

  2. If you're not using some Javascript framework that handles cross-browser compatibility to run code when the document is ready, the simplest way to make sure that a script runs after the rest of the document loads is to stick it in the bottom of the document. We want this, because this script needs to be able to traverse all the code blocks, which wouldn't necessarily exist in the DOM yet if the script was loaded and run in the <head>.

  3. Yes, I know that the sizes here are silly small; 3kb is a quarter of a millisecond on a very prosaic 100 megabit connection.

  4. If you are adapting this for a webpage that doesn't already have containing <div>s for each code block, you'll need to alter this code so that it creates one wrapped around each <pre> block. It will not work well if you put the button inside the <pre> itself, because if the code overflows the width of the <pre> (thus making it scrollable horizontally), the button will scroll with the content of the <pre> rather than remaining fixed. I learned this by doing :)