/ 最近 .rdf 追記 設定 本棚

脳log[SHJS: 2009-03-13~]



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と大差ない結果だった。一番外側の無名関数の実行部分をとりのぞいたらちゃんとローカル変数名の短縮もやってくれた。

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


2008年12月23日 (火) 新☆はてブ < スクロールが重いからピンを抜いたのにカテゴリを選び直すだけで元に戻る鳥頭。そこは Cookieを使うべきところです。(潔癖症の人はどうせ拒否設定してるでしょう)

[SHJS][javascript] SHJS-0.6がリリースされていた。(2008年12月15日)

アナウンスされている変更点は…… (注: 日本語部分は俺の勝手な訳のような注釈のようなもの)

December 15, 2008 - SHJS 0.6

SHJS 0.6 is available for download.

SHJS 0.6 includes several new features, improvements and bug fixes:

  • SHJS is now distributed under version 3 of the GNU General Public License. (Older releases of SHJS were distributed under version 2 of the GNU GPL.)

    ライセンスが GPLv2から GPLv3へ変更。

  • Markup inside pre elements is now preserved.

    PREタグの中の HTMLマークアップが保存される。(以前は Node.dataを再帰的に取り出したもの。乱暴にいうと PRE.{innerText|textContent}に相当するものが利用されていた。ver.0.5ではマークアップとして <br>のみが考慮されていた。)

  • Several new languages (from the latest release of GNU Source-highlight) are included: S-Lang, Scala, Java properties files, Desktop files, LSM (Linux Software Map) files, Xorg configuration files, RPM spec files, Haxe, LDAP files, GLSL, Objective Caml, Standard ML, JavaScript with DOM, and C (separate from the C++ language file).

    最新の GNU Source-highlightから新しい言語ファイルを追加。JavaScriptには DOMキーワードを含んだ lang/sh_javascript_dom.jsが追加された。(sh_javascript_dom = sh_javascript + applicationCache|closed|Components|content|controllers|crypto|defaultStatus|dialogArguments|directories|document|frameElement|frames|fullScreen|globalStorage|history|innerHeight|innerWidth|length|location|locationbar|menubar|name|navigator|opener|outerHeight|outerWidth|pageXOffset|pageYOffset|parent|personalbar|pkcs11|returnValue|screen|availTop|availLeft|availHeight|availWidth|colorDepth|height|left|pixelDepth|top|width|screenX|screenY|scrollbars|scrollMaxX|scrollMaxY|scrollX|scrollY|self|sessionStorage|sidebar|status|statusbar|toolbar|top|window + alert|addEventListener|atob|back|blur|btoa|captureEvents|clearInterval|clearTimeout|close|confirm|dump|escape|find|focus|forward|getAttention|getComputedStyle|getSelection|home|moveBy|moveTo|open|openDialog|postMessage|print|prompt|releaseEvents|removeEventListener|resizeBy|resizeTo|scroll|scrollBy|scrollByLines|scrollByPages|scrollTo|setInterval|setTimeout|showModalDialog|sizeToContent|stop|unescape|updateCommands|onabort|onbeforeunload|onblur|onchange|onclick|onclose|oncontextmenu|ondragdrop|onerror|onfocus|onkeydown|onkeypress|onkeyup|onload|onmousedown|onmousemove|onmouseout|onmouseover|onmouseup|onpaint|onreset|onresize|onscroll|onselect|onsubmit|onunload)

  • Many other languages have minor improvements.

    言語ファイルのアップデート。

  • Compressed .min.css stylesheets are now included in the distribution.

    最小化した CSSファイルを同梱。(.jsも .cssも YUI Compressorを使用。ver.0.5までは .jsのみが JSMinで処理されていた)

Please note that the format of language-specific JavaScript files has changed in SHJS 0.6. JavaScript language files from version 0.6 will not work with sh_main.js from previous releases, and vice versa. Make sure you upgrade both the sh_main.js file and language-specific files.

