Highlight.js のカスタマイズ サンプル

シンタックスハイライトのライブラリ Highlight.js のカスタマイズのサンプルです。

このサンプルでは、コードの上部にツールバーを追加して、行番号の表示・非表示や行の自動折り返しのあり・なし、コピーボタンの表示などが可能です。

スタイルの調整が必要になるなど実用レベルではありませんが、シンプルなサイトであればサンプルのコードをコピペで試すことができると思います。または、カスタマイズのご参考になれば何よりです。

完成したコードではないため修正する点等多々あるかと思いますので、自己責任でご利用ください。

以下で使用している Highlight.js のバージョンは 11.9.0 です。

関連ページ:

作成日:2024年1月11日

[更新]

概要

Highlight.js の CSS と JavaScript を読み込み、その後にサンプルの CSS と JavaScript コードを記述または読み込みます。必要に応じてサンプルコードを編集します(使い方参照)。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Hilight.js Sample</title>
  <!-- テーマ CSS の読み込み -->
  <link rel="stylesheet" href="path/to/atom-one-dark.min.css">
  <!-- 独自スタイル(サンプルコード)の読み込み -->
  <link rel="stylesheet" href="path/to/custom.css">
</head>
<body>
  ・・・中略・・・
  <!-- highlight.min.js の読み込み -->
  <script src="path/to/highlight.min.js"></script>
  <!-- サンプルコードの JS の読み込み -->
  <script src="path/to/custom.js"></script>
</body>
</html>

例えば、以下のように記述すると(コード部分は省略しています)、

<div class="hljs-wrap">
  <pre data-label="foo.js" data-line-highlight="3, 10-15" data-max-lines="20" class="pre-wrap"><code class="language-JavaScript">
  ・・・表示するコード・・・
</code></pre>
</div>

以下のように表示されます(表示しているコードの内容は本文と関係ありません)。

表示するラベルの指定や行のハイライトの指定などはカスタムデータ属性(data-*)を使います。

クラスを指定することで初期状態で行番号を非表示にしたり、自動折り返しなしにするなどのオプションも設定することができます。何も指定しなければデフォルトの設定が適用されます。

詳細:オプション

また、JavaScript(custom.js) でラッパー要素に指定するクラス名やデフォルトでツールバーや行番号を表示するかどうかなどの設定が可能です。

import { __ } from "@wordpress/i18n";
// InspectorControls を追加でインポート
import { useBlockProps, InspectorControls } from "@wordpress/block-editor";
// TextareaControl, PanelBody, TextControl を components からインポート
import { TextareaControl, PanelBody, TextControl } from "@wordpress/components";
import "./editor.scss";

export default function Edit({ attributes, setAttributes, isSelected }) {
  // テキストエリア(TextareaControl)の行数
  const codeTextRowCount = attributes.codeText.split(/\r|\r\n|\n/).length;
  const codeTextRows = codeTextRowCount > 3 ? codeTextRowCount : 3;

  // ブロックの内容を JSX で返す
  return (
    <>
      <InspectorControls>
        <PanelBody title={__("Settings", "my-highlight-block")}>
          <TextControl
            label={__("Language", "my-highlight-block")}
            value={attributes.language || ""}
            onChange={(value) => setAttributes({ language: value })}
          />
        </PanelBody>
      </InspectorControls>
      <div {...useBlockProps()}>
        <TextareaControl
          label={__("Highlight Code", "my-highlight-block")}
          value={attributes.codeText}
          onChange={(value) => setAttributes({ codeText: value })}
          rows={codeTextRows}
          placeholder={__("Write your code...", "my-highlight-block")}
          hideLabelFromVision={ isSelected ? false : true}
        />
      </div>
    </>
  );
}

ツールバーに表示できる項目は、言語名(左側)、行番号、行折り返し、コピーボタンです。ラベル(ファイル名やリンクなどの文字列)はツールバーの右上に表示するようにしていますが、CSS で調整可能です。

インラインの code 要素もハイライトできます。code 要素にハイライト用のクラスと必要に応じて言語名のクラスを指定します。

<p>Lorem ipsum dolor sit amet <code class="highlight">&lt;span class=&quot;line-num&quot;&gt;&lt;/span&gt;</code> adipisicing elit. </p>

上記は以下のように表示されます。

Lorem ipsum dolor sit amet <span class="line-num"></span> adipisicing elit.

ラッパー要素に toggle-accordion クラスを指定するか、details 要素と summary 要素を使った所定のマークアップを記述すれば、以下のようなアコーディオンでコードを表示することもできます。

JavaScript を表示
// サンプルコード(本文とは関係なし)
document.querySelectorAll('pre').forEach( (elem) => {
  elem.innerHTML = elem.innerHTML.replace(/\n*\s*<code (.*)>/g, '<code $1>').replace(/<\/code>\n*\s*/g, '</code>');
});

hljs.highlightAll(); 

使用しているテーマ

サンプルの CSS は atom-one-dark というテーマを使用することを前提に設定しているので、異なるテーマを使う場合は適宜色などを調整する必要があります。

Plugin API

このサンプルでは Hightlight.js の addPlugin() を使って独自のプラグインを定義しています。

Hightlight.js ドキュメント:Plugin API

以下ではコードの解説はありませんが、次のページに独自のプラグインを定義する方法や Hightlight.js の基本的な使い方を掲載していますので、よろしければ御覧ください。

Highlight.js でシンタックスハイライト

JavaScript

以下がカスタマイズ用の JavaScript です。

mySetUpHljsPlugins() は Highlight.js のカスタマイズ用プラグインを定義した関数です

mySetupHighlightJs() は Highlight.js の初期化とカスタマイズを定義した関数です。

myAddAccordionPanel() は開閉パネル(アコーディオンパネル)を追加する関数です。

mySetupToggleDetailsAnimation() はアコーディオンアニメーションでコードや任意の要素の開閉表示を行う関数です。

myCustomHighlightJsSettings は設定用のオブジェクトです。

デフォルトでは hljs-wrap クラスを指定した div 要素でラップした pre 要素内の code 要素をハイライトの対象にしますが、15行目で変更できます。

行の自動折り返しを有効にするかどうかは、17行目で指定できます(デフォルトは自動折り返ししない)。個々にクラスを指定して設定することができ、ツールバーで切り替えることができます。

code 要素に highlight クラスを指定すると、インラインで code 要素をハイライト表示します。21行目で指定するクラス名を変更できます。

ツールバーは highlight-toobar クラスを指定した div 要素を JavaScript で createElement を使って生成しています。高さは CSS で設定しますが、JavaScript でも指定する必要があります。

サンプルではツールバーの高さを 2rem としているので 32px(標準の場合)としていますが、高さを変更した場合や基準が異なる場合は JavaScript の29行目も変更します。

ツールバーやボタンのテキストは39-44行目で変更することができます。

document.addEventListener('DOMContentLoaded', () => {
  // Highlight.js のプラグインをセットアップ
  mySetUpHljsPlugins(myCustomHighlightJsSettings);
  // Highlight.js の初期化とカスタマイズの実行
  mySetupHighlightJs(myCustomHighlightJsSettings);
  // アコーディオンアニメーションの開閉パネルの追加(オプショナル)
  myAddAccordionPanel('toggle-accordion');
  // アコーディオンアニメーションの呼び出し(オプショナル)
  mySetupToggleDetailsAnimation();
});

const myCustomHighlightJsSettings = {
  bodyClassList: document.body.classList,
  // pre code のラッパーのクラス名
  wrapperClassName: 'hljs-wrap',
  // デフォルトで行の自動折り返しを有効にするかどうか
  preWrapOnInit: false,
  // pre 要素なし(インライン)の code 要素でもハイライトするかどうか
  useInlineHighlight: true,
  // インラインの code 要素でハイライトする場合に code 要素に指定するクラス(空の場合、全ての code 要素)
  inlineHighlightClassName: 'highlight',
  // 初期状態でコピーボタンを非表示
  noCopyBtnOnInit: false,
  // 初期状態で行番号を非表示(ツールバー使用時のみ有効)
  noLineNumOnInit: document.body.classList.contains('no-line-num') ? true : false,
  // ツールバーを表示(使用)するかどうか(全てのページで使用しない場合は false を指定)
  useToolbar: document.body.classList.contains('no-toolbar') ? false : true,
  // ツールバーの高さ(単位ピクセル)
  toolbarHeight: 32,
  // 以下 は 2rem をピクセルに変換して指定する場合の例
  // toolbarHeight: parseFloat(getComputedStyle(document.documentElement).fontSize) * 2,
  // 行数を指定して表示する場合の高さの調整値
  heightAdjustAmount: -3,
  // 行数を指定して表示する場合にスクロール量の調整値
  scrollAdjustAmount: 3,
  // 行数を指定して表示する場合のスクロール量の初期値
  scrollAmount: 0,
  // ツールバーやボタンのテキスト(ラベル)
  lineAutoWrapLabel: 'wrap',
  lineNumLabel: 'number',
  copyBtnLabel: 'Copy',
  copyBtnCompleteLabel: 'Copied',
  copyBtnFailedLabel: 'Failed',
  copyFailedMessage: 'Sorry, can not copy with this browser.',
}

