htsign's blog

ノンジャンルで書くつもりだけど技術系が多いんじゃないかと思います

テキストをパースして要素に追加する処理比較

有名なのはinnerHTMLですね。
ただ、これ以外にもテキストを評価してDOMツリーに追加する方法がありますので、今回はこれをやってみました。

単純にループで同じメソッドおよびプロパティ操作をしているだけです。
試行回数は1000回ですが、これはinnerHTMLの実行時間が長くなりすぎるからです。

まず計測用の関数を定義しておきます。
console.timeが使える場合はこれを使うようにし、使えない場合(IEさん)はDate()で時間を計測します。

var benchmark = function(callback){
	if (console.time && console.timeEnd){
		console.time("benchmark");
		callback();
		console.timeEnd("benchmark");
	}
	else {
		var time = new Date();
		callback();
		console.log("benchmark: ", new Date() - time);
	}
};


比較するのは次の4選手。
ご存じinnerHTMLに加えて、
insertAdjacentHTMLメソッド、
RangeオブジェクトのcreateContextualFragmentメソッド。
そしてDOMParserオブジェクトのparseFromStringメソッドです。

1) HTMLElement #innerHTML

benchmark(function(){
	Array.apply(null, Array(1000)).forEach(function(){
		document.body.innerHTML += "<div></div>";
	});
});

2) HTMLElement #insertAdjacentHTML

benchmark(function(){
	Array.apply(null, Array(1000)).forEach(function(){
		document.body.insertAdjacentHTML("beforeend", "<div></div>");
	});
});

3) Range #createContextualFragment

benchmark(function(){
	var range = document.createRange();
	range.selectNodeContents(document.body);
	Array.apply(null, Array(1000)).forEach(function(){
		document.body.appendChild(range.createContextualFragment("<div></div>"));
	});
});

4) DOMParser #parseFromString

benchmark(function(){
	var parser = new DOMParser();
	Array.apply(null, Array(1000)).forEach(function(){
		document.body.appendChild(
			parser.parseFromString("<div></div>", "application/xml").documentElement
		);
	});
});

結果

公平を期すため、ベンチ結果はabout:blank上で実行しました。

Internet Explorer 10 Mozilla Firefox 21 Google Chrome 26
innerHTML 3162ms (*) 619ms (*) 1238ms (*)
insertAdjacentHTML 49ms 13ms (*) 11ms
createContextualFragment 67ms 18ms (*) 9ms
parseFromString 78ms 83ms (*) 22ms

(*) ただし挿入対象のDOMツリーが巨大になればなるほど遅くなる

結論

  • Chromeは全体的に速い。流石Chrome先生。
  • Firefoxは基本的にDOMツリーがでかければでかいほど計算コストもでかくなる。
    普通の重量級ページで実行した場合、Firefoxが最も低速でした。
  • IEは可もなく不可もなく。
  • 何回も処理回す場合はinnerHTMLはなるべく避けるようにしましょう。
  • コード量と速度のバランスを考えるとinsertAdjacentHTMLがいいと思います。

っつーか

正直、別に今更やらなくてもベンチマーク取ってるサイトはググればいくらでも出てきますけどね…。

はてなダイアリーでキーワードポップアップ

Greasemonkey用スクリプト「キーワードポップアップ」の配布について - はてなダイアリー日記

ずいぶん昔の記事ですが、はてなさんはこんなの出してたんですね。
これを他のブラウザでも動くようにちょちょいと改造しましたよっていうのが今回のエントリ。
詳しく調べてないのでよく知りませんが、きっと俺以外にもやった人はたくさんいると思います。

本当はこのスクリプトをこのはてダに貼り付けたかったんですが、やってみたらはてなさんから「スクリプト埋め込みは許可してねーよ」って怒られたので仕方なしにコードだけ置いとく。

本当にごく一部しか触ってなくて、コードの99%くらいははてなさんのオリジナルのままです。
やったことと言えば、

  • MozOpacityとかいうクソベンダプリフィクス付きプロパティをopacityに直した
  • op = parseFloat(popup.style.opacity);みたいなコードをop = parseFloat(getComputedStyle(popup).opacity);に直した
  • setTimeout(loadKeyword, 600, rssurl, aTag.innerHTML);setTimeout(function(){ loadKeyword(rssurl, aTag.innerHTML) }, 600);に直した
  • 一部のイベントハンドラをイベントリスナに直した
  • NodeListオブジェクトに対してArray.prototype.slice.callした

