The framework CSS used in the work has the following problems, need to use postCSS to do some automatic handling.

  • The latter will override the former:.a{color: #fff} .a{background: #fff}, the latter takes effect
  • A maximum of two layers can be nested:.a .b .c {}Don’t take effect

Learn what PostCSS is and how to do some work with it.

Introduction to the

Postcss: PostCSS is a tool for transforming styles with JS plugins. These plugins can lint your CSS, Support variables and mixins, transpile Future CSS syntax, inline images, and more. postcss are tools that convert styles using JS plug-ins. These plug-ins can validate CSS, support variables and mixins, compile future CSS syntax, inline images, and more.

Its name, PostCSS, suggests its early use as a post-processor. That is, CSS compiled by less/sass. The most commonly used plug-in is Autoprefixer, which adds compatible prefixes based on the browser version.

Postcss, like Babel, converts style to AST, goes through a series of plug-in transformations, and then converts as T to generate a new style. As things have evolved, postcss is no longer an appropriate word for postprocessor. It is currently possible to convert less/sass code using postCSs-sass/postCSs-less (to convert less/sass to less/sass instead of directly to CSS), Precss can also be used instead of SASS (which feels immature).

Therefore, it is recommended to use postCSS and less/ sASS together. In webPack configuration, postCSs-loader must be written before sas-loader /less-loader.

module.exports = {
    module: {
        rules: [{test: /\.(css|less)$/i,
                use: ['style-loader'.'css-loader'.'postcss-loader'.'less-loader'],},]}}Copy the code

For more information on the uses of PostCSS, see github.com/postcss/pos…

The working process

General steps:

  • Make Tokens of CSS strings
  • Pass Tokens through rules to generate an AST tree
  • Pass the AST tree to the plug-in for processing
  • Generate new CSS resources from the processed AST tree (CSS strings, sourceMap, etc.)

CSS Input → Tokenizer → Parser → AST → Plugins → Stringifier Give 🌰 :

@import url('./default.css');
body {
  padding: 0;
  /* margin: 0; * /
}
Copy the code

1. input

'@import url('./default.css'); \nbody {\n padding: 0; \n /* margin: 0; */\n}\n'Copy the code

2. tokenizer

Tokenizer includes the following methods:

  • Back: The back method sets the return value of the next call to nextToken.
  • NextToken: obtains the nextToken.
  • EndOfFile: Determines whether the file is finished.
  • Position: Obtains the current token position.
// The nextToken method of tokenize.js simplifies code
function nextToken(opts) {
    If the back method was called before, the next call to nextToken will return the token set by the back method
    if (returned.length) return returned.pop()
    code = css.charCodeAt(pos)
    // Judge each character
    switch (code) { 
        case xxx: 
        break;
    }

    pos++
    return currentToken
  }
Copy the code

The nextToken method determines each character and generates tokens of the following type:

space:

  • \ n: a newline
  • : the blank space
  • \ f: page breaks
  • \ r: press enter
  • \ T: horizontal TAB character
  // Spaces, newlines, tabs, carriage returns, etc., are all treated as space tokens
  case NEWLINE:
  case SPACE: 
  {
    next = pos
    // loop, taking successive Spaces, newlines, carriage returns, etc., as a token
    do {
      next += 1
      code = css.charCodeAt(next)
    } while (
       / / if it is
    )
    // Intercepts the token value
    currentToken = ['space', css.slice(pos, next)]
    pos = next - 1
    break
  }
Copy the code

string:

  • ‘: single quotation mark
  • “: double quotation marks
 // Single quotation marks. The content between double quotation marks is regarded as a string token
  case SINGLE_QUOTE:
  case DOUBLE_QUOTE: {
    quote = code === SINGLE_QUOTE ? "'" : '"'
    next = pos
    do {
      next = css.indexOf(quote, next + 1)
      if (next === -1) {
        if (ignore || ignoreUnclosed) {
          next = pos + 1
          break
        } else {
          unclosed('string')}}}while (escaped)

    currentToken = ['string', css.slice(pos, next + 1), pos, next]
    pos = next
    break
  }
Copy the code

At-word: @ and the characters following it are regarded as at-word token @* : AT

case AT: {
   currentToken = ['at-word', css.slice(pos, next + 1), pos, next]
   pos = next
   break
 }
Copy the code

[and] : brackets

) : Close parenthesis

