最近気が付いたのだけど、CSS Loader は読み込んだスタイルシートのプロパティ値に引用符で括られた文字列を発見すると1、その中にある U+0080 以上の文字(簡単にいうと半角英数記号以外の文字2)を問答無用で 16 進エスケープしてしまうようだ。たとえば、
p::after { font-family: "☺️" }
このようなスタイル指定があったとして、これを CSS Loader を通して処理するとこうなる。
p::after { content: "\263A\FE0F" }
まあ、場合によっては冗長だったり無駄な処理であるものの、まともなブラウザならちゃんと解釈してよしなに扱ってくれるので、エスケープされることそのものは特に問題ではない。ちょっと困るのは、どうも文字列が空白を含む場合の処理があまりよろしくないことだ。
p::after { font-family: " ☺️ " }
前後に空白を入れただけだが、これが CSS Loader を通すとこうなってしまう(以下、分かりやすいように文字列中の連続した空白を .
で示す)。
p::after { font-family: " \263A\FE0F..." }
エスケープされた文字に続けて、スペースが 3 個連続するのが分かるだろうか。ひとつは元からあったものだけど、あとのふたつは CSS Loader が追加してしまうものだ。
ブラウザにはどう解釈されるのか
CSS における文字参照のためのエスケープについては、CSS 2.1 の時代から現在に至るまでそこまで大幅な変更はない。CSS Snapshot 2017(日本語訳)から判断するに、いま参照するべきものは CSS Syntax Module Level 3 のセクション 2.1(日本語訳) あたりかと思う(のだけど、いまいち自信がない)。
まあ、おおざっぱにまとめると次のような感じだ。
- バックスラッシュに続けて最大 6 桁の 16 進数で表記する。
- エスケープ文字列に続けて 16 進数を構成する文字(
[0-9a-fA-F]
)が続く場合、その区切りを明確にするために、- 常に 6 桁の 16 進数で表記するか、
- 間にひとつの空白文字をはさむ。
- 上記の仕組みのため、エスケープ文字列直後に出現する空白文字ひとつは無視される。
- よってエスケープ文字列に本物のスペースを続ける場合は、スペースをふたつ置く必要がある。
以上をふまえた上で、あらためて先のエスケープされたスタイルがブラウザによってどう解釈されるか考えてみる。最初のスペースは無視されるが、残りふたつはともにスペースとして扱われる。つまり、こう解釈される。
p::after { font-family: " ☺️.." }
…元の指定と違っちゃってるね。涙 つまり、スペースひとつであれば問題ないところに、なぜかふたつも追加してしまうのがおかしい訳だ。なんでこんな風にしてるのかまでは調べてないので知らないけど。
まあでも content
プロパティの値程度ならそんなに問題にはならないだろう。要素に white-space: pre
などを指定してない限り、文書内に挿入された時点で連続した空白はひとつにまとめられるので。
しかし、文字列を値にとる CSS プロパティは他にもある。font-family
プロパティなどに指定するフォントファミリー名も、引用符で括ればりっぱな文字列値だ。そして CSS Loader はこっちもエスケープしてしまう。
日本語フォントファミリー名で起こる問題
たとえば、次のようなスタイル指定があるとしよう。実際にこんな指定をしている人はいないだろうと思うが、まあサンプルってことで。
html {
font-family: "ヒラギノ角ゴ ProN W3", "MS Pゴシック", serif;
}
同じように CSS Loader を通すと、こうなってしまう。
html {
font-family: "\30D2\30E9\30AE\30CE\89D2\30B4...ProN W3", "\FF2D\FF33...\FF30\30B4\30B7\30C3\30AF", serif;
}
そしてブラウザはどう解釈するかというと、こうである。
html {
font-family: "ヒラギノ角ゴ..ProN W3", "MS..Pゴシック", serif;
}
当然ながら、こんなスペースがふたつはさまった名前のフォントはどこの OS にもないはずなので、総称フォントファミリーである serif
が適用されて、ブラウザデフォルトのセリフ(明朝)体で表示されることになる。
デモを用意したので、PC から3いろいろなブラウザで確認してみてほしい。比較用に node-sass で普通にビルドしたものも置いておく。元から日本語フォントファミリー名を認識しない macOS 版の Google Chrome やちょっと古い Safari を除けば、違いは一目瞭然だろう。
もし「単にブラウザがエスケープされた文字列自体を認識しないからでは?」と思うのであれば、開発者ツールを開いて html 要素に当たっているフォントファミリー指定の中からひとつだけ空白を削除してみればいい。問題なくヒラギノやMS Pゴシックで表示されるはずだ。
実際のところ、問題は起きなさそう
…と、ここまで書いておいてなんだけど、これが実際のところ問題になることがあるかというと、ほとんどないんじゃないかと思う。なぜなら「日本語名でしかフォント指定をしない」というウェブ制作者はほとんどいないだろうからだ。おそらくみんな英語名のみで指定してるか、少なくとも日本語名と英語名の両方を併記してることだろう。
そして、現在ある程度シェアのあるブラウザであれば、英語名で書かれた日本語フォントをうまく適用できないようなものはまずない4。ならば、エスケープされた日本語名がちょっと壊れてて無視されたところで、英語名の方をブラウザがちゃんと認識してフォールバックしてくれるだけのことだ。
なので、別に気にしなくていいんじゃないかと思う。だいたい今までもこの問題はあったはずなのに過去に話題になってた様子がないのは、別にこれが原因で致命的な問題が特に起きてなかったからじゃないの? ならええやん。
そうはいってもなんとかしたい人のために
長々と書いた割にこんな結論ではさすがに気が引けるので、いちおう fix する方法も書いておくことにする。
font-family-unescape-loader を使う
今回の件を調べるにあたって、とりあえず日本語で書かれた情報が見つからなかったので、リポジトリの issue を探したら昨年 8 月にすでに報告済みであった。ひととおり目を通すと、最後に次のようなものが置いてあった。
エスケープされたフォントファミリー名をとりあえず元に戻すだけのものだが、とりあえずこれを入れて CSS Loader のあとに適用されるように設定を書けばよかろう。ただし、インストールするには GitHub リポジトリを直接指定する必要がある。あと content
プロパティの文字列値などは対象外なので注意。
$ npm install --save-dev swcho/font-family-unescape-loader
そもそも引用符で括らない
エスケープされるのは文字列値、つまり引用符で括られた範囲のみであることを思い出そう。逆にいえば引用符で括らなければエスケープされないということである。なので、こうしてしまう。
html {
font-family: ヒラギノ角ゴ ProN W3, MS Pゴシック, serif;
}
「大丈夫なの?」と思うかもしれないが、これが問題にならないことは CSSWring v4.2.2 - ウェブログ - Hail2u.net で詳しく触れられているのでご一読のこと。実際にブラウザで試してみれば、大丈夫なことも分かると思う。元から日本語フォントファミリー名を認識しない macOS 版の(ry
とはいえ、フォント名によっては大丈夫でないこともあるし、それをいちいち判断するのも面倒だろうし、「とりあえず引用符で括っておく」というのが安全であるのは間違いないので、積極的には勧めない。
そもそも webpack で CSS を扱わない
問題のあるものは使わないのがいちばんだよね(おい
ていうかそれ以前に
そもそも日本語でフォントファミリー名を書かなければなんの問題もない。英語名だけ残してさくっと削除しよう。日本語のフォント名しか理解しない古いブラウザがあるって? えーと、そのブラウザでの閲覧ならたぶん、フォント以前のところでいろいろと表示上の問題が起きてそうな気がするんだけど、どうか。
-
値でないもの、たとえば属性セレクタである
p[title="こんにちは"]
の「こんにちは」とかはそのまま。 ↩ -
この例えは正確ではないかもしれないとは自分でも認識しているので、マサカリのご投入はお控えいただけますようよろしくお願い申し上げます。涙 ↩
-
モバイルでは確認不足なので、違いがないものもあるかもしれない。いちおう手元の iPhone SE ではチェックしたが、どっちもヒラギノで表示されてるように見えた。ただ Xcode の Simulator では両方ゴシック体ではあるものの微妙に違いが確認できたりと、どうもよく分からない。 ↩
-
だろうと思うのだけど、もしあるのなら教えてください。 ↩