/ 最近 .rdf 追記 編集 設定 本棚

脳log[20090313] SHJSに行番号表示機能を。



2009年03月13日 (金)

最終更新: 2014-12-24T10:54+0900

[SHJS] SHJSに行番号表示機能を。

個人的にはなくても不便はないけども、番号を表示する方法のアテができたので。

 番号を表示する方法

求める条件は

  • 選択&コピーで(改行コードまでは無理にしても)元のソースコードが手に入ること。余分に行番号までコピーされるなんてもってのほか。
  • 開始行番号が指定できること。できれば非推奨な <ol start="555">以外の方法で。(Chili 2.2はこの方法だった)

SyntaxHighlighterのように "view source", "copy to clipboard"機能を用意して、行番号が一緒にコピーされる欠点をカバーするのは次善の策。

WP-Syntaxやっているように、1行 2列のテーブルを作って、左の列に行番号を、右の列にハイライトされたソースコードを配置するのは、サポートするブラウザも多そうで良さげ。

でも行番号あり・なしで二通りの出力フォーマットを用意するのはスクリプトのサイズの面から避けたい。何と言っても、忘れていたけど、shjs-0.4.2をいじくったものであるこの日記の /shjs/sh_main.js はハイライトするついでに、各行に <span class="odd">、<span class="even">というタグをかぶせていたのだった(しかも 3行の追加だけで)。その方面でいくことにする。

つまり、CSS2の counter-reset, counter-increment, counter に全面的に頼った方法。contentで追加した文字列がコピペ不能なのがかえって幸いした。Fx 3.0.7、Safari 3.2.2、Opera 9.64、IE8で期待通りの表示を確認した。(※末尾に追記あり)

 スタイルシートに追加したルール

pre.sh_sourceCode.sh_numbered .odd:before,
pre.sh_sourceCode.sh_numbered .even:before {
  counter-increment: sh_sourceCode;
  content: counter(sh_sourceCode, decimal-leading-zero) ": ";
}

 sh_main.js (version 0.4.2)に加えた変更

--- sh_main.js-0.4.2	Mon May 12 23:07:40 2008
+++ sh_main.js	Fri Mar 13 23:29:34 2009
@@ -60,6 +60,8 @@
     currentStyle = style;
   };
 
+  var oddLine = false;
+
   var endOfLinePattern = /\r\n|\r|\n/g;
   endOfLinePattern.lastIndex = 0;
   var inputStringLength = inputString.length;
@@ -78,6 +80,7 @@
     }
 
     var line = inputString.substring(start, end);
+    builder.startElement((oddLine = !oddLine) ? 'odd' : 'even');
 
     var matchCache = null;
     var matchCacheState = -1;
@@ -158,6 +161,7 @@
       builder.endElement();
     }
     currentStyle = undefined;
+    builder.endElement();
     if (endOfLineMatch) {
       builder.text(endOfLineMatch[0]);
     }
@@ -307,8 +311,13 @@
 @param  element  a DOM <pre> element containing the source code to be highlighted
 @param  language  a language definition object
 */
-function sh_highlightElement(htmlDocument, element, language) {
+function sh_highlightElement(htmlDocument, element, language, firstline) {
   sh_addClass(element, "sh_sourceCode");
+  if (firstline !== null && ! isNaN(firstline)) {
+    // cssのセレクタで区別できるように。
+    this.sh_addClass(element, "sh_numbered");
+    element.style.counterReset = "sh_sourceCode " + (parseInt(firstline) - 1);
+  }
   var inputString;
   if (element.childNodes.length === 0) {
     return;
@@ -345,7 +354,8 @@
       if (prefix === "sh_") {
         var language = htmlClass.substring(3);
         if (language in sh_languages) {
-          sh_highlightElement(htmlDocument, element, sh_languages[language]);
+        // firstlineなんて非標準属性をでっちあげないで
+        // スクリプトにパラメータを渡す方法は?
+        // (class属性を乱用するのは気に入らない)
+          sh_highlightElement(htmlDocument, element, sh_languages[language], element.getAttribute("firstline"));
         }
         else {
           throw "Found <pre> element with class='" + htmlClass + "', but no such language exists";

sh_main.js (version 0.5)への変更も似たようなものだけど、sh_load()の中にも変更すべき場所がある。

sh_main.js (version 0.6)を対応させるのは面倒なので省略。0.4.2も実はそうだったんだけど、shjsはスタイルのネストを想定していない。例えばこれ。

// URL inside comment. <http://vvvvvv.sakura.ne.jp>

ハイライトされた結果の HTMLはこうなる。

<span class="sh_comment">// URL inside comment. &lt;</span><span class="sh_url"><a class="sh_url" href="http://vvvvvv.sakura.ne.jp">http://vvvvvv.sakura.ne.jp</a></span><span class="sh_comment">&gt;</span>

フラットな構造で、comment, url, commentと 3つの要素が並んでいる。commentが urlを含むような構造にはならない。0.6ではハイライト前後のタグ構造をマージする仕組みになっているから、0.4.2や 0.5のように ad hocなごまかしができなくて、まずはこの前提を取り払わなければいけない……。

 (付録) hikidoc.rb (revision 108) への変更

<<< language, number
>>>

<pre class="sh_language" firstline="number">
</pre>

に変換します。

--- hikidoc.rb.108	Thu Aug 28 22:11:00 2008
+++ hikidoc.rb	Fri Mar 13 23:05:05 2009
@@ -335,7 +378,7 @@
     @output.preformatted(@output.text(text))
   end
 
-  BLOCK_PRE_OPEN_RE = /\A<<<\s*(\w+)?/
+  BLOCK_PRE_OPEN_RE = /\A<<<\s*(.*\S)?/
   BLOCK_PRE_CLOSE_RE = /\A>>>/
 
   def compile_block_pre(f)
@@ -665,9 +706,18 @@
     end
 
     def block_preformatted(str, info)
-      syntax = info ? info.downcase : nil
+      syntax, firstline = *(info ? info.split(/\s*,\s*/) : [])
+      syntax = syntax.downcase if syntax
+      firstline = /\A[-+]?\d+\z/.match(firstline).to_a[0] if firstline
       if syntax
         begin
+          attr_firstline = firstline ? %Q( firstline="#{escape_html_param firstline}") : ""
+          @f.print %Q(<pre class="sh_#{escape_html_param syntax}"#{attr_firstline}>), text(str), "</pre>\n"
+          @f.puts inline_plugin(%Q(shjs #{syntax.dump}))
+          return  
+
           convertor = Syntax::Convertors::HTML.for_syntax(syntax)
           @f.puts convertor.convert(str)
           return

 追記: コピペまでは試してなかった。

お試しください。

Firefox> 行番号が選択されたり選択されなかったりする。見た目の選択範囲に関わらず行番号はコピーされない。
Safari> 行番号も選択範囲に入るがコピーはされない。
Opera> 行番号がコピーされる。
IE8> 行番号がコピーされる。
ダメダメだあ。(この機能は封印しよう)

 追記@2009-03-27: 最後で全部ひっくりかえしちゃった。

やっぱり一行二列の表を作る方法でいくことにした。

この方法だと、preを一旦取り除いて tableの下に追加する関係からか、同じ preにハイライト処理が二回走ってしまう(getElementsByTagName()で得られる NodeListが liveである、ということ)。sh_sourceCodeというクラス名を目印に、二度目以降の処理をスキップするよう動作を変更した。

Internet Explorerは 8になっても一筋縄ではいかないようで……。左右の列の<pre>の高さが、デフォルトスタイルシートと同じ font-size:80%でないと揃わない。「IE8互換表示」や「IE7」モードだと揃うんだけど。

二重処理を防ごうと Array.prototype.sliceを使って NodeListを Arrayに変換しようと思ったらまたしても IEの壁。オブジェクトを指定してください、と相変わらずわかりにくいエラーメッセージ。(prototype.jsが愚直にループをまわしてるのは IEのせいかもね)

<table>を使うとその中の <pre>の幅が、内容に同期している(最小にして十分なサイズ)。他の <pre>と同じように、いつでも本文と同じ幅に揃えたいなー。

 追記@2009-04-02: 0-paddingオプションとか。(暇だなあ)

<pre class="sh_javascript" firstline="00339">
  このような <pre>を出力すると……
</pre>
sh_putLinenumber: function(element, param, inputString) {
  var startline = parseInt(param, 10);
  var opt = /^([-+]?)(0*)(\d+)/.exec(param);
  var opt_explicit_sign = (opt[1] === '+') ? '+' : '';
  var opt_zero_padding = (0 !== opt[2].length) ? new Array(opt[2].length + opt[3].length + 1).join('0') : '';
  var re_zero_padding = new RegExp('^0+(?=\\d{' + opt_zero_padding.length + '})');
  var nums = inputString.match(/(?:\r\n?|\n)(?!$)|$/g);
  if (0 !== opt_explicit_sign.length || 0 !== opt_zero_padding.length) {
    for (var i = 0; i !== nums.length; ++i) {
      nums[i] =
        (0 < startline + i ? opt_explicit_sign : startline + i < 0 ? '-' : '') +
        (opt_zero_padding + Math.abs(startline + i)).replace(re_zero_padding, '') +
        nums[i];
    }
  }
  else {
    for (var i = 0; i !== nums.length; ++i) {
      nums[i] = '' + (startline + i) + nums[i];
    }
  }

  var d = element.ownerDocument;
  var e = {table:'table', tbody:'tbody', tr:'tr', tdLeft:'td', pre:'pre', tdRight:'td'};
  for (var p in e) {
    e[p] = d.createElement(e[p]);
  }
  element.parentNode.replaceChild(e['table'], element);
  e['table'].appendChild(e['tbody']).appendChild(e['tr']);
  e['tr'].appendChild(e['tdLeft']).appendChild(e['pre']).appendChild(d.createTextNode(nums.join('')));
  e['tr'].appendChild(e['tdRight']).appendChild(element);
  e['table'].className = 'sh_sourceTable';
  e['pre'].className = 'sh_sourceCode sh_numbers';
  return element;
},

341-344、346-354、358行がオプションのために追加した部分。(SyntaxHighlighter2.0の行ハイライト機能はこういときにつかうのだな。イラネと思っていたのを改めます)

固定幅というわけではなくて、0の数以上に繰り上がれば桁が増える。上の場合では 99999行目を超えたとき。テストする段で気付いたが、最低でも 92行のソースコードを貼らないと恩恵に与れない……。

正規表現を持ち出すまでもなく、適切な数の 0をくっつけるだけでよかったんだね(> JavaScriptのビルトインオブジェクトの拡張:ゼロパディング - 気まぐれショウルーム)。先に調べよう。過去に、適切な数の 0を知るために log(10)をとればいいと無邪気に考えていた苦い記憶があるので、最初に正規表現を持ち出してしまった(それもどうだ?)という事情があったりもするんだけど。(Number.toString(10).lengthで済んでしまうなんて!)

 345行目の /(?:\r\n?|\n)(?!$)|$/g というパターンについて

<pre>直後の改行は存在しないかのように扱われるが、</pre>直前の改行は存在する(スクリプトで取得できる)ものの表示されない(4つのブラウザで確認)。というわけで、末尾の空行に行番号を付けてしまうと列の左右で行の数が一致しなくなる(だから除外する)。


Google Chrome(1.0.154.53)は

<pre>.innerHTML = <pre>.innerHTML

とやるたびに先頭の改行文字を取り除いていってしまうんじゃないか? 個別のブラウザ対応は切りがないし、完全対応は不可能なので、行番号を付けるときは <pre>の最初と最後の行を空行にしないのが最も安全。

トンデモ IEさんは <pre>.innerHTML= だろうと <tag style="white-space:pre">.innerHTML= だろうと空白をトリミングしてくれますしね。

 追記@2009-04-04: 一行二列の TABLE方式にしたら shjs-0.6での対応も簡単だったので。

テストが不十分なので、langファイルの自動読み込み部分など、一度も実行されていない部分が動くかは不明。

最小化方法は JSMin。ためしに YUI Compressorにもかけてみたがローカル変数の短縮を全くやってくれなくて JSMinと大差ない結果だった。一番外側の無名関数の実行部分をとりのぞいたらちゃんとローカル変数名の短縮もやってくれた。

小手先の変更もいくつか加えた。(ブラウザ判別コードの実行を一度だけにしたり << 関数の中で分岐するんでなく、判別結果で関数を取り換える)