書式整形をした「pre枠」での異常

「Both-WH」は、「通常表示」の編集画面のカーソル位置から、HTMLソースコード(HTML表示)のその部分へ、直接にジャンプ移動を可能にするツールです。

 

「通常表示」の編集枠内で「Ctrl+F8」を押すと、一瞬でHTMLの問題の場所へカーソルを移動できます。 そして「HTML表示」の編集枠内で「Ctrl+F8」を押すと、一瞬で元の編集場所を表示する事ができます。「Tampermonkey」にこのツールを登録すれば、「HTML表示」ボタンをクリックする事は、殆ど無くなります。

 

私はこのツールを日常的に使用しますが、先日、意外な問題点に気付きました。 開発の段階では、「サロゲートペア」「異体字」「タブ文字」がある場合の問題に対応するために苦労をしました。 この問題はもはや完璧にクリアしたと思っていたのですが、見落としがありました。

 

コードを掲載する「pre枠」内で行頭に「半角空白」がある場合、書式整形を施した「pre枠」という特殊な条件下なのですが、同様の問題が発生しました。

 

 

問題の状態 

下は、コード掲載用の「pre枠」に、普通に他の場所からコードをコピーしたものと、それを「To Space (nbsp)」というツールで「 」を「半角空白」に置換える整形処理をしたもので、外見は全く同じです。

 

 

この両コードで、「mark[k].」のドット後方にカーソルを置き、ツールで「HTML」へジャンプして、再び「通常表示」に戻ったのが下です。

 

 

上側ではコードは移動前と同じですが、下側はドット以降が消えてありません。 移動直後の HTMLはそれぞれ下の様な状態です。

 

 

 

上側は正常ですが、下側は明らかに変な事になっています。 移動直後は、その前の状態を「Ctrl+Z」で復元できますが、下側にそれを試して「特殊タグ」を削除する前の状態を再現したのが下図です。

 

 

移動後に「特殊タグ」の文字数15文字を削除するために、本来は「mark[k].」の後にカーソルがなければなりません。 しかし、カーソルが右にズレています。

 

削除開始位置のズレは、この行の先頭にある「半角空白」の8文字が原因です。 本来は青の範囲の削除が、「8文字」だけ右にズレた赤の範囲の削除になったのです。

 

 

 

整形したコードが原因 

「pre枠」の外では「半角空白」は複数並べる事は出来ません。 これはHTMLの規則で、「半角空白」が並んだ様に見えても、実際は「 」が間に入っています。

上側の未整形のコードは「 」が多数入っています。 この様な状態では、行先頭からの文字数が正確にカウントされ、カーソル位置のズレは生じません。

 

一方、「pre枠」内は「半角空白」を複数並べる事が可能で、そのための枠です。 また、「 」を「半角空白」へ置換える書式整形は、文字数制限対策で必須です。 この「半角空白」が並ぶ状態は整形ツールを使わないと出現しない状況で、それゆえ今まで見落しに気付けませんでした。

 

以下は対策コードで、「サロゲートペア」「異体字」によるカーソル位置ズレを補正するコードの後に、これを追加しました。

 

let spa=0; // 行頭の半角空白によるindex補正値(Tab文字補正を含む)
for(let k=0; k<index_uni; k++){
    if(activeline.textContent.indexOf('\u0020', k)==k ||
       activeline.textContent.indexOf('\u00A0', k)==k){
        spa+=1; }
    else break; }
index_uni -=spa; // 行頭の半角空白によるズレ補正をしたindex

 

このコードは、行の先頭に並ぶ「半角空白」と「&nbsp;」の数をカウントして、カーソル位置のズレを補正します。 結果的に判った事ですが、これまで「Tab文字補正」をしていましたが、その補正をこのコードが引き受けてしまいます。 従って、「Tab文字補正」のコードは削除しました。

 

なお、行の先頭以外の位置で「半角空白」が複数有っても、位置ズレは生じません。

以上の対策により、「pre枠」内で行頭の「半角空白」による問題を改善しました。

 

 

「Both-WH」の仕様

● アメブロの「最新版エディタ」で動作します。 デフォルトデザインの編集画面でも、Stylus等でアレンジした編集画面でも、正常に動作します。

 

●「通常表示」のカーソル位置で「Ctrl + F8」を押すと、「HTML表示」を開いて、その箇所にカーソルを自動的に表示します。

 

●「HTML表示」で「Ctrl + F8」を押すと、ショートカットで最後に離れた「通常表示」の位置を表示します。 但し、戻った通常表示枠にはカーソルを入れません。 これは、ショートカット連打によるミスを防ぐためです。

 

