Highlight.js ボーダー用の span 要素を使わない(処理が重くて使えない) サンプル

この例の場合、各行のコンテンツの高さを取得して設定し、CSS で行番号の高さを 100% にしています。

そのため、自動折り返しをしてもボーダーは途切れませんし、行数を指定して表示した場合でも余分な空白は表示されません。また、行数を指定して表示した場合に、iPhone でもボーダーが表示されます。

※ 但し、非常に処理が重く、読み込みに時間がかかるため使い物になりません。

<!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" />
  <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>
  <script>
    // 以下のサンプルの JavaScript(custom.js)をコピペ
  </script>
</body>
</html>

処理が重い理由は、枠線を実際の行の高さに合わせるために ResizeObserver で全ての行の高さを算出して設定しているためです。

/* 処理が重くて使えない例 updated (changed) on: 2024/03/18  */
document.addEventListener('DOMContentLoaded', () => {
  // Highlight.js のプラグインをセットアップ
  mySetUpHljsPlugins(myCustomHighlightJsSettings);
  // Highlight.js の初期化とカスタマイズの実行
  mySetupHighlightJs(myCustomHighlightJsSettings);
  // アコーディオンアニメーションの開閉パネルの追加(オプショナル)
  myAddAccordionPanel('toggle-accordion');
  // アコーディオンアニメーションの呼び出し(オプショナル)
  mySetupToggleDetailsAnimation();
});

const myCustomHighlightJsSettings = {
  // pre code のラッパーのクラス名
  wrapperClassName: 'hljs-wrap',
  // pre 要素なし(インライン)の code 要素でもハイライトするかどうか
  useInlineHighlight: true,
  // インラインの code 要素でハイライトする場合に code 要素に指定するクラス(空の場合、全ての code 要素)
  inlineHighlightClassName: 'highlight',
}

