Highlight.js のカスタマイズ サンプル
シンタックスハイライトのライブラリ Highlight.js のカスタマイズのサンプルです。
このサンプルでは、コードの上部にツールバーを追加して、行番号の表示・非表示や行の自動折り返しのあり・なし、コピーボタンの表示などが可能です。
スタイルの調整が必要になるなど実用レベルではありませんが、シンプルなサイトであればサンプルのコードをコピペで試すことができると思います。または、カスタマイズのご参考になれば何よりです。
修正する点等多々あるかと思いますので、以下のコードをご使用される場合は自己責任でご利用ください。
以下で使用している Highlight.js のバージョンは 11.9.0 です。
関連ページ:
- Highlight.js でシンタックスハイライト
- Highlight.js を WordPress で使う
- WordPress Highlight.js カスタムブロックの作成
- WordPress Highlight.js カスタムブロック サンプル
作成日: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"><span class="line-num"></span></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 の基本的な使い方を掲載していますので、よろしければ御覧ください。
JavaScript
以下がカスタマイズ用の JavaScript です。
mySetUpHljsPlugins() は Highlight.js のカスタマイズ用プラグインを定義した関数です
mySetupHighlightJs() は Highlight.js の初期化とカスタマイズを定義した関数です。
myAddAccordionPanel() は開閉パネル(アコーディオンパネル)を追加する関数です。
mySetupToggleDetailsAnimation() はアコーディオンパネルの開閉表示アニメーションの関数です。
myCustomHighlightJsSettings は mySetUpHljsPlugins() と mySetupHighlightJs() の両方で使用する設定用のオブジェクトです。
デフォルトでは hljs-wrap クラスを指定した div 要素でラップした pre 要素内の code 要素をハイライトの対象にしますが、15行目で変更できます(※変更する場合は、CSS の該当部分も変更が必要です)。
code 要素に highlight クラスを指定すると、インラインで code 要素をハイライト表示します(pre 要素に囲まれていない場合)。19行目で指定するクラス名を変更できます。
デフォルトでは行番号右側に枠線を表示します。28行目の showLineNumBorder を false に変更して枠線を非表示にすることができます。また、枠線表示の処理を削除したバージョンもあります。
行の自動折り返しを有効にするかどうかは、30行目で指定できます(デフォルトは自動折り返ししない)。個々にクラスを指定して設定することができ、ツールバーで切り替えることができます。
ツールバーやボタンのテキストは50-56行目で変更することができます。
/* updated (changed) on: 2024/03/31 */
document.addEventListener('DOMContentLoaded', () => {
// Highlight.js のプラグインをセットアップ
mySetUpHljsPlugins(myCustomHighlightJsSettings);
// Highlight.js の初期化とカスタマイズの実行
mySetupHighlightJs(myCustomHighlightJsSettings);
// アコーディオンアニメーションの開閉パネルの追加(オプショナル)
myAddAccordionPanel(myCustomHighlightJsSettings.accordionClassName);
// アコーディオンアニメーションの呼び出し(オプショナル)
mySetupToggleDetailsAnimation();
});
const myCustomHighlightJsSettings = {
// pre code のラッパーのクラス名
wrapperClassName: 'hljs-wrap',
// pre 要素なし(インライン)の code 要素でもハイライトするかどうか
useInlineHighlight: true,
// インラインの code 要素でハイライトする場合に code 要素に指定するクラス(空の場合、全ての code 要素)
inlineHighlightClassName: 'highlight',
// アコーディオンパネルで表示する場合に、ラッパー要素に指定するクラス名
accordionClassName: 'toggle-accordion'
}
// Hightlight.js のプラグインを定義
function mySetUpHljsPlugins(settings) {
const { wrapperClassName, useInlineHighlight } = settings;
// 行番号横の枠線を表示するかどうか
const showLineNumBorder = true;
// デフォルトで行の自動折り返しを有効にするかどうか
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 getMaxLineValue(pre, lineNumSpans) {
let maxLineValue = null;
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;
}
}
return maxLineValue;
}
// data-max-lines 属性が指定された場合の表示領域の高さを更新する関数
function updateVisibleHeight(pre, el, lineNumSpans, maxLineValue, isSetMaxHeight = false) {
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() after:highlightElement でプラグイン(機能)を設定
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;
}
}
}
// コードをコピーする関数
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;
}
}
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) {
let borderSpan;
// code 要素のスタイルを取得
const elComputedStyle = window.getComputedStyle(el);
// code 要素の paddingBottom
const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
const elPaddingTop = parseFloat(elComputedStyle.paddingTop);
// 行番号横の枠線を表示する場合
if(showLineNumBorder) {
const codeHeight = el.offsetHeight;
borderSpan = document.createElement('span');
borderSpan.classList.add('hljs-border-span');
el.appendChild(borderSpan);
borderSpan.style.setProperty('position', 'absolute');
borderSpan.style.setProperty('height', codeHeight + 'px');
wrapper.classList.add('line-num-bordered');
}
// ResizeObserver で code 要素のサイズを監視
const codeObserver = new ResizeObserver((entries) => {
if (entries.length > 0 && entries[0]) {
if(showLineNumBorder) {
const entry = entries[0];
let resizedHeight = entry.contentBoxSize ? entry.contentBoxSize[0].blockSize : entry.contentRect.height;
borderSpan.style.setProperty('height', resizedHeight + elPaddingBottom + elPaddingTop + 'px');
}
// 行番号用の span 要素を全て取得
const lineNumSpans = el.getElementsByClassName("line-num");
// ハイライト行の高さの更新
const lineNumSpansArray = Array.prototype.slice.call( lineNumSpans );
// HTMLCollection を配列に変換して処理
lineNumSpansArray.forEach( (elem, index) => {
// line-num-highlight クラスが指定されていれば高さを更新
if(elem.classList.contains('line-num-highlight')) {
// ハイライト用の span 要素(span.line-highlight)
const highlightSpan = lineNumSpans[index].nextElementSibling;
if(highlightSpan) {
if(lineNumSpans.item(index) && lineNumSpans.item(index +1)) {
const spanOffsetTop = lineNumSpans.item(index).offsetTop;
const nextSpanOffsetTop = lineNumSpans.item(index +1).offsetTop;
const height = nextSpanOffsetTop - spanOffsetTop;
if(height !== 0) {
highlightSpan.style.setProperty('height', height + 'px');
}
}else if(lineNumSpans.item(index) && index === lineNumSpans.length - 1) {
// 最後の行の場合は getLastNumLineInfo() で取得
const lastLineNumHeight = getLastNumLineInfo(lineNumSpans, el).height;
if(lastLineNumHeight !== 0) {
highlightSpan.style.setProperty('height', lastLineNumHeight + 'px');
}
}
}
}
});
const maxLineValue = getMaxLineValue(pre, lineNumSpans);
// pre 要素に data-max-lines 属性が指定されていて有効な値であれば
if (maxLineValue) {
// 表示領域の高さを更新
updateVisibleHeight(pre, el, lineNumSpans, maxLineValue);
// 行番号横の枠線を表示する場合
if(showLineNumBorder) {
// 枠線用の span 要素の高さを更新
if(getLastNumLineInfo(lineNumSpans, el).offsetTop) {
borderSpan.style.setProperty('height', getLastNumLineInfo(lineNumSpans, el).offsetTop + elPaddingBottom + 'px');
}else{
// iPhone などでは高さを取得できないので、ボーダーを非表示に(パディング分だけ表示されてしまうため)
borderSpan.style.setProperty('border', 'none');
}
}
}
}
});
codeObserver.observe(el);
}
}
//指定された行をハイライト表示する関数
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;
// 指定された行を data-line-highlight 属性から取得して、その行にハイライト用のクラスを追加
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;
const lineNumSpans = el.getElementsByClassName('line-num');
const maxLineValue = getMaxLineValue(pre, lineNumSpans);
if (maxLineValue) {
updateVisibleHeight(pre, el, lineNumSpans, maxLineValue, 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');
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"]');
const lineNums = wrapper.getElementsByClassName('line-num');
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) => {
const lineNumSpans = el.getElementsByClassName('line-num');
const elComputedStyle = window.getComputedStyle(el);
const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
const maxLineValue = getMaxLineValue(pre, lineNumSpans);
// white-space プロパティを変更
if (e.currentTarget.checked) {
pre.style.setProperty('white-space', 'pre-wrap');
} else {
pre.style.setProperty('white-space', 'pre');
}
// 表示領域の高さを更新
if (maxLineValue) {
updateVisibleHeight(pre, el, lineNumSpans, maxLineValue);
}
// 行番号横の枠線を表示する場合は、行番号枠線(span 要素)の高さを更新
if (showLineNumBorder) {
const borderSpan = wrapper.querySelector('.hljs-border-span');
if(borderSpan) {
// 行番号枠線の高さの更新
borderSpan.style.setProperty('height', el.offsetHeight + 'px');
// pre 要素に data-max-lines 属性が指定されていて有効な値であれば
if (maxLineValue) {
borderSpan.style.setProperty('height', getLastNumLineInfo(lineNumSpans, el).offsetTop + elPaddingBottom + '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)
}
}
}
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;
}
}
});
}
};
CSS
以下がサンプルの CSS です。
使用している環境(基本的な設定や別途読み込んでいる CSS など)により、調整が必要になると思います。
ラッパー要素に指定するクラス名(hljs-wrap)を custom.js の wrapperClassName で変更した場合は、3行目のセレクタ .hljs-wrap を変更します(CSS ネスティングを使用しています)。
/* updated (changed) on: 2024/03/29 (CSS Nesting) */
/* pre 要素のラッパー要素(div)に指定するクラス */
.hljs-wrap {
position: relative;
/* pre 要素 */
pre {
overflow-wrap: break-word;
overflow-x: hidden;
padding: 0;
margin: 0;
/* 必要に応じてフォントサイズなどを設定 */
}
/* pre-wrap クラスを指定すると行を自動で折り返す */
pre.pre-wrap {
white-space: pre-wrap;
}
/* pre クラスを指定すると行を自動で折返さない */
pre.pre {
white-space: pre;
}
/* code 要素 */
pre code {
padding-left: 3rem;
position: relative;
/* 自動折り返しの設定は親要素(pre)の値を継承 */
white-space: inherit;
/* コード内に垂直方向のスクロールバーが表示されるのを防止(必要に応じて) */
overflow-y: hidden;
}
/* 行番号横枠線を表示する場合 */
&.line-num-bordered pre code {
padding-left: 3.25rem;
}
/* ツールバーを使わない場合の code 要素 */
&.no-toolbar pre code,
body.no-toolbar & pre code {
padding-bottom: 1.5rem;
padding-top: 2.5rem;
}
&.no-toolbar pre code.show-no-lang,
body.no-toolbar & pre code.show-no-lang {
padding-top: 1rem;
}
&.no-toolbar.has-copy-btn pre code.show-no-lang,
body.no-toolbar.has-copy-btn & pre code.show-no-lang {
padding-top: 2rem;
}
&.no-toolbar pre code,
body.no-toolbar & pre code {
padding-bottom: 1rem;
}
/* ツールバー */
.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;
}
&.no-lang .highlight-toolbar {
justify-content: flex-end;
}
.highlight-toolbar + pre {
margin-top: 0;
}
.highlight-toolbar label {
color: #888;
cursor: pointer;
margin: 0 10px 0 0;
transition: color 0.3s;
}
.highlight-toolbar input[type="checkbox"] {
background-color: #262b37;
transition: background-color 0.3s;
display: none;
}
@media screen and (min-width: 640px) {
.highlight-toolbar label {
margin: 0 10px 0 3px;
}
.highlight-toolbar input[type="checkbox"] {
display: block;
}
}
.highlight-toolbar input[type="checkbox"]:hover {
background-color: #0d37a9;
}
/* チェックボックスのスタイルのリセット */
.highlight-toolbar input[type="checkbox"] {
border-radius: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
/* チェックボックスのスタイル */
.highlight-toolbar input[type="checkbox"] {
position: relative;
width: 16px;
height: 16px;
border: 1px solid #333;
cursor: pointer;
}
/* チェックマークのスタイル */
.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;
}
.highlight-toolbar input[type="checkbox"]:checked + label {
color: #b9bfd0;
}
/* 言語名を表示 */
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;
}
code[data-language].hide-line-num::before {
left: 2.5rem;
}
/* ツールバーの中の言語名の表示 */
.highlight-toolbar .lng-span {
margin-right: auto;
margin-left: 10px;
font-size: 13px;
color: #ccc;
}
/* コピーボタン(ツールバーを使用しない場合) */
.hljs-copy-btn {
position: absolute;
top: 0;
right: 0;
background-color: #262b37;
border: none;
padding: 8px;
color: #999;
cursor: pointer;
transition: color 0.3s, background-color 0.3s;
}
.hljs-copy-btn:hover {
color: #eee;
background-color: #162858;
}
/* ツールバーの中のコピーボタン */
.highlight-toolbar .hljs-copy-btn {
position: relative;
margin: 0 10px;
padding: 2px 4px;
}
/* ラベルとリンク(data-label 属性と data-label-url 属性で指定した文字列) */
.hljs-label,
.hljs-label-url {
position: absolute;
top: -2rem;
right: 10px;
color: #999;
display: inline-block;
padding: 0.5rem 0;
}
.hljs-label-url {
color: #3987c7;
text-decoration: none;
}
.hljs-label-url:hover {
color: #55924f;
}
&.has-label {
margin-top: 4rem;
}
&:not(.has-label) {
margin-top: 2.25rem;
}
/* 行番号(CSS カウンター)*/
pre {
counter-reset: lineNumber;
}
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;
}
/* 行番号を非表示にする場合 */
pre code.hide-line-num span.line-num::before {
left: -2.5rem;
}
/* 行番号を非表示にする場合 */
pre code.hide-line-num {
margin-left: -2.5rem;
}
/* 行番号右側の枠線 */
.hljs-border-span {
background-color: transparent;
width: 2.5rem;
border-right: 1px solid #595a60;
top: 0;
left: 0;
}
/* 行番号非表示の場合(枠線なし) */
pre code.hide-line-num .hljs-border-span {
border: none;
}
/* 行のハイライト時の行番号部分 */
pre span.line-num.line-num-highlight::before {
color: #c2c21a;
/* background: #424638; */
}
/* 行のハイライト時のコード部分 */
.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;
}
/* 行数を指定して表示する場合にコード下に表示する領域 */
.scroll-footer {
margin: 0;
font-size: 0.75rem;
color: #999;
/* コードの背景色と同じ色 */
background-color: #282c34;
padding: 8px;
display: flex;
justify-content: flex-end;
}
/* .scroll-footer の右端に表示するテキスト*/
.scroll-footer-text {
margin-left: auto;
}
/* .scroll-footer-text の左側に表示するアイコン(色は fill='%23aaaaaa' の aaaaaa 部分) */
.scroll-footer-text::before {
content: "";
display: inline-block;
height: 0.875rem;
width: 0.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");
}
/* テーマ(atom-one-dark)のコメントの色を上書き */
.hljs-comment {
color: #6b788f;
}
}
/* コードの表示・非表示( details 要素と summary 要素によるアコーディオン)*/
details.toggle-code-animation {
border: none;
margin: 2rem 0;
.details-content-wrapper {
padding: 1rem 0;
}
.details-content {
overflow: hidden;
}
summary {
display: inline-block;
cursor: pointer;
position: relative;
padding: 0.5rem 1.5rem 0.5rem 2.25rem;
border: 1px solid #aaa;
font-size: 13px;
}
summary::-webkit-details-marker {
display: none;
}
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);
}
}
自動折返しの設定(white-space)
pre 要素や code 要素の CSS で、white-space に pre や pre-wrap を指定して行の折り返しの設定ができますが、このサンプルでは pre 要素に設定しています。
code 要素には white-space: inherit で、pre 要素の値を継承するようにしています(30行目)。
調整
例えば、このサイトの場合、古い Bootstrap の CSS を読み込んでいるのでツールバーのチェックボックスのスタイルでは追加で以下を指定しています。
画面幅が狭い場合は、チェックボックスを非表示にしていますが、そもそもチェックボックスではなくボタンの方が良いかも知れません。
.hljs-wrap .highlight-toolbar input[type="checkbox"] {
margin: 0;
margin-top: 0;
line-height: normal;
}
.hljs-wrap .highlight-toolbar input[type="checkbox"]:focus {
outline: none;
outline-offset: 0;
}
.hljs-wrap .highlight-toolbar label {
display: inline-block;
max-width: 100%;
margin-bottom: 0;
margin: 0 3px;
font-weight: normal;
}
テーマ
atom-one-dark 以外のテーマを使用する場合は、必要に応じて以下のスタイル(色関連)を調整します。
- .highlight-toolbar
- .highlight-toolbar label
- .highlight-toolbar input[type="checkbox"]:checked+label
- .hljs-wrap .highlight-toolbar .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-toolbar.hljs-reset-position-btn:hover
オプション
基本的な動作は JavaScript で指定しますが、コードごとにカスタムデータ属性やクラスを使ってオプションを指定することができます。
指定できるカスタムデータ属性
以下は 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-max-lines-offset | 数値 | data-max-lines で指定した表示部分のオフセットを指定(単位はピクセル) |
data-scroll-to | 数値 | data-max-lines を指定された際に、指定された行までスクロールします。※ツールバーを非表示の場合、言語名が隠れます(スクロールすれば表示されます) |
data-footer-text | 文字列 | data-max-lines を指定して行数指定して表示する際に、コード下の領域にスクロール可能なことを伝えるテキスト。デフォルトは custom.js の scrollableText の値(scrollable)。アイコンのみ表示するには半角スペースを指定。 |
以下は 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 でデフォルトのテキストを変更可能。 |
指定できるクラス属性
クラス | 説明 |
---|---|
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 | 行を自動で折り返さない |
no-scroll-footer | showScrollFooter(custom.js)が true で data-max-lines を指定している場合にコード下の情報領域(div.scroll-footer)を表示しない |
show-scroll-footer | showScrollFooter(custom.js)が false で data-max-lines を指定している場合にコード下の情報領域(div.scroll-footer)を表示する |
no-line-info | data-max-lines を指定している場合に、コード下の情報領域(div.scroll-footer)に行数の情報を表示しない |
show-line-info | data-max-lines を指定している場合に、コード下の情報領域(div.scroll-footer)に行数の情報を表示する |
no-scroll | data-max-lines を指定している場合、デフォルトではスクロールして他の部分を見ることができますが、no-scroll を指定するとスクロールできないようにします。このクラスを指定した場合、Copy ボタンをクリックしてコピーされるのは表示されている部分のみです。 |
target-blank | data-label-url で指定したリンクに target="_blank" rel="noopener" を追加 |
※ copy-no-xxxx-comments クラスはコピーする際にコメントを含めないオプションですが、コメントの記述されている位置やその前後のコードの内容により、正しく動作しない可能性があります。
クラス | 説明 |
---|---|
language-言語名 | 言語名を明示的に指定(Hightlight.js の仕様) |
show-no-lang | 言語名を表示しない(デフォルトは表示する) |
クラス | 説明 |
---|---|
no-toolbar | そのコードでツールバーを使わない(表示しない) |
toggle-accordion | 開閉ボタンを表示して、クリックするとアコーディオンアニメーションで表示 |
クラス | 説明 |
---|---|
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-言語名」の言語名がツールバーの左上に表示されます。
例えば、以下のように記述すると、
この場合、code 要素に「language-言語名」クラスを指定していないので、以下のように自動検出された言語(javascript)が表示されます。
const timer = setTimeout(console.log("hello!"), 500);
以下のように code 要素に「language-JavaScript」クラスを指定すると、
Highlight.js が言語名の大文字・小文字を区別しないので、「language-言語名」クラスに指定した言語名部分の JavaScript と表示されます。
const timer = setTimeout(console.log("hello!"), 500);
言語名を表示しない
言語名を表示しない場合は code 要素に show-no-lang クラスを指定します。
上記の場合、以下のように言語名は表示されません。
const timer = setTimeout(console.log("hello!"), 500);
実際の言語とは異なる言語名を表示
以下は code 要素に language-HTML クラスを指定しているので、HTML と言語名が表示されています。
<!-- <p>Hello!</p> -->
以下は code 要素に language-plaintext クラスを指定してプレインテキストと表示させていますが、data-set-lang="HTML"
を指定して、言語名を HTML と表示させる例です。
language-plaintext クラスを指定してプレインテキストとして表示しているので、HTML と解析されてコメントとして表示されるよりも少し明るく表示されています。
<!-- <p>Hello!</p> -->
また、data-set-lang 属性を使えば、言語名以外の任意の文字列(例えば、ファイル名)を言語名を表示する位置に表示できます。
ラベル
pre 要素に data-label 属性を指定して、ラベル(ファイル名などの任意の文字列)を右上に表示することができます。data-label-url に URL を指定するとリンクとして表示します。
また、その際、pre 要素に target-blank クラスを指定するとリンクの a 要素に target="_blank" rel="noopener" を追加します。
上記は以下のように表示されます。
<a href="https://example.com" target="_blank" rel="noopener">example.com</a>
行番号
デフォルトでは行番号が表示され、ツールバーに表示・非表示のチェックボックスが表示されます。
ツールバーののテキストは、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行目の preWrapOnInit
を true
に変更します。
行のハイライト
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行を表示します。
// 表示する行数を指定して 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 + visibleHeightAdjustAmount;
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 + scrollToHeightAdjustAmount) + 'px');
scrollAmount = scrollToOffsetTop - scrollAdjustAmount;
el.scroll(0, scrollAmount);
}
}
}
}
}
data-max-lines 属性を指定した場合、コードの下に行数やスクロール可能であることを示すテキストを表示する領域(div.scroll-footer)を表示します。
不要な場合は、個々に pre 要素に no-scroll-footer クラスを指定するか、custom.js の showScrollFooter を false にすればデフォルトで非表示なります。showScrollFooter を false した場合に、個々に表示するには pre 要素に show-scroll-footer クラスを指定します。
デフォルトでは行数の情報を表示しますが、個々に非表示にするには pre 要素に no-line-info クラスを指定します。showLineNumInfo を false にすれば表示しません。その場合、個々に表示にするには pre 要素に show-line-info クラスを指定します。
テキストのデフォルトは「scrollable」ですが、custom.js の scrollableText で変更できます。デフォルトでアイコンのみ表示する場合は、scrollableText の値を空にします。data-footer-text 属性を pre 要素に指定して個々にテキストを設定することもできます(空の値を指定すればアイコンのみになります)。
data-scroll-to 属性
同時に pre 要素に data-scroll-to 属性を指定すれば、指定した行を先頭に表示します。
以下は data-max-lines="20" data-scroll-to="10" と指定して、10行目から20行を表示しています。
※ デフォルトでは、ツールバーでの折り返しや行番号表示の切り替えをした場合やコード部分のサイズの変更が発生した場合にスクロール位置をもとに戻します。custom.js の resetScrollPosition の値を false にすると、スクロール位置をもとに戻しません。
// 表示する行数を指定して 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 + visibleHeightAdjustAmount;
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 + scrollToHeightAdjustAmount) + 'px');
scrollAmount = scrollToOffsetTop - scrollAdjustAmount;
el.scroll(0, scrollAmount);
}
}
}
}
}
※ ツールバーを非表示にしている場合、言語名部分もスクロールされるので言語名が隠れます(一番上までスクロールすれば言語名があります)。
no-scroll クラス
pre 要素に no-scroll クラスを指定すると、指定した範囲のみを表示できます(ユーザーは上下方向にスクロールして他の部分を見ることができません)。
また、no-scroll クラスを指定した場合、Copy ボタンをクリックすると、表示されている範囲のみをコピーします。
以下は前述と同じコンテンツですが、data-max-lines="20" data-scroll-to="10" class="no-scroll" を指定して10行目から20行だけ表示する例です。
// 表示する行数を指定して 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 + visibleHeightAdjustAmount;
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 + scrollToHeightAdjustAmount) + 'px');
scrollAmount = scrollToOffsetTop - scrollAdjustAmount;
el.scroll(0, scrollAmount);
}
}
}
}
}
PHP が使える環境であれば、file_get_contents() と htmlspecialchars() を使えば、サーバー上のファイルを読み込んで表示したい部分のみを指定して表示することができます。
例えば、以下のように指定すれば、
<div class="hljs-wrap">
<pre data-label="custom.js 抜粋" class="no-scroll" data-max-lines="28" data-scroll-to="272" data-line-highlight="284-298"><code class="language-JavaScript pre"><?php echo htmlspecialchars(file_get_contents('../highlight-js/custom.js'), ENT_QUOTES, 'UTF-8'); ?></code></pre>
</div>
以下のようにファイルの一部を表示することができます。
/* updated (changed) on: 2024/03/31 */
document.addEventListener('DOMContentLoaded', () => {
// Highlight.js のプラグインをセットアップ
mySetUpHljsPlugins(myCustomHighlightJsSettings);
// Highlight.js の初期化とカスタマイズの実行
mySetupHighlightJs(myCustomHighlightJsSettings);
// アコーディオンアニメーションの開閉パネルの追加(オプショナル)
myAddAccordionPanel(myCustomHighlightJsSettings.accordionClassName);
// アコーディオンアニメーションの呼び出し(オプショナル)
mySetupToggleDetailsAnimation();
});
const myCustomHighlightJsSettings = {
// pre code のラッパーのクラス名
wrapperClassName: 'hljs-wrap',
// pre 要素なし(インライン)の code 要素でもハイライトするかどうか
useInlineHighlight: true,
// インラインの code 要素でハイライトする場合に code 要素に指定するクラス(空の場合、全ての code 要素)
inlineHighlightClassName: 'highlight',
// アコーディオンパネルで表示する場合に、ラッパー要素に指定するクラス名
accordionClassName: 'toggle-accordion'
}
// Hightlight.js のプラグインを定義
function mySetUpHljsPlugins(settings) {
const { wrapperClassName, useInlineHighlight } = settings;
// 行番号横の枠線を表示するかどうか
const showLineNumBorder = true;
// デフォルトで行の自動折り返しを有効にするかどうか
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 getMaxLineValue(pre, lineNumSpans) {
let maxLineValue = null;
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;
}
}
return maxLineValue;
}
// data-max-lines 属性が指定された場合の表示領域の高さを更新する関数
function updateVisibleHeight(pre, el, lineNumSpans, maxLineValue, isSetMaxHeight = false) {
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() after:highlightElement でプラグイン(機能)を設定
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;
}
}
}
// コードをコピーする関数
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;
}
}
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) {
let borderSpan;
// code 要素のスタイルを取得
const elComputedStyle = window.getComputedStyle(el);
// code 要素の paddingBottom
const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
const elPaddingTop = parseFloat(elComputedStyle.paddingTop);
// 行番号横の枠線を表示する場合
if(showLineNumBorder) {
const codeHeight = el.offsetHeight;
borderSpan = document.createElement('span');
borderSpan.classList.add('hljs-border-span');
el.appendChild(borderSpan);
borderSpan.style.setProperty('position', 'absolute');
borderSpan.style.setProperty('height', codeHeight + 'px');
wrapper.classList.add('line-num-bordered');
}
// ResizeObserver で code 要素のサイズを監視
const codeObserver = new ResizeObserver((entries) => {
if (entries.length > 0 && entries[0]) {
if(showLineNumBorder) {
const entry = entries[0];
let resizedHeight = entry.contentBoxSize ? entry.contentBoxSize[0].blockSize : entry.contentRect.height;
borderSpan.style.setProperty('height', resizedHeight + elPaddingBottom + elPaddingTop + 'px');
}
// 行番号用の span 要素を全て取得
const lineNumSpans = el.getElementsByClassName("line-num");
// ハイライト行の高さの更新
const lineNumSpansArray = Array.prototype.slice.call( lineNumSpans );
// HTMLCollection を配列に変換して処理
lineNumSpansArray.forEach( (elem, index) => {
// line-num-highlight クラスが指定されていれば高さを更新
if(elem.classList.contains('line-num-highlight')) {
// ハイライト用の span 要素(span.line-highlight)
const highlightSpan = lineNumSpans[index].nextElementSibling;
if(highlightSpan) {
if(lineNumSpans.item(index) && lineNumSpans.item(index +1)) {
const spanOffsetTop = lineNumSpans.item(index).offsetTop;
const nextSpanOffsetTop = lineNumSpans.item(index +1).offsetTop;
const height = nextSpanOffsetTop - spanOffsetTop;
if(height !== 0) {
highlightSpan.style.setProperty('height', height + 'px');
}
}else if(lineNumSpans.item(index) && index === lineNumSpans.length - 1) {
// 最後の行の場合は getLastNumLineInfo() で取得
const lastLineNumHeight = getLastNumLineInfo(lineNumSpans, el).height;
if(lastLineNumHeight !== 0) {
highlightSpan.style.setProperty('height', lastLineNumHeight + 'px');
}
}
}
}
});
const maxLineValue = getMaxLineValue(pre, lineNumSpans);
// pre 要素に data-max-lines 属性が指定されていて有効な値であれば
if (maxLineValue) {
// 表示領域の高さを更新
updateVisibleHeight(pre, el, lineNumSpans, maxLineValue);
// 行番号横の枠線を表示する場合
if(showLineNumBorder) {
// 枠線用の span 要素の高さを更新
if(getLastNumLineInfo(lineNumSpans, el).offsetTop) {
borderSpan.style.setProperty('height', getLastNumLineInfo(lineNumSpans, el).offsetTop + elPaddingBottom + 'px');
}else{
// iPhone などでは高さを取得できないので、ボーダーを非表示に(パディング分だけ表示されてしまうため)
borderSpan.style.setProperty('border', 'none');
}
}
}
}
});
codeObserver.observe(el);
}
}
//指定された行をハイライト表示する関数
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;
// 指定された行を data-line-highlight 属性から取得して、その行にハイライト用のクラスを追加
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;
const lineNumSpans = el.getElementsByClassName('line-num');
const maxLineValue = getMaxLineValue(pre, lineNumSpans);
if (maxLineValue) {
updateVisibleHeight(pre, el, lineNumSpans, maxLineValue, 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');
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"]');
const lineNums = wrapper.getElementsByClassName('line-num');
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) => {
const lineNumSpans = el.getElementsByClassName('line-num');
const elComputedStyle = window.getComputedStyle(el);
const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
const maxLineValue = getMaxLineValue(pre, lineNumSpans);
// white-space プロパティを変更
if (e.currentTarget.checked) {
pre.style.setProperty('white-space', 'pre-wrap');
} else {
pre.style.setProperty('white-space', 'pre');
}
// 表示領域の高さを更新
if (maxLineValue) {
updateVisibleHeight(pre, el, lineNumSpans, maxLineValue);
}
// 行番号横の枠線を表示する場合は、行番号枠線(span 要素)の高さを更新
if (showLineNumBorder) {
const borderSpan = wrapper.querySelector('.hljs-border-span');
if(borderSpan) {
// 行番号枠線の高さの更新
borderSpan.style.setProperty('height', el.offsetHeight + 'px');
// pre 要素に data-max-lines 属性が指定されていて有効な値であれば
if (maxLineValue) {
borderSpan.style.setProperty('height', getLastNumLineInfo(lineNumSpans, el).offsetTop + elPaddingBottom + '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)
}
}
}
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;
}
}
});
}
};
位置の調整
設定しているスタイルによっては、行の位置(スクロール先や表示範囲)がずれる可能性があります。
data-max-lines 属性や data-scroll-to 属性を指定した場合に、行の位置がずれる場合、custom.js の以下の変数に px 単位の数値を指定することで表示位置を調整できます。負の値も指定できます。
変数名 | 説明 |
---|---|
visibleHeightAdjustAmount | data-max-lines を指定する場合の表示領域の高さの調整値(初期値:0) |
scrollToHeightAdjustAmount | data-scroll-to を指定して表示する場合の高さの調整値(初期値:0) |
scrollAdjustAmount | data-max-lines を指定して表示する場合にスクロール量の調整値(初期値:0) |
このサイトの場合、以下の値を指定して調整しています。
- visibleHeightAdjustAmount: 26,
- scrollToHeightAdjustAmount: 29,
- scrollAdjustAmount: 3,
行番号の枠線が表示されない(バグ)
iPhone などでは data-max-lines 属性を指定して表示する場合、行番号の右側の枠線が表示されません。
ツールバーの表示・非表示
デフォルトではツールバーを表示します。
ツールバーはページ単位またはコード単位で表示・非表示を切り替えられます。
body 要素に no-toolbar クラスを指定すると、そのページの全てのコードでツールバーを表示しません。
また、ラッパー要素(デフォルトは div.hljs-wrap)に no-toolbar クラスを指定すると、そのコードでツールバーを表示しません。
例えば、以下のようにラッパー要素に class="hljs-wrap no-toolbar" を指定するとツールバーを表示しません。この場合、行番号や行折り返しのチェックボックスは表示されません。
以下はツールバー、行番号、言語名、コピーボタンを表示しない例です。
アコーディオンパネル
ラッパー要素(div.hljs-wrap)に toggle-accordion クラスを指定すると、details 要素と summary 要素のマークアップを自動的に追加し、開閉ボタンを表示してアニメーションでコードを表示します。
ラッパー要素に指定するクラス名(toggle-accordion)は、custom.js の accordionClassName で変更できます。
以下のように表示されます。
<div class="hljs-wrap toggle-accordion">
<pre><code>...</code></pre>
</div>
開閉ボタンのテキストはデフォルトでは Open と Close ですが、ラッパー要素に data-open-text と data-close-text 属性を指定して任意の文字列を表示できます。
以下のように開閉ボタンのテキストに data-open-text と data-close-text 属性で指定した文字列が表示されます。
<div class="hljs-wrap toggle-accordion" data-open-text="コードを表示" data-close-text="コードを閉じる" >
<pre><code>...</code></pre>
</div>
複数のコードを開閉表示する場合は、toggle-accordion クラスを指定した div 要素でそれらのコードを囲みます。
以下のように表示されます。
<div class="hljs-wrap">
<pre><code>...</code></pre>
</div>
<div class="hljs-wrap">
<pre><code>...</code></pre>
</div>
アニメーションで表示するコンテンツはコードに限定していないので、toggle-accordion クラスを指定した div 要素で囲めばそのコンテンツをアニメーションで表示します。
Hello!
手動でアコーディオンパネルをマークアップ
クラスを指定して開閉パネルを追加した場合、details 要素を JavaScript で追加するので、再読み込みの際など、コンテンツがチラツキます。
それが気になる場合は、手動で以下のマークアップ(クラスを指定した details 要素と summary 要素、及び div 要素)を記述してアコーディオンパネルを表示することもできます。
<details class="toggle-code-animation">
<summary data-close-text="コンテンツを閉じる">コンテンツを開く</summary>
<div class="details-content-wrapper">
<div class="details-content">
ここにコードや表示するコンテンツを記述(省略)
</div>
</div>
</details>
コンテンツを開く
// 表示する行数を指定して 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 + visibleHeightAdjustAmount;
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 + scrollToHeightAdjustAmount) + 'px');
scrollAmount = scrollToOffsetTop - scrollAdjustAmount;
el.scroll(0, scrollAmount);
}
}
}
}
}
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: data-footer-text=\"$18\"}}${19: class=\"${20:no-copy-btn }${21:no-highlight-code }${22:no-line-num }${23:copy-no-${24:html-}comments }${25:no-scroll }${26:no-scroll-footer }${27:no-line-info }${28:copy-no-prompt}\"}><code${29: class=\"${30:language-${31:HTML}}${32: pre}${33: show-no-lang}\"}${34: data-set-lang=\"$35\"}>$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"
},
行番号のボーダー
上記のサンプルでは、行番号右にボーダー(枠線)を表示するための span 要素を JavaScript で別途作成して追加し、自動折り返しの有無やサイズの変更に合わせて高さを調整しています。
各行番号の span 要素に border を指定してボーダーを表示することもできます。
但し、行の自動折り返しを有効にした際に折り返しが発生するとボーダーが途切れます。これが気にならなければ、この方法のほうが処理が少しだけシンプルになります。
また、そもそもボーダーを表示しないのであれば、こちらのバージョンの方が良いかもしれません。
以下は、ボーダー(枠線)用の span 要素を使わないバージョンの JavaScript と CSS です。
/* 行番号枠線用の span 要素を作成しない(使わない)バージョン updated on: 2024/03/31 */
document.addEventListener('DOMContentLoaded', () => {
// Highlight.js のプラグインをセットアップ
mySetUpHljsPlugins(myCustomHighlightJsSettings);
// Highlight.js の初期化とカスタマイズの実行
mySetupHighlightJs(myCustomHighlightJsSettings);
// アコーディオンアニメーションの開閉パネルの追加(オプショナル)
myAddAccordionPanel(myCustomHighlightJsSettings.accordionClassName);
// アコーディオンアニメーションの呼び出し(オプショナル)
mySetupToggleDetailsAnimation();
});
const myCustomHighlightJsSettings = {
// pre code のラッパーのクラス名
wrapperClassName: 'hljs-wrap',
// pre 要素なし(インライン)の code 要素でもハイライトするかどうか
useInlineHighlight: true,
// インラインの code 要素でハイライトする場合に code 要素に指定するクラス(空の場合、全ての code 要素)
inlineHighlightClassName: 'highlight',
// アコーディオンパネルで表示場合にラッパー要素に指定するクラス名
accordionClassName: 'toggle-accordion'
}
// 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]) {
// 行番号用の span 要素を全て取得
const lineNumSpans = el.getElementsByClassName("line-num");
// ハイライト行の高さの更新
const lineNumSpansArray = Array.prototype.slice.call( lineNumSpans );
// HTMLCollection を配列に変換して処理
lineNumSpansArray.forEach( (elem, index) => {
// line-num-highlight クラスが指定されていれば高さを更新
if(elem.classList.contains('line-num-highlight')) {
// ハイライト用の span 要素(span.line-highlight)
const highlightSpan = lineNumSpans[index].nextElementSibling;
if(highlightSpan) {
if(lineNumSpans.item(index) && lineNumSpans.item(index +1)) {
const spanOffsetTop = lineNumSpans.item(index).offsetTop;
const nextSpanOffsetTop = lineNumSpans.item(index +1).offsetTop;
const height = nextSpanOffsetTop - spanOffsetTop;
if(height !== 0) {
highlightSpan.style.setProperty('height', height + 'px');
}
}else if(lineNumSpans.item(index) && index === lineNumSpans.length - 1) {
// 最後の行の場合(次の行がないのでダミーを挿入して取得)
const lastLineNumHeight = getLastNumLineInfo(lineNumSpans, el).height;
if(lastLineNumHeight !== 0) {
highlightSpan.style.setProperty('height', lastLineNumHeight + '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;
}
}
});
}
};
以下が CSS です。ボーダーを表示しない場合は 231行目の記述を削除します。
/* updated (changed) on: 2024/03/29 */
/* pre 要素のラッパー要素(div)に指定するクラス( CSS ネスティングを使用)*/
.hljs-wrap {
position: relative;
/* pre 要素 */
pre {
overflow-wrap: break-word;
overflow-x: hidden;
padding: 0;
margin: 0;
/* 必要に応じてフォントサイズなどを設定 */
}
/* pre-wrap クラスを指定すると行を自動で折り返す */
pre.pre-wrap {
white-space: pre-wrap;
}
/* pre クラスを指定すると行を自動で折返さない */
pre.pre {
white-space: pre;
}
/* 以下の padding-left を変更したら custom.js の codePadding も変更 */
pre code {
padding-left: 3rem;
position: relative;
/* 自動折り返しの設定は親要素(pre)の値を継承 */
white-space: inherit;
/* コード内に垂直方向のスクロールバーが表示されるのを防止(必要に応じて) */
overflow-y: hidden;
/* border-left: 3px solid red; */
}
/* ツールバーを使わない場合の code 要素 */
&.no-toolbar pre code,
body.no-toolbar & pre code {
padding-bottom: 1.5rem;
padding-top: 2.5rem;
}
&.no-toolbar pre code.show-no-lang,
body.no-toolbar & pre code.show-no-lang {
padding-top: 1rem;
}
&.no-toolbar.has-copy-btn pre code.show-no-lang,
body.no-toolbar.has-copy-btn & pre code.show-no-lang {
padding-top: 2rem;
}
&.no-toolbar pre.no-copy-btn code,
body.no-toolbar & pre.no-copy-btn code {
padding-bottom: 1rem;
}
/* ツールバー */
.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;
}
&.no-lang .highlight-toolbar {
justify-content: flex-end;
}
.highlight-toolbar + pre {
margin-top: 0;
}
.highlight-toolbar label {
color: #888;
cursor: pointer;
margin: 0 10px 0 0;
transition: color 0.3s;
}
.highlight-toolbar input[type="checkbox"] {
background-color: #262b37;
transition: background-color 0.3s;
display: none;
}
@media screen and (min-width: 640px) {
.highlight-toolbar label {
margin: 0 10px 0 3px;
}
.highlight-toolbar input[type="checkbox"] {
display: block;
}
}
.highlight-toolbar input[type="checkbox"]:hover {
background-color: #0d37a9;
}
/* チェックボックスのスタイルのリセット */
.highlight-toolbar input[type="checkbox"] {
border-radius: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
/* チェックボックスのスタイル */
.highlight-toolbar input[type="checkbox"] {
position: relative;
width: 16px;
height: 16px;
border: 1px solid #333;
cursor: pointer;
}
/* チェックマークのスタイル */
.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;
}
.highlight-toolbar input[type="checkbox"]:checked + label {
color: #b9bfd0;
}
/* 言語名を表示 */
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;
}
code[data-language].hide-line-num::before {
left: 2.5rem;
}
/* ツールバーの中の言語名の表示 */
.highlight-toolbar .lng-span {
margin-right: auto;
margin-left: 10px;
font-size: 13px;
color: #ccc;
}
/* コピーボタン(ツールバーを使用しない場合) */
.hljs-copy-btn {
position: absolute;
top: 0;
right: 0;
background-color: #262b37;
border: none;
padding: 8px;
color: #999;
cursor: pointer;
transition: color 0.3s, background-color 0.3s;
}
.hljs-copy-btn:hover {
color: #eee;
background-color: #162858;
}
/* ツールバーの中のコピーボタン */
.highlight-toolbar .hljs-copy-btn {
position: relative;
margin: 0 10px;
padding: 2px 4px;
}
/* ラベルとリンク(data-label 属性と data-label-url 属性で指定した文字列) */
.hljs-label,
.hljs-label-url {
position: absolute;
top: -2rem;
right: 10px;
color: #999;
display: inline-block;
padding: 0.5rem 0;
}
.hljs-label-url {
color: #3987c7;
text-decoration: none;
}
.hljs-label-url:hover {
color: #55924f;
}
&.has-label {
margin-top: 4rem;
}
/* 行番号(CSS カウンター)*/
pre {
counter-reset: lineNumber;
}
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;
}
/* 行番号を非表示にする場合 */
pre code.hide-line-num span.line-num::before {
left: -2.5rem;
}
/* 行番号を非表示にする場合 */
pre code.hide-line-num {
margin-left: -2.5rem;
}
/* 行のハイライト時の行番号部分 */
pre span.line-num.line-num-highlight::before {
color: #c2c21a;
/* background: #424638; */
}
/* 行のハイライト時のコード部分 */
.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;
}
/* 行数を指定して表示する場合にコード下に表示する領域 */
.scroll-footer {
margin: 0;
font-size: 0.75rem;
color: #999;
/* コードの背景色と同じ色 */
background-color: #282c34;
padding: 8px;
display: flex;
justify-content: flex-end;
}
/* .scroll-footer の右端に表示するテキスト*/
.scroll-footer-text {
margin-left: auto;
}
/* .scroll-footer-text の左側に表示するアイコン(色は fill='%23aaaaaa' の aaaaaa 部分) */
.scroll-footer-text::before {
content: "";
display: inline-block;
height: 0.875rem;
width: 0.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");
}
/* テーマ(atom-one-dark)のコメントの色を上書き */
.hljs-comment {
color: #6b788f;
}
}
/* コードの表示・非表示( details 要素と summary 要素によるアコーディオン)*/
details.toggle-code-animation {
border: none;
margin: 2rem 0;
.details-content-wrapper {
padding: 1rem 0;
}
.details-content {
overflow: hidden;
}
summary {
display: inline-block;
cursor: pointer;
position: relative;
padding: 0.5rem 0.5rem 0.5rem 36px;
border: 1px solid #aaa;
font-size: 13px;
}
summary::-webkit-details-marker {
display: none;
}
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);
}
}
シンプルなバージョン
このカスタマイズサンプルでは、行番号のボーダーの他に、指定した行をハイライトする際に、ハイライトされる行の高さを取得して設定する処理も記述しています。
以下は、ハイライトされる行の高さを取得する処理を行わなず、CSS で固定の高さを指定する JavaScript と CSS です。行番号枠線用の span 要素も使用しないので、前述の例より処理が更にシンプルになります。
/* シンプルバージョン updated on: 2024/03/31 */
document.addEventListener('DOMContentLoaded', () => {
// Highlight.js のプラグインをセットアップ
mySetUpHljsPlugins(myCustomHighlightJsSettings);
// Highlight.js の初期化とカスタマイズの実行
mySetupHighlightJs(myCustomHighlightJsSettings);
// アコーディオンアニメーションの開閉パネルの追加(オプショナル)
myAddAccordionPanel(myCustomHighlightJsSettings.accordionClassName);
// アコーディオンアニメーションの呼び出し(オプショナル)
mySetupToggleDetailsAnimation();
});
const myCustomHighlightJsSettings = {
// pre code のラッパーのクラス名
wrapperClassName: 'hljs-wrap',
// pre 要素なし(インライン)の code 要素でもハイライトするかどうか
useInlineHighlight: true,
// インラインの code 要素でハイライトする場合に code 要素に指定するクラス(空の場合、全ての code 要素)
inlineHighlightClassName: 'highlight',
// アコーディオンパネルで表示場合にラッパー要素に指定するクラス名
accordionClassName: 'toggle-accordion'
}
// 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';
// 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 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 lastLineOffsetTop = dummy2.offsetTop;
dummy.remove();
dummy2.remove();
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;
}
}
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]) {
// 行番号用の span 要素を全て取得
const lineNumSpans = el.getElementsByClassName("line-num");
// 表示領域の高さを更新
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;
}
}
});
}
};
以下が CSS です。
/* updated (changed) on: 2024/03/29 */
/* pre 要素のラッパー要素(div)に指定するクラス( CSS ネスティングを使用)*/
.hljs-wrap {
position: relative;
pre {
overflow-wrap: break-word;
overflow-x: hidden;
padding: 0;
margin: 0;
}
pre.pre-wrap {
white-space: pre-wrap;
}
pre.pre {
white-space: pre;
}
pre code {
padding-left: 3rem;
position: relative;
white-space: inherit;
overflow-y: hidden;
}
&.no-toolbar pre code,
body.no-toolbar & pre code {
padding-bottom: 1.5rem;
padding-top: 2.5rem;
}
&.no-toolbar pre code.show-no-lang,
body.no-toolbar & pre code.show-no-lang {
padding-top: 1rem;
}
&.no-toolbar.has-copy-btn pre code.show-no-lang,
body.no-toolbar.has-copy-btn & pre code.show-no-lang {
padding-top: 2rem;
}
&.no-toolbar pre.no-copy-btn code,
body.no-toolbar & pre.no-copy-btn code {
padding-bottom: 1rem;
}
.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;
}
&.no-lang .highlight-toolbar {
justify-content: flex-end;
}
.highlight-toolbar + pre {
margin-top: 0;
}
.highlight-toolbar label {
color: #888;
cursor: pointer;
margin: 0 10px 0 0;
transition: color 0.3s;
}
.highlight-toolbar input[type="checkbox"] {
background-color: #262b37;
transition: background-color 0.3s;
display: none;
}
@media screen and (min-width: 640px) {
.highlight-toolbar label {
margin: 0 10px 0 3px;
}
.highlight-toolbar input[type="checkbox"] {
display: block;
}
}
.highlight-toolbar input[type="checkbox"]:hover {
background-color: #0d37a9;
}
.highlight-toolbar input[type="checkbox"] {
border-radius: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.highlight-toolbar input[type="checkbox"] {
position: relative;
width: 16px;
height: 16px;
border: 1px solid #333;
cursor: pointer;
}
.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;
}
.highlight-toolbar input[type="checkbox"]:checked + label {
color: #b9bfd0;
}
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;
}
code[data-language].hide-line-num::before {
left: 2.5rem;
}
.highlight-toolbar .lng-span {
margin-right: auto;
margin-left: 10px;
font-size: 13px;
color: #ccc;
}
.hljs-copy-btn {
position: absolute;
top: 0;
right: 0;
background-color: #262b37;
border: none;
padding: 8px;
color: #999;
cursor: pointer;
transition: color 0.3s, background-color 0.3s;
}
.hljs-copy-btn:hover {
color: #eee;
background-color: #162858;
}
.highlight-toolbar .hljs-copy-btn {
position: relative;
margin: 0 10px;
padding: 2px 4px;
}
.hljs-label,
.hljs-label-url {
position: absolute;
top: -2rem;
right: 10px;
color: #999;
display: inline-block;
padding: 0.5rem 0;
}
.hljs-label-url {
color: #3987c7;
text-decoration: none;
}
.hljs-label-url:hover {
color: #55924f;
}
&.has-label {
margin-top: 4rem;
}
pre {
counter-reset: lineNumber;
}
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; */
}
pre code.hide-line-num span.line-num::before {
left: -2.5rem;
}
pre code.hide-line-num {
margin-left: -2.5rem;
}
pre span.line-num.line-num-highlight::before {
color: #c2c21a;
}
.line-highlight {
position: absolute;
left: 2.5rem;
width: calc(100% - 2.5rem);
margin-left: -2.5rem;
width: 100%;
/* 高さを指定 */
height: 1.1rem;
background: linear-gradient(
to right,
hsla(254, 15%, 51%, 0.2) 50%,
hsla(254, 15%, 51%, 0.01)
);
pointer-events: none;
}
/* 行数を指定して表示する場合にコード下に表示する領域 */
.scroll-footer {
margin: 0;
font-size: 0.75rem;
color: #999;
/* コードの背景色と同じ色 */
background-color: #282c34;
padding: 8px;
display: flex;
justify-content: flex-end;
}
/* .scroll-footer の右端に表示するテキスト*/
.scroll-footer-text {
margin-left: auto;
}
/* .scroll-footer-text の左側に表示するアイコン(色は fill='%23aaaaaa' の aaaaaa 部分) */
.scroll-footer-text::before {
content: "";
display: inline-block;
height: 0.875rem;
width: 0.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");
}
.hljs-comment {
color: #6b788f;
}
}
details.toggle-code-animation {
border: none;
margin: 2rem 0;
.details-content-wrapper {
padding: 1rem 0;
}
.details-content {
overflow: hidden;
}
summary {
display: inline-block;
cursor: pointer;
position: relative;
padding: 0.5rem 0.5rem 0.5rem 36px;
border: 1px solid #aaa;
font-size: 13px;
}
summary::-webkit-details-marker {
display: none;
}
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);
}
}
行数を指定して表示する機能を削除
行数(data-max-lines 属性)を指定して表示する機能を削除するとよりシンプルになります。
以下が JavaScript です。
/* 行数を指定して表示する機能を削除したバージョン(最もシンプルなバージョン) updated on: 2024/03/31 */
document.addEventListener('DOMContentLoaded', () => {
// Highlight.js のプラグインをセットアップ
mySetUpHljsPlugins(myCustomHighlightJsSettings);
// Highlight.js の初期化とカスタマイズの実行
mySetupHighlightJs(myCustomHighlightJsSettings);
// アコーディオンアニメーションの開閉パネルの追加(オプショナル)
myAddAccordionPanel(myCustomHighlightJsSettings.accordionClassName);
// アコーディオンアニメーションの呼び出し(オプショナル)
mySetupToggleDetailsAnimation();
});
const myCustomHighlightJsSettings = {
// pre code のラッパーのクラス名
wrapperClassName: 'hljs-wrap',
// pre 要素なし(インライン)の code 要素でもハイライトするかどうか
useInlineHighlight: true,
// インラインの code 要素でハイライトする場合に code 要素に指定するクラス(空の場合、全ての code 要素)
inlineHighlightClassName: 'highlight',
// アコーディオンパネルで表示場合にラッパー要素に指定するクラス名
accordionClassName: 'toggle-accordion'
}
// 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 lineAutoWrapLabel = 'wrap';
const lineNumLabel = 'number';
const copyBtnLabel = 'Copy';
const copyBtnCompleteLabel = 'Copied';
const copyBtnFailedLabel = 'Failed';
const copyFailedMessage = 'Sorry, can not copy with this browser.';
// 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);
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) {
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);
}
// 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;
}
}
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;
}
}
}
//指定された行をハイライト表示する関数
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');
}
}
}
})
}
}
}
// ラッパー要素にラベルやツールバーを表示する関数
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');
}
});
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;
}
}
});
}
};
以下が CSS です。
.hljs-wrap {
position: relative;
pre {
overflow-wrap: break-word;
overflow-x: hidden;
padding: 0;
}
pre.pre-wrap {
white-space: pre-wrap;
}
pre.pre {
white-space: pre;
}
pre code {
padding-left: 3rem;
position: relative;
white-space: inherit;
overflow-y: hidden;
}
&.no-toolbar pre code,
body.no-toolbar & pre code {
padding-bottom: 1.5rem;
padding-top: 2.5rem;
}
&.no-toolbar pre code.show-no-lang,
body.no-toolbar & pre code.show-no-lang {
padding-top: 1rem;
}
&.no-toolbar.has-copy-btn pre code.show-no-lang,
body.no-toolbar.has-copy-btn & pre code.show-no-lang {
padding-top: 2rem;
}
&.no-toolbar pre.no-copy-btn code,
body.no-toolbar & pre.no-copy-btn code {
padding-bottom: 1rem;
}
.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;
}
&.no-lang .highlight-toolbar {
justify-content: flex-end;
}
.highlight-toolbar + pre {
margin-top: 0;
}
.highlight-toolbar label {
color: #888;
cursor: pointer;
margin: 0 10px 0 0;
transition: color 0.3s;
}
.highlight-toolbar input[type="checkbox"] {
background-color: #262b37;
transition: background-color 0.3s;
display: none;
}
@media screen and (min-width: 640px) {
.highlight-toolbar label {
margin: 0 10px 0 3px;
}
.highlight-toolbar input[type="checkbox"] {
display: block;
}
}
.highlight-toolbar input[type="checkbox"]:hover {
background-color: #0d37a9;
}
.highlight-toolbar input[type="checkbox"] {
border-radius: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.highlight-toolbar input[type="checkbox"] {
position: relative;
width: 16px;
height: 16px;
border: 1px solid #333;
cursor: pointer;
}
.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;
}
.highlight-toolbar input[type="checkbox"]:checked + label {
color: #b9bfd0;
}
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;
}
code[data-language].hide-line-num::before {
left: 2.5rem;
}
.highlight-toolbar .lng-span {
margin-right: auto;
margin-left: 10px;
font-size: 13px;
color: #ccc;
}
.hljs-copy-btn {
position: absolute;
top: 0;
right: 0;
background-color: #262b37;
border: none;
padding: 8px;
color: #999;
cursor: pointer;
transition: color 0.3s, background-color 0.3s;
}
.hljs-copy-btn:hover {
color: #eee;
background-color: #162858;
}
.highlight-toolbar .hljs-copy-btn {
position: relative;
margin: 0 10px;
padding: 2px 4px;
}
.hljs-label,
.hljs-label-url {
position: absolute;
top: -2rem;
right: 10px;
color: #999;
display: inline-block;
padding: 0.5rem 0;
}
.hljs-label-url {
color: #3987c7;
text-decoration: none;
}
.hljs-label-url:hover {
color: #55924f;
}
&.has-label {
margin-top: 4rem;
}
pre {
counter-reset: lineNumber;
}
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;
}
pre code.hide-line-num span.line-num::before {
left: -2.5rem;
}
pre code.hide-line-num {
margin-left: -2.5rem;
}
pre span.line-num.line-num-highlight::before {
color: #c2c21a;
}
/* コード部分のハイライトが不要な場合は以下を削除 */
.line-highlight {
position: absolute;
left: 2.5rem;
width: calc(100% - 2.5rem);
margin-left: -2.5rem;
width: 100%;
height: 1.1rem;
background: linear-gradient(
to right,
hsla(254, 15%, 51%, 0.2) 50%,
hsla(254, 15%, 51%, 0.01)
);
pointer-events: none;
}
& .hljs-comment {
color: #6b788f;
}
}
details.toggle-code-animation {
border: none;
margin: 2rem 0;
.details-content-wrapper {
padding: 1rem 0;
}
.details-content {
overflow: hidden;
}
summary {
display: inline-block;
cursor: pointer;
position: relative;
padding: 0.5rem 0.5rem 0.5rem 36px;
border: 1px solid #aaa;
font-size: 13px;
}
summary::-webkit-details-marker {
display: none;
}
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);
}
}
WordPress で使う
WordPress で使用する例です。functions.php で CSS と JavaScript を読み込みます。
この例ではテーマフォルダの中に highlight-js というフォルダを作成して以下のファイルを保存します。
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({
'after:highlightElement': ({ el, result }) => {
// 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」ブロックを使う場合は、入力するコードをエスケープしなければなりませんが、コードブロックを使えば、エスケープ処理なしでコードをそのまま入力することができます。
オプションはクラス名を使って指定する必要がありますが、カスタムブロックを作成するよりは手軽に実装できます。詳細は以下のページを御覧ください。
カスタムブロックを作成
カスタムブロックを作成すれば、data-* 属性やクラス属性で指定するオプションをインスペクターに設定して、簡単にチェックボックスやテキストフィールドなどを使って指定できるようにすることができます。
また、コードブロック同様、pre 要素や code 要素のマークアップが不要で、ブロックに直接入力したコードをエスケープ処理なしでハイライト表示することができます。
詳細は以下のページを御覧ください。