PHPとJavaScriptでHTMLエンティティを扱う時のおさらい

  • 38
    Like
  • 6
    Comment
More than 1 year has passed since last update.

久々のおさらいシリーズ第5弾。

今回のテーマはHTMLエンティティについてまとめてみた次第。

HTMLエンティティの基本

HTMLを書いたことがある人なら知っていると思うが、HTMLエンティティとはブラウザがHTMLを表示する際に特別な意味を持つ記号などの文字を、特別な処理せずにそのまま出力させるために定義されている文字の代替コードのことで、日本語だと「文字実体参照」や「名前付き文字参照」と呼ばれている。例えば、半角空白を表す や、HTMLタグを表すための大なり小なり記号の<>などがエンティティとして有名だ。
実際には、静的なテキストとしてHTMLのタグ記述をHTML内に出力したい時などに、

<a href="//www.google.com">Link</a>

──とHTML内に記述すれば、ブラウザはその部分をタグとしてレンダリングせずに、

<a href="//www.google.com">Link</a>

──と素のタグ記述を出力してくれるのだ。

なお、「HTMLエンティティ」は通称というか俗称的なもので、正式な総称は「Character references (文字参照)」(rryuさんにご指摘いただきました)なのだが、広く一般的に「HTMLエンティティ」で括られてしまうことが多いため、当記事でもその通称で書いてます。

HTMLエンティティには、上記の&lt;&quot;のように名前付きエンティティ(名前付き文字参照)と、&#010;&#039などのような数値型エンティティ(10進数値文字参照と16進数値文字参照がある)の二種類に大別できる。名前付きエンティティは有名な記号以外はブラウザによって使用できるかどうかに差があり、数値型エンティティは基本的にどのブラウザでも共通で利用できるがHTML文書の文字コードに影響される。
一例として、主によく使われるエンティティと紛らわしい記号のリストを下記に掲載しておこう。

