<br>タグって簡潔で良いのだが
改行は<br>タグと思っていたが、ブラウザ上のエディタは <p>タグや <div>タグで段落を作り、それを改行とするタイプが多い様です。 アメブロのエディタがそうで、普通に「Enter」を押すと<p> </p> というコードが書き込まれます。「Shift + Enter」で<br>タグが書き込めますが、普通は<p> </p>だと言っている様に見えます。
どちらの改行でも見た目が同じなら、気にしないのが普通の人ですが、私はHTMLが均質でないのが気になります。 標準の<p> </p>改行と、<br>改行が入れ混じっているのは、どうも落ち着かない。
そんな事で、記事中の<br>タグ(コード表記は除外)をチェックするツールを作って、これまでチマチマと手作業で標準の改行に修正していたのですが、今回、チェックツールを自動的に置換えるツールに発展させました。
ブログエディタの自動整形機能が問題を複雑にする
「BR Checker」を最初に手掛けたのは今年の 6月頃 で、想定外の記事破損を避けるために、自動置換え機能を作りませんでした。 しかしここ数ヵ月、実際に手作業で修正をして来て、自動置換えが可能と判断しました。
ただ、自動置換えの際に問題になるのが、エディタの自動整形機能です。「通常表示」で<br>タグを置換えて改行を整えても、「HTML表示」に行って戻ると余計な改行が増えていたりします。 これは予測できず、結局は「HTML表示」「通常表示」を往復して、そのつど再修正をしていました。
<br>タグに関係する自動整形機能
以下はサンプルの3行の「通常表示」です。
これを「HTML表示」で見たところ。
●<br>タグを2行目の中間に入れます。
「通常表示」では、タグの所でちゃんと改行されて編集意図の通りです。
「HTML表示」に戻ると、<br>タグの改行がこの表示にも反映しています。 表示が変わったのですが、HTMLの内容は最初と同じで問題はありません。
●<br>タグを2行目の末尾に入れます。
「通常表示」に行くと、どういうわけか何も変わっていません。
「HTML表示」に戻ると、記入した<br>タグが勝手に削除されています。
こういうのがHTML自動整形機能の仕業です。 編集意図を覆す様な事が起こり、修正の修正が必要になります。
置換え自動化の策
<br>の配置には色々な場合があり、上記の様な「pの最後にある場合」以外にも例外処理があるかもしれません。 処理仕様が不明なので、最終的な結果で場合分けをして対処し、自動化をする事にしました。
手法はまず、<br>タグをスクリプトで一旦<p> </p>に置換えます。 これは「通常表示」からの書き換えで、スクリプトで「HTML表示」「通常表示」と移動して、意図的に「自動整形機能」を働かせます。 その上で、最初の見た目と同じ状態になる様に、再びクリプトで改行の数を調整するという方法です。
この方法でテストすると、元の<br>が2個の<p> </p>に増える場合と、1個の<p> </p>となる場合があり、それぞれ1個ずつ減らすと、最初の表示と同じになることが判って来ました。 これは自動整形機能の整形結果のパターンで、全の場合でその通りになるかは判りません。 もし例外があれば、その条件を追加して自動化すれば良いと考えてます。
処理部の肝心な部分
以下は、<br>タグを<p> </p>に書換え、「HTML表示」→「通常表示」と画面を移動して、自動整形機能で増えた<p> </p>を減らすというコードです。 <br>タグを置換えた<p> </p>には「brs」という class名を付け、これに注目して数を減らす処理をし、最後にこのclass名を削って、全くプレーンなHTMLに戻しています。
中央で「HTML表示」→「通常表示」の移動に「find_mirror()」「find_iframe()」という、目的の要素がDOM生成されるまで、10msec 間隔で何度も実行される待機関数を使っています。 これは ネット記事 から教わった方法で、とても好結果です。
これによって、「HTML表示」がされたら直ぐボタンを押し、「通常表示」に戻ったら直ぐに次の「second_rw()」の処理を実行できる様になりました。
また、「second_rw()」の開始に「400msec」というタイミングを開けていますが、これが無いと「second_rw()」の実行が時々飛ばされます。 もう一度全体を繰り返すと必ず実行され、ブラウザによっても差があり微妙です。 記事のデータを増やしたストレステストでは、400msec程度の余裕があると安定すると踏んでいます。 環境によってもっと増やした方が良いかも知れません。
「sign()」は、処理結果のパネル表示と<br>の位置を表示する関数で、この場合の動作は結果の表示だけです。 これは400msec以上なら適当なタイミングで良く、むしろ2次処理の動作が判る様に、遅めにしています。
<br>タグの可視化の方法で改善した点
チェッカーは、削除処理を自動化したとは言え、ユーザーが最初の<br>タグの位置を確認でき、自動処理後に問題が無いか確かめられる様にしています。
処理対象の<br>タグの可視化ですが、<br>タグの手前にマーク表示用の<i>タグを記入し、幅を「0」として本来の文字列が移動しない様にしていました。 以下は実際に記入していた<i>タグです。
これは、インラインのスタイル指定で長くなっています。 最終的に削除するタグですが、
▪チェッカーの使用時にHTML表示を調べる場合があり、タグが長くて本来のコードを判り難くする。
▪もしこのタグを消せなかった場合の事をカバーしていない。
で、この<i>タグを、スクリプトで<style>タグを書き込んでCSS修飾する形に改めました。 文章中に書き込む<i>タグは class名だけとなり、またタグ削除に失敗しても記事に影響がでない「空タグ」とし、クラス名も短く改めました。
このスクリプトを作り始めた頃は、<style>タグ記入によるCSS修飾テクニックはなかったので、少し進歩しました。
このマーク用<i>タグのCSSは以下です。
操作方法と表示
下は、「Ctrl + F9」のショートカットキーで表示される、ツールのパネルです。
このツールは「To Space」というコード表示「pre枠」の整形ツールと一緒に起動します。 両方とも記事の仕上げに必要で纏めましたが、別々に操作出来ます。
「通常表示」画面で「Ctrl + F9」を押すと、記事中の<br>数を「count」に表示し、記事中の <br>の位置を「▼」マークで知らせます。
もう一度「Ctrl + F9」を押すと、ツールはOFFになり、全てのマークが消えます。
「自動置換え」の実行
「自動置換え」は「Ctrl + F10」のショートカットキーで実行されます。 一瞬だけ「HTML表示」となり「通常表示」に戻ります。 置換えの結果で「count」が「0」となるはずですが、元に<br>があった場所の行間隔が妥当かを確認します。
「shell」は、このツールが2段階で処理する事に関係します。 最初の処理で、全ての<br>を<p> </p>に書換え、2段階目の処理でその数を調整します。 処理が最後まで正常に行われなかった場合、記事中に2段階目の<p> </p>が残ります。 その数をカウントして「shell」で示しますが、正常終了なら「0」です。(shellは抜け殻の意味)
「shell」が残るのは、2段階目の「second_rw()」が飛ばされた結果です。 現在は「second_rw()」の実行タイミングを遅らせる以上の良策がなく、これは不完全と感じています。 ただし「shell」が残った場合は、「Ctrl + F10」で置換え処理を再実行すると、たいてい正常終了させる事が出来ます。
なお念のために、「shell」が残っていると、記事保存時にダイアログを表示し、編集終了ができない様にしています。 2段階目の<p> </p>は「▼」の表示で場所が判る様にしているので、手作業で「▼」を削除し改行数の調整ができます。
「自動置換え」のみの実行
「Ctrl + F9」を押すと、「BR Checker」「To Space」の両方が起動します。その状態で「Ctrl + F10」を押すと「自動置換え」が実行されます。
一方、何も起動していない状態で「Ctrl + F10」を押した場合は、「自動置換え」のみが実行されます。 処理の結果が上部のパネルに表示されますが、この場合は「To Space」が起動していないので、下の様に「BR Checker」のみの表示です。
同時に起動するツール「To Space」について
To Spaceの機能は以下のページに説明があります。「pre枠」を作っていない記事では全く働かないので、気にする必要はありません。
「BR Checker + To Space (nbsp)」ver 1.3
以下のコードは Chrome版 / Firefox版の「Tampermonkey」で動作を確認しています。 コード全体を「Tampermonkey」にコピー&ペーストをする事で、このツールを利用出来ます。 Edgeでは、処理でスクロール位置が記事先頭に移動しますが、自動置換え処理は正常に動作します。
〔コピー方法〕 軽量シンプルなツール「PreBox Button 」を使うと
コード枠内を「Ctrl+左Click」➔「Copy code 」を「左Click」
の操作で、掲載コードのコピーが可能になります。
〔 BR Checker + To Space (nbsp) 〕ver. 1.3
// ==UserScript== // @name BR Checker + To Space (nbsp) // @namespace http://tampermonkey.net/ // @version 1.3 // @description Blogの書式整形ツール 統合版 // @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 marked1=0; // BR Checker 起動・非起動の指標 let marked2=0; // To Space (nbsp) 起動・非起動の指標 let target; let editor_iframe; let iframe_doc; let iframe_body; let pre_box; // PRE枠のコンテナブロック let css=[ '.brm { position: absolute }', 'i.brm:before { content: "▼"; color: red; margin-left: -2px; font-style: normal }', 'p.brs:before { content: "▼"; color: #008fff; margin-left: -2px; font-style: normal }', '.disp_line { display: inline-block; margin: 0 0 4px 8px; padding: 4px 15px 1px;', 'font-size: 16px; color: #fff; background: red }'].join(' '); target=document.getElementById('cke_1_contents'); // 監視 target let monitor=new MutationObserver(catch_key); monitor.observe(target, {childList: true}); // ショートカット待受け開始 catch_key(); function catch_key(){ let send; editor_iframe=document.querySelector('.cke_wysiwyg_frame'); if(editor_iframe){ // WYSIWYG表示が実行条件 iframe_doc=editor_iframe.contentWindow.document; if(marked1==1){ sign(); } if(marked2==1){ active(); } document.addEventListener("keydown", check_key); iframe_doc.addEventListener("keydown", check_key); function check_key(event){ let gate=-1; if(event.ctrlKey==true){ if(event.keyCode==120){ event.preventDefault(); send=120; gate=1; }} // ショートカット「Ctrl+F9」 if(event.ctrlKey==true){ if(event.keyCode==121){ event.preventDefault(); send=121; gate=1; }} // ショートカット「Ctrl+F10」 if(editor_iframe){ if(gate==1){ event.stopImmediatePropagation(); event.preventDefault(); set_task(send); }}} brfore_end(); }} // catch_key function set_task(send){ if(send==120){ //「Ctrl+F9」 BR Checker / To Space (nbsp) スイッチ if(marked1==0){ marked1=1; marked2=1; sign(); active(); } // BR Checker / To Space (nbsp) をON else if(marked1==1){ marked1=0; marked2=0; sign_clear(); disable(); }} // BR Checker / To Space (nbsp) をOFF if(send==121){ //「Ctrl+F10」BR Rewrite スイッチ marked1=1; sign_clear(); rewrite(); } } // set_task // ********** BR Rewrite Functions ************** function rewrite(){ let br_tag; let br_rewrite; let o_tag; let native_line; first_rw(go_back); function first_rw(){ let wysiwyg=iframe_doc.querySelector('html'); native_line=wysiwyg.scrollTop; // 通常表示のスクロール位置を記録 br_tag=iframe_doc.querySelectorAll('br'); for(let i=0; i < br_tag.length; i++){ if(br_tag[i].parentNode.tagName=="P" && br_tag[i].parentNode.childNodes.length !=1){ br_rewrite=iframe_doc.createElement("p"); br_rewrite.appendChild(iframe_doc.createTextNode("")); br_rewrite.setAttribute("class", "brs"); br_tag[i].parentNode.replaceChild(br_rewrite, br_tag[i]); }} go_back(); } function go_back(){ document.querySelector('button[data-mode="source"]').click(); // HTML表示に移動 let interval0=setInterval(find_mirror, 10); function find_mirror(){ let CodeMirror=document.querySelector('.CodeMirror'); if(CodeMirror){ clearInterval(interval0); document.querySelector('button[data-mode="wysiwyg"]').click(); go_back2(); }}} function go_back2(){ let interval1=setInterval(find_iframe, 10); function find_iframe(){ let editor_iframe=document.querySelector('.cke_wysiwyg_frame'); if(editor_iframe){ iframe_doc=editor_iframe.contentWindow.document; if(iframe_doc){ clearInterval(interval1); setTimeout( function(){ second_rw(); }, 400); setTimeout( function(){ sign(); }, 800); }}}} function second_rw(){ o_tag=iframe_doc.querySelectorAll('.brs'); if(o_tag.length >0){ for(let i=0; i < o_tag.length; i++){ if(o_tag[i].nextElementSibling==o_tag[i+1] ){ o_tag[i].classList.remove('brs'); } // 2連 .brs 生成の場合は先頭側class名を削除 else{ o_tag[i].remove(); }}} // 単体の .brs の場合は削除(上の後方側も含む) let wysiwyg=iframe_doc.querySelector('html'); wysiwyg.scrollTop=native_line; }} // 記録された通常表示のスクロール位置に移動 // ********** BR Checker Functions ************** function sign(){ let br_tag; let br_mark; let i_tag; let o_tag; let c_br=0; let c_br_span; let c_brrw=0; let c_brrw_span; let disp; let style_inn=iframe_doc.createElement('style'); // iframe内の BRのデザインを指定 style_inn.setAttribute("id", "style_inn"); style_inn.insertAdjacentHTML('afterbegin', css); let html=iframe_doc.getElementsByTagName('html')[0]; if(!html.querySelector('#style_inn')){ html.appendChild(style_inn); } i_tag=iframe_doc.querySelectorAll('.brm'); if(i_tag.length >0){ for(let i=0; i < i_tag.length; i++){ i_tag[i].remove(); }} // BRマーク削除してリセット o_tag=iframe_doc.querySelectorAll('.brs'); // 処理時の brshell の処理残りをカウント確認 if(o_tag.length >0){ c_brrw=o_tag.length; } else{ c_brrw=0; } br_tag=iframe_doc.querySelectorAll('br'); for(let i=0; i < br_tag.length; i++){ if(br_tag[i].parentNode.tagName=="P" && br_tag[i].parentNode.childNodes.length !=1){ c_br +=1; br_mark=iframe_doc.createElement("i"); br_mark.appendChild(iframe_doc.createTextNode("")); // 空白文字 br_mark.setAttribute("class", "brm"); br_tag[i].parentNode.insertBefore(br_mark, br_tag[i]); } else{ continue; }} // BRマーク表示 disp=document.createElement("span"); disp.setAttribute("class", "disp_line"); disp.appendChild(document.createTextNode("▼ BR Checker count : ")); c_br_span=document.createElement("span"); c_br_span.setAttribute("style", "font-weight: bold"); c_br_span.appendChild(document.createTextNode(c_br)); disp.appendChild(c_br_span); disp.appendChild(document.createTextNode(" / shell:")); c_brrw_span=document.createElement("span"); c_brrw_span.setAttribute("style", "font-weight: bold"); c_brrw_span.appendChild(document.createTextNode(c_brrw)); disp.appendChild(c_brrw_span); if(marked2==1){ disp.appendChild(document.createTextNode(" ■ To Space (nbsp) ")); } monitor.disconnect(); // MutationObserverを BR Checker 起動表示に反応させない let style_ext=document.createElement('style'); // disp_line のデザインを指定 style_ext.setAttribute("id", "style_ext"); style_ext.insertAdjacentHTML('afterbegin', css); if(!target.querySelector('#style_ext')){ target.appendChild(style_ext); } if(target.querySelector('.disp_line')){ target.querySelector('.disp_line').remove(); target.insertBefore(disp, editor_iframe); } else{ target.insertBefore(disp, editor_iframe); } monitor.observe(target, {childList: true}); } // BR Checker 起動表示 function sign_clear(){ let i_tag; editor_iframe=document.querySelector('.cke_wysiwyg_frame'); iframe_doc=editor_iframe.contentWindow.document; if(target.querySelector('.disp_line')){ target.querySelector('.disp_line').remove(); } // BR Checker 起動表示を削除 i_tag=iframe_doc.querySelectorAll('.brm'); if(i_tag.length >0){ for(let i=0; i < i_tag.length; i++){ i_tag[i].remove(); }}} // BRマーク削除 // ********** To Space (nbsp) Functions ************** function active(){ let buffer; let nbsp; let a_count; iframe_doc=editor_iframe.contentWindow.document; if(iframe_doc){ //「通常表示」が動作条件 iframe_body=iframe_doc.querySelector('body.cke_editable'); pre_box=iframe_body.querySelectorAll('div'); buffer=Array(pre_box.length); for(let i=0; i<pre_box.length; i++){ select_pre_box(i); } function select_pre_box(i){ pre_box[i].oncontextmenu=function(){ if(pre_box[i].firstChild.tagName=='PRE'){ select_box(select_do); return false; function select_box(select_do){ boxshadow(); setTimeout( select_do, 100); } function boxshadow(){ pre_box[i].style.outline='2px solid #03a9f4'; } function select_do(){ start_select(i); }}}} function start_select(i){ let regex=new RegExp('\u00A0', 'g'); let nbsp=pre_box[i].innerText.match(regex); let pa_count=pre_box[i].getElementsByTagName("a"); if(buffer[i] !=null){ let ok=confirm(" ❎ 変換前に戻しますか?"); if(ok){ pre_box[i].firstChild.innerHTML=buffer[i]; buffer[i]=null; pre_box[i].style.outline='none'; }} else{ if(nbsp !=null || pa_count.length !=0){ let ok; if(nbsp==null && pa_count.length !=0){ ok=confirm(" ⏬ 「 」 0 個\n"+ " 「a要素」 " + pa_count.length + " 個をテキストに置換えます"); } else if(nbsp !=null && pa_count.length==0){ ok=confirm(" ⏬ 「 」 " + nbsp.length + " 個を半角空白に変換します\n"+ " 「a要素」 0 個"); } else{ ok=confirm(" ⏬ 「 」 " + nbsp.length + " 個を半角空白に変換します\n"+ " 「a要素」 " + pa_count.length + " 個をテキストに置換えます"); } if(ok){ to_space(i); } else{ pre_box[i].style.outline='none'; }} else{ ; }}} function to_space(i){ a_count=0; buffer[i]=pre_box[i].firstChild.innerHTML; // 選択pre枠のデータバックアップ let child_node=pre_box[i].firstChild.childNodes; for(let t=0; t<child_node.length; t++){ if(child_node[t].nodeType==3){ child_node[t].nodeValue=child_node[t].nodeValue.replace(/\u00A0/g , '\u0020'); } else if(child_node[t].nodeType==1 && child_node[t].tagName=="A"){ let inner_node=child_node[t].childNodes; if(inner_node.length==1 && inner_node[0].nodeType==3){ pre_box[i].firstChild.replaceChild(inner_node[0], child_node[t]); } // text を a要素と入替え else{ a_count +=1; }} // 子ノードが1個の textの条件に当てはまらない a要素 else{ let child_node2=child_node[t].childNodes; if(child_node2){ for(let t2=0; t2<child_node2.length; t2++){ if(child_node2[t2].nodeType==3){ child_node2[t2].nodeValue=child_node2[t2].nodeValue.replace(/\u00A0/g , '\u0020'); } else{ let child_node3=child_node2[t2].childNodes; if(child_node3){ for(let t3=0; t3<child_node3.length; t3++){ if(child_node3[t3].nodeType==3){ child_node3[t3].nodeValue=child_node3[t3].nodeValue.replace(/\u00A0/g , '\u0020'); }}}}}}}} let regex=new RegExp('\u00A0', 'g'); nbsp=pre_box[i].innerText.match(regex); if(nbsp !=null){ alert("❌ 変換できなかった「 」の数 : " + nbsp.length );} if(a_count !=0){ alert("❌ 置換えが出来ない「a要素」 : " + a_count); }}}} function disable(){ for(let i=0; i<pre_box.length; i++){ if(pre_box[i].firstChild.tagName=='PRE'){ pre_box[i].style.outline='none'; }} // 処理済枠の outline を削除 pre_box=[]; }// 動作の無効化 // ********** Before End Functions ************** function brfore_end(){ var submitButton=document.querySelectorAll('.js-submitButton'); if(editor_iframe !=null){ //「通常表示」編集画面が実行条件 submitButton[0].addEventListener("mousedown", all_clear, false); submitButton[1].addEventListener("mousedown", all_clear, false); submitButton[2].addEventListener("mousedown", all_clear, false); } function all_clear(){ let o_tag=iframe_doc.querySelectorAll('.brs'); // 処理時の brshell の処理残りをカウント確認 if(o_tag.length >0){ alert("⛔ BR削除処理が不完全です BR-Shell数:" + o_tag.length +"\n\n" + " BR削除「Ctrl + F10」 を再実行してください"); event.stopImmediatePropagation(); event.preventDefault(); } else{ let i_tag; editor_iframe=document.querySelector('.cke_wysiwyg_frame'); iframe_doc=editor_iframe.contentWindow.document; i_tag=iframe_doc.querySelectorAll('.brm'); if(i_tag.length >=1){ for(let i=0; i < i_tag.length; i++){ i_tag[i].remove(); }} // マーク削除 let pre_box; iframe_body=iframe_doc.querySelector('body.cke_editable'); pre_box=iframe_body.querySelectorAll('div'); for(let i=0; i<pre_box.length; i++){ if(pre_box[i].firstChild.tagName=='PRE'){ pre_box[i].style.outline='none'; }}}}}// 処理済枠の outline を削除 });
〔追記〕 2019.12.03
Firefox のキー入力取得コードが Chromeと共用になったので、一部更新しました。
〔追記〕 2019.12.19
「記事の編集・削除」のページでのスクリプトエラー抑止のために、「@exclude」を追加しました。
「BR Checker」「To Space (nbsp)」最新版について
旧いバージョンの JavaScriptツールは、アメーバのページ構成の変更で動作しない場合があり、導入する場合は最新バージョンをお勧めします。
●「BR Checker」は、後に「BR Checker+Outro Style ⭐」に統合されています。 その最新バージョンへのリンクは、以下のページのリンクリストから探せます。
●「To Space (nbsp)」は、後に「PreBox Tools ⭐」に統合されています。 その最新バージョンへのリンクは、以下のページのリンクリストから探せます。