{and} : curly braces

; : a semicolon

: : the colon

Both are independent token types.

/ / [] {} :) Etc are independent token types
case OPEN_SQUARE:
case CLOSE_SQUARE:
case OPEN_CURLY:
case CLOSE_CURLY:
case COLON:
case SEMICOLON:
case CLOSE_PARENTHESES: {
    let controlChar = String.fromCharCode(code)
    currentToken = [controlChar, controlChar, pos]
    break
}
Copy the code

(and brackets

  • Url () : The value of URL () that is not enclosed by single or double quotation marks is considered as the brackets type token
  • Url (“) : if there is no close parenthesis), or matches the re as (type token, such as URL (“)).
  • Var (–main-color) : otherwise use brackets as tokens, such as var(–main-color).
// Special handling of the left parenthesis
case OPEN_PARENTHESES: {
prev = buffer.length ? buffer.pop()[1] : ' '
n = css.charCodeAt(pos + 1)
    // Attach brackets with extra brackets. // Attach brackets
    if (
      prev === 'url'&& n ! == SINGLE_QUOTE && n ! == DOUBLE_QUOTE && ) { next = posdo {
        escaped = false
        next = css.indexOf(') ', next + 1)
        if (next === -1) {
          if (ignore || ignoreUnclosed) {
            next = pos
            break
          } else {
            unclosed('bracket')}}}while (escaped)
      currentToken = ['brackets', css.slice(pos, next + 1), pos, next]
      pos = next
    } else {
      next = css.indexOf(') ', pos + 1)
      content = css.slice(pos, next + 1)
      // If there is no close parenthesis), or matches the re as (type token, such as url(")).
      if (next === -1 || RE_BAD_BRACKET.test(content)) {
        currentToken = ['('.'(', pos]
      } else {
        // Use brackets as extra brackets, like var(--main-color).
        currentToken = ['brackets', content, pos, next]
        pos = next
      }
    }
    break
}
Copy the code

Comment: The token of the comment type and Word type is used by default

  • / : slash
  • * : wildcard

word:

  • \ : Backslash
default: {
    if (code === SLASH && css.charCodeAt(pos + 1) === ASTERISK) {
      next = css.indexOf('* /', pos + 2) + 1
      if (next === 0) {
        if (ignore || ignoreUnclosed) {
          next = css.length
        } else {
          unclosed('comment')
        }
      }

      currentToken = ['comment', css.slice(pos, next + 1), pos, next]
      pos = next
    } else {
      RE_WORD_END.lastIndex = pos + 1
      RE_WORD_END.test(css)
      if (RE_WORD_END.lastIndex === 0) {
        next = css.length - 1
      } else {
        next = RE_WORD_END.lastIndex - 2
      }

      currentToken = ['word', css.slice(pos, next + 1), pos, next]
      buffer.push(currentToken)
      pos = next
    }

    break
}
Copy the code

Tokenizer creates the following tokens:

[ 'at-word', '@import', 0, 6 ]
[ 'space', ' ' ]
[ 'word', 'url', 8, 10 ]
[ '(', '(', 11 ]
[ 'string', "'./default.css'", 12, 26 ]
[ ')', ')', 27 ]
[ ';', ';', 28 ]
[ 'space', '\n' ]
[ 'word', 'body', 30, 33 ]
[ 'space', ' ' ]
[ '{', '{', 35 ]
[ 'space', '\n  ' ]
[ 'word', 'padding', 39, 45 ]
[ ':', ':', 46 ]
[ 'space', ' ' ]
[ 'word', '0', 48, 48 ]
[ ';', ';', 49 ]
[ 'space', '\n  ' ]
[ 'comment', '/* margin: 0; */', 53, 68 ]
[ 'space', '\n' ]
[ '}', '}', 70 ]
[ 'space', '\n' ]
Copy the code

