エスケープ処理を導入

以前に書きましたが、特にHTMLソースコードの検索を行う場合、検索機能を混乱させる特定の文字が「検索文字」に含まれる可能性が増えます。 その場合の結果は信頼性が無く、これはJavaScriptの検索では頻繁に問題になります。

 

例えば、下の囲み枠が記事中に沢山あり、背景色をまとめて変更したいとします。

 

 

HTML表示を調べると、このタイプの背景色の指定は「rgba(242, 250, 248)」で、ここは「HTML一括置換」をしたい所です。

 

 

そこで「S-R in Editor」を起動して、この色コードを「検索文字」にコピペして検索をするのですが、下の様に全くヒットしません。

 

 

これは「検索文字」に半角カッコの 「 ( 」  「 ) 」 が含まれているので、検索エンジンが正常に動作しないのが理由です。 これではツールが使えません。

 

 正規表現検索の「メタ文字」

以下の半角文字(メタ文字)が正規表現検索の検索文字列中にある時、特殊な意味を持つ記号として扱われます。 言い換えると、このメタ文字の機能を使って、検索機能を何倍にも高度化出来るのが正規表現検索です。

 

 \     .     ^     $     [     ]     *     +     ?     |     (     )     {     }

 

「S-R in Editor」の検索は正規表現検索で、これらの文字を含む「検索文字」で検索する場合は、メタ文字ではない事を検索エンジンに知らせる必要があります。

 

処理は簡単で、上のメタ文字があれば手前に「 \ 」(バックスラッシュ)を付けて検索エンジンに渡します。その操作はエスケープ処理と呼ばれます。 なおバックスラッシュは、日本語環境では「\」が表示されます。

 

\\    \.    \^    \$    \[    \]    \*    \+    \?    \|    \(    \)    \{    \} 

 

実際に「検索文字」の入力枠で 「 ( 」  「 ) 」 の前に「\」を記入して検索すると、下の様にちゃんと検索ができます。

 

 

 

自動的にエスケープ処理をするコード 

「S-R in Editor」の「検索文字」の入力文字列をチェックして、「メタ文字」があればその前に「 \ 」を自動的に記入する必要があります。 それには、既成のエスケープ処理コードを利用しました。 元コードは以下のページに掲載されています。

 

    綺麗に死ぬITエンジニア 

 

このページのコードは「グローバル関数」として利用する様に勧めていますが、そのままでは上手く使えず、コードをユーザースクリプト用の関数に書き換えました。

 

    function escapeRegExp(string){
        let reRegExp=/[\\^$.*+?()[\]{}|]/g;
        let reHasRegExp=new RegExp(reRegExp.source);
        return (string && reHasRegExp.test(string))
            ? string.replace(reRegExp, '\\$&')
            : string; }

 

上の関数を全体のコードの適当な場所で宣言して置き、例えば「ward」という変数に入った「検索文字」をエスケープ処理して「ec_ward」に取得するには、

 

let ec_ward=escapeRegExp(ward);

 

という書き方でOKです。 有難い!

 

このエスケープ処理を、「検索文字枠」の入力で検索するコードに追加して、無事にメタ文字対策が完了しました。

 

下は、先ほどの「rgba(242, 250, 248)」を「rgba(202, 220, 242)」に一括置換している所です。 全ての囲み枠が下図の背景色に変更出来ました。

 

 

これで、正規表現検索について何も考えなくても、HTMLコードを検索文字に入力できる様になりました。

 

 

意図せず「TEXT一括置換」が可能になっていた 

実は、作っている本人が実装したつもりが無かったのですが、一度「HTML置換」まで進み、次に漢字で検索をして「TEXT処理」を選択し「置換」に進むと、「HTML置換」と同じ要領で「TEXT置換」が出来てしまいました。

 

処理の流れを把握していない部分があり、下手な進み方をするとハイライト表示が残ったままになる等のバグがありますが、考えてみれば「一括置換」は「HTML処理」も「TEXT処理」も同じです。 問題はユーザーインターフェイスの纏め方で、これを見直せば、簡単に「TEXT一括置換」までは出来上がりそうです。

 

