書式整形をした「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枠」内は「半角空白」を複数並べる事が可能で、そのための枠です。 また、「 」を「半角空白」へ置換える書式整形は、文字数制限対策で必須です。 この「半角空白」が並ぶ状態は整形ツールを使わないと出現しない状況で、それゆえ今まで見落しに気付けませんでした。
以下は対策コードで、「サロゲートペア」「異体字」によるカーソル位置ズレを補正するコードの後に、これを追加しました。
このコードは、行の先頭に並ぶ「半角空白」と「 」の数をカウントして、カーソル位置のズレを補正します。 結果的に判った事ですが、これまで「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 ⭐」の最新バージョンへのリンクは、以下のページのリンクリストから探せます。