HTMLアンエスケープで数値参照128が€にならない理由とセミコロン無し参照
HTML の文字参照を元の文字に戻す処理(アンエスケープ)を正規表現で書くと、& や <、' のような普通の入力ではまず問題が出ない。ずれるのは、ブラウザが「壊れた参照」をどう読むかが仕様で細かく決まっている部分である。
たとえば € は、Unicode の U+0080(C1 制御文字)ではなく、ブラウザでは €(U+20AC)として表示される。� は NUL ではなく U+FFFD(置換文字)になり、© 2026 はセミコロンが無くても © 2026 になる。
この記事では、ハシトシステムの HTMLエスケープ/アンエスケープ の変換関数を、WHATWG HTML の規則を実装した Node.js のライブラリと同じ入力で比べ、どこが違うかを確かめる。最後に、数値文字参照を仕様どおりに戻す関数を作り、全コードポイントで答え合わせをする。
検証環境は Windows 11、Node.js v24.11.1、entities 7.0.0、parse5 8.0.0 である。entities は HTML の文字参照を扱うライブラリで、parse5 は WHATWG HTML のパーサーを JavaScript で実装したものである。
仕様で決まっていること
WHATWG HTML の 13.2.5 節(トークン化)に、文字参照の読み方が書かれている。数値文字参照は numeric character reference end state、名前付き文字参照は named character reference state で扱う。要点は次のとおりである。
| 入力 | ブラウザが返す文字 | 決まり |
|---|---|---|
� |
U+FFFD | 0 はパースエラーで U+FFFD |
� 以上 |
U+FFFD | Unicode の範囲外はパースエラーで U+FFFD |
�〜� |
U+FFFD | サロゲートはパースエラーで U+FFFD |
€〜Ÿ |
表の文字 | 27個は表で置き換える(0x80 は U+20AC、0x9F は U+0178) |
 など表に無い5個 |
そのまま | 0x81 0x8D 0x8F 0x90 0x9D は表に無い |
© 2026 |
© 2026 |
一部の名前はセミコロン無しでも読む |
属性値の ?a=1©=2 |
そのまま | 属性値の中で、次が = か英数字なら読まない |
0x80〜0x9F の表は、WHATWG Encoding Standard の windows-1252 のインデックスと突き合わせると、27個すべてが同じ文字だった。表に無い5個は、windows-1252 のインデックスでも同じ番号の制御文字になっている。Windows-1252 の番号のまま数値文字参照を書いたページでも、意図した文字が出るようにする規則である。
名前付き文字参照の一覧は仕様の entities.json にある。2026-11-13 に取得して数えると2231個あり、そのうち106個がセミコロン無しの形(& © < など)だった。名前は大文字小文字を区別する。& と " は一覧にあるが、&Amp; は無い。
属性値の規則は、URL のクエリで ©=2 のような書き方が昔から多かったためにある。仕様は理由を「for historical reasons」としている。本文中の ©=2 は ©=2 になるが、href の中では ©=2 のまま残る。
ツールの変換関数と比べる
HTMLエスケープ/アンエスケープ は、入力したテキストを文字実体参照に変換するか、その逆に戻すツールである。2026-11-13 に本番の HTML から unescapeHtml を取り出し、entities 7.0.0 の decodeHTML、parse5 8.0.0 で <p> の中身として読んだ結果と並べた。
| 入力 | ツール | entities / parse5(本文) |
|---|---|---|
€ |
U+0080 | U+20AC(€) |
Ÿ |
U+009F | U+0178(Ÿ) |
� |
U+0000 | U+FFFD |
� |
U+D800(単独のサロゲート) | U+FFFD |
� |
� のまま |
U+FFFD |
© |
© のまま |
© |
… |
… のまま |
… |
&Amp; |
& |
&Amp; のまま |
© 2026 |
© 2026 のまま |
© 2026 |
&lt; |
< |
< |
ツールの関数は、名前付き参照を amp lt gt quot apos nbsp の6個だけ持っている。ほかの名前は残し、名前は小文字にしてから引く。数値参照は String.fromCodePoint にそのまま渡している。違いはこの3点から出ている。
- C1 の表が無い。
€は U+0080 になる。U+0080 は表示する文字を持たない制御文字なので、€ のつもりで書かれた文字が出てこない。 - U+FFFD にする規則が無い。
�は NUL、�は単独のサロゲートになる。単独のサロゲートは UTF-8 にできないので、TextEncoderを通すと U+FFFD になり、encodeURIComponentに渡すとURIErrorになる。範囲外の値は参照のまま残る。 - 大文字小文字を区別しない。
&Amp;を&にする。ブラウザでは&Amp;は文字のまま表示される。
&lt; を < にするのは正しい。正規表現の replace は1回しか走らないので、& を戻した結果の < をもう一度 < にすることはない。2回かけると < になり、エスケープした意味が無くなる。
エスケープ側の escapeHtml は、& を最初に置き換えてから < > " ' を置き換える。順番が正しいので、< を入れると &lt; になる。エスケープ側には問題は見つからなかった。
アンエスケープ側の C1 の表と U+FFFD の扱い、大文字小文字の区別は、修正対象として記録した。入力したテキストはブラウザ内で処理され、サーバーには送られない。
どこまで自作するか
数値文字参照だけなら、規則は上の表で全部なので自作できる。名前付き参照は2231個あり、セミコロン無しの形では「一覧にある最長の名前」を選ぶ規則も加わる。本文の ¬it; は ¬ が一覧にあるので ¬it; になり、属性値の中ではそのまま残る。ここまで正規表現で再現するのは割に合わない。
Node.js では entities の decodeHTML(本文用)と decodeHTMLAttribute(属性値用)を使い分けると、parse5 で読んだ結果と一致した。
| 入力 | decodeHTML |
decodeHTMLAttribute |
|---|---|---|
?a=1©=2 |
?a=1©=2 |
?a=1©=2 |
?a=1©=2 |
?a=1©=2 |
?a=1©=2 |
¬it; |
¬it; |
¬it; |
属性値の中でも、セミコロンがあれば読む。©=2 は ©=2 になる。URL を HTML に埋めるときに & を & にしておけば、どちらの読み方でも元の URL に戻る。
JSON の文字列に入った \uD83D のような単独のサロゲートの扱いは サロゲートペアのエスケープの記事 に、JSON 側のエスケープは JSON 文字列のエスケープの記事 に書いた。
実際に試す
前提: Node.js(確認は v24.11.1)。1つ目のスクリプトは外部パッケージ不要である。
数値文字参照を、仕様の numeric character reference end state と同じ規則で文字に戻す。
1 | |
実行結果は次のとおりである。
1 | |
 は表に無いので U+0081 のまま返る。© 2026 はセミコロンが無いが、数字が終わった位置で参照が終わり、© と 2026 になる。
