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>お'
を入れ替える為の作戦ですが、
<div>
要素を一つ作り、そのinnerHTML
に'あい<span>うえ</sapn>お'
を食べさせてパース- パースしてできた子要素を
DocumentFragment
にappendChild
する 'あいうえお'
とDocumentFragment
をreplaceChild
で入れ替える
という方法を取ることにしました。
もっと簡単な方法あったらおしえてください。
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);
});
}
};
実際にはさらにscript
、style
要素のテキストノードを除外する処理も入れました。
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
に移動する際、
childNodes
をfor
などでイテレートすると失敗します。
// 失敗する
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
で表される。
「カスケーディングスタイルシートスタイルシート」になっちゃうけど気にしない。
LinkStyle
のsheet
プロパティで、その要素のStyleSheet
オブジェクトにアクセスできる。
CSSStyleSheet
にはinsertRule
というメソッドがあり、新しいルールを追加できる。
- insertRule
現在のスタイルシートに新しいスタイルルールを挿入します。
MDN: CSSStyleSheet
以上をふまえて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>
を作っただけでは.sheet
はnull
なので、先にページに追加してから.sheet
を通してCSSStyleSheet
にアクセスし操作する。
ついに検索・置換プラグインが爆誕
ここからは成果物の機能を説明していきます、いいですね。
インストール
-
Greasemonkey、Tampermonkey使う場合
- Greasemonkey、Tampermonkeyが入っていない場合は先にインストール
- ここのリンクでインストールウィザードが起動します。
-
ブックマークレットを使う場合
下のスクリプトをブックマークレットに登録。
(SafariはaddEventListener
が効かないっぽくて使えなかった)- 適当なページをブックマークする
- そのブックマークを編集してアドレスの部分に下のスクリプトをコピペ
- 分かりやすい名前に変更する
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のほうが便利なのでオススメです。
使い方
(ブックマークレットの場合はブックマークレットを開いてから)
r+p+l
キー同時押しで起動します、replace
のr+p+l
と覚えると分かりやすいです。- プロンプトが出るので検索したい文字を入力します。
- 同じく置換したい文字を入力します。
g
キーを押す度に検索した文字が切り替わります。
検索機能
プロンプトに入力した文字はそのまま正規表現オブジェクトに変換されるので、
それを利用した検索ができます。
(\d
、\w
などの\
バックスラッシュを\\d
のようにエスケープする必要は無い)
-
検索したい文字列にマッチさせる
javascript
-
.
(ドット)はどんな文字にもマッチする.っぱい
-
'|'(パイプ)で区切って複数の文字列にマッチさせる
製菓|成果|盛夏|生家|聖歌|生花|正貨|聖火
(月|火|水|木|金|土|日)曜日
-
HTTPアドレスにマッチさせる
https?://[a-z0-9./?%=~_#]+
-
r
で終わる単語にマッチさせる\w*[r](?=\W)
このように正規表現でマッチできるものならなんでもいけますが、
行頭(^
)・行末($
)などは、タグのマークアップの切れ目全てにマッチしてしまうのでイマイチな精度でした。
- 縦読みを抽出しようとしたけどイマイチ
^.{1}
置換機能
半角スペース区切りで複数の候補を登録できます。
$&
はマッチした文字列を表します、これはString.prototype.replace
と同じ動作です。
g
キーを押す度に次の候補に入れ替わります。
-
置換したい文字列を入力:検索ワード「理想」
現実
↓
-
複数の候補を半角スペース区切りで入力:検索ワード「好き」
嫌い どちらとも言えない 寿司
↓g
を押す
↓g
を押す
↓g
を押す
-
$&
で元の文字列を利用する:検索ワード「波動」$&拳ッッッ!!!
↓g
を押す
有用な使い方が思いつかない。