postcss

postcss

workflow

wokflow

PostCSS parses CSS to the tree of nodes (we call it AST). This tree may content:

  • Root: node of the top of the tree, which represent CSS file.
  • AtRule: statements begin with @ like @charset "UTF-8" or @media (screen) {}.
  • Rule: selector with declaration inside. For instance input, button {}.
  • Declaration: key-value pair like color: black;
  • Comment: stand-alone comment. Comments inside selectors, at-rule parameters and values are stored in node’s raws property.

Core Structures

  • Tokenizer lib/tokenize.js

    Tokenizer (aka Lexer) plays important role in syntax analysis.

    It accepts CSS string and returns a list of tokens.

    Token is a simple structure that describes some part of syntax like at-rule, comment or word. It can also contain positional information for more descriptive errors.

    For example, if we consider following CSS

    .className { color: #FFF; }
    

    corresponding tokens from PostCSS will be

    [
        ["word", ".className", 1, 1, 1, 10]
        ["space", " "]
        ["{", "{", 1, 12]
        ["space", " "]
        ["word", "color", 1, 14, 1, 18]
        [":", ":", 1, 19]
        ["space", " "]
        ["word", "#FFF" , 1, 21, 1, 23]
        [";", ";", 1, 24]
        ["space", " "]
        ["}", "}", 1, 26]
    ]
    

    As you can see from the example above a single token represented as a list and also space token doesn't have positional information.

    Let's look more closely on single token like word. As it was said each token represented as a list and follow such pattern.

    const token = [
         // represents token type
        'word',
    
        // represents matched word
        '.className',
    
        // This two numbers represent start position of token.
        // It is optional value as we saw in the example above,
        // tokens like `space` don't have such information.
    
        // Here the first number is line number and the second one is corresponding column.
        1, 1,
    
        // Next two numbers also optional and represent end position for multichar tokens like this one. Numbers follow same rule as was described above
        1, 10
    ]
    

    There are many patterns how tokenization could be done, PostCSS motto is performance and simplicity. Tokenization is a complex computing operation and takes a large amount of syntax analysis time ( ~90% ), that why PostCSS' Tokenizer looks dirty but it was optimized for speed. Any high-level constructs like classes could dramatically slow down tokenizer.

    PostCSS' Tokenizer uses some sort of streaming/chaining API where you expose nextToken() method to Parser. In this manner, we provide a clean interface for Parser and reduce memory usage by storing only a few tokens and not the whole list of tokens.

  • Parser lib/parse.js, lib/parser.js

    Parser is the main structure responsible for syntax analysis of incoming CSS. Parser produces a structure called Abstract Syntax Tree (AST) that could then be transformed by plugins later on.

    Parser works in common with Tokenizer and operates over tokens, not source string, as it would be a very inefficient operation.

    It uses mostly nextToken and back methods provided by Tokenizer for obtaining single or multiple tokens and then construct part of AST called Node.

    There are multiple Node types that PostCSS could produce but all of them inherit from base Node class.

  • Processor lib/processor.js

    Processor is a very plain structure that initializes plugins and runs syntax transformations

    It exposes only a few public API methods. Description of them could be found on API

  • Stringifier lib/stringify.js, lib/stringifier.js

    Stringifier is a base class that translates modified AST to pure CSS string. Stringifier traverses AST starting from provided Node and generates a raw string representation of it calling corresponding methods.

    The most essential method is Stringifier.stringify
    that accepts initial Node and semicolon indicator.
    You can learn more by checking stringifier.js

how to use

new plugin

module.exports = (opts = { }) => {

  // Work with options here

  return {
    postcssPlugin: 'PLUGIN_NAME',
    /*
    Root (root, postcss) {
      // Transform CSS AST here
    }
    */

    /*
    Declaration (decl, postcss) {
      // The faster way to find Declaration node
    }
    */

    /*
    Declaration: {
      color: (decl, postcss) {
        // The fastest way find Declaration node if you know property name
      }
    }
    */
  }
}
module.exports.postcss = true

use plugin

await postcss([plugin]).process('a { color: black }', { from })

源码阅读

function postcss (...plugins) {
  if (plugins.length === 1 && Array.isArray(plugins[0])) {
    plugins = plugins[0]
  }
  return new Processor(plugins, postcss)
}


class Processor {
  constructor (plugins = []) {
    this.version = '8.1.10'
    this.plugins = this.normalize(plugins)
  }

  normalize (plugins) {
    let normalized = []

    ......

    // 添加插件
    normalized.push(i)

    ......

    return normalized
  }

  .....

  process (css, opts = {}) {
    ......
    return new LazyResult(this, css, opts)
  }

}

class LazyResult {
  constructor (processor, css, opts) {
    this.stringified = false
    this.processed = false

    let root

    ......

    // parser css 
    root = parser(css, opts)

    ......

    this.result = new Result(processor, root, opts)
    this.helpers = { ...postcss, result: this.result, postcss }

    // 获取plugin代码
    this.plugins = this.processor.plugins.map(plugin => {
      if (typeof plugin === 'object' && plugin.prepare) {
        return { ...plugin, ...plugin.prepare(this.result) }
      } else {
        return plugin
      }
    })
  }


  then (onFulfilled, onRejected) {
    .... 

    // 执行
    return this.async().then(onFulfilled, onRejected)
  }

  async () {
    if (this.error) return Promise.reject(this.error)
    if (this.processed) return Promise.resolve(this.result)
    if (!this.processing) {
      this.processing = this.runAsync()
    }
    return this.processing
  }

  stringify () {
    if (this.error) throw this.error
    if (this.stringified) return this.result
    this.stringified = true

    this.sync()

    let opts = this.result.opts
    let str = stringify
    if (opts.syntax) str = opts.syntax.stringify
    if (opts.stringifier) str = opts.stringifier
    if (str.stringify) str = str.stringify

    let map = new MapGenerator(str, this.result.root, this.result.opts)
    let data = map.generate()
    this.result.css = data[0]
    this.result.map = data[1]

    return this.result
  }


  async runAsync () {
    this.plugin = 0
    for (let i = 0; i < this.plugins.length; i++) {
      let plugin = this.plugins[i]
      
      // 执行插件代码
      let promise = this.runOnRoot(plugin)
      if (isPromise(promise)) {
        try {
          await promise
        } catch (error) {
          throw this.handleError(error)
        }
      }
    }

    this.prepareVisitors()

    this.processed = true

    // 返回结果
    return this.stringify()
  }

  ......
}


function parse (css, opts) {
  let input = new Input(css, opts)
  let parser = new Parser(input)

  ......
  //css 解析
  parser.parse()

  //第一步解析为token
  //第二步根据token来解析css
 
  ......

  return parser.root
}

autoprefixer

PostCSS plugin to parse CSS and add vendor prefixes to CSS rules using values from Can I Use.

源代码


module.exports = (...reqs) => {

  ......

  // 返回 postcss 插件类型
  return {
    postcssPlugin: 'autoprefixer',

    prepare (result) {
      let prefixes = loadPrefixes({
        from: result.opts.from,
        env: options.env
      })

      return {
        Once (root) {
          timeCapsule(result, prefixes)
          if (options.remove !== false) {
            prefixes.processor.remove(root, result)
          }
          if (options.add !== false) {
            prefixes.processor.add(root, result)
          }
        }
      }
    },

    info (opts) {
      opts = opts || {}
      opts.from = opts.from || process.cwd()
      return info(loadPrefixes(opts))
    },

    options,
    browsers: reqs
  }
}
posted @ 2020-11-26 12:02  S&L·chuck  阅读(184)  评论(0编辑  收藏  举报