htsign's blog

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

DOM弄り

仕事中息抜きに書いてた。
文書の元々のソースコードと実際の表示が違うというのをやってみたかった。

ちなみに文書を読み込んだ時点で文書型は決まってしまうので、あとからDOCTYPE書き換えても意味ないです。

https://dl.dropboxusercontent.com/u/414379/www/DOMreplace/index.html

文字コードUTF-8(BOMなし)で作ったけど、英数だけだとバイナリレベルで見たときにASCIIと変わらない。
そうなると、DOM操作で日本語を挿入したときにWebブラウザが盛大に文字化け*1するので、それを防ぐために無理やり日本語をすべり込ませてみた。

DOCTYPEやDocumentそのものを作り出すメソッドがdocument.implementationにあるけど、まぁ普通にJavaScriptコードを書く分には一生お世話にならないメソッドたちでしょう。
JavaPerlなどの高位なプログラムからXMLを出力するときに使う為にあるものかなと。

CSSは特に何か意識したわけでもなく、なんとなく書いた。


JavaScript内で

var pi = document.createProcessingInstruction('xml', 'version="1.0" encoding="UTF-8"');
document.appendChild(pi); //==> <?xml version="1.0" encoding="UTF-8"?>

ってやってるけど、XML宣言はPIではないので、これは実は間違い。
比較的新しいバージョンのMozilla系エンジンでは動くけど、その他では例外を吐く。
その為try-catchで捕まえてエラーコードだけ出力するようにしておいてある。
XML宣言は、文字コードUTF-8の場合は省略してもいいことになっており、それ故にXHTMLでValidだと思います。

それから、createElementで要素生成してるけど、今回のケースではXHTMLで作っていることを明示的に示す必要があると思うので、本当ならcreateElementNSを使うべきだと思った。
めんどくさいのでしてない。

また、極力Web標準仕様に則って書いているため、古いIEIE7以前?)では

elem.setAttribute("class", "stretchable");

で処理がうまく通らないと思う。
理由は、古いIEではsetAttributeメソッドが、プロパティへの代入のシンタックスシュガーであるため。
つまりelem.setAttribute("class", "stretchable")IE内部でelem.class = "stretchable"として解釈されてしまっていた。というような記述をどこかで読んだ。

ちなみにsetAttributeについて、一部で第二引数にオブジェクトを入れてるコードを紹介しているサイトが散見されるけど、仕様上は第一引数も第二引数もプリミティブな文字列でないとダメだったはず。
どうしてもっていう場合は「古いIE向けのハックとして」程度に収めておくべきかと。

*1:試した限り、IE/Firefox/Chromeのそれぞれ現時点最新Verですべて発症

スーパーpre記法で書いたコード部分に行番号を出すようにした

こんなコードをフッタに入れた。

<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
<script>
jQuery(function($){
    $("pre.code").each(function(){
        var df = document.createDocumentFragment();
        var ol = $('<ol>').appendTo(df);
        $.each($(this).html().split("\n").filter(function(e, i, arr){
            return i !== arr.length - 1;
        }), function(i){
            $('<li>').html(this).appendTo(ol);
        });
        $(this).empty().append(df);
    });
});
</script>

jQueryで書いてるけど、ES5のメソッド使ってるので結局IE8以前では動きません。

今回はやっつけ実装なので、時間があったらまた見直してみる。

追記 (2014/01/18 13:55)

IE8以下でも動作するようにした。

<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
<script>
jQuery(function($){
    $("pre.code").each(function(){
        var df = document.createDocumentFragment();
        var ol = $('<ol>').appendTo(df);
        $.each($(this).html().split("\n").slice(0, -1), function(i){
            $('<li>').html(this).appendTo(ol);
        });
        $(this).empty().append(df);
    });
});
</script>

たぶん動作速度も上がったと思う。体感はできないけど。
どこが変わったかっつーとArray.prototype.filterをやめてArray.prototype.sliceにしただけ。
要は配列の最後の要素が邪魔だっただけなので、無理にfilter使う必要が全くなかった。

追記 2020/07/02

純粋な JavaScript のみで再実装しました。jQueryからの脱却。
処理内容自体は同じです。

<script>
document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('main-inner').querySelectorAll('pre.code').forEach(el => {
    const ol = document.createElement('ol');
    ol.append(...el.innerHTML.split('\n').slice(0, -1)
      .map(s => Object.assign(document.createElement('li'), { innerHTML: s })));
    el.innerHTML = '';
    el.append(ol);
  });
});
</script>

IEの開発者コンソールで見るテキストノードについて