● 通常の記事では、HTMLへ1sec程度で移動出来ます。 ただし、実際のHTML文字数が見た目より大変に多い場合や、記事が長くて移動場所が文書の最後に近い場合などでは、「HTML表示」を開くまでに数秒かかる場合があります。

 

● 想定外の理由で、ジャンプ後のマークタグ削除処理により、周辺の記事の文字列を不本意に削除する事が有り得ます。 移動直後に誤削除に気付いたら「Ctrl + Z」で削除を取り消せますが、重要な文書でのトラブルは、自己責任とお考えください。

 

● このツールの旧バージョンがある場合は、「Tampermonkey」の管理画面で旧バージョンのスクリプトを削除するか無効にしてください。

 

 

「Both-WH」ver. 3.1

以下のコードを「Tampermonkey」にコピー&ペーストして登録する事で、このツールが使用出来ます。 このツールはChrome版 / Firefox版 の「Tampermonkey」で動作を確認しています。

 

〔コピー方法〕 軽量シンプルなツール「PreBox Button   」を使うと

  コード枠内を「Ctrl+左Click」➔「Copy code 」を「左Click」

  の操作で、掲載コードのコピーが可能になります。

 

 

〔 Both-WH 〕ver. 3.1

 

// ==UserScript==
// @name         Both-WH
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  「通常表示」「HTML表示」のカーソル位置を「Ctrl+F8」で往復する
// @author       Ameba Blog User
// @match        https://blog.ameba.jp/ucs/entry/srventry*
// @exclude      https://blog.ameba.jp/ucs/entry/srventrylist.do*
// @grant        none
// ==/UserScript==