表示文字 エンティティ名 エンティティ数値 文字の説明
(タブ) - &#009; タブ
(改行) - &#010; 改行
&nbsp; &#160; 改行しない空白(no-break space)
- &#032; 半角空白
" &quot; &#034; ダブルクォート
& &amp; &#038; アンパサンド
' &apos; &#039; アポストロフィ(シングルクォート)
< &lt; &#060; 小なり(Less-than)
> &gt; &#062; 大なり(Greater-than)
[ - &#091; 開く角括弧
] - &#093; 閉じる角括弧
^ - &#094; キャレット
ˆ &circ; &#710; 曲アクセント(circumplex)
~ - &#126; チルダ
˜ &tilde &#732; アクセントのチルダ(tilde)
` - &#096; バッククォート
&ndash; &#8211; 半ダッシュ
&mdash; &#8212; ダッシュ
¥ &yen; &#165; 円(※バックスラッシュではない)
\ - &#092; バックスラッシュ
© &copy; &#169; コピーライト
® &reg; &#174; 登録商標
&trade; &#8482; トレードマーク
« &laquo; &#171; 左引用
» &raquo; &#187; 右引用
× &times; &#215; 乗算
÷ &divide; &#247; 除算

上記はHTMLエンティティのほんの一部だ。名前付きHTMLエンティティは他にもたくさんあるので、もっと詳しく調べたい時は下記のサイトなどを参照してみてほしい。

Character Entities

HTML特殊文字コード表

PHPにおけるHTMLエンティティの取り扱い

PHPにおいてHTMLエンティティを取り扱う場合、一般的に下記の関数(メソッド)を利用することになる。

  • htmlspecialchars()&,",',<,>をHTMLエンティティに変換する。
  • htmlentities() ─ 名前付きエンティティを持つ文字をHTMLエンティティに変換する。
  • mb_encode_numericentity() ─ すべての文字をHTML数値エンティティに変換する。
  • htmlspecialchars_decodehtmlspecialchars()で変換された文字を実体文字に変換する。
  • html_entity_decodehtmlentities()で変換された文字を実体文字に変換する。
  • mb_decode_numericentity() ─ HTML数値エンティティを実体文字に変換する。
  • mb_convert_encoding() ─ 文字エンコーディングを変換する。

HTMLエンティティへのエンコード

まず、HTMLエンティティへのエンコード関数について、それぞれの関数にどんな特徴があるか調べてみた。なお、文字コードはUTF-8である。

$entities = " <>&\"'ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽";
# "𡈽"はUnicodeの4バイト文字

var_dump( htmlspecialchars( $entities, ENT_QUOTES, 'UTF-8' ) );
var_dump( htmlentities( $entities, ENT_QUOTES, 'UTF-8' ) );
var_dump( mb_encode_numericentity( $entities, array( 0x0, 0xffff, 0, 0xffff ), 'UTF-8' ) );
var_dump( mb_encode_numericentity( $entities, array( 0x0, 0x10ffff, 0, 0xffffff ), 'UTF-8' ) );
var_dump( mb_convert_encoding( $entities, 'HTML-ENTITIES', 'UTF-8' ) );

上記の実行結果は下記のようになった。

string(102) " &lt;&gt;&amp;&quot;&#039;ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽"
string(198) " &lt;&gt;&amp;&quot;&#039;&circ;^&tilde;~&ndash;&mdash;&cent;&pound;&yen;&euro;&copy;&reg;&trade;&fnof;&OElig;&oelig;&bull;&hellip;&lsquo;&dagger;&permil;&alpha;&frasl;&larr;&spades;1Aaあア一𡈽"
string(234) "&#32;&#60;&#62;&#38;&#34;&#39;&#710;&#94;&#732;&#126;&#8211;&#8212;&#162;&#163;&#165;&#8364;&#169;&#174;&#8482;&#402;&#338;&#339;&#8226;&#8230;&#8216;&#8224;&#8240;&#945;&#8260;&#8592;&#9824;&#49;&#65;&#97;&#12354;&#65393;&#19968;𡈽"
string(239) "&#32;&#60;&#62;&#38;&#34;&#39;&#710;&#94;&#732;&#126;&#8211;&#8212;&#162;&#163;&#165;&#8364;&#169;&#174;&#8482;&#402;&#338;&#339;&#8226;&#8230;&#8216;&#8224;&#8240;&#945;&#8260;&#8592;&#9824;&#49;&#65;&#97;&#12354;&#65393;&#19968;&#135741;"
string(198) " <>&"'&circ;^&tilde;~&ndash;&mdash;&cent;&pound;&yen;&euro;&copy;&reg;&trade;&fnof;&OElig;&oelig;&bull;&hellip;&lsquo;&dagger;&permil;&alpha;&frasl;&larr;&spades;1Aa&#12354;&#65393;&#19968;&#135741;"

まず、htmlspecialchars()は第二引数にENT_QUOTESを指定してあげることで最大&,",',<,>の5つの文字をHTMLエンティティに変換してくれる。ENT_QUOTESなしで実行すると&,<,>の3文字しか変換されない。

次に、htmlentities()は名前付きエンティティを持っている文字のみをHTMLエンティティに変換する。だが、第二引数にENT_QUOTESを指定しないと",'は変換対象から除外されてしまうので注意が必要だ。

mb_encode_numericentity()は第二引数で指定した文字コード領域の配列に合致する文字のみをHTML数値エンティティに変換する。上記のコードではarray( 0x0, 0xffff, 0, 0xffff )array( 0x0, 0x10ffff, 0, 0x10ffff )の二つの例を紹介しているが、前者の領域指定ではUnicodeの4バイト文字のエンコードに対応できていないことがわかる。すべての文字を数値エンティティに変換するためには後者の指定が必要になる。

最後にmb_convert_encoding()は本来文字エンコーディングを変換するための関数だが、変換エンコーディングにHTML-ENTITIESを指定することでマルチバイト文字をHTML数値エンティティに、名前付きエンティティを持っている文字をHTMLエンティティに変換してくれる。ただ、&,",',<,>や一部の記号は変換されず、その法則性がよくわからないため使いづらい。

では、この4つの関数のパフォーマンスを計測してみよう。
下記のテストコードで、65533文字のUTF-8のランダム文字列に対して各エンコード処理を行い、それぞれの処理時間を計測してみることにする。

# UTF-8の文字でランダムな文字列を生成する
function rand_str( $max_length, $min_length=1, $utf8=true ) {
  static $utf8_chars = [];

  if ( $utf8 && ! $utf8_chars ) {
    for ( $i=1; $i<=65533; $i++ ) {
      $utf8_chars[] = mb_convert_encoding( "&#$i;", 'UTF-8', 'HTML-ENTITIES' );
    }

    $utf8_chars = preg_replace( '/[^\p{L}]/u', '', $utf8_chars );
    foreach ( $utf8_chars as $i => $char ) {
      if ( trim( $utf8_chars[$i] ) ) 
        $chars[] = $char;
    }

    $utf8_chars = $chars;
  }

  $chars = $utf8 ? $utf8_chars : str_split( 'abcdefghijklmnopqrstuvwxyz' );
  $num_chars = count( $chars );
  $string = '';

  $length = mt_rand( $min_length, $max_length );

  for ( $i=0; $i<$length; $i++ ) {
    $string .= $chars[mt_rand( 1, $num_chars ) - 1];
  }

  return $string;
}


# HTMLエンティティエンコード処理
function encode_test( $case, $test_data ) {
  $results = [];
  $proc_start = microtime( true );
  switch ( $case ) {
    case 'htmlspecialchars': 
      $res = htmlspecialchars( $test_data, ENT_QUOTES, 'UTF-8' );
      break;
    case 'htmlentities': 
      $res = htmlentities( $test_data, ENT_QUOTES, 'UTF-8' );
      break;
    case 'mb_encode_numericentity': 
      $res = mb_encode_numericentity( $test_data, array( 0x0, 0x10ffff, 0, 0xffffff ), 'UTF-8' );
      break;
    case 'mb_convert_encoding': 
      $res = mb_convert_encoding( $test_data, 'HTML-ENTITIES', 'UTF-8' );
      break;
  }
  $proc_time = microtime( true ) - $proc_start;
  $results[$case] = [ $proc_time . ' sec', $res ];
  var_dump( $results );
}

$test_data = rand_str( 65533, 65533, true );

foreach ( [ 'htmlspecialchars', 'htmlentities', 'mb_encode_numericentity', 'mb_convert_encoding' ] as $case ) {
  encode_test( $case, $test_data );
}

何回かやってみた結果はこのようになった。

関数名 平均処理時間
htmlspecialchars 0.001245 sec
htmlentities 0.001226 sec
mb_encode_numericentity 0.008830 sec
mb_convert_encoding 0.015991 sec

htmlspecialcharshtmlentitiesはほぼ同等のパフォーマンスで、mb_encode_numericentityはその約7倍、mb_convert_encodingでは約13倍と処理時間がかかる。しかし、さすがにどれもビルドインメソッドだけあって処理が速い。結論として、どれを使っても問題はなさそうだ。

──ということで、PHPで任意の文字列をHTMLエンティティに変換する際には、この4つの関数の特性を把握して必要に応じて最適な変換を行えば良い。ただ、データベースのクエリ文字列を安全にやり取りしたい場合などにhtmlspecialchars()htmlentities()だけのエンティティ化では不足することもあり得るので、どの文字までエンコード対象にするかをあらかじめ決めておき、その文字すべてをエンコードできる関数を選ぶのが良いのではないだろうか。

私的には、mb_encode_numericentity()にすべての文字コード領域を指定して利用するのがエンティティ化漏れを防ぐ一番の方法かとも思うのだが、すべての文字列がHTMLエンティティに置き換わることでデータとしてのバイト数が肥大化するデメリットはあるので、やはりケースバイケースではある。

HTMLエンティティのデコード

次にデコード関数の特徴を調べてみよう。
下記のようにすべての文字がHTMLエンティティ化された文字列を各デコード関数に食わせてみた。

$str = "&#32;&#60;&#62;&#38;&#34;&#39;&circ;&#94;&tilde;&#126;&ndash;&mdash;&cent;&pound;&yen;&euro;&copy;&reg;&trade;&fnof;&OElig;&oelig;&bull;&hellip;&lsquo;&dagger;&permil;&alpha;&frasl;&larr;&spades;&#49;&#65;&#97;&#12354;&#65393;&#19968;&#135741;";

var_dump( htmlspecialchars_decode( $str, ENT_QUOTES ) );
var_dump( html_entity_decode( $str, ENT_QUOTES ) );
var_dump( mb_decode_numericentity( $str, array( 0x0, 0x10ffff, 0, 0xffffff ), 'UTF-8' ) );
var_dump( mb_convert_encoding( $str, 'UTF-8', 'HTML-ENTITIES' ) );

実行結果は次のようになった。

string(223) "&#32;<>&"'&circ;&#94;&tilde;&#126;&ndash;&mdash;&cent;&pound;&yen;&euro;&copy;&reg;&trade;&fnof;&OElig;&oelig;&bull;&hellip;&lsquo;&dagger;&permil;&alpha;&frasl;&larr;&spades;&#49;&#65;&#97;&#12354;&#65393;&#19968;&#135741;"
string(82) " <>&"'ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽"
string(178) " <>&"'&circ;^&tilde;~&ndash;&mdash;&cent;&pound;&yen;&euro;&copy;&reg;&trade;&fnof;&OElig;&oelig;&bull;&hellip;&lsquo;&dagger;&permil;&alpha;&frasl;&larr;&spades;1Aaあア一𡈽"
string(82) " <>&"'ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽"

htmlspecialchars_decode()&lt;,&gt;,&amp;,&quot;,&#039;(もしくは&apos;)しか変換しないので、それ以外のHTMLエンティティはそのままである。また、第二引数にENT_QUOTESを指定しないと&quot;,&#039;も変換されない。

html_entity_decode()はエンコード時に対応していなかった名前付きエンティティや数値エンティティもすべて実体に変換してくれる。ただし、第二引数にENT_QUOTESを指定しないと&quot;,&#039;だけが変換されないので注意が必要だ。

mb_decode_numericentity()はエンコード時と同じように第二引数で指定した文字コード領域に応じて変換を行うが、対応しているのは数値エンティティだけだ。名前付きエンティティはこの関数だけでは変換できない。

mb_convert_encoding()による変換は名前付きエンティティや数値エンティティを含めすべてを実体に変換してくれる。

また、デコードのパフォーマンス結果は下記のとおりだ。

関数名 平均処理時間
htmlspecialchars_decode 0.004583 sec
html_entity_decode 0.004560 sec
mb_decode_numericentity 0.004901 sec
mb_convert_encoding 0.006132 sec

デコードについてはどれも同じようなパフォーマンスである。htmlspecialchars_decodehtml_entity_decodeについてエンコードよりコストがかかるものの、気にするような値ではない。

最終的に、デコード処理についてはmb_convert_encoding()か第二引数にENT_QUOTESを指定したhtml_entity_decode()の関数を使っておくのが無難だろう。

JavaScriptでのHTMLエンティティの取り扱い

結構やっかいなのが、JavaScriptでHTMLエンティティを取り扱う場合だ。そもそもJavaScriptはHTMLエンティティのエンコードやデコードを行う関数を持っていないため、JavaScriptでHTMLエンティティを取り扱おうとすると、独自で変換処理を実装してあげる必要がある。そして、その場合、どの程度の文字までエンティティ化するかによってコーディングコストが大きく変わってくる。

そもそも処理設計的に、サーバサイドへ文字列を受け渡す際にHTMLエンティティへのエンコードを行うのであれば、フロントエンドのJavaScriptではなく、受け取ったサーバサイド側のスクリプトで行った方が効率的である。エンティティ化された文字列はサイズが肥大化するため、その分サーバ側とやり取りする通信量が増えてしまうというデメリットが大きいのだ。

──とはいえ、ここではJavaScriptとPHP間とで相互にHTMLエンティティを取り扱えるようなエンコード/デコード処理を考えてみよう。

htmlspecialchars系互換のJavaScript版HTMLエンティティ処理

JavaScriptでHTMLエンティティのエンコードを行い、PHPのhtmlspecialchars_decodeでデコードする。もしくは、PHPのhtmlspecialcharsでエンコードされた文字をJavaScript側でデコードする場合のJavaScript側の実装だ。

htmlspecialcharshtmlspecialchars_decodeでは最大でも&,",',<,>の5つの文字しか変換対象にならないので、処理はある程度簡単である。

JavaScriptで実装

function htmlEntities( text, proc ) {
  var entities = [
    ['amp', '&'],
    ['apos', '\''],
    ['lt', '<'],
    ['gt', '>'],
  ];

  for ( var i=0, max=entities.length; i<max; i++ ) {
    if ( 'encode' === proc ) {
      text = text.replace(new RegExp( entities[i][1], 'g' ), "&"+entities[i][0]+';' ).replace( '"', '&quot;' );
    } else {
      text = text.replace( '&quot;', '"' ).replace(new RegExp( '&'+entities[i][0]+';', 'g' ), entities[i][1] );
    }
  }
  return text;
}

var str = " <>&\"'ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽";
console.log( htmlEntities( str, 'encode' ) );
// &lt;&gt;&amp;&quot;&apos;ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽

var entity = '&lt;&gt;&amp;&quot;&apos;ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽';
console.log( htmlEntities( entity, 'decode' ) );
// <>&"'ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽

ダブルクォート"だけentitiesテーブルに定義してもうまく変換されないため、それだけ別処理で変換している。

jQueryで実装

function htmlEntities( str, proc ) {
  if ( 'encode' === proc ) {
    str = $('<div/>').text( str ).html();
    return str.replace( '\'', '&apos;' ).replace( '"', '&quot;' );
  } else {
    return $("<div/>").html( str ).text();
  }
}

var str = " <>&\"'ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽";
console.log( htmlEntities( str, 'encode' ) );
// &lt;&gt;&amp;&quot;&apos;ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽

var entity = '&#32;&#60;&#62;&#38;&#34;&#39;&circ;&#94;&tilde;&#126;&ndash;&mdash;&cent;&pound;&yen;&euro;&copy;&reg;&trade;&fnof;&OElig;&oelig;&bull;&hellip;&lsquo;&dagger;&permil;&alpha;&frasl;&larr;&spades;&#49;&#65;&#97;&#12354;&#65393;&#19968;&#135741;';
console.log( htmlEntities( entity, 'decode' ) );
// <>&"'ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽

jQueryでの実装の方がコード量が少なく書ける。特にデコード処理は優秀で、名前付きエンティティと数値エンティティのどちらが含まれていてもすべて実体文字へデコードしてくれる。もしjQueryが使える環境でJavaScript側でのHTMLエンティティのデコードを行う場合はこの処理一択で良いと思われる(ワンライナーで書けるしね)。

htmlentities系互換のJavaScript版HTMLエンティティ処理

次に、JavaScriptでエンコードした文字をPHPのhtml_entity_decodeでデコードする。もしくは、PHPのhtmlentitiesでエンコードされた文字をJavaScript側でデコードする場合の実装を考えてみる。
名前付きエンティティをJavaScriptに解釈させるためには、すべてのエンティティ名の変換テーブルをJavaScript側に定義してあげる必要がある。

if ( typeof htmlEntities == 'undefined' ) {
  htmlEntities = function ( str, proc ) {
    if ( 'encode' == proc ) {
      return str.replace( /[\u0000-\uFFFF<>\&]/g, function(c) {
        return typeof htmlEntities.entityTable[c.charCodeAt(0)] != 'undefined' ? '&'+htmlEntities.entityTable[c.charCodeAt(0)]+';' : c;
      });
    } else {
      var elm = document.createElement( 'div' );
      str = escape( str ).replace( /%26/g,'&' ).replace( /%23/g,'#' ).replace( /%3B/g,';' );
      elm.innerHTML = unescape( str );
      str = elm.textContent;
      if ( elm.innerText ) {
        str = elm.innerText;
        elm.innerText = '';
      } else {
        str = elm.textContent;
        elm.textContent = '';
      }
      return str;
    }
  };

  htmlEntities.entityTable = {
    34 : 'quot', 
    38 : 'amp', 
    39 : 'apos', 
    60 : 'lt', 
    62 : 'gt', 
   160 : 'nbsp', 
   161 : 'iexcl', 
   162 : 'cent', 
   163 : 'pound', 
   164 : 'curren', 
   165 : 'yen', 
   166 : 'brvbar', 
   167 : 'sect', 
   168 : 'uml', 
   169 : 'copy', 
   170 : 'ordf', 
   171 : 'laquo', 
   172 : 'not', 
   173 : 'shy', 
   174 : 'reg', 
   175 : 'macr', 
   176 : 'deg', 
   177 : 'plusmn', 
   178 : 'sup2', 
   179 : 'sup3', 
   180 : 'acute', 
   181 : 'micro', 
   182 : 'para', 
   183 : 'middot', 
   184 : 'cedil', 
   185 : 'sup1', 
   186 : 'ordm', 
   187 : 'raquo', 
   188 : 'frac14', 
   189 : 'frac12', 
   190 : 'frac34', 
   191 : 'iquest', 
   192 : 'Agrave', 
   193 : 'Aacute', 
   194 : 'Acirc', 
   195 : 'Atilde', 
   196 : 'Auml', 
   197 : 'Aring', 
   198 : 'AElig', 
   199 : 'Ccedil', 
   200 : 'Egrave', 
   201 : 'Eacute', 
   202 : 'Ecirc', 
   203 : 'Euml', 
   204 : 'Igrave', 
   205 : 'Iacute', 
   206 : 'Icirc', 
   207 : 'Iuml', 
   208 : 'ETH', 
   209 : 'Ntilde', 
   210 : 'Ograve', 
   211 : 'Oacute', 
   212 : 'Ocirc', 
   213 : 'Otilde', 
   214 : 'Ouml', 
   215 : 'times', 
   216 : 'Oslash', 
   217 : 'Ugrave', 
   218 : 'Uacute', 
   219 : 'Ucirc', 
   220 : 'Uuml', 
   221 : 'Yacute', 
   222 : 'THORN', 
   223 : 'szlig', 
   224 : 'agrave', 
   225 : 'aacute', 
   226 : 'acirc', 
   227 : 'atilde', 
   228 : 'auml', 
   229 : 'aring', 
   230 : 'aelig', 
   231 : 'ccedil', 
   232 : 'egrave', 
   233 : 'eacute', 
   234 : 'ecirc', 
   235 : 'euml', 
   236 : 'igrave', 
   237 : 'iacute', 
   238 : 'icirc', 
   239 : 'iuml', 
   240 : 'eth', 
   241 : 'ntilde', 
   242 : 'ograve', 
   243 : 'oacute', 
   244 : 'ocirc', 
   245 : 'otilde', 
   246 : 'ouml', 
   247 : 'divide', 
   248 : 'oslash', 
   249 : 'ugrave', 
   250 : 'uacute', 
   251 : 'ucirc', 
   252 : 'uuml', 
   253 : 'yacute', 
   254 : 'thorn', 
   255 : 'yuml', 
   338 : 'OElig', 
   339 : 'oelig', 
   352 : 'Scaron', 
   353 : 'scaron', 
   376 : 'Yuml', 
   402 : 'fnof', 
   710 : 'circ', 
   732 : 'tilde', 
   913 : 'Alpha', 
   914 : 'Beta', 
   915 : 'Gamma', 
   916 : 'Delta', 
   917 : 'Epsilon', 
   918 : 'Zeta', 
   919 : 'Eta', 
   920 : 'Theta', 
   921 : 'Iota', 
   922 : 'Kappa', 
   923 : 'Lambda', 
   924 : 'Mu', 
   925 : 'Nu', 
   926 : 'Xi', 
   927 : 'Omicron', 
   928 : 'Pi', 
   929 : 'Rho', 
   931 : 'Sigma', 
   932 : 'Tau', 
   933 : 'Upsilon', 
   934 : 'Phi', 
   935 : 'Chi', 
   936 : 'Psi', 
   937 : 'Omega', 
   945 : 'alpha', 
   946 : 'beta', 
   947 : 'gamma', 
   948 : 'delta', 
   949 : 'epsilon', 
   950 : 'zeta', 
   951 : 'eta', 
   952 : 'theta', 
   953 : 'iota', 
   954 : 'kappa', 
   955 : 'lambda', 
   956 : 'mu', 
   957 : 'nu', 
   958 : 'xi', 
   959 : 'omicron', 
   960 : 'pi', 
   961 : 'rho', 
   962 : 'sigmaf', 
   963 : 'sigma', 
   964 : 'tau', 
   965 : 'upsilon', 
   966 : 'phi', 
   967 : 'chi', 
   968 : 'psi', 
   969 : 'omega', 
   977 : 'thetasym', 
   978 : 'upsih', 
   982 : 'piv', 
  8194 : 'ensp', 
  8195 : 'emsp', 
  8201 : 'thinsp', 
  8204 : 'zwnj', 
  8205 : 'zwj', 
  8206 : 'lrm', 
  8207 : 'rlm', 
  8211 : 'ndash', 
  8212 : 'mdash', 
  8216 : 'lsquo', 
  8217 : 'rsquo', 
  8218 : 'sbquo', 
  8220 : 'ldquo', 
  8221 : 'rdquo', 
  8222 : 'bdquo', 
  8224 : 'dagger', 
  8225 : 'Dagger', 
  8226 : 'bull', 
  8230 : 'hellip', 
  8240 : 'permil', 
  8242 : 'prime', 
  8243 : 'Prime', 
  8249 : 'lsaquo', 
  8250 : 'rsaquo', 
  8254 : 'oline', 
  8260 : 'frasl', 
  8364 : 'euro', 
  8465 : 'image', 
  8472 : 'weierp', 
  8476 : 'real', 
  8482 : 'trade', 
  8501 : 'alefsym', 
  8592 : 'larr', 
  8593 : 'uarr', 
  8594 : 'rarr', 
  8595 : 'darr', 
  8596 : 'harr', 
  8629 : 'crarr', 
  8656 : 'lArr', 
  8657 : 'uArr', 
  8658 : 'rArr', 
  8659 : 'dArr', 
  8660 : 'hArr', 
  8704 : 'forall', 
  8706 : 'part', 
  8707 : 'exist', 
  8709 : 'empty', 
  8711 : 'nabla', 
  8712 : 'isin', 
  8713 : 'notin', 
  8715 : 'ni', 
  8719 : 'prod', 
  8721 : 'sum', 
  8722 : 'minus', 
  8727 : 'lowast', 
  8730 : 'radic', 
  8733 : 'prop', 
  8734 : 'infin', 
  8736 : 'ang', 
  8743 : 'and', 
  8744 : 'or', 
  8745 : 'cap', 
  8746 : 'cup', 
  8747 : 'int', 
  8756 : 'there4', 
  8764 : 'sim', 
  8773 : 'cong', 
  8776 : 'asymp', 
  8800 : 'ne', 
  8801 : 'equiv', 
  8804 : 'le', 
  8805 : 'ge', 
  8834 : 'sub', 
  8835 : 'sup', 
  8836 : 'nsub', 
  8838 : 'sube', 
  8839 : 'supe', 
  8853 : 'oplus', 
  8855 : 'otimes', 
  8869 : 'perp', 
  8901 : 'sdot', 
  8968 : 'lceil', 
  8969 : 'rceil', 
  8970 : 'lfloor', 
  8971 : 'rfloor', 
  9001 : 'lang', 
  9002 : 'rang', 
  9674 : 'loz', 
  9824 : 'spades', 
  9827 : 'clubs', 
  9829 : 'hearts', 
  9830 : 'diams', 
  };
}

var str = " <>&\"'ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽";
console.log( htmlEntities( str, 'encode' ) );
//  &lt;&gt;&amp;&quot;&apos;&circ;^&tilde;~&ndash;&mdash;&cent;&pound;&yen;&euro;&copy;&reg;&trade;&fnof;&OElig;&oelig;&bull;&hellip;&lsquo;&dagger;&permil;&alpha;&frasl;&larr;&spades;1Aaあア一𡈽

var entity = ' &lt;&gt;&amp;&quot;&apos;&circ;^&tilde;~&ndash;&mdash;&cent;&pound;&yen;&euro;&copy;&reg;&trade;&fnof;&OElig;&oelig;&bull;&hellip;&lsquo;&dagger;&permil;&alpha;&frasl;&larr;&spades;1Aaあア一𡈽';
console.log( htmlEntities( entity, 'decode' ) );
//  <>&"'ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽

変換テーブルの定義もあってだいぶ長くなった…コーディングコストがかなり高い。
ちなみに、名前付きエンティティと数値エンティティが混在するmb_convert_encodingとの互換を行う場合、上記コードのエンコード部を下記のように書き換えれば良い。

return '&' + ( htmlEntities.entityTable[c.charCodeAt(0)] || '#' + c.charCodeAt(0) ) + ';';

mb_encode_numericentity系互換のJavaScript版HTMLエンティティ処理

そして、すべての文字を数値エンティティ化してやり取りする場合のJavaScript側の実装を考えてみる。

var htmlEntities = function( str, proc ) {
  if ( 'encode' == proc ) {
    var buffer = [];
    for ( var i=str.length-1; i>=0; i-- ) {
      buffer.unshift( ['&#', str[i].charCodeAt(), ';'].join('') );
    }
    return buffer.join('');
  } else {
    return str.replace( /&#(\d+);/g, function( match, dec ) {
      return String.fromCharCode( dec );
    });
  }
};

var str = " <>&\"'ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽";
console.log( htmlEntities( str, 'encode' ) );
// &#32;&#60;&#62;&#38;&#34;&#39;&#710;&#94;&#732;&#126;&#8211;&#8212;&#162;&#163;&#165;&#8364;&#169;&#174;&#8482;&#402;&#338;&#339;&#8226;&#8230;&#8216;&#8224;&#8240;&#945;&#8260;&#8592;&#9824;&#49;&#65;&#97;&#12354;&#65393;&#19968;&#55364;&#56893;

var entity = '&#32;&#60;&#62;&#38;&#34;&#39;&#710;&#94;&#732;&#126;&#8211;&#8212;&#162;&#163;&#165;&#8364;&#169;&#174;&#8482;&#402;&#338;&#339;&#8226;&#8230;&#8216;&#8224;&#8240;&#945;&#8260;&#8592;&#9824;&#49;&#65;&#97;&#12354;&#65393;&#19968;&#55364;&#56893;';
console.log( htmlEntities( entity, 'decode' ) );
//  <>&"'ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽

処理が単純なので、エンコードもデコードもすっきり書けて、イイ感じだ。

underscore.jsを利用したHTMLエンティティ

その他に、JavaScriptの拡張ライブラリである「underscore.js」を使ったケースを紹介しておこう。このunderscore.jsではHTMLエンティティのエンコード/デコードのメソッドがあらかじめ準備されている。エンコードの場合は_.escape()、デコードの場合は_.unescape()だけで利用できるため、使い勝手は抜群に良い。

var str = " <>&\"'`ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽";
console.log( _.escape( str ) );
//  &lt;&gt;&amp;&quot;&#x27;&#x60;ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽
var entity = '&lt;&gt;&amp;&quot;&#x27;&#x60;ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽';
console.log( _.unescape( entity ) );
// <>&"'`ˆ^˜~–—¢£¥€©®™ƒŒœ•…‘†‰α⁄←♠1Aaあア一𡈽

ただ、_.escape()メソッドではバッククォート`もエンティティ化してしまうのと、シングルクォートとバッククォートは16進数の数値エンティティに変換されてしまうため、PHP側と相互変換を行う場合にはその特性を把握したうえで処理を行う必要がある。

フロントエンドだけで処理が完結する時などの文字列のやり取りには最適なので、一考の余地はある。

まとめ

今回色々と試してみたが、HTMLエンティティの取り扱いは結構カオスだった。
それもこれも、名前付きエンティティの定義が中途半端に散在しているからだ。それらをどこまで処理するかによってフロントエンドもサーバーサイドも色々と工夫をしないといけないということが分かった。

まぁ、所感としては、すべからく数値型エンティティに変換してやり取りするのが一番無難で安定するといったところか。ただ、エンティティ化には文字列データの肥大化というデメリットが付きまとうので、フロントエンドとサーバーサイド間のデータにはエンティティ化したデータは用いない方が良いだろう。
その辺はシステムとしてのパフォーマンスを取るか、機種依存文字へのシステム耐性を取るかのトレードオフになってくるのかもしれないが…。

今回の考察は結構疲れました…。