// Hightlight.js のプラグインを定義
function mySetUpHljsPlugins(settings) {
  const {
    wrapperClassName,
    useInlineHighlight,
    noCopyBtnOnInit,
    useToolbar,
    toolbarHeight,
    heightAdjustAmount,
    scrollAdjustAmount,
    copyBtnLabel,
    copyBtnCompleteLabel,
    copyBtnFailedLabel,
    copyFailedMessage,
  } = settings;
  let { scrollAmount }  = settings;

  // Hightlight.js の addPlugin() でプラグインを定義
  hljs.addPlugin({
    'after:highlightElement': ({ el, result, text }) => {
      // ラッパー要素
      const wrapper = el.closest('.' + wrapperClassName);
      // pre 要素(親要素)
      const pre = el.parentElement;
      showLanguage(el, result, wrapper);
      copyCode(text, pre);
      addLineNumbers(el, result, wrapper, pre);
      highlightNumbers(el, pre);
      setMaxHeight(el, wrapper, pre);
    }
  });

  // 言語名を表示するプラグイン用の関数
  function showLanguage(el, result, wrapper) {
    if (el.classList.contains('show-no-lang')) {
      if (wrapper) wrapper.classList.add('no-lang');
      return;
    }
    if (el.hasAttribute('data-set-lang')) {
      addLanguageSpan(el.getAttribute('data-set-lang'));
      return;
    }
    if (result.language) {
      if (useToolbar) {
        addLanguageSpan(result.language);
      } else {
        el.dataset.language = result.language;
      }
    }
    function addLanguageSpan(language) {
      const languageSpan = document.createElement('span');
      languageSpan.setAttribute('class', 'lng-span');
      languageSpan.textContent = language;
      const wrapper = el.closest('.' + wrapperClassName);
      if (wrapper && !wrapper.classList.contains('no-toolbar')) {
        wrapper.appendChild(languageSpan);
      } else if (wrapper && wrapper.classList.contains('no-toolbar')) {
        el.dataset.language = language;
      }
    }
  }

  // コードをコピーするプラグイン用の関数
  function copyCode(text, pre) {
    const preClass = pre.classList;
    if (preClass.contains('no-copy-btn')) return;
    if (noCopyBtnOnInit && !preClass.contains('show-copy-btn')) return;
    if (useInlineHighlight && pre.nodeName !== 'PRE') return;
    const copyButton = document.createElement('button');
    copyButton.setAttribute('class', 'hljs-copy-btn');
    copyButton.textContent = copyBtnLabel;
    pre.after(copyButton);
    copyButton.addEventListener('click', () => {
      copyToClipboard(copyButton, text)
    });
    function copyToClipboard(btn, text) {
      if (!navigator.clipboard) {
        alert(copyFailedMessage);
      }
      if (preClass.contains('copy-no-prompt')) {
        text = text.replace(/^\$\s|^%\s/gm, '');
      }
      if (preClass.contains('copy-no-sl-comments') || preClass.contains('copy-no-comments')) {
        // 行の途中の「半角スペース + //」も削除(コメント以外も削除する可能性あり)
        text = text.replace(/^(\s*\/\/).*$\r?\n?/gm, "").replace(/(.*)\s\/\/.*/g, "$1");
      }
      // replace() の第2引数に関数 replaceComments を指定(正しくマッチしない可能性あり)
      if (preClass.contains('copy-no-ml-comments') || preClass.contains('copy-no-comments')) {
        text = text.replace(/^(.*)\/\*[\s\S]*?\*\/($\r?\n?)?/gm, replaceComments)
      }
      if (preClass.contains('copy-no-html-comments')) {
        text = text.replace(/^(.*)<!\-\-[\s\S]*?\-\->($\r?\n?)?/gm, replaceComments)
      }
      function replaceComments(match, p1, p2) {
        // コメントの後に改行がない場合(p2 は undefined)
        if (!p2) p2 = '';
        // コメントの前が空白文字の場合
        if (!p1.trim()) {
          if(p2) {
            return '';
          }
          return p1;
        } else {
          return p1 + p2;
        }
      }
      navigator.clipboard.writeText(text).then(
        () => {
          btn.textContent = copyBtnCompleteLabel;
          resetCopyBtnText(btn, 1500);
        },
        (error) => {
          btn.textContent = copyBtnFailedLabel;
          resetCopyBtnText(btn, 1500);
          console.log(error.message);
        }
      );
    };
    function resetCopyBtnText(btn, delay) {
      setTimeout(() => {
        btn.textContent = copyBtnLabel
      }, delay)
    }
  }

  // 行番号表示するプラグイン用の関数
  function addLineNumbers(el, result, wrapper, pre) {
    // 以下を修正(2024/02/26)
    el.innerHTML = result.value.replace( /^/gm, '<span class="line-num"></span>');
    let startNumOffset = 0;
    if (pre.hasAttribute('data-line-num-start')) {
      const startNumber = parseInt(pre.getAttribute('data-line-num-start'));
      if (startNumber || startNumber === 0) {
        pre.style.setProperty('counter-reset', 'lineNumber ' + (startNumber - 1));
        startNumOffset = startNumber - 1;
      }
    }
    if (wrapper) {
      const wrapperHeight = useToolbar && !wrapper.classList.contains('no-toolbar') ? wrapper.offsetHeight - toolbarHeight : wrapper.offsetHeight;
      const borderSpan = document.createElement('span');
      borderSpan.classList.add('hljs-border-span');
      el.appendChild(borderSpan);
      borderSpan.style.setProperty('position', 'absolute');
      borderSpan.style.setProperty('height', wrapperHeight + 'px');
      // code 要素のスタイルを取得
      const elComputedStyle = window.getComputedStyle(el);
      // code 要素の paddingBottom
      const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
      const wrapperObserver = new ResizeObserver((entries) => {
        if (entries.length > 0 && entries[0]) {
          const entry = entries[0];
          let resizedHeight = entry.contentBoxSize ? entry.contentBoxSize[0].blockSize : entry.contentRect.height;
          resizedHeight = useToolbar && !wrapper.classList.contains('no-toolbar') ? resizedHeight - toolbarHeight : resizedHeight;
          borderSpan.style.setProperty('height', resizedHeight + 'px');
          // wrapper 要素に max-lines クラスが指定されていれば
          if(wrapper.classList.contains('max-lines-enabled')) {
            // 行番号の最後の span 要素から高さを算出して枠線用の span 要素に設定
            const lineNumSpans = el.getElementsByClassName("line-num");
            if(lineNumSpans[lineNumSpans.length-1]) {
              const lastLineNumTop = lineNumSpans[lineNumSpans.length-1].offsetTop;
              // 行番号の最後の span 要素の高さを取得(1つ要素を追加して offsetTop の差分から現在の高さを取得)
              const dummy = document.createElement('span');
              dummy.innerHTML = "<br>";
              el.appendChild(dummy);
              const dummy2 = document.createElement('span');
              el.appendChild(dummy2);
              const dummy2OffsetTop = dummy2.offsetTop;
              const lastLineNumHeight = dummy2OffsetTop - lastLineNumTop;
              dummy.remove();
              dummy2.remove();
              borderSpan.style.setProperty('height', lastLineNumTop + lastLineNumHeight + elPaddingBottom + 'px');
            }
          }
          // ハイライト行の高さの更新
          const lineNumSpan = el.getElementsByClassName('line-num');
          // HTMLCollection を配列に変換
          const lineNumSpanArray =  Array.prototype.slice.call( lineNumSpan );
          lineNumSpanArray.forEach( (elem, index) => {
            // line-num-highlight クラスが指定されていれば高さを更新
            if(elem.classList.contains('line-num-highlight')) {
              // ハイライト用の span 要素(span.line-highlight)
              const highlightSpan = lineNumSpan[index].nextElementSibling;
              if(highlightSpan) {
                if(lineNumSpan.item(index) && lineNumSpan.item(index +1)) {
                  const spanOffsetTop =  lineNumSpan.item(index).offsetTop;
                  const nextSpanOffsetTop =  lineNumSpan.item(index +1).offsetTop;
                  const height = nextSpanOffsetTop - spanOffsetTop;
                  if(height !== 0) {
                    highlightSpan.style.setProperty('height', height + 'px');
                  }
                }else if(lineNumSpan.item(index) && index === lineNumSpan.length - 1) {
                  // 最後の行の場合は次の行との差分を算出できないので、ダミーを挿入して高さを取得
                  const spanOffsetTop =  lineNumSpan.item(index).offsetTop;
                  const dummy = document.createElement('span');
                  dummy.innerHTML = "<br>";
                  el.appendChild(dummy);
                  const dummy2 = document.createElement('span');
                  el.appendChild(dummy2);
                  const dummy2OffsetTop = dummy2.offsetTop;
                  const height = dummy2OffsetTop - spanOffsetTop;
                  dummy.remove();
                  dummy2.remove();
                  if(height !== 0) {
                    highlightSpan.style.setProperty('height', height + 'px');
                  }
                }
              }
            }
          });
        }
      });
      wrapperObserver.observe(wrapper);
    }
  }

  //指定された行をハイライト表示する関数
  function highlightNumbers(el, pre) {
    if (pre.hasAttribute('data-line-highlight')) {
      const targetLines = pre.getAttribute('data-line-highlight');
      const highlightCode = pre.classList.contains('no-highlight-code') ? false : true;
      const highlightNumber = pre.classList.contains('no-highlight-number') ? false : true;
      const targets = targetLines.split(',').map((val) => val.trim());
      if (targets.length > 0) {
        const lineNumSpan = el.getElementsByClassName('line-num');
        const lineLength = lineNumSpan.length;
        targets.forEach((target) => {
          let startNumOffset = 0;
          if (pre.hasAttribute('data-line-num-start')) {
            const startNumber = parseInt(pre.getAttribute('data-line-num-start'));
            if (startNumber || startNumber === 0) {
              startNumOffset = startNumber - 1;
            }
          }
          const range = target.split('-');
          if (range.length === 2) {
            if (range[0] !== '') {
              const start = startNumOffset === 0 ? parseInt(range[0]) : parseInt(range[0]) - startNumOffset;
              const end = startNumOffset === 0 ? parseInt(range[1]) : parseInt(range[1]) - startNumOffset;
              if (start && end) {
                if (end >= start) {
                  for (let i = start; i <= end; i++) {
                    addClassToSpan(i);
                  }
                } else {
                  for (let i = end; i <= start; i++) {
                    addClassToSpan(i);
                  }
                }
              }
            } else {
              const negativeNum = (startNumOffset === 0 ? parseInt(range[1]) : parseInt(range[1])) * -1;
              addClassToSpan(negativeNum - startNumOffset);
            }
          } else if (range.length === 1) {
            addClassToSpan(startNumOffset === 0 ? parseInt(target) : parseInt(target) - startNumOffset);
          }
          function addClassToSpan(number) {
            if (number > 0 && number <= lineLength) {
              if (highlightCode) {
                const highlightSpan = document.createElement('span');
                highlightSpan.className = 'line-highlight';
                lineNumSpan.item(number - 1).after(highlightSpan);
              }
              if (highlightNumber) {
                lineNumSpan.item(number - 1).classList.add('line-num-highlight');
              }
            }
          }
        })
      }
    }
  }

  // 表示する行数を指定して code 要素に max-height を設定するプラグイン用の関数
  function setMaxHeight(el, wrapper, pre) {
    if (!pre.hasAttribute('data-max-lines')) return;
    if (!wrapper) return;
    // 表示する行数を指定している場合は wrapper 要素に max-lines クラスを追加
    wrapper.classList.add('max-lines-enabled');
    const lineNumSpan = el.getElementsByClassName('line-num');
    if (lineNumSpan.length > 0) {
      const dataMaxLine = parseInt(pre.getAttribute('data-max-lines'));
      if (dataMaxLine && lineNumSpan.length > dataMaxLine && lineNumSpan.item(dataMaxLine)) {
        // 表示する最後の行から max-height を算出
        const dataMaxLineOffset = parseInt(pre.getAttribute('data-max-lines-offset'));
        const heightOffset = dataMaxLineOffset ? dataMaxLineOffset : 0;
        // 表示する最後の行の次の行の要素の offsetTop
        const targetOffsetTop = lineNumSpan.item(dataMaxLine).offsetTop;
        const elComputedStyle = window.getComputedStyle(el);
        const elPaddingTop = parseFloat(elComputedStyle.paddingTop);
        const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
        const visibleHeight = targetOffsetTop + heightOffset - elPaddingTop - elPaddingBottom;
        el.style.setProperty('max-height', visibleHeight + 'px');
        el.style.setProperty('overflow-y', 'scroll');
        // code 要素の高さとスクロール位置の調整
        if (pre.hasAttribute('data-scroll-to')) {
          let dataScrollTo = parseInt(pre.getAttribute('data-scroll-to'));
          const dataMaxLine = parseInt(pre.getAttribute('data-max-lines'));
          if (pre.hasAttribute('data-line-num-start')) {
            const startNumber = parseInt(pre.getAttribute('data-line-num-start'));
            if(startNumber) {
              dataScrollTo -= startNumber -1;
            }
          }
          if(dataScrollTo + dataMaxLine > lineNumSpan.length + 1) {
            console.log('data-scroll-to or data-max-line is not valid number')
            return;
          }
          if (dataScrollTo && dataMaxLine && lineNumSpan.item(dataScrollTo - 1) && lineNumSpan.item(dataScrollTo + dataMaxLine - 2)) {
            const scrollToOffsetTop = lineNumSpan.item(dataScrollTo - 1).offsetTop;
            const lastLineRow = lineNumSpan.item(dataScrollTo + dataMaxLine - 2).offsetTop;
            const dataMaxLineOffset = parseInt(pre.getAttribute('data-max-lines-offset'));
            const heightOffset = dataMaxLineOffset ? dataMaxLineOffset : 0;
            el.style.setProperty('max-height', (lastLineRow - scrollToOffsetTop + heightOffset + heightAdjustAmount) + 'px');
            scrollAmount = scrollToOffsetTop - scrollAdjustAmount;
            el.scroll(0, scrollAmount);
          }
        }
      }
    }
  }
}

