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 をぜひに。
flex-grow, flex-shrink, flex-basis について
flex-grow CSS プロパティは、flex アイテムの flex grow factor を指定します。これは、アイテムが flex コンテナ内のスペースをどれだけ占有するかを指定します。
MDN: flex-grow
MDN の説明がシンプルすぎてよく分からなかったので、もう少し詳しく調べた内容をまとめておきます。
説明中に出てくるスクショの動作サンプルは下の記事にあります。
この記事は上の動作サンプル記事が長くなっため分離した補足記事みたいな感じです。
flexbox
の基本
flexbox の基本的な動作は以下のページが個人的には分かりやすかったのでオススメです。
- CSS3 Flexbox の各プロパティの使い方をヴィジュアルで詳しく解説
flexbox の仕様全般に関する説明が分かりやすい - Flexboxを使うなら知っておきたい「flexアイテム」の幅の計算方法
flex-grow に関する説明が分かりやすい
flex-grow
ある flex アイテムが、 flex コンテナ内の他の flex アイテムと比較して、どのくらい大きくなろうとするかを整数値で指定する。
W3C の flex grow
の説明にはこうある。
This
component sets flex-grow longhand and specifies the flex grow factor, which determines how much the flex item will grow relative to the rest of the flex items in the flex container when positive free space is distributed. When omitted, it is set to 1.
W3C: flex-grow
意味としては、
この数値は、ポジティブなフリースペースが分配された時に、 flex アイテムが flex コンテナ内の他の flex アイテムと比較してどれくらい増大されるかを決定します。
ということだと思う。
flex-grow
はあくまでも余ったスペースを各アイテムで分け合う。
分配されるのは領域全体ではなく、余ったスペース(以下ポジティヴなフリースペース)。
ある領域を1:2:3
の比率で分割したいとして、単純に flex-grow
をそれぞれ1
,2
,3
と指定しても上手くいかない。
上は width
を 60px
, 120px
, 180px
に指定した display: block
の要素。
下は flex-grow
を 1
, 2
, 3
に指定した display: flex
の要素。
上と下を見比べると、下の比率が1:2:3
になっていないことが分かる。
下の図について細く見ていくと、余ったスペースが各アイテムに対してどのように分配されているのかが分かる。
flex コンテナの width
は360px
。
flex アイテムの width
は auto
、 flex アイテム内のコンテンツは40px
の span
要素なので、 flex アイテムの幅は40px
となる。
flex-grow
が分け合うポジティヴなフリースペースとは、 flex コンテナから flex アイテムの幅を引いた値。360px - (40px + 40px + 40px) = 240px
のことを指す。
この240px
を flex-grow
の値1:2:3
の比率で分配し、 width
に足した合わせた値が最終的な幅となる。
(40 + 1/6 * 240) + (40 + 2/6 * 240) + (40 + 3/6 * 240)
80 + 120 + 160
この例では比率が2:3:4
になる。
flex-shrink
ある flex アイテムが、 flex コンテナ内の他の flex アイテムと比較して、どのくらい小さくなろうとするかを整数値で指定する。
W3C の flex-shrink
の説明にはこうある。
This
component sets flex-shrink longhand and specifies the flex shrink factor, which determines how much the flex item will shrink relative to the rest of the flex items in the flex container when negative free space is distributed.
W3C: flex-shrink
意味としては、
この数値は、ネガティブなフリースペースが分配された時に、 flex アイテムが flex コンテナ内の他の flex アイテムと比較してどれくらい縮小されるかを決定します。
ということだと思う。
ネガティヴなフリースペースというが分かりにくい。
flex コンテナの幅と flex アイテムの幅の差がポジティヴかネガティヴかというふうに考えると分かりやすいかもしれない。
flex-grow
の時と逆の状況を考える。
flex アイテムの width
を 160px
にして、 flex コンテナからはみ出すようにしてみる。
- flex アイテムを全て足した幅が flex コンテナより小さい
360px - (40px + 40px + 40px) = 240px
= ポジティブなフリースペース。 - flex アイテムを全て足した幅が flex コンテナより大きい
360px - (160px + 160px + 160px) = -120px
= ネガティヴなフリースペース
flex-shrink
はこのネガティヴなフリースペースをどう分配するか、ということを指定する。
下のスクショは、
上が display: block
+ float: left
、 width: 120px
の要素。
下が flex-shrink
を 1
,2
,3
に指定した display: flex
の要素となっている。
ネガティヴなフリースペース -120px
を、 flex-shrink
の値1:2:3
の比率で分配すると、
(160 + 1/6 * -120) + (160 + 2/6 * -120) + (160 + 3/6 * -120)
140 + 120 + 100
flex アイテムはそれぞれ140px
、120px
、100px
となり、比率は7:6:5
となる。
flex-basis
flex-basis
についてはまだ仕様が不安定な印象です。
とりあえず MDN の説明を引用。
flex-basis CSS プロパティは、flex アイテムの初期 main size である flex basis を指定します。box-sizing を使用して別に指定されていない限り、このプロパティが content-box の寸法を定義します。
MDN: flex-basis
要はベースとなる width
か height
を指定するということだと思う。
box-sizing
の影響を受けるので、 border
、 padding
を含めた値を指定したい場合は box-sizing
を border-box
にする。
(ただし、IEでは box-sizing
の指定に関するバグ有り。 Flexbugs: 7. flex-basis doesn't account for box-sizing:border-box)
指定出来る値は width
、 height
に設定出来る値であれば何でも良い。
あるいは、 content
を指定する。
content
結論から書くと、動作がブラウザによってバラバラなので、現段階では使わないほうが良さそうです(2016年9月4日)。
コンテンツサイズに応じてよしなにやってくれる的な設定なんじゃないかーぐらいのことしか分かりませんでした。
W3C の説明も
Indicates automatic sizing, based on the flex item’s content.
と一行だけです。
実際の動作サンプルについては別記事に追記があります。
メモを揉め: Case Studies in Flexbox - flex-grow, flex-shrink, flex-basis
ブラウザ間の差異の原因になっている(2016年9月4日現在)
flex-basis
は Chrome 、 Firefox 、 Edge グループと Safari 、 IE11 グループとで動作が異なる。
2017年8月17日現在は IE11 のみ動作が異なる。
Chrome 、 Firefox 、 Edge はコンテンツのサイズを維持。
Safari 、 IE11 はコンテンツサイズを無視する。
Chrome 、 Firefox 、 Safari 、 Edge はコンテンツのサイズを維持。
IE11 はコンテンツサイズを無視する。
こちらも詳しくは別記事に記述があります。
メモを揉め: Case Studies in Flexbox - flex-grow, flex-shrink, flex-basis
その他の考慮すべきこと
ここまでの flex-grow
、 flex-shrink
の動作は、余った、あるいは足りないスペースを flex-grow
、 flex-shrink
に基いて分配するという、ある意味シンプルなルールです。
しかし、場合によっては様々な要因によって結果が予想しにくくなることがあります。
再計算が必要になる場合
フリースペースの分配が行われた後、何らかの原因によって再びネガティブスペースを作ってしまった場合、再計算が行われます。
0以下になる場合
flex
コンテナの幅が 360px
、
flex
アイテムの flex-shrink
が 1
、 2
、 3
、
width
が 400px
、
min-wditn
が 0
に設定されていた場合、下のスクショの様な結果になる。
順を追って見ていくと、
flex
コンテナからflex
アイテム3つの合計を引く
360 - (400 + 400 + 400) = -840
- ネガティブなフリースペース
-840px
をflex-shrink
に基いて分配する
(400 - 1/6 * 840) + (400 - 2/6 * 840) + (400 - 3/6 * 840)
- 3つ目の
flex
アイテムが0
を下回ってしまう
(260) + (120) + (-20)
- 再び生まれたネガティブなフリースペース
-20px
の再分配が行われる
(260 - 1/3 * 20) + (120 - 2/3 * 20) + (0)
- 最終的なサイズが決定する
253.3333... + 106.6666... + 0
コンテンツサイズを下回る場合
Chrome 、 Firefox 、 Edge の flex-basis
の挙動により起こる現象。
flex
コンテナの幅が 360px
、
flex
アイテムの flex-grow
が 1
、 5
、 6
、
flex-basis
が 0
に設定されていた場合、下のスクショの様な結果になる。
順を追って見ていくと、
flex
コンテナからflex
アイテム3つの合計を引く
360 - (0 + 0 + 0) = 360
- ポジティブなフリースペース
360px
をflex-grow
に基いて分配する
(0 + 1/12 * 360) + (0 + 5/12 * 360) + (0 + 6/12 * 360)
- 1つ目の
flex
アイテムがコンテンツサイズの40px
を下回ってしまう
(30) + (150) + (180)
- 1つ目の
40px
を除いた320px
が再びポジティブなフリースペースとして再分配される(40) + (0 + 5/11 * 320) + (180 + 6/11 * 320)
- 最終的なサイズが決定する
40 + 145.4545... + 174.5454...
margin, paddingを下回る場合
margin
、 padding
は flex-grow
、 flex-shrink
によって増大も縮小もしないため、フリースペースの分配結果がこれらを下回る場合は再分配が行われる。
flex
コンテナの幅が 360px
、
flex
アイテムの flex-shrink
が 1
、 2
、 3
、
width
が 160px
、
左右の padding
が 40px
、
min-width
が 0
に設定されていた場合、下のスクショの様な結果になる。
flex
コンテナからflex
アイテム3つの合計を引く
360 - (240 + 240 + 240) = -360
- ポジティブなフリースペース
360px
をflex-grow
に基いて分配する
(240 - 1/6 * 360) + (240 - 2/6 * 360) + (240 - 3/6 * 360)
- 3つ目の
flex
アイテムが左右のpadding
の合計80px
を下回ってしまう
(180) + (120) + (60)
- 再び生まれたネガティブなフリースペース
-20px
の再分配が行われる
(180 - 1/3 * 20) + (120 - 2/3 * 20) + (80)
- 最終的なサイズが決定する
173.3333... + 106.6666... + 80
それでも解決しない場合
再分配しても以下の様に、
0
を下回るmin-width
を下回る- margin, paddingを下回る
- コンテンツサイズを下回る
に当てはまる場合は、サイズが下回らない様にコンテナからはみ出し、分配はそこで終わりとなる。
flex
コンテナの幅が 360px
、
flex
アイテムの flex-shrink
が 1
、 2
、 3
、
width
が 0px
、
左右の padding
が 70px
に設定されていた場合、下のスクショの様な結果になる。
左右の padding
の合計 140px
より縮小することが出来ないため、 flex コンテナからはみ出る。