You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

231 lines
7.7 KiB

4 years ago
  1. # How to Write Custom Syntax
  2. PostCSS can transform styles in any syntax, and is not limited to just CSS.
  3. By writing a custom syntax, you can transform styles in any desired format.
  4. Writing a custom syntax is much harder than writing a PostCSS plugin, but
  5. it is an awesome adventure.
  6. There are 3 types of PostCSS syntax packages:
  7. * **Parser** to parse input string to node’s tree.
  8. * **Stringifier** to generate output string by node’s tree.
  9. * **Syntax** contains both parser and stringifier.
  10. ## Syntax
  11. A good example of a custom syntax is [SCSS]. Some users may want to transform
  12. SCSS sources with PostCSS plugins, for example if they need to add vendor
  13. prefixes or change the property order. So this syntax should output SCSS from
  14. an SCSS input.
  15. The syntax API is a very simple plain object, with `parse` & `stringify`
  16. functions:
  17. ```js
  18. module.exports = {
  19. parse: require('./parse'),
  20. stringify: require('./stringify')
  21. }
  22. ```
  23. [SCSS]: https://github.com/postcss/postcss-scss
  24. ## Parser
  25. A good example of a parser is [Safe Parser], which parses malformed/broken CSS.
  26. Because there is no point to generate broken output, this package only provides
  27. a parser.
  28. The parser API is a function which receives a string & returns a [`Root`] node.
  29. The second argument is a function which receives an object with PostCSS options.
  30. ```js
  31. const postcss = require('postcss')
  32. module.exports = function parse (css, opts) {
  33. const root = postcss.root()
  34. // Add other nodes to root
  35. return root
  36. }
  37. ```
  38. [Safe Parser]: https://github.com/postcss/postcss-safe-parser
  39. [`Root`]: http://api.postcss.org/Root.html
  40. ### Main Theory
  41. There are many books about parsers; but do not worry because CSS syntax is
  42. very easy, and so the parser will be much simpler than a programming language
  43. parser.
  44. The default PostCSS parser contains two steps:
  45. 1. [Tokenizer] which reads input string character by character and builds a
  46. tokens array. For example, it joins space symbols to a `['space', '\n ']`
  47. token, and detects strings to a `['string', '"\"{"']` token.
  48. 2. [Parser] which reads the tokens array, creates node instances and
  49. builds a tree.
  50. [Tokenizer]: https://github.com/postcss/postcss/blob/master/lib/tokenize.es6
  51. [Parser]: https://github.com/postcss/postcss/blob/master/lib/parser.es6
  52. ### Performance
  53. Parsing input is often the most time consuming task in CSS processors. So it
  54. is very important to have a fast parser.
  55. The main rule of optimization is that there is no performance without a
  56. benchmark. You can look at [PostCSS benchmarks] to build your own.
  57. Of parsing tasks, the tokenize step will often take the most time, so its
  58. performance should be prioritized. Unfortunately, classes, functions and
  59. high level structures can slow down your tokenizer. Be ready to write dirty
  60. code with repeated statements. This is why it is difficult to extend the
  61. default [PostCSS tokenizer]; copy & paste will be a necessary evil.
  62. Second optimization is using character codes instead of strings.
  63. ```js
  64. // Slow
  65. string[i] === '{'
  66. // Fast
  67. const OPEN_CURLY = 123 // `{'
  68. string.charCodeAt(i) === OPEN_CURLY
  69. ```
  70. Third optimization is “fast jumps”. If you find open quotes, you can find
  71. next closing quote much faster by `indexOf`:
  72. ```js
  73. // Simple jump
  74. next = string.indexOf('"', currentPosition + 1)
  75. // Jump by RegExp
  76. regexp.lastIndex = currentPosion + 1
  77. regexp.test(string)
  78. next = regexp.lastIndex
  79. ```
  80. The parser can be a well written class. There is no need in copy-paste and
  81. hardcore optimization there. You can extend the default [PostCSS parser].
  82. [PostCSS benchmarks]: https://github.com/postcss/benchmark
  83. [PostCSS tokenizer]: https://github.com/postcss/postcss/blob/master/lib/tokenize.es6
  84. [PostCSS parser]: https://github.com/postcss/postcss/blob/master/lib/parser.es6
  85. ### Node Source
  86. Every node should have `source` property to generate correct source map.
  87. This property contains `start` and `end` properties with `{ line, column }`,
  88. and `input` property with an [`Input`] instance.
  89. Your tokenizer should save the original position so that you can propagate
  90. the values to the parser, to ensure that the source map is correctly updated.
  91. [`Input`]: https://github.com/postcss/postcss/blob/master/lib/input.es6
  92. ### Raw Values
  93. A good PostCSS parser should provide all information (including spaces symbols)
  94. to generate byte-to-byte equal output. It is not so difficult, but respectful
  95. for user input and allow integration smoke tests.
  96. A parser should save all additional symbols to `node.raws` object.
  97. It is an open structure for you, you can add additional keys.
  98. For example, [SCSS parser] saves comment types (`/* */` or `//`)
  99. in `node.raws.inline`.
  100. The default parser cleans CSS values from comments and spaces.
  101. It saves the original value with comments to `node.raws.value.raw` and uses it,
  102. if the node value was not changed.
  103. [SCSS parser]: https://github.com/postcss/postcss-scss
  104. ### Tests
  105. Of course, all parsers in the PostCSS ecosystem must have tests.
  106. If your parser just extends CSS syntax (like [SCSS] or [Safe Parser]),
  107. you can use the [PostCSS Parser Tests]. It contains unit & integration tests.
  108. [PostCSS Parser Tests]: https://github.com/postcss/postcss-parser-tests
  109. ## Stringifier
  110. A style guide generator is a good example of a stringifier. It generates output
  111. HTML which contains CSS components. For this use case, a parser isn't necessary,
  112. so the package should just contain a stringifier.
  113. The Stringifier API is little bit more complicated, than the parser API.
  114. PostCSS generates a source map, so a stringifier can’t just return a string.
  115. It must link every substring with its source node.
  116. A Stringifier is a function which receives [`Root`] node and builder callback.
  117. Then it calls builder with every node’s string and node instance.
  118. ```js
  119. module.exports = function stringify (root, builder) {
  120. // Some magic
  121. const string = decl.prop + ':' + decl.value + ';'
  122. builder(string, decl)
  123. // Some science
  124. };
  125. ```
  126. ### Main Theory
  127. PostCSS [default stringifier] is just a class with a method for each node type
  128. and many methods to detect raw properties.
  129. In most cases it will be enough just to extend this class,
  130. like in [SCSS stringifier].
  131. [default stringifier]: https://github.com/postcss/postcss/blob/master/lib/stringifier.es6
  132. [SCSS stringifier]: https://github.com/postcss/postcss-scss/blob/master/lib/scss-stringifier.es6
  133. ### Builder Function
  134. A builder function will be passed to `stringify` function as second argument.
  135. For example, the default PostCSS stringifier class saves it
  136. to `this.builder` property.
  137. Builder receives output substring and source node to append this substring
  138. to the final output.
  139. Some nodes contain other nodes in the middle. For example, a rule has a `{`
  140. at the beginning, many declarations inside and a closing `}`.
  141. For these cases, you should pass a third argument to builder function:
  142. `'start'` or `'end'` string:
  143. ```js
  144. this.builder(rule.selector + '{', rule, 'start')
  145. // Stringify declarations inside
  146. this.builder('}', rule, 'end')
  147. ```
  148. ### Raw Values
  149. A good PostCSS custom syntax saves all symbols and provide byte-to-byte equal
  150. output if there were no changes.
  151. This is why every node has `node.raws` object to store space symbol, etc.
  152. Be careful, because sometimes these raw properties will not be present; some
  153. nodes may be built manually, or may lose their indentation when they are moved
  154. to another parent node.
  155. This is why the default stringifier has a `raw()` method to autodetect raw
  156. properties by other nodes. For example, it will look at other nodes to detect
  157. indent size and them multiply it with the current node depth.
  158. ### Tests
  159. A stringifier must have tests too.
  160. You can use unit and integration test cases from [PostCSS Parser Tests].
  161. Just compare input CSS with CSS after your parser and stringifier.
  162. [PostCSS Parser Tests]: https://github.com/postcss/postcss-parser-tests