Skip to content

joeframbach/postcss-params

Repository files navigation

npm version Build Status Coverage Status Code Climate dependencies Status devDependencies Status

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

About

Parse media-query-like params into PostCSS plugins

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published