// Highlight.js の初期化とカスタマイズする関数。
// targetWrapper は単一の要素のみに適用する場合に指定(WordPress のエディタでのプレビュー用に使用する場合に指定)
function mySetupHighlightJs(settings, targetWrapper = false) {
  const {
    wrapperClassName,
    preWrapOnInit,
    useInlineHighlight,
    inlineHighlightClassName,
    noLineNumOnInit,
    useToolbar,
    heightAdjustAmount,
    scrollAdjustAmount,
    lineAutoWrapLabel,
    lineNumLabel,
  } = settings;
  let { scrollAmount }  = settings;

  // 全てのラッパー要素を取得
  const wrappers = document.getElementsByClassName(wrapperClassName);

  // インラインでハイライトする場合
  if (useInlineHighlight) {
    // pre 要素なしの code 要素でハイライト
    const inlineHighlightElems = document.getElementsByClassName(inlineHighlightClassName);
    for (const elem of inlineHighlightElems) {
      if (elem.parentElement.nodeName !== 'PRE') {
        hljs.highlightElement(elem);
      }
    }
  }

  // Highlight.js の初期化とセットアップ
  if (wrappers.length > 0 && !targetWrapper) {
    for (const wrapper of wrappers) {
      hljs.highlightElement(wrapper.querySelector('pre code'));
    }
    for (let i=0; i<wrappers.length; i++ ) {
      setUpWrapper(wrappers[i], i);
    }
  }else if (targetWrapper) {
    const code = targetWrapper.querySelector('code');
    if(code) {
      hljs.highlightElement(code);
      setUpWrapper(targetWrapper, 0);
    }
  }

  function setUpWrapper(wrapper, index) {
    const pre = wrapper.querySelector('pre');
    const code = wrapper.querySelector('pre code');
    if (!pre || !code) return;
    const preClass = pre.classList;
    const wrapperClass = wrapper.classList;
    if(preWrapOnInit) preClass.add('pre-wrap');
    if (pre.hasAttribute('data-label')) {
      const label = pre.getAttribute('data-label');
      let element;
      if (pre.hasAttribute('data-label-url')) {
        element = document.createElement('a');
        element.href = pre.getAttribute('data-label-url');
        element.classList.add('hljs-label-url');
        if (preClass.contains('target-blank')) {
          element.target = "_blank";
          element.rel = "noopener";
        }
      } else {
        element = document.createElement('span');
        element.classList.add('hljs-label');
      }
      element.textContent = label;
      wrapper.appendChild(element);
      wrapperClass.add('has-label');
    }

    // no-line-num クラスを指定した要素の行番号を非表示
    if (preClass.contains('no-line-num')) {
      code.classList.add('hide-line-num');
    }

    // ツールバーの追加
    if (useToolbar) {
      if (!wrapperClass.contains('no-toolbar')) {
        const toolbar = document.createElement('div');
        toolbar.setAttribute('class', 'highlight-toobar');
        const noLineNumChecked = noLineNumOnInit ? '' : ' checked';
        let lineWrapChecked = preWrapOnInit ? ' checked' : '';
        if(preClass.contains('pre')) {
          lineWrapChecked = '';
        }else if(preClass.contains('pre-wrap')){
          lineWrapChecked = ' checked';
        }
        toolbar.innerHTML = `<input type="checkbox" id="line-auto-wrap${index}" name="line-auto-wrap"${lineWrapChecked}>
<label for="line-auto-wrap${index}">${lineAutoWrapLabel}</label>`;
        const noLineNum = preClass.contains('no-line-num');
        if (!noLineNum) {
          toolbar.insertAdjacentHTML('afterbegin', `<input type="checkbox" id="line-num-check${index}" name="line-num-check"${noLineNumChecked}>
<label for="line-num-check${index}">${lineNumLabel}</label>`);
        }
        wrapper.insertBefore(toolbar, wrapper.firstElementChild);
        const lineNumCheck = toolbar.querySelector('[name="line-num-check"]');
        const borderSpan = wrapper.querySelector('.hljs-border-span');
        const lineNums = wrapper.getElementsByClassName('line-num');
        if (borderSpan) {
          if (lineNumCheck) {
            lineNumCheck.addEventListener('change', (e) => {
              if (e.currentTarget.checked) {
                code.classList.remove('hide-line-num');
              } else {
                code.classList.add('hide-line-num');
              }
            });
            if (noLineNumOnInit) {
              code.classList.add('hide-line-num');
            }
          }
        }
        const lineAutoWrapCheck = toolbar.querySelector('[name="line-auto-wrap"]');
        lineAutoWrapCheck.addEventListener('change', (e) => {
          const lineNumSpan = code.getElementsByClassName('line-num');
          const elComputedStyle = window.getComputedStyle(code);
          const elPaddingTop = parseFloat(elComputedStyle.paddingTop);
          const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
          // 行を折り返す場合とそうでない場合の処理
          if (e.currentTarget.checked) {
            pre.style.setProperty('white-space', 'pre-wrap');
            // 行番号用 span 要素の高さを更新
            updateBorderSpan();
            updateHighlightSpan();
          } else {
            pre.style.setProperty('white-space', 'pre');
            // 行番号用 span 要素の高さを更新
            updateBorderSpan();
            updateHighlightSpan();
          }
          // 行番号用 span 要素の高さを更新する関数
          function updateBorderSpan() {
            if (borderSpan) {
              // 行番号枠線の高さの更新
              borderSpan.style.setProperty('height', code.offsetHeight + 'px');
              // 表示する行数(max-height)を指定している場合は code 要素の高さは固定なので、最後の行のオフセットトップを使う
              if (pre.hasAttribute('data-max-lines') && lineNumSpan.item(lineNumSpan.length - 1) && lineNumSpan.length > 0) {
                const codeHeight = lineNumSpan.item(lineNumSpan.length - 1).offsetTop + lineNumSpan.item(lineNumSpan.length - 1).offsetHeight;
                borderSpan.style.setProperty('height', (codeHeight + elPaddingBottom) + 'px');
                const dataMaxLine = parseInt(pre.getAttribute('data-max-lines'));
                if (dataMaxLine && lineNumSpan.length > dataMaxLine) {
                  // 行番号の最後の span 要素から高さを算出して枠線用の span 要素に設定
                  const lineNumSpans = code.getElementsByClassName("line-num");
                  if(lineNumSpans[lineNumSpans.length-1]) {
                    const lastLineNumTop = lineNumSpans[lineNumSpans.length-1].offsetTop;
                    // 行番号の最後の span 要素の高さを取得(1つ要素を追加して offsetTop の差分から現在の高さを取得)
                    const dummy = document.createElement('span');
                    dummy.innerHTML = "<br>";
                    code.appendChild(dummy);
                    const dummy2 = document.createElement('span');
                    code.appendChild(dummy2);
                    const dummy2OffsetTop = dummy2.offsetTop;
                    const lastLineNumHeight = dummy2OffsetTop - lastLineNumTop;
                    dummy.remove();
                    dummy2.remove();
                    borderSpan.style.setProperty('height', lastLineNumTop + lastLineNumHeight + elPaddingBottom + 'px');
                  }
                  // data-scroll-to が指定されている場合
                  if (pre.hasAttribute('data-scroll-to')) {
                    const dataScrollTo = parseInt(pre.getAttribute('data-scroll-to'));
                    const dataMaxLine = parseInt(pre.getAttribute('data-max-lines'));
                    if(dataScrollTo + dataMaxLine > lineNumSpan.length + 1) {
                      console.log('data-scroll-to or data-max-line is not valid number')
                      return;
                    }
                    if (dataScrollTo && dataMaxLine && lineNumSpan.item(dataScrollTo - 1) && lineNumSpan.item(dataScrollTo + dataMaxLine - 2)) {
                      const scrollToOffsetTop = lineNumSpan.item(dataScrollTo - 1).offsetTop;
                      const lastLineRow = lineNumSpan.item(dataScrollTo + dataMaxLine - 2).offsetTop;
                      const dataMaxLineOffset = parseInt(pre.getAttribute('data-max-lines-offset'));
                      const heightOffset = dataMaxLineOffset ? dataMaxLineOffset : 0;
                      code.style.setProperty('max-height', (lastLineRow - scrollToOffsetTop + heightOffset + heightAdjustAmount) + 'px');
                      scrollAmount = scrollToOffsetTop - scrollAdjustAmount;
                    }
                  }
                }
              }
            }
          }
          function updateHighlightSpan() {
            // ハイライト行の高さの更新
            const lineNumSpan = code.getElementsByClassName('line-num');
            // HTMLCollection を配列に変換
            const lineNumSpanArray =  Array.prototype.slice.call( lineNumSpan );
            lineNumSpanArray.forEach( (elem, index) => {
              // line-num-highlight クラスが指定されていれば高さを更新
              if(elem.classList.contains('line-num-highlight')) {
                // ハイライト用の span 要素(span.line-highlight)
                const highlightSpan = lineNumSpan[index].nextElementSibling;
                if(highlightSpan) {
                  if(lineNumSpan.item(index) && lineNumSpan.item(index +1)) {
                    const spanOffsetTop =  lineNumSpan.item(index).offsetTop;
                    const nextSpanOffsetTop =  lineNumSpan.item(index +1).offsetTop;
                    const height = nextSpanOffsetTop - spanOffsetTop;
                    if(height !== 0) {
                      highlightSpan.style.setProperty('height', height + 'px');
                    }
                  }else if(lineNumSpan.item(index) && index === lineNumSpan.length - 1) {
                    // 最後の行の場合は次の行との差分を算出できないので、ダミーを挿入して高さを取得
                    const spanOffsetTop =  lineNumSpan.item(index).offsetTop;
                    const dummy = document.createElement('span');
                    dummy.innerHTML = "<br>";
                    code.appendChild(dummy);
                    const dummy2 = document.createElement('span');
                    code.appendChild(dummy2);
                    const dummy2OffsetTop = dummy2.offsetTop;
                    const height = dummy2OffsetTop - spanOffsetTop;
                    dummy.remove();
                    dummy2.remove();
                    if(height !== 0) {
                      highlightSpan.style.setProperty('height', height + 'px');
                    }
                  }
                }
              }
            });
          }
        });
        const langSpan = wrapper.querySelector('.lng-span');
        if (langSpan) {
          toolbar.insertBefore(langSpan, toolbar.firstElementChild)
        }
        const copyBtn = wrapper.querySelector('.hljs-copy-btn');
        if (copyBtn) {
          toolbar.appendChild(copyBtn)
        }
      }
    }
  }
}

// 開閉パネル(アコーディオンパネル)を要素に追加する関数
function myAddAccordionPanel(targetClassName) {
  // details 要素に付与するクラス
  const detailsClass = 'toggle-code-animation';
  // details 要素内のコンテンツを格納する div 要素に付与するクラス
  const detailsContentClass = 'details-content';
  // details 要素内のコンテンツを格納する要素のラッパーに付与するクラス
  const detailsContentWrapperClass = 'details-content-wrapper';
  // パネルを追加する要素を取得
  const targetElems = document.getElementsByClassName(targetClassName);
  for (const elem of targetElems) {
    // アコーディオンパネルを開くボタンのテキスト
    let summaryOpenText = "Open";
    // アコーディオンパネルを閉じるボタンのテキスト
    let summaryCloseText = "Close";
    if (elem) {
      if(elem.hasAttribute('data-open-text')) {
        summaryOpenText = elem.getAttribute('data-open-text');
      }
      if(elem.hasAttribute('data-close-text')) {
        summaryCloseText = elem.getAttribute('data-close-text');
      }
      // details 要素を作成
      const detailsElem = document.createElement('details');
      detailsElem.classList.add(detailsClass);
      // 作成した details 要素の HTML(summary 要素と div 要素)を設定
      detailsElem.innerHTML = `<summary data-close-text="${summaryCloseText}">${summaryOpenText}</summary>
  <div class="${detailsContentWrapperClass}">
    <div class="${detailsContentClass}"></div>
  </div>`;
      // コードブロックのラッパー要素を details 要素でラップする
      elem.insertAdjacentElement('beforebegin', detailsElem);
      detailsElem.querySelector('.' + detailsContentClass).appendChild(elem);
    }
  }
}

// アコーディオンアニメーションの関数の定義(elem は特定の要素のみに適用する場合に指定)
function mySetupToggleDetailsAnimation(elem) {
  // ボタンのラベル(summary 要素のテキストが空の場合)
  const accodionOpenBtnDefaultLabel = 'Open';
  // 閉じるボタンのラベル(summary 要素に data-close-text 属性が指定されていない場合)
  const accodionCloseBtnDefaultLabel = 'Close';
  // toggle-code-animation クラスの details 要素を全て取得
  const details = document.getElementsByClassName('toggle-code-animation');
  // 引数 elem が指定されていればその要素のみを対象に setupAccordion() を呼び出す(WordPress のプレビューモード用)
  if(elem) {
    setupAccordion(elem);
  }else{
    for(const elem of details) {
      setupAccordion(elem);
    }
  }
  // アコーディオンアニメーションを設定
  function setupAccordion(elem) {
    const summary = elem.querySelector('summary');
    const content = elem.querySelector('.details-content');
    const summaryText = summary.textContent.trim() ? summary.textContent : accodionOpenBtnDefaultLabel;
    if (!summary.textContent.trim()) summary.textContent = summaryText;
    const summaryCloseText = summary.dataset.closeText ? summary.dataset.closeText : accodionCloseBtnDefaultLabel;
    let isAnimating = false;
    summary.addEventListener('click', (e) => {
      e.preventDefault();
      if (isAnimating === true) {
        return;
      }
      if (elem.open) {
        summary.textContent = summaryText;
        isAnimating = true;
        const closeDetails = content.animate(
          {
            opacity: [1, 0],
            height: [content.offsetHeight + 'px', 0],
          },
          {
            duration: 300,
            easing: 'ease-in',
          }
        );
        const rotateIcon = summary.animate(
          { rotate: ["90deg", "0deg"] },
          {
            duration: 300,
            pseudoElement: "::before",
            easing: 'ease-in',
            fill: 'forwards',
          }
        );
        closeDetails.onfinish = () => {
          elem.removeAttribute('open');
          isAnimating = false;
        }
      } else {
        elem.setAttribute('open', 'true');
        summary.textContent = summaryCloseText;
        isAnimating = true;
        const openDetails = content.animate(
          {
            opacity: [0, 1],
            height: [0, content.offsetHeight + 'px'],
          },
          {
            duration: 300,
            easing: 'ease-in',
          }
        );
        const rotateIcon = summary.animate(
          { rotate: ["0deg", "90deg"] },
          {
            duration: 300,
            pseudoElement: "::before",
            easing: 'ease-in',
            fill: 'forwards',
          }
        );
        openDetails.onfinish = () => {
          isAnimating = false;
        }
      }
    });
  }
};

CSS

以下がサンプルの CSS です。

使用している環境(基本的な設定や別途読み込んでいる CSS など)により、調整が必要になると思います。

/* pre 要素のラッパー */
.hljs-wrap {
  position: relative;
}

