HUGO:コードブロックのコピーボタンを表示する&コンソールのときはプロンプト($)を除外する

Today I Learned

コマンドの実行を示すとき、コマンドであることを伝えるためにも、次のようにプロンプト($)を書くことが多い。

Markdownの例
```bash {iscopy=true}
$ hugo new posts/ramen.md
$ hugo server
```

反対に、コードブロックをコピーしたときのテキストにプロンプトが含まれていると、コマンドとして実行できない。
プロンプト($)を除外してコピーできると便利そうなので、その実現方法を調べる。

コードブロックのコピーボタンを表示する

実装方法

コードブロックにコピーボタンが表示されている

HUGOにはCode Block Render Hooksという、コードブロックがHTMLにレンダリングされる際、処理を差し込める機能がある。
Code Block Render Hooksの使い方は次の記事で説明したため、詳細の詳細は割愛する。

v0.93.0で追加されたCode Block Render Hooksを使って、コードブロックにファイル名を表示する方法を説明します。
ひよこまめ

今回は、次のように言語の識別子の後に{iscopy=true}と書くと、コピーボタンを表示することにする。

Markdownの例
```js {iscopy=true}
(() => {
  'use strict';
  console.log("Hello World!");
})();
```

実装

layouts/_default/_markup/render-codeblock.htmlは、次のように書く。
iscopytrueが渡されたときだけコピーボタンのDOMを生成する。

layouts/_default/_markup/render-codeblock.html
<div class="codeblock-container">
  {{ if .Attributes.iscopy }}
  <button class="codeblock-button" title="copy">
    Copy
  </button>{{ end }}
  <div class="codeblock-content">
    {{- highlight ( .Inner | safeHTML) .Type .Options }}
  </div>
</div>

コピーボタンを押した際の処理を行うJavaScriptのコードは次のとおり。

assets/js/copy.js
(function () {
  const copyToClipboad = async (text) => {
    if (navigator.clipboard) {
      navigator.clipboard.writeText(text);
    }
  };
  window.addEventListener("load", function () {
    const button = document.querySelector(".codeblock-button");
    if (button) {
      button.addEventListener("click", function () {
        const codeblock = document.querySelector(".codeblock-content");
        if (codeblock) {
          // 最後にスペース 2 つが入るので除外する
          const content = codeblock.textContent.replace(/  $/g, "");
          copyToClipboad(content);
        }
      });
    }
  });
})();

このJavaScriptのコードをHUGOテンプレートで読み込む。

layouts/_default/baseof.html
<body>
<!-- 省略 -->
{{- $js := resources.Get "js/copy.js" | resources.Minify | resources.Fingerprint -}}
{{- with $js -}}
<script src="{{ .RelPermalink }}"></script>
{{- end -}}
</body>

動作確認

[Copy]を押すと、次の内容をコピーできた。

(() => {
  'use strict';
  console.log("Hello World!");
})();

コピー時にプロンプト($)を除外する

実装

プロンプト($)を書くのは、言語の識別子にbashを指定したときだけにする。
render-codeblock.html内で分岐しても良いが、render-codeblock-言語の識別子.htmlというファイルで言語ごとに処理を分けることもできる。
今回の言語の識別子はbashなのでlayouts/_default/_markup/render-codeblock-bash.htmlというファイルを作成する。

layouts/_default/_markup/render-codeblock-bash.html
<!-- 内容は「コードブロックのコピーボタンを表示する」と同じなので省略 -->

プロンプトを取り除く処理は、JavaScript側で行う。
コードブロックのコピーボタンを表示するで示したコードに対し、冒頭の$ を正規表現で取り除く処理を追加する。

assets/js/copy.js
(function () {
  const copyToClipboad = async (text) => {
    if (navigator.clipboard) {
      navigator.clipboard.writeText(text);
    }
  };
  window.addEventListener("load", function () {
    const button = document.querySelector(".codeblock-button");
    if (button) {
      button.addEventListener("click", function () {
        const codeblock = document.querySelector(".codeblock-content");
        if (codeblock) {
          // 最後にスペース 2 つが入るので除外する
          const content = codeblock.textContent.replace(/  $/g, "");
-          copyToClipboad(content);
+          const contentWithoutPrompt = content.replace(/^\$\s+/gm, "");
+          copyToClipboad(contentWithoutPrompt);
        }
      });
    }
  });
})();

JavaScriptのコードをテンプレートで読み込む処理は、先ほどと同じなので省略する。

動作確認

[Copy]を押すと、次の内容をコピーできた。

hugo new posts/ramen.md
hugo server

補足:CSS

今回は、コピーボタンに次のCSSを適用した。

copy.css
.codeblock-button {
  width: fit-content;
  margin: 1rem 0 0;
  padding: 0.4rem;
  font-size: 0.9rem;
  color: rgb(255, 255, 255);
  background-color: #05acc1;
  border: 0;
}

.codeblock-button:hover {
  filter: brightness(1.25);
}

.codeblock-button + .codeblock-content {
  margin-top: -1.25rem;
}