data: URL 向けに文字列を%エンコーディングするRubyワンライナー

[RFC2396]:https://datatracker.ietf.org/doc/html/rfc2396#appendix-A
[RFC2397]:https://datatracker.ietf.org/doc/html/rfc2397
[WHATWG]:https://www.whatwg.org/
[Fetch Standard]:https://fetch.spec.whatwg.org/
[Wikipedia]:https://ja.wikipedia.org/wiki/Data_URI_scheme
[Base64]:https://ja.wikipedia.org/wiki/Base64

CSS に [data: URL][Wikipedia] を使って、データを埋め込む場合、画像などは [Base64] エンコーディングを利用しますが、埋め込みたいデータが SVG などのテキストをベースとするデータの場合、 [Base64] エンコードではなく、URIの標準的なエスケープ方法(％エンコーディング) を使う方が可読性やサイズなどの点で有利になる場合があります。

この際に重要なのは、埋め込む際にどの文字をエスケープしなければならないかの情報です。
この記事では、 [Fetch Standard] の要件の簡単なまとめと、それに従ったエスケープを行うための Ruby ワンライナーを記載します。

:::note
とりあえず結論を見たい方は [エスケープを行うワンライナー](#エスケープを行うワンライナー) を御覧ください。
:::

## エスケープが必要な文字

### RFC2397 (Obsoleted)

"data" URL scheme を提案した [RFC2397] で定義されているデータ本体（body）に用利できる文字は、[RFC2396] で定義されているURIに利用できる文字（`uric`）に準拠しており、エンコードしなければならない文字の判定は大体以下の Ruby コードで表すことが出来ました：
```ruby:RFC2397準拠
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
```
要するに、この仕様では [文字クラス](https://docs.ruby-lang.org/ja/latest/doc/spec=2fregexp.html#string)(正規表現における文字の集合) `alphanum`、`mark`、`reserved` のいずれにも含まれていない文字はエスケープ対象です。
※こちらの仕様では結構多くの文字がエスケープされます。

### Fetch Standard

しかし、近年のブラウザ標準を定めている [WHATWG] の [Fetch Standard] ではこの仕様を大幅に緩和していて、簡単にいうと以下のような仕様になります。

1) data: URL の body 部分は byte sequence で構成される。
1) byte sequence は `0xXX` 形式の byte をスペースで区切ったものとして表記される。
1) ただし、byte の値の範囲が `0x20` &ndash; `0x7e` に収まっている byte sequence は通常の文字列として表示しても良い。※body はこの規定に基づいて文字列で表現されている。
1) body を解釈する前には%デコードを行う。すなわち、エスケープするべき文字は data: URL では%エンコードされる。
1) `#` (`0x23`) は URI における Fragment の予約文字として解釈されるので、この値が body 中で出てくる場合はエスケープする

また、この data: URL を CSS や HTML に埋め込む場合、以下のような記法になります：
```CSS:CSS
:root {
  --ver: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'>...</svg>");
}
```
```HTML:HTML
<img src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'>...</svg>"
```

この際、URLの中身はダブルクォーテーション `"` により囲んで記載する関係で、CSSに利用する場合はこの文字もエスケープしないとブラウザのパーサが理解することが出来なくなりますので、次の副次的な要件が発生します：

1) `"` (`0x22`) もエスケープする

## エスケープを行うワンライナー

上記を反映した、SVGなどのテキストデータを data: URL の body に埋め込むためにエスケープするための Ruby ワンライナーは以下になります：
```bash:Fetch Standard準拠
ruby -r cgi -npe '$_.chomp!.gsub!(/[^ !\x24-\x7e]/){|c|CGI.escape(c)}' < $INPUT
```
※改行コードを残す場合は `chomp!` を削除して `$_.gsub!` としてください。

補足: `[\x20-\x21]` は２文字だけなので下記のように実際の文字で表記しています：
|正規表現|文字|補足|
|-------|---|---|
|`\x20`|` `|半角スペース|
|`\x21`|`!`|エクスクラメーションマーク|

## 多くの場合動く代替案

Chrome などの実際のブラウザの多くでは body 部分の文字列が `[\x20-\x7e]` の範囲に収まっていなくても UTF-8 の範囲であればきちんと解釈してくれるように見えますので、標準に厳密に準拠する必要がないのであれば、以下の変換でもおそらく十分です。
```bash:省略版
ruby -r cgi -npe '$_.chomp!.gsub!(/[#"]/){|c|CGI.escape(c)}' < $INPUT
```

## SVGやXMLなどを埋め込む際のコツ

XMLや（XMLに基づく）SVGでは、Attribute Value を囲うための記号として、ダブルクォーテーション `"` だけでなくシングルクォーテーション `'` も使えるので、できるだけシングルクォーテーションを使うようにすると、エスケープの量を最小化することが出来ます。

```xml:例1.svg ※こちらだとエスケープが増える
<svg xmlns="http://www.w3.org/2000/svg" style="background:#AAA"></svg>
```
```txt:エスケープ後
<svg xmlns=%22http://www.w3.org/2000/svg%22 style=%22background:%23AAA%22></svg>
```
`例1.svg` と同じ内容をシングルクォーテーションで書いたものはこちら：
```xml:例2.svg ※こちらがベター
<svg xmlns='http://www.w3.org/2000/svg' style='background:#AAA'></svg>
```
```txt:エスケープ後
<svg xmlns='http://www.w3.org/2000/svg' style='background:%23AAA'></svg>
```
こちらだとエスケープは `#` 一箇所です。