HTMLに&#8203;などの特殊空白文字が含まれていないか検品する

（[Gitのpre-commitでしっかり検査したい方はこちら](https://qiita.com/soten_bluesky/items/f4d9a8871a42ca94ff72)）

## ブックマークレット

```javascript
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エンコードされてしっかり文字列として解釈されるので、正しくリンクが繋がりません。目視ではこの特殊文字がいないように見えるので、原因に気づくまでに時間がかかってしまうかもしれないですね。

```javascript
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
```

`&#8203;`はエディタ上で視認できませんでした。ただ、「?」と表記されることもあるようです。
※ VS Code、Sublime Text3では確認できませんでした。

## 空白文字は意外と種類がある

NeqZ8y5◆mmft4k9vgtL6さんの公開している5chってどうよ?内、「[2. スペースは" "だけじゃない的な話](http://anti.rosx.net/etc/memo/002_space.html)」によると、50種類以上もスペースに似た何かがあるようです。

このブックマークレットは、当該記事で紹介されている空白文字をページ内から検出するものです。

アラートダイアログで結果を表示し、コンソールに詳細を出力します。


## おまけで元コード

```javascript
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');
}();
```

## 出典

- [2. スペースは" "だけじゃない的な話](http://anti.rosx.net/etc/memo/002_space.html)