古い言語ファイル(lang/*.js)と新しいメインスクリプト(sh_main.js)は互換性がない(逆も同じ)。両方入れ替えるべし。

大きな変更は <pre></pre>内の HTMLマークアップがシンタックスハイライト後も保存されること。(タグがたすき掛けになるときはどうするんだろ?)

言語ファイルの変更は小さくて、"next"、"regex"、"style"、"exit"というプロパティを持ったパターンオブジェクトが、3要素の配列になっている。

 異種document対応が必要では? (sh_main.js)

function sh_highlightElement(element, language) {
  sh_addClass(element, 'sh_sourceCode');
  var originalTags = [];
  var inputString = sh_extractTags(element, originalTags);
  var highlightTags = sh_highlightString(inputString, language);
  var tags = sh_mergeTags(originalTags, highlightTags);
  // この documentFragmentはグローバル変数の document由来。
  var documentFragment = sh_insertTags(tags, inputString);
  while (element.hasChildNodes()) {
    element.removeChild(element.firstChild);
  }
  // element.ownerDocument != documentFragment.ownerDocumentのとき失敗しませんか?
  element.appendChild(documentFragment);
}

コメントを参照のこと。IEのバージョンが 5.5くらいだった時に失敗したような記憶が根拠で、確証はないし、レアケースだとは思うけど。(フレームをまたいで sh_highlightElement(element, language)を呼び出したとき(=スクリプトとエレメントが異なるドキュメントに属するとき)に起こるかなぁ?)

 タグのたすき掛けの結果

 before
<pre class="sh_ruby">
require 'sqlite3'
<strong>require</strong> 'sqlite3'
<strong>req</strong>uire 'sqlite3'
</pre>
<pre class="sh_javascript">
/*
http://example.com
http://example<em>.</em>com
*/
</pre>
 after
<pre class="sh_ruby sh_sourceCode">
<span class="sh_preproc">require</span> <span class="sh_string">'sqlite3'</span>
<strong><span class="sh_preproc">require</span></strong> <span class="sh_string">'sqlite3'</span>
<strong><span class="sh_preproc">req</span></strong><span class="sh_preproc">uire</span> <span class="sh_string">'sqlite3'</span>
</pre>
<pre class="sh_javascript sh_sourceCode">
<span class="sh_comment">/*</span>
<a href="http://example.com" class="sh_url">http://example.com</a>
<a href="http://example.com" class="sh_url">http://example</a><em><a href="http://example.com" class="sh_url">.</a></em><a href="http://example.com" class="sh_url">com</a>
<span class="sh_comment">*/</span>
</pre>

SHJSの挿入するタグは必要に応じてぶつ切りにされるみたい。

 追記@2009-02-25: 手製の言語ファイル( rubyと javascript)を 0.6フォーマットに変換した。(最小化方法は依然として JSMin)

移行スクリプトはこれ( migrate_05_06.js )。shjs-0.5までの lang/sh_*.jsファイルをドロップすると lang/sh_*.06.jsというファイルができてくるという寸法。ちなみに JScript製。

テストもかねてバージョン 0.6を走らせてみたけど、軽くなってる道理がない*ので、この日記では shjs-0.4.2に手を入れたものを使い続けている。

 追記@2009-04-06: 0.6いいね。

言語ファイルのフォーマット変更は速度的に有利。

パターンマッチの結果を Stateをまたいで保存するようになっているので、この日記の sh_ruby.jsのようにあっちこっち跳びまわる言語ファイルに有利に働く。いちばん時間を消費しているのが RegExp.exec()と DOMツリーへの HTML断片の追加なのでパターンマッチ結果のキャッシュは大事。(もっとも 0.4.2のときからキャッシュの拡大は個人的にやっていた)

* <pre>内のマークアップを保存するためにハイライト前と後の、二つの HTML文字列をマージしている。でもその機能、俺個人はいらないのよね。


2008年08月20日 (水) [Firefox] CSSの contentで追加した文字はコピペできない。アイタタタ

[SHJS] SHJS-0.5 がリリースされてた。(2008-08-18)

  • SHJS can now automatically load language script files (feature request #2007022 - thanks to Michal Nazarewicz and Eugene Marcotte).
  • Highlighting of C/C++ and JavaScript has been improved.
  • A new language file for Oracle SQL has been added (thanks to Mike Breeze).
  • Case-insensitive regular expressions are now handled more efficiently.
  • SHJS now treats <br> tags as line terminators in input (bug #2054144 - thanks to Altforweilerer).
  • Compressed .min.js files are now generated with YUI Compressor.

 メモ

  • languageファイルの自動読み込みは Msxml2.XMLHTTPか XMLHttpRequestを利用する。

    <script>タグを挿入するのかも*と思っていたが今風のやり方だった。(SourceForgeの Feature Requestsで両派の議論があったようで、0.5では Asynchronous XMLHttpRequestを採用したけど将来かわるかもー、だって)

  • lang/sh_javascript.jsの全変更点(多分)は

    • import, package, prototypeキーワードのハイライトがなくなった。
    • 正規表現リテラルのハイライトルールが追加された。
    • class, interfaceのハイライトルールがなくなった。
    • $を含む関数名に対応した。
    • /**コメント*/ と /*コメント*/ のネストに対応しなくなった。

    新たに対応したものも対応が外れたものもあるが、どちらも JavaScriptの仕様に近づくための変更という点は共通。

    それでもやっぱり Javaっぽいのは、java.langまるごとインクルードだった前バージョンの source-highlight-lang/javascript.langが、java.langの中身をベースに足し引きしたものに変わったに過ぎないから。

  • 「大文字小文字を無視する正規表現を効率的に」っていうのは lang/sh_sql.jsを見るにこういうこと。

    /[Vv][Aa][Rr][Cc][Hh][Aa][Rr]/ // ver. 0.4.2
    /VARCHAR/i                     // ver. 0.5

    ……。

  • YUI Compressorはローカル変数名を縮めることで JSMin以上の圧縮を図る。

    他にも、obj["prop"]を obj.propに(可能なら)したり、連結されるリテラル文字列を予め一つにしたり、オブジェクトノーテイション(って言うの?)のプロパティ名部分の引用符を取り除いたり({"p1":v1, "p2":v2} -> {p1:v1, p2:v2})、するらしい。

    おれは JSMinでアグレッシブ(level 3 of 3)に最小化する(と、不要な空白と全ての改行が取り除かれるので、セミコロンインサーションの余地がなくなって、一つのセミコロンも省略できなくなる)のが好きなんだけど。何より Javaの実行環境がないから、YUI Compressorは動かない……。

 SHJSについて (勝手に宣伝)

  •  JavaScriptで実装されています

    ブラウザ(クライアントサイド)で実行されるのでサーバーの負荷が増えません。

  •  ダウンロード量は最小限に保たれます

    メインスクリプト(最小化されたものが数KB)は必須ですが各言語ファイルはオプションです。名前も知らない言語の定義ファイルまでブラウザにダウンロードしてもらう必要はありません。

  •  ハイライト対象の言語名の指定が必須です

    自動認識のような不確かなものに頼りません。(自分の書いているものが何語なのか知らない人は少ないでしょう。あえて情報を削ってスクリプトの仕事を増やす必要はありません)

  •  言語の定義はなんでもありです

    言語ファイルは、状態オブジェクトの配列です。各状態は正規表現のパターンを一つ以上持ち、マッチしたパターンにより配色と遷移先を決定します。状態が増えるのをいとわなければ何でもできます

    ダブルクォーテーション文字列や数字や URLなどにマッチする正規表現を並べて順番にマッチングさせるだけのハイライターより一段上のパターン認識が可能です。(有名どころの SyntaxHighlighter 2.0は XRegExpというライブラリを使用していて、これが Perl5や鬼車や .NET並の正規表現を JavaScriptでも使用可能にしています。これも一歩踏み出す一つの方法ですが、ほとんど XMLのハイライトでしか使われてないようにも見えるのがもったいない)

 この日記で使っているお手製の SHJS言語ファイル(Rubyと JavaScript)*4

JavaScriptの方は letみたいな新しいキーワードには対応していないが、JScript5.5(ECMAScript3)に準拠したスクリプトのハイライトに可能な限り対応している。Javaもどきの実装とは全然違います。

Rubyの方もがんばったけど、こちらは正規表現による字句解析レベルでは判断の付かない要素が多くて、例えば

  str1 = % hoge ; #=> "hoge"
  str2 = "%04d-%02d-%02d" % [2008, 8, 20] #=> "2008-08-20"

% をメソッドと判断する(下段)か %!string! とする(上段)かは文脈がないと決められない。現在は後者が誤って %!string! と判断されている(%!string!記法の区切り文字としてスペースを認めなければ、より多くのケースで妥当な表示が得られるのはわかっているが……)。また、改行を含む %!string! リテラルにも対応していない(はてなはこれができる。悔しい)が、かっこを使った %記法( %[string]など )では改行を含むことができる。

既知の不具合はこれだけ。(知らないだけ)

* その場合スクリプトが実行されないことがあったような……(検索結果>http://la.ma.la/blog/diary_200612061928.htm ) と思ったが、IEで innerHTMLを使った場合の話だった。とはいえ、DOMで <script>エレメントを作成することは可能で、実行もされるというのだろうか?

 厳密ではない。感覚的な言葉。

 この文では普通の正規表現以上の表現力(例えばかっこの対応を調べられる)があるのを説明できていないような。(ある状態から別の状態へ移動するだけでなく後戻りすることもできるから、というのでは説明になっているだろうか?

*4 2008年12月にリリースされた shjs-0.6でフォーマットが変更になったので shjs-0.5用。


2008年05月30日 (金) ViewVCに URLをパラメータとして与えて、外部の生Subversionリポジトリに ViewVCの皮をかぶせたい。

[SHJS] 続・shjs-0.4.2: 重箱の隅、つっつきます

$ で GREPしてみたらこういうものが無数に見つかった。だいたいが一行コメントの中に対応した state。終了条件は行末で、URLを含んでいれば sh_urlとしてマークする。

    {
      'exit': true,
      'regex': /$/g
    },
    {
      'regex': /(?:<?)[A-Za-z0-9_\.\/\-_]+@[A-Za-z0-9_\.\/\-_]+(?:>?)/g,
      'style': 'sh_url'
    },

URLが改行の直前まで続いていれば、終了条件としての行末の検出がスキップされて一行コメントが次の行まで継続する。まさしく 20080513p01の問題の繰り返し。

結局、sh_main.jsに非互換な変更を加えるのは問題大ありだと判明したので sh_javascript.jsで対応することにしましたよ、と。

  [ // state 2: in "string"
    {
      regex: /\\[\\"]/g
    },
    {
      next: 6,
      regex: /\\$/gm
    },
    {
      exit: true,
      regex: /"|$/gm
    }
  ],
  [ // state 6: eat an end-of-line ※空行は食べられないよ
    {
      exit: true,
      regex: /^/gm
    }
  ]

動作確認は昨日の日記で。


2008年05月29日 (木) 内容と関係のない画一的な帯は間違いなくゴミ(コバルト文庫のことです)。帯の下を意識させるデザインのときの帯もゴミ。役に立つ文字が書いていない帯もゴミ。帯を外すと間抜けなカバーはダメダメ。(最近は買った本の帯をカバーの下に隠すことが多い。<でも捨てられない)

[SHJS] shjs-0.4.2: 重箱の隅、つっつきます

例えば、JavaScriptのリテラル文字列では \ と改行のシークェンスは空文字を意味している。つまりこういうこと

var str = "空白を含まない\
ひとつながりの文字列です";

このシークェンスを認めるように、ダブルクォーテーション文字列の終了条件として次のようなものを shjs/lang/sh_javascript.js に含めてみたがうまくいかなかった。

  [ // "string"
    {
      // \\ と \" と \(改行) を 1つのシークェンスとして
      // 食べてしまう。終了位置を見誤らないためであって、
      // 特に何をするということもない。/\\(.|$)/gm でも構わない。
      regex: /\\(?:[\\"]|$)/gm
    },
    {
      // エスケープされていない " に出会ったら "~" の中に
      // いるという状態(state)から exitする。
      // " がないまま行末に達したら、終端されていない不正な
      // 文字列だと判断して、やはり exitする。
      exit: true,
      regex: /"|$/gm
    }
  ],

少し前に「行末に達した時点でマッチングを打ち切っていたのが間違い。$は空文字列にもマッチする。全てのマッチに失敗するまで続ける必要があった(20080513p01)」と自分のミスを書いて、これを修正したのだが、shjs-0.4.2はもちろん正しく、全てのマッチが失敗するまで続けている。

そうすると何が起こるか。/\\$/gm にマッチした後でも /"|$/gm のマッチに成功してしまい、結果、行末に \ があろうがなかろうが exitしてしまう。

もちろん行末に達したからといってすぐにマッチングを打ち切っては 20080513p01と同じ間違いを犯すことになるので、同一 state内で*二回以上*行末にマッチすることがないように sh_main.jsを変更した。

内の方のループの、頻繁に実行される部分に if が増えたのが気に入らないものの、悪影響のある非互換でもないし、首尾は上々だし(冒頭の文字列のハイライト結果が見本)、悪くない。

var str = "終端されていない
不正な文字列です";

 追記@2008-05-30: この場合はどうする?

var str = "終端されていない\"
不正な文字列です";

*たまたま*行末にある \" にマッチしたことで、終了条件である行末の検出がスキップされて、次の行までが文字列だと判断されている*。\" とのマッチは \$ と違い行末を要求していないから、この場合は一行目で exitしてほしい。

* 20080530p01で修正したので文章とハイライト結果が食い違っているかもしれない。


2008年05月27日 (火) [Firefox] 領域を選択してのソース表示は、スクリプトに書き換えられた最新の HTMLを反映しているのが便利。

[Firefox][javascript][SHJS] <pre>が真っ白になり、黒色の領域が出現する。

例えばこのページ http://vvvvvv.sakura.ne.jp/ds14050/diary/20080112-7.html 。Endキーで末尾に移動して PageUpで戻っていくと空白の PREが目に入ると思う。その少し上にはページの内容を覆い隠す黒い領域があるはず。(そうでなければ修正されたのだろう。Firefox2で最初に確認し、Firefox3.0RC1でも直っていなかったが)

大量の PREが存在したり、一つだけでも巨大な PREが存在する場合に起こる様子。innerHTMLで PREの内容を置き換えているのも原因になっているかもしれない。

画面の末端にスクロールした状態でページをリロード(F5 or Ctrl+R)すると下方の PREが正常に表示される反面、上端付近の PREに同じ問題が生じる。遠方の PREの書き換えに問題があるのでは?

真っ白の PREの中で、右クリックしたりテキストを選択したりといったアクションを起こせば正常に表示されることが多い。


あと、PREの中から開始した選択は PREの外に出られなかったり。(これは TEXTAREAと違い PREでは Ctrl+Aで全文選択ができないために用意された代替手段だという気もする)


2008年05月13日 (火) qr/…/と同じものは Rubyにないと思っていたが Regexp#to_sがそれ。正規表現リテラルの式展開と組み合わせて使おう。hikidoc.rbでは昔から使われていたのに何を見ていたのか。

[SHJS] URLのハイパーリンク化とバグ潰し

オリジナルの sh_javascript.jsはコメントの中の URLっぽい部分とメールアドレスっぽい部分をハイパーリンクにしていた。機能が劣るのは遺憾なので sh_javascript.jsと sh_ruby.jsに、コメントと文字列の中の URLっぽい部分をハイパーリンク化する機能を追加した。

その過程で気付いた、一行コメントの終了条件などに使われている $アンカーのマッチングが行われない場合があったのを修正した。(行末に達した時点でマッチングを打ち切っていたのが間違い。$は空文字列にもマッチする。全てのマッチに失敗するまで続ける必要があった)。これは自分が 2008-02-25に持ち込んだバグでオリジナルには存在しない。

 サンプル / テストケース

  • 一行目:コメント内の URLはリンクになっているか?
  • 二行目:一行目のコメントの続きだと誤認されていないか?
# http://vvvvvv.sakura.ne.jp/ds14050/badboy/log/
How is this line highlighted ?

 最新の、未来において変更されている可能性のあるファイルへのリンク


2008年05月08日 (木) [Vista] 「プログラムから開く」が便利なんだけどフォルダを対象にできないのが玉に瑕。Unknown\shell\openasを directory\shell\openasにコピーするとダイアログを開くことはできるがプログラムのリストがコンテクストメニューに展開されたりはしない。

最終更新: 2009-09-01T05:05+0900

[SHJS][Ruby] '%04d-%02d-%02d' % [2008, 5, 8] がうまくハイライトできない理由

このようにハイライトされます。

'%04d-%02d-%02d' % [2008, 5, 8]

(整形した)HTMLソースはこう。

<span class="sh_string">'%04d-%02d-%02d'</span> 
<span class="sh_string">% [2008, </span>
<span class="sh_number">5</span>
<span class="sh_symbol">,</span> 
<span class="sh_number">8</span>
<span class="sh_cbracket">]</span>

「% [2008, 」が一つの文字列にされてしまっている。どういう判断なのかと調べれば、%!string! と同じものだと見なされていた。(そのルールは自分で書いたんだけども)

知っていたでしょうか? %リテラルの区切りには空白(改行も!)が使えるのでした。(alnumと mbchar以外なら OKっぽい。変態すぎるよ)


2008年02月12日 (火) エクスプローラがメモリを放さない。プライベートワーキングセットは 14MBでも、Vistaより前のタスクマネージャが表示していた数字では 215MBになる。仮想メモリ不足ってメッセージが何度も出るんですけどー。

最終更新: 2010-03-21T03:27+0900

[javascript][SHJS] JSLint <http://www.jslint.com>

SHJSの sh_main.jsを高速化したことを以前書いた。(20080204p01)

 対応ブラウザ

SHJSのページには動作を確認したブラウザとして以下が挙げられている。

  • Firefox 2
  • Internet Explorer 6
  • Internet Explorer 7
  • Opera 9
  • Safari 3

sh_main.jsの修正版は Firefox2IE7Opera9で正しく動作することの確認と速度の比較を行っている。

IE6での確認は IE7から戻すのが面倒なので省略する。

Sarari3は Vistaで動くものがダウンロードできるので確認してみたところ動いた。(表示も正常)

いじったことで対応ブラウザが減っていなくて良かった。(IE6は?)

 JSLint

SHJSの作者は Code Conventions for the JavaScript Programming Languagejslint: The JavaScript Verifier かそれに類似した文書を読んでいるに違いない。(これらのページを今日発見した)

というのも、sh_main.jsを JSLintでチェックしてみたが、こういうエラーしか出なかった。

Error:

Implied global: document 362, sh_languages 347, window 332

このエラーは JSLint向けにコメントを埋め込めば取り除けるものだし、そうしないと不可避だともいえるもの。

 さてさて、自分がいじったことでどれだけお行儀の悪いスタイルが混入したのでしょうか
Error:

Implied global: document 207 360, sh_languages 332, window 329

Problem at line 73 character 48: Use the array literal notation [].

   matchCaches = language.matchCaches = new Array(language.length);

Problem at line 86 character 17: 'i' is already defined.

   for(var i = matchCaches.length-1; i !== -1; --i) {

Problem at line 97 character 22: 'i' is already defined.

   for (var i = state.length-1; i !== -1; --i) {

Problem at line 110 character 17: 'i' is already defined.

   var i = (pair[0] & 0x3F);

Problem at line 280 character 15: Use '!==' to compare with '0'.

   while(0 != this._currentStyles.length) {

Problem at line 389 character 14: 'node' is already defined.

   var node = this.free_;
 いいわけ
  • ローカル変数の再宣言に関するものは全て forループで初期化される変数が原因。(i と node)
  • 「var array = new Array(length);」を「var array = []; array.length = length;」や「var array = Array.prototype.slice.call({length:length}, 0);」 と書き換えることは拒否する。

    (new Array(length)が一番簡潔で自然な書き方)

    (JavaScript 1.7の配列内包に書き換えるのには吝かでない)

  • != 0 の代わりに !== 0 と書け? 書き換えましたm( __ __ )m

(一つを除いて) 無視できる警告*ばかりで良かった。

* 無視したら警告の意味がない。forループの変数なんて(古い VC使い以外には)スコープの誤解を招きやすいという理由で、避けなければいけないものの筆頭ともいえる。


2008年02月04日 (月)

[SHJS][javascript] sh_main.jsの高速化

SHJSのブラウザでの実行時間を削るには sh_main.js(SHJSのメインスクリプト)を速くするか、正規表現を効率的なものにする方法がある。(>遅い正規表現(20080116p01))。

正規表現に関してできることは限られるうえ、知識も少ない(『詳説 正規表現 第三版』待ち)ので、可能な限り文字クラスや文字集合といわれるものを使うように気を付けただけにとどまる。(sh_ruby.js, sh_javascript.js)

メインスクリプトの sh_main.jsに対してできることは多い。この日記の現在?の最新ページ(2008年1月12日から7日間)を表示して、sh_highlightDocument()前後での経過時間を表示したところこのようになった。

Firefox2IE7(64-bit)IE7(32-bit)Opera9.25
sh_main.js (0.4.2)935ms1050ms1270ms1260±150ms
改変版600ms680ms865ms1200±150ms
削減率36%35%32%5%

ハイライト対象が少なくて数ミリ秒で処理が終わるような場合はオーバーヘッドのために改変版の方が 1-2ミリ秒遅くなるが、それよりもスクリプトがブラウザをロックする時間が長くなるような場合にこそ速度改善が必要なので OK。

代償としてファイルサイズが sh_main.jsで 10.5KiBから 12.7KiBへ +2.2KiB。jsmin圧縮後の sh_main.min.jsで 6.22KiBから 7.82KiBへ +1.60KiB。Apacheによる gzip圧縮やブラウザのキャッシュに期待します。

 (補足) Operaについて

普段は全く Operaを使わないし、詳しくもない。むしろ Operaではキーボードを使ったブラウジングもままならない。そんな人間が Firefox+Firebugを頼りに sh_main.jsの修正を行ったので Operaの速度が改善しないのは仕方のない部分がある。(IEは改善したが)。(あんだけいじってトータルで変わらない方がすごい。どこが足を引っぱっているのだろう)。リテラル文字列と Stringオブジェクトの差が他のブラウザより大きいらしいが、それが原因?

EfficientJavaScript - Dev.Opera - 効率的な JavaScript (www.hyuki.com)

Operaでの JavaScriptの実行時間が他のブラウザに比べて長いのははっきりした理由があって、Operaはスクリプトが全力疾走中であってもユーザーの操作に対する反応を後回しにしたりしない。これは偉い。ユーザーを待たせない代わりにスクリプトが遅れるのは当然の代償で仕方がない。

あ、スクリプトでなく再描画が律速してるから改善しないということ?

 (かんせいしたへびのえにあしをかきくわえるこうい、とまでは言わないが) この日記で使用中のファイルへのリンク

(常に最新版だが一時的にバグが混入していることがあるかも)

 追記@今日:sh_main.jsをちょっと修正。

すぐ上のリンク先はすでに変更が反映されている。

これら二つの記事を参考に escapeHTML()を変更した。測定に使ったページでは 9000回ちかく呼び出されるメソッドなので影響はバカにならない。といっても 600msだったのが 590msを切るようになった、というレベル。むしろ下請けfunctionを隠蔽できたことの方が嬉しい。

escapeHTML()自体、sh_builderのインターフェイスではないので、外部から呼び出せないようにすべきかもしれないが、functionをかぶせるたびに呼び出しのオーバーヘッドが増える気がしてそうはしていない。

 追記@今日

SHJSの patternStackオブジェクトは外部と完全に独立して動作するのに、sh_highlightString()が呼ばれるたびに無名クラスとそのインスタンスを作成するような方法がとられている。コンストラクタと prototypeを書こう。(sh_highlightString()は HTML文書内の <pre class="sh_XXX">の数だけしか呼ばれないから影響は小さいが。件のページでは 58回)。

sh_highlightString()からしか使われないのにスタックの可視範囲が広がるのが気になるなら、さっき覚えた無名functionで二つをくるんでしまえば良い。

var sh_highlightString = (function(){
  var Stack = function(){
    this.stack_ = [];
  };
  Stack.prototype.getLength = function(){/* ... */};
  // ……
  return function(){
    var patternStack = new Stack();
    /* sh_highlightStringの中身がつづく…… */
  };
})();

まあ、速度が改善するわけではないので、書き直さないんだけど。

 追記@2008-02-25

innerHTMLや textContent、innerTextの使用は堕落だという気もするが、冗長な上に呼び出しを重ねることで遅くなる DOMメソッドがいけない。


2008年01月16日 (水) Pythonかわいいよ、Python

[SHJS][SakuraEditor][javascript] SHJSと SakuraEditor用のハイライトルールファイル

SHJSの javascript定義ファイル(lang/sh_javascript.js)の元になったファイル(javascript.lang)の中身がこれ。

include "java.lang"

subst keyword = "abstract|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|final|finally|for|function|goto|if|implements|in|instanceof|interface|native|new|null|private|protected|prototype|public|return|static|super|switch|synchronized|throw|throws|this|transient|true|try|typeof|var|volatile|while|with"

javaて……。キーワードにしても使ったことのないものがいっぱい。

あまりにあんまりなんで一から書いた。(sh_javascript.js, sh_javascript.min.js)。 参照したのは JScript5.5の HTMLHelpなので JScript.NETや ECMAScript4には対応していない。古典的な JavaScript。

ついでに同じものを SakuraEditorにも。(javascript_keywords.zip)


2008年01月05日 (土)

[SHJS][javascript]SHJS Ruby定義ファイル(sh_ruby.js) 私的改訂版 まとめ。(2008-01-17 最終更新)

ファイルはこちら20080101p01

あいだにコメントを挟みながら一連の DIFF差分(sh_ruby.js.diff)を見ていく。

 かっこ(?:)を省略して減量。

--- sh_ruby.js.original	Fri Aug  3 12:16:32 2007
+++ sh_ruby.js	Sat Jan 19 01:35:32 2008
@@ -2,123 +2,427 @@
   this.sh_languages = {};
 }
 sh_languages['ruby'] = [
   [
     {
-      'regex': /\b(?:require)\b/g,
-      'style': 'sh_preproc'
+      regex: /\brequire\b/g,
+      style: 'sh_preproc'
     },

 #から後ろはすべてコメントなんていう強力すぎるルールは後ろに移動して優先度を下げる。

-    {
-      'next': 1,
-      'regex': /#/g,
-      'style': 'sh_comment'

 必ず最初に実行される処理(BEGIN)、最後に実行される処理(END、at_exit)、スクリプトを終了する(exit)など無視できない働きを持ったメソッドをハイライト。

+    { // part of Kernel methods.
+      regex: /\b(?:exit!?|(?:abort|at_exit|BEGIN|callcc|END|eval|exec|fork|load|spawn|syscall|system|trap|warn)\b)/g,
+      style: 'sh_preproc'
     },

 \bの後ろの +-は絶対にマッチしないので前に出してマッチするように。

 数値リテラルの定義が cpp,java,pascal,perl,php,prolog,python,sh,sql,tclと共通だったので Ruby専用に。(先頭、末尾や連続するアンダースコアやが許容されているがべつに Rubyインタープリタじゃないので気にしない)

     {
-      'regex': /\b[+-]?(?:(?:0x[A-Fa-f0-9]+)|(?:(?:[\d]*\.)?[\d]+(?:[eE][+-]?[\d]+)?))u?(?:(?:int(?:8|16|32|64))|L)?\b/g,
-      'style': 'sh_number'
+      regex: /[+-]?\b(?:0(?:x[A-Fa-f0-9_]+|d[\d_]+|b[01_]+|o?[0-7_]+)|(?:0\.)?[\d_]+(?:[Ee][+-]?[\d_]+)?)\b/g,
+      style: 'sh_number'
     },

 javascriptのオブジェクトリテラルのプロパティ名部分は引用符を省ける(ので省く)。

     {
-      'next': 2,
-      'regex': /"/g,
-      'style': 'sh_string'
+      next: 2,
+      regex: /"/g,
+      style: 'sh_string'
     },
     {
-      'next': 3,
-      'regex': /'/g,
-      'style': 'sh_string'
+      next: 3,
+      regex: /'/g,
+      style: 'sh_string'
     },

 <<メソッドや <<HEREDOCUMENTに誤ってマッチしないように、HTMLタグっぽいものだけをハイライト(そもそもなんで <hoge>を文字列としてハイライトするのかは GNU Source-highlightのみぞ知る)。

     {
-      'next': 4,
-      'regex': /</g,
-      'style': 'sh_string'
+      next: 4,
+      regex: /<(?=[\w\/])/g,
+      style: 'sh_string'
     },

 /regexp/i 型の正規表現リテラルの条件を厳しくして URLに誤ってマッチしないように。またオプション部分もマッチに含めるように。

     {
-      'regex': /\/[^\n]*\//g,
-      'style': 'sh_regexp'
+      regex: /\/(?:\\.|[^\n\\\/])*\/[eimnosux]*(?![A-Za-z])/g,
+      style: 'sh_regexp'
     },

 別途定義したので不完全な %r{regexp}の定義は削除。

     {
-      'regex': /(%r)(\{(?:\\\}|#\{[A-Za-z0-9]+\}|[^}])*\})/g,
-      'style': ['sh_symbol', 'sh_regexp']

 定義と処理の流れに関わるキーワードやメソッドを選択。除外したものはすべて定数や無視できないメソッドとして別に定義した。

+      regex: /(?:\b(?:alias|attr(?:_reader|_writer|_accessor)?|begin|break|case|do|else|elsif|end|ensure|for|if|in|include|lambda|loop|next|proc|raise|redo|rescue|retry|return|super|then|undef|unless|until|when|while|yield|and|not|or|def|class|module|catch|fail|throw)\b|&&|\|\|)/g,
+      style: 'sh_keyword'
     },
     {
-      'regex': /\b(?:alias|begin|BEGIN|break|case|defined|do|else|elsif|end|END|ensure|for|if|in|include|loop|next|raise|redo|rescue|retry|return|super|then|undef|unless|until|when|while|yield|false|nil|self|true|__FILE__|__LINE__|and|not|or|def|class|module|catch|fail|load|throw)\b/g,
-      'style': 'sh_keyword'

 不要なグループ化(?:)とエスケープ(\=)を削除。

+      next: 5,
+      regex: /^=begin/g,
+      style: 'sh_comment'
     },
     {
-      'next': 5,
-      'regex': /(?:^\=begin)/g,
-      'style': 'sh_comment'

 グローバル変数は別に定義したのでここではインスタンス変数とクラス変数のみを扱うことに。

+      regex: /@@?[A-Za-z_][A-Za-z0-9_]*/g,
+      style: 'sh_type'
     },
-    {
-      'regex': /(?:\$[#]?|@@|@)(?:[A-Za-z0-9_]+|'|\"|\/)/g,
-      'style': 'sh_type'

 グローバル変数専用のルールを追加。

+    { // global variables
+      regex: /\$(?:[_&~`'\+\?!@=\/\\,;\.<>\*\$:"]|-?[A-Za-z0-9_]+\b)/g,
+      style: 'sh_type'
+    },

 各種かっこ(<[{}]>)を使った正規表現、コマンド、文字列配列、文字列、シンボルリテラルのルールを追加。(後ろの方に関連する追加部分あり)

+    { // %r(regexp)
+      next: 6,
+      regex: /%r(?=[\(<\[\{])/g,
+      style: 'sh_regexp'
+    },
+    { // %x(command), %w(array)
+      next: 11,
+      regex: /%[xWw](?=[\(<\[\{])/g,
+      style: 'sh_normal'
+    },
+    { // %(string), %s(symbol)
+      next: 16,
+      regex: /%[Qqs]?(?=[\(<\[\{])/g,
+      style: 'sh_string'
+    },

 かっこ以外の記号を使った正規表現、コマンド、文字列配列、文字列、シンボルリテラルのルールを追加。

+    { // %r!regexp!i
+      regex: /%r([ -'*-\/:;=?@\\^_`|~])(?:\\.|.)*?\1[eimnosux](?![A-Za-z])/g,
+      style: 'sh_regexp'
+    },
+    { // %x!command!, %w!array!
+      regex: /%[xWw]?([ -'*-\/:;=?@\\^_`|~])(?:\\.|.)*?\1/g,
+      style: 'sh_string'
+    },
+    { // %!string!, %s!symbol!
+      regex: /%[Qqs]?([ -'*-\/:;=?@\\^_`|~])(?:\\.|.)*?\1/g,
+      style: 'sh_string'
+    },

 シンボルリテラルのルールを追加。(コロンにクォーテーションマークが続くタイプのシンボル(:"symbol")は現状でも、コロンがsh_symbol、その後ろがsh_stringとして認識されるので対応する必要がない)

+    { // Symbol
+      regex: /(:)((?:@@?|\$|[A-Za-z_])\w+\b[!\?]?)/g,
+      style: ['sh_symbol', 'sh_string']
+    },
+    { // Symbol
+      regex: /(:)(\+|~|\*\*?|-|\/|%|<=>|<<?|>>?|^|===?|=~|!~|&|\|)(?=[^\w\d]|$)/g,
+      style: ['sh_symbol', 'sh_string']
+    },

 定数のルールを追加。

+    { // Constants
+      regex: /\b[A-Z]\w+\b/g,
+      style: 'sh_function'
+    },
+    { // Constants
+      regex: /\b(?:self|nil(?!\?)|true|false|__FILE__|__LINE__)\b/g,
+      style: 'sh_function'
+    },

 大文字や数字で始まって ?か !で終わるメソッドはないので(ないよね?)、そのように正規表現を変更。

+    { // don't highlight ? and ! as symbols if they are part of a method call
+      regex: /\b[a-z_]\w*[!\?]/g,
+      style: 'sh_normal'
     },
     {
-      'regex': /[A-Za-z0-9]+(?:\?|!)/g,
-      'style': 'sh_normal'

 ::hogeを Symbolと誤認しないように、先に ::を sh_symbolにしてしまう。

 プロパティ名部分の引用符を省略

+      regex: /~|!|%|\^|\*|\(|\)|-|\+|=|\[|\]|\\|::?|;|,|\.|\/|\?|&|<|>|\|/g,
+      style: 'sh_symbol'
     },
     {
-      'regex': /~|!|%|\^|\*|\(|\)|-|\+|=|\[|\]|\\|:|;|,|\.|\/|\?|&|<|>|\|/g,
-      'style': 'sh_symbol'
+      regex: /(#)(\{)/g,
+      style: ['sh_symbol', 'sh_cbracket']
     },
     {
-      'regex': /(#)(\{)/g,
-      'style': ['sh_symbol', 'sh_cbracket']
+      regex: /\{|\}/g,
+      style: 'sh_cbracket'
     },
     {
-      'regex': /\{|\}/g,
-      'style': 'sh_cbracket'

 #一行コメント ルールを前の方から最後のここまで移動。

+      next: 1,
+      regex: /#/g,
+      style: 'sh_comment'
     }
   ],

 プロパティ名部分の引用符を省略

 グループ化(?:)の代わりに文字クラス( [] )を使用して字数削減。

 'exit'するときの 'style'指定はなくてもかまわないので削除。

 不要なグループ化とエスケープを削除。(?:\=end)

   [
     {
-      'exit': true,
-      'regex': /$/g
+      exit: true,
+      regex: /$/g
     }
   ],
   [
     {
-      'exit': true,
-      'regex': /$/g
+      exit: true,
+      regex: /$/g
     },
     {
-      'regex': /\\(?:\\|")/g
+      regex: /\\[\\"]/g
     },
     {
-      'exit': true,
-      'regex': /"/g,
-      'style': 'sh_string'
+      exit: true,
+      regex: /"/g
     }
   ],
   [
     {
-      'exit': true,
-      'regex': /$/g
+      exit: true,
+      regex: /$/g
     },
     {
-      'regex': /\\(?:\\|')/g
+      regex: /\\[\\']/g
     },
     {
-      'exit': true,
-      'regex': /'/g,
-      'style': 'sh_string'
+      exit: true,
+      regex: /'/g
     }
   ],
   [
     {
-      'exit': true,
-      'regex': /$/g
+      exit: true,
+      regex: /$/g
     },
     {
-      'exit': true,
-      'regex': />/g,
-      'style': 'sh_string'
+      exit: true,
+      regex: />/g
     }
   ],
   [
     {
-      'exit': true,
-      'regex': /^(?:\=end)/g,
-      'style': 'sh_comment'
+      exit: true,
+      regex: /^=end/g
     }
+  ],

 各種かっこ(<[{}]>)を使ったリテラルのルール(後半部分)

+  [ // state 6-10: %r(regexp)
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 7,
+      regex: /\(/g,
+      style: 'sh_regexp'
+    },
+    {
+      next: 8,
+      regex: /</g,
+      style: 'sh_regexp'
+    },
+    {
+      next: 9,
+      regex: /\[/g,
+      style: 'sh_regexp'
+    },
+    {
+      next: 10,
+      regex: /\{/g,
+      style: 'sh_regexp'
+    },
+    {
+      exit: true,
+      regex: /[)>\]}][eimnosux]*/g,
+      style: 'sh_regexp'
+    }
+  ],
+  [
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 6,
+      regex: /(?=\()/g
+    },
+    {
+      exit: true,
+      regex: /(?=\))/g
+    }
+  ],
+  [
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 6,
+      regex: /(?=<)/g
+    },
+    {
+      exit: true,
+      regex: /(?=>)/g
+    }
+  ],
+  [
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 6,
+      regex: /(?=\[)/g
+    },
+    {
+      exit: true,
+      regex: /(?=])/g
+    }
+  ],
+  [
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 6,
+      regex: /(?={)/g
+    },
+    {
+      exit: true,
+      regex: /(?=})/g
+    }
+  ],
+  [ // state 11-15: %x(command)
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 12,
+      regex: /\(/g,
+      style: 'sh_normal'
+    },
+    {
+      next: 13,
+      regex: /</g,
+      style: 'sh_normal'
+    },
+    {
+      next: 14,
+      regex: /\[/g,
+      style: 'sh_normal'
+    },
+    {
+      next: 15,
+      regex: /\{/g,
+      style: 'sh_normal'
+    },
+    {
+      exit: true,
+      regex: /[)>\]}]/g,
+      style: 'sh_normal'
+    }
+  ],
+  [
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 11,
+      regex: /(?=\()/g
+    },
+    {
+      exit: true,
+      regex: /(?=\))/g
+    }
+  ],
+  [
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 11,
+      regex: /(?=<)/g
+    },
+    {
+      exit: true,
+      regex: /(?=>)/g
+    }
+  ],
+  [
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 11,
+      regex: /(?=\[)/g
+    },
+    {
+      exit: true,
+      regex: /(?=])/g
+    }
+  ],
+  [
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 11,
+      regex: /(?={)/g
+    },
+    {
+      exit: true,
+      regex: /(?=})/g
+    }
+  ],
+  [ // state 16-20: %Q(string)
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 17,
+      regex: /\(/g,
+      style: 'sh_string'
+    },
+    {
+      next: 18,
+      regex: /</g,
+      style: 'sh_string'
+    },
+    {
+      next: 19,
+      regex: /\[/g,
+      style: 'sh_string'
+    },
+    {
+      next: 20,
+      regex: /\{/g,
+      style: 'sh_string'
+    },
+    {
+      exit: true,
+      regex: /[)>\]}]/g,
+      style: 'sh_string'
+    }
+  ],
+  [
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 16,
+      regex: /(?=\()/g
+    },
+    {
+      exit: true,
+      regex: /(?=\))/g
+    }
+  ],
+  [
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 16,
+      regex: /(?=<)/g
+    },
+    {
+      exit: true,
+      regex: /(?=>)/g
+    }
+  ],
+  [
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 16,
+      regex: /(?=\[)/g
+    },
+    {
+      exit: true,
+      regex: /(?=])/g
+    }
+  ],
+  [
+    {
+      exit: true,
+      regex: /$/g
+    },
+    {
+      next: 16,
+      regex: /(?={)/g
+    },
+    {
+      exit: true,
+      regex: /(?=})/g
+    }
   ]
 ];

2008年01月03日 (木)

[SHJS][javascript] jsmin.js

SHJSのスクリプトは全て、機能が同じでファイルサイズが違う hoge.jsと hoge.min.jsの二種類が収録されている。言語ごとに定義ファイルが分かれているのもおそらく転送量を抑えるためで、個々の jsファイルのほとんどが数キロバイトに収まっている。

*.min.jsファイルは JSMINというツールで空白を詰めることで作られている。JSMINのオリジナルは DOS実行ファイルだけど、C#、Java、JavaScript、Perl、PHP、Python、OCAML、Rubyの実装もある。javascriptを圧縮するのなら javascriptを使いたいよね、ということで javascriptバージョンの jsmin.jsをダウンロードしてきた。

jsmin.jsの中には jsmin()という関数が一つだけある。これに javascriptのソースを渡すとコンパクトになったソースが返ってくるのだけどどうやって実行しよう。jsmin.jsと同じ場所にあった test.htmlをブラウザで表示してテキストエリアにソースを貼り付けて実行するのもありだが sh_ruby.jsをちょこちょこいじってる身としては毎回となると面倒くさい。

というわけで J(ava)Scriptで exec_jsmin.jsというのを書いた。jsmin.jsと同じ場所に置いたこのファイルに *.jsファイルをドロップすると *.min.jsというファイルを作成する。

var fso = new ActiveXObject("Scripting.FileSystemObject");

function ReadFile(path)
{
	var ts = fso.OpenTextFile(path, 1, false);
	var text = ts.ReadAll();
	ts.Close();
	return text;
}

function WriteFile(path, text)
{
	var ts = fso.CreateTextFile(path, true, false);
	ts.Write(text);
	ts.Close();
}

eval(ReadFile(fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), "jsmin.js")));

var args = WScript.Arguments;
for(var i = 0; i < args.Length; ++i)
{
	var path = args(i);
	if(fso.FileExists(path)) {
		var path_min = fso.BuildPath(fso.GetParentFolderName(path), fso.GetBaseName(path)) + '.min.js';
		WriteFile(path_min, jsmin(ReadFile(path)));
	} else {
		WScript.Echo("FileNotExist:"+path);
	}
}

最初から最後まで J(ava)Scriptで完結して満足です。

[SHJS][javascript]まだまだいじってます。>SHJS | \bを正しく使用 & わずかに減量

ファイルはこちら。20080101p01

頭の方から変更点を見ていく。

 #includeに相当するもの (sh_preproc)

-      'regex': /\b(?:require)\b/g,
+      'regex': /\brequire\b/g,

require一つだけだからかっこで囲む必要はない。

 使用頻度は低いけど無視できないメソッドたち (sh_preprocを流用)

-      'regex': /\b(?:defined\?|Array|Floar|Integer|String|abort|callcc|exec|exit!?|fork|proc|lambda|set_trace_func|spawn|syscall|system|trace_var|trap|untrace_var|warn)\b/g,
+      'regex': /\b(?:defined\?|exit!?|(?:abort|callcc|exec|fork|set_trace_func|spawn|syscall|system|trace_var|trap|untrace_var|warn)\b)/g,

Array、Floar(Floatのスペルミスでした)、Integer、Stringを取り除いて、定数のルールが適用されるように。sh_preprocではなく sh_functionになる。

lambdaと procも取り除いて、sh_keywordに含めることにした。

\bは defined?の ?と exit!の !の直前にマッチし、?の後や !の後にはマッチしないので正しくマッチするように修正。

 シンボル (sh_string)

-    { // Symbol
-      'regex': /:(?:(?:@@|@|\$)?\w+[\?!]?|\+=?|!=?|~|\*\*=?|-=?|\*=?|\/=?|%=?|<<=?|>>=?|&=?|\|=?|^=?|>=?|<=?|<=>|===?|=~|!~|&&=?|\|\|=?|\.\.|\.\.\.|=)(?=\s|$)/g,
-      'style': 'sh_string'
-    },
+    { // Symbol
+      'regex': /(:)((?:@@|@|\$)?\w+\b[!\?]?)/g,
+      'style': ['sh_symbol', 'sh_string']
+    },
+    { // Symbol
+      'regex': /(:)(\+|~|\*\*|-|\*|\/|%|<<?|>>?|^|<=>|===?|=~|!~|&|\|)(?=[^\w\d]|$)/g,
+      'style': ['sh_symbol', 'sh_string']
+    },

あまりにルールが乖離してるので Symbolのルールを分割。加えて、不正な Symbolリテラルをルールから除外(代入、複合代入、:&&、:||、:...など)

リテラルの先頭の : を sh_stringから sh_symbolにしたのは

:"hoge"
:hoge

の整合性をとるため。

 正規表現リテラル (/regexp/i、sh_regexp)

-      'regex': /\/[^\n]*\//g,
+      'regex': /\/(?:\\.|[^\n\\\/])*\/[eimnosux]*(?!\w)/g,

正規表現リテラルのオプション部分もマッチに含めるように。あと条件を厳しくしたので URLに誤マッチすることが減るはず。

 制御構造と定義に関わるキーワードやメソッド (sh_keyword)

-      'regex': /(?:\b(?:alias|begin|BEGIN|at_exit|break|case|do|else|elsif|end|END|ensure|for|if|in|include|loop|next|raise|redo|rescue|retry|return|super|then|undef|unless|until|when|while|yield|and|not|or|def|class|module|catch|fail|load|throw)\b|&&|\|\|)/g,
+      'regex': /(?:\b(?:alias|begin|BEGIN|at_exit|break|case|do|else|elsif|end|END|ensure|for|if|in|include|lambda|loop|next|proc|raise|redo|rescue|retry|return|super|then|undef|unless|until|when|while|yield|and|not|or|def|class|module|catch|fail|load|throw)\b|&&|\|\|)/g,

lambdaと procを sh_preprocから sh_keywordへ持ってきた。どちらもメソッドになりうる重要な要素だと思うから。

 定数 (sh_function)

-      'regex': /\b[A-Z]\w+[!\?]?(?=\b|$)/g,
+      'regex': /\b[A-Z]\w+\b[!\?]?/g,

\bを正しく使用。最後の [!\?]?は不要でした。試してみたらエラーになった。

-      'regex': /\b(?:false|nil(?!\?)|true|self|__FILE__|__LINE__)(?=\b|$)/g,
+      'regex': /\b(?:false|nil(?!\?)|true|self|__FILE__|__LINE__)\b/g,

 よくわからないもの (sh_normal)

-      'regex': /[a-z0-9_]+(?:\?|!)/g,
+      'regex': /\b[a-z0-9_]+[!\?]?/g,

末尾が ?や !のメソッドだけを拾い上げたかったのだろうか?ローカル変数っぽいものにもマッチするようにしたけど、どのみち色はつかないので害はない。因みに文字配列リテラル( %w(one two three) )も適切なクラスが見つからなかったので sh_normalにしている。

 文字列リテラルとタグとコメント(=begin〜=end)

-      'style': 'sh_string'
-      'style': 'sh_string'
-      'style': 'sh_string'
-      'style': 'sh_commend'

'string'、"string"、<tagname>、=begin〜=endの終了条件部分から styleを取り除く。なくても出力は変わらない。それにしても HTMLタグっぽいものにマッチするルールがあるのはなぜだろう。Web用言語だと思われてるのかな?(<stdio>や <stdlib> のたぐいの可能性もある)。不都合はないので消さないけど。