|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- # How to Write Custom Syntax
-
- PostCSS can transform styles in any syntax, and is not limited to just CSS.
- By writing a custom syntax, you can transform styles in any desired format.
-
- Writing a custom syntax is much harder than writing a PostCSS plugin, but
- it is an awesome adventure.
-
- There are 3 types of PostCSS syntax packages:
-
- * **Parser** to parse input string to node’s tree.
- * **Stringifier** to generate output string by node’s tree.
- * **Syntax** contains both parser and stringifier.
-
- ## Syntax
-
- A good example of a custom syntax is [SCSS]. Some users may want to transform
- SCSS sources with PostCSS plugins, for example if they need to add vendor
- prefixes or change the property order. So this syntax should output SCSS from
- an SCSS input.
-
- The syntax API is a very simple plain object, with `parse` & `stringify`
- functions:
-
- ```js
- module.exports = {
- parse: require('./parse'),
- stringify: require('./stringify')
- }
- ```
-
- [SCSS]: https://github.com/postcss/postcss-scss
-
- ## Parser
-
- A good example of a parser is [Safe Parser], which parses malformed/broken CSS.
- Because there is no point to generate broken output, this package only provides
- a parser.
-
- The parser API is a function which receives a string & returns a [`Root`] node.
- The second argument is a function which receives an object with PostCSS options.
-
- ```js
- const postcss = require('postcss')
-
- module.exports = function parse (css, opts) {
- const root = postcss.root()
- // Add other nodes to root
- return root
- }
- ```
-
- [Safe Parser]: https://github.com/postcss/postcss-safe-parser
- [`Root`]: http://api.postcss.org/Root.html
-
- ### Main Theory
-
- There are many books about parsers; but do not worry because CSS syntax is
- very easy, and so the parser will be much simpler than a programming language
- parser.
-
- The default PostCSS parser contains two steps:
-
- 1. [Tokenizer] which reads input string character by character and builds a
- tokens array. For example, it joins space symbols to a `['space', '\n ']`
- token, and detects strings to a `['string', '"\"{"']` token.
- 2. [Parser] which reads the tokens array, creates node instances and
- builds a tree.
-
- [Tokenizer]: https://github.com/postcss/postcss/blob/master/lib/tokenize.es6
- [Parser]: https://github.com/postcss/postcss/blob/master/lib/parser.es6
-
- ### Performance
-
- Parsing input is often the most time consuming task in CSS processors. So it
- is very important to have a fast parser.
-
- The main rule of optimization is that there is no performance without a
- benchmark. You can look at [PostCSS benchmarks] to build your own.
-
- Of parsing tasks, the tokenize step will often take the most time, so its
- performance should be prioritized. Unfortunately, classes, functions and
- high level structures can slow down your tokenizer. Be ready to write dirty
- code with repeated statements. This is why it is difficult to extend the
- default [PostCSS tokenizer]; copy & paste will be a necessary evil.
-
- Second optimization is using character codes instead of strings.
-
- ```js
- // Slow
- string[i] === '{'
-
- // Fast
- const OPEN_CURLY = 123 // `{'
- string.charCodeAt(i) === OPEN_CURLY
- ```
-
- Third optimization is “fast jumps”. If you find open quotes, you can find
- next closing quote much faster by `indexOf`:
-
- ```js
- // Simple jump
- next = string.indexOf('"', currentPosition + 1)
-
- // Jump by RegExp
- regexp.lastIndex = currentPosion + 1
- regexp.test(string)
- next = regexp.lastIndex
- ```
-
- The parser can be a well written class. There is no need in copy-paste and
- hardcore optimization there. You can extend the default [PostCSS parser].
-
- [PostCSS benchmarks]: https://github.com/postcss/benchmark
- [PostCSS tokenizer]: https://github.com/postcss/postcss/blob/master/lib/tokenize.es6
- [PostCSS parser]: https://github.com/postcss/postcss/blob/master/lib/parser.es6
-
- ### Node Source
-
- Every node should have `source` property to generate correct source map.
- This property contains `start` and `end` properties with `{ line, column }`,
- and `input` property with an [`Input`] instance.
-
- Your tokenizer should save the original position so that you can propagate
- the values to the parser, to ensure that the source map is correctly updated.
-
- [`Input`]: https://github.com/postcss/postcss/blob/master/lib/input.es6
-
- ### Raw Values
-
- A good PostCSS parser should provide all information (including spaces symbols)
- to generate byte-to-byte equal output. It is not so difficult, but respectful
- for user input and allow integration smoke tests.
-
- A parser should save all additional symbols to `node.raws` object.
- It is an open structure for you, you can add additional keys.
- For example, [SCSS parser] saves comment types (`/* */` or `//`)
- in `node.raws.inline`.
-
- The default parser cleans CSS values from comments and spaces.
- It saves the original value with comments to `node.raws.value.raw` and uses it,
- if the node value was not changed.
-
- [SCSS parser]: https://github.com/postcss/postcss-scss
-
- ### Tests
-
- Of course, all parsers in the PostCSS ecosystem must have tests.
-
- If your parser just extends CSS syntax (like [SCSS] or [Safe Parser]),
- you can use the [PostCSS Parser Tests]. It contains unit & integration tests.
-
- [PostCSS Parser Tests]: https://github.com/postcss/postcss-parser-tests
-
- ## Stringifier
-
- A style guide generator is a good example of a stringifier. It generates output
- HTML which contains CSS components. For this use case, a parser isn't necessary,
- so the package should just contain a stringifier.
-
- The Stringifier API is little bit more complicated, than the parser API.
- PostCSS generates a source map, so a stringifier can’t just return a string.
- It must link every substring with its source node.
-
- A Stringifier is a function which receives [`Root`] node and builder callback.
- Then it calls builder with every node’s string and node instance.
-
- ```js
- module.exports = function stringify (root, builder) {
- // Some magic
- const string = decl.prop + ':' + decl.value + ';'
- builder(string, decl)
- // Some science
- };
- ```
-
- ### Main Theory
-
- PostCSS [default stringifier] is just a class with a method for each node type
- and many methods to detect raw properties.
-
- In most cases it will be enough just to extend this class,
- like in [SCSS stringifier].
-
- [default stringifier]: https://github.com/postcss/postcss/blob/master/lib/stringifier.es6
- [SCSS stringifier]: https://github.com/postcss/postcss-scss/blob/master/lib/scss-stringifier.es6
-
- ### Builder Function
-
- A builder function will be passed to `stringify` function as second argument.
- For example, the default PostCSS stringifier class saves it
- to `this.builder` property.
-
- Builder receives output substring and source node to append this substring
- to the final output.
-
- Some nodes contain other nodes in the middle. For example, a rule has a `{`
- at the beginning, many declarations inside and a closing `}`.
-
- For these cases, you should pass a third argument to builder function:
- `'start'` or `'end'` string:
-
- ```js
- this.builder(rule.selector + '{', rule, 'start')
- // Stringify declarations inside
- this.builder('}', rule, 'end')
- ```
-
- ### Raw Values
-
- A good PostCSS custom syntax saves all symbols and provide byte-to-byte equal
- output if there were no changes.
-
- This is why every node has `node.raws` object to store space symbol, etc.
-
- All data related to source code and not CSS structure, should be in `Node#raws`. For instance, `postcss-scss` keep in `Comment#raws.inline` boolean marker of inline comment (`// comment` instead of `/* comment */`).
-
- Be careful, because sometimes these raw properties will not be present; some
- nodes may be built manually, or may lose their indentation when they are moved
- to another parent node.
-
- This is why the default stringifier has a `raw()` method to autodetect raw
- properties by other nodes. For example, it will look at other nodes to detect
- indent size and them multiply it with the current node depth.
-
- ### Tests
-
- A stringifier must have tests too.
-
- You can use unit and integration test cases from [PostCSS Parser Tests].
- Just compare input CSS with CSS after your parser and stringifier.
-
- [PostCSS Parser Tests]: https://github.com/postcss/postcss-parser-tests
|