// Hightlight.js のプラグインを定義
function mySetUpHljsPlugins(settings) {
  const { wrapperClassName, useInlineHighlight } = settings;
  // デフォルトで行の自動折り返しを有効にするかどうか
  const preWrapOnInit = false;
  // 初期状態でコピーボタンを非表示
  const noCopyBtnOnInit = false;
  // 初期状態で行番号を非表示(ツールバー使用時のみ有効)
  const noLineNumOnInit = document.body.classList.contains('no-line-num') ? true  : false;
  // ツールバーを表示(使用)するかどうか(全てのページで使用しない場合は false を指定)
  const useToolbar = document.body.classList.contains('no-toolbar') ? false  : true;
  // 行数を指定して表示する場合の表示領域の高さの調整値
  const visibleHeightAdjustAmount = 0;
  // 行数を指定して表示する際に data-scroll-to を指定する場合の高さの調整値
  const scrollToHeightAdjustAmount = 0;
  // 行数を指定して表示する場合にスクロール量の調整値
  const scrollAdjustAmount = 0;
  // 行数を指定して表示する場合に折り返しや行番号表示の切り替えやウィンドウサイズの変更が発生した際にスクロール位置を元に戻すかどうか
  const resetScrollPosition = true;
  // 行数を指定して表示する場合にコードの下に情報領域(div.scroll-footer)を表示するかどうか
  const showScrollFooter = true;
  // 行数を指定して表示する場合にコードの下の情報領域(div.scroll-footer)に行数を表示するかどうか
  const showLineNumInfo = true;
  // ツールバーやボタンのテキスト(ラベル)
  const lineAutoWrapLabel = 'wrap';
  const lineNumLabel = 'number';
  const copyBtnLabel = 'Copy';
  const copyBtnCompleteLabel = 'Copied';
  const copyBtnFailedLabel = 'Failed';
  const copyFailedMessage = 'Sorry, can not copy with this browser.';
  const scrollableText = 'scrollable';

  // 最後の行の offsetTop と高さを取得する関数(lineNumSpans は行番号の全ての span 要素、el は code 要素)
  function getLastNumLineInfo(lineNumSpans, el) {
    // offsetTop と高さを格納するオブジェクト
    const info = {};
    const lastLineNumTop = lineNumSpans.item(lineNumSpans.length-1).offsetTop;
    // dummy 要素を追加して offsetTop の差分から現在の高さを取得
    const dummy = document.createElement('span');
    dummy.innerHTML = "<br>";
    el.appendChild(dummy);
    const dummy2 = document.createElement('span');
    el.appendChild(dummy2);
    const dummy2OffsetTop = dummy2.offsetTop;
    // offsetTop プロパティを設定
    info.offsetTop = dummy2OffsetTop;
    const lastLineNumHeight = dummy2OffsetTop - lastLineNumTop;
    // height プロパティを設定
    info.height = lastLineNumHeight
    dummy.remove();
    dummy2.remove();
    return info;
  }

  // data-max-lines 属性が指定された場合の表示領域の高さを設定及び更新する関数
  function updateVisibleHeight(pre, el, isSetMaxHeight = false) {
    let maxLineValue = null;
    const lineNumSpans = el.getElementsByClassName('line-num');
    if(pre.hasAttribute('data-max-lines') && lineNumSpans.length > 0) {
      const dataMaxLines = parseInt(pre.getAttribute('data-max-lines'));
      if(dataMaxLines && dataMaxLines >0 && dataMaxLines < lineNumSpans.length){
        maxLineValue = dataMaxLines;
      }
    }
    if(maxLineValue) {
      const elComputedStyle = window.getComputedStyle(el);
      const elPaddingTop = parseFloat(elComputedStyle.paddingTop);
      const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
      const elPaddingY = elPaddingTop + elPaddingBottom;
      if (lineNumSpans.item(maxLineValue)) {
        // data-max-lines-offset の値を取得
        const maxLineValueOffset = pre.getAttribute('data-max-lines-offset') ? parseInt(pre.getAttribute('data-max-lines-offset')): 0;
        // 表示する最後の行の次の行の span 要素の offsetTop
        const maxNextOffsetTop = lineNumSpans.item(maxLineValue).offsetTop;
        const visibleHeight = maxNextOffsetTop + maxLineValueOffset - elPaddingY + visibleHeightAdjustAmount;
        el.style.setProperty('height', visibleHeight + 'px');
        el.style.setProperty('overflow-y', 'scroll');
        if(pre.classList.contains('no-scroll')) {
          el.style.setProperty('overflow-y', 'hidden');
        }
      }
      // data-scroll-to 属性が指定されている場合
      if (pre.hasAttribute('data-scroll-to')) {
        let dataScrollTo = parseInt(pre.getAttribute('data-scroll-to'));
        if (pre.hasAttribute('data-line-num-start')) {
          const startNumber = parseInt(pre.getAttribute('data-line-num-start'));
          if(startNumber) {
            dataScrollTo -= startNumber -1;
          }
        }
        if(dataScrollTo + maxLineValue > lineNumSpans.length + 1) {
          console.log(`data-scroll-to: ${dataScrollTo} or data-max-line : ${maxLineValue} is not valid.`)
          return;
        }
        if (dataScrollTo && lineNumSpans.item(dataScrollTo - 1) ) {
          const scrollToOffsetTop = lineNumSpans.item(dataScrollTo - 1).offsetTop;
          const maxLineValueOffset = pre.getAttribute('data-max-lines-offset') ? parseInt(pre.getAttribute('data-max-lines-offset')): 0;
          let visibleHeight;
          if(lineNumSpans.item(dataScrollTo + maxLineValue - 1)) {
            const lastRowOffsetTop = lineNumSpans.item(dataScrollTo + maxLineValue - 1).offsetTop;
            visibleHeight = lastRowOffsetTop - scrollToOffsetTop + maxLineValueOffset + scrollToHeightAdjustAmount - elPaddingY;
          }else if(dataScrollTo + maxLineValue === lineNumSpans.length + 1 && lineNumSpans.item(dataScrollTo) ) {
            // スクロールにより最終行を表示する場合
            const lastLineOffsetTop = getLastNumLineInfo(lineNumSpans, el).offsetTop;
            visibleHeight = lastLineOffsetTop - scrollToOffsetTop + maxLineValueOffset + scrollToHeightAdjustAmount - elPaddingTop;
          }
          el.style.setProperty('height', visibleHeight + 'px');
          if(isSetMaxHeight) {
            // 行数を指定して高さを設定する関数 setMaxHeight() での呼び出しの場合(一度スクロールを実行して終了)
            const scrollAmount = scrollToOffsetTop - scrollAdjustAmount;
            el.scroll(0, scrollAmount);
            return;
          }
          if(resetScrollPosition) {
            const scrollAmount = scrollToOffsetTop - scrollAdjustAmount;
            el.scroll(0, scrollAmount);
          }
        }
      }
    }
  }

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

  // コードをコピーする関数 (addPlugin で呼び出す)
  function copyCode(text, pre, wrapper) {
    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);
    wrapper.classList.add('has-copy-btn');
    copyButton.addEventListener('click', () => {
      copyToClipboard(copyButton, text)
    });
    function copyToClipboard(btn, text) {
      if (!navigator.clipboard) {
        alert(copyFailedMessage);
      }
      // data-max-lines 属性と no-scroll クラスが指定されている場合は、表示されている部分のみをコピー
      if(preClass.contains('no-scroll') && pre.hasAttribute('data-max-lines')) {
        let startLine =  1;
        let endLine = parseInt(pre.getAttribute('data-max-lines'));
        if(pre.hasAttribute('data-scroll-to')) {
          const scrollTo = parseInt(pre.getAttribute('data-scroll-to'));
          if(scrollTo) {
            startLine = scrollTo;
            endLine += scrollTo -1;
          }
        }
        if (pre.hasAttribute('data-line-num-start')) {
          const startNumber = parseInt(pre.getAttribute('data-line-num-start'));
          if(startNumber) {
            startLine -= startNumber -1;
            endLine -= startNumber -1;
          }
        }
        // text を改行で分割
        const textArray = text.split(/\r?\n/);
        let visibleText = '';
        if(startLine >=1 && endLine < textArray.length){
          for(let i=startLine-1; i<=endLine-1; i++) {
            if(i !== endLine-1) {
              visibleText += textArray[i] + "\n";
            }else{
              visibleText += textArray[i];
            }
          }
        }
        text = visibleText;
      }
      // プロンプト文字($ と %)を除外してコピー
      if (preClass.contains('copy-no-prompt')) {
        text = text.replace(/^\$\s|^%\s/gm, '');
      }
      // シングルラインコメントを除外してコピー
      if (preClass.contains('copy-no-sl-comments') || preClass.contains('copy-no-comments')) {
        // 行の途中の「半角スペース + //」も削除。 [^\S\r\n] は改行を除く空白にマッチ(コメント以外も削除する可能性あり)
        text = text.replace(/^([^\S\r\n]*\/\/).*$\r?\n?/gm, "").replace(/(.*)\s\/\/.*/g, "$1");
      }
      // マルチラインコメントを除外してコピー
      if (preClass.contains('copy-no-ml-comments') || preClass.contains('copy-no-comments')) {
        // replace() の第2引数に関数 replaceComments を指定(正しくマッチしない可能性あり)
        text = text.replace(/^(.*)\/\*[\s\S]*?\*\/($\r?\n?)?/gm, replaceComments)
      }
      // HTML コメントを除外してコピー
      if (preClass.contains('copy-no-html-comments')) {
        // replace() の第2引数に関数 replaceComments を指定
        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) {
    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) {
      // ResizeObserver で code 要素のサイズを監視
      const codeObserver = new ResizeObserver((entries) => {
        if (entries.length > 0 && entries[0]) {
          const entry = entries[0];
          // 行番号用の span 要素を全て取得
          const lineNumSpans = el.getElementsByClassName("line-num");
          // 行番号用の span 要素とその兄弟要素に高さを設定(枠線の高さとハイライト表示の高さに必要)
          for (let i = 0; i < lineNumSpans.length; i++) {
            const nextSiblingSpan = lineNumSpans[i].nextElementSibling;
            // 行番号用の span 要素に兄弟要素が存在する場合
            if(nextSiblingSpan) {
              if(lineNumSpans[i] && lineNumSpans[i+1]) {
                const spanOffsetTop =  lineNumSpans[i].offsetTop;
                const nextSpanOffsetTop =  lineNumSpans[i+1].offsetTop;
                const height = nextSpanOffsetTop - spanOffsetTop;
                if(height !== 0) {
                  lineNumSpans[i].style.setProperty('height', height + 'px');
                  nextSiblingSpan.style.setProperty('height', height + 'px');
                }
              }else if(lineNumSpans[i] && i === lineNumSpans.length - 1) {
                // 最後の行の場合(次の行がないのでダミーを挿入して取得)
                const lastLineNumHeight = getLastNumLineInfo(lineNumSpans, el).height;
                if(lastLineNumHeight !== 0) {
                  lineNumSpans[i].style.setProperty('height', lastLineNumHeight + 'px');
                  nextSiblingSpan.style.setProperty('height', lastLineNumHeight + 'px');
                }
              }
            }else{
              // 行番号用の span 要素に兄弟要素が存在しない場合
              if(lineNumSpans[i] && lineNumSpans[i-1] && lineNumSpans[i+1]) {
                const prevSpanOffsetTop =  lineNumSpans[i-1].offsetTop;
                const prevSpanOffsetHeight = lineNumSpans[i-1].offsetHeight;
                const nextSpanOffsetTop =  lineNumSpans[i+1].offsetTop;
                const height = nextSpanOffsetTop - prevSpanOffsetHeight - prevSpanOffsetTop;
                if(height !== 0) {
                  lineNumSpans[i].style.setProperty('height', height + 'px');
                }
              }
            }
          }
          // 表示領域の高さを更新
          updateVisibleHeight(pre, el)
        }
      });
      codeObserver.observe(el);
    }
  }

  //指定された行をハイライト表示する関数
  function highlightNumbers(el, pre) {
     // pre 要素に data-line-highlight 属性が指定されていれば
    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 lineNumSpans = el.getElementsByClassName('line-num');
        const lineLength = lineNumSpans.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';
                lineNumSpans.item(number - 1).after(highlightSpan);
              }
              if (highlightNumber) {
                lineNumSpans.item(number - 1).classList.add('line-num-highlight');
              }
            }
          }
        })
      }
    }
  }

  // 表示する行数を指定して表示領域(code 要素に height)を設定する関数
  function setMaxHeight(el, wrapper, pre) {
    if (!pre.hasAttribute('data-max-lines')) return;
    if (!wrapper) return;
    // 表示領域(code 要素に height)を設定
    updateVisibleHeight( pre, el, true);
    if(pre.classList.contains('no-scroll')) return;
    // コードの下に情報領域(div.scroll-footer)を表示
    if(showScrollFooter && !pre.classList.contains('no-scroll-footer') || !showScrollFooter && pre.classList.contains('show-scroll-footer')) {
      const footerText = pre.hasAttribute('data-footer-text') ? pre.getAttribute('data-footer-text') : scrollableText;
      wrapper.insertAdjacentHTML('beforeend', `<div class="scroll-footer"><span class="scroll-footer-text">${footerText}</span></div>`);
      const scrollFooter = wrapper.querySelector('.scroll-footer');
      const maxLineValue = pre.getAttribute('data-max-lines');
      const lineNumSpans = el.getElementsByClassName('line-num');
      if(showLineNumInfo && !pre.classList.contains('no-line-info') || !showLineNumInfo && pre.classList.contains('show-line-info')) {
        scrollFooter.insertAdjacentHTML('afterbegin', `<span class="line-count">Showing ${maxLineValue} of ${lineNumSpans.length} lines</span>`);
      }
    }
  }

  // ラッパー要素にラベルやツールバーを表示する関数
  let index = 0;
  function setUpWrapper(el, wrapper, pre) {
    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')) {
      el.classList.add('hide-line-num');
    }
    // ツールバーの追加
    if (useToolbar) {
      if (!wrapperClass.contains('no-toolbar')) {
        const toolbar = document.createElement('div');
        toolbar.setAttribute('class', 'highlight-toolbar');
        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"]');
        if (lineNumCheck) {
          lineNumCheck.addEventListener('change', (e) => {
            if (e.currentTarget.checked) {
              el.classList.remove('hide-line-num');
            } else {
              el.classList.add('hide-line-num');
            }
          });
          if (noLineNumOnInit) {
            el.classList.add('hide-line-num');
          }
        }
        const lineAutoWrapCheck = toolbar.querySelector('[name="line-auto-wrap"]');
        lineAutoWrapCheck.addEventListener('change', (e) => {
          // white-space プロパティを変更
          if (e.currentTarget.checked) {
            pre.style.setProperty('white-space', 'pre-wrap');
          } else {
            pre.style.setProperty('white-space', 'pre');
          }
          // 表示領域の高さを更新
          updateVisibleHeight( pre, el)
        });
        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)
        }
      }
    }
    index ++;
  }
}

