Case studies: Two color management plugins for PostCSS

Vladimir Kuznetsov, Huntflow

Case studies: Two color management plugins for PostCSS

Vladimir Kuznetsov, Huntflow

CSS-Minsk-JS, 2018

PostCSS is a tool for transforming CSS with JavaScript

PostCSS converts CSS into an AST

PostCSS converts CSS into an AST and back

PostCSS plugins transform the AST

PostCSS plugins transform the AST

Autoprefixer

PostCSS plugins

  • cssnext
  • postcss-custom-properties
  • pixrem
  • postcss-unmq
  • postcss-hash-classname
  • postcss-namespace
  • postcss-assets-rebase
  • postcss-cachebuster
  • postcss-svg
  • postcss-at2x
  • postcss-bem-to-js
  • postcss-click

PostCSS plugins

  • postcss-circle
  • postcss-triangle
  • postcss-clearfix
  • postcss-focus
  • postcss-aspect-ratio
  • postcss-zindex
  • postcss-conditionals
  • postcss-define-property
  • postcss-each
  • postcss-for
  • stylelint

It might happen that an existing plugin works not as you’re expecting.
What can you do?

Сомнения

My first PostCSS plugin

postcss-unwrap-at-media

/* Input */
.block { width: 100%; }
@media (min-width: 720px) {
  .block { width: 25%; }
}
/* Output */
.block { width: 100%; }
.block { width: 25%; }

Case #1

postcss-alter-color