IEの開発者コンソールで

var textNodeIterator = document.createNodeIterator(
    document,
    NodeFilter.SHOW_TEXT,
    function(node){
        return /^\s*$/.test(node.textContent)
        ? NodeFilter.FILTER_ACCEPT
        : NodeFilter.FILTER_SKIP;
    },
    false);
textNodeIterator.nextNode();

ってやるとコンソールにEmptyTextNodeって表示される。
あー空のテキストノードに特別に名前つけてんだーって思いました。っていうそれだけのお話。

それだけだと寂しいので少しだけ解説。

Document#createNodeIteratorNodeIteratorっつーオブジェクトが作られる。
これはDOM Level 2で定義されたインターフェースで、ドキュメント全体から条件に一致したDOMノードを自由に取ってこれる便利なアレ。

イテレータなので、nextNode()previousNode()で内部のポインタが動き、逐一ポインタが指すノードを返す。
ポインタが終端・または始端にある状態でそれぞれnextNode()previousNode()を実行するとnullを返す。
このインターフェースには現在のポインタに位置するノードを返すメソッド・プロパティはないので、

var currentNode;
currentNode = textNodeIterator.nextNode();

などとしないと値を保持できない。

ちなみにNodeIteratorには上記2つ以外にdetach()を加えた3つしかメソッドが存在しない。

以下、引数について書く。

第一引数がルートノード。
ここに指定したノード、およびその子孫から条件に一致したものを抜き取る。

第二引数がどの属性のノードを持ってくるか。
ここでまた新しくNodeFilterインターフェースが出てきたけど、ここにNodeIteratorTreeWalker*1で使われる定数が保持されてる。
今回はテキストノードのみが欲しかったのでNodeFilter.SHOW_TEXTとした。

第三引数には、第一引数と第二引数の条件にマッチしたノードを引数としたコールバック関数を置く。*2
この関数の戻り値としてNodeFilter.FILTER_ACCEPTを受け取るとそのノードはパスされ、逆にNodeFilter.FILTER_SKIPを受け取るとそのノードは弾かれる。
他にNodeFilter.FILTER_REJECTというのもあるが、これはNodeIteratorを使っている場合、NodeFilter.FILTER_SKIPと同じ意味を持つ。
違いが出てくるのはTreeWalkerの場合だけど、ここでは割愛。

ちなみに、フィルタリングなんか必要ねぇよっていう場合は、第三引数はnullでいいです。

第四引数は、エンティティ参照を文字列で置き換えるかの真偽値。だと思う。
trueにすると、例えば「&spades;」が「♠」に置き換えられる。たぶん。
実は試したことない。
勝手に置き換わると後々面倒だし、無難にfalseにしとくのがいいと思う。

正規表現部分については今更説明必要ないと思うけど、一応。
/^\s*$/の意味するところは、最初から最後まで半角スペースやタブ文字、改行コードのみになっていればマッチ。
プラスではなくアスタリスクなので0回以上の出現、つまり空文字の場合もマッチする。


これらを総合して、冒頭のコードの意味を見ると、
ドキュメント全体から、半角スペース・タブ文字・改行文字のみで構成された文字列、あるいは空文字のテキストノードをイテレータとして抽出し、イテレータの最初の1つを取り出した。
と言った感じ。

以上。
少しだけ解説のつもりが、記事の大半が解説になってしまった。

*1:NodeIteratorが平面的に列挙するのに対して、TreeWalkerは木構造をそのまま持ってくる。より自由に階層を潜ることが出来るが、メソッドも多くて複雑。

*2:"acceptNode"をキーとしたハッシュにして置いてもいい

UserExtension for Ancia

以下のスクリプトは軽量Webブラウザ「Ancia(アンシア)」向けのものです。
公式サイト: 名称未決定 - FrontPage

特定のURLで自動的に発動するタイプ

  • nicosearch.js2013/12/04
    • niconicoの検索窓にサジェスト機能を付与します。
  • visibleExtIcon.js2013/12/07
    • リンクのURLの拡張子がスクリプトに内蔵された一覧に合致すると、そのカーソルの近くに拡張子アイコンを表示します。
  • searchWiki.js2013/12/09
    • サイト内検索を拡張します。
      文字列を選択すると「検索」ボタンがマウスカーソルの近くに表示されます。

能動的に呼び出すタイプ

  • userStylesheetEditor.js2013/12/19
    • Ancia向けUserStyleSheetの編集に特化したエディターを提供します。

スクリプトについて

これらの一部、または全部は公式サイトでも配布されています。
ただし公式サイトのものはバージョンが古い場合があります。