// Highlight.js の初期化とカスタマイズを適用する関数。
// targetWrapper は単一の要素のみに適用する場合に指定(WordPress のエディタでのプレビュー用に使用する場合に指定)
function mySetupHighlightJs(settings, targetWrapper = false) {
  const { wrapperClassName, useInlineHighlight, inlineHighlightClassName } = 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'));
    }
  }else if (targetWrapper) {
    const code = targetWrapper.querySelector('code');
    if(code) {
      hljs.highlightElement(code);
    }
  }
}

// 開閉パネル(アコーディオンパネル)を指定されたクラスを持つ要素(または第2引数で渡された単一の要素)に追加する関数
function myAddAccordionPanel(targetClassName, elem = null) {
  // details 要素に付与するクラス
  const detailsClass = 'toggle-code-animation';
  // details 要素内のコンテンツを格納する div 要素に付与するクラス
  const detailsContentClass = 'details-content';
  // details 要素内のコンテンツを格納する要素のラッパーに付与するクラス
  const detailsContentWrapperClass = 'details-content-wrapper';
  // 第2引数の elem が指定されていなければ、第1引数のクラス名を使って要素を取得してパネルを追加
  if(!elem) {
    // パネルを追加する要素を取得
    const targetElems = document.getElementsByClassName(targetClassName);
    for (const elem of targetElems) {
      addPanel(elem);
    }
  }else{
    // 第2引数の elem が指定されていれば、その要素にパネルを追加
    addPanel(elem);
  }
  // アコーディオンパネルを受け取った要素に追加する関数
  function addPanel(elem) {
    // アコーディオンパネルを開くボタンのテキスト
    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;
        }
      }
    });
  }
};
/* updated (changed) on: 2024/03/18  */
.hljs-wrap {
  position: relative;
}

