初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を押す

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