読者です 読者をやめる 読者になる 読者になる

メモを揉め

お勉強の覚書。

RealDOM再入門

Greasemonkey、初Tampermonkey、初ブックマークレット触ってみました。
生DOMいっぱいさわりました。

ブラウザで検索・置換

いくつかサンプルコードを見ているとDocument.body.innerHTML正規表現で検索・置換するものが多かった、
というのはつまり以下の様な感じ

 

var html = document.body.innerHTML;
var replaced = html.replace(/探してます/gmi, '$1');
document.body.innerHTML = replaced;

あれ?こんなに簡単なの!?と思ったんだけど、
ふと疑問に感じたのは、innerHTMLで取ってきた文字列ってまさに「HTML」そのものだよな、
ってことはclassとかの属性もそれこそタグも検索・置換の対称になって崩れちゃいそうだけどなーと思ったら崩れた。

公開されているブックマークレットってコードが圧縮、エスケープしてあるものが多いのでちゃんとは読んでないんだけど、document.bodyから.childNodes行って、そのまた.childNodesって感じで検索して、テキストノードのみを検索・置換の対称にしてるっぽいものもあったので、そのアプローチを真似ることにした。

じゃあテキストノードだけに同じことをすれば…あれw?

同じようにinnerHTMLを抜き出して、加工して戻すことは出来ない。
なぜならばテキストノードにはtextContentプロパティはあってもinnerHTMLは無い。

テキストノードの要素全体を<span>で囲むなら

 

var newChild = document.createElement('span');
newChild.textContent = text.textContent;
text.parentNode.replaceChild(newChild, text);

で行けるとおもうんだけど、
テキストノードの一部だけ囲むということは…

これが

こうなるわけです。

ああやっぱりそんなにシンプルに書ける訳ないよな、DOMの操作って少し丁寧にやり出した瞬間、一気に複雑になる印象ある。

RealDOM再入門

HTMLをさわり始めた頃、生DOMを触ること無くjQueryに頼りきりだったので知らないことが多いわけです…ここだけの話。

まず先ほどの'あいうえお''あい<span>うえ</sapn>お'を入れ替える為の作戦ですが、

  1. <div>要素を一つ作り、そのinnerHTML'あい<span>うえ</sapn>お'を食べさせてパース
  2. パースしてできた子要素をDocumentFragmentappendChildする
  3. 'あいうえお'DocumentFragmentreplaceChildで入れ替える

という方法を取ることにしました。
もっと簡単な方法あったらおしえてください。

DOMツリーを検索してテキストノードのみに処理をかける

 

var searchTextNode = function(el, cb) {

    if (el.nodeType === Node.TEXT_NODE) {

        el.parentNode.replaceChild(cb(el.textContent), el);

    } else if (el.firstChild) {

        var childRef = Array.prototype.slice.call(el.childNodes, 0);

        childRef.forEach(function(child) {
            searchTextNode(child, cb);
        });

    }

};

実際にはさらにscriptstyle要素のテキストノードを除外する処理も入れました。

Nodeには.nodeTypeがある

これでそのノードがテキストノードかどうかをチェック出来る。

 

if (el.nodeType === Node.TEXT_NODE) {
    // ...
}

Node.TEXT_NODE === 3 です。
MDN: Node.nodeType

childNodesは子要素をいじると長さが変化するので参照を取る

childNodesはいわゆるArrayライクなオブジェクトで、DOMツリーと密接に紐付いている。
今回のように子要素を別の複数の要素に入れ替える場合

 

// 配列の長さをlenに保存しているため早めにループを抜けて失敗する
for (var i = 0, len = el.childNodes.length; i < len; i++) {
    process(el.childNodes[i]);
}

のような形でイテレートしようとすると

こうなって失敗する。

子要素への参照をArrayに保存しておいてイテレートすればうまくいく&ArrayなのでforEachなども使える。

 

var childRef = Array.prototype.slice.call(el.childNodes, 0);

childRef.forEach(function(child) {
    process(child);
});

テキストノードの一部を<span>で囲む

 

var wrapTextPart = function(txt) {

    var replaced = txt.replace(/見つけたい/gmi, '$&');
    var fragment = document.createDocumentFragment();
    var parser   = document.createElement('div');

    parser.innerHTML = replaced;

    while (parser.firstChild) {
        fragment.appendChild(parser.firstChild);
    }

    return fragment;
};

ここの注意点も先ほどと同じくchildeNodesが変化する点。
<div>要素でパースして出来た子要素をDocumentFragmentに移動する際、
childNodesforなどでイテレートすると失敗します。

 

// 失敗する
for (var i = 0; i < parser.childNodes.length; i++) {
    fragment.appendChild(parser.childNodes[i]);
}

インデックスをインクリメントして全ての子要素を移そうとするけど、一つ移すごとに<div>childNodes.lengthは短くなるので全ての子要素を移すことができずに終了する。

CSSルールをJavaScriptで追加する

検索した文字列は分かりやすいように背景の色を変えてハイライトします。
JSからCSSのルールを追加してみます。

ここで言うCSSは、HTMLElement.styleから要素の見た目を操作するやつではなく、
新しいスタイルシートやルールを追加したりする操作です。

まず<style>はJSではHTMLStyleElementで表現される

 

var styleEle = Document.createElement('style');

そして、<style>LinkStyleでもある。