.hljs-wrap pre {
  overflow-wrap: break-word;
  overflow-x: hidden;
  padding: 0;
  margin: 0;
}

.hljs-wrap pre.pre-wrap {
  white-space: pre-wrap;
}

.hljs-wrap pre.pre {
  white-space: pre;
}

.hljs-wrap pre code {
  padding-left: 3rem;
  position: relative;
  white-space: inherit;
  overflow-y: hidden;
}

.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.has-copy-btn pre code.show-no-lang,
body.no-toolbar.has-copy-btn .hljs-wrap pre code.show-no-lang {
  padding-top: 2rem;
}

.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-toolbar {
  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-toolbar {
  justify-content: flex-end;
}

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

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

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

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

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

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

.hljs-wrap .highlight-toolbar input[type="checkbox"] {
  border-radius: 0;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}

.hljs-wrap .highlight-toolbar input[type="checkbox"] {
  position: relative;
  width: 16px;
  height: 16px;
  border: 1px solid #333;
  cursor: pointer;
}

.hljs-wrap .highlight-toolbar 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-toolbar 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: #40547d;
  z-index: 5;
}

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

.hljs-wrap .highlight-toolbar .lng-span {
  margin-right: auto;
  margin-left: 10px;
  font-size: 13px;
  color: #ccc;
}