As you can see, the token is an array. Take the first token as an example. The data structure is as follows

[' the at - word ', / / type '@ import', / / the value of 0, / / starting position 6 / / terminate position]Copy the code

3. parser

The Parser loop calls tokenizer’s nextToken method until the file ends. Some algorithms and conditional judgments are used during the loop to create nodes and then build the AST. The above example generates the following AST:

3.1 the node

The base class of Node and Container nodes, where Container inherits from Node. The AST consists of the following nodes

  • Root: inherits from Container. The root node of the AST represents the entire CSS file
  • AtRule: inherits from Container. Statements that start with @ and have the core property params, for example: @import URL (‘./default.css’), @keyframes shaking {}. Params for the url (‘. / default. CSS ‘)
  • Rule: Inherited from Container. A selector with a declared core property of selector, for example: body {}, selector body
  • Declaration: Inherited from Node. Declaration, is a key-value pair with core properties prop, value, for example: padding: 0, prop is padding, value is 0
  • Comment: Inherited from Node. Margin: 0; margin: 0;

The node contains some general properties

  • Type: indicates the node type

  • Parent: indicates the parent node

  • Source: indicates the resource information of a storage node. Calculate sourcemap

    • Input: input
    • Start: indicates the start position of a node
    • End: indicates the end position of a node
  • Raws: Additional symbols for storage nodes, semicolons, Spaces, comments, etc., which are concatenated in the Stringify procedure

    General:

    • Before: The space symbols before The node. It also stores* and _ symbols before the declaration (IE hack).

    On the Rule:

    • After: The space symbols after The last child of The node to The end of The node. The space symbol between the last child node and the end of the node
    • Between: The symbols between The selector and{For rules. Symbol between selector and {
    • Semicolon: the Containstrueif the last child has an (optional) semicolon. The last child node has; It is true
    • ownSemicolon: Contains trueif there is semicolon after rule. If rule is followed by; It is true

    Acting on the Comment

    • Left: The space symbols between/ *And the comment’s text. /* The space symbol between the comment content
    • Right: The space symbols between The comment’s text. */ And The space symbols between The comment content are used on The Declaration
    • Important: The content of The important statement. Is it important
    • value: Declaration value with comments. Declared values with comments.

Each node has its own API. For details, see the PostCSS API

3.2 Generation Process

class Parser {  
  parse() {
    let token
    while (!this.tokenizer.endOfFile()) {
      token = this.tokenizer.nextToken()
      switch (token[0]) {
        case 'space':
          this.spaces += token[1]
          break
        case '; ':
          this.freeSemicolon(token)
          break
        case '} ':
          this.end(token)
          break
        case 'comment':
          this.comment(token)
          break
        case 'at-word':
          this.atrule(token)
          break
        case '{':
          this.emptyRule(token)
          break
        default:
          this.other(token)
          break}}this.endFile()
  }
}
Copy the code

Create a root node and set current to root. Use the tokens variable to store tokens that have been traversed but not yet used.

  • encounterat-ruleToken to create an atRule node
  • encounter{Token to create a rule node
    • Generate the selector property of the rule from the tokens stored in tokens
    • Set current to the rule node
    • Push the rule node to the current nodes
  • encounter;Token to create a DECL node. There is a special case: declaration is; If it is the last rule, you can leave it out. , such as. A {color: blue},
    • Push decL nodes into current Nodes.
  • encountercommentToken to create a comment node
  • encounter}Token thinks when the rule ends
    • Set current to current-.parent (the parent of the current node)

Specific process can see the source: github.com/postcss/pos…

Algorithms used:

  1. Depth-first traversal
  2. Matching parentheses

4. plugins

Plugins are then called to modify the AST tree derived from the Parser. Plugins are executed in lazy-result.js.

class LazyResult {
  get [Symbol.toStringTag]() {
    return 'LazyResult'
  }
  get processor() {
    return this.result.processor
  }
  get opts() {
    return this.result.opts
  }
  / / get the CSS
  get css() {
    return this.stringify().css
  }
  get content() {
    return this.stringify().content
  }
  get map() {
    return this.stringify().map
  }
  / / to get root
  get root() {
    return this.sync().root
  }
  get messages() {
    return this.sync().messages
  }
}
Copy the code

Result. CSS, result.map, and result.root are all executed when they are accessed.

stringify() {
    if (this.error) throw this.error
    if (this.stringified) return this.result
    this.stringified = true
    // Execute plugins synchronously
    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
    // Generate map and CSS
    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
 }
Copy the code

When accessing result.css, the plug-in is executed synchronously, and then the processed AST is used to generate CSS and sourcemap

sync() {
    if (this.error) throw this.error
    if (this.processed) return this.result
    this.processed = true

    if (this.processing) {
      throw this.getAsyncError()
    }

    for (let plugin of this.plugins) {
      let promise = this.runOnRoot(plugin)
      if (isPromise(promise)) {
        throw this.getAsyncError()
      }
    }
    Collect accessors first
    this.prepareVisitors()
    if (this.hasListener) {
      let root = this.result.root
      // If root is dirty, re-execute the plug-in on root
      while(! root[isClean]) { root[isClean] =true
        this.walkSync(root)
      }
      if (this.listeners.OnceExit) {
        this.visitSync(this.listeners.OnceExit, root)
      }
    }

    return this.result
  }
Copy the code

The new plug-in supports accessors of two types: Enter and Exit. Once, Root, AtRule, Rule, Declaration, Comment, etc. Call OnceExit, RootExit, AtRuleExit… Will be called after all child nodes have finished processing. Declaration and AtRule support listening on attributes, such as:

module.exports = (opts = {}) = > {
    Declaration: {
        color: decl= > {}
        The '*': decl= >{}},AtRule: {
        media: atRule= >{}}Rule(){}}Copy the code

PrepareVisitors methods collect these listeners and add them to listeners. For example, the code above adds declaration-color, Declaration*, atrule-media, and Rule.

prepareVisitors() {
    this.listeners = {}
    let add = (plugin, type, cb) = > {
      if (!this.listeners[type]) this.listeners[type] = []
      this.listeners[type].push([plugin, cb])
    }
    for (let plugin of this.plugins) {
      if (typeof plugin === 'object') {
        for (let event in plugin) {
          if(! NOT_VISITORS[event]) {if (typeof plugin[event] === 'object') {
              for (let filter in plugin[event]) {
                if (filter === The '*') {
                  add(plugin, event, plugin[event][filter])
                } else {
                  add(
                    plugin,
                    event + The '-' + filter.toLowerCase(),
                    plugin[event][filter]
                  )
                }
              }
            } else if (typeof plugin[event] === 'function') {
              add(plugin, event, plugin[event])
            }
          }
        }
      }
    }
    this.hasListener = Object.keys(this.listeners).length > 0
}
Copy the code

Then, during walkSync, it determines the type of accessors that the node can have and recursively calls the CHILDREN if it is CHILDREN, or executes the accessors if it is other executable accessors such as Rule.

 walkSync(node) {
    node[isClean] = true
    let events = getEvents(node)
    for (let event of events) {
      if (event === CHILDREN) {
        if (node.nodes) {
          node.each(child= > {
            if(! child[isClean])this.walkSync(child)
          })
        }
      } else {
        let visitors = this.listeners[event]
        if (visitors) {
          if (this.visitSync(visitors, node.toProxy())) return}}}}Copy the code

If you perform some operations on a node that have side effects, such as append, prepend, remove, insertBefore, insertAfter, etc., the side effect node[isClean] = false is cyclically marked upwards. Until root[isClean] = false. This can cause the plug-in to execute again, or even cause an endless loop.

5. stringifier

The stringifier traverses the AST tree in a hierarchy starting from root and concatenates node data into strings based on node types.

// stringifier. Js simplifies code
class Stringifier {
  constructor(builder) {
    this.builder = builder
  }
  stringify(node, semicolon) {
    // Invoke the corresponding type node
    this[node.type](node, semicolon)
  }
  // Root node processing
  root(node) {
    this.body(node)
    if (node.raws.after) this.builder(node.raws.after)
  }
  // root node processing
   body(node) {
    let last = node.nodes.length - 1
    while (last > 0) {
      if(node.nodes[last].type ! = ='comment') break
      last -= 1
    }

    let semicolon = this.raw(node, 'semicolon')
    for (let i = 0; i < node.nodes.length; i++) {
      let child = node.nodes[i]
      let before = this.raw(child, 'before')
      if (before) this.builder(before)
      this.stringify(child, last ! == i || semicolon) } }// Comment type node stitching
  comment(node) {}
  // DecL type node splicing
  decl(node, semicolon) {}
  // Node splicing of rule type
  rule(node) {}
  // Splicing at-rule nodes
  atrule(node, semicolon) {}
  // Block node processing, rule, at-rule(@media, etc.)
  block(node, start){}
  // Raw information processing
  raw(){}}Copy the code

Root, body, comment, DECl, rule, atrule, block, and RAW are string splicing functions of different types of nodes and information. The whole process starts from root, does the sequence traversal, root→body→rule/atrule/comment/decl, and then concatenates the string through Builder. Builder is a splicing function:

const builder = (str, node, type) = > {
    this.css += str
}
Copy the code

Plugins plugins

1. The old way

const postcss = require('postcss');

module.exports = postcss.plugin('postcss-plugin-old'.function (opts) {
  return function (root) {
    root.walkRules((rule) = > {
      if (rule.selector === 'body') {
        rule.append(postcss.decl({ prop: 'margin'.value: '0' }));
        rule.append(postcss.decl({ prop: 'font-size'.value: '14px'})); }}); }; });Copy the code

The old writing method needs to introduce PostCSS, so the plug-in needs to set postCSS to peerDependence, and then use the API of PostCSS to operate AST.

2. The new way

// Use symbol to mark the processed nodes
const processd = Symbol('processed');
module.exports = (opts = {}) = > {
  return {
    postcssPlugin: 'postcss-plugin-new'.Once() {},
    OnceExit(root) {
      root.walkDecls((decl) = > {
        // Delete a node
        if (decl.prop === 'color') {
          decl.value = '#ee3'; }}); },Rule(rule, { Declaration }) {
      if(! rule[processd]) {if (rule.selector === 'body') {
          rule.append(new Declaration({ prop: 'color'.value: '# 333' }));
        }
        rule[processd] = true; }},Declaration: {
      padding: (decl) = > {},
      margin: (decl) = > {
        if (decl.value === '0') {
          decl.value = '10px'; }}},DeclarationExit() {},
    prepare(result){
        const variables = {};
        return {
            Declaration(){}
            OnceExit(){}}}}; };module.exports.postcss = true;
Copy the code

The new version eliminates the need to introduce PostCSS and adds a Visitor.

  • There are two types of accessors: Enter and Exit. For example, Declaration is executed when a DECL node is accessed, and DeclarationExit is processed after all Declaration accessors are processed.
  • You can use prepare() to dynamically generate accessors.
  • The first argument to the accessor is node, the node to be accessed, which can be operated on directly by calling node’s methods.
  • The accessor’s second argument is {… Postcss, result: this.result, postcss}, convenient to call methods on postcss.

For more information, please refer to the official document write-a-plugin

Grammatical syntax

Postcss-less and postCSs-SCSS are syntax. They recognize this syntax and convert it. CSS is not compiled

The internal implementations also inherit from Tokenizer and Parser classes and override some of the internal methods.

Syntax does not support // comments. If syntax is not specified as postcss-scss, postCSS will report CssSyntaxError: Unknown word.

  1. First, tokenizer needs to recognize // as a comment token
 function nextToken(){
     // ...
     if(){
     } else if (code === SLASH && n === SLASH) {
      RE_NEW_LINE.lastIndex = pos + 1
      RE_NEW_LINE.test(css)
      if (RE_NEW_LINE.lastIndex === 0) {
        next = css.length - 1
      } else {
        next = RE_NEW_LINE.lastIndex - 2
      }
    
      content = css.slice(pos, next + 1)
      // inline means a // comment
      currentToken = ['comment', content, pos, next, 'inline']
    
      pos = next
    }
}
Copy the code
  1. Parser then needs to build it into a Node and store source, RAWS, etc.
class ProParser extends Parser{
    comment (token) {
        if (token[4= = ='inline') {
            let node = new Comment()
            this.init(node, token[2])
            node.raws.inline = true
            let pos = this.input.fromOffset(token[3])
            node.source.end = { offset: token[3].line: pos.line, column: pos.col       }
            
            let text = token[1].slice(2)
            if (/^\s*$/.test(text)) {
                node.text = ' '
                node.raws.left = text
                node.raws.right = ' '
            } else {
                let match = text.match(/^(\s*)([^]*\S)(\s*)$/)
                let fixed = match[2].replace(/(\*\/|\/\*)/g.'* / / *)
                node.text = fixed
                node.raws.left = match[1]
                node.raws.right = match[3]
                node.raws.text = match[2]}}else {
            super.comment(token)
        }
    }
}
Copy the code
  1. Finally, the stringifier needs to concatenate it into a string
 class ProStringifier extends Stringifier {
     comment (node) {
        let left = this.raw(node, 'left'.'commentLeft')
        let right = this.raw(node, 'right'.'commentRight')
    
        if (node.raws.inline) {
          let text = node.raws.text || node.text
          this.builder('/ /' + left + text + right, node)
        } else {
          this.builder('/ *' + left + node.text + right + '* /', node)
        }
     }
 }
Copy the code

To solve the opening

For the opening scene, the idea is as follows:

  1. Split by selector, for example.a .b{}Split into, a {}, {b}And combine the declaration of the rule with the same selector.
  2. The selectorsplit(' ').length>2For cutting processing
module.exports = (options = {}) = > {
  return {
    postcssPlugin: 'postcss-plugin-crop-css',
    Once (root, { postcss }) {
      const selectorRuleMap = new Map()
      root.walkRules((rule) = > {
        const { selector } = rule
        const selectorUnits = selector.split(', ')
        for (let selectorUnit of selectorUnits) {
          let selectorUnitArr = selectorUnit.split(' ')
          // An error is reported when the selector exceeds two layers
          if (selectorUnitArr.length > 2) {
            throw rule.error('no more than two nested levels')}const selectorCrop = selectorUnitArr.join(' ').replace('\n'.' ')
          const existSelectorRule = selectorRuleMap.get(selectorCrop)
          const nodes = existSelectorRule ? [existSelectorRule.nodes, rule.nodes] : rule.nodes
          const newRule = new postcss.Rule({
            selector: selectorCrop,
            source: rule.source,
            nodes
          })
          selectorRuleMap.set(selectorCrop, newRule)
        }
        rule.remove()
      })
      selectorRuleMap.forEach(selectorRule= > {
        root.append(selectorRule)
      })
    }
  }
}

module.exports.postcss = true
Copy the code

reference

  1. postcss