絶対可憐チルドレン #7~#12

ある意味ハーレムもの。


えろい回に限ってあからさまな力の入れようでどん引き(笑

まあ、結果的に面白いなら、それもアリか。

ドナルドはどう思う?

JavaScript: $n

// 仮作成版。完成版は後述
var $n = function(nodeName, attrs){
    var _parent = this, _callee = arguments.callee, _node = document.createElement(nodeName);
    for (var i in attrs) {
        if (i === "style") {
            var ns = this.node.style, as = attrs.style;
            for (var j in as) ns[j] = as[j];
        } else {
            this.node[i] = attrs[i];
        }
    }
    return {
        node: _node,
        create: function(){
            var self = this, parent;
            while ((parent = self.parent()).node) self = parent;
            return self.node;
        },
        text: function(text){
            _node.appendChild(document.createTextNode(text));
            return this;
        },
        child: function(){
            var child = _callee.apply(this, arguments);
            _node.appendChild(child.node);
            return child;
        },
        parent: function(){
            return _parent;
        },
        do: function(F, thisp){
            F.call(thisp, this.node, this);
            return this;
        },
    };
};
var iso8601 = function(d){
    var ary = [d.getMonth()+1, d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()].map(function(v){ return ("0"+v).slice(-2); });
    // Date#getTimezoneOffsetとかいうのもあるけどパス
    return d.getFullYear() + "-" + ary.slice(0,2).join("-") + "T" + ary.slice(2).join(":") + "+09:00";
};

console.dirxml(
$n("div", {className: "hentry"})
    .child("h2", {className: "entry-title"})
         .child("a", {rel: "bookmark", href: location.href, textContent: "ENTRY-TITLE"}).parent()
         .parent()
    .child("div", {className: "entry-content"})
         .child("p")
              .text("ENTRY-CONTENT").parent()
         .parent()
    .child("abbr", {className: "updated", title: iso8601(new Date()), textContent: (new Date()).toString()}).parent()
    .child("address", {className: "vcard author"})
         .child("a", {className: "url fn", href: location.href, textContent: "AUTHOR"})
         .create()
)
<div class="hentry">
    <h2 class="entry-title">
        <a rel="bookmark" href="http://mayokara.info/note/">ENTRY-TITLE</a>
    </h2>
    <div class="entry-content">
        <p>ENTRY-CONTENT</p>
    </div>
    <abbr class="updated" title="2008-07-13T06:17:56+09:00">Sun Jul 13 2008 06:17:56 GMT+0900 (JST)</abbr>
    <address class="vcard author">
        <a class="url fn" href="http://mayokara.info/note/">AUTHOR</a>
    </address>
</div>

どう見てもjQuery。

doを挟むことで、既にあるノードをappendChildしたりできる。

livedoor Readerのはどんなんだっけかな。

parent().child()の省略形でsibling()、はちょっとややこしいか。


実際のところ、Range#createContextualFragment使ったほうが楽だとは思う。

追記

prototype仕様の完成版できた。

var $n = function(nodeName, attrs){
    if (!(this instanceof $n)) return new $n(nodeName, attrs);
    if (!nodeName) { this.node = document.createDocumentFragment(); return; }
    this.node = document.createElement(nodeName);
    for (var i in attrs) {
        if (i === "style") {
            var ns = this.node.style, as = attrs.style;
            for (var j in as) ns[j] = as[j];
        } else {
            this.node[i] = attrs[i];
        }
    }
};
$n.prototype = {
    _parent: null,
    node: null,
    create: function(){
        var self = this;
        while (self._parent) self = self._parent;
        return self.node;
    },
    text: function(text){
        this.node.appendChild(document.createTextNode(text));
        return this;
    },
    child: function(nodeName, attrs){
        var child = new $n(nodeName, attrs);
        this.node.appendChild(child.node);
        child._parent = this;
        return child;
    },
    sibling: function(/* nodeName, attrs */){
        var parent = this._parent;
        return parent.child.apply(parent, arguments);
    },
    parent: function(){
        return this._parent;
    },
    do: function(F, thisp){
        F.call(thisp, this.node, this);
        return this;
    },
};

prototypeにオブジェクトを直接代入してるせいで、this.constructorが消えてしまうのが痛いなあ。

使い方

// ルートにdivを作る、引数なしでDocumentFragment
$n("div")
    // プロパティを指定してdivをappendChild
    .child("div", {className: "section", style: {margin: "1em 0"}})
        // textNodeをappendChild
        .text("何かのテキスト")
        // さらにanchorをappendChild
        .child("a", {href: location.href, textContent: "リンク"})
            // aの中から出る
            .parent()
    // div.sectionの弟ノードを作る
    .sibling("ul")
        .child("li", {textContent: "list1"})
        // 2つ目以降は.sibling()で追加する(.parent().child()と等価)
        .sibling("li", {textContent: "list2"})
        .sibling("li", {textContent: "list3"})
        .sibling("li", {textContent: "list4"})
            // li[4]の中から出る
            .parent()
    // さらに弟ノードを作って事前に用意したDocumentFragmentなどを入れる
    .sibling("div").do(function(v){ v.appendChild(df); })
    // 作成終了、ルートノードを返す
    .create()

こんな感じ。

JavaScript: application/xhtml+xmlなページを扱う際の注意

application/xhtml+xmlなページでcontinue_reading.user.jsが動かない問題が解決した。

既に更新済。以下覚書。

Range#createContextualFragmentでillegal string例外が起こる

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

この2行のせい。

つまり、<\?\w+とか<!\w+でこけている。

<--(HTMLにおけるコメント)は通る。

というわけで

var html = str.replace(/^[\s\S]*<html[^>]*>|<\/html>[\s\S]*$/ig, "");

解決。

innerHTMLに代入しようとすると例外が起こる

innerHTML、読み取り専用っぽい。

というわけで、DOM関数で組み立ててappendChildして解決。

余録:document.implementation.createDocument

http://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-102161490

var doc = document.implementation.createDocument("", "html", null);

でOK。

第2引数はdocumentElementを参照するために必要。

余録:Range#createContextualFragment

html/body要素が消える。

Firefox2ではhead要素も消え、事前にrange.setStartAfter(document.body)などとして何らかの選択をしないと評価されない。

「continue_reading.user.js」更新 その2

Greasemonkey: continue_reading.user.js - mayokara note

  • loadedURLsを保持するようにした
    • from AutoPagerize
    • 3→2→3→2→……みたいなパターンを回避できる
  • Google Images用のDocumentFilterを添付した
    • ソースの冒頭部でON/OFF可能(デフォルトはON)
    • DocumentFilterはnextLinkを引数に取る
  • パース時の処理をtry catchで括った

Google Imagesはウィンドウサイズに応じてカラム数を変えてくる

iframe.style.width = document.documentElement.clientWidth + "px"で対処。

overflow: hidden; width: 1px; height: 1px;なdiv要素で隠す。

document.adoptNode(node)とdocument.importNode(node, true)

外部ドキュメントからのノードは、現在のドキュメントに挿入する前に importNode() を使ってクローンを作る (あるいは adoptNode() を使って取り込む) べきです。

DOM:document.adoptNode - MDC

エラーは出さないけど、使うべきだそうな。

  • document.adoptNode(node)
    • 他のドキュメントのnodedocumentの所属にする
    • 読み込み中のimg要素を移した後、元のドキュメントを閉じると、読み込みが完了しない
  • document.importNode(node, true)
    • 他のドキュメントのnodedocumentに複製する
    • 読み込み中のimg要素を移した後、元のドキュメントを閉じても、読み込み続ける

iframe.contentWindow.addEventListener("DOMContentLoaded", F, false)

iframe.style.display = "none"だと発生しない(Firefox2だけ?

iframe.style.visibility = "hidden"の場合は問題ない。

application/xhtml+xmlなページのときRange#createContextualFragmentするとillegal characterエラー

相変わらず、さっぱり意味がわからない。

GM_xmlhttpRequestでoverrideMimeTypeをtext/htmlにしてるのがまずいのかなあ。

「continue_reading.user.js」更新

Greasemonkey: continue_reading.user.js - mayokara note

  • ELControllerを使うようにした
  • URLインクリメントの対象にsearch値も含むように
    • リンクチェックするので実害はないと判断
    • Wassr 検索で動くように
  • 通常時とロード中のアイコンの色がAutoPagerizeと逆だったのを修正

現状の問題点

  • 継ぎ足し分が大きすぎる
    • 本文の単位量が少なくエントリ数の多いブログなど
    • 対応難
  • Googleの検索結果に地図を含む場合誤爆する
    • offsetWidthが異常に大きい要素があるのが原因
    • 対応難
  • pageElementがposition: absolute;のとき領域が被る
    • position: relative;に書き換えれば一応何とかなる
      • デザインが崩れて見れなくなるページが出てくる可能性がある
    • 対応保留
  • Twitterで最新の発言が何度も継ぎ足される
    • classNameにhentryがついてるせい
      • ピンポイントな対応は避けたい
      • DocumentFilter作れば一応できる
    • 対応保留
  • application/xhtml+xmlなページで動かない
    • Range#createContextualFragmentでillegal characterエラー
    • 取得時にtry catchしておくべきかも
    • 調査中

予定

  • Google Images対策のDocumentFilter化
    • 1回ミスるの覚悟なら可能だけど微妙
  • findPageElement重い
    • td要素が大量にあるときとかひどい
    • チェック済のノードにぶつかったら打ち切りたい
    • 上手い方法を考え中
      • 事前に兄弟ノードのサイズを比べてフィルタする?
    • 要素固有の ID を取得する - IT戦記が使えそう
      • だがprototype触る

追記

AutoPagerizeでtr要素の継ぎ足し時にレイアウトが崩れることがある件 - 0x廃棄階層 - 統治局

このパッチを参考に、pageElementの先頭がtr要素のときナビゲーションにdiv要素ではなくtr+td要素を使うようにした。

これにより、AutoPagerize – Userscripts.orgのコメント欄などの表示が崩れなくなる。

Twitterはdiv要素にもhentryをつけてるので効果なし。

JavaScript: ページング部分からnextLinkを取得(未解決)

たとえばGoogleの検索結果の6ページ目で以下を実行すると

$g('/descendant::a[self::node()+1=following::a[1]]', null, function(v){
    return v.textContent;
})
// -> ["1", "2", "3", "4", "7", "8", "9", "10", "11", "12", "13", "14"]

こうなる。

すなわち、6だけテキストになっているため、元々a要素がない6とその前の5が落ちる。


これを踏まえて。

// location.href === "http://www.google.co.jp/search?q=hoge&start=50"
var nextLink = null, prev = 0;
$g('/descendant::a[self::node()+1=following::a[1]]', null).some(function(v){
    if (!prev || prev+1 == v.textContent) {
        prev = Number(v.textContent);
    } else {
        nextLink = v.href;
        return true;
    }
});
console.log(nextLink || "NOT FOUND");
// -> http://www.google.co.jp/search?hl=ja&q=hoge&start=60&sa=N

うむ、いけそう。

……なんだけど、このままだと1ページ目のときと最後の1ページ前のとき取れない。

1ページ目のときは「2」ちょうどのパターンで何とかなるけど、どうするかな。

追記

$g('/descendant::a[self::node()-1=preceding::a[1] or self::node()+1=following::a[1]]', null, function(v){
    return v.textContent;
})
// -> ["1", "2", "3", "4", "5", "7", "8", "9", "10", "11", "12", "13", "14", "15"]

前も見るようにするとこう。

この場合は、1ページ目と最後のページで結果を区別できないのが痛い。

あと、両者共通して「Comment(0) Trackback(1)」の数字にだけリンクが貼られているときなどに誤爆する。

配列の要素が3つ以上、とかの条件をつければ何とかならないこともないが、総ページ数が3ページ以下のときに検出できなくなる。

hashがあるかどうかを見る方法もあるけど、汎用性がない。

複数の画像を指して1、2、3とリンクを貼っているページもある。


総じて微妙。

JavaScript: add/removeEventListenerラッパー

var ELController = function(target, type, handler, useCapture){
    return {
        on: function(){
            target.addEventListener(type, handler, useCapture);
        },
        off: function(){
            target.removeEventListener(type, handler, useCapture);
        },
    };
};
var onclick = new ELController(window, "click", function(){
    alert(1);
}, false);

(click) // -> none

onclick.on();

(click) // -> alert(1)

onclick.off();

(click) // -> none

クロージャを使って直接オブジェクトを返す形にすると、this._typeなどとして値を保持する必要がなくなる。

Firefox上では、EventListenerという名前はDOM2 EventsのEventListenerインタフェースとして、既に使われているみたい。


複数の同一の EventListener が、同じ EventTarget に同じ引数で登録された場合、重複するインスタンスは反映されません。EventListener が 2 度呼び出されることはなく、重複するインスタンスは反映されないので、removeEventListener で手動で削除する必要はありません。

DOM:element.addEventListener - MDC

イベントターゲット上にある現在のどのイベントリスナーも指定していない引数付きの removeEventListener は、何の効果もありません。

DOM:element.removeEventListener - MDC

イベントリスナは関数(というより、EventListenerインタフェース)単位で管理されているようで、連続でonしても1度しか実行されず、onしていない状態でoffしても特にエラーにはならない。

なので、状態を記憶して実行を制御しなくてもOK。

コンストラクタ関数の返り値とnew

コンストラクタ関数は別のオブジェクトを返り値として返すことも可能で、その場合は返されたオブジェクトがnew式の値となる。thisの値であったオブジェクトは破棄される。

JavaScript 第5版 - 9章 クラスとコンストラクタとプロトタイプ(1) - (DxD)∞

newをハッタリに使える。結果は関数呼び出しのときと同じ。

インスタンス生成とオブジェクト返却の比較

  • インスタンス生成
    • __proto__prototypeを繋ぐだけなのでコストが小さい
    • 内部値がpublicになってしまう
  • オブジェクト返却
    • 毎回オブジェクトを作って返すのでわずかにコストがかかる
    • 内部値をprivateにできる

JavaScript: Firefox3でdocument.evaluateの第5引数を使うとSTRING_TYPEの返り値が変

var xp = null;
(xp = document.evaluate('//a/@href', document, null, XPathResult.STRING_TYPE, xp)).stringValue
// -> "http://mayokara.info/note/"
(xp = document.evaluate('//a/@href', document, null, XPathResult.STRING_TYPE, xp)).stringValue
// -> "http://mayokara.info/note/http://mayokara.info/note/"

Firefox2では増えないんだけど、Firefox3だと増える。

きちんと調べたわけではないけど、どうもXPathResult.STRING_TYPEのときだけっぽい。

不具合?


JavaScript: $g - mayokara note

使い回さないように戻した。

JavaScript: element.getBoundingClientRectとdocument.getBoxObjectForの対応

var box = document.getBoxObjectFor(elem);
// 上端の絶対座標 box.y
elem.getBoundingClientRect().top + window.scrollY
// 下端の絶対座標 box.y + box.height
elem.getBoundingClientRect().bottom + window.scrollY
// 左端の絶対座標 box.x
elem.getBoundingClientRect().left + window.scrollX
// 右端の絶対座標 box.x + box.width
elem.getBoundingClientRect().right + window.scrollX

document.getBoxObjectForの値が整数なのに対して、element.getBoundingClientRectの値は小数になる。

また、document.getBoxObjectForはborderを含むのに対し、element.getBoundingClientRectはborderを含まない(?)。


というわけで、draggable_textarea.user.js更新。

Greasemonkey: continue_reading.user.js

continue_reading.user.js

SITEINFOなしで動くAutoPagerize的な何か。

nextLinkの推定

Greasemonkey: nisefastforward5.user.js - mayokara note

nisefastforward5.user.jsと同じく、以下の順で探索。

  1. link[@rel="next"]
  2. a[@rel="next"]
  3. 特定のキーワードで始まるリンク(デフォルトは「Next」「次」の2つ)
  4. URLをインクリメントして、そこにリンクが貼られていればそれを使う
  5. テキストが「2」ちょうどのリンク

@rel="next"が存在せず、@rel="prev"のみが存在するときは進行方向を逆にする(Twitterなど)。

pageElementの推定

JavaScript: pageElementの推定 - mayokara note

これの「親要素も使う」版を手直ししたものを採用。

親・子ノードとの描画領域比を使う。

ちょっと重いかも。

microformatsが見つかったときは、それを利用する。

microformats

hAtom、xFolk、AutoPagerize独自形式に対応。

AutoPagerizeとの互換性

window.AutoPagerize.addDocumentFilterwindow.AutoPagerize.addFilterを実装。

その他

  • GM_xmlhttpRequestdocument.implementation.createDocument+Range#createContextualFragment方式
  • 任意の場所をダブルクリックでON/OFF切り替え