くらいです。

IEの場合、9以降でないと動かないと思います。
ただし addEventListener for IE8 のようなスクリプトを実行するか、

var addEvent = function(target, evt, func){
  if (window.addEventListener) {
    return target.addEventListener(evt, func, false);
  }
  else if (window.attachEvent) {
    return target.attachEvent("on" + evt, func);
  }
  else {
    target["on" + evt] = func;
  }
};

こんな感じのスクリプトを実行して、イベント関連の記述をちょこっといじれば古いIEでも動くかもしれません。
CSSのopacityプロパティは古いIEでは解釈できないのでfilter:progid:DXImageTransform〜みたいなクソめんどくさいIE独自プロパティを追加してあげる必要もあります。


このスクリプトをはてダ上で実行して、キーワード上にマウスカーソルを持って行くとキーワードの説明がポップアップします。

こんな感じです。結構便利です。

ブログパーツみたいに貼り付けられたらもっと便利だったんですけどねー。


追記:試してないけど、古いIEならCSSのbehaviorプロパティでスクリプト埋め込めるかもしんない。

マルチメディア要素へのリンクのクリックイベントに<audio>や<video>なウィンドウをポップアップする機能をオーバーライドするブックマークレット

たぶんこれで完成形。

比較的新しいAPIをフル活用しているので、動かなかったらそんなブラウザを使っている自分が悪いと思ってください。
一応IE10とChrome26で動作を確認しています。
Firefox20ではちょっと確認した限り動作しませんでした。まぁFirefoxはクズってことで


(function(){
  var checked = [];        // 同じURLに複数回リクエストを送るのを防ぐためのリストを格納
  var timeout = 30 * 1000; // タイムアウトになるまでの時間(単位:ms)
  
  var allowList = ["audio", "video"];

  if (!HTMLElement.prototype.addPopup) { // addPopupメソッドを要素に追加

    Object.defineProperty(HTMLElement.prototype, "addPopup", {
      value: function(url){
        var self = this;

        var xhr = new XMLHttpRequest(); // HEADリクエストを送る
        xhr.open("HEAD", url, true);
        xhr.onreadystatechange = function(){
          if (xhr.readyState === 4) {
            console.log(xhr.status + " : " + url);

            if (xhr.status !== 200) return;

            var header = xhr.getResponseHeader("Content-Type");
            var type   = header.split("/")[0];

            if (allowList.some(function(v){ return v === type })) { // いずれかにヒットすれば true
              console.log(header + " - OK");
              addPopupItem(self, type);
            }
          }
        };
        xhr.send(null);

        window.setTimeout(function(){ // 一定時間でタイムアウト
          xhr.abort();
        }, timeout);
      }
    });
  }
  else {
    return false;
  }

  var elems = document.querySelectorAll("a[href], area[href]");
  elems = Array.prototype.slice.call(elems); // リンカブル要素を全て抽出

  for (var i in elems) {
    var url = elems[i].href;

    if (url.indexOf("http") === 0) {

      url = url.split("#")[0]; // URLに # を含む場合は除去

      if (checked.every(function(li){ return url !== li })) { // 全てとアンマッチなら true
        checked.push(url);
        elems[i].addPopup(url);
      }
    }
  }

  function addPopupItem(target, mediaType) { // ポップアップを生成
    target.onclick = function(evt){
      if (evt.ctrlKey && evt.altKey) {
        return true; // Ctrl+Alt を押しながらクリックすれば普段のクリックと同じ効果
      }
      else {
        openPopup();
        evt.preventDefault();
      }
    };

    function openPopup() {
      var popup = window.open("", "media-popup", "titlebar=no,menubar=no,toolbar=no,location=no,status=no,scrollbars=no");
      var doc   = popup.document;
      var rect;
      var element;

      var diff = {
        width:  popup.outerWidth  - doc.documentElement.clientWidth,
        height: popup.outerHeight - doc.documentElement.clientHeight
      };

      if (!rect) {
        element = doc.createElement(mediaType);
        element.setAttribute("controls", "controls");
        doc.body.appendChild(element);
        rect = element.getBoundingClientRect();
        doc.body.removeChild(element);
      }
      
      doc.body.addEventListener("DOMSubtreeModified", function(){ // 要素数が変化したらウィンドウサイズを変える
        var len = this.childNodes.length;
        popup.resizeTo( rect.width + diff.width, rect.height * len + diff.height );
      }, false);

      var style = doc.createElement("style");
      doc.querySelector("head").appendChild(style);
      var s = style.sheet, i = 0;
      [
        "body {"
        + "margin: 0;"
        + "padding: 0;"
        + "}",
        "audio {"
        + "display: block;"
        + "margin: auto;"
        + "}"
      ].forEach(function(value){
        s.insertRule(value, i++);
      });

      element = doc.createElement(mediaType);
      element.setAttribute("src", target.href);
      element.setAttribute("autoplay", "autoplay");
      element.setAttribute("controls", "controls");
      element.addEventListener("ended", function(){ // 終端まで移動したら自らを削除する
        this.parentElement.removeChild(this);
        close();
      }, false);

      /*
        末尾に挿入する
        appendChildでないのはIEがDOMSubtreeModifiedに反応しない(らしい)ため
        http://msdn.microsoft.com/en-us/library/gg558014(v=vs.85).aspx
      */
      doc.body.insertBefore(element, null);

      function close() { // ポップアップ内のメディアがゼロのとき、子ウィンドウを閉じる
        doc.querySelectorAll( allowList.join(",") ).length === 0
        ? popup.close() : 0;
      }
    }
  }

})();

