Help us understand the problem. What is going on with this article?

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

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

出典

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした