Regular Expression Cookbook
Oreilly & Associates Inc
¥ 4,073
「XRegExp: JavaScript regex library」は、「SyntaxHighlighter(Ver.2.0)」で使われていたので知った。XRegExpの作者が、本の著者の一人、Steven Levithan。
XRegExp(Ver.0.6.1)が、Firefox 2,3、Internet Explorer 5.5-8β1、Safari 3.1、Opera 9.27の JavaScriptに付け加えて利用可能にする正規表現の機能は
後方参照も可。String.replace()での使用も可。
「.」が改行にもマッチ。
ほとんどの空白を無意味なものにしたり(=パターンを自由にインデントできる)、行コメント(#から改行まで)を利用可能にする。
(?#ここにコメント)
こんなの。\p{L}, \p{M}, \p{N}, \p{InHiragana}, \p{InKatakana}。否定は大文字のPで \P{}。
文字集合の中で使えないという制限があるが、若干の工夫でなんとかなる。
使い所が限定されていそうだったり、使い方が難しそうな機能として
(独り言) 名前付きキャプチャグループをサポートしたなら、そのキャプチャ結果をスタックして、そこにパターンを繰り返し適用する書き方を用意することで、matchRecursive()なんてスペシャルメソッドは不要にできるのでは? <何を言ってるのか自分でわかってないよ
XRegExpのコンストラクタに「脳log[2008-01-11-p01] 鬼車すごい。正規表現で再帰ができる。」で書いたようなパターン文字列を渡して、再帰を認識するマッチングを行いたいです。
正規表現に関連するいくつかのメソッドを上書きし、ブラウザ間の差異を吸収するとともに、仕様通りの動作に統一したりもする。
IEなどが、幅0の文字にマッチしたときに lastIndexを不当にインクリメントするのを修正。
(独り言) limitを指定したときに戻ってくる配列の要素数が limitと一致しない。XRegExpのバグ? もちろん separatorにキャプチャグループは使っていない。
例えば再帰の深さの上限を 10や 20と決めてしまって、XRegExpのコンストラクタでごにょごにょパターン文字列を展開してみればどうだろう。JavaScriptで正規表現を一から実装しようというプロジェクトではないから、自ずと制限が定まってしまうのだけど、上限付きでもそれが 20もあれば十分という気がする。
驚いた。大部分がライブラリの機能紹介という退屈な(<作者本人が一番よく知っているから)文章に紛れ込んだバグ報告(とも呼べそうにないもの)を不自由な Google翻訳から見つけ出すとは。
1.0のソースも眺めてみたいけれど巨大な変更履歴にちょっと後込み。そのうちね。
最終更新: 2014-12-24T10:54+0900
個人的にはなくても不便はないけども、番号を表示する方法のアテができたので。
求める条件は
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-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. <</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">></span>
フラットな構造で、comment, url, commentと 3つの要素が並んでいる。commentが urlを含むような構造にはならない。0.6ではハイライト前後のタグ構造をマージする仕組みになっているから、0.4.2や 0.5のように ad hocなごまかしができなくて、まずはこの前提を取り払わなければいけない……。
<<< 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> 行番号がコピーされる。ダメダメだあ。(この機能は封印しよう)
やっぱり一行二列の表を作る方法でいくことにした。
この方法だと、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>と同じように、いつでも本文と同じ幅に揃えたいなー。
<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で済んでしまうなんて!)
<pre>直後の改行は存在しないかのように扱われるが、</pre>直前の改行は存在する(スクリプトで取得できる)ものの表示されない(4つのブラウザで確認)。というわけで、末尾の空行に行番号を付けてしまうと列の左右で行の数が一致しなくなる(だから除外する)。
Google Chrome(1.0.154.53)は
<pre>.innerHTML = <pre>.innerHTML
とやるたびに先頭の改行文字を取り除いていってしまうんじゃないか? 個別のブラウザ対応は切りがないし、完全対応は不可能なので、行番号を付けるときは <pre>の最初と最後の行を空行にしないのが最も安全。
トンデモ IEさんは <pre>.innerHTML= だろうと <tag style="white-space:pre">.innerHTML= だろうと空白をトリミングしてくれますしね。
テストが不十分なので、langファイルの自動読み込み部分など、一度も実行されていない部分が動くかは不明。
最小化方法は JSMin。ためしに YUI Compressorにもかけてみたがローカル変数の短縮を全くやってくれなくて JSMinと大差ない結果だった。一番外側の無名関数の実行部分をとりのぞいたらちゃんとローカル変数名の短縮もやってくれた。
小手先の変更もいくつか加えた。(ブラウザ判別コードの実行を一度だけにしたり << 関数の中で分岐するんでなく、判別結果で関数を取り換える)
「折りたためる」「立体」…。これは……、と読んでみれば果たして「建築家阿竹(あたけ)克人さん(56)が開発した」とあった。
なんでそんな連想が働いたかといえば数日前に読んだこの本。
試してみた。SecurityErrorが出た。リンク先の ruby-1.9.1とは違い、こちらは ruby-1.8.7-p72でのエラー内容。
>irb irb(main):001:0> require 'open-uri' => true irb(main):002:0> $SAFE=1 => 1 irb(main):003:0> open 'http://vvvvvv.sn25p.dip.jp/301.rb' SecurityError: Insecure operation - [] from C:/Program Files (x86)/ruby/lib/ruby/1.8/open-uri.rb:577:in `[]' from C:/Program Files (x86)/ruby/lib/ruby/1.8/open-uri.rb:577:in `find_proxy' from C:/Program Files (x86)/ruby/lib/ruby/1.8/open-uri.rb:147:in `open_loop' from C:/Program Files (x86)/ruby/lib/ruby/1.8/open-uri.rb:164:in `call' from C:/Program Files (x86)/ruby/lib/ruby/1.8/open-uri.rb:164:in `open_loop' from C:/Program Files (x86)/ruby/lib/ruby/1.8/open-uri.rb:162:in `catch' from C:/Program Files (x86)/ruby/lib/ruby/1.8/open-uri.rb:162:in `open_loop' from C:/Program Files (x86)/ruby/lib/ruby/1.8/open-uri.rb:132:in `open_uri' from C:/Program Files (x86)/ruby/lib/ruby/1.8/open-uri.rb:518:in `open' from C:/Program Files (x86)/ruby/lib/ruby/1.8/open-uri.rb:30:in `open' from (irb):3 irb(main):004:0>
301.rbはこう。
#!ruby require 'cgi' cgi = CGI.new; print cgi.header({ 'Status' => '301 Moved Permanently', 'Location' => 'http://vvvvvv.sn25p.dip.jp/index.html' });
ホストネームを指定しない場合は大丈夫だった。例えばこんな。
#!ruby require 'cgi' cgi = CGI.new; print cgi.header({ 'Status' => '301 Moved Permanently', 'Location' => '/index.html' });
どこまで by designなんだろう。
過去にこんなことを書いた。
過去の日記を「編集」するとその日の全てのセクションが rdfに上ってくるのね。tDiary-2.3系の目玉はセクション単位での編集機能だなあ。
セクション単位の編集機能を実装しおえて、makerss.rbの対応もやっとくか、とソースを読んでみたらきっちり内容比較をしていた。だから当然、変更されたセクションのみが RSSで上に上がってくる。一文字も変更せずに日記を送信した場合、RSSも更新されない。ローカルの tDiaryで確認した。でもサーバー( http://vvvvvv.sakura.ne.jp/ds14050/diary/ )ではそうなっていない。過去に書いたとおり、どんな場合でもその日のすべてのセクションが上がってくる。
そういえばこんなこともあった。
日記を更新すると、TDiary::Config#data_path/category/ 以下の、カテゴリごとに作られるキャッシュファイルがずいぶんたくさん更新される。全部ではないが半分近い 21のファイルが更新されていた。日記の内容はというと一つのカテゴリしか使っていない。
根が同じかどうかはわからないが、なにかがおかしい。
そういえば、makerss.rbの作成するキャッシュファイルもカテゴリインデックスと同じ PStore形式だった。におう、におうぞ。
必ずしも上がってくるというものでもないみたい。サーバーの makerss.rbも概ね期待通りの動作をしている。うにゅう。
あまりに古い日記を編集していて、makerss.cacheから記事が掃き出されているために変更の有無を確認できない、というのとも違うんだけど。……と思うんだけど。(もういいや)
とりあえず内容を比較する前に stripしてみた。これで様子見。(複数のエントリが同じ時刻に更新されたかのように記録されることがなくなければ O.K.)
if cache[id].section.body_to_html.strip != section.body_to_html.strip or cache[id].section.subtitle_to_html.strip != section.subtitle_to_html.strip then cache[id] = RDFSection::new( id, Time::now, section ) end
最終更新: 2009-12-10T00:55+0900
ルールを守るために未来日記を書く人もいるようですが、これはあくまで普通の日記だし、書いた日付と内容は切り離せないので、一日に複数のエントリを書いたりもします。だから、セクション単位での編集機能を、もちろんブラウザで。
編集画面 | プレビュー画面 | プレビュー画面(競合あり) | 登録画面(競合あり) |
---|---|---|---|
変更内容はこのような感じ。
例えば、2009年2月26日の第1セクションの編集フォームへのリンクはこうなる。
update.rb?edit=1;year=2009;month=2;day=26;section=1
現在編集中のセクションが他の人によって書き換えられていたり、セクションの挿入や削除によってセクションナンバーが変わっていたときは、エラーメッセージを表示して(プレビュー画面でも確認可能)、変更をコミットしない。(「戻る」の後でも直前の編集内容が残っているかどうかは使用しているブラウザ次第)
従来の一日単位での編集では失われていた*著者情報が、セクション単位の編集では保存される。(編集対象のセクションは一番最後の変更者名になる)
ついでに、ある日の編集フォームを利用して、別の日に追記したとき――編集画面で日付を書きかえた後、この日付の日記を編集ボタンを押さずに、登録ボタンを押すことで可能――、記録されていなかった*著者名も記録する。
というわけで、skel/update.rhtml.zhと skel/preview.rhtml.zhは手つかずなので、従来の編集画面と機能になる。
skel/update.rhtml.enと skel/preview.rhtml.enは書きかえたけど動作未確認。
とはいえ、スタイル関連で利用するメソッドは each_section、append、replaceだけなので動くはず(期待)。
ただ、Diary#to_srcが Section#to_srcを単純に連結したものである、という仮定をおいてしまっているのだが、新旧wiki_style、rd_style、emptdiary_style、hatena_style、markdown_styleは大丈夫なものの、etdiary_styleは少しだけ違っているのが若干気になる。(といっても末尾の改行を一つにまとめてるだけなんだけど)
type="submit" name="edit"
上記が共通部分。(ボタンのラベルも送信されるけど、使われないので違ってて構わない)
新旧Wikiスタイルの場合、*.td2にはないよね。
tdiary_styleの場合、書き出される(TdiarySection#to_srcに含まれる)けど、読み込んだらカテゴリの一つになってしまいそう。
etdiary_styleも書き出すけど、読み込みは考えてなさそう。EtdiarySection#initializeに明示的に authorを渡しても使われない。
不毛だ。
プラグインの表示したフォームを送信すると、次の画面が普通の一日分の編集画面になる。<input type="hidden" name="date" value="yyyymmdd"> というようなフォームを埋め込む責任が個々のプラグインに委ねられているのでどうしようもない。
TODO: 元の画面に戻ることを保証することと、プレビュー画面に form_procを表示すること。
プレビュー画面への form_proc表示はもうやってるし、form_procを利用したファイルのアップロードを別タブの編集画面でやれば、変更内容が失われる心配もない。知ってるから困らないけど、わかりにくいのは確か。
一日表示のとき、セクションごとに編集リンクを付ける。
add_subtitle_proc {|date, section, subtitle| subtitle += %Q(<span class="adminmenu edit_section"><a href="#{h @conf.update}?edit=1;year=#{@date.year};month=#{@date.month};day=#{@date.day};section=#{section}" rel="nofollow">[edit]</a></span>); } if @mode == 'day';
サブタイトル(<h3>の中)に関係のないテキストを加えるより、サブタイトルの直前に挿入されるこっちの方がいいかも。
add_section_enter_proc {|date, section| %Q(<span class="adminmenu edit_section"><a href="#{h @conf.update}?edit=1;year=#{@date.year};month=#{@date.month};day=#{@date.day};section=#{section}" title="edit (author only)" rel="nofollow">✍</a></span>); } if @mode == 'day';
問題のコード(再掲)はこれ。あるハッシュのキーについて繰り返しているのに、ハッシュにそのキーが存在しない。(すべてのキーが見つからないわけではないが、見つからないキーはいつでも見つからない)
categorized.keys.each do |c| PStore.new(cache_file(c)).transaction do |db| categorized.fetch(c) #=> key not found (KeyError) db['category'] = {} unless db.root?('category') db['category'].update(categorized[c]) end end
fetchをブロックの最初に持って行くと、そこではエラーにならない。
categorized.keys.each do |c| categorized.fetch(c) #=> O.K. PStore.new(cache_file(c)).transaction do |db| db['category'] = {} unless db.root?('category') db['category'].update(categorized[c]) end end
cache_file(c)の呼び出しが原因。その中でも includeしてある ERB::Utilの u()メソッドが核心。
categorized.keys.each do |c| ::ERB::Util.u(c) categorized.fetch(c) #=> key not found (KeyError) PStore.new(cache_file(c)).transaction do |db| db['category'] = {} unless db.root?('category') db['category'].update(categorized[c]) end end
引数にした文字列のエンコーディングが変わってしまっている。
categorized.keys.each do |c| enc1 = c.encoding; ::ERB::Util.u(c) enc2 = c.encoding categorized.fetch(c) { raise "#{enc1} #{enc2} #{::ERB.version}" } #=> UTF-8 ASCII-8BIT erb.rb [2.1.0 2009-01-11] (RuntimeError) PStore.new(cache_file(c)).transaction do |db| db['category'] = {} unless db.root?('category') db['category'].update(categorized[c]) end end
ERB::Util.url_encodeの定義を見ると、引数の文字列を dupした後にエンコーディングを変更しているにも関わらず、呼び出し元に影響を与えてしまっている。
def url_encode(s) s.to_s.dup.force_encoding("ASCII-8BIT").gsub(/[^a-zA-Z0-9_\-.]/n) { sprintf("%%%02X", $&.unpack("C")[0]) } end alias u url_encode
そんなわけだから呼び出し側(category.rb)で
u( c.dup )
なんてやっても効果はなく、
u( ""+c )
あるいは
u( "#{c}" )
とやって初めて、今回の現象を回避することができた。
これは、文字列の複製を遅らせた結果、期せずしておこった現象にみえる。
バグのはずなんだけど、irbで再現しようと思ってもできないんだこれが。
http://redmine.ruby-lang.org/issues/show/1929
(ここに、見るべき場所を見つけることもできなかった人間がひとり)
* 正しくは ruby-1.9.2dev(2009-02-03)
♭ Steven LevithanHi. I translated this page using Google Translate. The la..