window.addEventListener('load', function(){
    let editor_iframe;
    let iframe_doc;
    let wysiwyg; // 通常表示の iframe内 html
    let native_line; // 通常表示のスクロール位置
    let selection;
    let range;
    let insert_node;
    let mark_regex;
    let activeline;
    let codemirror_scroll;


    let target=document.getElementById('cke_1_contents'); // 監視 target は3箇所で共用
    let monitor1=new MutationObserver( catch_key );
    monitor1.observe(target, {childList: true}); // ショートカット待受け開始

    catch_key();

    function catch_key(){
        if(document.querySelector('.cke_wysiwyg_frame') !=null){ //「通常表示」から実行開始
            editor_iframe=document.querySelector('.cke_wysiwyg_frame');
            iframe_doc=editor_iframe.contentWindow.document;

            if(iframe_doc.querySelector('#i')){
                iframe_doc.querySelector('#i').remove(); } // 安全モードの場合に通常表示に戻り自動削除🔴

            iframe_doc.addEventListener('keydown', check_key);
            function check_key(event){
                if(event.which==17 || event.ctrlKey==true){
                    if(event.which==119 || event.keyCode==119){ set_mark(); }}}


            function set_mark(){
                selection=iframe_doc.getSelection();
                range=selection.getRangeAt(0);
                insert_node=iframe_doc.createElement("i"); // iタグ 空タグ
                insert_node.appendChild(iframe_doc.createTextNode("\u200B"));
                insert_node.setAttribute("id", "i");
                range.insertNode(insert_node); // カーソル位置にマークタグを書き込む

                wysiwyg=iframe_doc.querySelector('html');
                native_line=wysiwyg.scrollTop; // 通常表示のスクロール位置を記録

                let html_button=document.querySelector('button[data-mode="source"]');
                html_button.click( in_CodeMirror() ); // HTML表示に移動 🟦

                function in_CodeMirror(){
                    let monitor2=new MutationObserver( task_CodeMirror );
                    monitor2.observe(target, {childList: true});

                    function task_CodeMirror(){
                        if(document.querySelector('.CodeMirror-activeline pre')){ // アクティブ行が条件
                            function key_in(key_Code){
                                let keyEvent=new KeyboardEvent('keydown', {keyCode: key_Code});
                                document.querySelector('.CodeMirror textarea').dispatchEvent(keyEvent); }
                            mark_regex=RegExp('<i id="i">•</i>');

                            for(let j=0; j<120; j++){
                                let code=document.querySelector('.CodeMirror-code');
                                if(mark_regex.test(code.textContent)==true ){ break; }
                                else{ key_in(34); }} // PageDownで検索エリアを探す

                            let line_count=0;
                            for(let j=0; j<3000; j++){
                                activeline=document.querySelector('.CodeMirror-activeline pre');
                                if(mark_regex.test(activeline.textContent)==true ){ break; }
                                else{ line_count +=1; key_in(40); }} // Down でアクティブ行を下方へ移動

                            let index_uni=activeline.textContent.indexOf('<i id="i">•</i>'); // unicode16の文字数
                            let real=activeline.textContent.match(/./ug); // サロゲートペアを1文字に文字列を配列化
                            // console.log('real: ' + real); // 配列チェック用のメンテナンスコード 🔴

                            let sur=0; // サロゲートペア文字補正値
                            for(let k=0; k<index_uni; k++){
                                let str=real.slice(index_uni - k, index_uni - k +15).join('');
                                if(str=='<i id="i">•</i>'){ sur=k; break; }}
                            index_uni -=sur; // サロゲートペア文字のズレ補正したindex

                            let vas=0; // 異体字の結合文字補正値
                            for(let k=0; k<index_uni; k++){
                                if(real[k]=='•'){ vas +=1; } // 組文字に使用されるゼロ幅接合子
                                if(real[k]=='\uFE0F'){
                                    if(real[k+1]=='\u20E3'){ vas +=2; } // 特殊な▢数字の異体字の場合
                                    else{ vas +=1; }} // 異体字に使用される結合文字
                                if(real[k]=='\uFE0E'){ vas +=1; }} // 異体字に使用される結合文字
                            index_uni -=vas; // 異体字結合文字のズレ補正したindex

                            let spa=0; // 行頭の半角空白によるindex補正値(Tab文字補正を含む)
                            for(let k=0; k<index_uni; k++){
                                if(activeline.textContent.indexOf('\u0020', k)==k ||
                                   activeline.textContent.indexOf('\u00A0', k)==k){
                                    spa+=1; }
                                else break; }
                            index_uni -=spa; // 行頭の半角空白によるズレ補正をしたindex

                            key_in(36); // brの改行で行頭の絵文字の後にカーソルが入る場合を修正する
                            for(let i=0; i<index_uni; i++){ key_in(39); } // アクティブ行内で右方へindex値だけ移動
                            let zero_stop=0; //「HTML表示」でマークタグ削除をしない安全モードは「1」に変更 🔴
                            if(zero_stop==1){ key_in(37); } // 左へ1文字移動するだけで マークタグを削除しない
                            else{
                                for(let i=0; i<15; i++){ key_in(46); }} // マークタグ文字列の15文字を削除

                            codemirror_scroll=document.querySelector('.CodeMirror-scroll');
                            let win_height=codemirror_scroll.clientHeight;
                            let styles=getComputedStyle(document.querySelector('.cm-bracket'));
                            let line_height=parseFloat(styles.lineHeight);
                            let scroll=0;
                            if(line_count*line_height>=0.4*win_height){
                                if(line_count*line_height>=win_height){ scroll=0.6*win_height; }
                                else{ scroll=line_count*line_height - 0.4*win_height; }}
                            codemirror_scroll.scrollTop +=scroll;

                            document.querySelector('.CodeMirror textarea').focus(); // 入力窓にカーソルを入れる
                            monitor2.disconnect(); } // task_CodeMirrorの終了で監視ループを抜ける
                    }} // in_CodeMirror
            } // set_mark
        } // WYSIWYG表示での場合


        else if(document.querySelector('.CodeMirror') !=null){ //「HTML表示」から実行開始
            document.querySelector('.CodeMirror').addEventListener('keydown', function(event){
                if(event.which==17 || event.ctrlKey==true){
                    if(event.which==119 || event.keyCode==119){ to_native_line(); }}});


            function to_native_line(){
                let wysiwyg_button=document.querySelector('button[data-mode="wysiwyg"]');
                wysiwyg_button.click( in_wysiwyg() ); // 通常表示に移動 🟦

                function in_wysiwyg(){
                    let monitor3=new MutationObserver( task_wysiwyg );
                    monitor3.observe(target, {childList: true});

                    task_wysiwyg();

                    function task_wysiwyg(){
                        if(document.querySelector('.cke_wysiwyg_frame') !=null){ //「通常表示」が条件
                            editor_iframe=document.querySelector('.cke_wysiwyg_frame');
                            iframe_doc=editor_iframe.contentWindow.document;
                            wysiwyg=iframe_doc.querySelector('html');

                            wysiwyg.scrollTop=native_line; // 記録された通常表示のスクロール位置に移動
                            if(wysiwyg.scrollTop==native_line){
                                monitor3.disconnect(); }}} // task_wysiwyg の終了で監視ループを抜ける
                } // in_wysiwyg
            } // to_native_line
        } // HTML表示での場合
    } // catch_key

});

 

 

〔追記〕 2019.12.19

「記事の編集・削除」のページでエラーが出るのを防ぐため、「@exclude」を追加しました。

 

 

 

「Both-WH ⭐」最新版について 

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

 

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