/* pre 要素 */
.hljs-wrap pre {
  overflow-wrap: break-word;
  overflow-x: hidden;
  padding: 0;
  /* 必要に応じてフォントサイズなどを設定 */
}

/* pre-wrap クラスを指定すると行を自動で折り返す */
.hljs-wrap pre.pre-wrap {
  white-space: pre-wrap;
}

/* pre クラスを指定すると行を自動で折返さない */
.hljs-wrap pre.pre {
  white-space: pre;
}

/* code 要素 */
.hljs-wrap pre code {
  padding-left: 3rem;
  position: relative;
 /* 自動折り返しの設定は親要素(pre)の値を継承 */
  white-space: inherit;
  /* コード内に垂直方向のスクロールバーが表示されるのを防止(必要に応じて) */
  overflow-y: hidden;
}

/* ツールバーを使わない場合の code 要素 */
.hljs-wrap.no-toolbar pre code,
body.no-toolbar .hljs-wrap pre code {
  padding-bottom: 1.5rem;
  padding-top: 2.5rem;
}

.hljs-wrap.no-toolbar pre code.show-no-lang,
body.no-toolbar .hljs-wrap pre code.show-no-lang {
  padding-top: 1rem;
}

.hljs-wrap.no-toolbar pre.no-copy-btn code,
body.no-toolbar .hljs-wrap pre.no-copy-btn code {
  padding-bottom: 1rem;
}

/* ツールバー */
.hljs-wrap .highlight-toobar {
  /* ツールバーの高さ(変更した場合は custom.js の toolbarHeight の値も変更)*/
  height: 2rem;
  background-color: #3a3e4a;
  padding-right: 5px;
  color: #999;
  font-size: 12px;
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: center;
}

.hljs-wrap.no-lang .highlight-toobar {
  justify-content: flex-end;
}

.hljs-wrap .highlight-toobar+pre {
  margin-top: 0;
}

.hljs-wrap .highlight-toobar label {
  color: #888;
  cursor: pointer;
  margin: 0 10px 0 0;
  transition: color .3s;
}

.hljs-wrap .highlight-toobar input[type="checkbox"] {
  background-color: #262b37;
  transition: background-color .3s;
  display: none;
}

@media screen and (min-width : 640px) {
  .hljs-wrap .highlight-toobar label {
    margin: 0 10px 0 3px;
  }

  .hljs-wrap .highlight-toobar input[type="checkbox"] {
    display: block;
  }
}

.hljs-wrap .highlight-toobar input[type="checkbox"]:hover {
  background-color: #0d37a9;
}

/* チェックボックスのスタイルのリセット */
.hljs-wrap .highlight-toobar input[type="checkbox"] {
  border-radius: 0;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}

/* チェックボックスのスタイル */
.hljs-wrap .highlight-toobar input[type="checkbox"] {
  position: relative;
  width: 16px;
  height: 16px;
  border: 1px solid #333;
  cursor: pointer;
}

/* チェックマークのスタイル */
.hljs-wrap .highlight-toobar input[type="checkbox"]:checked:before {
  content: '';
  position: absolute;
  top: 0px;
  left: 4px;
  transform: rotate(45deg);
  width: 4px;
  height: 8px;
  border-right: 2px solid #bbb;
  border-bottom: 3px solid #bbb;
}

.hljs-wrap .highlight-toobar input[type="checkbox"]:checked+label {
  color: #b9bfd0;
}

/* 言語名を表示 */
.hljs-wrap code[data-language]::before {
  content: attr(data-language);
  position: absolute;
  top: 0;
  left: 0;
  color: #ccc;
  display: inline-block;
  padding: 0.5rem 1rem;
  /* コードの背景色と同じにする場合は background-color: #282c34; */
  background-color: #40547d;
  z-index: 5;
}

.hljs-wrap code[data-language].hide-line-num::before {
	left: 2.5rem;
}

/* ツールバーの中の言語名の表示 */
.hljs-wrap .highlight-toobar .lng-span {
  margin-right: auto;
  margin-left: 10px;
  font-size: 13px;
  color: #ccc;
}

/* コピーボタン(ツールバーを使用しない場合) */
.hljs-wrap .hljs-copy-btn {
  position: absolute;
  bottom: 0;
  right: 0;
  background-color: #262b37;
  border: none;
  padding: 2px 4px;
  color: #999;
  cursor: pointer;
  transition: color .3s, background-color .3s;
}

.hljs-wrap .hljs-copy-btn:hover {
  color: #eee;
  background-color: #162858;
}

/* ツールバーの中のコピーボタン */
.hljs-wrap .highlight-toobar .hljs-copy-btn {
  position: relative;
  margin: 0 10px;
}

/* ラベルとリンク(data-label 属性と data-label-url 属性で指定した文字列) */
.hljs-wrap .hljs-label,
.hljs-wrap .hljs-label-url {
  position: absolute;
  top: -2rem;
  right: 10px;
  color: #999;
  display: inline-block;
  padding: 0.5rem 0;
}

.hljs-wrap .hljs-label-url {
  color: #3987C7;
  text-decoration: none;
}

.hljs-wrap .hljs-label-url:hover {
  color: #55924f;
}

.hljs-wrap.has-label {
  margin-top: 4rem;
}

/* 行番号(CSS カウンター)*/
.hljs-wrap pre {
  counter-reset: lineNumber;
}

.hljs-wrap pre span.line-num::before {
  counter-increment: lineNumber;
  content: counter(lineNumber);
  min-width: 2.5rem;
  /* 以下では inline-block ですが、JavaScript で表示・非表示を切り替える際は inline にしないとずれてしまいます */
  display: inline-block;
  color: #777;
  text-align: center;
  position: absolute;
  left: 0;
  background: #282c34;
}

/* 行番号を非表示にする場合 */
.hljs-wrap pre code.hide-line-num span.line-num::before {
  left: -2.5rem;
}

/* 行番号を非表示にする場合 */
.hljs-wrap pre code.hide-line-num {
  margin-left: -2.5rem;
}

/* 行番号右側の枠線 */
.hljs-wrap .hljs-border-span {
  background-color: transparent;
  width: 2.5rem;
  border-right: 1px solid #595a60;
  top: 0;
  left: 0;
}

/* 行番号非表示の場合(枠線なし) */
.hljs-wrap pre code.hide-line-num .hljs-border-span {
  border: none;
}

/* 行のハイライト時の行番号部分 */
.hljs-wrap pre span.line-num.line-num-highlight::before {
  color: #c2c21a;
  /* background: #424638; */
}

/* 行のハイライト時のコード部分 */
.hljs-wrap .line-highlight {
  position:absolute;
  /* 行番号の幅と合わせる */
  left: 2.5rem;
  width: calc(100% - 2.5rem);
  margin-left: -2.5rem;
  width: 100%;
  background: linear-gradient(to right, hsla(254, 15%, 51%, 0.2) 50%, hsla(254, 15%, 51%, 0.01));
  pointer-events: none;
}

/* コードの表示・非表示( details 要素と summary 要素によるアコーディオン)*/
details.toggle-code-animation {
  border: none;
  margin: 2rem 0;
}

details.toggle-code-animation .details-content-wrapper {
  padding: 1rem 0;
}

details.toggle-code-animation .details-content {
  overflow: hidden;
}

details.toggle-code-animation summary {
  display: inline-block;
  cursor: pointer;
  position: relative;
  padding: 0.5rem 0.5rem 0.5rem 36px;
  border: 1px solid #aaa;
  font-size: 13px;
}

details.toggle-code-animation summary::-webkit-details-marker {
  display: none;
}

details.toggle-code-animation summary::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 10px;
  margin: auto 0;
  width: 8px;
  height: 8px;
  border-top: 3px solid #097b27;
  border-right: 3px solid #097b27;
  transform: rotate(45deg);
}

/* テーマ(atom-one-dark)のコメントの色を上書き */
.hljs-wrap .hljs-comment {
  color: #6b788f;
}

自動折返しの設定(white-space)

pre 要素や code 要素の CSS で、white-space に pre や pre-wrap を指定して行の折り返しの設定ができますが、このサンプルでは pre 要素に設定しています。

code 要素には white-space: inherit で、pre 要素の値を継承するようにしています(29行目)。

調整

例えば、このサイトの場合、古い Bootstrap の CSS を読み込んでいるのでツールバーのチェックボックスのスタイルでは追加で以下を指定しています。

画面幅が狭い場合は、チェックボックスを非表示にしていますが、そもそもチェックボックスではなくボタンの方が良いかも知れません。

.hljs-wrap .highlight-toobar input[type="checkbox"] {
  margin: 0;
  margin-top: 0;
  line-height: normal;
}

.hljs-wrap .highlight-toobar input[type="checkbox"]:focus {
  outline: none;
  outline-offset: 0;
}

.hljs-wrap .highlight-toobar label {
  display: inline-block;
  max-width: 100%;
  margin-bottom: 0;
  margin: 0 3px;
  font-weight: normal;
}

テーマ

atom-one-dark 以外のテーマを使用する場合は、必要に応じて以下のスタイル(色関連)を調整します。

  • .highlight-toobar
  • .highlight-toobar label
  • .highlight-toobar input[type="checkbox"]:checked+label
  • .hljs-wrap .highlight-toobar .lng-span
  • .hljs-wrap .hljs-copy-btn
  • .hljs-wrap .hljs-copy-btn:hover
  • .hljs-wrap pre span.line-num::before
  • .hljs-wrap .hljs-border-span
  • .hljs-wrap .line-highlight
  • .hljs-wrap pre span.line-num.line-num-highlight::before
  • .hljs-wrap .hljs-expand-btn
  • .hljs-wrap .hljs-expand-btn:hover
  • .hljs-wrap .hljs-reset-position-btn
  • .hljs-wrap .highlight-toobar.hljs-reset-position-btn:hover