<style> 要素と rel="stylesheet" が指定された <link> 要素 (<link rel="stylesheet">) は LinkStyle インタフェースを持ちます。
MDN: LinkStyle

CSSのルールを表すのがCSSRuleスタイルシートを表すのがStyleSheetをinheritしたCSSStyleSheetで表される。
「カスケーディングスタイルシートスタイルシート」になっちゃうけど気にしない。

LinkStylesheetプロパティで、その要素のStyleSheetオブジェクトにアクセスできる。

CSSStyleSheetにはinsertRuleというメソッドがあり、新しいルールを追加できる。

以上をふまえてJSでスタイルシートを追加する処理を書くと

 

var cssText  = '.find-and-replace {background-color:rgba(255,255,0,0.2);}';
var styleEle = document.createElement('style');

document.head.appendChild(styleSheet);   // <- 先にページに追加する
styleSheet.sheet.insertRule(cssText, 0); // <- .sheetにアクセスする

こうなった。

注意点として、<span>を作っただけでは.sheetnullなので、先にページに追加してから.sheetを通してCSSStyleSheetにアクセスし操作する。

ついに検索・置換プラグインが爆誕

ここからは成果物の機能を説明していきます、いいですね。

インストール

javascript:var%20main=function(){var%20e=/find-and-replace/,d=document.createElement("style");document.head.appendChild(d);d.sheet.insertRule(".find-and-replace{background-color:rgba(255,255,0,0.2);}",0);if(d=prompt("検索する文字列(正規表現使用可)")||""){var%20f=new%20RegExp(d,"gmi"),g=(prompt("置換したい文字列を半角スペース区切りで列挙")||"").split("%20");g.push("$&");var%20k=function(a,b){if(a.nodeType===Node.TEXT_NODE){var%20c=a.parentNode.tagName,d=e.test(a.parentNode.className);"SCRIPT"===c||"STYLE"===c||d||a.parentNode.replaceChild(b(a.textContent),a)}else%20a.firstChild&&Array.prototype.slice.call(a.childNodes,0).forEach(function(a){k(a,b)})};k(document.body,function(a){var%20b=document.createDocumentFragment();if(f.test(a)){a=a.replace(f,'<span%20class="find-and-replace"%20data-cache="$&">$&');var%20c=document.createElement("div");for(c.innerHTML=a;c.firstChild;)b.appendChild(c.firstChild)}else%20b.textContent=a;return%20b});var%20l=document.getElementsByClassName("find-and-replace"),h=0;changeWord=function(){if(l){var%20a=g[h++];h%=g.length;Array.prototype.forEach.call(l,function(b){var%20c=b.getAttribute("data-cache");b.textContent=c.replace(new%20RegExp(c,"gmi"),a)});return!1}}}},changeWord=null,keyMap={82:1,80:2,76:4,71:8},flag=0,downHandler=function(e){keyHandler(e,!0)},upHandler=function(e){keyHandler(e,!1)},keyHandler=function(e,d){var%20f=keyMap[e.which];if(f){flag=d?flag|f:flag&~f;if(7===flag)return%20flag=0,main(),!1;if(8===flag){flag=0;if(!changeWord)return!0;changeWord();return!1}}return!0};document.addEventListener("keydown",downHandler);document.addEventListener("keyup",upHandler);

でも、Greasemonkey、Tampermonkeyのほうが便利なのでオススメです。

使い方

ブックマークレットの場合はブックマークレットを開いてから)

  1. r+p+lキー同時押しで起動します、replacer+p+lと覚えると分かりやすいです。
  2. プロンプトが出るので検索したい文字を入力します。
  3. 同じく置換したい文字を入力します。
  4. gキーを押す度に検索した文字が切り替わります。

検索機能

プロンプトに入力した文字はそのまま正規表現オブジェクトに変換されるので、
それを利用した検索ができます。
\d\wなどの\バックスラッシュを\\dのようにエスケープする必要は無い)

  1. 検索したい文字列にマッチさせる

    • javascript


  2. .(ドット)はどんな文字にもマッチする

    • .っぱい


  3. '|'(パイプ)で区切って複数の文字列にマッチさせる

    • 製菓|成果|盛夏|生家|聖歌|生花|正貨|聖火





    • (月|火|水|木|金|土|日)曜日


  4. HTTPアドレスにマッチさせる

    • https?://[a-z0-9./?%=~_#]+


  5. rで終わる単語にマッチさせる

    • \w*[r](?=\W)


このように正規表現でマッチできるものならなんでもいけますが、
行頭(^)・行末($)などは、タグのマークアップの切れ目全てにマッチしてしまうのでイマイチな精度でした。

  • 縦読みを抽出しようとしたけどイマイチ

    • ^.{1}


置換機能

半角スペース区切りで複数の候補を登録できます。
$&はマッチした文字列を表します、これはString.prototype.replaceと同じ動作です。

gキーを押す度に次の候補に入れ替わります。

  1. 置換したい文字列を入力:検索ワード「理想」

    • 現実





  2. 複数の候補を半角スペース区切りで入力:検索ワード「好き」

    • 嫌い どちらとも言えない 寿司





      gを押す





      gを押す





      gを押す


  3. $&で元の文字列を利用する:検索ワード「波動」

    • $&拳ッッッ!!!





      gを押す





有用な使い方が思いつかない。