HTMLのpattern属性とJavaScript正規表現のunicodeオプション

こんにちは、Webアプリケーションエンジニアのid:nanto_viです。みなさんHTMLのフォーム検証機能は使っていますか? 近年は各Webブラウザの対応も進み、お手軽にフォームの利便性を高められるようになっています。

そんなフォーム検証機能のひとつがinput要素のpattern属性です。pattern属性の値にJavaScriptの正規表現パターンを指定することで、ユーザーの入力が意図しないものであった場合、フォーム送信ができなくなります。下図は5桁の数字の入力が求められるところに3桁しか入力せずフォーム送信しようとしたところです。ブラウザに組み込みのエラー表示が出現し、またそのメッセージにtitle属性の値が使われていることを確認できるでしょう。(pattern属性を指定する際には、title属性に書式の説明を記述することが推奨されています。)

<input type="text" name="code" pattern="[0-90-9]{5}"
       title="5桁の数字を入力してください">

f:id:nanto_vi:20160428155855p:plain

pattern属性に指定したパターンは全体一致で評価されるので、わざわざパターンの先頭・末尾に^$をつける必要はありません。逆に部分一致で指定したいとなったら先頭・末尾に.*?.*を明示的につける必要があります。

さて、pattern属性を使って「3文字以内の文字列」という制限をかけるにはどうしたらいいでしょうか? 改行を除く任意の文字を表すメタ文字.を使って以下のように書けるはずです。

<input type="text" name="food" pattern=".{0,3}"
       title="3文字以内で入力してください">

ところが、ここに「🍣🍡🍵」の3文字を入力してみると、ブラウザによっては検証が通りません。なぜでしょうか? 困ったときは仕様を見るのが一番です。

If an input element has a pattern attribute specified, and the attribute's value, when compiled as a JavaScript regular expression with only the "u" flag specified,

4.10.5.3.6 The pattern attribute - HTML Standard

pattern属性の値が/uフラグ付きの正規表現としてコンパイルされると書いてありますね。/uフラグ(unicodeオプション)はECMAScript 2015 (ES6)で導入された正規表現のオプションで、マッチする単位を「UTF-16の符号単位」から「Unicodeの符号位置」に変更するなどの効果があります。

JavaScriptの文字列はUTF-16符号化形式で表現されます。UTF-16において多くの文字はひとつの符号単位(16ビット符号なし整数値)で表現されますが、BMP(基本多言語面)外の文字はサロゲートペアと呼ばれるふたつの符号単位の組み合わせで表現されることになります。従来のJavaScript正規表現では.がひとつの符号単位にマッチするため、それが必ずしも「1文字」(ひとつの符号位置)にマッチするとは言えない状況でした。/uフラグを使うことで.が「1文字」にマッチするようになります。(ここで「結合文字や異体字セレクタは?」と思った方は、この記事がなくても大丈夫そうですね😜)

/^.$/.test('🍣');   // => false
/^.$/u.test('🍣');  // => true

各ブラウザが/uフラグをサポートしたのは比較的最近のことなので、ちょっと古めのブラウザだとpattern属性の値が/uフラグなしの正規表現パターンとして扱われます。「🍣🍡🍵」はいずれもBMP外の文字であり、計6個の符号単位からなる文字列なので検証に失敗していたのです。

ではそのようなブラウザでも「3文字以内」を表現するためにはどうしたらいいでしょうか? サロゲートペアに使われる領域(U+D800U+DFFF)に文字が割り当てられていないことを考えると、以下のように書けます。

<input type="text" name="food"
       pattern="(?:[\uD800-\uDBFF][\uDC00-\uDFFF]|.){0,3}"
       title="3文字以内で入力してください">

/uフラグに対応した環境では前半([\uD800-\uDBFF][\uDC00-\uDFFF])にマッチする文字は存在せず、後半(.)に改行以外の文字がマッチします。/uフラグに対応していない環境では前半にBMP外の文字がマッチし、後半に改行以外のBMP内の文字がマッチすることで、やはり「1文字」ごとにマッチを進めていけるのです。

任意の文字を表す正規表現パターン

以上では改行にマッチさせる必要がないため.を用いましたが、改行にもマッチさせようと思ったら.の部分を[\S\s]などに置き換える必要があります。/uフラグ付きなら[\u{0}-\u{10FFFF}]とも書けます。

ちなみに、Perlでは/sフラグを指定することで.が改行にもマッチするようになります。

maxlength属性

「3文字以内」と聞いてmaxlength属性のことを思い浮かべた方も多いでしょう。しかし、maxlength属性にはいくつかの注意事項があり、何文字以内という制約を表すのには使いづらい面があります。

  1. ブラウザによっては、日本語入力時の変換前の文字までmaxlength属性の制約を受けてしまいます。
  2. maxlength属性の値はUTF-16の符号単位の数を表しており、BMP外の文字に対しては期待通り機能しません。

ASCIIの範囲内での入力を求めるのならmaxlength属性でもよいでしょうが、そうでないなら上で述べたようにpattern属性を使ったほうがユーザーにとってより使いやすいかもしれません。

終わりに

絵文字の隆盛もあって今やBMP外の文字は広く巷に流通するところですが、それを扱うソフトウェア側では特別な注意が必要なことも多々あります。はてなでは、ユーザーから多彩な入力を受け付け世界に伝えるべく、バリエーション豊かなエンジニアを募集しています。

hatenacorp.jp