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