オプション

基本的な動作は JavaScript で指定しますが、コードごとにカスタムデータ属性やクラスを使ってオプションを指定することができます。

指定できるカスタムデータ属性

以下は pre 要素に指定できるカスタムデータ属性です。

pre 要素に指定できるカスタムデータ属性(数値は半角数字のこと)
カスタムデータ属性 説明
data-label 文字列 ラベル(ファイル名などの任意の文字列)を右上に表示。data-label-url に URL を指定するとリンクとして表示。
data-label-url 文字列 data-label のテキストのリンク(href)
data-line-highlight 数値 指定した行を CSS で指定した背景色でハイライト表示。カンマ区切りで指定。ハイフンでレンジ指定も可能(負の値のレンジ指定は不可)
data-line-num-start 数値 開始行番号
data-max-lines 数値 表示する行数を指定(data-show-until と併用はできません)
data-max-lines-offset 数値 data-max-lines で指定した表示部分のオフセットを指定(単位はピクセル)
data-scroll-to 数値 data-max-lines を指定された際に、指定された行までスクロール

以下は code 要素に指定できるカスタムデータ属性です。

code 要素に指定できるカスタムデータ属性
カスタムデータ属性 説明
data-set-lang 文字列 code 要素に指定する言語クラス「language-言語名」とは異なる言語名を表示。Hightlight.js の動作(表示)としては自動検出または「language-言語名」の言語名で表示するが、言語部分のラベルを別の言語名や任意の文字列にしたい場合などに使用。

以下はラッパー要素(div.hljs-wrap)に指定できるカスタムデータ属性です。

ラッパー要素に指定できるカスタムデータ属性
カスタムデータ属性 説明
data-open-text 文字列 開閉パネルのボタンに表示する文字列(デフォルト は Open)。custom.js の変数 summaryOpenText でデフォルトのテキストを変更可能。
data-close-text 文字列 開閉パネルのボタンに表示する文字列(デフォルト は Close)。custom.js の変数 summaryCloseText でデフォルトのテキストを変更可能。

指定できるクラス属性

pre 要素に指定できるクラス属性
クラス 説明
no-copy-btn コピーボタンを表示しない
show-copy-btn コピーボタンを表示。※JavaScript で noCopyBtnOnInit を true にした場合のみ有効。
copy-no-prompt コピーする際に、プロンプト($ または % と続く半角スペース)をコピーに含めない
copy-no-sl-comments コピーする際に、// から始まるコメント部分をコピーに含めない(※)
copy-no-ml-comments コピーする際に、/* */ コメント部分をコピーに含めない(※)
copy-no-comments コピーする際に、///* */ コメント部分をコピーに含めない(※)
copy-no-html-comments コピーする際に、HTML コメント <!-- --> 部分をコピーに含めない(※)
no-highlight-code data-line-highlight を指定して行をハイライトする際にコード部分はハイライトしない(行番号のみハイライト)
no-line-num 行番号を表示しない
pre-wrap 行を自動で折り返す
pre 行を自動で折り返さない
reset-btn data-max-lines を指定している場合に、行の表示位置をリセットするボタンを表示する
target-blank data-label-url で指定したリンクに target="_blank" rel="noopener" を追加

※ copy-no-xxxx-comments クラスは、コメントの記述されている位置やその前後のコードの内容により、正しく動作しない可能性があります。

code 要素に指定できるクラス属性
クラス 説明
language-言語名 言語名を明示的に指定(Hightlight.js の仕様)
show-no-lang 言語名を表示しない(デフォルトは表示する)
ラッパー要素(デフォルトは div.hljs-wrap)に指定できるクラス属性
クラス 説明
no-toolbar そのコードでツールバーを使わない(表示しない)
toggle-accordion 開閉ボタンを表示して、クリックするとアコーディオンアニメーションで表示
body 要素に指定できるクラス属性
クラス 説明
no-toolbar そのページの全てのコードでツールバーを使わない(表示しない)
no-line-num そのページの全てのコードで行番号を初期状態で非表示

使い方

このカスタマイズサンプルを試すには、以下のように Highlight.js を CDN で読み込むのが簡単です。

そして、サンプルの CSS と JavaScript をコピーして style タグと script タグに貼り付けます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Hilight.js カスタマイズ Sample</title>
  <!-- Hilight.js テーマ CSS の読み込み -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css" integrity="sha512-Jk4AqjWsdSzSWCSuQTfYRIF84Rq/eV0G2+tu07byYwHcbTGfdmLrHjUSwvzp5HvbiqK4ibmNwdcG49Y5RGYPTg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
  <!-- カスタマイズ用 CSS の読み込み -->
  <style>
    /* サンプルの CSS(custom.css)を貼り付け */
  </style>
</head>
<body>

  <div class="hljs-wrap">
    <pre><code>コードを記述</code></pre>
  </div>

  <!-- Hilight.js JavaScript の読み込み -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <!-- highlight.js の初期化とカスタマイズ用の JavaScript の読み込み -->
  <script>
    // サンプルの JavaScript(custom.js)を貼り付け
  </script>
</body>
</html>

サンプル

基本的な使い方は、hljs-wrap クラスを指定した div 要素で pre 要素と code 要素を囲みます。

ツールバーの左側には code 要素に指定した language-xxxx の言語名(xxxx)、または自動検出された言語名が表示されます。※手動でクラス名を指定する場合、その指定した文字が表示されるので、例えば、language-JavaScript と指定すれば、言語名は JavaScript と表示されます。

pre 要素に data-label 属性を指定すると、その文字列が右上に表示されます。

行番号と行の折り返しのチェックボックス、コピーボタンはデフォルトで表示されます。

コピーボタンは pre 要素に no-copy-btn クラスを指定すれば非表示になります。また、JavaScript で noCopyBtnOnInit を true に変更すると、デフォルトではコピーボタンは表示せず、pre 要素に show-copy-btn クラスを指定した場合にのみ表示されます。

<div class="hljs-wrap"> <!-- hljs-wrap クラスを指定した div 要素でラップ -->
  <pre data-label="foo.html"><code class="language-HTML">コードを記述</code></pre>
</div>

言語名

デフォルトでは、自動検出または code 要素に指定する言語クラス「language-言語名」の言語名がツールバーの左上に表示されます。

例えば、以下のように記述すると、

<div class="hljs-wrap">
  <pre><code>const timer = setTimeout(console.log(&quot;hello!&quot;), 500);</code></pre>
</div>

この場合、code 要素に「language-言語名」クラスを指定していないので、以下のように自動検出された言語(javascript)が表示されます。

const timer = setTimeout(console.log("hello!"), 500);

以下のように code 要素に「language-JavaScript」クラスを指定すると、

<div class="hljs-wrap">
  <pre><code class="language-JavaScript">const timer = setTimeout(console.log(&quot;hello!&quot;), 500);</code></pre>
</div>

Highlight.js が言語名の大文字・小文字を区別しないので、「language-言語名」クラスに指定した言語名部分の JavaScript と表示されます。

const timer = setTimeout(console.log("hello!"), 500);

言語名を表示しない

言語名を表示しない場合は code 要素に show-no-lang クラスを指定します。

<div class="hljs-wrap">
  <pre><code class="language-JavaScript show-no-lang">const timer = setTimeout(console.log(&quot;hello!&quot;), 500);</code></pre>
</div>

上記の場合、以下のように言語名は表示されません。

const timer = setTimeout(console.log("hello!"), 500);

実際の言語とは異なる言語名を表示

以下は code 要素に language-HTML クラスを指定しているので、HTML と言語名が表示されています。

<!-- <p>Hello!</p> -->

以下は code 要素に language-plaintext クラスを指定してプレインテキストと表示させていますが、data-set-lang="HTML" を指定して、言語名を HTML と表示させる例です。

<div class="hljs-wrap">
  <pre><code data-set-lang="HTML" class="language-plaintext">&lt;!-- &lt;p&gt;Hello!&lt;/p&gt; --&gt;</code></pre>
</div>

language-plaintext クラスを指定してプレインテキストとして表示しているので、HTML と解析されてコメントとして表示されるよりも少し明るく表示されています。

<!-- <p>Hello!</p> -->

また、data-set-lang 属性を使えば、言語名以外の任意の文字列(例えば、ファイル名)を言語名を表示する位置に表示できます。

ラベル

pre 要素に data-label 属性を指定して、ラベル(ファイル名などの任意の文字列)を右上に表示することができます。data-label-url に URL を指定するとリンクとして表示します。

また、その際、pre 要素に target-blank クラスを指定するとリンクの a 要素に target="_blank" rel="noopener" を追加します。

<div class="hljs-wrap">
  <pre data-label="example.com" data-label-url="https://example.com" class="target-blank"><code class="language-HTML">&lt;a href=&quot;https://example.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;example.com&lt;/a&gt;</code></pre>
</div>

上記は以下のように表示されます。

<a href="https://example.com" target="_blank" rel="noopener">example.com</a>

コピーボタン

デフォルトではツールバーにコピーボタンが表示されます。

ボタンのテキストは、JavaScript で変更できます。

pre 要素に no-copy-btn クラスを指定すると、コピーボタンを表示しません。

また、pre 要素に copy-no-comments クラスを指定すると、コピーする際にコードのコメント部分をコピーに含めません。但し、必ずしも期待通りに動作するとは限りません。(オプション)。

行番号

デフォルトでは行番号が表示され、ツールバーに表示・非表示のチェックボックスが表示されます。

ツールバーののテキストは、JavaScript で変更できます。

ページ単位で初期状態で行番号を非表示にするには、body 要素に no-line-num クラスを指定します。