解説書くのすげーめんどくさいんですが一応解説

前回はウィンドウのサイズを中身の大きさに合わせて変更する術が思いつかなかったので適当にサイズ決めてましたが、
ウィンドウサイズはwindow.outerWidthwindow.outerHeightで取れるし
ウィンドウの中身はdocument.documentElement.clientWidthdocument.documentElement.clientHeightで取れることに
今更気づいてその辺をルーチンに組み込みました。
気づいてみれば呆気ないものですね…。


で、ウィンドウサイズを変更するタイミングですが、当然中身のサイズが変わったときです。
中身のサイズが変わるということはDOMツリーに変化があるということ。

DOMツリーが変わると発火する便利なイベントがあります。
それがMutationEventsと呼ばれるものです。
実はこのMutationEvents、利用を推奨されていません
可能ならばDOM4のMutationObserverを使って欲しいと、MDNに書かれています。*1
しかしながら、MutationObserverはIEでは実装されていません。
機能的には同じなので今回はMutationEventsを利用しました。

var diff = {
  width:  popup.outerWidth  - doc.documentElement.clientWidth,
  height: popup.outerHeight - doc.documentElement.clientHeight
};

if (!rect) {
  element = doc.createElement(mediaType);
  element.setAttribute("controls", "controls");
  doc.body.appendChild(element);
  rect = element.getBoundingClientRect();
  doc.body.removeChild(element);
}

doc.body.addEventListener("DOMSubtreeModified", function(){ // 要素数が変化したらウィンドウサイズを変える
  var len = this.childNodes.length;
  popup.resizeTo( rect.width + diff.width, rect.height * len + diff.height );
}, false);

この部分が今回のキモです。

一旦要素の大きさを測るために空のメディア要素を作ってすぐ削除しています。
そしてこれを元にして、DOMツリーが変化する度にウィンドウの大きさを変えていくのです。

これを見れば勘のいい方はすぐ気づくかと思いますが、
最初に作成される要素が<audio><video>かで大きさの基準が決まってしまいます。
つまり<audio><video>が混在する場合、ウィンドウの大きさがえらいことになってしまいます。
なのでこれは運用で回避してください。めんどくさいので仕様バグとします。


イベントはDOMSubtreeModifiedで拾っています。
読んで字のごとく、ツリーに変化があったときに発火します。
これ以外にもMutationEventsには、属性に変化があった時を検知するDOMAttrModifiedや、ツリーへの挿入時のみ検知するDOMNodeInsertedなど便利なイベントがたくさんあります。
しかし繰り返しますがMutationEventsは非推奨です

非推奨と言えば、(使うのは)やめろやめろと言われていたにもかかわらずHTML5で正式に採用が決定した<iframe>とか、
標準技術じゃないよーと言われているにもかかわらずかなり普及しているinnerHTMLとかいろいろありますねー。

まぁMutationEventsに関してはあまり普及してない感じしますし、
IE11がMutationObserverをどうするのかは知りませんが、採用するのであれば取って代わられるのかもしれません。
もしかしたらそのまま残るのかもしれません。
別にどっちでもいいです。

