htmlwidgetsでD3.jsを使おうとしたら文字コードの闇に飲まれかけた話

追記('15/01/02):pandoc側で修正が入ったので(Issue #1842)、minifyされてないファイルでもそのうち動くようになるはずです。


好きなJavascriptの可視化パッケージをRの世界にサクッと持ち込む(htmlwidgets) - Technically, technophobic.で、「サクッと持ち込む」と言いつつサクッと持ち込めなかった理由についての話です。

地味です。あんまり面白くないです。

問題

つくったパッケージで書いたグラフが、RStudio上だとちゃんと表示されるのに、Rmarkdownだとうまく表示されない、という謎の現象が起こっていました。詳しくはhtmlwidgetsのIssueを見てください。

D3.js(not minified) contains corrupted characters after rmarkdown::render() · Issue #56 · ramnathv/htmlwidgets · GitHub

原因

調べたところ、どうも問題はRmarkdownでJavascriptをData URIに埋め込むときに起こっていることが分かりました。Data URIでの埋め込みについては後述するのでとりあえず省略します。

ブラウザの開発者ツールでエラーが起こってる行を見ると、文字化けしています。

f:id:yutannihilation:20141228001726p:plain

RStudio上で見たときに同じ行がどうなっているかというと、、なんとUTF-8の文字が使われています。

f:id:yutannihilation:20141228001738p:plain

全く知らなかったんですが、JavascriptってUTF-8の文字をふつうに使えるんですね。

TIL: JavaScript allows for UTF8 variable names, so var ಠ_ಠ = 5; is a perfectly valid assignment : javascript

D3.jsの言い分

これにつまづく人はしばしばいるらしく、D3.jsではIssueが立ってました。

d3.v3.js does not work if document encoding is not UTF8 · Issue #1195 · mbostock/d3 · GitHub

ここで提案されている解決策は3つです。

  1. ドキュメントの先頭あたりに<meta charset="utf-8">と書いてcharsetを宣言する
  2. srcタグの中で<script charset="utf-8" src="d3.js"></script>という感じでcharsetを指定する
  3. minifyされるとASCII文字になるのでd3.min.jsを使う

しかし、knitされたファイルを見ると、

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">

<head>

<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="pandoc" />

ということで、ばっちり解決策1を使ってるはずです。なのになんで文字化けするんでしょう?

d3.min.jsを使うととりあえず動く。

そのあと解決策2を試してもむりで、もうだめかも、と絶望感を感じながら解決策3を試すと、なんと、動きました!!!

めでたしめでたし!

   

...なんですが、なんかもやもやするのでもう少し調べてみます。

Data URI scheme

さっき説明を端折りましたが、Data URI schemeとは何なんでしょう? これは、画像とかJavascript/CSSファイルをbase64エンコードしてページに埋め込める仕組みです。

具体的には、RStudio上ではこんな感じになっているところが、

<script src="lib/d3-3.5.2/d3.js"></script>

RmarkdownをknitしたHTMLファイルだと、以下のようになっています。

<script src="data:application/x-javascript,%28function%28%29%20%7B%0A%20%20%22use%20...

これは、rmarkdownが内部的に使っているpandoc--standaloneオプションを利用しています。

Data URI schemeの仕様

Data URI schemeRFC2397によると以下のようなフォーマットです。

     data:[<mediatype>][;base64],<data>  
     dataurl    := "data:" [ mediatype ] [ ";base64" ] "," data  
     mediatype  := [ type "/" subtype ] *( ";" parameter )  
     data       := *urlchar  
     parameter  := attribute "=" value  

で、このparametercharsetを指定できるんですが、気になる記述があります。

If <mediatype> is omitted, it defaults to text/plain;charset=US-ASCII. As a shorthand, "text/plain" can be omitted but the charset parameter supplied.

え、デフォルトがUS-ASCII? この21世紀にそんな暴挙ありですか??

この部分が、MIMEタイプだけ指定してcharsetを指定していない場合に言及しているのかよく分からないんですが、現実、ブラウザはそういう感じ(他の場所でcharsetを指定してあっても、Data URIに書いてあるやつはASCIIとみなす)の挙動をしています。うーん。

じゃあcharset指定すればよくない?

そう思ってpandocのソースまで見たんですが、ここにはたしてcharsetが入れれる余地があるのかよく分かりません。。

https://github.com/jgm/pandoc/blob/bf00556c722dde161f3bed3da710fe97d0d5033e/src/Text/Pandoc/SelfContained.hs#L62

思い余ってpandocにIssueまで立ててしまいましたが、単なる勘違いかもしれません。

Charset should be specified when JS/CSS files are embeded in data URI by `--standalone` · Issue #1842 · jgm/pandoc · GitHub

結論

よく分かんないけど、とりあえずminifyされたファイル使っとけばおkなんでしょ?という流れに私が立てたIssueはなってます。若干もやっとしますが、bowerでダウンロードされるファイルにはたいがい*.min.jsがあるので、まあそれで問題ないはずです。

まさかRを使っていて、ブラウザの開発者ツールを開いてRFCを読み出す日が来るとは思ってませんでした。つらい。。

次回予告

上のような話になってるんですが、Rのパッケージビルド時にGulpでconcat+uglifyしてminifyされたJSとCSSを自動生成する方法を無駄に模索中です。たぶん使う人いないと思いますけど、、ここまで来たらまとめようかなと思ったり思わなかったりしています。