その後は、「TEXT順次置換」が組めれば、最初の案のツールは完成します。

 

 

「S-R in Editor」 ver. 1.0 

以下の「S-R in Editor」は、「検索語入力」~「TEXTのヒット文字列の巡回チェック」~「HTML置換・置換チェックとUNDO」が可能な、制作過程のツールです。 このバージョンから正規表現検索のエスケープ処理を導入しました。

 

Chrome版 / Firefox版の「Tampermonkey」の新規作成編集枠に、以下のスクリプトコードをコピー&ペーストする事で、このテストコードを試すことが出来ます。

 

〔コピー方法〕 右サイドバーの   マークのボタンを1度押してください。

 コード枠内の右クリック ➔ コード全体の選択 ➔ コピー操作 が可能になります。

 

 

〔 S-R in Editor 〕 ver. 1.0

 

// ==UserScript==
// @name         S-R in Editor
// @namespace  http://tampermonkey.net/
// @version      1.0
// @description  通常編集枠で実行できる 検索 / 置換 ツール
// @author       Ameba Blog User
// @match        https://blog.ameba.jp/ucs/entry/srventry*
// @grant         none
// ==/UserScript==


window.addEventListener('load', function(){
    let t_flag, h_flag;
    let buffer;
    let editor_iframe;
    let iframe_doc;
    let iframe_html;
    let iframe_body;
    let js_cover;

    let search_box;
    let search_word;
    let search_word_es;
    let replace_box;
    let replace_word;
    let result_box;
    let r_select;
    let s_1;
    let s_2;
    let s_3;
    let s_4;
    let s_radio;
    let t_select;

    let ua=0; // Chromeの場合のフラグ
    let agent=window.navigator.userAgent.toLowerCase();
    if(agent.indexOf('firefox') > -1){ ua=1; } // Firefoxの場合のフラグ


    let cke_1_contents=document.querySelector('#cke_1_contents'); // 監視 target
    let monitor=new MutationObserver(catch_key);
    monitor.observe(cke_1_contents, {childList: true}); // ショートカット待受け開始

    catch_key();

    function catch_key(){
        search_box=document.querySelector('#search_box');
        editor_iframe=document.querySelector('.cke_wysiwyg_frame');
        if(editor_iframe){ //「通常表示」の場合
            search_box=document.querySelector('#search_box');
            if(search_box){
                search_box.disabled=false; }

            document.addEventListener("keydown", check_key); // documentは先に指定
            iframe_doc=editor_iframe.contentWindow.document;
            iframe_doc.addEventListener("keydown", check_key); // iframeは後に指定

            function check_key(event){
                if(event.ctrlKey==true){
                    if(event.keyCode==123){
                        event.preventDefault();
                        if(h_flag!=2){ // HTML置換時でなければ「Ctrl+F12」でON/OFF
                            search_replace(); }}}
                if(event.keyCode==9 || event.keyCode==16 || event.keyCode==17 ||
                   event.keyCode==18 ||event.keyCode==19 || event.keyCode==27){
                    if(h_flag==2){ // HTML置換時に「Tab/Shift/Ctrl/Alt/Pause/Esc」を無効化
                        event.preventDefault();
                        out_h_flag_2();}}}}
        else{ //「HTML表示」の場合
            if_html(); }

        function out_h_flag_2(){
            js_cover.style.display='none';
            cke_1_contents.style.zIndex='0';
            if(iframe_body){
                iframe_body.contentEditable='true'; } // 編集可能にする
            iframe_body.innerHTML=buffer; // 置換処理をUNDO ⏹
            s_2.style.cursor='pointer';
            r_select.disabled=false;
            search_box.disabled=false;
            replace_box.disabled=false;
            s_3.style.display='none';
            s_4.style.display='none';
            replace_box.focus();
            h_flag=1; } // 1=処理開始

        function if_html(){
            if(search_box){
                search_box.disabled=true; }
            result_box.textContent=' ';
            s_1.style.display='none';
            s_2.style.display='none';
            replace_box.style.display='none';
            replace_box.value='';
            s_3.style.display='none';
            s_4.style.display='none';
            s_radio.style.display='none'; }
    } // catch_key


    function search_replace(){
        let i_body=document.querySelector('body.l-body');
        editor_iframe=document.querySelector('.cke_wysiwyg_frame');
        if(editor_iframe){ //「通常表示」の場合
            iframe_doc=editor_iframe.contentWindow.document;
            iframe_html=iframe_doc.querySelector('html');
            iframe_body=iframe_doc.querySelector('.cke_editable'); }

        let css=
            '#s_container {position: fixed; top: 12px; left: calc(50% - 490px); '+
            'background: #fff; border: 1px solid #aaa; border-radius: 4px; '+
            'padding: 6px 15px; min-width: 948px; z-index: 10;}'+
            '#search_box {width: 215px;} #replace_box {width: 215px; display: none;}'+
            '::placeholder {font-size: 15px; color: #bbb;}'+
            '#s_container input:disabled {color: #000; background: #eef1f3;}'+
            '#s_container input {font-size: 16px; padding: 2px 6px 0; -moz-appearance: none;}'+
            '#result {display: inline-block; min-width: 12px; padding: 4px 6px 2px; '+
            'margin-left: 5px; border: 1px solid #aaa; font-size: 16px;}'+
            '.s_sw {display: inline-block; vertical-align: -9px; font-size: 15px; '+
            'padding: 5px 8px 2px; border: 1px solid #aaa; overflow: hidden;}'+
            '.s_1 {margin-left: 15px; min-width: 4em; display: none;}'+
            '.s_2 {margin: 0 5px 0 15px; cursor: pointer; display: none; position: relative;}'+
            '.s_3, .s_4 {margin-left: 5px; color:#fff; background: #1e88e5; '+
            'cursor: pointer; display: none;}'+
            '#r_select {position: absolute; top: -15px; opacity: .6; box-shadow: 0 0 0 6em #fff;}'+
            '#r_select:checked {opacity: 1; box-shadow: 0 0 0 6em #d2eff4; z-index: -1;}'+
            '.s_radio {font-size: 15px; line-height: 16px; padding-left: 10px; display: none;}'+
            '.s_radio input{vertical-align: -2px; margin-left: 12px;}'+
            '.js_cover {position: fixed; top: 0; width: 100%; height: 100%; '+
            'background: rgba(0, 0, 0, .6); z-index: 10; display: none;}';

        let style_tag=document.createElement("style"); // css設定styleタグ
        style_tag.type="text/css";
        style_tag.setAttribute("class", "ep");
        style_tag.appendChild(iframe_doc.createTextNode(css));
        if(i_body.querySelector('.ep')){
            i_body.querySelector('.ep').remove(); }
        i_body.appendChild(style_tag);

        js_cover=document.createElement("div"); // クリック操作のブロックカバー
        js_cover.setAttribute("class", "js_cover");
        if(i_body.querySelector('.js_cover')){
            i_body.querySelector('.js_cover').remove(); }
        document.querySelector('#js-container').appendChild(js_cover);

        let css_iframe='.cke_editable m {background: #ffcc00;}'; // mタグの背景色指定

        let style_tag_iframe=iframe_doc.createElement("style");
        style_tag_iframe.type="text/css";
        style_tag_iframe.setAttribute("class", "ep");
        style_tag_iframe.appendChild(document.createTextNode(css_iframe));
        if(iframe_html.querySelector('.ep')){
            iframe_html.querySelector('.ep').remove(); }
        iframe_html.appendChild(style_tag_iframe);


        let s_container=document.querySelector('#s_container');
        if(!s_container){ //#s_containerが無い場合 生成して開始
            let insert_node_d=document.createElement('div');
            insert_node_d.setAttribute('id', 's_container');
            i_body.appendChild(insert_node_d);
            insert_node_d.innerHTML=
                '<input id="search_box" placeholder="検索文字" autocomplete="off">'+
                '<span id="result"> </span>'+
                '<span class="s_sw s_1"> </span>'+
                '<label for="r_select" class="s_sw s_2">'+
                '<input id="r_select" type="checkbox">置換</label>'+
                '<input id="replace_box" placeholder="置換文字" autocomplete="off">'+
                '<span class="s_radio"><input name="t_select" type="radio">一括処理'+
                '<input name="t_select" type="radio">順次</span>'+
                '<span><span class="s_sw s_3">OK</span>'+
                '<span class="s_sw s_4">UNDO</span></span>';

            search_box=document.querySelector('#search_box');
            result_box=document.querySelector('#result');
            r_select=document.querySelector('#r_select')
            replace_box=document.querySelector('#replace_box');
            s_1=document.querySelector('.s_1');
            s_2=document.querySelector('.s_2');
            s_3=document.querySelector('.s_3');
            s_4=document.querySelector('.s_4');
            s_radio=document.querySelector('.s_radio');
            t_select=document.getElementsByName('t_select');
            t_select[0].checked=true;
            let count_t, count_h, t_index;


            let native_line=iframe_html.scrollTop; // 通常表示のスクロール位置を記録
            search_box.focus();
            search_box.onkeydown=function(event){ // 🔽 検索ツール操作の開始点
                if(event.keyCode==13){
                    t_flag=0; // 0=検索前
                    h_flag=0; // 0=検索前
                    search_word=search_box.value;
                    get_search(); // bufferを取得する
                    rbox_disp(); } // bufferをリセットする
                if(event.keyCode==9){ //「Tab」で置換入力
                    event.preventDefault();
                    if(t_flag>0 || h_flag>0){
                        s_2.style.display='inline-block';
                        r_select.checked = true;
                        replace_box.style.display='inline-block';
                        setTimeout(function(){
                            replace_box.focus();},10); }}}

            search_box.onchange=function(){ //「Enter」を押さず移動した場合は検索語を再表示
                if(search_box.value!==search_word){
                    search_box.style.outline='2px solid #2196f3';
                    search_box.style.outlineOffset='-3px';
                    setTimeout(()=>{
                        search_box.style.outline='none';
                        search_box.value=search_word; }, 500); }}

            function get_search(){
                search_word_es=escapeRegExp(search_word); // 🔳 RegExp

                editor_iframe=document.querySelector('.cke_wysiwyg_frame'); // ここで取得
                if(editor_iframe){ //「通常表示」が実行条件
                    iframe_doc=editor_iframe.contentWindow.document;
                    iframe_body=iframe_doc.querySelector('.cke_editable');

                    buffer=iframe_body.innerHTML; // ハイライト表示のためソースコードを保存 🟦

                    let childs=searchNodes(iframe_body);
                    count_t=0; // テキストノードのヒット数
                    let result_t
                    for(let k=0; k<childs.length; k++){
                        if(childs[k].nodeType==3){
                            result_t=childs[k].textContent.match(new RegExp(search_word_es, 'g')); // 🔳 RegExp
                            if(result_t){
                                count_t+=result_t.length; }}}
                    count_h=0; // HTMLソース全体のヒット数
                    let result_h=iframe_body.innerHTML.match(new RegExp(search_word_es, 'g')); // 🔳 RegExp
                    if(result_h){
                        count_h=result_h.length; }}}

            function searchNodes(root){ // 全子孫ノードリストを作成
                var list=[];
                var search=function (node){
                    while (node !=null){
                        list.push(node);
                        search(node.firstChild);
                        node=node.nextSibling; }}
                search(root.firstChild);
                return list; }

            function rbox_disp(){
                r_select.checked=false;
                search_box.disabled=false;
                replace_box.disabled=false;
                replace_box.value='';

                if(count_t!=0 && count_h-count_t==0){
                    s_1.textContent='TEXT処理';
                    s_1.style.display='inline-block';
                    s_1.style.color='#000';
                    s_2.style.display='inline-block';
                    replace_box.style.display='none';
                    s_radio.style.display='none';
                    s_3.style.display='none';
                    s_4.style.display='none';
                    t_flag=1; // 1=処理開始
                    highlight(); }

                if(count_t!=0 && count_h-count_t!=0){
                    result_box.textContent='T:'+count_t+' H:'+(count_h-count_t);
                    s_1.textContent='処理不能';
                    s_1.style.display='inline-block';
                    s_1.style.color='red';
                    s_2.style.display='none';
                    replace_box.style.display='none';
                    s_radio.style.display='none';
                    s_3.style.display='none';
                    s_4.style.display='none'; }

                if(count_t==0 && count_h-count_t!=0){
                    result_box.textContent='H:'+(count_h-count_t);
                    s_1.textContent='HTML処理';
                    s_1.style.display='inline-block';
                    s_1.style.color='#000';
                    s_2.style.display='inline-block';
                    replace_box.style.display='none';
                    s_radio.style.display='none';
                    s_3.style.display='none';
                    s_4.style.display='none';
                    h_flag=1; // 1=処理開始
                    html_replace(); }

                if(count_t==0 && count_h-count_t==0){
                    result_box.textContent='T:'+count_t+' H:'+(count_h-count_t);
                    s_1.textContent=' - - - ';
                    s_1.style.display='inline-block';
                    s_1.style.color='#000';
                    s_2.style.display='none';
                    replace_box.style.display='none';
                    s_radio.style.display='none';
                    s_3.style.display='none';
                    s_4.style.display='none'; }}


            function highlight(){
                replace_word='<m>'+ search_box.value +'</m>';
                iframe_body.innerHTML=
                    iframe_body.innerHTML.replace(new RegExp(search_word_es, 'g'), replace_word); // 🔳 RegExp
                let mark=iframe_body.querySelectorAll('m');

                iframe_body.focus();
                let k=0;
                view(0);
                result_box.textContent='T:'+count_t+'│1';

                t_flag=2; // 2=巡回
                while(k<mark.length && t_flag==2){
                    k=next(k); }

                function next(k){
                    iframe_doc.addEventListener("keydown", check_key);
                    function check_key(event){
                        if(t_flag==2){
                            if(event.keyCode==37 || event.keyCode==38){ //「←」「↑」
                                event.preventDefault();
                                if(k>0){
                                    k=k-1; }
                                result_box.textContent='T:'+count_t+'│'+(k+1);
                                view(k);
                                return k; }
                            if(event.keyCode==39 || event.keyCode==40){ //「→」「↓」
                                event.preventDefault();
                                if(k<mark.length -1){
                                    k+=1; }
                                result_box.textContent='T:'+count_t+'│'+(k+1);
                                view(k);
                                return k; }
                            if(event.keyCode==13 || event.keyCode==27 ){ //「Enter」「Esc」
                                event.preventDefault();
                                search_box.focus(); }
                            else{
                                event.preventDefault();
                                search_box.focus();
                                setTimeout( function(){
                                    let keyEvent=new KeyboardEvent('keydown', {keyCode: 8});
                                    search_box.dispatchEvent(keyEvent);
                                },200);
                                setTimeout( function(){
                                    let keyEvent=new KeyboardEvent('keydown', {keyCode: 27});
                                    search_box.dispatchEvent(keyEvent);
                                },300);
                                setTimeout( function(){
                                    search_box.value=search_word;
                                },400); }}}

                    search_box.onfocus=function(){
                        if(t_flag==2){
                            result_box.textContent='T:'+count_t+'│-';
                            iframe_html.scrollTop=native_line; // 検索開始位置に戻る
                            iframe_body.innerHTML=buffer; // highlight を抜ける時はリセット ⏹
                            t_flag=1; }}} // 1=検索開始

                function view(k){
                    mark[k].scrollIntoView();
                    i_body.scrollIntoView(); }

                iframe_doc.addEventListener("click", stop_out); // 編集画面のクリックで巡回終了
                function stop_out(){
                    if(t_flag==1){ // 1=検索開始
                        native_line=iframe_html.scrollTop; } // クリックした場所をスクロール位置に指定
                    else if(t_flag==2){ // 巡回ループ内の場合
                        result_box.textContent='T:'+count_t+'│-';
                        native_line=iframe_html.scrollTop; // クリックした場所をスクロール位置に指定
                        iframe_body.innerHTML=buffer; // highlight を抜ける時はリセット ⏹
                        t_flag=1; }} // 1=検索開始
            } // highlight()


            function html_replace(){ // HTML置換処理
                r_select.onclick=function(){
                    if(r_select.checked){
                        replace_box.style.display='inline-block';
                        replace_box.focus(); }
                    else{
                        replace_box.style.display='none';
                        replace_box.value=''; }}

                replace_box.focus();
                html_roop();

                function html_roop(){
                    replace_box.onkeydown=function(event){ // 🔽 置換操作の開始点
                        if(event.keyCode==13 && r_select.checked){
                            buffer=iframe_body.innerHTML; // HTML置換の処理直前のソースコード保存 🟦
                            replace_word=replace_box.value;
                            iframe_body.innerHTML=
                                iframe_body.innerHTML.replace(new RegExp(search_word_es, 'g'), replace_word); // 🔳 RegExp
                            js_cover.style.display='block';
                            cke_1_contents.style.zIndex='11';
                            iframe_body.contentEditable='false'; // 編集不可にする
                            s_2.style.cursor='default';
                            r_select.disabled=true;
                            search_box.disabled=true;
                            replace_box.disabled=true;
                            s_3.style.display='inline-block';
                            s_4.style.display='inline-block';
                            h_flag=2; } // 2=置換時

                        if(event.keyCode==9 || event.keyCode==27){ //「Tab」「Esc」で処理前に戻る
                            event.preventDefault();
                            result_box.textContent=' ';
                            s_1.style.display='none';
                            s_2.style.display='none';
                            r_select.checked=false;
                            replace_box.style.display='none';
                            replace_box.value='';
                            search_box.focus();
                            h_flag=0; }} // 処理前

                    s_3.onclick=function(){
                        js_cover.style.display='none';
                        cke_1_contents.style.zIndex='0';
                        iframe_body.contentEditable='true'; // 編集可能にする
                        s_2.style.cursor='pointer';
                        r_select.disabled=false;
                        search_box.disabled=false;
                        replace_box.disabled=false;
                        result_box.textContent=' '; // 検索結果は変更される
                        s_1.style.display='none';
                        s_2.style.display='none';
                        r_select.checked=false;
                        replace_box.style.display='none';
                        replace_box.value='';
                        s_3.style.display='none';
                        s_4.style.display='none';
                        search_box.focus();
                        h_flag=0; } // 検索前

                    s_4.onclick=function(){
                        js_cover.style.display='none';
                        cke_1_contents.style.zIndex='0';
                        iframe_body.contentEditable='true'; // 編集可能にする
                        s_2.style.cursor='pointer';
                        r_select.disabled=false;
                        search_box.disabled=false;
                        replace_box.disabled=false;
                        s_3.style.display='none';
                        s_4.style.display='none';
                        iframe_body.innerHTML=buffer; // 置換処理をUNDO ⏹
                        replace_box.focus();
                        h_flag=1; } // 1=処理開始

                } // html_roop()
            } // html_replace()
        } // #s_container が無い場合「Ctrl+F12」で開始

        else{ // #s_container がある場合は「Ctrl+F12」で終了
            document.querySelector('#s_container').remove(); }
    } // search_replace()


    function escapeRegExp(string){
        let reRegExp=/[\\^$.*+?()[\]{}|]/g;
        let reHasRegExp=new RegExp(reRegExp.source);
        return (string && reHasRegExp.test(string))
            ? string.replace(reRegExp, '\\$&')
        : string; }

})

 

 

 

「S-R in Editor ⭐」最新版について 

旧いバージョンの JavaScriptツールは、アメーバのページ構成の変更で動作しない場合があり、導入する場合は最新バージョンをお勧めします。

 

●「S-R in Editor ⭐」の最新バージョンへのリンクは、以下のページのリンクリストから探せます。