*1:https://developer.mozilla.org/en-US/docs/DOM/Mutation_events
URLでは言及されていませんが、非推奨なのはどうやら同期的に動作するから、らしいです。
つまりイベントで発火した処理が全て終わるまで次の処理に移れない、ということみたいですね。

MIMEタイプがaudio/xxxやvideo/xxxなときにやをどうにかこうにかするスクリプトを改造した

以前書いたの<a>の直後にエレメントを追加して直接ページ上で再生するやつだったけど、今回のはwindow.openを使って子ウィンドウを作るタイプにした。

こっちの方がスタイルシートの影響範囲をいちいち考えなくて済むから楽かもしれない。

audio/xxxvideo/xxxなリンクをクリックする度に、子ウィンドウを開いてそこに<audio><video>な要素を配置します。
既に子ウィンドウが出ている場合は、同じ子ウィンドウの末尾に要素を追加していきます。
それぞれのメディア要素は再生が終わると自身を殺します。
他のすべてのメディア要素が死んでいる状態で、最後のメディア要素が終了すると子ウィンドウが閉じる仕組みです。

(function(){
  var checked = [];        // 同じURLに複数回リクエストを送るのを防ぐためのリストを格納
  var timeout = 30 * 1000; // タイムアウトになるまでの時間(単位:ms)
  
  var allowList = ["audio", "video"];

  if (!HTMLAnchorElement.prototype.addPopup) { // addPopupメソッドをA要素に追加

    Object.defineProperty(HTMLAnchorElement.prototype, "addPopup", {
      value: function(url){
        var self = this;

        var xhr = new XMLHttpRequest(); // HEADリクエストを送る
        xhr.open("HEAD", url, true);
        xhr.onreadystatechange = function(){
          if (xhr.readyState === 4) {
            console.log(xhr.status + " : " + url);

            if (xhr.status !== 200) return;

            var header = xhr.getResponseHeader("Content-Type");
            var type   = header.split("/")[0];

            if (allowList.some(function(v){ return v === type })) { // いずれかにヒットすれば true
              console.log(header + " - OK");
              addPopupItem(self, type);
            }
          }
        };
        xhr.send(null);

        window.setTimeout(function(){ // 一定時間でタイムアウト
          xhr.abort();
        }, timeout);
      }
    });
  }
  else {
    return false;
  }

  var elems = document.getElementsByTagName("a");
  elems = Array.prototype.slice.call(elems); // A要素を全て抽出

  for (var i in elems) {
    var url = elems[i].href;

    if (url.indexOf("http") === 0) {

      url = url.indexOf("#") + 1 ? url.split("#")[0] : url; // URLに # を含む場合は除去

      if (checked.every(function(li){ return url !== li })) { // 全てとアンマッチなら true
        checked.push(url);
        elems[i].addPopup(url);
      }
    }
  }

  function addPopupItem(target, mediaType) { // ポップアップを生成
    target.onclick = function(evt){
      if (evt.ctrlKey && evt.altKey) {
        return true; // Ctrl+Alt を押しながらクリックすれば普段のクリックと同じ効果
      }
      else {
        openPopup();
        return false;
      }
    };

    function openPopup() {
      var popup = window.open("", "media-popup", "menubar=no,toolbar=no,location=no,status=no,scrollbars=yes");
      var doc   = popup.document;

      var style = doc.createElement("style");
      doc.querySelector("head").appendChild(style);
      var s = style.sheet, i = 0;
      [
        "body {"
        + "margin: 0;"
        + "padding: 0;"
        + "}",
        "audio {"
        + "display: block;"
        + "margin: auto;"
        + "}"
      ].forEach(function(value){
        s.insertRule(value, i++);
      });

      var element; // 殻の中身を定義

      element = doc.createElement(mediaType);
      element.setAttribute("src", target.href);
      element.setAttribute("autoplay", "autoplay");
      element.setAttribute("controls", "controls");
      element.addEventListener("ended", function(){ // 終端まで移動したら自らを削除する
        this.parentElement.removeChild(this);
        close();
      }, false);

      doc.body.appendChild(element);
      var rect = element.getBoundingClientRect();
      popup.resizeTo(rect.width + 80, rect.height + 200);

      function close() { // 再生中のメディアがゼロのとき、子ウィンドウを閉じる
        doc.querySelectorAll( allowList.join(",") ).length === 0
        ? popup.close() : 0;
      }
    }
  }

})();
ブックマークレット
javascript:(function(d,A,c,l,P,E,i,u){l=["audio","video"],A=function(E,t,P){P=function(p,d,S,e,r){p=open("","media-popup","menubar=no,toolbar=no,location=no,status=no,scrollbars=yes"),d=p.document,S=d.createElement("style"),i=0;d.querySelector("head").appendChild(S);["body{margin:0;padding:0}","audio{display:block;margin:auto}"].forEach(function(s){S.sheet.insertRule(s,i++)});e=d.createElement(t);e.src=E.href,e.autoplay="autoplay",e.controls="controls";e.addEventListener("ended",function(){this.parentElement.removeChild(this);d.querySelectorAll(l.join(",")).length===0?p.close():0},false);d.body.appendChild(e);r=e.getBoundingClientRect();p.resizeTo(r.width+80,r.height+200)},E.onclick=function(e){if(e.ctrlKey&&e.altKey)return true;else{P();return false}}},c=[],P=HTMLAnchorElement.prototype;if(P.addPopup)return false;else{Object.defineProperty(P,"addPopup",{value:function(u,S,X){S=this,X=new XMLHttpRequest();X.open("HEAD",u);X.onreadystatechange=function(h,t){if(X.readyState===4){console.log(X.status+" : "+u);if(X.status!==200)return;h=X.getResponseHeader("Content-Type"),t=h.split("/")[0];if(l.some(function(v){return v===t})){console.log(h+" - OK");A(S,t)}}};X.send();setTimeout(function(){X.abort()},3e4)}})}E=d.querySelectorAll("a");for(i=E.length;i;){u=E[--i].href.split("#")[0];if(/^http/.test(u))if(c.every(function(i){return u!==i}))E[i].addPopup(c[c.length]=u)}})(document)

