UNPKG

postcss-params

Version:

Parse media-query-like params into PostCSS plugins

260 lines (190 loc) 7.78 kB
[![npm version](https://img.shields.io/npm/v/postcss-params.svg)](https://www.npmjs.com/package/postcss-params) [![Build Status](https://img.shields.io/travis/joeframbach/postcss-params.svg)](https://travis-ci.org/joeframbach/postcss-params) [![Coverage Status](https://img.shields.io/coveralls/joeframbach/postcss-params.svg)](https://coveralls.io/github/joeframbach/postcss-params?branch=master) [![Code Climate](https://img.shields.io/codeclimate/github/joeframbach/postcss-params.svg)](https://codeclimate.com/github/joeframbach/postcss-params) [![dependencies Status](https://img.shields.io/david/joeframbach/postcss-params.svg)](https://david-dm.org/joeframbach/postcss-params) [![devDependencies Status](https://img.shields.io/david/dev/joeframbach/postcss-params.svg)](https://david-dm.org/joeframbach/postcss-params?type=dev) # PostCSS Params `postcss-params` has two usage modes: 1. Target devices/clients based on a build configuration, much like media queries. 2. Pass strings from css to your postcss plugin, using a familiar syntax. Some sites serve different assets to different clients. For example, you may have some IE-specific css hacks that you only want to serve to IE browsers. Or you want to load certain fonts for certain countries. PostCSS is a good way to keep all your code in one file, then generate separate assets. @my-plugin (browser: ie) { button { background-color: red; } } @my-plugin not (browser: ie) { button { background-color: green; } } `postcss-params` helps you write a plugin which reads the `(browser: ie)` parameter string, and keep or discard the block accordingly. Build two assets, with configurations `{browser: ie}` and `{}`. PostCSS will generate two assets. One you can serve to your locked-in customers browsing from their lunch breaks at BigCorp. The other asset you can serve to the rest of the civilized world. --- ## `buildComparator` `buildComparator` accepts a param string and returns a function. The resulting *comparator* function accepts a configuration object, and returns `true` if the params match the configuration, and `false` otherwise. See the tests in `tests/buildComparator` for more examples. CSS: @my-plugin (region: cn) { body { font-family: ".PingFang-SC-Regular", sans-serif; } } @my-plugin not (region: cn) { body { font-family: "Helvetica Neue", Arial, sans-serif !default; } } Plugin: const { buildComparator } = require('postcss-params'); postcss.plugin('my-plugin', (configuration) => (root) => { root.walkAtRules('my-plugin', (atRule) => { const comparator = buildComparator(atRule.params); if (comparator(configuration)) { atRule.replaceWith(atRule.nodes); } else { atRule.remove(); } }); }); Running postcss with various configuration objects will result in css assets suitable for separate intended audiences. For example, you may serve a different font-family in China, but not want to load this asset for all countries. `configuration` is provided to PostCSS through your build tool. Example configuration: { debug: true, region: "us", theme: "blue" } --- ## `buildAst` `buildAst` gives you finer control and access to the params written in css. Given this simple rule: @my-plugin (theme: red) { body { background-color: theme-color; } } and this plugin: const { buildAst } = require('postcss-params'); postcss.plugin('my-plugin', (configuration) => (root) => { root.walkAtRules('my-plugin', (atRule) => { const ast = buildAst(atRule.params); console.log(ast); }); }); This AST is generated: { feature: "theme", value: "red" } --- Given this more complicated rule: @my-plugin (debug), (region: cn) and (theme: red), (region: us) and (theme: blue), not (production) and (staging) { body { background-color: red; } } Plugin: const { buildAst } = require('postcss-params'); postcss.plugin('my-plugin', (configuration) => (root) => { root.walkAtRules('my-plugin', (atRule) => { const ast = buildAst(atRule.params); console.log(ast); }); }); This AST is generated: any ├─ { feature: debug, value: true } ├─ all │ ├─ { feature: region, value: cn } │ └─ { feature: theme, value: red } ├─ all │ ├─ { feature: region, value: us } │ └─ { feature: theme, value: blue } └─ all ├─ { feature: production, not: true } └─ { feature: staging, not: true } Now your PostCSS plugin can make use of this AST to pull values into variables, or decide to keep or discard the block. Limitations: * Feature names and values must NOT contain characters `(),:` * Errors are fairly opaque (`'Expected L_PAREN'`) * Feature values are optional, so comparators must expect String or Undefined * Feature can only have one value. `(f: a), (f: b)` can't be `(f: a, b)` - As a hack, `(f: a|b)` is legal, and the comparator can split on `|` --- ## AST Object Reference The resulting AST from `buildAst` is a tree structure. There are three possible nodes in this tree: - `any`: if any item resolves true, return true. - `all`: if all items resolve true, return true. - `feature`: compare the param value with the config value. - `not`: if feature returns true, return false. And vice-versa. Given this contrived rule: @my-plugin (debug), (region: cn) and (theme: red), (region: us) and (theme: blue), not (production) and (staging) { body { background-color: red; } } This AST is generated: any ├─ { feature: debug, value: true } ├─ all │ ├─ { feature: region, value: cn } │ └─ { feature: theme, value: red } ├─ all │ ├─ { feature: region, value: us } │ └─ { feature: theme, value: blue } └─ all ├─ { feature: production, not: true } └─ { feature: staging, not: true } See the tests in `tests/buildAst` for more examples. --- ## About the syntax All PostCSS at-rules follow the structure `@plugin-name params { body }`. Plugins are given the `params` as a string, with no provisions for parsing, or even a standard format for clients to write params. CSS already has an analogous structure for media queries: - `@media media-query [, media-query]* { rule-list }` where `media-query` can take either form: - `[NOT|ONLY]? media-type [AND (media-feature[: value]?)]*` - `(media-feature[: value]?) [AND (media-feature[: value]?)]*` However, this has some limitations: 1. `media-type` is not meant for general-purpose use. It accepts specific values, e.g., `all`, `screen`, `print`. We are only interested in `media-feature` usage. 2. `NOT` must be used with `media-type`. (`NOT (media-feature)` is illegal). 3. `ONLY` was a hack for older browsers. So we have defined a similar grammar which: 1. removes `media-type` entirely. 2. allows `NOT` to be juxtaposed with `media-feature`. 3. removes `ONLY`. --- ## ASTs are automatically flattened `any` and `all` nodes with a single child are replaced with that child node. any └─ all └─ { feature: debug, value: true } is the same as { feature: debug, value: true } --- ## LL(1) Grammar Reference - Please do not LL(2+) CommaSeparatedList : MediaQuery [ COMMA MediaQuery ]* MediaQuery : [NOT]? Feature [ AND Feature ]* Feature : L_PAREN IDENT [ COLON IDENT ]? R_PAREN