デフォルトで(全てのページで)初期状態で行番号を非表示にするには、JavaScript の noLineNumOnInit の値を true に書き換えます。いずれの場合も、ツールバーのチェックボックスで表示することができます。

pre 要素に no-line-num クラスを指定すれば、コードごとに行番号を非表示にできます。この場合、行番号のチェックボックスは表示されません(ユーザーが行番号を表示することはできません)。

開始番号の指定

pre 要素に data-line-num-start 属性を設定し、開始番号を指定すれば、その番号から行を指定します。

必要であれば、負の値も指定できます。

行の折り返し

デフォルトではコード内で自動的に行を折り返ないようになっています。

各コードの pre 要素に pre-wrap クラスを指定すれば、コード内で自動的に行を折り返します。ユーザーはチェックボックスで折り返しの設定を切り替えることができます。

例えば、以下のように pre 要素に pre-wrap クラスを指定すれば

<div class="hljs-wrap">
  <pre class="pre-wrap"><code class="language-JavaScript">...</code></pre>
</div>

以下のように初期状態で行を折り返して表示されます。

if (wrapper) {
  const wrapperHeight = useToolbar && !wrapper.classList.contains('no-toolbar') ? wrapper.offsetHeight - toolbarHeight : wrapper.offsetHeight;
  const borderSpan = document.createElement('span');
  borderSpan.classList.add('hljs-border-span');
  el.appendChild(borderSpan);
  borderSpan.style.setProperty('position', 'absolute');
  borderSpan.style.setProperty('height', wrapperHeight + 'px');
  const myObserver = new ResizeObserver((entries) => {
    if (entries.length > 0 && entries[0]) {
      const entry = entries[0];
      let resizedHeight = entry.contentBoxSize ? entry.contentBoxSize[0].blockSize : entry.contentRect.height;
      resizedHeight = useToolbar && !wrapper.classList.contains('no-toolbar') ? resizedHeight - toolbarHeight : resizedHeight;
      borderSpan.style.setProperty('height', resizedHeight + 'px');
      if (borderSpan.hasAttribute('data-height')) {
        borderSpan.style.setProperty('height', borderSpan.getAttribute('data-height') + 'px');
      }
    }
  });
  myObserver.observe(wrapper);
}

デフォルトで行を折り返す設定にするには、JavaScript(custom.js)の 17行目の preWrapOnInittrue に変更します。

行のハイライト

pre 要素に data-line-highlight 属性を設定し、ハイライトする行の番号を指定します。複数行を指定する場合は、カンマ区切りまたはハイフンでレンジ指定することができます。但し、負の値のレンジ指定はできません。

以下は pre 要素に data-line-highlight="4, 7-9" を指定した場合の例です。

function randomResult(delay: number) {
  return new Promise<number>((resolve,reject) => {
    setTimeout(() => {
      const rand = Math.floor( Math.random() * 10 );
      if(rand % 2 === 0) {
        resolve(rand);
      }else{
        reject(new Error(`失敗`));
      }
    }, delay);
  });
}

pre 要素に no-highlight-code クラスを指定すると行番号のみハイライトします。

表示する行数を指定

pre 要素の data-max-lines 属性に表示する行数を指定すれば、その行の位置までの高さを code 要素に max-height として設定して、非表示部分はスクロールして見れるようにします。

例えば、pre 要素に data-max-lines="20" を指定すると以下のように最初の20行を表示します。

function setMaxHeight(el, wrapper, pre) {
  if (!pre.hasAttribute('data-max-lines')) return;
  if (!wrapper) return;
  const wrapperHeight = useToolbar && !wrapper.classList.contains('no-toolbar') ? wrapper.offsetHeight - toolbarHeight : wrapper.offsetHeight;
  const lineNumSpan = el.getElementsByClassName('line-num');
  if (lineNumSpan.length > 0) {
    const dataMaxLine = parseInt(pre.getAttribute('data-max-lines'));
    if (dataMaxLine && lineNumSpan.length > dataMaxLine) {
      const dataMaxLineOffset = parseInt(pre.getAttribute('data-max-lines-offset'));
      const heightOffset = dataMaxLineOffset ? dataMaxLineOffset : 0;
      const targetOffsetTop = lineNumSpan.item(dataMaxLine).offsetTop;
      const elComputedStyle = window.getComputedStyle(el);
      const elPaddingTop = parseFloat(elComputedStyle.paddingTop);
      const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
      const visibleHeight = targetOffsetTop + heightOffset - elPaddingTop - elPaddingBottom;
      el.style.setProperty('max-height', visibleHeight + 'px');
      el.setAttribute('data-border-span-height', wrapperHeight);
      el.style.setProperty('overflow-y', 'scroll');
      // code 要素の高さとスクロール位置の調整
      if (pre.hasAttribute('data-scroll-to')) {
        let dataScrollTo = parseInt(pre.getAttribute('data-scroll-to'));
        const dataMaxLine = parseInt(pre.getAttribute('data-max-lines'));
        if (pre.hasAttribute('data-line-num-start')) {
          const startNumber = parseInt(pre.getAttribute('data-line-num-start'));
          if(startNumber) {
            dataScrollTo -= startNumber -1;
          }
        }
        if(dataScrollTo + dataMaxLine > lineNumSpan.length + 1) {
          console.log('data-scroll-to or data-max-line is not valid number')
          return;
        }
        if (dataScrollTo && dataMaxLine && lineNumSpan.item(dataScrollTo - 1)) {
          const scrollToOffsetTop = lineNumSpan.item(dataScrollTo - 1).offsetTop;
          const lastLineRow = lineNumSpan.item(dataScrollTo + dataMaxLine - 2).offsetTop;
          const dataMaxLineOffset = parseInt(pre.getAttribute('data-max-lines-offset'));
          const heightOffset = dataMaxLineOffset ? dataMaxLineOffset : 0;
          el.style.setProperty('max-height', (lastLineRow - scrollToOffsetTop + heightOffset + heightAdjustAmount) + 'px');
          scrollAmount = scrollToOffsetTop - scrollAdjustAmount;
          el.scroll(0, scrollAmount);
        }
      }
    }
  }
}

同時に pre 要素に data-scroll-to 属性を指定すれば、指定した行を先頭に表示します。

以下は data-max-lines="20" data-scroll-to="10" と指定して、10行目から20行を表示しています。

function setMaxHeight(el, wrapper, pre) {
  if (!pre.hasAttribute('data-max-lines')) return;
  if (!wrapper) return;
  const wrapperHeight = useToolbar && !wrapper.classList.contains('no-toolbar') ? wrapper.offsetHeight - toolbarHeight : wrapper.offsetHeight;
  const lineNumSpan = el.getElementsByClassName('line-num');
  if (lineNumSpan.length > 0) {
    const dataMaxLine = parseInt(pre.getAttribute('data-max-lines'));
    if (dataMaxLine && lineNumSpan.length > dataMaxLine) {
      const dataMaxLineOffset = parseInt(pre.getAttribute('data-max-lines-offset'));
      const heightOffset = dataMaxLineOffset ? dataMaxLineOffset : 0;
      const targetOffsetTop = lineNumSpan.item(dataMaxLine).offsetTop;
      const elComputedStyle = window.getComputedStyle(el);
      const elPaddingTop = parseFloat(elComputedStyle.paddingTop);
      const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
      const visibleHeight = targetOffsetTop + heightOffset - elPaddingTop - elPaddingBottom;
      el.style.setProperty('max-height', visibleHeight + 'px');
      el.setAttribute('data-border-span-height', wrapperHeight);
      el.style.setProperty('overflow-y', 'scroll');
      // code 要素の高さとスクロール位置の調整
      if (pre.hasAttribute('data-scroll-to')) {
        let dataScrollTo = parseInt(pre.getAttribute('data-scroll-to'));
        const dataMaxLine = parseInt(pre.getAttribute('data-max-lines'));
        if (pre.hasAttribute('data-line-num-start')) {
          const startNumber = parseInt(pre.getAttribute('data-line-num-start'));
          if(startNumber) {
            dataScrollTo -= startNumber -1;
          }
        }
        if(dataScrollTo + dataMaxLine > lineNumSpan.length + 1) {
          console.log('data-scroll-to or data-max-line is not valid number')
          return;
        }
        if (dataScrollTo && dataMaxLine && lineNumSpan.item(dataScrollTo - 1)) {
          const scrollToOffsetTop = lineNumSpan.item(dataScrollTo - 1).offsetTop;
          const lastLineRow = lineNumSpan.item(dataScrollTo + dataMaxLine - 2).offsetTop;
          const dataMaxLineOffset = parseInt(pre.getAttribute('data-max-lines-offset'));
          const heightOffset = dataMaxLineOffset ? dataMaxLineOffset : 0;
          el.style.setProperty('max-height', (lastLineRow - scrollToOffsetTop + heightOffset + heightAdjustAmount) + 'px');
          scrollAmount = scrollToOffsetTop - scrollAdjustAmount;
          el.scroll(0, scrollAmount);
        }
      }
    }
  }
}

使用しているスタイルによっては、行の位置がずれる可能性があります。JavaScript(custom.js)の変数 heightAdjustAmount の値や CSS を環境に合わせて修正する必要があるかも知れません。

ツールバーの表示・非表示

デフォルトではツールバーを表示します。

ツールバーはページ単位またはコード単位で表示・非表示を切り替えられます。

body 要素に no-toolbar クラスを指定すると、そのページの全てのコードでツールバーを表示しません。

また、ラッパー要素(デフォルトは div.hljs-wrap)に no-toolbar クラスを指定すると、そのコードでツールバーを表示しません。