目立たないけど、ブックマークレット版の方はコード量を減らすためにかなりの工夫を凝らしてるつもりです。
可能な限りセミコロンやブレースを削ったり、効率的なビルドイン関数を使ったり、ちまちまやってます。
読んでもらえると楽しいです。たぶん。可読性最悪ですが。

MIMEタイプが audio/xxx か video/xxx なファイルをで直接参照していた場合にやを直後に追加するスクリプト

あっちの界隈でよくあるADV*1なんかだと、公式サイトのキャラ紹介にmp3なファイルが直接置かれている場合が少なくありません。
特にIEさんやFxさんはこの手のものに出会うと、デフォルトでは一旦DLしてローカルの別のアプリケーションで再生する、という挙動になっています。

例えばゴミプラグインの代表格であるQuickTime辺りをインストールしておけばブラウザ上で再生させることも可能でしょう。
でもそれも外部のアプリケーションに頼っていることに変わりありません。

そこでHTML5で新しく定義された<audio><video>を使ってみましょう。
これらはブラウザ上にメディアファイルを埋め込むことができるものです。
従来のHTMLで言えば embed, object, applet 辺りの仲間でしょうか。

これらを埋め込むことでブラウザ上でも快適に音楽や動画を鑑賞することができます。

それでは

以下コード

