node-inlining

HTMLからlink要素で参照しているCSSの内容をstyle属性に全部展開するNode.jsパッケージ、node-inliningを書いていた。HTMLとCSSを別々に普通に書き、このパッケージに含まれるCLIプログラムでコンパイルすると、HTMLメールとしてうまく機能するHTMLができあがるということになる。GitHubで推奨されている外部リソースに依存しない静的なエラー・ページを作成するためにも使えるかもしれない。

CLIプログラムはごく簡単に使うことができる。

$ npm install -g inlining
$ inlining input.html >output.html

これでoutput.htmlにインライン化されたHTMLファイルが吐かれる。処理例はREADMEの簡単な例やtestディレクトリーを見てくれればわかるはずだ。

Node.jsパッケージとしての利用は少しややこしくなる。

var inlining = require("inlining");
inlining(fs.readFileSync("input.html", "utf8"), function (result) {
  console.log(result);
});

引数は以下の3つになる。

  1. HTMLコード
  2. HTMLファイルの(想定される)パス(省略可能)
  3. コールバック

HTMLコードを直接渡すと処理して、コールバック関数が処理結果を引数として実行される。HTMLファイルのパスは相対パスを解決するために使っている。省略した場合はカレント・ディレクトリーになる。


パッケージ内では以下の様な順で処理される。

  1. rel="stylesheet"であるlink要素を列挙
    1. href属性の値をパスとして解決
    2. CSSを読み込んでパース
      1. 読み込みに失敗したらスキップして次のlink要素へ
    3. ルールセットのセレクターを分割
      1. セレクターにマッチする要素を列挙
      2. ルールセットの内容を連結して、style属性の値に設定
    4. ルールセットを削除
    5. 残ったCSSをstyle要素の内容としてhead要素に追加
    6. link要素を削除
  2. 処理結果を標準出力に出力

@mediaルールなどのstyle属性へ記述できないルールセット群はそのまま残り、出力HTMLのhead/styleにそのままコピーされることになる。ここで詳細度が逆転してしまう可能性があるので、@mediaルールで上書きしたい場合は!importantフラグを駆使する必要がある。他、相対パスで指定された画像ファイルなどはDataURLで埋め込まれる。


内部ではHTMLをパースしてDOM API群を提供してくれるjsdomパッケージとおなじみCSSをパースしてくれるpostcssパッケージを利用した。jsdomはその存在は知っていたものの、初めてまともに使った。概ね使いやすかったが、やはりバグとはいえないまでも、いくつか特徴的な挙動は持つようだ。

例えばjsdomでHTMLElement.style.cssTextを使うとノーマライズされてしまう。そのためベンダー拡張プリフィックス付きのプロパティーや存在しないプロパティー(fooとか)、そして未知のプロパティー(font-feature-settingsプロパティーとか)がうまく追加できなかった。仕方がないのでElement.setAttribute()を使って強引にそのまま設定している。

またquerySelectorAll()::-moz-selection擬似要素を含むものなど不明なセレクターを投げるとブラウザーと同じように例外を吐く。ブラウザーではそのまま処理は続行されるが、Node.js上では当然落ちる。使いづらいが挙動としては正しそうなため、try..catchで握りつぶして無視した。

最後に完全なHTMLソースを手に入れることに少し苦労した。window.document.documentElement.innerHTMLだとhtml要素が除外され、window.document.documentElement.outerHTMLだとDOCTYPEが拾えない。window.document.doctypeを使って連結するのは少しややこしすぎる。どうやら専用の非標準APIが用意されているようで、それを使うとうまく手に入れられた。

var jsdom = require("jsdom");

jsdom.env(
  "<!DOCTYPE html><p>Lorem ipsum</p>",
  function (errors, window) {
    console.log(window.document.documentElement.innerHTML);
    // <head></head><body><p>Lorem ipsum</p></body>
    console.log(window.document.documentElement.outerHTML);
    // <html><head></head><body><p>Lorem ipsum</p></body></html>
    console.log(jsdom.serializeDocument(window.document));
    // <!DOCTYPE html><html><head></head><body><p>Lorem ipsum</p></body></html>
  }
);

他、MIMEタイプの推定にはmimeパッケージ、画像のDataURL化にはBuffer.toString("base64")に当たるものを利用した。


とりあえず動くところまでという形で書いた。続きはわからないけれど、必要な機能はもうあまりなさそうだ。強いて言うのならHTMLファイル内の画像ファイルのDataURL化くらいだろうか。