文字数カウントのズレ問題

「HTML表示」編集画面は特異な構成です。 HTMLコードに見えているのは演出であり、実際はタグ括弧の「<」「>」に始まり、コードの文字列をことごとくバラバラに分解して並べています。 HTML編集枠の作業は、普通の「textarea」の編集に見えますが、実際は内部で特殊な操作が行われています。

 

この理由で、「通常表示」で書き込んだマークタグの削除は、簡単ではありません。 カーソル位置を人が編集する様にキー操作で移動し、人が押す様に削除キーを押して、マークタグを「編集で消す」操作をスクリプトで行う必要があります。「通常表示」なら簡単なコードで可能な「削除」が、「HTML表示」ではとても困難です。

 

これを更に難しくするのが、サロゲートペア文字の問題です。 サロゲートペアの文字は、普通に扱うと 1文字が2文字にカウントされます。 マークタグが段落最初から何文字目で始まるかを調べ、カーソルを移動して削除しますが、サロゲートペア文字が有ると、文字数のカウントがズレて来ます。

 

しかしこれは、サロゲートペアの文字を一般の文字同様に1文字として扱う配列に納める方法で、クリア出来ます。

 

難関は、一部のWin10絵文字で使われる「異体字」「組文字」の問題です。 これは、絵文字以外にも、多国語対応などで既に導入されている技術の様です。(普通のブログで見る事は少ないですが)

 

この種の文字は、Win10絵文字では1/6程度が該当し、それらの文字がこのツールで移動する段落内にあると、文字数カウントがズレます。 これまでの「Both-WH」では、この種の文字をチェックして、マークタグの削除をしない対処をしていました。

 

 

ズレが生じる機序を究明 

今回は、ズレが生じる原因を詳しく調べました。 以下は、文字カウントのズレを生じる絵文字の例です。 絵文字の右の「*あいうえお*」は、実際の移動テスト用です。

 

1文字ズレ 

 

「HTML表示」画面で、この文字周辺のカーソルの動きを調べます。

 

 

すると、他の「<」「p」「>」「💻」等は、カーソルが1文字ずつ移動しますが、「🐱」は、隣の「」を併せて1文字として移動します。 言い換えると「」文字はカーソルが飛ばします。 しかるに、この行で文字数カウントする基準になる配列は以下の様な状態です。

 

 

この行で、ある文字へ何回「→」キーを押せば到達できるかのカウント数は、「」以降では配列のindexより1減ることになります。 これが「1文字ズレ」の原因です。「HTML表示」では文字化けしていますが、本来の絵文字とコードは以下です。

 

  🐱‍💻   \uD83D\uDC31\u200D\uD83D\uDCBB

 

先の「」の文字コードは「\u200D」で、これはゼロ幅接合子と呼ばれます。 これがあれば、1文字分ズレる事が判りました。

 

2文字ズレ 

 

 

先の「\u200D」に続いて「HTML表示」で何も表示されませんが、「\uFE0F」という異体字の結合文字が「♀️」文字の後ろにあります。「HTML表示」の画面では、カーソルがこの結合文字を飛ばします。

 

この文字周辺の文字カウントを調べる基準の配列は以下です。

 

 

この絵文字の後方の文字には、配列のインデックスよりも2だけ少ない数の「→」で到達できます。 下は、本来の絵文字とコードです。

 

  🏄‍♀️  \uD83C\uDFC4\u200D\u2640\uFE0F

 

3文字ズレ 

 

 

 

  ⛹️‍♀️  \u26F9\uFE0F\u200D\u2640\uFE0F

 

同様に、この絵文字では3個の特殊文字が有り、文字後方では 3文字分ズレます。

 

囲み数字(組文字) 

以下の11個の文字は、特殊な組文字です。

 

 #️⃣  \u0023\uFE0F\u20E3

  *️⃣  \u002A\uFE0F\u20E3

 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ \u0030\uFE0F\u20E3~\u0039\uFE0F\u20E3

 

 

 

この文字周辺のカーソルの動きを見ると、組文字は1個の文字として扱われます。 基準となる配列を見ると妙なカンマが表示されています。

 

 

しかし、要は3個分の文字コードを1文字のカーソル移動で通り過ぎるので、文字数のカウントは2文字ズレる事になります。

 

 

文字のズレ数を補正するコード 

以上の様に、どういう原理で文字数カウントがズレるのかを調べた結果、一定の規則がある事が判りました。 そして現在、この法則に当てはまらない文字が見つかりません。 これまで判っている文字数カウントがズレるWin10絵文字の全ては、この規則をコード化する事でズレが補正できました。

 

この事により、特殊文字を指定するフィルターが不要になりました。 更に、新しいWin10絵文字が追加されても、このコードで文字ズレが補正できると思います。

 

 

「Both-WH」の仕様

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

 

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

 

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

 

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

 

● 想定外の理由により、ジャンプ後のマークタグ削除処理によって、周辺の記事の1~2文字を不本意に削除する事が有り得ます。 重要な文書でのトラブルは、自己責任とお考えください。

 

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

 

 

「Both-WH」ver. 2.0

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

 

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

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

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

 

 

〔 Both-WH 〕ver. 2.0

 

// ==UserScript==
// @name         Both-WH
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  「通常表示」「HTML表示」のカーソル位置を「Ctrl+F8」で往復する
// @author       Ameba Blog User
// @match        https://blog.ameba.jp/ucs/entry/srventry*
// @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;

            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; // 通常表示のスクロール位置を記録

                document.querySelector('button[data-mode="source"]').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>');

                            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); }} // アクティブ行を下方へ移動


                            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; }}

                            let index_sur=index_uni - sur; // サロゲートペア文字のズレ補正したindex

                            let vas=0; // 異体字の結合文字補正値
                            for(let k=0; k<index_sur; 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; }} // 異体字に使用される結合文字

                            let index_vas=index_sur - vas; // 異体字結合文字のズレ補正したindex

                            let tab=0; // タブ文字によるindexの補正値
                            if(activeline.getElementsByClassName('cm-tab')){
                                tab=4*(activeline.getElementsByClassName('cm-tab').length); }

                            let index_tab=index_vas - tab; // タブ文字によるズレ補正をしたindex

                            key_in(36); // brの改行で行頭の絵文字の後にカーソルが入る場合を修正する
                            for(let i=0; i<index_tab; 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(){
                document.querySelector('button[data-mode="wysiwyg"]').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');

                            if(iframe_doc.querySelector('#i')){
                                iframe_doc.querySelector('#i').remove(); }

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

})();

 

 

 

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

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

 

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

 

 

 

 

「Elements Palette」のコード変更について 

「Elements Palette」では、HTMLに書き込んだ「空タグ」が自動削除されてしまうため、タグ内の仮要素として「\u200B」と言う「ゼロ幅スペース」文字を利用していました。 一方、「Both-WH」では「」の文字(コードは「\u200D」)で1文字補正するコードを使っていますが、「HTML編集画面」は「\u200B」「\u200D」の区別をせず、同じ「」の文字を表示して扱います。

 

この結果、「Elements Palette」で記入した「h2」「h3」「  アイコン」の周辺で、「Both-WH」が文字数カウントのズレを生じる事が判りました。 この問題は、とても特殊ですが、他に利用できる「スペース文字」を探すと「\u200A」(HAIR SPACE / 非常に狭い空白)があり、こちらは「HTML編集画面」が「\u200D」と異なる扱いをするので、問題を生じません。

 

これを受け、「Elements Palette」のコードの「\u200B」を「\u200A」に改める事にしました。 次ページで「Elements Palette」と、関連する書き直しのツールとを纏めます。