UNPKG

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