Parse media-query-like params into PostCSS plugins
npm install 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.
---
buildComparatorbuildComparator 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, andfalse 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"
}
---
buildAstbuildAst 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 |
---
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.
---
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.
---
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 }
---
CommaSeparatedList
: MediaQuery [ COMMA MediaQuery ]*
MediaQuery
: [NOT]? Feature [ AND Feature ]*
Feature
: L_PAREN IDENT [ COLON IDENT ]? R_PAREN