prototype.jsを読み解く

第2回 Prototypeライブラリ(198~639行目)

この記事を読むのに必要な時間:およそ 19.5 分

0251:   strip: function() {
0252:     return this.replace(/^\s+/, '').replace(/\s+$/, '');
0253:   },
0254: 
0255:   stripTags: function() {
0256:     return this.replace(/<\/?[^>]+>/gi, '');
0257:   },
0258: 
0259:   stripScripts: function() {
0260:     return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
0261:   },
0262: 

strip()では,文字列の前後の空白を除去しています。String.replace()が置換後の Stringオブジェクトを返すので,先頭の空白を除去するreplace(),最後の空白を除去するreplace()と連続処理させて返しています。

stripTags()では「HTMLタグのようなもの」を除去しています。この正規表現は

< <自身
\/? あってもなくてもいい / 文字 (これで開くタグ,閉じるタグをカバー)
[^>]+ タグ終端となる > 文字以外が1文字以上連続したもの
> >自身
gi g は置換をグローバルに,i は大文字小文字を無視(これは不要なはず)

という内容になっていて,HTMLタグのようなものを空文字列に置換します。

stripScripts()では,Prototype.ScriptFragment(すなわち'<script[^>]*>([\\S\\s]*?)<\/script>'という文字列)を使って<script>タグとその中身をごっそりと除去します。正規表現の中身は

<script[^>]*> <script>タグ(属性があっても可)
([\\S\\s]*?) [\\S\\s]で任意の文字(.では改行がマッチしない⁠⁠。それに*が付いて0文字以上の任意の文字。最後に?が付くので欲張らないマッチとなる。あとはその周囲をextractScripts()などで使うためにグループ化
<\/script> 要素を閉じるための<\/script>タグ

となっています。

0263:   extractScripts: function() {
0264:     var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
0265:     var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
0266:     return (this.match(matchAll) || []).map(function(scriptTag) {
0267:       return (scriptTag.match(matchOne) || ['', ''])[1];
0268:     });
0269:   },
0270: 
0271:   evalScripts: function() {
0272:     return this.extractScripts().map(function(script) { return eval(script) });
0273:   },
0274: 

extractScript()ではPrototype.ScriptFragment正規表現を使って,Stringオブジェクト内の<script>タグを見つけて,そのタグの内側を配列で返します。

まず,matchAll,matchOneという正規表現オブジェクトを作成します。

これらは同じ正規表現ルールを用い,フラグgが付いているかどうかだけが異なります。

gが付いているmatchAllをString.match()メソッドに使うと,複数回マッチしたものが配列として返り,gが付いていないmatchOneは「インデックス0がマッチした文字列全体,インデックス1以降がグループ化部分にマッチした部分文字列」という配列が返ります。

そして, (this.match(matchAll) || []) で,thisに含まれる全ての<script>タグ部分を配列で返します。もしString.match()がマッチしなかった場合,nullを返すのでその場合は||以降の[]が値となります。

次に,その配列を.map(function(s){ return (s.match(matchOne) || ['',''])[1] })に渡します。map()はEnumerable.map()として定義されているもので,後述します。

ここで,map()の対象となる配列の要素各々には<script>要素がまるごと文字列で入っています。

その各々に対して,今度はイテレータ関数の中でmatchOneでRegExp.global = falseでマッチさせています。

すると,Prototype.ScriptFragmentに含まれるグループ化の()により,一つ目の要素がマッチ文字列全体(この場合は<script>要素全体⁠⁠,二つ目の要素がグループ化の一つ目が入った配列が返されます。

match()が失敗した場合は['','']という配列を返すようにして,そこに対して (配列)[1] という演算を行い二番目の要素だけを取り出し,それをreturnで返しています。

これをextractScript()の呼出し側に返すことで,最終的に「タグの内側の文字列を配列で返す」というこの関数の目的となる処理が達成されます。

271行目からのevalScript()では,上記のextractScript()を呼び出した上で,返ってきた配列に対してmap()で各々をeval()する,という処理を行っています。

これにより,文字列インスタンス内に含まれる<script>タグそれぞれに対し,中身をeval()する,ということになります。

0275:   escapeHTML: function() {
0276:     var self = arguments.callee;
0277:     self.text.data = this;
0278:     return self.div.innerHTML;
0279:   },
0280: 
0281:   unescapeHTML: function() {
0282:     var div = document.createElement('div');
0283:     div.innerHTML = this.stripTags();
0284:     return div.childNodes[0] ? (div.childNodes.length > 1 ?
0285:       $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
0286:       div.childNodes[0].nodeValue) : '';
0287:   },
0288: 

275行目からのescapeHTML()では,&,<,>などの文字を,&amp;,&lt;,&gt;という形でエスケープする処理を行います。

ここでは単純にreplace()などを使うのではなく,空のdiv要素の内側にテキストノードを作成し,そこに該当文字列を代入し,divのinnerHTMLの値を返す,ということでブラウザ側にエスケープ処理を任せるようになっています。

276行目のvar selfはarguments.calleeが自分自身を指す関数オブジェクトなので,escapeHTML自身が入ります。

別途もう少し後の419行目あたりにおいて,String.prototype.escapeHTML.divに空のdocument.createElement('div')を,String.prototype.escapeHTML.textにdocument.createTextNode('')を用意しています。これを424行目のwith (String.prototype.escapeHTML) div.appendChild(text);によって,divの内側にテキストノードが入るように準備します。String.escapeHTML()の呼出しでは,ここで予め準備されたdiv, テキストノードを何度も再利用しています。

これにより,長い文字列でもそれほど速度を落とすことなく,よく使われるであろう置換処理を行うことができるようになっています。

ただし,402行目において,WebKitかIEの場合には上記の処理は行わずに,String.replace()を使った地味な置換処理をするような形でescapeHTML()関数自体を置き換えています。そのため,上記の処理が行われるのはそれ以外のブラウザー,ということになります。

これは,IEではテキストノードに含まれる\nがinnerHTMLを経由すると消えてしまう,ということへの対処のためです。WebKit(Safari)は単に最適化のためにこちらの関数を使っているようです。

このメソッドescapeHTML()は,該当文字に対してのみ置換が行われる,という想定なので,空白文字であろうとも消えてしまう,というのは困るのでそのような形になっています。

unescapeHTML()では,タグを除去した上でescapeHTML()と逆の処理を行います。

ここでタグを除去しているのは,対象となる文字列が意味のあるHTMLタグが含まれない状態(escapeHTML()をした後の状態)である,という前提のためです。

空のdiv要素を作成し,そのinnerHTMLにstripTags()した文字列を入れます。その際,文字が存在する場合にはchildNodesが作成されるので,その中身を結合したものを返します。

IEなどではchildNodes[0]にテキストノードがひとつだけ入るので,それだけを返せばいいのですが,Firefoxで4096バイト以上のテキストをinnerHTMLに入れた場合に複数のchildNodesに分割される,という報告があったために,childNodes配列に対してinject()を適用することにより,全てのchildNodesの値を結合して返す,という実装となっています。

著者プロフィール

栗山淳(くりやまじゅん)

S2ファクトリー株式会社株式会社イメージソース所属。
本業はWeb制作会社の裏方。得意分野はFreeBSDやPerlのはずだが,必要に迫られるとHTML/CSSやJavaScriptも書く。