[Hugo] コードブロックをコピーするボタンを配置する

2022-12-05 (月)

Hugo の Markdown で書いた記事内のコードブロックに対して、クリップボードにコピーするボタンを動的に配置する方法です。

環境

  • Hugo 0.106.0
  • Windows 10 Pro 64bit 22H2 19045.2251

結果

public void Copy()
{
    // ここにカーソルを当てると (もしくはタッチすると)、右上にクリップボードにコピーするボタンが表示されます。
}

注意点

linenos=inline は未対応

linenos=inline 形式のコピーは未対応です。行番号とコードが混在してコピーされます。
linenos=table 形式なら問題ありません。

例えば、以下のように行番号を表示した場合です。

1```go {linenos=inline}
2// ... code
3```

ハイライト言語の指定が必須

ハイライト言語を指定しないと、コピーボタンが表示されません。

```
// ... code
```

特に指定がない場合は、text を指定すれば、コピーボタンが表示されます。

```text
// ... code
```

実装

主にこちらを参考にしました。
https://aaronluna.dev/blog/add-copy-button-to-code-blocks-hugo-chroma/

以下の点を修正しています。

  • マウスオーバー (スマホならタップ) した場合のみ、コピーボタンが表示されるように変更。
  • highlight-wrapper クラスを廃止。

copy-code-button.js

コードブロックに動的にボタンを追加する js です。

function createCopyButton(highlightDiv) {
    const button = document.createElement("button");
    button.className = "copy-code-button";
    button.type = "button";
    button.innerText = "Copy";
    button.addEventListener("click", () => copyCodeToClipboard(button, highlightDiv));
    addCopyButtonToDom(button, highlightDiv);
  }

  async function copyCodeToClipboard(button, highlightDiv) {
    const codeToCopy = highlightDiv.querySelector("pre.chroma > code[data-lang]").innerText;
    try {
      result = await navigator.permissions.query({ name: "clipboard-write" });
      if (result.state == "granted" || result.state == "prompt") {
        await navigator.clipboard.writeText(codeToCopy);
      } else {
        copyCodeBlockExecCommand(codeToCopy, highlightDiv);
      }
    } catch (_) {
      copyCodeBlockExecCommand(codeToCopy, highlightDiv);
    }
    finally {
      codeWasCopied(button);
    }
  }

  function copyCodeBlockExecCommand(codeToCopy, highlightDiv) {
    const textArea = document.createElement("textArea");
    textArea.contentEditable = 'true'
    textArea.readOnly = 'false'
    textArea.className = "copyable-text-area";
    textArea.value = codeToCopy;
    highlightDiv.insertBefore(textArea, highlightDiv.firstChild);
    const range = document.createRange()
    range.selectNodeContents(textArea)
    const sel = window.getSelection()
    sel.removeAllRanges()
    sel.addRange(range)
    textArea.setSelectionRange(0, 999999)
    document.execCommand("copy");
    highlightDiv.removeChild(textArea);
  }

  function codeWasCopied(button) {
    button.blur();
    button.innerText = "Copied!";
    setTimeout(function() {
      button.innerText = "Copy";
    }, 2000);
  }

  function addCopyButtonToDom(button, highlightDiv) {
    highlightDiv.appendChild(button);
  }

  document.querySelectorAll(".highlight")
    .forEach(highlightDiv => createCopyButton(highlightDiv));

copy-code-button.css

コピーボタンを配置するための .css です。

.highlight {
  position: relative;
}

.copy-code-button {
  position: absolute;
  z-index: 2;
  right: 0;
  top: 0;
  font-size: 13px;
  font-weight: 700;
  line-height: 14px;
  letter-spacing: 0.5px;
  width: 65px;
  color: #232326;
  background-color: #7f7f7f;
  border: 1.25px solid #232326;
  border-top-left-radius: 0;
  border-top-right-radius: 4px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 4px;
  white-space: nowrap;
  padding: 4px 4px 5px 4px;
  margin: 0 0 0 1px;
  cursor: pointer;
  opacity: 0.6;
}

.copy-code-button:hover,
.copy-code-button:focus,
.copy-code-button:active,
.copy-code-button:active:hover {
  color: #222225;
  background-color: #b3b3b3;
  opacity: 0.8;
}

.copyable-text-area {
  position: absolute;
  height: 0;
  z-index: -1;
  opacity: .01;
}

.highlight .copy-code-button {
  visibility: hidden;
}

.highlight:hover .copy-code-button {
  visibility: visible;
}

baseof.html

上記を適用するテンプレートの .html 例です。
この例では、minify, fingerprint などは考慮しません。

<html>
  <head>
  {{ if (findRE "<pre" .Content 1) }}
    <link rel="stylesheet" href="xxx/copy-button.min.css" integrity="xxx=" media="screen">
  {{{ end }}}
  </head>

  <body>
  {{ if (findRE "<pre" .Content 1) }}
    <script async src="xxx/copy-code-button.min.js"></script>
  {{ end }}
  </body>
</html>

感謝

2022-12-05 (月)