(function(){
  var checked = [];        // 同じURLに複数回リクエストを送るのを防ぐためのリストを格納
  var timeout = 30 * 1000; // タイムアウトになるまでの時間(単位:ms)

  if (!HTMLAnchorElement.prototype.addPopup) { // addPopupメソッドをA要素に追加

    Object.defineProperty(HTMLAnchorElement.prototype, "addPopup", {
      value: function(url){
        var self = this;

        var xhr = new XMLHttpRequest(); // HEADリクエストを送る
        xhr.open("HEAD", url, true);
        xhr.onreadystatechange = function(){
          if (xhr.readyState === 4) {
            console.log(xhr.status + " : " + url);

            if (xhr.status !== 200) return;

            var header = xhr.getResponseHeader("Content-Type");
            var type   = header.split("/")[0];
            var allowList = ["audio", "video"];

            if (allowList.some(function(v){ return v === type })) { // いずれかにヒットすれば true
              console.log(header + " - OK");
              addPopupItem(self, type);
            }
          }
        };
        xhr.send(null);

        window.setTimeout(function(){ // 一定時間でタイムアウト
          xhr.abort();
        }, timeout);
      }
    });
  }
  else {
    return false;
  }

  // 他のスタイルから影響を受けないよう Date.now() を追加
  var thisClass = "media-popup-" + Date.now();

  var elems = document.getElementsByTagName("a");
  elems = Array.prototype.slice.call(elems); // A要素を全て抽出

  var popupItem = (function(){ // ポップアップ用の殻を作成
    var div = document.createElement("div");
    div.className = thisClass;
    return div;
  })();

  var popupStyle = function(){ // スタイル定義
    var style = document.createElement("style");
    document.getElementsByTagName("head")[0].appendChild(style);
    var s = style.sheet;

    // カーソルを置いたらポップアップするように
    s.insertRule("." + thisClass + "{"
    + "display: none !important;"
    + "text-indent: 0 !important;"
    + "z-index: 100 !important;"
    + "}", 0);
    s.insertRule("a:hover + ." + thisClass + ", ." + thisClass + ":hover {"
    + "display: block !important;"
    + "}", s.cssRules.length);

    s.insertRule("." + thisClass + "{"
    + "position: absolute !important;"
    + "}", s.cssRules.length);
  };
  popupStyle();

  for (var i in elems) {
    var url = elems[i].href;
    
    if (url.indexOf("http") === 0) {
      
      url = url.split("#")[0]; // URLに # を含む場合は除去
      
      if (checked.every(function(li){ return url !== li })) { // 全てとアンマッチなら true
        checked.push(url);
        elems[i].addPopup(url);
      }
    }
  }

  function addPopupItem(target, mediaType) { // ポップアップを生成
    var popup = popupItem.cloneNode(), s = popup.style;
    s.left = target.offsetLeft + "px";
    s.top  = target.offsetTop + target.offsetHeight + "px";

    var element; // 殻の中身を定義

    element = document.createElement(mediaType);
    element.setAttribute("src", target.href);
    element.setAttribute("preload", "metadata");
    element.setAttribute("controls", "controls");

    popup.appendChild(element);
    target.parentElement.insertBefore(popup, target.nextSibling); // 目標の直後に配置

    // 某ADVの公式サイトでなぜかレイアウトが崩れるので対症療法的な何か
    var objRect = popup.getBoundingClientRect();
    if (objRect.left === 0 && objRect.top === 0) {
      s.left = s.top = "auto";
    }
  }

})();
ブックマークレット


原理的には非常に簡単なもので、

  1. ドキュメント上からA要素をすべて抜き出し、
  2. それぞれの参照先に一気にHEADリクエストを送り、
  3. MIMEタイプを取得、
  4. それが audio/mpeg や video/mp4 等の場合にポップアップを作成する。

というもの。


うまくいくとこのように

マウスカーソルを重ねるだけで

再生窓が表示されます。(画像はIE10のものです。再生窓の形状はブラウザにより異なります。)

あとはマウスカーソルを再生ボタンに持って行ってクリックするだけです。
ね、簡単でしょう?


スクリプトの性質上、ブラウザやその設定によっては相手方のサーバーに負荷をかける恐れがあります。ご利用は計画的に。
MIMEタイプとブラウザの組み合わせによっては再生することができません。例えばFirefoxは audio/mpeg を再生出来ません。
※IE8以下では余裕で動きませんが、そもそも<audio>とか<video>に対応していないのでご安心ください。

更新履歴

  • 2013/03/03 05:36
    • URLに「#」が含まれていた場合のログ表示が#付きURLのままだったのを修正しました。
    • 一度リクエストを送った箇所には再度送らないようにしました。
      • 代わりに同じURLの<audio>,<video>が複数回出ても2つ目以降にはポップアップが付かないようになりました。
        • これについては後々いい感じの解決法を考えて修正予定。
  • 2013/02/27 23:42
    • URLに「#」が含まれると必ず404を返す事象に対処しました。
    • CSSプロパティに対して!importantキーワードを付与するようにしました。
  • 2013/02/26 21:45
    • 少しだけリファクタリングしました。
    • 一部サイトでなぜか彼方に飛んでいったので強引に修正しました。
  • 2013/02/26 01:44
    • 一部のサイトにおいて、 text-indent でかっ飛ばされていたので対抗しました。
    • ログを出力するようにしました。
    • 半角スペースをいちいち%20に置き換えていたのをやめました。

*1:いわゆるエロゲ