postcss-params
has two usage modes:
- Target devices/clients based on a build configuration, much like media queries.
- 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
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
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|
- As a hack,
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:
media-type
is not meant for general-purpose use. It accepts specific values, e.g.,all
,screen
,print
. We are only interested inmedia-feature
usage.NOT
must be used withmedia-type
. (NOT (media-feature)
is illegal).ONLY
was a hack for older browsers.
So we have defined a similar grammar which:
- removes
media-type
entirely. - allows
NOT
to be juxtaposed withmedia-feature
. - 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