{ from: 'darkslategray', to: '#556832' }
p { color: darkslategray; }
a { color: #2F4F4F; }
.quote {
  border-left: 1px solid rgb(47, 79, 79);
  border-right: solid hsl(180, 25%, 25%) 1px;
}

postcss-alter-color

{ from: 'darkslategray', to: '#556832' }
p { color: darkslategray; }
a { color: #2F4F4F; }
.quote {
  border-left: 1px solid rgb(47, 79, 79);
  border-right: solid hsl(180, 25%, 25%) 1px;
}

postcss-alter-color

{ from: 'darkslategray', to: '#556832' }
p { color: #556832; }
a { color: #556832; }
.quote {
  border-left: 1px solid #556832;
  border-right: solid #556832 1px;
}

api.postcss.org

Abstract Syntax Tree

The types of the nodes

  • rule (div, .list)
  • declaration (color, background)
  • @rule (media, charset)
  • comment

Traversal methods

  • walkRules()
  • walkDecls()
  • walkAtRules()
  • walkComments()
  • walk()
        #main, div.sidebar {
          background: white; color: rgb(47, 79, 79);
        }
    
        { type: "root", nodes: [
          { type: "rule", selector: "#main, div.sidebar", nodes: [
            { type: "decl", prop: "background", value: "white" },
            { type: "decl", prop: "color", value: "rgb(47, 79, 79)" }
          ] }
        ] }
    
        function plugin(options) {
          return function (root) {
            root.walkDecls(function (decl) {
              if (decl.value === options.from)
                decl.value = options.to;
            });
          };
        }
        module.exports = postcss.plugin('alter-color', plugin);
    

The ways to define a color

Solid color

  • white
  • #333
  • #556832
  • rgb(47, 79, 79)
  • hsl(180, 25%, 25%)

parse-color

{ rgb: [ 255, 165, 0 ],
  hsl: [ 39, 100, 50 ],
  keyword: 'orange',
  hex: '#ffa500',
  rgba: [ 255, 165, 0, 1 ],
  hsla: [ 39, 100, 50, 1 ] }

Parsing a decl

#main { border: 1px solid #2F4F4F }
{
  type: "decl",
  prop: "border",
  value: "1px solid #2F4F4F"
}
Idea!

CSSTree

        root.walkDecls(decl => {
          const parsedValue = cssTree.parse(
            decl.value,
            { context: 'value' }
          );
          // you can modify parsedValue here
          decl.value = cssTree.generate(parsedValue);
        });
    
A spinning top
"1px solid #2F4F4F"
        { type: "Value", children: [
          { type: "Dimension", value: 1, unit: "px" },
          { type: "WhiteSpace", value: " " },
          { type: "Identifier", name: "solid" },
          { type: "WhiteSpace", value: " " },
          { type: "HexColor", value: "2F4F4F" }
        ] }
    
"darkslategray"
        { type: "Value", children: [
          { type: "Identifier", name: "darkslategray" }
        ] }
    
rgb(47, 79, 79)
        { type: "Value", children: [
          { type: "Function", name: "rgb", "children": [
            { type: "Number", value: "47"},
            { type: "Operator", value: ","},
            { type: "Number", value: "79"},
            { type: "Operator", value: ","},
            { type: "Number", value: "79"}
          ] }
        ] }
    

The types of the CSSTree nodes,
which can contain color

CSSTree AST traversal

        function identifierVisitor(node, item, list) {
          if (node.name === 'darkslategray') {
            // modify the node
          }
        }
        cssTree.walk(parsedValue, {
          visit: 'Identifier',
          enter: identifierVisitor
        });
    
        function identifierVisitor(node, item, list) {
          if (node.name === 'darkslategray') {
            const data = cssTree.fromPlainObject({
              type: 'HexColor',
              value: '556832'
            });
            list.replace(item, list.createItem(data));
          }
        }
    
        cssTree.walk(parsedValue, {
          visit: 'HexColor',
          enter: hexColorVisitor
        });
        cssTree.walk(parsedValue, {
          visit: 'Function',
          enter: functionVisitor
        });
    

rgb(), rgba(), hsl() and hsla()

        { type: "Value", children: [
          { type: "Function", name: "rgb", "children": [
            { type: "Number", value: "47"},
            { type: "Operator", value: ","},
            { type: "Number", value: "79"},
            { type: "Operator", value: ","},
            { type: "Number", value: "79"}
          ] }
        ] }
    
        { type: "Value", children: [
          { type: "Function", name: "rgb", "children": [
            { type: "Percentage", value: "18.4%"},
            { type: "Operator", value: ","},
            { type: "Percentage", value: "30.98%"},
            { type: "Operator", value: ","},
            { type: "Percentage", value: "30.98%"}
          ] }
        ] }
    
Blowing head

postcss-alter-color

Takeaway

  1. Learned a bit about AST explorer.

Case #2

postcss-themes-generator

  Website Portal Business International
Primary        
Secondary        
Accent        

Theme “Website”

Theme “Student Portal”

Theme “International”

Theme “Business”

CSS custom properties

        :root                { --primary-color:  #E54096; }
        .theme-portal        { --primary-color:  #0080C5; }
        .theme-business      { --primary-color:  #C1D730; }
        .theme-international { --primary-color:  #E5352D; }
        .tile_primary { background: var(--primary-color); }
    

Possible solutions

Objectives

Configuration

[ { "name": "primary", "match": "#E54096",
  "themes": {
    "portal": "#0080C5",
    "business": "#C1D730",
    "international": "#E5352D"
  }
}, { name: "secondary", … } ]

Result

        .tile_primary { color:  #E54096 }
        .theme-portal .tile_primary { color:  #0080C5 }
        .theme-business .tile_primary { color:  #C1D730 }
        .theme-international .tile_primary { color:  #E5352D }
    
        const plugin = options => (root, result) => {
		  root.walkRules(rule => {
            const config = getConfig(rule, options);
            const themed = Object.keys(config.themes)
              .map(name => makeThemedRule(rule, name, config));
            themed.unshift(rule.clone());
            rule.replaceWith(themed);
          });
        };
    
        function getConfig(rule, options) {
          return options.find(config => {
            // .block_modifier or .block__element_modifier
            const p = `_${config.name}(:[a-zA-Z]+)?(\\s.*)?$`;
            const rx = new RegExp(p);
            return rule.selectors.every(s => rx.test(s));
          });
        }
    
        {
          "name": "primary",
          "match": "#E54096",
          "themes": {
            "portal": "#0080C5",
            "business": "#C1D730",
            "international": "#E5352D"
          }
        }
    
        const plugin = options => (root, result) => {
		  root.walkRules(rule => {
            const config = getConfig(rule, options);
            const themed = Object.keys(config.themes)
              .map(name => makeThemedRule(rule, name, config));
            themed.unshift(rule.clone());
            rule.replaceWith(themed);
          });
        };
    
        function makeThemedRule(rule, name, config) {
          const themedRule = rule.clone();
          themedRule.selectors = themedRule.selectors
            .map(selector => `.theme-${name} ${selector}`);
          themedRule.walkDecls(getDeclProcessor(name, config));
          return themedRule;
        }
    
        function getDeclProcessor(themeName, config) {
          return decl => {
            const ast = cssTree
              .parse(decl.value, { context: 'value' });
            cssTree.walk(ast, { visit: 'HexColor', … });
            cssTree.walk(ast, { visit: 'Function', … });
            decl.value = cssTree.generate(ast);
          };
        }
    

Suspicious rule

A rule contains modifier, but plugin can’t find any color to change.

rule.warn(result, 'Suspicious rule');
Suspect

Tricky selectors

        .tile_primary .text-tile__heading {
          color: #E54096;
        }
        .tile__badge_primary:hover {
          background-color: #E54096;
        }
    

Light and dark shades

        .button_primary[disabled] { 
          background: lighten( #E54096, 20%); /*  #F19AC7 */
        }
        .overlay_primary {
          background: darken( #E54096, 20%); /*  #A81763 */
        }
    

Light and dark shades

Dark Base Light
hsl(329, 76%, 37%) hsl(329, 76%, 57%) hsl(329, 76%, 77%)
hsl(201, 100%, 19%) hsl(201, 100%, 39%) hsl(201, 100%, 59%)
hsl(68, 68%, 32%) hsl(68, 68%, 52%) hsl(68, 68%, 72%)
hsl(3, 78%, 34%) hsl(3, 78%, 54%) hsl(3, 78%, 74%)

In HSL color space, hue and saturation components remains the same, and the difference between the lightness components we can add to the base color.

#F19AC7 hsl(329, 76%, 77%) hsl(329, 76%, 57%) → Δl = 20%

#0080C5 hsl(201, 100%, 39%) hsl(201, 100%, 59%) #2EB6FF

postcss-themes-generator

Takeaway

  1. The plugin helps the team keep CSS clean and tidy.

Abstract Syntax Tree

James Steinbach

Writing Your First PostCSS Plugin

Vladimir Kuznetsov

@mistakster (English)
@mista_k (больше про жизнь)

noteskeeper.ru

Slides: bit.ly/cmj18-cm