この関数が entities と同じ結果を返すかを、0 から 0x110001 までの全コードポイントについて、10進と16進の両方で確かめる。空のディレクトリで npm i entities@7.0.0 を実行し、上の numref.mjs を同じ場所に置いてから次を実行する。
1 | |
1 | |
222万8231通りで差は0だった。名前付き参照も扱う必要があるなら、自作の表を増やすより entities の decodeHTML と decodeHTMLAttribute を使い、本文と属性値で使い分ける。
まとめ
€〜Ÿのうち27個は、ブラウザでは Windows-1252 の文字に置き換わる。€は U+0080 ではなく€になる。�、サロゲート(�〜�)、�以上は U+FFFD になる。String.fromCodePointにそのまま渡すと NUL や単独のサロゲートができる。- 名前付き参照は2231個あり、大文字小文字を区別する。106個はセミコロン無しでも読まれる。
- 属性値の中では、セミコロンが無く次が
=か英数字の参照は読まない。本文用と属性値用でデコードを分ける。 - アンエスケープは1回だけかける。
&lt;は<に戻るのが正しい。
結果は 2026-11-13 時点の上記の環境とバージョンで得たものである。
参考
- WHATWG HTML Living Standard — 13.2.5 Tokenization(Character reference state、Named character reference state、Numeric character reference end state)、13.5 Named character references
- WHATWG HTML —
entities.json(名前付き文字参照の一覧) - entities 7.0.0(
decodeHTML/decodeHTMLAttribute)、parse5 8.0.0 のソースと実行結果 - WHATWG Encoding Standard — windows-1252 のインデックス
このバージョンはその範囲に入るのか
依存の話でいちばん間違えやすいのは ^1.2.3 や ~1.2.3 がどこまでを許すかです(^0.2.3 のようにメジャーが 0 のときは規則が変わります)。範囲とバージョンを貼ると、下限・上限に展開したうえで一致・不一致とその理由を表示するsemver 範囲判定ツールを置いています。ブラウザの中だけで動き、入力はどこにも送信しません。