data: URL 向けに文字列を%エンコーディングするRubyワンライナー
CSS に data: URL を使って、データを埋め込む場合、画像などは Base64 エンコーディングを利用しますが、埋め込みたいデータが SVG などのテキストをベースとするデータの場合、 Base64 エンコードではなく、URIの標準的なエスケープ方法(%エンコーディング) を使う方が可読性やサイズなどの点で有利になる場合があります。
この際に重要なのは、埋め込む際にどの文字をエスケープしなければならないかの情報です。
この記事では、 Fetch Standard の要件の簡単なまとめと、それに従ったエスケープを行うための Ruby ワンライナーを記載します。
エスケープが必要な文字
RFC2397 (Obsoleted)
"data" URL scheme を提案した RFC2397 で定義されているデータ本体(body)に用利できる文字は、RFC2396 で定義されているURIに利用できる文字(uric
)に準拠しており、エンコードしなければならない文字の判定は大体以下の Ruby コードで表すことが出来ました:
require "erb"
alphanum = %q{[a-zA-Z0-9]}
mark = %q{[-_.!~*'()]}
reserved = %q{[;/?:@&=+$,]}
char2escape = /[^#{reserved}#{alphanum}#{mark}]/
ans = ''
while gets
print($_.chomp.gsub(char2escape){|c|ERB::Util.url_encode(c)})
end
要するに、この仕様では 文字クラス(正規表現における文字の集合) alphanum
、mark
、reserved
のいずれにも含まれていない文字はエスケープ対象です。
※こちらの仕様では結構多くの文字がエスケープされます。
Fetch Standard
しかし、近年のブラウザ標準を定めている WHATWG の Fetch Standard ではこの仕様を大幅に緩和していて、簡単にいうと以下のような仕様になります。
- data: URL の body 部分は byte sequence で構成される。
- byte sequence は
0xXX
形式の byte をスペースで区切ったものとして表記される。 - ただし、byte の値の範囲が
0x20
–0x7e
に収まっている byte sequence は通常の文字列として表示しても良い。※body はこの規定に基づいて文字列で表現されている。 - body を解釈する前には%デコードを行う。すなわち、エスケープするべき文字は data: URL では%エンコードされる。
-
#
(0x23
) は URI における Fragment の予約文字として解釈されるので、この値が body 中で出てくる場合はエスケープする
また、この data: URL を CSS や HTML に埋め込む場合、以下のような記法になります:
:root {
--ver: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'>...</svg>");
}
<img src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'>...</svg>"
この際、URLの中身はダブルクォーテーション "
により囲んで記載する関係で、CSSに利用する場合はこの文字もエスケープしないとブラウザのパーサが理解することが出来なくなりますので、次の副次的な要件が発生します:
-
"
(0x22
) もエスケープする
エスケープを行うワンライナー
上記を反映した、SVGなどのテキストデータを data: URL の body に埋め込むためにエスケープするための Ruby ワンライナーは以下になります:
ruby -r cgi -npe '$_.chomp!.gsub!(/[^ !\x24-\x7e]/){|c|CGI.escape(c)}' < $INPUT
※改行コードを残す場合は chomp!
を削除して $_.gsub!
としてください。
補足: [\x20-\x21]
は2文字だけなので下記のように実際の文字で表記しています:
正規表現 | 文字 | 補足 |
---|---|---|
\x20 |
|
半角スペース |
\x21 |
! |
エクスクラメーションマーク |
多くの場合動く代替案
Chrome などの実際のブラウザの多くでは body 部分の文字列が [\x20-\x7e]
の範囲に収まっていなくても UTF-8 の範囲であればきちんと解釈してくれるように見えますので、標準に厳密に準拠する必要がないのであれば、以下の変換でもおそらく十分です。
ruby -r cgi -npe '$_.chomp!.gsub!(/[#"]/){|c|CGI.escape(c)}' < $INPUT
SVGやXMLなどを埋め込む際のコツ
XMLや(XMLに基づく)SVGでは、Attribute Value を囲うための記号として、ダブルクォーテーション "
だけでなくシングルクォーテーション '
も使えるので、できるだけシングルクォーテーションを使うようにすると、エスケープの量を最小化することが出来ます。
<svg xmlns="http://www.w3.org/2000/svg" style="background:#AAA"></svg>
<svg xmlns=%22http://www.w3.org/2000/svg%22 style=%22background:%23AAA%22></svg>
例1.svg
と同じ内容をシングルクォーテーションで書いたものはこちら:
<svg xmlns='http://www.w3.org/2000/svg' style='background:#AAA'></svg>
<svg xmlns='http://www.w3.org/2000/svg' style='background:%23AAA'></svg>
こちらだとエスケープは #
一箇所です。
Discussion