Stylus JavaScript API とプラグインの仕組みについて
先日 foovar という Stylus のライブラリを公開しました。
Stylus の変数を JS ファイルにエクスポートするためのライブラリです。
その時に調べた Stylus JavaScript API の詳細や、プラグイン作成の方法です。
仕様の変更などにより記事の内容が古くなったらすぐ分かるように、記事のテストを書いてみました。
Stylus プラグインの仕組み
プラグインの実体は関数を返す関数(高階関数)です。
これを module.exports
するだけでプラグインの体を成します(役に立たないということを除けば)。
npm で公開すればすぐにプラグインとして利用可能です。
module.exports = function() { return function() {}; };
外側の関数は use
(後述)した際に呼ばれ、オプションを引数に受取ります。
内側の関数は Renderer
のインスタンスを引数に受け取ります(コード中stylus
)。
module.exports = function(...options) { return function(stylus) {}; };
内側の関数内の this
はこの Renderer
インスタンスに束縛されて呼び出されます。
require('stylus')()
で得られるインスタンスも同じく Renderer
のインスタンスです。
module.exports = function(...options) { return function(stylus) { this.condtructor.name // 'Renderer' this === stylus; // true this.constructor === require('stylus')().constructor; // true }; };
基本的にはこの Renderer
インスタンスを介して Stylus の世界とやり取りをすることになります。
Stylus から呼び出せる関数を JS で定義したり、あらかじめ用意した .styl
ファイルへのパスを通したりといった感じです。
この Renderer
インスタンスの API については公式のドキュメントに詳しい情報があります。
プラグインの読み込みとオプションの渡し方
プラグインを読み込む方法は3つあります。
- ビルトイン関数
use(path)
を使う - JavaScript API の
.use(fn)
を使う - CLI の
--use name
オプションを使う
いずれも use
という言葉が使われており、このキーワードがプラグイン読み込みを指すと思われる。
ビルトイン関数 use(path)
JS ファイルのパスを指定して呼び出す。
プラグイン名での呼び出しは出来ない。
第2引数にはハッシュをオプションとして渡せる。
渡せるのはハッシュのみで、それ以外はエラーになる。
また第3引数以降は無視される。
use('./my-plugin.js') use('../../node_modules/plugin/index.js') // 名前での呼び出しは出来ない。 node_modules 配下のエントリポイントを直接指定することは出来る use('../../node_modules/plugin/index.js', { foo: 20px }) // オプションに渡せるのはハッシュのみ
JavaScript API の .use(fn)
前述の高階関数の内側の関数のみを渡す。
require
で読み込んだモジュールを実行して内側の関数を取り出し use
に渡すという使い方になる。
const stylus = require('stylus'); stylus(source) .use(require('plugin-name')()) .render(/*...*/);
外側の関数にはオプションを渡すことが出来る。
require
で読み込んで関数を直接呼び出すだけなので、好きな数の引数を好きな型で渡すことができる。
use(path)
、 CLI の --use
オプションとAPIを揃えるために、オプションはオブジェクト1個に限定したほうがいいかもしれない。
stylus(source) .use(require('plugin-name')({foo, 'bar'})) .render(/*...*/);
CLI の --use name
オプション
--use
に続けてプラグイン名を渡すと読み込んでくれる。
短縮形は -u
。
$ stylus --use plugin-name path/to/source.styl # あるいは $ stylus -u plugin-name path/to/source.styl
--with
に続けて文字列を渡すと、 JS としてパースされ外側の関数にオプションとして渡される。
なぜかドキュメントに載ってないし、 --help
にも出てこない。
この場合複数の引数は指定できないので、複数のオプションを扱いたい場合はオブジェクトや配列で渡す必要がある。
$ stylus -u plugin-name --with '{ foo: true }' path/to/source.styl
Stylus のトークンを表現するクラス
ここで記載する API はごく一部です。
より詳しいクラスの詳細はここらへんを見ると良さそう。
これらのコンストラクタを new
して Stylus のトークンを自前で生成することもできます。
トークンクラスへのアクセス
evaluator.renderer.nodes.*
で Stylus のトークンクラスにアクセスできる。
evaluator
は JavaScript API の define(name, fn)
で定義した関数内から this
で参照することができる。
const fn = function() { return new this.renderer.nodes.String('hello world'); }; module.exports = function() { return function(renderer) { renderer .define('hello-world', fn); }; };
以下、各クラスのコンストラクタの使い方、プロパティの型、コンソールに出力した結果です。
Expression
式を表すクラス。
nodes
内の要素が複数であれば tuple
もしくは list
となる。(要素が一つの場合を長さが1の tuple
と言うこともできる)
tuple
、list
の違いは isList
で判定できる。
constructor
Expression(isList)
isList
: boolean
instanceMethod
push(node)
:node
をthis.nodes
に追加するnode
: node
instanceProperty
nodes
: arrayisList
: boolean
{ lineno: 1, column: 14, filename: 'index.css', nodes: [ { lineno: 1, column: 14, filename: 'index.css', val: 'some string', string: 'some string', prefixed: false, quote: '\'' } ], isList: undefined }
Object
Stylus
のハッシュを表すクラス。
vals
には JS のオブジェクトが入っており、各キーにはさらに Stylus のトークンが入る。
constructor
Object()
instanceMethod
set(key, value)
:value
の値をkey
にセットするkey
: stringvalue
: node
instanceProperty
vals
: object
{ lineno: 1, column: 10, filename: 'index.css', vals: { foo: { lineno: 1, column: 19, filename: 'index.css', nodes: [Object], isList: undefined } } }
String
Stylus
の文字列を表すクラス。
constructor
String(val, quote)
val
: string
instanceProperty
string
: stringval
: string
{ lineno: 1, column: 14, filename: 'index.css', val: 'some string', string: 'some string', prefixed: false, quote: '\'' }
Unit
単位付き(あるいは無し)の数値を表すクラス。
10px
、1.7em
、200ms
、0
などは全て Unit
のインスタンスとなる。
constructor
Unit(val, type)
val
: numbertype
: string
instanceProperty
val
: numbertype
: stringraw
: string
{ lineno: 1, column: 10, filename: 'index.css', val: 20, type: 'px', raw: '20px' }
RGBA
RGBA形式の色を表現するクラス。
#112233
, #11223344
, rgb(11,22,33)
, rgba(11,22,33,44)
などは全て RGBA
のインスタンスとなる。
16進数表記の時のみ raw
プロパティが存在し、16進数表記の文字列が取得できる。
constructor
RGBA(r, g, b, a)
r
: numberg
: numberb
: numbera
: number
instanceProperty
r
: numberg
: numberb
: numbera
: numberraw
: string
{ lineno: 2, column: 16, filename: 'index.css', r: 17, g: 34, b: 51, a: 0.26666666666666666, name: '', rgba: [Circular], raw: '#11223344' }
HSLA
HSLA形式の色を表現するクラス。
hsl(11,22,33)
, hsla(11,22,33,44)
などは全て HSLA
のインスタンスとなる。
constructor
HSLA(h, s, l, a)
h
: numbers
: numberl
: numbera
: number
instanceProperty
h
: numbers
: numberl
: numbera
: number
{ lineno: 3, column: 12, filename: 'index.css', h: 11, s: 22, l: 33, a: 0.4, hsla: [Circular] }
Ident
auto
、inherit
、none
等のトークンを表現するクラス。
constructor
Ident(name, val, mixin)
name
: string
instanceProperty
name
: stringstring
: string
{ lineno: 1, column: 13, filename: 'index.css', name: 'auto', string: 'auto', val: null, mixin: false }
Call
cubic-bezier()
等の関数呼び出しを表現するクラス。
constructor
Call(name, args)
name
: stringargs
: expression
instanceProperty
name
: stringargs
: expression
{ lineno: 1, column: 40, filename: 'index.css', name: 'cubic-bezier', args: { lineno: 1, column: 20, filename: 'index.css', nodes: [ [Object], [Object], [Object], [Object] ], isList: undefined, map: {} } }
テスト
記事の公開時点では Stylus v0.54.5 を対象にテストしています。
all-user/inspect-stylus
Stylus の変数に JS からアクセスする方法を考える
transition
、animation
が使えるようになり、擬似セレクタの種類も増えてきたことで、スタイル周りの多くのことがCSSだけ(あるいはclassList
のadd
、remove
程度の操作)で書けるようになってきた気がする。
それでも、どうしても JavaScript で操作したい時もまだあって、 Stylus で利用している変数を JavaScript 内でも参照したくなることがある。
いくつか方法を試した結果、結局 Stylus のプラグインを自作することにした。
Atom + pigments
やりたかったことをまとめると、
- Stylus の変数定義ファイルが存在すること
- JS からは
"20px"
のような文字列ではなく、可能な限り20
のように使用しやすい形で取得できること - 実際に変数の定義を書く場所は JS でも Stylus でも良い
- JS 側、 Stylus 側共にコーディングの制約が少ないこと
という感じです。
Stylus の変数定義ファイルが欲しい理由は、
この pigments というプラグインが Stylus の構文をある程度解釈してくれるので、変数にもハイライトが効くようになる。これが大変便利。
(ここでいう変数定義ファイルとはdefine(key, value)
ではなく、key = value
の構文で代入しているコード)
検討した他の手段
調べてみると既にいろんな方法で同じような試みがあった。
rosetta
Stylusだけでなく、Sass、LESSで使うことも出来る。 JS => Stylus でも Stylus => JSでもなく、.rose という拡張子の rosetta ファイルから JS と Stylus(もしくは Sass、LESS)を書き出す仕組みになっている。 最初 README を読んだ時、Stylus で変数定義して rosetta を require すればあら不思議、Stylus の変数が使えちゃう、みたいなノリで書いてあるから勘違いしたがどうやら違う。
それでも rosetta ファイルが Stylus と同じ様に書けるならまあ問題ないかと思い、実際に rosetta ファイルを作ってコンパイルしてみたが、コンパイルエラーになった。
rosetta ファイルは Stylus とシンタックスは同じと書いてあるが、変数名の-
や、タプルの代入などはシンタックスエラーになる。
ということは、つまり Stylus の ファイルをそのまま使えないということになるし、define メソッドなどで定義されたものも共有出来ない。
既存の Stylus が流用できないのは面倒くさそうなので使うのはやめた。
stylus-export-loader
webpack 用ではあるがやりたいことには概ねマッチしている感じがする。 なぜか npm に登録されてないので github リポジトリを package.json に書いてインストールする。
変数定義用の Stylus ファイルを用意しておき、 !stylus-export-loader!
+ そのパスを webpack.ProvidePlugin
経由で読み込むことで任意のネームスペースから Stylus の変数にアクセスできるようになる(と思ったけど結局うまく動かなかったよ。)
stylus-vars, stylus-var
どちらも Stylus の変数を JS で定義出来るよーというもの。 Stylus の変数定義ファイルが生成されないのでこれも見送った。
そこで foovar ですよ
foovar を使うと Stylus の変数を JS ファイルに書き出す事ができる。
Stylus の実行時に変数スコープを調べるので、Stylus 側では制約無く変数を定義して使うことが出来る。
変数定義ファイルも用意出来る。
これでやりたかったことは全部できるようになった。
インストール
$ npm i -D foovar
使い方
CLI の場合
$ stylus -u foovar path/to/file.styl
webpack + stylus-loader の場合
// webpack.config.js module.exports = { stylus: { use: [require('foovar')()] } };
Stylus ファイルの任意の場所で foovar(path)
を呼び出す。
その時点で定義されている変数が JS ファイルにエクスポートされる。
foo = 10px foovar('src/StyleDefinitions.js')
エクスポートされた JS は require
して使うことが出来る。
const StyleDefinitions = require('./src/StyleDefinitions.js'); StyleDefinitions.foo(); // 10 StyleDefinitions.foo.type; // 'px' StyleDefinitions.foo.css; // '10px'
エクスポートできる変数のタイプ
string
、 unit
、 ident
、 rgba
、 hsla
、 cubic-bezier
、 tuple
、 list
、 hash
を書き出すことが出来る。
unit
は px
、 %
、em
、 ms
、 mm
…等の単位付き(あるいは無し)の数値のことで、 varName.type
はその単位を返す(無しの場合は undefined
)。
Stylus:$var-name |
JS:varName() |
JS:varName.type |
JS:varName.css |
---|---|---|---|
'some text' |
'some text' |
'string' |
'some text' |
20px |
20 |
'px' |
'20px' |
50% |
50 |
'%' |
'50%' |
255 |
255 |
undefined |
'255' |
auto |
'auto' |
'ident' |
'auto' |
#112233 |
[17,34,51,1] |
'rgba' |
'#112233' |
#11223344 |
[17,34,51,0.26666666666666666] |
'rgba' |
'#11223344' |
rgba(11,22,33,.4) |
[11,22,33,0.4] |
'rgba' |
'rgba(11,22,33,0.4)' |
hsl(11,22%,33%) |
[11,22,33,1] |
'hsla' |
'hsla(11,22%,33%,1)' |
hsla(11,22%,33%,.4) |
[11,22,33,0.4] |
'hsla' |
'hsla(11,22%,33%,0.4)' |
cubic-bezier(1,0,1,0) |
[1,0,1,0] |
'cubic-bezier' |
'cubic-bezier(1,0,1,0)' |
10px 20px 30px 40px |
[FoovarValue instance x 4] |
'tuple' |
undefined |
1em, 2em, 3em, 4em |
[FoovarValue instance x 4] |
'list' |
undefined |
{ foo: 1em } |
{ foo: FoovarValue instance } |
'hash' |
undefined |
tuple
、 list
、 hash
に関しては、配列・オブジェクトの各要素がそれぞれ type
、 css
を持つ FoovarValue
のインスタンスになっている。
各要素は単項の値と同じように type
、 css
へアクセス出来る。
// foo = 10px 20px 30px 40px // bar = { baz: 1em } const StyleDefinitions = require('./src/StyleDefinitions.js'); StyleDefinitions.foo()[0]() // 10 StyleDefinitions.foo()[1].type // 'px' StyleDefinitions.foo()[2].css // '30px' StyleDefinitions.bar().baz() // 1 StyleDefinitions.bar().baz.type // 'em' StyleDefinitions.bar().baz.css // '1em'
普段よく自分がアクセスしたくなりやすいものを中心に実装した。URL とかはまだ対応していない。
いずれ欲しくなったときに追加するかも知れないししないかもしれない。
Stylus の変数にアクセスしたくなった時は foovar をぜひに。
swift_combination を Swift 3.0 に対応させました
Xcode 8.0 がリリースされていたので早速インストールして Swift 3.0 を試してみた。
拙作のライブラリ swift_combination を開いてみると既存のコードを Swift 3.0 のシンタックスにコンバートするか聞かれたので、とりあえずつっこんでみる。
下のコードは配列の長さを指定して 0
で埋めるという処理だが、微妙に API の名前が変わってたりした。
var indices = [Int](count: length, repeatedValue: 0) // before var indices = [Int](repeating: 0, count: length) // after
もう一箇所は for in
を使ってる箇所で enumerate
が enumerated
に変更されている。
for (i, slct) in unselected.enumerate() { // before ... } for (i, slct) in unselected.enumerated() { // after ... }
細かい API の変更は全部これで直してくれるっぽい。
手動で直した箇所
ビルドしてみると以下の2点で警告が出た。
第一引数をラベル無し引数で渡す場合は明示的に指定する必要がある
Swift 3.0 では基本的に引数のラベルは省略しない事になったらしい。
let combos = combination([0, 1, 2], length: 2) // before let combos = combination(arr: [0, 1, 2], length: 2) // after
ライブラリはこれに合わせて全部ラベル付きで呼び出すように修正したけど、ラベルを省略したコードをそのまま動かしたい場合は、関数定義の引数ラベルの前に _
を付ければ一応オッケーらしい。
public func combination<T>(arr:[T], length:Int? = nil) -> [[T]] { // before ... } public func combination<T>(_ arr:[T], length:Int? = nil) -> [[T]] { // after ... }
関数の戻り値を使用しない場合は明示的に指定する必要がある
関数定義の方で戻り値を指定している場合、それを使わないと警告が出る。
_ = myFunc()
のように _
への代入を書くか、関数定義の上に @discardableResult
を付けることで警告を消すことができた。
_combination(arr: arr, length: _len){ ret.append($0) } // 警告が出る _ = _combination(arr: arr, length: _len){ ret.append($0) } // 警告が消える @discardableResult // これがあれば戻り値を破棄出来る internal func _combination<T>(arr:[T], length:Int, process:([T]) -> ()) -> [T] { ... }
とりあえず @discardableResult
を関数定義に付ける方向で対応することにした。