(Gitのpre-commitでしっかり検査したい方はこちら)
ブックマークレット
javascript:!function(){const o=(o=>{const e=[];for(const o of[11,847,8203,8204,8205,8206,8207,8232,8233,8234,8235,8236,8237,8238,8289,8290,8291,65279])e.push(String.fromCharCode(o));return e})(),e=[],t=[],n=o=>{let e=o.parentNode,t=0;for(;e;)e=e.parentNode,t++;return t};for(const o of[...document.all]){const t=n(o);e[t]||(e[t]=[]),e[t].push(o)}e.reverse();for(const n of e)if(n&&n[Symbol.iterator])for(const e of n)for(const n of o)if(-1!==e.outerHTML.indexOf(n)){const o=document.createElement("div");let r=null;e.style.backgroundColor="tomato",e.style.outline="2px solid tomato",o.innerHTML=e.outerHTML.replace(new RegExp(n,"g"),""),r=o.firstElementChild,e.replaceWith(r),t.push([r,e])}if(t.length){console.group("★の位置に特殊文字を検出しました");for(const{0:e,1:n}of t){let{outerHTML:t}=n;e.replaceWith(n);for(const e of o)t=t.replace(new RegExp(e,"g"),"★");console.log(n,t)}return console.groupEnd(),void window.alert(`${t.length}個の要素に特殊空白文字が含まれているようです`)}window.alert("OK")}();
概要
Microsoft製品(だけではないけれども)を利用していると、原稿に特殊な目に見えない空白文字が紛れ込むことがあります。
代表格が文字コード8203
のゼロ幅スペースです。一見すると何も入力されていないように見えるますが、実際には目に見えない空白文字が入力されています。
これがURLの中に紛れていたりすると非常に厄介です。URLエンコードされてしっかり文字列として解釈されるので、正しくリンクが繋がりません。目視ではこの特殊文字がいないように見えるので、原因に気づくまでに時間がかかってしまうかもしれないですね。
console.log('abc'.length); // > 3
console.log('a'.charCodeAt(0)); // > 97
console.log(String.fromCharCode(97)); // > "a"
console.log(String.fromCharCode(97).length); // > 1
console.log(String.fromCharCode(8203)); // > ""
console.log(String.fromCharCode(8203).length); // > 1
​
はエディタ上で視認できませんでした。ただ、「?」と表記されることもあるようです。
※ VS Code、Sublime Text3では確認できませんでした。
空白文字は意外と種類がある
NeqZ8y5◆mmft4k9vgtL6さんの公開している5chってどうよ?内、「2. スペースは" "だけじゃない的な話」によると、50種類以上もスペースに似た何かがあるようです。
このブックマークレットは、当該記事で紹介されている空白文字をページ内から検出するものです。
アラートダイアログで結果を表示し、コンソールに詳細を出力します。
おまけで元コード
void function () {
// 検査する文字列
const strSet = (charCodeSet => {
const set = [];
for (const charCode of charCodeSet) {
set.push(String.fromCharCode(charCode));
}
return set;
})([
// 引用:http://anti.rosx.net/etc/memo/002_space.html
11, // 垂直タブ
847, // 結合書記素接合子
8203, // ゼロ幅スペース
8204, // ゼロ幅非接合子
8205, // ゼロ幅接合子
8206, // 記述方向制御(左から右へ)
8207, // 記述方向制御(右から左へ)
8232, // 行区切り文字
8233, // 段落区切り文字
8234, // LRE
8235, // RLE
8236, // PDF
8237, // LRO
8238, // RLO
8289, // 関数適用
8290, // 不可視の乗算記号
8291, // 不可視の区切り文字
65279 // ゼロ幅のノーブレークスペース
]);
const targets = []; // すべての要素が親要素の多い順(ネストの深い順)に格納される
const cache = []; // もともとのマークアップをキャッシュする
const getParentLength = node => {
let parent = node.parentNode;
let count = 0;
while (parent) {
parent = parent.parentNode;
count++;
}
return count;
};
for (const node of [...document.all]) {
const length = getParentLength(node);
if (!targets[length]) {
targets[length] = [];
}
targets[length].push(node);
}
// 親の数が多い順
targets.reverse();
for (const layer of targets) {
if (!layer || !layer[Symbol.iterator]) continue;
for (const node of layer) {
for (const str of strSet) {
if (node.outerHTML.indexOf(str) === -1) continue;
{
const dummyContainer = document.createElement('div');
let firstElementChild = null;
node.style.backgroundColor = 'tomato';
node.style.outline = '2px solid tomato';
dummyContainer.innerHTML = node.outerHTML.replace(
new RegExp(str, 'g'),
''
);
// 一次置き換え後の要素のアドレスを確保しておく
firstElementChild = dummyContainer.firstElementChild;
// 上位階層が多重検出されないように
// いったん特殊文字を削除したマークアップに置き換える
node.replaceWith(firstElementChild);
cache.push([
firstElementChild,
node
]);
}
}
}
}
if (cache.length) {
console.group('★の位置に特殊文字を検出しました');
for (const {0: dummy, 1: origin} of cache) {
let {outerHTML} = origin;
dummy.replaceWith(origin);
for (const str of strSet) {
outerHTML = outerHTML.replace(new RegExp(str, 'g'), '★');
}
console.log(origin, outerHTML);
}
console.groupEnd();
window.alert(`${cache.length}個の要素に特殊空白文字が含まれているようです`);
return;
}
window.alert('OK');
}();