.hljs-wrap .hljs-copy-btn {
  position: absolute;
  top: 0;
  right: 0;
  background-color: #262b37;
  border: none;
  padding: 8px;
  color: #999;
  cursor: pointer;
  transition: color .3s, background-color .3s;
}

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

.hljs-wrap .highlight-toolbar .hljs-copy-btn {
  position: relative;
  margin: 0 10px;
  padding: 2px 4px;
}

.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;
}

.hljs-wrap pre {
  counter-reset: lineNumber;
}

/* 以下を追加*/
.hljs-wrap pre span.line-num {
  position:absolute;
  left: 0;
}

.hljs-wrap pre span.line-num::before {
  counter-increment: lineNumber;
  content: counter(lineNumber);
  min-width: 2.5rem;
  display: inline-block;
  color: #777;
  text-align: center;
  position: absolute;
  left: 0;
  background: #282c34;
  border-right: 1px solid #595a60;
  /* 高さを 100% に*/
  height: 100%;
}

.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 pre span.line-num.line-num-highlight::before {
  color: #c2c21a;
}

.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;
}

/* 行数を指定して表示する場合にコード下に表示する領域 */
.hljs-wrap .scroll-footer {
  margin: 0;
  font-size: .75rem;
  color: #999;
  /* コードの背景色と同じ色 */
  background-color: #282c34;
  padding: 8px;
  display: flex;
  justify-content: flex-end;
}

/* .scroll-footer の右端に表示するテキスト*/
.hljs-wrap .scroll-footer-text {
  margin-left: auto;
}

/* .scroll-footer-text の左側に表示するアイコン(色は fill='%23aaaaaa' の aaaaaa 部分) */
.hljs-wrap .scroll-footer-text::before{
  content: "";
  display: inline-block;
  height: .875rem;
  width: .875rem;
  vertical-align: -2px;
  margin-right: 8px;
  background-repeat: no-repeat;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23aaaaaa' viewBox='0 0 16 16'%3E  %3Cpath fill-rule='evenodd' d='M11.5 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L11 2.707V14.5a.5.5 0 0 0 .5.5m-7-14a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L4 13.293V1.5a.5.5 0 0 1 .5-.5'/%3E%3C/svg%3E");
}

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);
}

.hljs-wrap .hljs-comment {
  color: #6b788f;
}

Highlight.js のカスタマイズ サンプル ページへ