Highlight.js を WordPress で使う
CDN 経由またはダウンロードした Highlight.js の JavaScript と CSS を functions.php で読み込み、コードブロックにコードを直接記述してシンタックスハイライト表示する方法です。
比較的簡単に WordPress で Highlight.js を使ったハイライト表示を実装できます。
また、コードブロックを使うので、入力するコードはエスケープ処理が不要です。必要に応じて簡単な記述で行番号やコピーボタンなども表示することができます。
使用しているバージョンは以下になります。
- WordPress: v6.4.3
- Highlight.js: v11.9.0
作成日:2024年2月16日
関連ページ
- WordPress Highlight.js カスタムブロックの作成
- WordPress Highlight.js カスタムブロック サンプル
- Highlight.js でシンタックスハイライト
- Highlight.js のカスタマイズ サンプル
最も簡単な方法
Highlight.js の JavaScript と CSS を CDN 経由で読み込んで使う方法です。
functions.php に以下の読み込みの記述を追加するだけでハイライト表示することができます。
Highlight.js を CDN で読み込む
テーマの functions.php に以下を記述して、Highlight.js の CSS と JavaScript を wp_enqueue_scripts アクションフックで CDN 経由で読み込みます。
その際、Highlight.js の JavaScript の読み込みの後で、wp_add_inline_script を使って script タグで hljs.highlightAll();
を呼び出して Highlight.js を初期化します。
初期化の際に hljs オブジェクトのメソッド highlightAll()
を呼び出すと、<pre><code>〜</code></pre>
で囲まれた全てのコードがハイライト表示されます。
コードブロックに記述した内容は wp-block-code クラスが付与された pre 要素と code 要素で囲まれて出力されるので、以下によりコードブロックに記述した内容は全てハイライト表示されるようになります。
以下の例では atom-one-dark.min.css という Highlight.js のテーマスタイルを読み込んでいます。
どのようなテーマがあるかは Examples ページで確認することができます。
例えば、デフォルトのスタイルを読み込むにはファイル名部分は default.min.css になります(10行目)。
function add_my_hljs_styles_and_scripts() {
// 管理画面では何もしない
if (is_admin()) return;
// Hightlight.js の CSS(atom-one-dark.min.css)の読み込み
wp_enqueue_style(
// ハンドル名(任意の名前)
'atom-one-dark',
'//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css',
array(),
NULL
);
// Hightlight.js の JavaScript(highlight.min.js)の読み込み
wp_enqueue_script(
'highlightJS', // ハンドル名(任意の名前)
'//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js',
array(),
NULL,
true
);
// Highlight.js の初期化
wp_add_inline_script(
// 上記で登録した JavaScript のハンドル名を指定
'highlightJS',
// script タグに出力する JavaScript(Highlight.js の初期化)
'hljs.highlightAll();'
);
}
add_action('wp_enqueue_scripts', 'add_my_hljs_styles_and_scripts');
SRI を使用する場合
必要に応じて SRI 用の integrity 属性などを指定することもできます(記述が少し長くなります)。以下は integrity 属性などを指定して読み込む例です。
integrity 属性などの値はテーマ(スタイル)ごとに異なるので CDN などで確認が必要です。
function add_my_hljs_styles_and_scripts() {
// 管理画面では何もしない
if (is_admin()) return;
// Hightlight.js テーマ CSS(atom-one-dark.min.css)の読み込み
wp_enqueue_style(
// ハンドル名(任意の名前)
'atom-one-dark',
'//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css',
array(),
NULL
);
// highlight.min.js の読み込み
wp_enqueue_script(
'highlightJS', // ハンドル名(任意の名前)
'//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js',
array(),
NULL,
true
);
}
add_action('wp_enqueue_scripts', 'add_my_hljs_styles_and_scripts');
// integrity 属性を追加
function change_stylesheet_link($html, $handle, $href) {
if (is_admin()) {
return $html;
}
// ハンドル名が atom-one-dark の場合
if ($handle === 'atom-one-dark') {
$html = '<link rel="stylesheet" href="' . $href . '" integrity="sha512-Jk4AqjWsdSzSWCSuQTfYRIF84Rq/eV0G2+tu07byYwHcbTGfdmLrHjUSwvzp5HvbiqK4ibmNwdcG49Y5RGYPTg==" crossorigin="anonymous" referrerpolicy="no-referrer">' . "\n";
}
return $html;
}
add_filter('style_loader_tag', 'change_stylesheet_link', 10, 3);
function change_script_tag($tag, $handle, $src) {
if (is_admin()) {
return $tag;
}
// ハンドル名が highlightJS の場合
if ($handle === 'highlightJS') {
$tag = '<script src="' . $src . '" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>' . "\n";
}
return $tag;
}
add_filter('script_loader_tag', 'change_script_tag', 10, 3);
// 初期化のインラインスクリプトを追加
function add_my_hljs_init() {
if (is_single()) {
?>
<script>
hljs.highlightAll();
</script>
<?php
}
}
add_action('wp_footer', 'add_my_hljs_init', 99);
CSS や JavaScript ファイルの読み込みについての詳細は以下のページを御覧ください。
コードブロックにコードを記述
投稿画面で「コード」ブロックを選択します。「コード」ブロックが表示されない場合は「全て表示」をクリックして探します(テキストカテゴリにあります)。
コードブロックの中に表示したいコードを記述します。入力したコードは自動的にエスケープされるので、エスケープ処理は必要ありません。
投稿を保存してフロントエンド側を確認すると、この例の場合は以下のように表示されます。
デベロッパーツールで確認すると、この例では Highlight.js の言語の自動検出により、code 要素には language-csharp
クラスが付与されています。また、以下の例ではコードの周りにテーマのスタイルによってパディングが適用されています。
これだけで基本的なシンタックスハイライト表示ができました。
但し、言語の自動検出が必ずしも正確ではないので、期待した表示と異なる場合や明示的に言語名を指定したい場合など、必要に応じてコードブロックごとに CSS クラスを指定します。
CSS クラスを指定
必要に応じて、右側のインスペクターの「高度な設定」の「追加 CSS クラス」に、言語を指定するためのクラス(language-xxxx)を追加します。この例では language-javascript クラスを指定します。
「高度な設定」で追加したクラスはコードブロックの場合、pre 要素に追加されます。
投稿を保存してフロントエンド側を確認すると、上記で指定した language-javascript クラスが pre 要素に追加され、code 要素のクラスも language-javascript クラスに変わっています。
pre 要素に追加された言語クラス(language-xxxx)は、Highlight.js により自動的に code 要素のクラスにも追加されます。
これによりシンタックスハイライト表示(function の文字色)も変わっています。
スタイルを設定
テーマによっては、pre 要素にパディングが設定されているなど、追加のスタイルの設定が必要になる場合があります。
この例で使用しているテーマの場合、pre 要素にパディングが設定されているので、style.css にコードのパディングを削除する記述を追加しています。
コードブロックで挿入される pre 要素には wp-block-code
クラスが付与されます。
以下のいずれの CSS も、全てのコードブロックのパディングを0にします。
pre.wp-block-code {
padding: 0;
}
Twenty Twenty-Four などの場合
Twenty Twenty-Four のテーマをそのまま使用する場合など、上記を style.css に記述しただけでは適用されない場合は、必要に応じて functions.php で style.css を読み込みます。
//style.css を head 内に読み込む
function add_my_style_css() {
wp_enqueue_style('style', get_stylesheet_uri());
}
add_action('wp_enqueue_scripts', 'add_my_style_css');
関連ページ:WordPress CSSやJavaScriptファイルの読み込み
または、wp_theme_json_data_theme フィルターを使って theme.json を上書きすることもできます。
以下はテーマ Twenty Twenty-Four で functions.php に wp_theme_json_data_theme フィルタを記述してコードブロックのパディングを全て0にする例です。
function my_core_code_filter_theme_json_theme($theme_json) {
$new_data = array(
'version' => 2,
'styles' => array(
'blocks' => array(
'core/code' => array(
'spacing' => array(
'padding' => array(
'bottom' => '0',
'left' => '0',
'right' => '0',
'top' => '0',
),
),
),
),
),
);
return $theme_json->update_with($new_data);
}
add_filter('wp_theme_json_data_theme', 'my_core_code_filter_theme_json_theme');
インラインでスタイルを出力
以下は style.css などにスタイルを記述するのではなく、インラインでスタイルを出力する例です。
以下を functions.php に記述します。
function add_my_custom_highlight_css( ){
if(!is_admin()) {
?>
<style>
pre.wp-block-code {
padding: 0;
}
</style>
<?php
}
}
add_action( 'wp_head', 'add_my_custom_highlight_css', 999 );
ダウンロードして使う場合
CDN 経由ではなく、Highlight.js のファイルを ダウンロードページ からダウンロードして使う例です。
関連ページ:Highlight.js でシンタックスハイライト
この例では JavaScript(highlight.min.js)と CSS(atom-one-dark.min.css)をダウンロードして、テーマフォルダに highlight-js というフォルダを作成して保存します。
functions.php を以下のように変更して、ダウンロードして配置したファイルを読み込みます。
function add_my_hljs_styles_and_scripts() {
if (is_admin()) return;
// 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' ) ) //更新時キャッシュクリア
);
// 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
);
// Highlight.js の初期化
wp_add_inline_script(
// 上記で登録した JavaScript のハンドル名を指定
'highlightJS',
// script タグに出力する JavaScript(Highlight.js の初期化)
'hljs.highlightAll();'
);
}
add_action('wp_enqueue_scripts', 'add_my_hljs_styles_and_scripts');
ハイライト表示の制御
これまでの場合、コードブロックに記述した全てのコードに Highlight.js が適用されてハイライト表示されますが、場合によっては、Highlight.js を適用したくないコードもあります。
以下はデフォルトではコードブロックに記述したコードをハイライト表示し、no-highlight
クラスを指定した場合は、ハイライト表示しない(Highlight.js を適用しない)ようにする例です。
また、ハイライト表示するコードブロックの要素を hljs-wrap
というクラスを指定した div 要素でラップするので、そのクラスを使ってハイライト表示するコードブロックのスタイルを指定することができます。
highlight-js フォルダを作成してある場合は、その中に JavaScript ファイル setup.js を作成します。
CDN を使用する場合は、highlight-js フォルダを作成し、setup.js のみを追加します。
以下を作成した setup.js に記述します。
この JavaScript では必要な変数の定義やハイライト表示などの処理を関数 mySetupHighlightJS にまとめて、DOMContentLoaded で呼び出しています(DOMContentLoaded を使わなくても body の閉じタグの前で setup.js を読み込むので問題はありません)。
ハイライト表示したくないコードには、「高度な設定」→「追加 CSS クラス」で no-highlight
クラスを指定します(クラス名は12行目で変更できます)。
デフォルトでコードをハイライト表示しない場合は、10行目の true を false に変更します。その場合、ハイライト表示するコードには、「追加 CSS クラス」で highlight
クラスを指定します。
また、ラッパー要素に指定するクラス名(hljs-wrap
)は8行目で変更することができます。
// 処理を関数にまとめて DOMContentLoaded で呼び出す
document.addEventListener("DOMContentLoaded", () => {
mySetupHighlightJS();
});
function mySetupHighlightJS() {
// コードブロックのラッパー要素に付与するクラス名
const wrapperClass = "hljs-wrap";
// デフォルトでコードをハイライト表示するかどうか(true の場合は、デフォルトでハイライト表示します)
const highlightCodeByDefault = true;
// デフォルトでハイライト表示する場合に、ハイライトしないコードブロックに指定するクラス
const noHighlightClass = "no-highlight";
// デフォルトでハイライト表示しない場合に、ハイライトするコードブロックに指定するクラス
const highlightClass = "highlight";
// 全てのコードブロックの要素を取得
const blockCodeElems = document.getElementsByClassName("wp-block-code");
if(blockCodeElems.length > 0) {
// デフォルトでコードをハイライト表示する場合
if(highlightCodeByDefault) {
for (const elem of blockCodeElems) {
if(!elem.classList.contains(noHighlightClass)) {
// Highlight.js で初期化する関数を実行
initHighlightJs(elem);
}
}
}else{
// デフォルトでコードをハイライト表示しない場合
for (const elem of blockCodeElems) {
if(elem.classList.contains(highlightClass)) {
// Highlight.js で初期化する関数を実行
initHighlightJs(elem);
}
}
}
}
// コードブロックの要素をラッパーで囲み Highlight.js で初期化する関数
function initHighlightJs(elem) {
// コードブロックの要素をラッパー(div.hljs-wrap)で囲む
const wrapper = document.createElement("div");
wrapper.className = wrapperClass;
elem.insertAdjacentElement("beforebegin", wrapper);
wrapper.appendChild(elem);
// Highlight.js で初期化
hljs.highlightElement(wrapper.querySelector("code"));
}
}
functions.php に上記の JavaScript(setup.js)の読み込みを追加します。
上記の setup.js には Highlight.js の初期化の処理が記述されているので、今まで wp_add_inline_script で追加していたインラインスクリプトによる初期化処理は不要なので削除しています。
function add_my_hljs_styles_and_scripts() {
if (is_admin()) return;
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_script(
'highlightJS',
get_theme_file_uri( '/highlight-js/highlight.min.js' ),
array(),
filemtime( get_theme_file_path( '/highlight-js/highlight.min.js' ) ),
true
);
// setup.js の読み込み
wp_enqueue_script(
'setup', // ハンドル名(任意の名前)
get_theme_file_uri( '/highlight-js/setup.js' ),
array('highlightJS'), // 依存ファイル(上記 highlight.min.js のハンドル名を指定)
filemtime( get_theme_file_path( '/highlight-js/setup.js' ) ),
true
);
}
add_action( 'wp_enqueue_scripts', 'add_my_hljs_styles_and_scripts' );
CDN を使用する場合
function add_my_hljs_styles_and_scripts() {
if (is_admin()) return;
wp_enqueue_style(
'atom-one-dark',
'//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css',
array(),
NULL
);
wp_enqueue_script(
'highlightJS',
'//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js',
array(),
NULL,
true
);
// setup.js の読み込み
wp_enqueue_script(
'setup', // ハンドル名(任意の名前)
get_theme_file_uri( '/highlight-js/setup.js' ),
array('highlightJS'), // 依存ファイル(上記 highlight.min.js のハンドル名を指定)
filemtime( get_theme_file_path( '/highlight-js/setup.js' ) ),
true
);
}
add_action( 'wp_enqueue_scripts', 'add_my_hljs_styles_and_scripts' );
// integrity 属性などの追加(SRI を使用する場合)
function change_stylesheet_link($html, $handle, $href) {
if (is_admin()) {
return $html;
}
if ($handle === 'atom-one-dark') {
$html = '<link rel="stylesheet" href="' . $href . '" integrity="sha512-Jk4AqjWsdSzSWCSuQTfYRIF84Rq/eV0G2+tu07byYwHcbTGfdmLrHjUSwvzp5HvbiqK4ibmNwdcG49Y5RGYPTg==" crossorigin="anonymous" referrerpolicy="no-referrer">' . "\n";
}
return $html;
}
add_filter('style_loader_tag', 'change_stylesheet_link', 10, 3);
function change_script_tag($tag, $handle, $src) {
if (is_admin()) {
return $tag;
}
if ($handle === 'highlightJS') {
$tag = '<script src="' . $src . '" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>' . "\n";
}
return $tag;
}
add_filter('script_loader_tag', 'change_script_tag', 10, 3);
ハイライト表示するブロックには、必要に応じて(明示的に言語を指定したい場合は)言語名クラス(language-xxxx)を指定します。言語名クラスを省略した場合は、Highlight.js により自動検出されます。
ハイライト表示しないブロックには no-highlight
クラスを指定します。
上記の場合、フロントエンド側では以下のように no-highlight
クラスを指定したコードブロックはハイライト表示されません。
行番号を表示
setup.js に JavaScript で各行の先頭に行番号用の span 要素を追加し、CSS で CSS カウンターを使って行番号を表示することができます。
以下はデフォルトでは行番号を表示し、「高度な設定」の「追加 CSS クラス」で no-line-num
クラスを指定すれば、行番号を表示しないようにする例です。
Highlight.js では addPlugin() を使って独自のプラグインを定義することができます。
※ addPlugin() を使った定義は Highlight.js の初期化の処理の前に記述する必要があります。
setup.js を以下のように変更します。以下では行番号表示のための関数 addLineNumbers を定義して hljs.addPlugin() の after:highlightElement で呼び出しています。
document.addEventListener("DOMContentLoaded", () => {
mySetupHighlightJS();
});
function mySetupHighlightJS() {
// Highlight.js プラグインの定義
hljs.addPlugin({
"after:highlightElement": ({ el, result }) => {
// pre 要素(親要素)を取得して以下の関数の呼び出しで引数に渡す
const pre = el.parentElement;
// 行番号を表示する関数を呼び出す
addLineNumbers(el, result, pre);
},
});
// 行番号表示のための span 要素を追加
function addLineNumbers(el, result, pre) {
// pre 要素に no-line-num クラスが指定されていなければ span 要素を追加
if(pre && !pre.classList.contains("no-line-num")){
el.innerHTML = result.value.replace(
/^/gm,
'<span class="line-num"></span>'
);
}
}
// 以下は前述のコードと同じ
const wrapperClass = "hljs-wrap";
const highlightCodeByDefault = true;
const noHighlightClass = "no-highlight";
const highlightClass = "highlight";
const blockCodeElems = document.getElementsByClassName("wp-block-code");
if(blockCodeElems.length > 0) {
if(highlightCodeByDefault) {
for (const elem of blockCodeElems) {
if(!elem.classList.contains(noHighlightClass)) {
initHighlightJs(elem);
}
}
}else{
for (const elem of blockCodeElems) {
if(elem.classList.contains(highlightClass)) {
initHighlightJs(elem);
}
}
}
}
function initHighlightJs(elem) {
const wrapper = document.createElement("div");
wrapper.className = wrapperClass;
elem.insertAdjacentElement("beforebegin", wrapper);
wrapper.appendChild(elem);
hljs.highlightElement(wrapper.querySelector("code"));
}
}
以下の CSS ファイル(custom.css)を作成して highlight-js フォルダに保存します。
※ pre 要素のパディングの削除も以下の CSS に追加しているので、スタイルを設定で追加した pre 要素のパディングの削除の記述は削除できます。
ラッパー要素のクラス名を変更する場合、簡単に1箇所(2行目のクラスセレクタ)の書き換えで済むように、CSS ネスティングを使用しています。
/* ラッパー要素( CSS ネスティング) */
.hljs-wrap {
/* 起点となる要素でカウントする値を初期化 */
pre {
/* カウンター名を指定 */
counter-reset: lineNumber;
/* ついでにここで pre 要素のパディングを削除 */
padding: 0;
}
/* カウント対象の要素に擬似要素で counter-increment と content プロパティを指定 */
pre span.line-num::before {
/* 対象のカウンター名を指定 */
counter-increment: lineNumber;
/* content プロパティにカウンター名を指定 */
content: counter(lineNumber);
display: inline-block;
/* 行番号の幅(フォント設定などにより調節) */
min-width: 1.5rem;
/* 行番号の色 (以下は好みで調整します) */
color: #5f9168;
/* 行番号とコードの余白 */
margin-right: 5px;
padding-right: 5px;
/* 中央寄せ */
text-align: center;
}
}
CSS ネスティングを使用しない場合
/* 起点となる要素でカウントする値を初期化 */
.hljs-wrap pre {
/* カウンター名を指定 */
counter-reset: lineNumber;
/* ついでにここで pre 要素のパディングを削除 */
padding: 0;
}
/* カウント対象の要素に擬似要素で counter-increment と content プロパティを指定 */
.hljs-wrap pre span.line-num::before {
/* 対象のカウンター名を指定 */
counter-increment: lineNumber;
/* content プロパティにカウンター名を指定 */
content: counter(lineNumber);
display: inline-block;
/* 行番号の幅(フォント設定などにより調節) */
min-width: 1.5rem;
/* 行番号の色 (以下は好みで調整します) */
color: #5f9168;
/* 行番号とコードの余白 */
margin-right: 5px;
padding-right: 5px;
/* 中央寄せ */
text-align: center;
}
functions.php に上記で保存した custom.css の読み込みを追加します。
function add_my_hljs_styles_and_scripts() {
if (is_admin()) return;
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' ) )
);
// custom.css の読み込みを追加
wp_enqueue_style(
'custom',
get_theme_file_uri( '/highlight-js/custom.css' ),
array('atom-one-dark'),
filemtime( get_theme_file_path( '/highlight-js/custom.css' ) )
);
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
);
wp_enqueue_script(
'setup',
get_theme_file_uri( '/highlight-js/setup.js' ),
array('highlightJS'),
filemtime( get_theme_file_path( '/highlight-js/setup.js' ) ),
true
);
}
add_action( 'wp_enqueue_scripts', 'add_my_hljs_styles_and_scripts' );
CDN を使用する場合
function add_my_hljs_styles_and_scripts() {
if (is_admin()) return;
wp_enqueue_style(
'atom-one-dark',
'//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css',
array(),
NULL
);
// custom.css の読み込みを追加
wp_enqueue_style(
'custom',
get_theme_file_uri( '/highlight-js/custom.css' ),
array('atom-one-dark'),
filemtime( get_theme_file_path( '/highlight-js/custom.css' ) )
);
wp_enqueue_script(
'highlightJS',
'//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js',
array(),
NULL,
true
);
wp_enqueue_script(
'setup',
get_theme_file_uri( '/highlight-js/setup.js' ),
array('highlightJS'),
filemtime( get_theme_file_path( '/highlight-js/setup.js' ) ),
true
);
}
add_action( 'wp_enqueue_scripts', 'add_my_hljs_styles_and_scripts' );
// integrity 属性などの追加
function change_stylesheet_link($html, $handle, $href) {
if (is_admin()) {
return $html;
}
if ($handle === 'atom-one-dark') {
$html = '<link rel="stylesheet" href="' . $href . '" integrity="sha512-Jk4AqjWsdSzSWCSuQTfYRIF84Rq/eV0G2+tu07byYwHcbTGfdmLrHjUSwvzp5HvbiqK4ibmNwdcG49Y5RGYPTg==" crossorigin="anonymous" referrerpolicy="no-referrer">' . "\n";
}
return $html;
}
add_filter('style_loader_tag', 'change_stylesheet_link', 10, 3);
function change_script_tag($tag, $handle, $src) {
if (is_admin()) {
return $tag;
}
if ($handle === 'highlightJS') {
$tag = '<script src="' . $src . '" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>' . "\n";
}
return $tag;
}
add_filter('script_loader_tag', 'change_script_tag', 10, 3);
全てのファイルを保存して、フロントエンド側を確認すると、例えば以下のように行番号が表示されます。
行番号を表示しない場合は、高度な設定」の「追加 CSS クラス」に no-line-num
クラスを指定します。
行の折り返し
コードブロックの code 要素にはデフォルトで以下が設定されていて、white-space: pre-wrap
により、行は自動折り返しする設定になっています。
自動折り返ししたくない場合は、例えば、custom.css に以下を追加します。
.wp-block-code code {
white-space: pre;
}
自動折り返しの制御
「高度な設定」の「追加 CSS クラス」を使って、自動折り返しの設定を制御することもできます。
例えば、custom.css に以下を追加すると、デフォルトでは自動折り返しなし、「追加 CSS クラス」に pre-wrap クラスを指定すると自動折り返しするようになります。
以下ではハイライト表示するコードを対象とするように、ラッパー要素の .hljs-wrap
を指定しています。
.hljs-wrap .wp-block-code code {
white-space: pre
}
.hljs-wrap pre.pre-wrap code {
white-space: pre-wrap
}
言語名を表示
Highlight.js の Plugin API を使って、言語クラスで指定した言語名、または自動検出される言語名を表示することもできます(詳細:Plugin API 言語名を表示)。
以下は「高度な設定」の「追加 CSS クラス」で show-lang
クラスを指定すれば、言語名を表示する例です。setup.js を以下のように変更します。
document.addEventListener("DOMContentLoaded", () => {
mySetupHighlightJS();
});
function mySetupHighlightJS() {
// Highlight.js プラグインの定義
hljs.addPlugin({
"after:highlightElement": ({ el, result, text }) => {
const pre = el.parentElement;
// 行番号を表示
addLineNumbers(el, result, pre);
// コピーボタンを表示
copyCode(text, pre);
// 言語名を表示
showLanguage(el, result, pre);
},
});
// 言語名を表示するプラグイン用の関数(デフォルトで言語名を表示しない場合)
function showLanguage(el, result, pre) {
if(result.language && pre.classList.contains("show-lang")) {
el.dataset.language = result.language;
}
}
// 以下は前述のコードと同じ
function copyCode(text, pre) {
if (pre && !pre.classList.contains("no-copy")) {
const copyButton = document.createElement("button");
copyButton.setAttribute("class", "hljs-copy-btn");
copyButton.textContent = "Copy";
pre.after(copyButton);
pre.querySelector("code").classList.add("copy-btn-added");
copyButton.addEventListener("click", () => {
copyToClipboard(copyButton, text);
});
}
function copyToClipboard(btn, text) {
if (!navigator.clipboard) {
alert("Sorry, can not copy");
}
navigator.clipboard.writeText(text).then(
() => {
btn.textContent = "Copied";
resetCopyBtnText(btn, 1500);
},
(error) => {
btn.textContent = "Failed";
resetCopyBtnText(btn, 1500);
console.log(error.message);
}
);
}
function resetCopyBtnText(btn, delay) {
setTimeout(() => {
btn.textContent = "Copy";
}, delay);
}
}
function addLineNumbers(el, result, pre) {
if (pre && !pre.classList.contains("no-line-num")) {
el.innerHTML = result.value.replace(
/^/gm,
'<span class="line-num"></span>'
);
}
}
const wrapperClass = "hljs-wrap";
const highlightCodeByDefault = true;
const noHighlightClass = "no-highlight";
const highlightClass = "highlight";
const blockCodeElems = document.getElementsByClassName("wp-block-code");
if(blockCodeElems.length > 0) {
if(highlightCodeByDefault) {
for (const elem of blockCodeElems) {
if(!elem.classList.contains(noHighlightClass)) {
initHighlightJs(elem);
}
}
}else{
for (const elem of blockCodeElems) {
if(elem.classList.contains(highlightClass)) {
initHighlightJs(elem);
}
}
}
}
function initHighlightJs(elem) {
const wrapper = document.createElement("div");
wrapper.className = wrapperClass;
elem.insertAdjacentElement("beforebegin", wrapper);
wrapper.appendChild(elem);
hljs.highlightElement(wrapper.querySelector("code"));
}
}
デフォルトで言語名を表示する場合は、20〜24行目を以下のように変更します。この場合、show-no-lang クラスを指定すれば言語名を表示しません。
function showLanguage(el, result, pre) {
if(result.language && !pre.classList.contains('show-no-lang')) {
el.dataset.language = result.language;
}
}
上記で追加した関数により、言語名は data-language
属性に出力されるので、code[data-language]::before
で疑似要素を使って表示します。
custom.css に以下の45〜58行目を追加します。
.hljs-wrap {
/* コピーボタンや言語名の絶対配置の基準 */
position: relative;
pre {
counter-reset: lineNumber;
padding: 0;
}
pre span.line-num::before {
counter-increment: lineNumber;
content: counter(lineNumber);
display: inline-block;
min-width: 1.5rem;
color: #5f9168;
margin-right: 5px;
padding-right: 5px;
text-align: center;
}
.wp-block-code code {
white-space: pre;
}
pre.pre-wrap code {
white-space: pre-wrap;
}
.hljs-copy-btn {
position: absolute;
top: 0;
right: 0;
border: 1px solid #474646;
padding: 5px 10px;
background-color: #201e1e;
color: #999;
cursor: pointer;
}
code.copy-btn-added {
padding-top: 2rem;
}
/* 言語名を表示 */
pre code[data-language]::before {
content: attr(data-language);
position: absolute;
top: 0;
left: 0;
color: #aaa;
display: inline-block;
padding: 0.5rem;
}
/* 言語名が表示されている場合はパディングトップを追加 */
pre code[data-language] {
padding-top: 2.5rem;
}
}
例えば、以下のように「高度な設定」の「追加 CSS クラス」で show-lang を指定して言語クラスを指定しない場合は、
自動検出された言語名(全ての小文字の javascript)が表示されます。
言語クラス language-xxxx を指定すると、
言語クラスで指定された言語名(大文字と小文字の JavaScript)が表示されます。
スクロール表示
コードの行が長い場合に、高さを指定して残りの部分をスクロールして見れるようにすることができます。
例えば、高さを 200px として残りをスクロールするようにするには、以下のような pre 要素に h-200 クラスが指定された場合の height と overflow-y を設定した CSS を用意します。
.hljs-wrap pre.h-200 {
height: 200px;
overflow-y: scroll;
}
そして、インスペクターの「高度な設定」→「追加 CSS クラス」で h-200 クラスを指定します。
言語名を表示している場合は、スクロールすると、言語名の背景が透けてしまうので、例えば、以下のように背景色を指定して、幅いっぱいに表示するようにすると良いかもしれません。
/* 言語名を表示 */
.hljs-wrap pre code[data-language]::before {
content: attr(data-language);
position:absolute;
top: 0;
left: 0;
color: #aaa;
display: inline-block;
padding: 0.5rem;
/* 幅いっぱいに表示するため以下を追加 */
right: 0;
/* 背景色を追加 */
background-color: #282c34;
}
ただし、この場合、予め特定の高さのクラスを用意する必要があります。
行数や高さを指定して表示
以下は、表示する行数や高さを指定して、残りの部分をスクロールして見るようにする例です。
この例では行数の指定は、mxl-n(n は行数)と mxh-n(n は高さ px )というクラスを使います。そして JavaScript で n の値を取得して高さを算出してインラインのスタイルを設定します。
以下のように setup.js に setMaxLineHeight() 関数(18-57)とその呼出し(13)を追加します。
document.addEventListener("DOMContentLoaded", () => {
mySetupHighlightJS();
});
function mySetupHighlightJS() {
hljs.addPlugin({
"after:highlightElement": ({ el, result, text }) => {
const pre = el.parentElement;
addLineNumbers(el, result, pre);
copyCode(text, pre);
showLanguage(el, result, pre);
// 指定された行数または高さで表示
setMaxLineHeight(el, pre);
},
});
// 指定された行数または高さで表示する関数
function setMaxLineHeight(el, pre) {
// pre 要素に指定されているクラスを取得
const preClassName = pre.className;
if (preClassName) {
// 正規表現パターンを作成(mxl-数値 | mxh-数値)
const maxLineClassRegex = new RegExp("mxl-(\\d+)|mxh-(\\d+)");
// 行数または高さを指定するクラスにマッチするかどうか
const matched = preClassName.match(maxLineClassRegex);
// マッチする場合は、行数の数値部分 matched[1] または高さの数値部分 matched[2] を取得して高さを設定
if (matched) {
// 各行の先頭の span 要素(.line-num)を全て取得
const lineNumSpans = el.getElementsByClassName("line-num");
// code 要素のスタイル
const elComputedStyle = window.getComputedStyle(el);
// code 要素の垂直方向のパディング
const elPaddingY = parseFloat(elComputedStyle.paddingTop) + parseFloat(elComputedStyle.paddingBottom);
// 行数のパターンにマッチした場合
if (matched[1]) {
const maxLines = parseInt(matched[1]);
// 指定された行数が有効な値であれば
if (lineNumSpans.length > 0 && maxLines < lineNumSpans.length) {
// 指定された行の次の行の行番号の span 要素
const targetLineNumSpan = lineNumSpans[maxLines];
if (targetLineNumSpan) {
// 指定された行の次の行の行番号の span 要素の offsetTop を使って code 要素の高さを設定(** 3 は調整値:必要に応じて)
el.style.setProperty( "height", targetLineNumSpan.offsetTop -elPaddingY + 3 + "px");
el.style.setProperty("overflow-y", "scroll");
}
}
// 高さのパターンにマッチした場合
} else if (matched[2]) {
const maxHeight = parseInt(matched[2]);
if (el.offsetHeight && el.offsetHeight > maxHeight && maxHeight > elPaddingY) {
el.style.setProperty("height", maxHeight -elPaddingY + "px");
el.style.setProperty("overflow-y", "scroll");
}
}
}
}
}
// 以下は前述のコードと同じ
function showLanguage(el, result, pre) {
if(result.language && !pre.classList.contains('show-no-lang')) {
el.dataset.language = result.language;
}
}
function copyCode(text, pre) {
if (pre && !pre.classList.contains("no-copy")) {
const copyButton = document.createElement("button");
copyButton.setAttribute("class", "hljs-copy-btn");
copyButton.textContent = "Copy";
pre.after(copyButton);
pre.querySelector("code").classList.add("copy-btn-added");
copyButton.addEventListener("click", () => {
copyToClipboard(copyButton, text);
});
}
function copyToClipboard(btn, text) {
if (!navigator.clipboard) {
alert("Sorry, can not copy");
}
navigator.clipboard.writeText(text).then(
() => {
btn.textContent = "Copied";
resetCopyBtnText(btn, 1500);
},
(error) => {
btn.textContent = "Failed";
resetCopyBtnText(btn, 1500);
console.log(error.message);
}
);
}
function resetCopyBtnText(btn, delay) {
setTimeout(() => {
btn.textContent = "Copy";
}, delay);
}
}
function addLineNumbers(el, result, pre) {
if (pre && !pre.classList.contains("no-line-num")) {
el.innerHTML = result.value.replace(
/^/gm,
'<span class="line-num"></span>'
);
}
}
const wrapperClass = "hljs-wrap";
const highlightCodeByDefault = true;
const noHighlightClass = "no-highlight";
const highlightClass = "highlight";
const blockCodeElems = document.getElementsByClassName("wp-block-code");
if(blockCodeElems.length > 0) {
if(highlightCodeByDefault) {
for (const elem of blockCodeElems) {
if(!elem.classList.contains(noHighlightClass)) {
initHighlightJs(elem);
}
}
}else{
for (const elem of blockCodeElems) {
if(elem.classList.contains(highlightClass)) {
initHighlightJs(elem);
}
}
}
}
function initHighlightJs(elem) {
const wrapper = document.createElement("div");
wrapper.className = wrapperClass;
elem.insertAdjacentElement("beforebegin", wrapper);
wrapper.appendChild(elem);
hljs.highlightElement(wrapper.querySelector("code"));
}
}
例えば、「高度な設定」→「追加 CSS クラス」で mxl-30 クラスを指定すると、最初の30行分の高さで表示し、mxh-400 と高さ400pxで表示します。
※ mxl-n と mxh-n の両方のクラスが指定されている場合は、最初に指定されているものが適用されます。
また、この機能は行番号の表示で、追加した各行の span 要素(.line-num)を利用するので、「追加 CSS クラス」で no-line-num クラスを指定して行番号を非表示にしている場合は機能しません。
テーマ Twenty Twenty-Four の場合、上記を適用すると pre 要素に角丸(border-radius)が適用されるので、CSS で .hljs-wrap pre に border-radius の設定を追加する必要があるかもしれません。
CSS を表示
.hljs-wrap {
position: relative;
pre {
counter-reset: lineNumber;
padding: 0;
/* 角丸(好みで) */
border-radius: 0;
}
pre span.line-num::before {
counter-increment: lineNumber;
content: counter(lineNumber);
display: inline-block;
min-width: 1.5rem;
color: #5f9168;
margin-right: 5px;
padding-right: 5px;
text-align: center;
}
.wp-block-code code {
white-space: pre;
}
pre.pre-wrap code {
white-space: pre-wrap;
}
.hljs-copy-btn {
position: absolute;
top: 0;
right: 0;
border: 1px solid #474646;
padding: 5px 10px;
background-color: #201e1e;
color: #999;
cursor: pointer;
}
code.copy-btn-added {
padding-top: 2rem;
}
pre code[data-language]::before {
content: attr(data-language);
position:absolute;
top: 0;
left: 0;
color: #aaa;
display: inline-block;
padding: 0.5rem;
/* 幅いっぱいに表示するため以下を追加 */
right: 0;
/* 背景色を追加 */
background-color: #282c34;
}
pre code[data-language] {
padding-top: 2.5rem;
}
}
ツールバーの追加
以下はツールバーを表示して、ユーザーが行の折り返しや行番号の表示・非表示をボタンで切り替えられるようにする例です。また、ツールバーにはコピーボタンや言語名も表示します。
setup.js を以下のように書き換えます。
これまで同様、処理は関数にまとめて DOMContentLoaded を使って呼び出しています。
ツールバーの追加以外はこれまでの内容とほぼ同じです。詳細はコメントを御覧ください。
document.addEventListener('DOMContentLoaded', () => {
mySetupHighlightJS();
});
function mySetupHighlightJS() {
// ツールバーの自動改行ボタンのラベル
const lineWrapLabel = "wrap";
// ツールバーの行番号ボタンのラベル
const lineNumLabel = "number";
// コピーボタンのラベルとメッセージ
const copyBtnLabel = "Copy";
const copyBtnCompleteLabel = "Copied";
const copyBtnFailedLabel = "Failed";
const copyFailedMessage = "Sorry, can not copy with this browser.";
// デフォルトでコードをハイライト表示するかどうか
const highlightByDefault = true;
// デフォルトでハイライト表示する場合に、ハイライトしないコードブロックに指定するクラス名
const noHighlightClass = "no-highlight";
// デフォルトでハイライト表示しない場合に、ハイライトするコードブロックに指定するクラス名
const highlightClass = "highlight";
// デフォルトで行番号を表示するかどうか
const showLineNumByDefault = true;
// 行番号を表示しない場合に指定するクラス名(変更不可)
const noLineNumClass = "no-line-num";
// デフォルトで行番号を表示しない場合に、個別に行番号を表示する場合に指定するクラス名
const showLineNumClass= "show-line-num";
// 行の開始番号を指定するクラスの接頭辞(start-10:開始行番号を10にする)
const lineNumStartPrefix = "start-";
// デフォルトで行を自動折り返しするかどうか
const lineWrapByDefault = true;
// デフォルトでツールバーを表示するかどうか
const showToolbarByDefault = true;
// 個別にツールバーを表示しない場合に指定するクラス名
const noToolbarClass = "no-toolbar";
// 個別にツールバーを表示する場合に指定するクラス名
const showToolbarClass = "show-toolbar";
// デフォルトで言語名を表示するかどうか
const showLangNameByDefault = true;
// 個別に言語名を表示しない場合に指定するクラス名
const noLangClass = "show-no-lang";
// 個別に言語名を表示する場合に指定するクラス名
const showLangClass = "show-lang";
// デフォルトでコピーボタンを表示するかどうか
const showCopyByDefault = true;
// コピーボタンを表示しない場合に指定するクラス名
const noCopyClass = "no-copy";
// 個別にツールバーを表示する場合に指定するクラス名
const showCopyClass = "show-copy";
// 行数を指定して表示する場合に指定するクラス名の接頭辞(数値の前の部分)
const maxLinePrefix = "mxl-";
// 高さ(px)を指定して表示する場合に指定するクラス名の接頭辞(数値の前の部分)
const maxHeightePrefix = "mxh-";
// 行数や高さを指定して表示する場合にコードの下に情報バーを表示するかどうか
const showScrollFooter = true;
// 上記を表示する場合に表示するデフォルトのテキスト
const scrollableText = 'scrollable';
// 情報バーを表示する場合に行数の情報を表示するかどうか(mxh-* を指定した場合)
const showLineNumInfo = true;
// コードブロックのラッパー要素のクラス名(変更不可)
const wrapperClass = "hljs-wrap";
// ツールバーの div 要素のクラス名
const toolbarElemClass = "highlight-toolbar";
/* Highlight.js 独自プラグインの定義 */
hljs.addPlugin({
"after:highlightElement": ({ el, result, text }) => {
// pre 要素(親要素)を取得して関数の呼び出しで引数に追加
const pre = el.parentElement;
if( pre.tagName === 'PRE') {
// 行番号を表示
addLineNumbers(el, result, pre);
// コピーボタンを表示
copyCode(text, pre);
// 言語名を表示
showLanguage(el, result, pre);
// 指定された行数または高さで表示
setMaxLineHeight(el, pre);
}
},
});
// 行番号を表示する関数
function addLineNumbers(el, result, pre) {
el.innerHTML = result.value.replace( /^/gm, '<span class="line-num"></span>');
// pre 要素に指定されているクラス
const preClassName = pre.className;
if (preClassName) {
// 正規表現パターンを作成(start-数値)
const startLineClassRegex = new RegExp(
`${lineNumStartPrefix}(-?\\d+)`
);
// 開始行番号を指定するクラスにマッチするかどうか
const matched = preClassName.match(startLineClassRegex);
// マッチする場合は、数値部分 matched[1] を取得して開始番号を設定
if (matched && matched[1]) {
const startNumber = parseInt(matched[1]);
if (startNumber || startNumber === 0) {
pre.style.setProperty('counter-reset', 'lineNumber ' + (startNumber - 1));
}
}
}
}
// コードをコピーするボタンを追加する関数
function copyCode(text, pre) {
if (showCopyByDefault && !pre.classList.contains(noCopyClass) || pre.classList.contains(showCopyClass)) {
const copyButton = document.createElement("button");
copyButton.setAttribute("class", "hljs-copy-btn");
copyButton.textContent = copyBtnLabel;
pre.after(copyButton);
pre.querySelector("code").classList.add("copy-btn-added");
copyButton.addEventListener("click", () => {
copyToClipboard(copyButton, text);
});
}
function copyToClipboard(btn, text) {
if (!navigator.clipboard) {
alert(copyFailedMessage);
}
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 showLanguage(el, result) {
if (result.language) {
// 設定先を code 要素から pre 要素に変更
el.parentElement.dataset.language = result.language;
}
}
// 指定された行数または高さで表示する関数
function setMaxLineHeight(el, pre) {
// pre 要素に指定されているクラスを取得
const preClassName = pre.className;
if (preClassName) {
// 正規表現パターンを作成(mxl-数値 | mxh-数値)
const maxLineClassRegex = new RegExp(`${maxLinePrefix}(\\d+)|${maxHeightePrefix}(\\d+)`);
// 行数または高さを指定するクラスにマッチするかどうか
const matched = preClassName.match(maxLineClassRegex);
// マッチする場合は、行数の数値部分 matched[1] または高さの数値部分 matched[2] を取得して高さを設定
if (matched) {
// 各行の先頭の span 要素(.line-num)を全て取得
const lineNumSpans = el.getElementsByClassName("line-num");
// code 要素のスタイル
const elComputedStyle = window.getComputedStyle(el);
// code 要素の垂直方向のパディング
const elPaddingY = parseFloat(elComputedStyle.paddingTop) + parseFloat(elComputedStyle.paddingBottom);
// 行数を指定されたかどうかのフラグ
let isMaxLineNum = false;
// 行数のパターンにマッチした場合
if (matched[1]) {
isMaxLineNum = true;
const maxLines = parseInt(matched[1]);
// 指定された行数が有効な値であれば
if (lineNumSpans.length > 0 && maxLines < lineNumSpans.length) {
// 指定された行の次の行の行番号の span 要素
const targetLineNumSpan = lineNumSpans[maxLines];
if (targetLineNumSpan) {
// 指定された行の次の行の行番号の span 要素の offsetTop を使って code 要素の高さを設定(** 3 は調整値:必要に応じて)
el.style.setProperty( "height", targetLineNumSpan.offsetTop -elPaddingY + 3 + "px");
el.style.setProperty("overflow-y", "scroll");
// 情報バーを表示
addScrollFooter(isMaxLineNum, lineNumSpans, maxLines);
}
}
// 高さのパターンにマッチした場合
} else if (matched[2]) {
const maxHeight = parseInt(matched[2]);
if (el.offsetHeight && el.offsetHeight > maxHeight && maxHeight > elPaddingY) {
// ツールバーがない場合の code 要素の高さが maxHeight になる(正確に maxHeight にはならない)
el.style.setProperty("height", maxHeight -elPaddingY + "px");
el.style.setProperty("overflow-y", "scroll");
addScrollFooter();
}
}
}
// コードの下に情報バーを表示する関数
function addScrollFooter(isMaxLineNum, lineNumSpans, maxLines) {
if(showScrollFooter && !pre.classList.contains('no-scroll-footer') || !showScrollFooter && pre.classList.contains('show-scroll-footer')) {
const wrapper = pre.parentElement;
wrapper.insertAdjacentHTML('beforeend', `<div class="scroll-footer"><span class="scroll-footer-text">${scrollableText}</span></div>`);
// 行数(mxh-*)を指定された場合
if(isMaxLineNum) {
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 ${maxLines} of ${lineNumSpans.length} lines</span>`);
}
}
}
}
}
}
/* ここまで Highlight.js 独自プラグインの定義(初期化の前に定義) */
// 全てのコードブロックの要素を取得
const blockCodeElems = document.getElementsByClassName("wp-block-code");
// コードブロックの要素があればコードブロックの要素をラッパーで囲んで初期化
if(blockCodeElems.length > 0) {
// デフォルトでコードをハイライト表示する場合
if(highlightByDefault) {
for (const elem of blockCodeElems) {
if(!elem.classList.contains(noHighlightClass)) {
initHighlightJs(elem);
}
}
}else{
// デフォルトでコードをハイライト表示しない場合
for (const elem of blockCodeElems) {
if(elem.classList.contains(highlightClass)) {
initHighlightJs(elem);
}
}
}
}
// コードブロックの要素をラッパーで囲み Highlight.js で初期化する関数(pre 要素を引数に受け取る)
function initHighlightJs(elem) {
// コードブロックの要素をラッパーで囲む
const wrapper = document.createElement('div');
wrapper.className = wrapperClass;
elem.insertAdjacentElement('beforebegin', wrapper);
wrapper.appendChild(elem);
const pre = wrapper.querySelector("pre");
const code = wrapper.querySelector("code");
if(pre && code) {
// Highlight.js で code 要素を初期化
hljs.highlightElement(code);
// 行番号の表示・非表示の調整
lineNumControl(pre, code);
// 行折り返しの調整
lineWrapControl(pre);
// 言語名の表示・非表示の調整
languageNameControl(pre);
// ツールバーを追加する関数の呼び出し
addToolbar(wrapper, pre, code);
}
}
// 言語名の表示・非表示の調整
function languageNameControl(pre) {
const preClassList = pre.classList;
if( !preClassList.contains(showLangClass) && (!showLangNameByDefault || preClassList.contains(noLangClass))) {
pre.setAttribute('data-language', '');
pre.classList.add('empty-lang-name');
}
}
// 行番号の表示・非表示の調整
function lineNumControl(pre, code) {
if(!showLineNumByDefault && !pre.classList.contains(showLineNumClass) || pre.classList.contains(noLineNumClass) ) {
code.classList.add('hide-line-num');
}
}
// 行折り返しの調整
function lineWrapControl(pre) {
const preClassList = pre.classList;
if(!lineWrapByDefault && !preClassList.contains("pre-wrap")) preClassList.add("pre");
}
// ツールバーを追加する関数
function addToolbar(wrapper, pre, code) {
const preClassList = pre.classList;
if (preClassList.contains(noToolbarClass) || (!showToolbarByDefault && !preClassList.contains(showToolbarClass))) {
return;
}
const toolbar = document.createElement("div");
toolbar.setAttribute("class", toolbarElemClass);
// ツールバーの HTML に行番号と折り返しの切り替えボタンを追加
toolbar.innerHTML = `<button type="button" class="line-num-btn">${lineNumLabel}</button>
<button type="button" class="line-wrap-btn">${lineWrapLabel}</button>`;
wrapper.insertBefore(toolbar, wrapper.firstElementChild);
wrapper.classList.add("toolbar-added");
// 行番号表示が有効かどうか
let showLineNums = showLineNumByDefault;
if (preClassList.contains(noLineNumClass)) showLineNums = false;
if (preClassList.contains(showLineNumClass)) showLineNums = true;
// 行番号表示が有効な場合はボタンに active クラスを追加し、無効な場合は非表示
const lineNumBtn = toolbar.querySelector(".line-num-btn");
if (showLineNums) {
lineNumBtn.classList.add("active");
}
// 行番号表示切り替えボタンにクリックイベントを設定
lineNumBtn.addEventListener("click", () => {
if (showLineNums) {
code.classList.add('hide-line-num');
}else{
code.classList.remove('hide-line-num');
}
showLineNums = !showLineNums;
lineNumBtn.classList.toggle("active");
});
// 自動折り返しが有効かどうか
let enableLineWrap = lineWrapByDefault;
if (preClassList.contains("pre")) enableLineWrap = false;
if (preClassList.contains("pre-wrap")) enableLineWrap = true;
if(!enableLineWrap) preClassList.add("pre");
const lineWrapBtn = toolbar.querySelector(".line-wrap-btn");
// 自動折り返しが有効な場合はボタンに active クラスを追加
if (enableLineWrap) lineWrapBtn.classList.add("active");
// 自動折り返し切り替えボタンにクリックイベントを設定
lineWrapBtn.addEventListener("click", () => {
if (!enableLineWrap) {
resetWhiteSpaceClasses();
preClassList.add("pre-wrap");
} else {
resetWhiteSpaceClasses();
preClassList.add("pre");
}
enableLineWrap = !enableLineWrap;
lineWrapBtn.classList.toggle("active");
});
function resetWhiteSpaceClasses() {
preClassList.remove('pre');
preClassList.remove('pre-wrap');
}
const copyBtn = wrapper.querySelector(".hljs-copy-btn");
// コピーボタンが追加されていれば、ツールバーに移動
if (copyBtn) {
toolbar.appendChild(copyBtn);
}
}
}
行番号の表示・非表示の切り替えは、hide-line-num というクラスの着脱で行っています。CSS で行番号の span 要素を絶対配置にして、クラスの有無で位置を変更することで表示・非表示を切り替えています。
前項の「スクロール表示」とは異なり、no-line-num クラスを指定して行番号を非表示にしている場合でも mxl-n クラスで表示する行数を指定することができます。
mxl-n または mxh-n クラスで行数や高さを指定してする場合、コードの下にスクロール可能であることを示すバーを表示します。mxl-n で行数を指定して表示する場合は、そのバーに行数の情報も表示します。
また、code 要素を行番号の絶対配置の基準とするため position: relative にするので、言語名の表示を code 要素から pre 要素に設定するように変更しています。
CSS
custom.css を以下のように書き換えます。
デフォルトでは、コードブロックの設定と同様、行を折り返すようになっています(デフォルトの折り返しの設定は setup.js の lineWrapByDefault で変更できます)。
個々に折り返さないようにするには、「高度な設定」の「追加 CSS クラス」で pre クラスを指定します。逆にデフォルトで折り返さないようにしている場合は、pre-wrap クラスを指定すれば折り返します。
使用しているテーマのフォントサイズなどにより、ツールバーの高さや文字の大きさなど以下のスタイルを調整する必要があると思います。
.hljs-wrap {
position: relative;
pre {
counter-reset: lineNumber;
/* オーバーフローは隠す */
overflow-x: hidden;
padding: 0;
margin: 0;
border-radius: 0;
}
pre code {
/* 行番号部分の幅を確保 */
padding-left: 3.25rem;
/* 行番号部の絶対配置の基準 */
position: relative;
}
/* span.line-num の擬似要素で行番号を表示 */
pre code span.line-num::before {
counter-increment: lineNumber;
content: counter(lineNumber);
display: inline-block;
/* 絶対配置 */
position: absolute;
left: 0;
/* 行番号の幅(フォント設定などにより調整) */
width: 2.5rem;
/* 行番号の色 (以下は好みで調整) */
color: #5f9168;
/* 行番号とコードの余白 */
margin-right: 5px;
padding-right: 5px;
/* 中央寄せ */
text-align: center;
}
/* 行番号を非表示にする場合 */
pre code.hide-line-num span.line-num::before {
left: -2.5rem;
}
/* 行番号を非表示にする場合 */
pre code.hide-line-num {
margin-left: -2.5rem;
}
/* pre クラスを指定すると折り返しなし */
pre.pre code {
white-space: pre;
}
/* pre-wrap クラスを指定すると自動的に折り返し */
pre.pre-wrap code {
white-space: pre-wrap;
}
/* コピーボタンのスタイル(好みに応じて変更) */
.hljs-copy-btn {
position: absolute;
top: 0;
right: 0;
border: 1px solid #474646;
padding: 5px 10px;
background-color: #201e1e;
color: #999;
cursor: pointer;
z-index: 100;
}
/* ツールバーを表示している場合はコピーボタンの position を変更 */
.highlight-toolbar .hljs-copy-btn {
position: relative;
}
/* コピーボタンが表示されている場合のパディングトップ */
code.copy-btn-added {
padding-top: 2rem;
}
pre.no-toolbar code.copy-btn-added {
padding-top: 1.5rem;
}
/* ツールバーにコピーボタンが表示されている場合のパディングトップ */
&.toolbar-added code.copy-btn-added {
padding-top: 1em;
}
/* 疑似要素と content で言語名を表示(code 要素から pre 要素に変更) */
pre[data-language]::before {
content: attr(data-language);
position: absolute;
top: 0;
left: 0;
right: 0;
color: #aaa;
display: inline-block;
padding: 0.5rem;
pointer-events: none;
z-index: 50;
}
&:not(.toolbar-added) pre[data-language]::before {
background-color: #282c34;
}
pre[data-language],
pre[data-language].no-toolbar.no-copy {
padding-top: 2rem;
}
pre[data-language].no-toolbar {
padding-top: 1rem;
}
&.toolbar-added pre[data-language],
pre[data-language].no-toolbar.no-copy.show-no-lang {
padding-top: 0;
}
pre[data-language].empty-lang-name {
padding-top: 0;
}
/* ツールバー */
.highlight-toolbar {
height: 2.5rem;
background-color: #3a3e4a;
padding-right: 5px;
color: #999;
font-size: 12px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;
}
/* ツールバーと pre 要素の間のマージン */
.highlight-toolbar + pre {
margin-top: 0;
}
/* ツールバーの行番号表示と折り返し切り替えボタン */
.line-num-btn,
.line-wrap-btn {
background-color: transparent;
border: none;
color: #999;
cursor: pointer;
}
/* 行番号表示と折り返し切り替えボタンが有効時の色 */
.line-num-btn.active,
.line-wrap-btn.active {
color: #2799a3;
}
/* 行数を指定して表示する場合にコード下に表示する領域 */
.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");
}
}
functions.php は変わりません(前述の 行番号を表示 と同じです)。
使用例
以下は、「高度な設定」の「追加 CSS クラス」で mxl-20 と pre クラスを指定して、最初の20行を折り返しなしで表示する例です。
また、この例では明示的に言語名を language-JavaScript と指定しています。
上記の場合、以下のように表示されます。
mxl-20 クラスの指定により、最初の20行が表示され、pre クラスを指定しているので、ツールバーのラベル wrap は無効になっています。また、行数を指定して表示しているので左下に表示している行数と全体の行数、右側にスクロール可能であることを示すテキストとアイコンを表示しています。
行番号の表示・非表示と行の折り返しの有効・無効はツールバーの wrap や number をクリックして切り替えることができます。
以下は setup.js でデフォルトの動作を指定する変数とその初期値です。
個々のコードブロックで別途クラスを指定してデフォルトの動作を変更できるものもあります。
setup.js で指定できる初期値
変数 | 初期値 | 説明 |
---|---|---|
highlightByDefault | true | デフォルトでコードをハイライト表示するかどうか。false を指定した場合は、ハイライトするコードブロックごとに highlight クラスを指定 |
showLineNumByDefault | true | デフォルトで行番号を表示するかどうか。ツールバーで表示・非表示を切り替え可能。 |
lineWrapByDefault | true | デフォルトで行を自動折り返しするかどうか。ツールバーで切り替えが可能です。 |
showToolbarByDefault | true | デフォルトでツールバーを表示するかどうか。 |
showLangNameByDefault | true | デフォルトで言語名を表示するかどうか。 |
showCopyByDefault | true | デフォルトでコピーボタンを表示するかどうか。 |
showScrollFooter | true | 行数や高さを指定して表示する場合にコードの下に情報バーを表示するかどうか |
scrollableText | "scrollable" | 情報バーを表示する場合にスクロール可能であることを示すためのテキスト。空文字 "" を指定するとアイコンのみを表示 |
showLineNumInfo | true | 情報バーを表示する場合に行数の情報を表示するかどうか(mxh-* を指定した場合) |
wrapperClass | "hljs-wrap" | コードブロックのラッパー要素のクラス名 |
上記以外にも、ツールバーに表示するラベルや、コードブロックに指定するクラス名なども定義(変更)することができます。
const lineWrapLabel = "wrap"; // ツールバーの自動改行ボタンのラベル
const lineNumLabel = "number"; // ツールバーの行番号ボタンのラベル
const copyBtnLabel = "Copy"; // コピーボタンのラベル
const copyBtnCompleteLabel = "Copied";
const copyBtnFailedLabel = "Failed";
const copyFailedMessage = "Sorry, can not copy with this browser.";
const highlightByDefault = true;
const noHighlightClass = "no-highlight";
const highlightClass = "highlight";
const showLineNumByDefault = true;
const noLineNumClass = "no-line-num";
const showLineNumClass= "show-line-num";
const lineNumStartPrefix = "start-";
const lineWrapByDefault = true;
const showToolbarByDefault = true;
const noToolbarClass = "no-toolbar";
const showToolbarClass = "show-toolbar";
const showLangNameByDefault = true;
const noLangClass = "show-no-lang";
const showLangClass = "show-lang";
const showCopyByDefault = true;
const noCopyClass = "no-copy";
const showCopyClass = "show-copy";
const maxLinePrefix = "mxl-";
const maxHeightePrefix = "mxh-";
const showScrollFooter = true;
const scrollableText = '';
const showLineNumInfo = true;
const wrapperClass = "hljs-wrap";
const toolbarElemClass = "highlight-toolbar";
const accordionClass = "accordion"; // 後述のアコーディオンパネルの機能を追加した場合
コードブロックごとに個別に指定できるクラスには、以下のようなものがあります(setup.js の先頭のあたりで宣言しています)。
「高度な設定」の「追加 CSS クラス」で指定できるクラス
クラス | 説明 |
---|---|
no-highlight | ハイライトしないコードブロックに指定するクラス |
highlight | デフォルトでハイライト表示しない場合に、ハイライトするコードブロックに指定するクラス |
no-line-num | 行番号を表示しない場合に指定するクラス |
show-line-num | デフォルトで行番号を表示しない場合に、個別に行番号を表示する際に指定するクラス |
start- | 行の開始番号を指定するクラス(例:start-10 開始行番号を10にする) |
no-toolbar | ツールバーを表示しない場合に指定するクラス |
show-toolbar | デフォルトでツールバーを表示しない場合に、個別にツールバーを表示する際に指定するクラス |
show-no-lang | 言語名を表示しない場合に指定するクラス |
show-lang | デフォルトで言語名を表示しない場合に、言語名を表示する際に指定するクラス |
no-copy | コピーボタンを表示しない場合に指定するクラス |
show-copy | デフォルトでコピーボタンを表示しない場合に、コピーボタンを表示する際に指定するクラス |
mxl- | 行数を指定して表示する場合に指定するクラス(例:mxl-20 最初の20行を表示) |
mxh- | 高さを指定して表示する場合に指定するクラス(例:mxh-300 300px だけ表示) |
no-scroll-footer | 行数を指定して表示する際に、情報バーを表示しない場合に指定するクラス |
show-scroll-footer | デフォルトで情報バーを表示しない場合に、個別に情報バーを表示する際に指定するクラス |
no-line-info | 情報バーに行番号の情報を表示しない場合に指定するクラス |
show-line-info | デフォルトで情報バーに行番号の情報を表示しない場合に、個別に番号の情報を表示する際に指定するクラス |
行番号に枠線を表示
行番号の右側に枠線を表示する場合の例です。
各行番号の span 要素に border を設定することもできますが、その場合、折り返しが発生した場合に、各要素の高さを更新する必要があるので、枠線用の span 要素を作成しています。
そして、ResizeObserver でラッパーのサイズに変更が発生した場合は、枠線用の span 要素の高さを更新します(112-150)。その際に、行数が指定されている場合の処理が必要なので、行数が指定されている場合は208行目で、pre 要素に max-lines クラスを追加するようにします。
また、ツールバーの折り返しの切り替えで高さが変わる場合も枠線用の span 要素の高さを更新します(382-389)。
document.addEventListener('DOMContentLoaded', () => {
mySetupHighlightJS();
});
function mySetupHighlightJS() {
// ツールバーの自動改行ボタンのラベル
const lineWrapLabel = "wrap";
// ツールバーの行番号ボタンのラベル
const lineNumLabel = "number";
// コピーボタンのラベルとメッセージ
const copyBtnLabel = "Copy";
const copyBtnCompleteLabel = "Copied";
const copyBtnFailedLabel = "Failed";
const copyFailedMessage = "Sorry, can not copy with this browser.";
// デフォルトでコードをハイライト表示するかどうか
const highlightByDefault = true;
// デフォルトでハイライト表示する場合に、ハイライトしないコードブロックに指定するクラス名
const noHighlightClass = "no-highlight";
// デフォルトでハイライト表示しない場合に、ハイライトするコードブロックに指定するクラス名
const highlightClass = "highlight";
// デフォルトで行番号を表示するかどうか
const showLineNumByDefault = true;
// 行番号を表示しない場合に指定するクラス名(変更不可)
const noLineNumClass = "no-line-num";
// デフォルトで行番号を表示しない場合に、個別に行番号を表示する場合に指定するクラス名
const showLineNumClass= "show-line-num";
// 行の開始番号を指定するクラスの接頭辞(start-10:開始行番号を10にする)
const lineNumStartPrefix = "start-";
// デフォルトで行を自動折り返しするかどうか
const lineWrapByDefault = true;
// デフォルトでツールバーを表示するかどうか
const showToolbarByDefault = true;
// 個別にツールバーを表示しない場合に指定するクラス名
const noToolbarClass = "no-toolbar";
// 個別にツールバーを表示する場合に指定するクラス名
const showToolbarClass = "show-toolbar";
// デフォルトで言語名を表示するかどうか
const showLangNameByDefault = true;
// 個別に言語名を表示しない場合に指定するクラス名
const noLangClass = "show-no-lang";
// 個別に言語名を表示する場合に指定するクラス名
const showLangClass = "show-lang";
// デフォルトでコピーボタンを表示するかどうか
const showCopyByDefault = true;
// コピーボタンを表示しない場合に指定するクラス名
const noCopyClass = "no-copy";
// 個別にツールバーを表示する場合に指定するクラス名
const showCopyClass = "show-copy";
// 行数を指定して表示する場合に指定するクラス名の接頭辞(数値の前の部分)
const maxLinePrefix = "mxl-";
// 高さ(px)を指定して表示する場合に指定するクラス名の接頭辞(数値の前の部分)
const maxHeightePrefix = "mxh-";
// 行数や高さを指定して表示する場合にコードの下に情報バーを表示するかどうか
const showScrollFooter = true;
// 上記を表示する場合に表示するデフォルトのテキスト
const scrollableText = 'scrollable';
// 情報バーを表示する場合に行数の情報を表示するかどうか(mxh-* を指定した場合)
const showLineNumInfo = true;
// コードブロックのラッパー要素のクラス名(変更不可)
const wrapperClass = "hljs-wrap";
// ツールバーの div 要素のクラス名
const toolbarElemClass = "highlight-toolbar";
/* Highlight.js 独自プラグインの定義 */
hljs.addPlugin({
"after:highlightElement": ({ el, result, text }) => {
// pre 要素(親要素)を取得して関数の呼び出しで引数に追加
const pre = el.parentElement;
if( pre.tagName === 'PRE') {
// 行番号を表示
addLineNumbers(el, result, pre);
// コピーボタンを表示
copyCode(text, pre);
// 言語名を表示
showLanguage(el, result, pre);
// 指定された行数または高さで表示
setMaxLineHeight(el, pre);
}
},
});
// 行番号を表示する関数
function addLineNumbers(el, result, pre) {
el.innerHTML = result.value.replace( /^/gm, '<span class="line-num"></span>');
// pre 要素に指定されているクラス
const preClassName = pre.className;
if (preClassName) {
// 正規表現パターンを作成(start-数値)
const startLineClassRegex = new RegExp(
`${lineNumStartPrefix}(-?\\d+)`
);
// 開始行番号を指定するクラスにマッチするかどうか
const matched = preClassName.match(startLineClassRegex);
// マッチする場合は、数値部分 matched[1] を取得して開始番号を設定
if (matched && matched[1]) {
const startNumber = parseInt(matched[1]);
if (startNumber || startNumber === 0) {
pre.style.setProperty('counter-reset', 'lineNumber ' + (startNumber - 1));
}
}
}
// ★★★ 追加 ★★★
// ラッパーの要素を取得
const wrapper = el.closest('.' + wrapperClass);
// ラッパーの高さを取得
const wrapperHeight = wrapper.offsetHeight;
// 枠線用の span 要素を作成
const borderSpan = document.createElement('span');
// 枠線用の span 要素にクラスを設定
borderSpan.classList.add('hljs-border-span');
// 枠線用の span 要素を code 要素(el)に追加
el.appendChild(borderSpan);
// 枠線用の span 要素に position: absolute を設定
borderSpan.style.setProperty('position', 'absolute');
// 枠線用の span 要素にラッパーの高さを設定
borderSpan.style.setProperty('height', wrapperHeight + 'px');
// code 要素のスタイルを取得
const elComputedStyle = window.getComputedStyle(el);
// code 要素の paddingBottom
const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
// ResizeObserver でラッパーのサイズを監視して枠線用の span 要素の高さを更新
const myObserver = new ResizeObserver((entries) => {
if (entries.length > 0 && entries[0]) {
const entry = entries[0];
// contentBoxSize が使えればその値を、そうでなければ contentRect の値を使用
const newHeight = entry.contentBoxSize ? entry.contentBoxSize[0].blockSize : entry.contentRect.height;
// 枠線用の span 要素にラッパーのサイズ変更後の高さを設定
borderSpan.style.setProperty('height', newHeight + 'px');
// pre 要素に max-lines クラスが指定されていれば
if(pre.classList.contains('max-lines')) {
// 行番号の最後の span 要素から高さを算出して枠線用の span 要素に設定
const lineNumSpans = el.getElementsByClassName("line-num");
if(lineNumSpans[lineNumSpans.length-1]) {
const lastLineNumTop = lineNumSpans[lineNumSpans.length-1].offsetTop;
const lastLineNumHeight = lineNumSpans[lineNumSpans.length-1].offsetHeight;
borderSpan.style.setProperty('height', lastLineNumTop + lastLineNumHeight + elPaddingBottom + 'px');
}
}
}
});
myObserver.observe(wrapper);
// ★★★ ここまで ★★★
}
// コードをコピーするボタンを追加する関数
function copyCode(text, pre) {
if (showCopyByDefault && !pre.classList.contains(noCopyClass) || pre.classList.contains(showCopyClass)) {
const copyButton = document.createElement("button");
copyButton.setAttribute("class", "hljs-copy-btn");
copyButton.textContent = copyBtnLabel;
pre.after(copyButton);
pre.querySelector("code").classList.add("copy-btn-added");
copyButton.addEventListener("click", () => {
copyToClipboard(copyButton, text);
});
}
function copyToClipboard(btn, text) {
if (!navigator.clipboard) {
alert(copyFailedMessage);
}
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 showLanguage(el, result) {
if (result.language) {
// 設定先を code 要素から pre 要素に変更
el.parentElement.dataset.language = result.language;
}
}
// 指定された行数または高さで表示する関数
function setMaxLineHeight(el, pre) {
// pre 要素に指定されているクラスを取得
const preClassName = pre.className;
if (preClassName) {
// 正規表現パターンを作成(mxl-数値 | mxh-数値)
const maxLineClassRegex = new RegExp(`${maxLinePrefix}(\\d+)|${maxHeightePrefix}(\\d+)`);
// 行数または高さを指定するクラスにマッチするかどうか
const matched = preClassName.match(maxLineClassRegex);
// マッチする場合は、行数の数値部分 matched[1] または高さの数値部分 matched[2] を取得して高さを設定
if (matched) {
pre.classList.add('max-lines'); // ★★★ 追加 ★★★
// 各行の先頭の span 要素(.line-num)を全て取得
const lineNumSpans = el.getElementsByClassName("line-num");
// code 要素のスタイル
const elComputedStyle = window.getComputedStyle(el);
// code 要素の垂直方向のパディング
const elPaddingY = parseFloat(elComputedStyle.paddingTop) + parseFloat(elComputedStyle.paddingBottom);
// 行数を指定されたかどうかのフラグ
let isMaxLineNum = false;
// 行数のパターンにマッチした場合
if (matched[1]) {
isMaxLineNum = true;
const maxLines = parseInt(matched[1]);
// 指定された行数が有効な値であれば
if (lineNumSpans.length > 0 && maxLines < lineNumSpans.length) {
// 指定された行の次の行の行番号の span 要素
const targetLineNumSpan = lineNumSpans[maxLines];
if (targetLineNumSpan) {
// 指定された行の次の行の行番号の span 要素の offsetTop を使って code 要素の高さを設定(** 3 は調整値:必要に応じて)
el.style.setProperty( "height", targetLineNumSpan.offsetTop -elPaddingY + 3 + "px");
el.style.setProperty("overflow-y", "scroll");
// 情報バーを表示
addScrollFooter(isMaxLineNum, lineNumSpans, maxLines);
}
}
// 高さのパターンにマッチした場合
} else if (matched[2]) {
const maxHeight = parseInt(matched[2]);
if (el.offsetHeight && el.offsetHeight > maxHeight && maxHeight > elPaddingY) {
// ツールバーがない場合の code 要素の高さが maxHeight になる(正確に maxHeight にはならない)
el.style.setProperty("height", maxHeight -elPaddingY + "px");
el.style.setProperty("overflow-y", "scroll");
addScrollFooter();
}
}
}
// コードの下に情報バーを表示する関数
function addScrollFooter(isMaxLineNum, lineNumSpans, maxLines) {
if(showScrollFooter && !pre.classList.contains('no-scroll-footer') || !showScrollFooter && pre.classList.contains('show-scroll-footer')) {
const wrapper = pre.parentElement;
wrapper.insertAdjacentHTML('beforeend', `<div class="scroll-footer"><span class="scroll-footer-text">${scrollableText}</span></div>`);
// 行数(mxh-*)を指定された場合
if(isMaxLineNum) {
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 ${maxLines} of ${lineNumSpans.length} lines</span>`);
}
}
}
}
}
}
/* ここまで Highlight.js 独自プラグインの定義(初期化の前に定義) */
// 全てのコードブロックの要素を取得
const blockCodeElems = document.getElementsByClassName("wp-block-code");
// コードブロックの要素があればコードブロックの要素をラッパーで囲んで初期化
if(blockCodeElems.length > 0) {
// デフォルトでコードをハイライト表示する場合
if(highlightByDefault) {
for (const elem of blockCodeElems) {
if(!elem.classList.contains(noHighlightClass)) {
initHighlightJs(elem);
}
}
}else{
// デフォルトでコードをハイライト表示しない場合
for (const elem of blockCodeElems) {
if(elem.classList.contains(highlightClass)) {
initHighlightJs(elem);
}
}
}
}
// コードブロックの要素をラッパーで囲み Highlight.js で初期化する関数(pre 要素を引数に受け取る)
function initHighlightJs(elem) {
// コードブロックの要素をラッパーで囲む
const wrapper = document.createElement('div');
wrapper.className = wrapperClass;
elem.insertAdjacentElement('beforebegin', wrapper);
wrapper.appendChild(elem);
const pre = wrapper.querySelector("pre");
const code = wrapper.querySelector("code");
if(pre && code) {
// Highlight.js で code 要素を初期化
hljs.highlightElement(code);
// 行番号の表示・非表示の調整
lineNumControl(pre, code);
// 行折り返しの調整
lineWrapControl(pre);
// 言語名の表示・非表示の調整
languageNameControl(pre);
// ツールバーを追加する関数の呼び出し
addToolbar(wrapper, pre, code);
}
}
// 言語名の表示・非表示の調整
function languageNameControl(pre) {
const preClassList = pre.classList;
if( !preClassList.contains(showLangClass) && (!showLangNameByDefault || preClassList.contains(noLangClass))) {
pre.setAttribute('data-language', '');
pre.classList.add('empty-lang-name');
}
}
// 行番号の表示・非表示の調整
function lineNumControl(pre, code) {
if(!showLineNumByDefault && !pre.classList.contains(showLineNumClass) || pre.classList.contains(noLineNumClass) ) {
code.classList.add('hide-line-num');
}
}
// 行折り返しの調整
function lineWrapControl(pre) {
const preClassList = pre.classList;
if(!lineWrapByDefault && !preClassList.contains("pre-wrap")) preClassList.add("pre");
}
// ツールバーを追加する関数
function addToolbar(wrapper, pre, code) {
const preClassList = pre.classList;
if (preClassList.contains(noToolbarClass) || (!showToolbarByDefault && !preClassList.contains(showToolbarClass))) {
return;
}
const toolbar = document.createElement("div");
toolbar.setAttribute("class", toolbarElemClass);
// ツールバーの HTML に行番号と折り返しの切り替えボタンを追加
toolbar.innerHTML = `<button type="button" class="line-num-btn">${lineNumLabel}</button>
<button type="button" class="line-wrap-btn">${lineWrapLabel}</button>`;
wrapper.insertBefore(toolbar, wrapper.firstElementChild);
wrapper.classList.add("toolbar-added");
// 行番号表示が有効かどうか
let showLineNums = showLineNumByDefault;
if (preClassList.contains(noLineNumClass)) showLineNums = false;
if (preClassList.contains(showLineNumClass)) showLineNums = true;
// 行番号表示が有効な場合はボタンに active クラスを追加し、無効な場合は非表示
const lineNumBtn = toolbar.querySelector(".line-num-btn");
if (showLineNums) {
lineNumBtn.classList.add("active");
}
// 行番号表示切り替えボタンにクリックイベントを設定
lineNumBtn.addEventListener("click", () => {
if (showLineNums) {
code.classList.add('hide-line-num');
}else{
code.classList.remove('hide-line-num');
}
showLineNums = !showLineNums;
lineNumBtn.classList.toggle("active");
});
// 自動折り返しが有効かどうか
let enableLineWrap = lineWrapByDefault;
if (preClassList.contains("pre")) enableLineWrap = false;
if (preClassList.contains("pre-wrap")) enableLineWrap = true;
if(!enableLineWrap) preClassList.add("pre");
const lineWrapBtn = toolbar.querySelector(".line-wrap-btn");
// 自動折り返しが有効な場合はボタンに active クラスを追加
if (enableLineWrap) lineWrapBtn.classList.add("active");
// 自動折り返し切り替えボタンにクリックイベントを設定
lineWrapBtn.addEventListener("click", () => {
if (!enableLineWrap) {
resetWhiteSpaceClasses();
preClassList.add("pre-wrap");
} else {
resetWhiteSpaceClasses();
preClassList.add("pre");
}
enableLineWrap = !enableLineWrap;
lineWrapBtn.classList.toggle("active");
// ★★★ 追加 ★★★
const lineNumSpans = code.getElementsByClassName("line-num");
const lineNumSpansLength = lineNumSpans.length;
const lastLineNumSpan = lineNumSpans[lineNumSpansLength-1];
const borderSpan = code.querySelector('.hljs-border-span');
const codePaddingBottom = parseFloat(window.getComputedStyle(code).paddingBottom);
if(lastLineNumSpan) {
borderSpan.style.setProperty('height', lastLineNumSpan.offsetTop + lastLineNumSpan.offsetHeight + codePaddingBottom + 'px')
}
// ★★★ ここまで ★★★
});
function resetWhiteSpaceClasses() {
preClassList.remove('pre');
preClassList.remove('pre-wrap');
}
const copyBtn = wrapper.querySelector(".hljs-copy-btn");
// コピーボタンが追加されていれば、ツールバーに移動
if (copyBtn) {
toolbar.appendChild(copyBtn);
}
}
}
関連ページ:Highlight.js でシンタックスハイライト(行番号を表示)
以下が CSS です。
行番号の幅などは、環境により調整する必要があるかと思います。
.hljs-wrap {
position: relative;
pre {
counter-reset: lineNumber;
/* オーバーフローは隠す */
overflow-x: hidden;
padding: 0;
margin: 0;
border-radius: 0;
}
pre code {
/* 行番号部分の幅を確保 */
padding-left: 3.25rem;
/* 行番号部の絶対配置の基準 */
position: relative;
/* ★★★ オーバーフローは隠す(追加)★★★ */
overflow-y: hidden;
}
/* span.line-num の擬似要素で行番号を表示 */
pre code span.line-num::before {
counter-increment: lineNumber;
content: counter(lineNumber);
display: inline-block;
/* 絶対配置 */
position: absolute;
left: 0;
/* 行番号の幅(フォント設定などにより調整) */
width: 2.5rem;
/* 行番号の色 (以下は好みで調整) */
color: #5f9168;
/* 行番号とコードの余白 */
margin-right: 5px;
padding-right: 5px;
/* 中央寄せ */
text-align: center;
}
/* 行番号を非表示にする場合 */
pre code.hide-line-num span.line-num::before {
left: -2.5rem;
}
/* 行番号を非表示にする場合 */
pre code.hide-line-num {
margin-left: -2.5rem;
}
/* ★★★ 枠線用の span 要素(追加) ★★★ */
code .hljs-border-span {
background-color: transparent;
/* 行番号(span.line-num::before)の幅と合わせる */
width: 2.5rem;
border-right: 1px solid #3d435b;
position: absolute;
top: 0;
left: 0;
}
/* ★★★ 枠線用の span 要素(追加) ★★★ */
code.hide-line-num .hljs-border-span {
border-right: none;
}
/* pre クラスを指定すると折り返しなし */
pre.pre code {
white-space: pre;
}
/* pre-wrap クラスを指定すると自動的に折り返し */
pre.pre-wrap code {
white-space: pre-wrap;
}
/* コピーボタンのスタイル(好みに応じて変更) */
.hljs-copy-btn {
position: absolute;
top: 0;
right: 0;
border: 1px solid #474646;
padding: 5px 10px;
background-color: #201e1e;
color: #999;
cursor: pointer;
z-index: 100;
}
/* ツールバーを表示している場合はコピーボタンの position を変更 */
.highlight-toolbar .hljs-copy-btn {
position: relative;
}
/* コピーボタンが表示されている場合のパディングトップ */
code.copy-btn-added {
padding-top: 2rem;
}
pre.no-toolbar code.copy-btn-added {
padding-top: 1.5rem;
}
/* ツールバーにコピーボタンが表示されている場合のパディングトップ */
&.toolbar-added code.copy-btn-added {
padding-top: 1em;
}
/* 疑似要素と content で言語名を表示(code 要素から pre 要素に変更) */
pre[data-language]::before {
content: attr(data-language);
position: absolute;
top: 0;
left: 0;
right: 0;
color: #aaa;
display: inline-block;
padding: 0.5rem;
pointer-events: none;
z-index: 50;
}
&:not(.toolbar-added) pre[data-language]::before {
background-color: #282c34;
}
pre[data-language],
pre[data-language].no-toolbar.no-copy {
padding-top: 2rem;
}
pre[data-language].no-toolbar {
padding-top: 1rem;
}
&.toolbar-added pre[data-language],
pre[data-language].no-toolbar.no-copy.show-no-lang {
padding-top: 0;
}
pre[data-language].empty-lang-name {
padding-top: 0;
}
/* ツールバー */
.highlight-toolbar {
height: 2.5rem;
background-color: #3a3e4a;
padding-right: 5px;
color: #999;
font-size: 12px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;
}
/* ツールバーと pre 要素の間のマージン */
.highlight-toolbar + pre {
margin-top: 0;
}
/* ツールバーの行番号表示と折り返し切り替えボタン */
.line-num-btn,
.line-wrap-btn {
background-color: transparent;
border: none;
color: #999;
cursor: pointer;
}
/* 行番号表示と折り返し切り替えボタンが有効時の色 */
.line-num-btn.active,
.line-wrap-btn.active {
color: #2799a3;
}
/* 行数を指定して表示する場合にコード下に表示する領域 */
.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");
}
}
例えば、行を折り返す場合、以下のように表示されます。
行を折り返さない場合は、以下のように表示されます。
ファイル名の表示
コードブロックとは別に、段落ブロックを使ってファイル名を表示する例です。
段落ブロックを使ってファイル名を入力し、「高度な設定」の「追加 CSS クラス」にファイル名用のクラスを指定します。
custom.css にファイル名のスタイル及び、コードブロックとのマージンを隣接(兄弟)セレクタ +
を使って設定します。以下ではファイル名に指定するクラスを .file-name としています。
/* ファイル名のスタイル(好みに合わせて) */
.file-name {
color: #777;
/* ファイル名とブロックの間のマージン */
margin-bottom: 0;
margin-top: 2rem;
font-size: 1.1rem;
font-family:'Courier New', Courier, monospace;
}
/* ファイル名とブロックの間のマージン */
.file-name + .hljs-wrap {
margin-top: 0;
}
例えば、以下のように表示されます。
言語名と入れ替える
段落ブロックと CSS でファイル名を表示できますが、ファイル名の位置をコードブロックの右上に表示したり、段落ブロックで入力したファイル名を言語名の位置に表示するなどは難しいです。
以下は段落ブロックで入力したファイル名をコードブロックのラッパー内に移動させて、必要に応じて言語名の位置に表示できるようにする例です。
コードブロックの直前の段落ブロックに file-name
クラスが指定されていれば、コードブロックのラッパーに追加します。段落ブロックに file-name
クラスに加えて、replace
クラスが指定されていれば、言語名を非表示にして、言語名の位置にファイル名を表示します。
直前の段落ブロックにいずれのクラスも指定されていなければ何もしません。
setup.js にファイル名をラッパー内に配置する関数 addFileName() の定義と呼び出しを追加します。
document.addEventListener('DOMContentLoaded', () => {
mySetupHighlightJS();
});
function mySetupHighlightJS() {
// ツールバーの自動改行ボタンのラベル
const lineWrapLabel = "wrap";
// ツールバーの行番号ボタンのラベル
const lineNumLabel = "number";
// コピーボタンのラベルとメッセージ
const copyBtnLabel = "Copy";
const copyBtnCompleteLabel = "Copied";
const copyBtnFailedLabel = "Failed";
const copyFailedMessage = "Sorry, can not copy with this browser.";
// デフォルトでコードをハイライト表示するかどうか
const highlightByDefault = true;
// デフォルトでハイライト表示する場合に、ハイライトしないコードブロックに指定するクラス名
const noHighlightClass = "no-highlight";
// デフォルトでハイライト表示しない場合に、ハイライトするコードブロックに指定するクラス名
const highlightClass = "highlight";
// デフォルトで行番号を表示するかどうか
const showLineNumByDefault = true;
// 行番号を表示しない場合に指定するクラス名(変更不可)
const noLineNumClass = "no-line-num";
// デフォルトで行番号を表示しない場合に、個別に行番号を表示する場合に指定するクラス名
const showLineNumClass= "show-line-num";
// 行の開始番号を指定するクラスの接頭辞(start-10:開始行番号を10にする)
const lineNumStartPrefix = "start-";
// デフォルトで行を自動折り返しするかどうか
const lineWrapByDefault = true;
// デフォルトでツールバーを表示するかどうか
const showToolbarByDefault = true;
// 個別にツールバーを表示しない場合に指定するクラス名
const noToolbarClass = "no-toolbar";
// 個別にツールバーを表示する場合に指定するクラス名
const showToolbarClass = "show-toolbar";
// デフォルトで言語名を表示するかどうか
const showLangNameByDefault = true;
// 個別に言語名を表示しない場合に指定するクラス名
const noLangClass = "show-no-lang";
// 個別に言語名を表示する場合に指定するクラス名
const showLangClass = "show-lang";
// デフォルトでコピーボタンを表示するかどうか
const showCopyByDefault = true;
// コピーボタンを表示しない場合に指定するクラス名
const noCopyClass = "no-copy";
// 個別にツールバーを表示する場合に指定するクラス名
const showCopyClass = "show-copy";
// 行数を指定して表示する場合に指定するクラス名の接頭辞(数値の前の部分)
const maxLinePrefix = "mxl-";
// 高さ(px)を指定して表示する場合に指定するクラス名の接頭辞(数値の前の部分)
const maxHeightePrefix = "mxh-";
// 行数や高さを指定して表示する場合にコードの下に情報バーを表示するかどうか
const showScrollFooter = true;
// 上記を表示する場合に表示するデフォルトのテキスト
const scrollableText = 'scrollable';
// 情報バーを表示する場合に行数の情報を表示するかどうか(mxh-* を指定した場合)
const showLineNumInfo = true;
// コードブロックのラッパー要素のクラス名(変更不可)
const wrapperClass = "hljs-wrap";
// ツールバーの div 要素のクラス名
const toolbarElemClass = "highlight-toolbar";
/* Highlight.js 独自プラグインの定義 */
hljs.addPlugin({
"after:highlightElement": ({ el, result, text }) => {
// pre 要素(親要素)を取得して関数の呼び出しで引数に追加
const pre = el.parentElement;
if( pre.tagName === 'PRE') {
// 行番号を表示
addLineNumbers(el, result, pre);
// コピーボタンを表示
copyCode(text, pre);
// 言語名を表示
showLanguage(el, result, pre);
// 指定された行数または高さで表示
setMaxLineHeight(el, pre);
}
},
});
// 行番号を表示する関数
function addLineNumbers(el, result, pre) {
el.innerHTML = result.value.replace( /^/gm, '<span class="line-num"></span>');
// pre 要素に指定されているクラス
const preClassName = pre.className;
if (preClassName) {
// 正規表現パターンを作成(start-数値)
const startLineClassRegex = new RegExp(
`${lineNumStartPrefix}(-?\\d+)`
);
// 開始行番号を指定するクラスにマッチするかどうか
const matched = preClassName.match(startLineClassRegex);
// マッチする場合は、数値部分 matched[1] を取得して開始番号を設定
if (matched && matched[1]) {
const startNumber = parseInt(matched[1]);
if (startNumber || startNumber === 0) {
pre.style.setProperty('counter-reset', 'lineNumber ' + (startNumber - 1));
}
}
}
// ★★★ 追加 ★★★
// ラッパーの要素を取得
const wrapper = el.closest('.' + wrapperClass);
// ラッパーの高さを取得
const wrapperHeight = wrapper.offsetHeight;
// 枠線用の span 要素を作成
const borderSpan = document.createElement('span');
// 枠線用の span 要素にクラスを設定
borderSpan.classList.add('hljs-border-span');
// 枠線用の span 要素を code 要素(el)に追加
el.appendChild(borderSpan);
// 枠線用の span 要素に position: absolute を設定
borderSpan.style.setProperty('position', 'absolute');
// 枠線用の span 要素にラッパーの高さを設定
borderSpan.style.setProperty('height', wrapperHeight + 'px');
// code 要素のスタイルを取得
const elComputedStyle = window.getComputedStyle(el);
// code 要素の paddingBottom
const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
// ResizeObserver でラッパーのサイズを監視して枠線用の span 要素の高さを更新
const myObserver = new ResizeObserver((entries) => {
if (entries.length > 0 && entries[0]) {
const entry = entries[0];
// contentBoxSize が使えればその値を、そうでなければ contentRect の値を使用
const newHeight = entry.contentBoxSize ? entry.contentBoxSize[0].blockSize : entry.contentRect.height;
// 枠線用の span 要素にラッパーのサイズ変更後の高さを設定
borderSpan.style.setProperty('height', newHeight + 'px');
// pre 要素に max-lines クラスが指定されていれば
if(pre.classList.contains('max-lines')) {
// 行番号の最後の span 要素から高さを算出して枠線用の span 要素に設定
const lineNumSpans = el.getElementsByClassName("line-num");
if(lineNumSpans[lineNumSpans.length-1]) {
const lastLineNumTop = lineNumSpans[lineNumSpans.length-1].offsetTop;
const lastLineNumHeight = lineNumSpans[lineNumSpans.length-1].offsetHeight;
borderSpan.style.setProperty('height', lastLineNumTop + lastLineNumHeight + elPaddingBottom + 'px');
}
}
}
});
myObserver.observe(wrapper);
// ★★★ ここまで ★★★
}
// コードをコピーするボタンを追加する関数
function copyCode(text, pre) {
if (showCopyByDefault && !pre.classList.contains(noCopyClass) || pre.classList.contains(showCopyClass)) {
const copyButton = document.createElement("button");
copyButton.setAttribute("class", "hljs-copy-btn");
copyButton.textContent = copyBtnLabel;
pre.after(copyButton);
pre.querySelector("code").classList.add("copy-btn-added");
copyButton.addEventListener("click", () => {
copyToClipboard(copyButton, text);
});
}
function copyToClipboard(btn, text) {
if (!navigator.clipboard) {
alert(copyFailedMessage);
}
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 showLanguage(el, result) {
if (result.language) {
// 設定先を code 要素から pre 要素に変更
el.parentElement.dataset.language = result.language;
}
}
// 指定された行数または高さで表示する関数
function setMaxLineHeight(el, pre) {
// pre 要素に指定されているクラスを取得
const preClassName = pre.className;
if (preClassName) {
// 正規表現パターンを作成(mxl-数値 | mxh-数値)
const maxLineClassRegex = new RegExp(`${maxLinePrefix}(\\d+)|${maxHeightePrefix}(\\d+)`);
// 行数または高さを指定するクラスにマッチするかどうか
const matched = preClassName.match(maxLineClassRegex);
// マッチする場合は、行数の数値部分 matched[1] または高さの数値部分 matched[2] を取得して高さを設定
if (matched) {
pre.classList.add('max-lines'); // ★★★ 追加 ★★★
// 各行の先頭の span 要素(.line-num)を全て取得
const lineNumSpans = el.getElementsByClassName("line-num");
// code 要素のスタイル
const elComputedStyle = window.getComputedStyle(el);
// code 要素の垂直方向のパディング
const elPaddingY = parseFloat(elComputedStyle.paddingTop) + parseFloat(elComputedStyle.paddingBottom);
// 行数を指定されたかどうかのフラグ
let isMaxLineNum = false;
// 行数のパターンにマッチした場合
if (matched[1]) {
isMaxLineNum = true;
const maxLines = parseInt(matched[1]);
// 指定された行数が有効な値であれば
if (lineNumSpans.length > 0 && maxLines < lineNumSpans.length) {
// 指定された行の次の行の行番号の span 要素
const targetLineNumSpan = lineNumSpans[maxLines];
if (targetLineNumSpan) {
// 指定された行の次の行の行番号の span 要素の offsetTop を使って code 要素の高さを設定(** 3 は調整値:必要に応じて)
el.style.setProperty( "height", targetLineNumSpan.offsetTop -elPaddingY + 3 + "px");
el.style.setProperty("overflow-y", "scroll");
// 情報バーを表示
addScrollFooter(isMaxLineNum, lineNumSpans, maxLines);
}
}
// 高さのパターンにマッチした場合
} else if (matched[2]) {
const maxHeight = parseInt(matched[2]);
if (el.offsetHeight && el.offsetHeight > maxHeight && maxHeight > elPaddingY) {
// ツールバーがない場合の code 要素の高さが maxHeight になる(正確に maxHeight にはならない)
el.style.setProperty("height", maxHeight -elPaddingY + "px");
el.style.setProperty("overflow-y", "scroll");
addScrollFooter();
}
}
}
// コードの下に情報バーを表示する関数
function addScrollFooter(isMaxLineNum, lineNumSpans, maxLines) {
if(showScrollFooter && !pre.classList.contains('no-scroll-footer') || !showScrollFooter && pre.classList.contains('show-scroll-footer')) {
const wrapper = pre.parentElement;
wrapper.insertAdjacentHTML('beforeend', `<div class="scroll-footer"><span class="scroll-footer-text">${scrollableText}</span></div>`);
// 行数(mxh-*)を指定された場合
if(isMaxLineNum) {
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 ${maxLines} of ${lineNumSpans.length} lines</span>`);
}
}
}
}
}
}
/* ここまで Highlight.js 独自プラグインの定義(初期化の前に定義) */
// 全てのコードブロックの要素を取得
const blockCodeElems = document.getElementsByClassName("wp-block-code");
// コードブロックの要素があればコードブロックの要素をラッパーで囲んで初期化
if(blockCodeElems.length > 0) {
// デフォルトでコードをハイライト表示する場合
if(highlightByDefault) {
for (const elem of blockCodeElems) {
if(!elem.classList.contains(noHighlightClass)) {
initHighlightJs(elem);
}
}
}else{
// デフォルトでコードをハイライト表示しない場合
for (const elem of blockCodeElems) {
if(elem.classList.contains(highlightClass)) {
initHighlightJs(elem);
}
}
}
}
// コードブロックの要素をラッパーで囲み Highlight.js で初期化する関数(pre 要素を引数に受け取る)
function initHighlightJs(elem) {
// コードブロックの要素をラッパーで囲む
const wrapper = document.createElement('div');
wrapper.className = wrapperClass;
elem.insertAdjacentElement('beforebegin', wrapper);
wrapper.appendChild(elem);
const pre = wrapper.querySelector("pre");
const code = wrapper.querySelector("code");
if(pre && code) {
// Highlight.js で code 要素を初期化
hljs.highlightElement(code);
// 行番号の表示・非表示の調整
lineNumControl(pre, code);
// 行折り返しの調整
lineWrapControl(pre);
// 言語名の表示・非表示の調整
languageNameControl(pre);
// ツールバーを追加する関数の呼び出し
addToolbar(wrapper, pre, code);
// ファイル名を追加する関数の呼び出し(★★★ 追加 ★★★)
addFileName(wrapper, pre);
}
}
// ファイル名を追加する関数(★★★ 追加 ★★★)
function addFileName(wrapper) {
const prevES = wrapper.previousElementSibling;
// 直前の段落ブロックが存在すれば以下を実行
if(prevES) {
// 直前の段落ブロックに file-name クラスが指定されていれば
if(prevES.classList.contains('file-name')) {
// 更に replace クラスが指定されていれば
if(prevES.classList.contains('replace')) {
// ラッパーに replace-lang-name クラスを追加
wrapper.classList.add('replace-lang-name');
}
// 直前の file-name クラスが指定された段落ブロックを削除
prevES.remove();
// span 要素を作成
const fileNameSpan = document.createElement('span');
// 作成した span 要素のテキストに段落ブロックのテキストを設定
fileNameSpan.textContent = prevES.textContent;
// span 要素にクラスを設定
fileNameSpan.setAttribute('class', 'file-name');
// ラッパー(div.hljs-wrap)に span 要素を追加
wrapper.appendChild(fileNameSpan);
// ラッパーに file-name-added クラスを追加
wrapper.classList.add('file-name-added');
}
}
}
// 言語名の表示・非表示の調整
function languageNameControl(pre) {
const preClassList = pre.classList;
if( !preClassList.contains(showLangClass) && (!showLangNameByDefault || preClassList.contains(noLangClass))) {
pre.setAttribute('data-language', '');
pre.classList.add('empty-lang-name');
}
}
// 行番号の表示・非表示の調整
function lineNumControl(pre, code) {
if(!showLineNumByDefault && !pre.classList.contains(showLineNumClass) || pre.classList.contains(noLineNumClass) ) {
code.classList.add('hide-line-num');
}
}
// 行折り返しの調整
function lineWrapControl(pre) {
const preClassList = pre.classList;
if(!lineWrapByDefault && !preClassList.contains("pre-wrap")) preClassList.add("pre");
}
// ツールバーを追加する関数
function addToolbar(wrapper, pre, code) {
const preClassList = pre.classList;
if (preClassList.contains(noToolbarClass) || (!showToolbarByDefault && !preClassList.contains(showToolbarClass))) {
return;
}
const toolbar = document.createElement("div");
toolbar.setAttribute("class", toolbarElemClass);
// ツールバーの HTML に行番号と折り返しの切り替えボタンを追加
toolbar.innerHTML = `<button type="button" class="line-num-btn">${lineNumLabel}</button>
<button type="button" class="line-wrap-btn">${lineWrapLabel}</button>`;
wrapper.insertBefore(toolbar, wrapper.firstElementChild);
wrapper.classList.add("toolbar-added");
// 行番号表示が有効かどうか
let showLineNums = showLineNumByDefault;
if (preClassList.contains(noLineNumClass)) showLineNums = false;
if (preClassList.contains(showLineNumClass)) showLineNums = true;
// 行番号表示が有効な場合はボタンに active クラスを追加し、無効な場合は非表示
const lineNumBtn = toolbar.querySelector(".line-num-btn");
if (showLineNums) {
lineNumBtn.classList.add("active");
}
// 行番号表示切り替えボタンにクリックイベントを設定
lineNumBtn.addEventListener("click", () => {
if (showLineNums) {
code.classList.add('hide-line-num');
}else{
code.classList.remove('hide-line-num');
}
showLineNums = !showLineNums;
lineNumBtn.classList.toggle("active");
});
// 自動折り返しが有効かどうか
let enableLineWrap = lineWrapByDefault;
if (preClassList.contains("pre")) enableLineWrap = false;
if (preClassList.contains("pre-wrap")) enableLineWrap = true;
if(!enableLineWrap) preClassList.add("pre");
const lineWrapBtn = toolbar.querySelector(".line-wrap-btn");
// 自動折り返しが有効な場合はボタンに active クラスを追加
if (enableLineWrap) lineWrapBtn.classList.add("active");
// 自動折り返し切り替えボタンにクリックイベントを設定
lineWrapBtn.addEventListener("click", () => {
if (!enableLineWrap) {
resetWhiteSpaceClasses();
preClassList.add("pre-wrap");
} else {
resetWhiteSpaceClasses();
preClassList.add("pre");
}
enableLineWrap = !enableLineWrap;
lineWrapBtn.classList.toggle("active");
// ★★★ 追加 ★★★
const lineNumSpans = code.getElementsByClassName("line-num");
const lineNumSpansLength = lineNumSpans.length;
const lastLineNumSpan = lineNumSpans[lineNumSpansLength-1];
const borderSpan = code.querySelector('.hljs-border-span');
const codePaddingBottom = parseFloat(window.getComputedStyle(code).paddingBottom);
if(lastLineNumSpan) {
borderSpan.style.setProperty('height', lastLineNumSpan.offsetTop + lastLineNumSpan.offsetHeight + codePaddingBottom + 'px')
}
// ★★★ ここまで ★★★
});
function resetWhiteSpaceClasses() {
preClassList.remove('pre');
preClassList.remove('pre-wrap');
}
const copyBtn = wrapper.querySelector(".hljs-copy-btn");
// コピーボタンが追加されていれば、ツールバーに移動
if (copyBtn) {
toolbar.appendChild(copyBtn);
}
}
}
custom.css にファイル名のスタイル(208-236行目)を追加します。
.hljs-wrap {
position: relative;
pre {
counter-reset: lineNumber;
/* オーバーフローは隠す */
overflow-x: hidden;
padding: 0;
margin: 0;
border-radius: 0;
}
pre code {
/* 行番号部分の幅を確保 */
padding-left: 3.25rem;
/* 行番号部の絶対配置の基準 */
position: relative;
/* ★★★ オーバーフローは隠す(追加)★★★ */
overflow-y: hidden;
}
/* span.line-num の擬似要素で行番号を表示 */
pre code span.line-num::before {
counter-increment: lineNumber;
content: counter(lineNumber);
display: inline-block;
/* 絶対配置 */
position: absolute;
left: 0;
/* 行番号の幅(フォント設定などにより調整) */
width: 2.5rem;
/* 行番号の色 (以下は好みで調整) */
color: #5f9168;
/* 行番号とコードの余白 */
margin-right: 5px;
padding-right: 5px;
/* 中央寄せ */
text-align: center;
}
/* 行番号を非表示にする場合 */
pre code.hide-line-num span.line-num::before {
left: -2.5rem;
}
/* 行番号を非表示にする場合 */
pre code.hide-line-num {
margin-left: -2.5rem;
}
/* ★★★ 枠線用の span 要素(追加) ★★★ */
code .hljs-border-span {
background-color: transparent;
/* 行番号(span.line-num::before)の幅と合わせる */
width: 2.5rem;
border-right: 1px solid #3d435b;
position: absolute;
top: 0;
left: 0;
}
/* ★★★ 枠線用の span 要素(追加) ★★★ */
code.hide-line-num .hljs-border-span {
border-right: none;
}
/* pre クラスを指定すると折り返しなし */
pre.pre code {
white-space: pre;
}
/* pre-wrap クラスを指定すると自動的に折り返し */
pre.pre-wrap code {
white-space: pre-wrap;
}
/* コピーボタンのスタイル(好みに応じて変更) */
.hljs-copy-btn {
position: absolute;
top: 0;
right: 0;
border: 1px solid #474646;
padding: 5px 10px;
background-color: #201e1e;
color: #999;
cursor: pointer;
z-index: 100;
}
/* ツールバーを表示している場合はコピーボタンの position を変更 */
.highlight-toolbar .hljs-copy-btn {
position: relative;
}
/* コピーボタンが表示されている場合のパディングトップ */
code.copy-btn-added {
padding-top: 2rem;
}
pre.no-toolbar code.copy-btn-added {
padding-top: 1.5rem;
}
/* ツールバーにコピーボタンが表示されている場合のパディングトップ */
&.toolbar-added code.copy-btn-added {
padding-top: 1em;
}
/* 疑似要素と content で言語名を表示(code 要素から pre 要素に変更) */
pre[data-language]::before {
content: attr(data-language);
position: absolute;
top: 0;
left: 0;
right: 0;
color: #aaa;
display: inline-block;
padding: 0.5rem;
pointer-events: none;
z-index: 50;
}
&:not(.toolbar-added) pre[data-language]::before {
background-color: #282c34;
}
pre[data-language],
pre[data-language].no-toolbar.no-copy {
padding-top: 2rem;
}
pre[data-language].no-toolbar {
padding-top: 1rem;
}
&.toolbar-added pre[data-language],
pre[data-language].no-toolbar.no-copy.show-no-lang {
padding-top: 0;
}
pre[data-language].empty-lang-name {
padding-top: 0;
}
/* ツールバー */
.highlight-toolbar {
height: 2.5rem;
background-color: #3a3e4a;
padding-right: 5px;
color: #999;
font-size: 12px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;
}
/* ツールバーと pre 要素の間のマージン */
.highlight-toolbar + pre {
margin-top: 0;
}
/* ツールバーの行番号表示と折り返し切り替えボタン */
.line-num-btn,
.line-wrap-btn {
background-color: transparent;
border: none;
color: #999;
cursor: pointer;
}
/* 行番号表示と折り返し切り替えボタンが有効時の色 */
.line-num-btn.active,
.line-wrap-btn.active {
color: #2799a3;
}
/* 行数を指定して表示する場合にコード下に表示する領域 */
.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");
}
/* ファイル名を絶対配置(位置などは環境に応じて調整) */
.file-name {
position: absolute;
top: -1.5rem;
right: 5px;
}
/* 言語名を空(半角スペース)に */
&.replace-lang-name pre[data-language]::before {
content: " ";
}
/* ツールバーがない場合のパディング */
&.replace-lang-name.no-toolbar pre[data-language] {
padding-top: 2rem;
}
/* 言語名の位置にファイル名を表示する場合のスタイル */
&.replace-lang-name .file-name {
top: 0.5rem;
left: 0.5rem;
color: #ccc;
pointer-events: none;
}
/* ファイル名を上部に表示する場合のラッパーのマージンの調整(環境に応じて) */
&:not(.replace-lang-name).file-name-added {
margin-top: 3rem;
}
}
例えば、段落ブロックに file-name と replace クラスを指定すると、
言語名の位置にファイル名を表示します。
段落ブロックに file-name クラスのみを指定すると、以下のように言語名はそのままで、ファイル名は右上に表示されます。
アコーディオンパネル
details と summary 要素で作成したアコーディオンパネルのアニメーションで、コードを開閉する機能を追加する例です。
アコーディオンパネルを追加する関数 addAccordionAnimationToCodeBlocks を別途定義(328-428)して、mySetupHighlightJS() の中で呼び出しています(324行目)。
addAccordionAnimationToCodeBlocks() は第1引数にコードブロックのラッパー要素のクラス名を、第2引数にアコーディオンパネルで表示するコードブロックに指定するクラス名を受け取ります。
アコーディオンパネルで表示するコードブロックに指定するクラス名は 46 行目で定義しています。
その他の部分は前述の例と同じで変更はありません。
document.addEventListener('DOMContentLoaded', () => {
mySetupHighlightJS();
});
function mySetupHighlightJS() {
const lineWrapLabel = "wrap";
const lineNumLabel = "number";
const copyBtnLabel = "Copy";
const copyBtnCompleteLabel = "Copied";
const copyBtnFailedLabel = "Failed";
const copyFailedMessage = "Sorry, can not copy with this browser.";
const highlightByDefault = true;
const noHighlightClass = "no-highlight";
const highlightClass = "highlight";
const showLineNumByDefault = true;
const noLineNumClass = "no-line-num";
const showLineNumClass= "show-line-num";
const lineNumStartPrefix = "start-";
const lineWrapByDefault = true;
const showToolbarByDefault = true;
const noToolbarClass = "no-toolbar";
const showToolbarClass = "show-toolbar";
const showLangNameByDefault = true;
const noLangClass = "show-no-lang";
const showLangClass = "show-lang";
const showCopyByDefault = true;
const noCopyClass = "no-copy";
const showCopyClass = "show-copy";
const maxLinePrefix = "mxl-";
const maxHeightePrefix = "mxh-";
const showScrollFooter = true;
const scrollableText = 'scrollable';
const showLineNumInfo = true;
const wrapperClass = "hljs-wrap";
const toolbarElemClass = "highlight-toolbar";
// アコーディオンパネルで表示する場合に指定するクラス
const accordionClass = "accordion";
hljs.addPlugin({
"after:highlightElement": ({ el, result, text }) => {
const pre = el.parentElement;
if( pre.tagName === 'PRE') {
addLineNumbers(el, result, pre);
copyCode(text, pre);
showLanguage(el, result, pre);
setMaxLineHeight(el, pre);
}
},
});
function addLineNumbers(el, result, pre) {
el.innerHTML = result.value.replace( /^/gm, '<span class="line-num"></span>');
const preClassName = pre.className;
if (preClassName) {
const startLineClassRegex = new RegExp(
`${lineNumStartPrefix}(-?\\d+)`
);
const matched = preClassName.match(startLineClassRegex);
if (matched && matched[1]) {
const startNumber = parseInt(matched[1]);
if (startNumber || startNumber === 0) {
pre.style.setProperty('counter-reset', 'lineNumber ' + (startNumber - 1));
}
}
}
const wrapper = el.closest('.' + wrapperClass);
const wrapperHeight = 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 elComputedStyle = window.getComputedStyle(el);
const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
const myObserver = new ResizeObserver((entries) => {
if (entries.length > 0 && entries[0]) {
const entry = entries[0];
const newHeight = entry.contentBoxSize ? entry.contentBoxSize[0].blockSize : entry.contentRect.height;
borderSpan.style.setProperty('height', newHeight + 'px');
if(pre.classList.contains('max-lines')) {
const lineNumSpans = el.getElementsByClassName("line-num");
if(lineNumSpans[lineNumSpans.length-1]) {
const lastLineNumTop = lineNumSpans[lineNumSpans.length-1].offsetTop;
const lastLineNumHeight = lineNumSpans[lineNumSpans.length-1].offsetHeight;
borderSpan.style.setProperty('height', lastLineNumTop + lastLineNumHeight + elPaddingBottom + 'px');
}
}
}
});
myObserver.observe(wrapper);
}
function copyCode(text, pre) {
if (showCopyByDefault && !pre.classList.contains(noCopyClass) || pre.classList.contains(showCopyClass)) {
const copyButton = document.createElement("button");
copyButton.setAttribute("class", "hljs-copy-btn");
copyButton.textContent = copyBtnLabel;
pre.after(copyButton);
pre.querySelector("code").classList.add("copy-btn-added");
copyButton.addEventListener("click", () => {
copyToClipboard(copyButton, text);
});
}
function copyToClipboard(btn, text) {
if (!navigator.clipboard) {
alert(copyFailedMessage);
}
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 showLanguage(el, result) {
if (result.language) {
el.parentElement.dataset.language = result.language;
}
}
function setMaxLineHeight(el, pre) {
const preClassName = pre.className;
if (preClassName) {
const maxLineClassRegex = new RegExp(`${maxLinePrefix}(\\d+)|${maxHeightePrefix}(\\d+)`);
const matched = preClassName.match(maxLineClassRegex);
if (matched) {
pre.classList.add('max-lines');
const lineNumSpans = el.getElementsByClassName("line-num");
const elComputedStyle = window.getComputedStyle(el);
const elPaddingY = parseFloat(elComputedStyle.paddingTop) + parseFloat(elComputedStyle.paddingBottom);
let isMaxLineNum = false;
if (matched[1]) {
isMaxLineNum = true;
const maxLines = parseInt(matched[1]);
if (lineNumSpans.length > 0 && maxLines < lineNumSpans.length) {
const targetLineNumSpan = lineNumSpans[maxLines];
if (targetLineNumSpan) {
el.style.setProperty( "height", targetLineNumSpan.offsetTop -elPaddingY + 3 + "px");
el.style.setProperty("overflow-y", "scroll");
addScrollFooter(isMaxLineNum, lineNumSpans, maxLines);
}
}
} else if (matched[2]) {
const maxHeight = parseInt(matched[2]);
if (el.offsetHeight && el.offsetHeight > maxHeight && maxHeight > elPaddingY) {
el.style.setProperty("height", maxHeight -elPaddingY + "px");
el.style.setProperty("overflow-y", "scroll");
addScrollFooter();
}
}
}
function addScrollFooter(isMaxLineNum, lineNumSpans, maxLines) {
if(showScrollFooter && !pre.classList.contains('no-scroll-footer') || !showScrollFooter && pre.classList.contains('show-scroll-footer')) {
const wrapper = pre.parentElement;
wrapper.insertAdjacentHTML('beforeend', `<div class="scroll-footer"><span class="scroll-footer-text">${scrollableText}</span></div>`);
if(isMaxLineNum) {
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 ${maxLines} of ${lineNumSpans.length} lines</span>`);
}
}
}
}
}
}
const blockCodeElems = document.getElementsByClassName("wp-block-code");
if(blockCodeElems.length > 0) {
if(highlightByDefault) {
for (const elem of blockCodeElems) {
if(!elem.classList.contains(noHighlightClass)) {
initHighlightJs(elem);
}
}
}else{
for (const elem of blockCodeElems) {
if(elem.classList.contains(highlightClass)) {
initHighlightJs(elem);
}
}
}
}
function initHighlightJs(elem) {
const wrapper = document.createElement('div');
wrapper.className = wrapperClass;
elem.insertAdjacentElement('beforebegin', wrapper);
wrapper.appendChild(elem);
const pre = wrapper.querySelector("pre");
const code = wrapper.querySelector("code");
if(pre && code) {
hljs.highlightElement(code);
lineNumControl(pre, code);
lineWrapControl(pre);
languageNameControl(pre);
addToolbar(wrapper, pre, code);
addFileName(wrapper, pre);
}
}
function addFileName(wrapper) {
const prevES = wrapper.previousElementSibling;
if(prevES) {
if(prevES.classList.contains('file-name')) {
if(prevES.classList.contains('replace')) {
wrapper.classList.add('replace-lang-name');
}
prevES.remove();
const fileNameSpan = document.createElement('span');
fileNameSpan.textContent = prevES.textContent;
fileNameSpan.setAttribute('class', 'file-name');
wrapper.appendChild(fileNameSpan);
wrapper.classList.add('file-name-added');
}
}
}
function languageNameControl(pre) {
const preClassList = pre.classList;
if( !preClassList.contains(showLangClass) && (!showLangNameByDefault || preClassList.contains(noLangClass))) {
pre.setAttribute('data-language', '');
pre.classList.add('empty-lang-name');
}
}
function lineNumControl(pre, code) {
if(!showLineNumByDefault && !pre.classList.contains(showLineNumClass) || pre.classList.contains(noLineNumClass) ) {
code.classList.add('hide-line-num');
}
}
function lineWrapControl(pre) {
const preClassList = pre.classList;
if(!lineWrapByDefault && !preClassList.contains("pre-wrap")) preClassList.add("pre");
}
function addToolbar(wrapper, pre, code) {
const preClassList = pre.classList;
if (preClassList.contains(noToolbarClass) || (!showToolbarByDefault && !preClassList.contains(showToolbarClass))) {
return;
}
const toolbar = document.createElement("div");
toolbar.setAttribute("class", toolbarElemClass);
toolbar.innerHTML = `<button type="button" class="line-num-btn">${lineNumLabel}</button>
<button type="button" class="line-wrap-btn">${lineWrapLabel}</button>`;
wrapper.insertBefore(toolbar, wrapper.firstElementChild);
wrapper.classList.add("toolbar-added");
let showLineNums = showLineNumByDefault;
if (preClassList.contains(noLineNumClass)) showLineNums = false;
if (preClassList.contains(showLineNumClass)) showLineNums = true;
const lineNumBtn = toolbar.querySelector(".line-num-btn");
if (showLineNums) {
lineNumBtn.classList.add("active");
}
lineNumBtn.addEventListener("click", () => {
if (showLineNums) {
code.classList.add('hide-line-num');
}else{
code.classList.remove('hide-line-num');
}
showLineNums = !showLineNums;
lineNumBtn.classList.toggle("active");
});
let enableLineWrap = lineWrapByDefault;
if (preClassList.contains("pre")) enableLineWrap = false;
if (preClassList.contains("pre-wrap")) enableLineWrap = true;
if(!enableLineWrap) preClassList.add("pre");
const lineWrapBtn = toolbar.querySelector(".line-wrap-btn");
if (enableLineWrap) lineWrapBtn.classList.add("active");
lineWrapBtn.addEventListener("click", () => {
if (!enableLineWrap) {
resetWhiteSpaceClasses();
preClassList.add("pre-wrap");
} else {
resetWhiteSpaceClasses();
preClassList.add("pre");
}
enableLineWrap = !enableLineWrap;
lineWrapBtn.classList.toggle("active");
const lineNumSpans = code.getElementsByClassName("line-num");
const lineNumSpansLength = lineNumSpans.length;
const lastLineNumSpan = lineNumSpans[lineNumSpansLength-1];
const borderSpan = code.querySelector('.hljs-border-span');
const codePaddingBottom = parseFloat(window.getComputedStyle(code).paddingBottom);
if(lastLineNumSpan) {
borderSpan.style.setProperty('height', lastLineNumSpan.offsetTop + lastLineNumSpan.offsetHeight + codePaddingBottom + 'px')
}
});
function resetWhiteSpaceClasses() {
preClassList.remove('pre');
preClassList.remove('pre-wrap');
}
const copyBtn = wrapper.querySelector(".hljs-copy-btn");
if (copyBtn) {
toolbar.appendChild(copyBtn);
}
}
// 開閉パネルとアコーディオンアニメーションを追加する関数の呼び出し
addAccordionAnimationToCodeBlocks(wrapperClass, accordionClass);
}
// 開閉パネルとアコーディオンアニメーションを追加する関数
function addAccordionAnimationToCodeBlocks(targetWrapperSelector, targetClassName) {
// アコーディオンパネルを開くボタンのテキスト
const summaryOpenText = "コードを表示";
// アコーディオンパネルを閉じるボタンのテキスト
const summaryCloseText = "コードを閉じる";
// details 要素に付与するクラス
const detailsClass='wdl-toggle-code-animation';
// details 要素内のコンテンツを格納する div 要素に付与するクラス
const detailsContentClass='details-content';
// details 要素内のコンテンツを格納する要素のラッパーに付与するクラス
const detailsContentWrapperClass='details-content-wrapper';
// 各コードブロックのラッパー要素内の pre 要素のクラスを調べて、対象の要素を details 要素でラップする
const targetWrapperElems = document.getElementsByClassName(targetWrapperSelector);
for (const elem of targetWrapperElems) {
const pre = elem.querySelector('pre');
// pre 要素に targetClassName('accordion')が指定されていれば
if(pre && pre.classList.contains(targetClassName)) {
// details 要素を作成
const details = document.createElement('details');
details.classList.add(detailsClass);
// 作成した details 要素の HTML(summary 要素と div 要素)を設定
details.innerHTML = `<summary data-close-text="${summaryCloseText}">${summaryOpenText}</summary>
<div class="${detailsContentWrapperClass}">
<div class="${detailsContentClass}"></div>
</div>`;
// コードブロックのラッパー要素を details 要素でラップする
elem.insertAdjacentElement('beforebegin', details);
details.querySelector('.' + detailsContentClass).appendChild(elem);
}
}
// details と summary を使ったアコーディオンアニメーションの関数
const details = document.getElementsByClassName(detailsClass);
for (const elem of details) {
const summary = elem.querySelector('summary');
const content = elem.querySelector('.' + detailsContentClass);
const summaryText = summary.textContent;
const summaryCloseText = summary.dataset.closeText ? summary.dataset.closeText : '閉じる';
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;
}
}
});
}
}
custom.css にアコーディオンパネルのスタイル(201行目〜)を追加します。
.hljs-wrap {
position: relative;
pre {
counter-reset: lineNumber;
overflow-x: hidden;
padding: 0;
margin: 0;
border-radius: 0;
}
pre code {
padding-left: 3.25rem;
position: relative;
overflow-y: hidden;
}
pre code span.line-num::before {
counter-increment: lineNumber;
content: counter(lineNumber);
display: inline-block;
position: absolute;
left: 0;
width: 2.5rem;
color: #5f9168;
margin-right: 5px;
padding-right: 5px;
text-align: center;
}
pre code.hide-line-num span.line-num::before {
left: -2.5rem;
}
pre code.hide-line-num {
margin-left: -2.5rem;
}
code .hljs-border-span {
background-color: transparent;
width: 2.5rem;
border-right: 1px solid #3d435b;
position: absolute;
top: 0;
left: 0;
}
code.hide-line-num .hljs-border-span {
border-right: none;
}
pre.pre code {
white-space: pre;
}
pre.pre-wrap code {
white-space: pre-wrap;
}
.hljs-copy-btn {
position: absolute;
top: 0;
right: 0;
border: 1px solid #474646;
padding: 5px 10px;
background-color: #201e1e;
color: #999;
cursor: pointer;
z-index: 100;
}
.highlight-toolbar .hljs-copy-btn {
position: relative;
}
code.copy-btn-added {
padding-top: 2rem;
}
pre.no-toolbar code.copy-btn-added {
padding-top: 1.5rem;
}
&.toolbar-added code.copy-btn-added {
padding-top: 1em;
}
pre[data-language]::before {
content: attr(data-language);
position: absolute;
top: 0;
left: 0;
right: 0;
color: #aaa;
display: inline-block;
padding: 0.5rem;
pointer-events: none;
z-index: 50;
}
&:not(.toolbar-added) pre[data-language]::before {
background-color: #282c34;
}
pre[data-language],
pre[data-language].no-toolbar.no-copy {
padding-top: 2rem;
}
pre[data-language].no-toolbar {
padding-top: 1rem;
}
&.toolbar-added pre[data-language],
pre[data-language].no-toolbar.no-copy.show-no-lang {
padding-top: 0;
}
pre[data-language].empty-lang-name {
padding-top: 0;
}
.highlight-toolbar {
height: 2.5rem;
background-color: #3a3e4a;
padding-right: 5px;
color: #999;
font-size: 12px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;
}
.highlight-toolbar + pre {
margin-top: 0;
}
.line-num-btn,
.line-wrap-btn {
background-color: transparent;
border: none;
color: #999;
cursor: pointer;
}
.line-num-btn.active,
.line-wrap-btn.active {
color: #2799a3;
}
.scroll-footer {
margin: 0;
font-size: 0.75rem;
color: #999;
background-color: #282c34;
padding: 8px;
display: flex;
justify-content: flex-end;
}
.scroll-footer-text {
margin-left: auto;
}
.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");
}
.file-name {
position: absolute;
top: -1.5rem;
right: 5px;
}
&.replace-lang-name pre[data-language]::before {
content: " ";
}
&.replace-lang-name.no-toolbar pre[data-language] {
padding-top: 2rem;
}
&.replace-lang-name .file-name {
top: 0.5rem;
left: 0.5rem;
color: #ccc;
pointer-events: none;
}
&:not(.replace-lang-name).file-name-added {
margin-top: 3rem;
}
}
/* アコーディオンパネルのスタイル */
details.wdl-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 1rem 0.5rem 32px;
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 クラス」に accordion クラスを指定すると、
以下のようにパネルを開閉するボタンが表示され、クリックすると、
アニメーションでパネルが開いてコードが表示されます。
チラツキ
後から JavaScript で details 要素を追加してコンテンツ部分を非表示にしているので、再読み込みの際などチラツキが発生します。
ファイルの読み込みを減らす
Highlight.js のファイルをダウンロードして使用する場合、合計で4つのファイルを読み込みますが、CSS(atom-one-dark.min.css と custom.css)と JavaScript(highlight.min.js と setup.js)をそれぞれ1つにまとめれば、ファイルの読み込みを減らすことができます。
カスタムブロック
カスタムブロックを作成すれば、「高度な設定」→「追加 CSS クラス」で指定するオプションやファイル名、開閉ボタンのテキストなどをインスペクターに設定して、チェックボックスなどのフォーム部品を使って簡単に指定できるようにすることができます。
詳細は以下のページを御覧ください。