Yet Another SyntaxHighlighter
探してたわけではないのだけれど highlight.js とかいうのが軽いらしいのだ。
というわけで、SyntaxHighlighter から highlight.js への切り替えの顛末。
tl;dr
- highlight.js
- こんな感じで使う
- HTML のエスケープが必要
- 予約語が Bold になるのがなあ
- 行番号がつかない
- 行のハイライトもない
- SyntaxHighlighter からの移行
- タブの空白変換
- fc2 のテンプレートエディタの問題
- というわけで
- code タグで使える カスタム data 属性
- escape html
- マーカー行
- 常に行番号が表示される : Android
- コードのエリアが右端で折り返しになっている : Android
- マーカー行の改善
- code 直後の改行の削除
- 後でやるかも
- 参考URL
- 試してないやつ
こんな感じで使う
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/dracula.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<pre><code class="hljs css">
.elem {
font-size: 16px;
}
</code></pre>
HTML のエスケープが必要
SyntaxHighlighter は、pre で HTML のタグを囲っても、それなりに表示してくれたんだけど、highlight.js は text content を扱うだけ。
まあ、普通と言えば普通。
こうやれば良い、かな。
window.addEventListener("DOMContentLoaded", function() {
Array.prototype.forEach.call(document.querySelectorAll("pre > code"), function(code) {
var text = document.createTextNode(code.innerHTML);
code.innerHTML = "";
code.appendChild(text);
hljs.highlightBlock(code);
});
});
あー、でも pre > code の中で、開始・終了タグが一致していないと崩れちゃう。
SyntaxHighlighter は、どうやってるんだ?
→ 崩れるのは一緒だった :-)
予約語が Bold になるのがなあ
桁がずれちゃう。
あ、Bold でも幅が変わらないフォントを使うのか。
pre.hljs code {
font-family: monospace;
}
行番号がつかない
作成者の信条だったら仕方ない。
別に用意した人がいるのな。
https://github.com/wcoder/highlightjs-line-numbers.js/
左側に float: left; な行番号用の要素を入れてる。
同じフォント、line-height を使うことで、コード部分とずれることが無い(はず)。
開始行番号は 1 に固定されてるのか。
このプラグインだと、行番号を文字で埋めてるけど、ol タグを使うという手もある。
こっちの方が長いな (´・ω・`)
これだと、start 属性で開始番号を変えられるとか思ったけど、どうせ javascript で要素を作るんだから変わんねえか。
行のハイライトもない
レンダリングされた code の中を見ると分かるけど、そもそも行単位のマーキングをしていない。
それが軽さにつながるんだろうし、行番号を導入しづらい理由でもある(いや、信条だった)。
こんな要素を行番号の前に入れると、行のハイライトもできる。
width: 100% だと、右にはみ出る。
body に margin-left が設定されていると、.marker の left も調整しなきゃいけない。
イマイチ (´・ω・`)
SyntaxHighlighter からの移行
- pre の内側を code でくくる
- brush: の指定を pre の class に移す(省略しても良さげ)
だけで移行できる。
とはいえ、いちいち手で直していくの面倒(たいして記事は書いてないけれど)。
これもスクリプトで対応だな。
- pre[class*="brush:"] で抽出
- html escape をするついでに、code 要素を間に挟む
- class の brush: から言語を取り出して、code のクラスに追加する
言語の指定は、変換しなくてもそのまま使えそう。
http://highlightjs.readthedocs.io/en/latest/css-classes-reference.html#language-names-and-aliases
タブの空白変換
configure() の tabReplace って、行頭のタブしか変換してくれないのな (´・ω・`)
fc2 のテンプレートエディタの問題
スクリプト中の正規表現にある \r\n が、テンプレートとしてそのままアップロードできない。
改行に変えられちゃう。
多分、テンプレートエディタだけじゃなくテキスト更新の全般の問題。
この記事をアップしたときも、記事中のスクリプトの "<" が "<" に変換されて困った。
<script> </script> の中なのに :-|
しかも、全ての "<" が駄目なわけじゃなくて、あるひとつだけが。
意味わからん。
悩んでても仕方ないので、外部スクリプトにする。
fc2 のファイルアップロードは操作が面倒なので、置き場所は、とりあえず Gist 。
と思ったら、 Gist の RAW は、Content-Type を text/plain で返すので、スクリプトとして読み込めない。
http://qiita.com/cognitom/items/041b48d8cf746ab54f06
というわけで、githack を利用。
Gist の URL は、リビジョン(ハッシュ)がつくので、更新する度に URL が変わる。
こちらも、面倒っちゃあ面倒だったのだ。
というわけで
こんな感じに。
HTML
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/styles/agate.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/highlight.min.js"></script>
<script src="https://gist.githack.com/a-kuma3/114172cfc6a78b66c42fe7e6379182b0/raw/811043179755d3929627c44edf735772c9a3fa1c/hljs_akuma3.js"></script>
javascript
window.addEventListener("DOMContentLoaded", function() {
hljs.configure({
tabReplace: ' ', // 4 spaces
});
function create_escaped_textnode(ele) {
return document.createTextNode(
ele.innerHTML
.replace(/^[\r\n]+|[\r\n]$/g, "")
.replace(/</g, "<") // single "<" already escaped in innerHTML
.replace(/>/g, ">")
);
}
function hijs_escape_html(e) {
if (e.dataset.notEscapeHtml === undefined) {
var text = create_escaped_textnode(e);
e.innerHTML = "";
e.appendChild(text);
}
return e;
}
function hijs_migrate_syntaxhighlighter(e) { // pre[class*="brush:"]
var text = create_escaped_textnode(e);
e.innerHTML = "";
var code = document.createElement("code");
code.appendChild(text);
e.appendChild(code);
var m = /brush:\s*(\w+)/.exec(e.className);
if (m) {
code.classList.add(m[1]);
}
m = /highlight:\s*\[([^\]]+)\]/.exec(e.className);
if (m) {
code.dataset.markerLine = m[1];
}
/* TODO:
gutter
first-line
*/
return code;
}
function hijs_modify_line_number(code, start, marker) {
var lines = code.textContent.split(/\r\n|\r|\n/).length;
if (lines > 1) {
var need_linenumber = start > 0;
var need_marker = marker && marker.length > 0;
var line_content = need_linenumber ?
function(n) { return n + "."; } :
function(n) { return ""; };
var marker_map = {};
if (need_marker) {
marker.forEach(function(item) {
marker_map[item] = true;
});
}
var r = code.parentNode.getBoundingClientRect();
var marker_style = "width: " + (r.width - 6*2) + "px;";
var i = need_linenumber ? start : 1;
var nums = [];
while (lines--) {
var n = i++;
var line = line_content(n);
if (marker_map[n]) {
line = ' ' + line;
}
nums.push(line);
}
var e = document.createElement("code");
e.className = "hljs hljs-line-numbers" + (need_linenumber ? "" : " only-marker");
e.style.float = "left";
e.style.textAlign = "right";
e.innerHTML = nums.join("\n");
code.parentNode.insertBefore(e, code);
}
}
// line number
function extract_attr(code, attr) {
var lineNumber = code.dataset.lineNumber;
if (lineNumber !== undefined) {
attr.start_line = lineNumber === "" ? 1 : code.dataset.lineNumber;
}
var markerLine = code.dataset.markerLine;
if (markerLine !== undefined) {
attr.marker_line = markerLine.split(/[,\s]+/).reduce(function(a, i) {
if (/^\d+$/.test(i)) {
a.push(parseInt(i,10));
}
return a;
}, []);
console.log(attr.marker_line);
}
return attr;
/* TODO:
check parent node (PRE) ?
*/
}
Array.prototype.forEach.call(document.querySelectorAll("pre > code"), function(code) {
code = hijs_escape_html(code);
code.parentNode.classList.add("hljs-container");
hljs.highlightBlock(code);
});
// migration for SyntaxHighlighter
Array.prototype.forEach.call(document.querySelectorAll('pre[class*="brush:"]'), function(code) {
code = hijs_migrate_syntaxhighlighter(code);
code.parentNode.classList.add("hljs-container");
hljs.highlightBlock(code);
});
Array.prototype.forEach.call(document.querySelectorAll("pre > code"), function(code) {
var attr = extract_attr(code, {});
hijs_modify_line_number(code, attr.start_line, attr.marker_line);
});
/* TODO:
copy code to clipboard
*/
});
CSS
pre.hljs-container {
white-space: pre;
}
pre > code.hljs {
font-family: Menlo, Consolas, 'DejaVu Sans Mono', monospace;
}
.hljs-line-numbers {
border-right: 1px solid gray;
padding-left: 1em;
}
.hljs-line-numbers.only-marker {
padding-left: 0;
padding-right: 0;
border-right: none;
}
.hljs-line-numbers .marker {
width: 100%;
position: absolute;
background-color: rgba(255,255,255,0.4);
margin: 0 6px;
}
Gist にもアップしてある。
https://gist.github.com/a-kuma3/114172cfc6a78b66c42fe7e6379182b0
code タグで使える カスタム data 属性
data-line-number
行番号を表示する。
属性だけを指定するか、数値を指定。
数値を指定した場合には、その数値から行番号が始まる。
data-marker-line
マーカー行の指定。
カンマ区切りで行番号を指定する。
data-not-escape-html
HTML のエスケープをしない。
script や style を書くと そのまま動いちゃうので、手で HTML のエスケープをしなきゃいけないこともある。
その場合は、data-not-escape-html を指定して、code の中の記述をそのまま使う。
escape html
タグの一部と見なされない "<" や ">" があると、innerHTML で取得したときには実体参照の "<" や ">" になっている。
なので、実体参照を文字に直さなくちゃいけない。
ele.innerHTML
.replace(/^[\r\n]+|[\r\n]$/g, "")
.replace(/</g, "<") // single "<" already escaped in innerHTML
.replace(/>/g, ">")
);
ちなみに、最初の置き換えは pre
に続く code
の後にある改行が空行扱いされちゃうことの対処。
マーカー行
サイズの指定が無理やり感。
pre の幅が欲しいんだけど、間に code が挟まっているから width: inherit;
は使えず。
width: 100%;
はさらに上位の幅が撮れちゃう。
body に margin-left が指定してあると、left
も指定しないと左位置がずれちゃう (´・ω・`)
HTML と CSS だけでどうにかならんものかな……
常に行番号が表示される : Android
カスタム data 属性がサポートされているものの、属性を指定していないときに undefined ではなくて空文字列が返される。
undefined との比較ではなく、hasAttribute メソッドを使って判定する。
// if (code.dataset.lineNumber !== undefined) {
if (code.hasAttribute("data-line-number")) {
attr.start_line = code.dataset.lineNumber === "" ? 1 : code.dataset.lineNumber;
}
コードのエリアが右端で折り返しになっている : Android
テンプレートの css で pre に指定されている word-wrap も打ち消さなくちゃいけない。
pre.hljs-container {
white-space: pre;
word-wrap: normal; /* ← break-word */
}
マーカー行の改善
getBoundingClientRect()
メソッドで幅を求めて、style に絶対値を直接指定してたのを、要素の配置とかを変えてスタイルシートだけでどうにかしてみた。
<pre>
<code class="hljs-markers"> <-- 行番号とは別のエリアにする -->
...
<span class="marker"> </span>
...
</code>
<code class="hljs-line-numbers">
</code>
<code>
</code>
</pre>
pre {
position: relative;
}
.hljs-markers {
display: block;
position: absolute;
width: 100%;
}
.marker {
position: absolute;
background-color: rgba(255, 180, 180, 0.20);
left: 6px;
right: 6px;
}
- 親を static か relative にして
- 子供を absolute にし
- left と right を両方指定する
code 直後の改行の削除
html の escape とは別に、pre の内側の code タグの直後、直前にある改行を取り除く処理を関数化。innerHTML
+ String.replace
よりは、DOM で操作した方が速いような気がするのだ。
function chomp(e) {
var c = e.firstChild;
if (c.nodeType == 3 && c.data.length > 0 && c.data[0] == "\n") {
c.deleteData(0, 1);
}
c = e.lastChild;
if (c.nodeType == 3 && c.data.length > 0 && c.data[c.data.length - 1] == "\n") {
c.deleteData(c.data.length - 1, 1);
}
}
処理が重複しているように見えるので、改善するとこんな感じか。
function chomp(e) {
function remove_newline(node, ofs) {
if (node.nodeType == 3 && node.data.length > 0) { // 3 : Text
if (ofs < 0) {
ofs = node.data.length - 1;
}
if (node.data[ofs] == "\n") {
node.deleteData(ofs, 1);
}
}
}
remove_newline(e.firstChild, 0);
remove_newline(e.lastChild, -1);
}
んー、ベタに書いた方が分かりやすい気がするし、多分、速くなるわけでもない。
却下。
後でやるかも
- SyntaxHighlighter の gutter と first-line の migrate(使ってなかった気がする)
- カスタム data 属性は、code だけじゃなくて、上位の pre についてても読み取る
- コードをクリップボードにコピー
マーカー行のスタイルnot-escape-html で code 直後の改行が残るAndroid の標準ブラウザで崩れる常に行番号が表示されるコードのエリアが右端で折り返しになっている
参考URL
- highlight.js
- シンタックスハイライトをgoogle code prettifyからhighlight.jsに変えた | masuP.net
- ソースコードをハイライトする軽量ライブラリ、Highlight.jsの使い方
試してないやつ
Google Code Prettify
https://github.com/google/code-prettify
http://mae.chab.in/archives/2963
http://blog.s-giken.net/230.html
最初に試したのがこれ。
ちょろっと試してみたんだけど、なんか、うまく動かんかった。
highlight.js の方は素直に動いたので、こちらは見限った。
Prism.js
http://prismjs.com/
https://webtatan.com/blog/wordpress/prismjs
言語をあらかじめ選んでおかなくちゃいけない、というところにひっかかって、試してない。
プラグインは良いよね。
HTML エスケープが必要なら、別にこれじゃなくても、って感じ。
Unescaped Markup というプラグインで HTMLエスケープをしなくてもハイライトの対象にできる。
- 関連記事
-
- RSSリーダーのブログパーツ (2010/12/04)
- ん、SNS系ボタンが設置できるようになってる (2011/12/16)
- はてなスター (2013/01/01)
- meta description を変えてみた (2015/09/16)
- Yet Another SyntaxHighlighter (2017/01/27)