例えば、以下のようにラッパー要素に class="hljs-wrap no-toolbar" を指定するとツールバーを表示しません。この場合、行番号や行折り返しのチェックボックスは表示されません。

<div class="hljs-wrap no-toolbar">
  <pre data-label="foo.html"><code class="language-HTML">...</code></pre>
</div>

以下はツールバー、行番号、言語名、コピーボタンを表示しない例です。

<div class="hljs-wrap no-toolbar">
  <pre class="no-line-num no-copy-btn"><code class="show-no-lang">...</code></pre>
</div>

アコーディオンパネル

ラッパー要素(div.hljs-wrap)に toggle-accordion クラスを指定すると、details 要素と summary 要素のマークアップを自動的に追加し、開閉ボタンを表示してアニメーションでコードを表示します。

<div class="hljs-wrap toggle-accordion">
  <pre><code>...</code></pre>
</div>
<div class="hljs-wrap toggle-accordion">
  <pre><code>...</code></pre>
</div>

開閉ボタンのテキストはデフォルトでは Open と Close ですが、ラッパー要素に data-open-text と data-close-text 属性を指定して任意の文字列を表示できます。

<div class="hljs-wrap toggle-accordion" data-open-text="コードを表示" data-close-text="コードを閉じる" >
  <pre><code>...</code></pre>
</div>
<div class="hljs-wrap toggle-accordion" data-open-text="コードを表示" data-close-text="コードを閉じる" >
  <pre><code>...</code></pre>
</div>

複数のコードを開閉表示する場合は、toggle-accordion クラスを指定した div 要素でそれらのコードを囲みます。

<div class="toggle-accordion" data-open-text="複数表示" data-close-text="複数閉じる">
  <div class="hljs-wrap">
    <pre><code>...</code></pre>
  </div>
  <div class="hljs-wrap">
    <pre><code>...</code></pre>
  </div>
</div>
<div class="hljs-wrap">
    <pre><code>...</code></pre>
  </div>
<div class="hljs-wrap">
    <pre><code>...</code></pre>
  </div>

アニメーションで表示するコンテンツはコードに限定していないので、toggle-accordion クラスを指定した div 要素で囲めばそのコンテンツをアニメーションで表示します。

<div class="toggle-accordion" data-open-text="Hello を表示" data-close-text="Hello を閉じる">
  <p>Hello!</p>
</div>

Hello!

手動でアコーディオンパネルをマークアップ

クラスを指定して開閉パネルを追加した場合、details 要素を JavaScript で追加するので、再読み込みの際など、コンテンツがチラツキます。

それが気になる場合は、手動で以下の details 要素と summary 要素のマークアップを記述してアコーディオンパネルを表示することもできます。

<details class="toggle-code-animation">
  <summary data-close-text="コンテンツを閉じる">コンテンツを開く</summary>
  <div class="details-content-wrapper">
    <div class="details-content">
      ここにコードや表示するコンテンツを記述(※ data-max-lines 属性は指定できない)
    </div>
  </div>
</details>
コンテンツを開く
ここにコードや表示するコンテンツを記述(※ data-max-lines 属性は指定できない)

関連ページ:details と summary 要素 + JavaScript で作るアコーディオン

手動でマークアップする場合の制限事項

手動で details 要素と summary 要素のマークアップを記述した場合、コンテンツ(div.details-content)のコードに、data-max-lines 属性を指定すると、Google Chrome では期待通りに表示されますが FireFox や Safari、iOS(iPhone)ではコンテンツの高さが取得できず、正しく表示されません。

VS Code スニペット

VS Code を使っている場合、定形のテキスト(HTML コードなど)をユーザースニペットとして登録しておくことができ、タブストップやプレースホルダーを使用して簡単にコードを入力できます。

関連ページ:VS Code で Web 制作(ユーザースニペット)

以下はハイライト用のコードの VS Code スニペットのサンプルで、よく使うオプションと全てのオプションの2つのスニペットを登録する例です。

"Hightlight.js Syntax Highlihgt ": {
  "prefix": "hljs",
  "body": [
    "<div class=\"hljs-wrap\">",
    "\t<pre${1: data-label=\"$2\"}><code${3: class=\"language-${4:HTML}\"}>$5</code></pre>",
    "</div>"
  ],
  "description": "Code Block for Hightlight.js"
},
"Hightlight.js Syntax Highlihgt Full Options ": {
  "prefix": "hljs full",
  "body": [
    "<div class=\"hljs-wrap${1: no-toolbar}\">",
    "\t<pre${2: data-label=\"$3\"}${4: data-label-url=\"$5\"}${6: data-line-highlight=\"$7\"}${8: data-line-num-start=\"$9\"}${10:${11: data-max-lines=\"$12\"}${13: data-max-lines-offset=\"${14:0}\"}${15: data-scroll-to=\"$16\"}}${17: class=\"${18:no-copy-btn }${19:no-highlight-code }${20:no-line-num }${21:copy-no-${22:html-}comments }${23:copy-no-prompt}\"}><code${24: class=\"${25:language-${26:HTML}}${27: pre}${28: show-no-lang}\"}${29: data-set-lang=\"$30\"}>$0</code></pre>",
    "</div>"
  ],
  "description": "Code Block for Hightlight.js with full options"
},

以下は details 要素と summary 要素を使ったアコーディオンアニメーションでコードを表示・非表示する際のスニペットの例です。

"Accordion Code Bloc ": {
  "prefix": "accordion code",
  "body": [
    "<details class=\"toggle-code-animation\">",
    "\t<summary data-close-text=\"${1:コードを閉じる}\">${2:コードを見る}</summary>",
    "\t<div class=\"details-content-wrapper\">",
    "\t\t<div class=\"details-content\">",
    "\t\t\t$0",
    "\t\t</div>",
    "\t</div>",
    "</details>"
  ],
  "description": "Code Block for Hightlight.js"
},

WordPress で使う

WordPress で使用する例です。functions.php で CSS と JavaScript を読み込みます。

この例ではテーマフォルダの中に highlight-js というフォルダを作成して以下のファイルを保存します。

highlight-js
├── atom-one-dark.min.css  // Highlight.js のテーマ CSS
├── custom.css  // カスタマイズ用スタイル
├── custom.js  // カスタマイズ用 JavaScript
└── highlight.min.js  // Highlight.js の JavaScript

functions.php に以下を記述して、CSS と JavaScript を読み込みます。

CSS は wp_enqueue_style() を使って、JavaScript は wp_enqueue_script() を使って登録し、wp_enqueue_scripts アクションフックで読み込みます。

関連ページ:WordPress CSS や JavaScript ファイルの読み込み

function add_my_hljs_styles_and_scripts() {
  // Hightlight.js テーマ CSS(atom-one-dark.min.css)の読み込み
  wp_enqueue_style(
    'atom-one-dark',
    get_theme_file_uri( '/highlight-js/atom-one-dark.min.css' ),
    array(),
    filemtime( get_theme_file_path( '/highlight-js/atom-one-dark.min.css' ) )
  );
  // カスタマイズ用スタイルシートの読み込み
  wp_enqueue_style(
    'highlight-js-custom-style',
    get_theme_file_uri( '/highlight-js/custom.css'),
    array('atom-one-dark'),
    filemtime( get_theme_file_path( '/highlight-js/custom.css' ) )
  );

  // highlight.min.js の読み込み
  wp_enqueue_script(
    'highlightJS',
    get_theme_file_uri( '/highlight-js/highlight.min.js' ),
    array(),
    filemtime( get_theme_file_path( '/highlight-js/highlight.min.js' ) ),
    true
  );

  // カスタマイズ用 JavaScript の読み込み
  wp_enqueue_script(
    'highlight-custom-js',
    get_theme_file_uri( '/highlight-js/custom.js' ),
    array( 'highlightJS' ),
    filemtime( get_theme_file_path( '/highlight-js/custom.js' ) ),
    true
  );
}
add_action( 'wp_enqueue_scripts', 'add_my_hljs_styles_and_scripts' );

編集画面でコードを挿入する位置でブロックの追加の + ボタンをクリックして「カスタム HTML」ブロックを追加します。

すでに「カスタム HTML」ブロックを使用したことがある場合は最近使用したブロックに表示されますが、表示されない場合は「すべて表示」をクリックします。

「カスタム HTML」ブロックに例えば以下を記述すると、

<div class="hljs-wrap">
    <pre data-label="foo.js" data-line-highlight="6"><code class="language-JavaScript">hljs.addPlugin({
    &#39;after:highlightElement&#39;: ({ el, result }) =&gt; {
      // result の language プロパティが undefined でなければ
      if(result.language) {
        // language を result から取得して code 要素(el)の data-language 属性に設定
        el.dataset.language = result.language;
      }
    }
  });</code></pre>
  </div>
</div>

code 要素内に記述するコードの HTML 特殊文字はエスケープする必要があります。

HTML 特殊文字を手動でエスケープするのは大変なのでオンラインツールを利用するか、自分でツールを作成すると便利です。

関連ページ:HTML特殊文字変換ツール

以下のように表示されます。

上記はテーマ Twenty Twenty-Four での表示例です。テーマのスタイルによっては追加でスタイルを調整する必要があります。

コードブロックを使う

「カスタム HTML」ブロックを使う場合は、入力するコードをエスケープしなければなりませんが、コードブロックを使えば、エスケープ処理なしでコードをそのまま入力することができます。

オプションはクラス名を使って指定する必要がありますが、カスタムブロックを作成するよりは手軽に実装できます。詳細は以下のページを御覧ください。

カスタムブロックを作成

カスタムブロックを作成すれば、入力したコードをエスケープ処理なしでハイライト表示することができ、また、必要に応じてオプションをインスペクターに設定することもできます。詳細は以下のページを御覧ください。