使い方
以下の HTML をコピーして、style タグと script タグに以下のサンプルの CSS と JavaScript を貼り付けます。
<div class="hljs-wrap"> でラップした <pre><code>〜</code></pre> にエスケープ処理したコードを記述し、必要に応じてオプションを指定します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hilight.js カスタマイズ Sample</title>
<!-- Hilight.js テーマ CSS の読み込み -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css" integrity="sha512-Jk4AqjWsdSzSWCSuQTfYRIF84Rq/eV0G2+tu07byYwHcbTGfdmLrHjUSwvzp5HvbiqK4ibmNwdcG49Y5RGYPTg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
/* 以下のサンプルの CSS(custom.css)をコピペ */
</style>
</head>
<body>
<div class="hljs-wrap">
<pre><code>ハイライト表示するコードを記述</code></pre>
</div>
<!-- Hilight.js JavaScript の読み込み -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
// 以下のサンプルの JavaScript(custom.js)をコピペ
</script>
</body>
</html>
/* updated (changed) on: 2024/03/29 (CSS Nesting) */
/* pre 要素のラッパー要素(div)に指定するクラス */
.hljs-wrap {
position: relative;
/* pre 要素 */
pre {
overflow-wrap: break-word;
overflow-x: hidden;
padding: 0;
margin: 0;
/* 必要に応じてフォントサイズなどを設定 */
}
/* pre-wrap クラスを指定すると行を自動で折り返す */
pre.pre-wrap {
white-space: pre-wrap;
}
/* pre クラスを指定すると行を自動で折返さない */
pre.pre {
white-space: pre;
}
/* code 要素 */
pre code {
padding-left: 3rem;
position: relative;
/* 自動折り返しの設定は親要素(pre)の値を継承 */
white-space: inherit;
/* コード内に垂直方向のスクロールバーが表示されるのを防止(必要に応じて) */
overflow-y: hidden;
}
/* 行番号横枠線を表示する場合 */
&.line-num-bordered pre code {
padding-left: 3.25rem;
}
/* ツールバーを使わない場合の code 要素 */
&.no-toolbar pre code,
body.no-toolbar & pre code {
padding-bottom: 1.5rem;
padding-top: 2.5rem;
}
&.no-toolbar pre code.show-no-lang,
body.no-toolbar & pre code.show-no-lang {
padding-top: 1rem;
}
&.no-toolbar.has-copy-btn pre code.show-no-lang,
body.no-toolbar.has-copy-btn & pre code.show-no-lang {
padding-top: 2rem;
}
&.no-toolbar pre code,
body.no-toolbar & pre code {
padding-bottom: 1rem;
}
/* ツールバー */
.highlight-toolbar {
height: 2rem;
background-color: #3a3e4a;
padding-right: 5px;
color: #999;
font-size: 12px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
}
&.no-lang .highlight-toolbar {
justify-content: flex-end;
}
.highlight-toolbar + pre {
margin-top: 0;
}
.highlight-toolbar label {
color: #888;
cursor: pointer;
margin: 0 10px 0 0;
transition: color 0.3s;
}
.highlight-toolbar input[type="checkbox"] {
background-color: #262b37;
transition: background-color 0.3s;
display: none;
}
@media screen and (min-width: 640px) {
.highlight-toolbar label {
margin: 0 10px 0 3px;
}
.highlight-toolbar input[type="checkbox"] {
display: block;
}
}
.highlight-toolbar input[type="checkbox"]:hover {
background-color: #0d37a9;
}
/* チェックボックスのスタイルのリセット */
.highlight-toolbar input[type="checkbox"] {
border-radius: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
/* チェックボックスのスタイル */
.highlight-toolbar input[type="checkbox"] {
position: relative;
width: 16px;
height: 16px;
border: 1px solid #333;
cursor: pointer;
}
/* チェックマークのスタイル */
.highlight-toolbar input[type="checkbox"]:checked:before {
content: "";
position: absolute;
top: 0px;
left: 4px;
transform: rotate(45deg);
width: 4px;
height: 8px;
border-right: 2px solid #bbb;
border-bottom: 3px solid #bbb;
}
.highlight-toolbar input[type="checkbox"]:checked + label {
color: #b9bfd0;
}
/* 言語名を表示 */
code[data-language]::before {
content: attr(data-language);
position: absolute;
top: 0;
left: 0;
color: #ccc;
display: inline-block;
padding: 0.5rem 1rem;
/* コードの背景色と同じにする場合は background-color: #282c34; */
background-color: #40547d;
z-index: 5;
}
code[data-language].hide-line-num::before {
left: 2.5rem;
}
/* ツールバーの中の言語名の表示 */
.highlight-toolbar .lng-span {
margin-right: auto;
margin-left: 10px;
font-size: 13px;
color: #ccc;
}
/* コピーボタン(ツールバーを使用しない場合) */
.hljs-copy-btn {
position: absolute;
top: 0;
right: 0;
background-color: #262b37;
border: none;
padding: 8px;
color: #999;
cursor: pointer;
transition: color 0.3s, background-color 0.3s;
}
.hljs-copy-btn:hover {
color: #eee;
background-color: #162858;
}
/* ツールバーの中のコピーボタン */
.highlight-toolbar .hljs-copy-btn {
position: relative;
margin: 0 10px;
padding: 2px 4px;
}
/* ラベルとリンク(data-label 属性と data-label-url 属性で指定した文字列) */
.hljs-label,
.hljs-label-url {
position: absolute;
top: -2rem;
right: 10px;
color: #999;
display: inline-block;
padding: 0.5rem 0;
}
.hljs-label-url {
color: #3987c7;
text-decoration: none;
}
.hljs-label-url:hover {
color: #55924f;
}
&.has-label {
margin-top: 4rem;
}
&:not(.has-label) {
margin-top: 2.25rem;
}
/* 行番号(CSS カウンター)*/
pre {
counter-reset: lineNumber;
}
pre span.line-num::before {
counter-increment: lineNumber;
content: counter(lineNumber);
min-width: 2.5rem;
display: inline-block;
color: #777;
text-align: center;
position: absolute;
left: 0;
background: #282c34;
}
/* 行番号を非表示にする場合 */
pre code.hide-line-num span.line-num::before {
left: -2.5rem;
}
/* 行番号を非表示にする場合 */
pre code.hide-line-num {
margin-left: -2.5rem;
}
/* 行番号右側の枠線 */
.hljs-border-span {
background-color: transparent;
width: 2.5rem;
border-right: 1px solid #595a60;
top: 0;
left: 0;
}
/* 行番号非表示の場合(枠線なし) */
pre code.hide-line-num .hljs-border-span {
border: none;
}
/* 行のハイライト時の行番号部分 */
pre span.line-num.line-num-highlight::before {
color: #c2c21a;
/* background: #424638; */
}
/* 行のハイライト時のコード部分 */
.line-highlight {
position: absolute;
/* 行番号の幅と合わせる */
left: 2.5rem;
width: calc(100% - 2.5rem);
margin-left: -2.5rem;
width: 100%;
background: linear-gradient(
to right,
hsla(254, 15%, 51%, 0.2) 50%,
hsla(254, 15%, 51%, 0.01)
);
pointer-events: none;
}
/* 行数を指定して表示する場合にコード下に表示する領域 */
.scroll-footer {
margin: 0;
font-size: 0.75rem;
color: #999;
/* コードの背景色と同じ色 */
background-color: #282c34;
padding: 8px;
display: flex;
justify-content: flex-end;
}
/* .scroll-footer の右端に表示するテキスト*/
.scroll-footer-text {
margin-left: auto;
}
/* .scroll-footer-text の左側に表示するアイコン(色は fill='%23aaaaaa' の aaaaaa 部分) */
.scroll-footer-text::before {
content: "";
display: inline-block;
height: 0.875rem;
width: 0.875rem;
vertical-align: -2px;
margin-right: 8px;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23aaaaaa' viewBox='0 0 16 16'%3E %3Cpath fill-rule='evenodd' d='M11.5 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L11 2.707V14.5a.5.5 0 0 0 .5.5m-7-14a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L4 13.293V1.5a.5.5 0 0 1 .5-.5'/%3E%3C/svg%3E");
}
/* テーマ(atom-one-dark)のコメントの色を上書き */
.hljs-comment {
color: #6b788f;
}
}
/* コードの表示・非表示( details 要素と summary 要素によるアコーディオン)*/
details.toggle-code-animation {
border: none;
margin: 2rem 0;
.details-content-wrapper {
padding: 1rem 0;
}
.details-content {
overflow: hidden;
}
summary {
display: inline-block;
cursor: pointer;
position: relative;
padding: 0.5rem 1.5rem 0.5rem 2.25rem;
border: 1px solid #aaa;
font-size: 13px;
}
summary::-webkit-details-marker {
display: none;
}
summary::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 10px;
margin: auto 0;
width: 8px;
height: 8px;
border-top: 3px solid #097b27;
border-right: 3px solid #097b27;
transform: rotate(45deg);
}
}
/* updated (changed) on: 2024/03/31 */
document.addEventListener('DOMContentLoaded', () => {
// Highlight.js のプラグインをセットアップ
mySetUpHljsPlugins(myCustomHighlightJsSettings);
// Highlight.js の初期化とカスタマイズの実行
mySetupHighlightJs(myCustomHighlightJsSettings);
// アコーディオンアニメーションの開閉パネルの追加(オプショナル)
myAddAccordionPanel(myCustomHighlightJsSettings.accordionClassName);
// アコーディオンアニメーションの呼び出し(オプショナル)
mySetupToggleDetailsAnimation();
});
const myCustomHighlightJsSettings = {
// pre code のラッパーのクラス名
wrapperClassName: 'hljs-wrap',
// pre 要素なし(インライン)の code 要素でもハイライトするかどうか
useInlineHighlight: true,
// インラインの code 要素でハイライトする場合に code 要素に指定するクラス(空の場合、全ての code 要素)
inlineHighlightClassName: 'highlight',
// アコーディオンパネルで表示する場合に、ラッパー要素に指定するクラス名
accordionClassName: 'toggle-accordion'
}
// Hightlight.js のプラグインを定義
function mySetUpHljsPlugins(settings) {
const { wrapperClassName, useInlineHighlight } = settings;
// 行番号横の枠線を表示するかどうか
const showLineNumBorder = true;
// デフォルトで行の自動折り返しを有効にするかどうか
const preWrapOnInit = false;
// 初期状態でコピーボタンを非表示
const noCopyBtnOnInit = false;
// 初期状態で行番号を非表示(ツールバー使用時のみ有効)
const noLineNumOnInit = document.body.classList.contains('no-line-num') ? true : false;
// ツールバーを表示(使用)するかどうか(全てのページで使用しない場合は false を指定)
const useToolbar = document.body.classList.contains('no-toolbar') ? false : true;
// 行数を指定して表示する場合の表示領域の高さの調整値
const visibleHeightAdjustAmount = 0;
// 行数を指定して表示する際に data-scroll-to を指定する場合の高さの調整値
const scrollToHeightAdjustAmount = 0;
// 行数を指定して表示する場合にスクロール量の調整値
const scrollAdjustAmount = 0;
// 行数を指定して表示する場合に折り返しや行番号表示の切り替えやウィンドウサイズの変更が発生した際にスクロール位置を元に戻すかどうか
const resetScrollPosition = true;
// 行数を指定して表示する場合にコードの下に情報領域(div.scroll-footer)を表示するかどうか
const showScrollFooter = true;
// 行数を指定して表示する場合にコードの下の情報領域(div.scroll-footer)に行数を表示するかどうか
const showLineNumInfo = true;
// ツールバーやボタンのテキスト(ラベル)
const lineAutoWrapLabel = 'wrap';
const lineNumLabel = 'number';
const copyBtnLabel = 'Copy';
const copyBtnCompleteLabel = 'Copied';
const copyBtnFailedLabel = 'Failed';
const copyFailedMessage = 'Sorry, can not copy with this browser.';
const scrollableText = 'scrollable';
// 最後の行の offsetTop と高さを取得する関数(lineNumSpans は行番号の全ての span 要素、el は code 要素)
function getLastNumLineInfo(lineNumSpans, el) {
// offsetTop と高さを格納するオブジェクト
const info = {};
const lastLineNumTop = lineNumSpans.item(lineNumSpans.length-1).offsetTop;
// dummy 要素を追加して offsetTop の差分から現在の高さを取得
const dummy = document.createElement('span');
dummy.innerHTML = "<br>";
el.appendChild(dummy);
const dummy2 = document.createElement('span');
el.appendChild(dummy2);
const dummy2OffsetTop = dummy2.offsetTop;
// offsetTop プロパティを設定
info.offsetTop = dummy2OffsetTop;
const lastLineNumHeight = dummy2OffsetTop - lastLineNumTop;
// height プロパティを設定
info.height = lastLineNumHeight
dummy.remove();
dummy2.remove();
return info;
}
// data-max-lines 属性に指定された値が有効化どうかを確認し、整数で返す関数
function getMaxLineValue(pre, lineNumSpans) {
let maxLineValue = null;
if(pre.hasAttribute('data-max-lines') && lineNumSpans.length > 0) {
const dataMaxLines = parseInt(pre.getAttribute('data-max-lines'));
if(dataMaxLines && dataMaxLines >0 && dataMaxLines < lineNumSpans.length){
maxLineValue = dataMaxLines;
}
}
return maxLineValue;
}
// data-max-lines 属性が指定された場合の表示領域の高さを更新する関数
function updateVisibleHeight(pre, el, lineNumSpans, maxLineValue, isSetMaxHeight = false) {
const elComputedStyle = window.getComputedStyle(el);
const elPaddingTop = parseFloat(elComputedStyle.paddingTop);
const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
const elPaddingY = elPaddingTop + elPaddingBottom;
if (lineNumSpans.item(maxLineValue)) {
// data-max-lines-offset の値を取得
const maxLineValueOffset = pre.getAttribute('data-max-lines-offset') ? parseInt(pre.getAttribute('data-max-lines-offset')): 0;
// 表示する最後の行の次の行の span 要素の offsetTop
const maxNextOffsetTop = lineNumSpans.item(maxLineValue).offsetTop;
const visibleHeight = maxNextOffsetTop + maxLineValueOffset - elPaddingY + visibleHeightAdjustAmount;
el.style.setProperty('height', visibleHeight + 'px');
el.style.setProperty('overflow-y', 'scroll');
if(pre.classList.contains('no-scroll')) {
el.style.setProperty('overflow-y', 'hidden');
}
}
// data-scroll-to 属性が指定されている場合
if (pre.hasAttribute('data-scroll-to')) {
let dataScrollTo = parseInt(pre.getAttribute('data-scroll-to'));
if (pre.hasAttribute('data-line-num-start')) {
const startNumber = parseInt(pre.getAttribute('data-line-num-start'));
if(startNumber) {
dataScrollTo -= startNumber -1;
}
}
if(dataScrollTo + maxLineValue > lineNumSpans.length + 1) {
// 指定された値が有効でなければコンソールにメッセージを表示して終了
console.log(`data-scroll-to: ${dataScrollTo} or data-max-line : ${maxLineValue} is not valid.`)
return;
}
if (dataScrollTo && lineNumSpans.item(dataScrollTo - 1) ) {
const scrollToOffsetTop = lineNumSpans.item(dataScrollTo - 1).offsetTop;
const maxLineValueOffset = pre.getAttribute('data-max-lines-offset') ? parseInt(pre.getAttribute('data-max-lines-offset')): 0;
let visibleHeight;
if(lineNumSpans.item(dataScrollTo + maxLineValue - 1)) {
const lastRowOffsetTop = lineNumSpans.item(dataScrollTo + maxLineValue - 1).offsetTop;
visibleHeight = lastRowOffsetTop - scrollToOffsetTop + maxLineValueOffset + scrollToHeightAdjustAmount - elPaddingY;
}else if(dataScrollTo + maxLineValue === lineNumSpans.length + 1 && lineNumSpans.item(dataScrollTo) ) {
// スクロールにより最終行を表示する場合
const lastLineOffsetTop = getLastNumLineInfo(lineNumSpans, el).offsetTop;
visibleHeight = lastLineOffsetTop - scrollToOffsetTop + maxLineValueOffset + scrollToHeightAdjustAmount - elPaddingTop;
}
el.style.setProperty('height', visibleHeight + 'px');
if(isSetMaxHeight) {
// 行数を指定して高さを設定する関数 setMaxHeight() での呼び出しの場合(一度スクロールを実行して終了)
const scrollAmount = scrollToOffsetTop - scrollAdjustAmount;
el.scroll(0, scrollAmount);
return;
}
if(resetScrollPosition) {
const scrollAmount = scrollToOffsetTop - scrollAdjustAmount;
el.scroll(0, scrollAmount);
}
}
}
}
// Hightlight.js の addPlugin() after:highlightElement でプラグイン(機能)を設定
hljs.addPlugin({
'after:highlightElement': ({ el, result, text }) => {
// ラッパー要素
const wrapper = el.closest('.' + wrapperClassName);
// pre 要素(親要素)
const pre = el.parentElement;
// 個別に定義した関数を呼び出す
if(wrapper && pre) {
showLanguage(el, result, wrapper);
copyCode(text, pre, wrapper);
addLineNumbers(el, result, wrapper, pre);
highlightNumbers(el, pre);
setMaxHeight(el, wrapper, pre);
setUpWrapper(el, wrapper, pre);
}
}
});
// 言語名を表示する関数
function showLanguage(el, result, wrapper) {
if (el.classList.contains('show-no-lang')) {
if (wrapper) wrapper.classList.add('no-lang');
return;
}
if (el.hasAttribute('data-set-lang')) {
addLanguageSpan(el.getAttribute('data-set-lang'));
return;
}
if (result.language) {
if (useToolbar) {
addLanguageSpan(result.language);
} else {
el.dataset.language = result.language;
}
}
function addLanguageSpan(language) {
const languageSpan = document.createElement('span');
languageSpan.setAttribute('class', 'lng-span');
languageSpan.textContent = language;
const wrapper = el.closest('.' + wrapperClassName);
if (wrapper && !wrapper.classList.contains('no-toolbar')) {
wrapper.appendChild(languageSpan);
} else if (wrapper && wrapper.classList.contains('no-toolbar')) {
el.dataset.language = language;
}
}
}
// コードをコピーする関数
function copyCode(text, pre, wrapper) {
const preClass = pre.classList;
if (preClass.contains('no-copy-btn')) return;
if (noCopyBtnOnInit && !preClass.contains('show-copy-btn')) return;
if (useInlineHighlight && pre.nodeName !== 'PRE') return;
const copyButton = document.createElement('button');
copyButton.setAttribute('class', 'hljs-copy-btn');
copyButton.textContent = copyBtnLabel;
pre.after(copyButton);
wrapper.classList.add('has-copy-btn');
copyButton.addEventListener('click', () => {
copyToClipboard(copyButton, text)
});
function copyToClipboard(btn, text) {
if (!navigator.clipboard) {
alert(copyFailedMessage);
}
// data-max-lines 属性と no-scroll クラスが指定されている場合は、表示されている部分のみをコピー
if(preClass.contains('no-scroll') && pre.hasAttribute('data-max-lines')) {
let startLine = 1;
let endLine = parseInt(pre.getAttribute('data-max-lines'));
if(pre.hasAttribute('data-scroll-to')) {
const scrollTo = parseInt(pre.getAttribute('data-scroll-to'));
if(scrollTo) {
startLine = scrollTo;
endLine += scrollTo -1;
}
}
if (pre.hasAttribute('data-line-num-start')) {
const startNumber = parseInt(pre.getAttribute('data-line-num-start'));
if(startNumber) {
startLine -= startNumber -1;
endLine -= startNumber -1;
}
}
const textArray = text.split(/\r?\n/);
let visibleText = '';
if(startLine >=1 && endLine < textArray.length){
for(let i=startLine-1; i<=endLine-1; i++) {
if(i !== endLine-1) {
visibleText += textArray[i] + "\n";
}else{
visibleText += textArray[i];
}
}
}
text = visibleText;
}
// プロンプト文字($ と %)を除外してコピー
if (preClass.contains('copy-no-prompt')) {
text = text.replace(/^\$\s|^%\s/gm, '');
}
// シングルラインコメントを除外してコピー
if (preClass.contains('copy-no-sl-comments') || preClass.contains('copy-no-comments')) {
// 行の途中の「半角スペース + //」も削除。 [^\S\r\n] は改行を除く空白にマッチ(コメント以外も削除する可能性あり)
text = text.replace(/^([^\S\r\n]*\/\/).*$\r?\n?/gm, "").replace(/(.*)\s\/\/.*/g, "$1");
}
// マルチラインコメントを除外してコピー
if (preClass.contains('copy-no-ml-comments') || preClass.contains('copy-no-comments')) {
// replace() の第2引数に関数 replaceComments を指定(正しくマッチしない可能性あり)
text = text.replace(/^(.*)\/\*[\s\S]*?\*\/($\r?\n?)?/gm, replaceComments)
}
// HTML コメントを除外してコピー
if (preClass.contains('copy-no-html-comments')) {
// replace() の第2引数に関数 replaceComments を指定
text = text.replace(/^(.*)<!\-\-[\s\S]*?\-\->($\r?\n?)?/gm, replaceComments)
}
function replaceComments(match, p1, p2) {
// コメントの後に改行がない場合(p2 は undefined)
if (!p2) p2 = '';
// コメントの前が空白文字の場合
if (!p1.trim()) {
if(p2) {
return '';
}
return p1;
} else {
return p1 + p2;
}
}
navigator.clipboard.writeText(text).then(
() => {
btn.textContent = copyBtnCompleteLabel;
resetCopyBtnText(btn, 1500);
},
(error) => {
btn.textContent = copyBtnFailedLabel;
resetCopyBtnText(btn, 1500);
console.log(error.message);
}
);
};
function resetCopyBtnText(btn, delay) {
setTimeout(() => {
btn.textContent = copyBtnLabel
}, delay)
}
}
// 行番号表示する関数
function addLineNumbers(el, result, wrapper, pre) {
el.innerHTML = result.value.replace( /^/gm, '<span class="line-num"></span>');
let startNumOffset = 0;
if (pre.hasAttribute('data-line-num-start')) {
const startNumber = parseInt(pre.getAttribute('data-line-num-start'));
if (startNumber || startNumber === 0) {
pre.style.setProperty('counter-reset', 'lineNumber ' + (startNumber - 1));
startNumOffset = startNumber - 1;
}
}
if (wrapper) {
let borderSpan;
// code 要素のスタイルを取得
const elComputedStyle = window.getComputedStyle(el);
// code 要素の paddingBottom
const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
const elPaddingTop = parseFloat(elComputedStyle.paddingTop);
// 行番号横の枠線を表示する場合
if(showLineNumBorder) {
const codeHeight = el.offsetHeight;
borderSpan = document.createElement('span');
borderSpan.classList.add('hljs-border-span');
el.appendChild(borderSpan);
borderSpan.style.setProperty('position', 'absolute');
borderSpan.style.setProperty('height', codeHeight + 'px');
wrapper.classList.add('line-num-bordered');
}
// ResizeObserver で code 要素のサイズを監視
const codeObserver = new ResizeObserver((entries) => {
if (entries.length > 0 && entries[0]) {
if(showLineNumBorder) {
const entry = entries[0];
let resizedHeight = entry.contentBoxSize ? entry.contentBoxSize[0].blockSize : entry.contentRect.height;
borderSpan.style.setProperty('height', resizedHeight + elPaddingBottom + elPaddingTop + 'px');
}
// 行番号用の span 要素を全て取得
const lineNumSpans = el.getElementsByClassName("line-num");
// ハイライト行の高さの更新
const lineNumSpansArray = Array.prototype.slice.call( lineNumSpans );
// HTMLCollection を配列に変換して処理
lineNumSpansArray.forEach( (elem, index) => {
// line-num-highlight クラスが指定されていれば高さを更新
if(elem.classList.contains('line-num-highlight')) {
// ハイライト用の span 要素(span.line-highlight)
const highlightSpan = lineNumSpans[index].nextElementSibling;
if(highlightSpan) {
if(lineNumSpans.item(index) && lineNumSpans.item(index +1)) {
const spanOffsetTop = lineNumSpans.item(index).offsetTop;
const nextSpanOffsetTop = lineNumSpans.item(index +1).offsetTop;
const height = nextSpanOffsetTop - spanOffsetTop;
if(height !== 0) {
highlightSpan.style.setProperty('height', height + 'px');
}
}else if(lineNumSpans.item(index) && index === lineNumSpans.length - 1) {
// 最後の行の場合は getLastNumLineInfo() で取得
const lastLineNumHeight = getLastNumLineInfo(lineNumSpans, el).height;
if(lastLineNumHeight !== 0) {
highlightSpan.style.setProperty('height', lastLineNumHeight + 'px');
}
}
}
}
});
const maxLineValue = getMaxLineValue(pre, lineNumSpans);
// pre 要素に data-max-lines 属性が指定されていて有効な値であれば
if (maxLineValue) {
// 表示領域の高さを更新
updateVisibleHeight(pre, el, lineNumSpans, maxLineValue);
// 行番号横の枠線を表示する場合
if(showLineNumBorder) {
// 枠線用の span 要素の高さを更新
if(getLastNumLineInfo(lineNumSpans, el).offsetTop) {
borderSpan.style.setProperty('height', getLastNumLineInfo(lineNumSpans, el).offsetTop + elPaddingBottom + 'px');
}else{
// iPhone などでは高さを取得できないので、ボーダーを非表示に(パディング分だけ表示されてしまうため)
borderSpan.style.setProperty('border', 'none');
}
}
}
}
});
codeObserver.observe(el);
}
}
//指定された行をハイライト表示する関数
function highlightNumbers(el, pre) {
if (pre.hasAttribute('data-line-highlight')) {
const targetLines = pre.getAttribute('data-line-highlight');
const highlightCode = pre.classList.contains('no-highlight-code') ? false : true;
const highlightNumber = pre.classList.contains('no-highlight-number') ? false : true;
// 指定された行を data-line-highlight 属性から取得して、その行にハイライト用のクラスを追加
const targets = targetLines.split(',').map((val) => val.trim());
if (targets.length > 0) {
const lineNumSpans = el.getElementsByClassName('line-num');
const lineLength = lineNumSpans.length;
targets.forEach((target) => {
let startNumOffset = 0;
if (pre.hasAttribute('data-line-num-start')) {
const startNumber = parseInt(pre.getAttribute('data-line-num-start'));
if (startNumber || startNumber === 0) {
startNumOffset = startNumber - 1;
}
}
const range = target.split('-');
if (range.length === 2) {
if (range[0] !== '') {
const start = startNumOffset === 0 ? parseInt(range[0]) : parseInt(range[0]) - startNumOffset;
const end = startNumOffset === 0 ? parseInt(range[1]) : parseInt(range[1]) - startNumOffset;
if (start && end) {
if (end >= start) {
for (let i = start; i <= end; i++) {
addClassToSpan(i);
}
} else {
for (let i = end; i <= start; i++) {
addClassToSpan(i);
}
}
}
} else {
const negativeNum = (startNumOffset === 0 ? parseInt(range[1]) : parseInt(range[1])) * -1;
addClassToSpan(negativeNum - startNumOffset);
}
} else if (range.length === 1) {
addClassToSpan(startNumOffset === 0 ? parseInt(target) : parseInt(target) - startNumOffset);
}
function addClassToSpan(number) {
if (number > 0 && number <= lineLength) {
if (highlightCode) {
const highlightSpan = document.createElement('span');
highlightSpan.className = 'line-highlight';
lineNumSpans.item(number - 1).after(highlightSpan);
}
if (highlightNumber) {
lineNumSpans.item(number - 1).classList.add('line-num-highlight');
}
}
}
})
}
}
}
// 表示する行数を指定して code 要素に height を設定する関数
function setMaxHeight(el, wrapper, pre) {
if (!pre.hasAttribute('data-max-lines')) return;
if (!wrapper) return;
const lineNumSpans = el.getElementsByClassName('line-num');
const maxLineValue = getMaxLineValue(pre, lineNumSpans);
if (maxLineValue) {
updateVisibleHeight(pre, el, lineNumSpans, maxLineValue, true);
}
if(pre.classList.contains('no-scroll')) return;
// コードの下に情報領域(div.scroll-footer)を表示
if(showScrollFooter && !pre.classList.contains('no-scroll-footer') || !showScrollFooter && pre.classList.contains('show-scroll-footer')) {
const footerText = pre.hasAttribute('data-footer-text') ? pre.getAttribute('data-footer-text') : scrollableText;
wrapper.insertAdjacentHTML('beforeend', `<div class="scroll-footer"><span class="scroll-footer-text">${footerText}</span></div>`);
const scrollFooter = wrapper.querySelector('.scroll-footer');
if(showLineNumInfo && !pre.classList.contains('no-line-info') || !showLineNumInfo && pre.classList.contains('show-line-info')) {
scrollFooter.insertAdjacentHTML('afterbegin', `<span class="line-count">Showing ${maxLineValue} of ${lineNumSpans.length} lines</span>`);
}
}
}
// ラッパー要素にラベルやツールバーを表示する関数
let index = 0;
function setUpWrapper(el, wrapper, pre) {
const preClass = pre.classList;
const wrapperClass = wrapper.classList;
if(preWrapOnInit) preClass.add('pre-wrap');
// ラベルの追加
if (pre.hasAttribute('data-label')) {
const label = pre.getAttribute('data-label');
let element;
if (pre.hasAttribute('data-label-url')) {
element = document.createElement('a');
element.href = pre.getAttribute('data-label-url');
element.classList.add('hljs-label-url');
if (preClass.contains('target-blank')) {
element.target = "_blank";
element.rel = "noopener";
}
} else {
element = document.createElement('span');
element.classList.add('hljs-label');
}
element.textContent = label;
wrapper.appendChild(element);
wrapperClass.add('has-label');
}
// no-line-num クラスを指定した要素の行番号を非表示
if (preClass.contains('no-line-num')) {
el.classList.add('hide-line-num');
}
// ツールバーの追加
if (useToolbar) {
if (!wrapperClass.contains('no-toolbar')) {
const toolbar = document.createElement('div');
toolbar.setAttribute('class', 'highlight-toolbar');
const noLineNumChecked = noLineNumOnInit ? '' : ' checked';
let lineWrapChecked = preWrapOnInit ? ' checked' : '';
if(preClass.contains('pre')) {
lineWrapChecked = '';
}else if(preClass.contains('pre-wrap')){
lineWrapChecked = ' checked';
}
toolbar.innerHTML = `<input type="checkbox" id="line-auto-wrap${index}" name="line-auto-wrap"${lineWrapChecked}>
<label for="line-auto-wrap${index}">${lineAutoWrapLabel}</label>`;
const noLineNum = preClass.contains('no-line-num');
if (!noLineNum) {
toolbar.insertAdjacentHTML('afterbegin', `<input type="checkbox" id="line-num-check${index}" name="line-num-check"${noLineNumChecked}>
<label for="line-num-check${index}">${lineNumLabel}</label>`);
}
wrapper.insertBefore(toolbar, wrapper.firstElementChild);
const lineNumCheck = toolbar.querySelector('[name="line-num-check"]');
const lineNums = wrapper.getElementsByClassName('line-num');
if (lineNumCheck) {
lineNumCheck.addEventListener('change', (e) => {
if (e.currentTarget.checked) {
el.classList.remove('hide-line-num');
} else {
el.classList.add('hide-line-num');
}
});
if (noLineNumOnInit) {
el.classList.add('hide-line-num');
}
}
const lineAutoWrapCheck = toolbar.querySelector('[name="line-auto-wrap"]');
lineAutoWrapCheck.addEventListener('change', (e) => {
const lineNumSpans = el.getElementsByClassName('line-num');
const elComputedStyle = window.getComputedStyle(el);
const elPaddingBottom = parseFloat(elComputedStyle.paddingBottom);
const maxLineValue = getMaxLineValue(pre, lineNumSpans);
// white-space プロパティを変更
if (e.currentTarget.checked) {
pre.style.setProperty('white-space', 'pre-wrap');
} else {
pre.style.setProperty('white-space', 'pre');
}
// 表示領域の高さを更新
if (maxLineValue) {
updateVisibleHeight(pre, el, lineNumSpans, maxLineValue);
}
// 行番号横の枠線を表示する場合は、行番号枠線(span 要素)の高さを更新
if (showLineNumBorder) {
const borderSpan = wrapper.querySelector('.hljs-border-span');
if(borderSpan) {
// 行番号枠線の高さの更新
borderSpan.style.setProperty('height', el.offsetHeight + 'px');
// pre 要素に data-max-lines 属性が指定されていて有効な値であれば
if (maxLineValue) {
borderSpan.style.setProperty('height', getLastNumLineInfo(lineNumSpans, el).offsetTop + elPaddingBottom + 'px');
}
}
}
});
const langSpan = wrapper.querySelector('.lng-span');
if (langSpan) {
toolbar.insertBefore(langSpan, toolbar.firstElementChild)
}
const copyBtn = wrapper.querySelector('.hljs-copy-btn');
if (copyBtn) {
toolbar.appendChild(copyBtn)
}
}
}
index ++;
}
}
// Highlight.js の初期化とカスタマイズを適用する関数。
// targetWrapper は単一の要素のみに適用する場合に指定(WordPress のエディタでのプレビュー用に使用する場合に指定)
function mySetupHighlightJs(settings, targetWrapper = false) {
const { wrapperClassName, useInlineHighlight, inlineHighlightClassName } = settings;
// 全てのラッパー要素を取得
const wrappers = document.getElementsByClassName(wrapperClassName);
// インラインでハイライトする場合
if (useInlineHighlight) {
// pre 要素なしの code 要素でハイライト
const inlineHighlightElems = document.getElementsByClassName(inlineHighlightClassName);
for (const elem of inlineHighlightElems) {
if (elem.parentElement.nodeName !== 'PRE') {
hljs.highlightElement(elem);
}
}
}
// Highlight.js の初期化とセットアップ
if (wrappers.length > 0 && !targetWrapper) {
for (const wrapper of wrappers) {
hljs.highlightElement(wrapper.querySelector('pre code'));
}
}else if (targetWrapper) {
const code = targetWrapper.querySelector('code');
if(code) {
hljs.highlightElement(code);
}
}
}
// 開閉パネル(アコーディオンパネル)を指定されたクラスを持つ要素(または第2引数で渡された単一の要素)に追加する関数
function myAddAccordionPanel(targetClassName, elem = null) {
// details 要素に付与するクラス
const detailsClass = 'toggle-code-animation';
// details 要素内のコンテンツを格納する div 要素に付与するクラス
const detailsContentClass = 'details-content';
// details 要素内のコンテンツを格納する要素のラッパーに付与するクラス
const detailsContentWrapperClass = 'details-content-wrapper';
// 第2引数の elem が指定されていなければ、第1引数のクラス名を使って要素を取得してパネルを追加
if(!elem) {
// パネルを追加する要素を取得
const targetElems = document.getElementsByClassName(targetClassName);
for (const elem of targetElems) {
addPanel(elem);
}
}else{
// 第2引数の elem が指定されていれば、その要素にパネルを追加
addPanel(elem);
}
// アコーディオンパネルを受け取った要素に追加する関数
function addPanel(elem) {
// アコーディオンパネルを開くボタンのテキスト
let summaryOpenText = "Open";
// アコーディオンパネルを閉じるボタンのテキスト
let summaryCloseText = "Close";
if (elem) {
if(elem.hasAttribute('data-open-text')) {
summaryOpenText = elem.getAttribute('data-open-text');
}
if(elem.hasAttribute('data-close-text')) {
summaryCloseText = elem.getAttribute('data-close-text');
}
// details 要素を作成
const detailsElem = document.createElement('details');
detailsElem.classList.add(detailsClass);
// 作成した details 要素の HTML(summary 要素と div 要素)を設定
detailsElem.innerHTML = `<summary data-close-text="${summaryCloseText}">${summaryOpenText}</summary>
<div class="${detailsContentWrapperClass}">
<div class="${detailsContentClass}"></div>
</div>`;
// コードブロックのラッパー要素を details 要素でラップする
elem.insertAdjacentElement('beforebegin', detailsElem);
detailsElem.querySelector('.' + detailsContentClass).appendChild(elem);
}
}
}
// アコーディオンアニメーションの関数の定義(elem は特定の要素のみに適用する場合に指定)
function mySetupToggleDetailsAnimation(elem) {
// ボタンのラベル(summary 要素のテキストが空の場合)
const accodionOpenBtnDefaultLabel = 'Open';
// 閉じるボタンのラベル(summary 要素に data-close-text 属性が指定されていない場合)
const accodionCloseBtnDefaultLabel = 'Close';
// toggle-code-animation クラスの details 要素を全て取得
const details = document.getElementsByClassName('toggle-code-animation');
// 引数 elem が指定されていればその要素のみを対象に setupAccordion() を呼び出す(WordPress のプレビューモード用)
if(elem) {
setupAccordion(elem);
}else{
for(const elem of details) {
setupAccordion(elem);
}
}
// アコーディオンアニメーションを設定
function setupAccordion(elem) {
const summary = elem.querySelector('summary');
const content = elem.querySelector('.details-content');
const summaryText = summary.textContent.trim() ? summary.textContent : accodionOpenBtnDefaultLabel;
if (!summary.textContent.trim()) summary.textContent = summaryText;
const summaryCloseText = summary.dataset.closeText ? summary.dataset.closeText : accodionCloseBtnDefaultLabel;
let isAnimating = false;
summary.addEventListener('click', (e) => {
e.preventDefault();
if (isAnimating === true) {
return;
}
if (elem.open) {
summary.textContent = summaryText;
isAnimating = true;
const closeDetails = content.animate(
{
opacity: [1, 0],
height: [content.offsetHeight + 'px', 0],
},
{
duration: 300,
easing: 'ease-in',
}
);
const rotateIcon = summary.animate(
{ rotate: ["90deg", "0deg"] },
{
duration: 300,
pseudoElement: "::before",
easing: 'ease-in',
fill: 'forwards',
}
);
closeDetails.onfinish = () => {
elem.removeAttribute('open');
isAnimating = false;
}
} else {
elem.setAttribute('open', 'true');
summary.textContent = summaryCloseText;
isAnimating = true;
const openDetails = content.animate(
{
opacity: [0, 1],
height: [0, content.offsetHeight + 'px'],
},
{
duration: 300,
easing: 'ease-in',
}
);
const rotateIcon = summary.animate(
{ rotate: ["0deg", "90deg"] },
{
duration: 300,
pseudoElement: "::before",
easing: 'ease-in',
fill: 'forwards',
}
);
openDetails.onfinish = () => {
isAnimating = false;
}
}
});
}
};
例えば以下のように記述すると、
<div class="hljs-wrap">
<pre data-label="foo.js"><code>function hello() {
console.log("Hello!");
}</code></pre>
</div>
pre 要素の data-label 属性に指定した文字が右上に出力され、その他のオプションはデフォルトで表示されます。
function hello() {
console.log("Hello!");
}
以下は、ラッパー要素に toggle-accordion クラスとパネルボタンのテキストを、pre 要素に data-line-highlight でハイライトする行を、data-max-lines で表示する行数を指定してアコーディオンパネルで表示する例です。
<div class="hljs-wrap toggle-accordion" data-open-text="サンプルを見る" data-close-text="サンプルを閉じる">
<pre data-line-highlight="5, 15, 18-23, 35" data-max-lines="20"><code class="language-JavaScript">
・・・中略・・・
</code></pre>
</div>
以下のように表示されます。
import { __ } from "@wordpress/i18n";
// InspectorControls を追加でインポート
import { useBlockProps, InspectorControls } from "@wordpress/block-editor";
// TextareaControl, PanelBody, TextControl を components からインポート
import { TextareaControl, PanelBody, TextControl } from "@wordpress/components";
import "./editor.scss";
export default function Edit({ attributes, setAttributes, isSelected }) {
// テキストエリア(TextareaControl)の行数
const codeTextRowCount = attributes.codeText.split(/\r|\r\n|\n/).length;
const codeTextRows = codeTextRowCount > 3 ? codeTextRowCount : 3;
// ブロックの内容を JSX で返す
return (
<>
<InspectorControls>
<PanelBody title={__("Settings", "my-highlight-block")}>
<TextControl
label={__("Language", "my-highlight-block")}
value={attributes.language || ""}
onChange={(value) => setAttributes({ language: value })}
/>
</PanelBody>
</InspectorControls>
<div {...useBlockProps()}>
<TextareaControl
label={__("Highlight Code", "my-highlight-block")}
value={attributes.codeText}
onChange={(value) => setAttributes({ codeText: value })}
rows={codeTextRows}
placeholder={__("Write your code...", "my-highlight-block")}
hideLabelFromVision={ isSelected ? false : true}
/>
</div>
</>
);
}