Direct Proxiesを使ってaltCSSを作ってみた

  • 7
    Like
  • 0
    Comment
More than 1 year has passed since last update.

Direct Proxiesを使ってJSSSのPreprocessorを作ってみた

最初は「Direct Proxiesでハマった話」って題名で仕様についてまとめる予定だったんですが、
ハマらなかったので作ってみた系に変更しました。

背景

Sassやlessなど、著名なCSS Preprocessorが何個かありますが
どうも満足がいかないなぁとよく思っていました。

pluginをカジュアルにnpmのエコシステムに載せたいし、pluginのテストを楽に実装したいし、デバッグを簡単にしたいし、、、

そこで、JavaScriptっぽいCSSを作ろうとするのが
このAdvent Calender参加者あるあるなのかなと思います。

CSS内でJavaScriptを書こうとしてみたり
https://www.npmjs.org/package/jscss

いっそ動的に指定する方に倒してみたり
https://github.com/Box9/jss

などなど。

自分なりに色々探してみたんですが、
1996年に同じようなことをやろうとした人たちがいたことを知りました。それがJSSSというSpecの提案です。

JavaScript Based Style Sheet
http://www.w3.org/Submission/1996/1/WD-jsss-960822

これは、動的にJavaScriptでstyleを指定するAPIを取り決めたものです。例えば
こんなCSSを

h1 {
  color: blue;
}

このようなJavaScriptで表現できるというものです。

document.tags.h1.color = "blue";
// or
tags.h1.color = "blue";

この仕様をCSS Preprocessorとして作ってみようかなぁというのがはじまりでした。

作っていく

仕組みは至ってシンプルで

  1. tags, classed, idsにプロパティを都合よく挿入
  2. JSを実行し、ObjectをCSS ASTに変換していく
  3. CSS ASTからCSSを書き出す

という流れにすることにしました。

CSS ASTに変換する周りは、結果を見ながら答え合わせしていくだけなので
JSSSをJavaScriptとして実行してもエラーを出さないところに注力することになります。

最初は、この表現の実現です

tags.h1.color = "blue";

あるObject(今回はtags)に対して
存在するかどうか分からないプロパティ(今回はh1)に値を追加を可能にする必要がありました。

再現コード例:

var foo = {};
foo.bar.baz = 'Yo';

当然これは、
"Cannot set property 'baz' of undefined"
と怒られます。

しかし、Direct Proxies(以下DP)を使うと
値の挿入を行うタイミングで自身のプロパティを確認し、無ければ追加することができます。

var handler = function () {};
handler.get = function (target, name, receiver) {
  // 自身にプロパティが存在しない
  if (name in receiver == false) {
    // 参照ではない
    if (name != 'inspect') {
      // 上記の条件の際にプロパティを追加してくれるProxyを追加
      target[name] = new Proxy({}, handler);
    }
  }

  return target[name];
};

var foo = new Proxy({}, handler);
foo.bar.baz = 'Yo'; // => (・ω<) オッケー

そして、
ここまでは良かったんですが、JSSSの仕様にあるcontextualという機能の実装を行おうとした際に表題の問題にぶつかりました

ProxyのhandlerでProxyを作れない?

contextual はこんな実装

/**
 * tags.H1.color => red
 * tags.EM.color => red
 **/
contextual(tags.H1, tags.EM).color = "red";

Proxyのsetを利用して実装してみました

var contextual = function () {
  var args = arguments;
  var handler = function () {};
  handler.set = function(target, prop, value, receiver) {
    Object.keys(args).forEach(function (key) {
      args[key][prop] = value;
    });
  };

  return new Proxy({}, handler);
};

これを元に、前述のProxyを呼ぶと

var handler = function () {};
handler.get = function (target, name, receiver) {
  if (name in receiver == false) {
    if (name != 'inspect') {
      target[name] = new Proxy({}, handler);
    }
  }

  return target[name];
};

var tags = new Proxy({}, handler);
var ids = new Proxy({}, handler);

var contextual = function () {
  var args = arguments;
  var handler = function () {};
  handler.set = function(target, prop, value, receiver) {
    Object.keys(args).forEach(function (key) {
      args[key][prop] = value;
    });
  };

  return new Proxy({}, handler);
};

contextual(tags.H1, ids.foo).color = 'Red';

と、これで実装できました。

で、

これNode.jsで実装したら動かなかったんですが
https://github.com/watilde/jsss-compiler/issues/1

Firefox(39以上)だと動いたので、これにて記事を終わりにします;(
Nodeの--harmonyだとDPが古い方のAPIだったりして微妙に挙動がうまいこといってないのかなぁ...。

明日は @h_demon さんです!
自作altJSとか超たのしみ <3

余談

最初 @hokaccha さんと日付とタイトルが完全に被ってた