mirror of
				https://github.com/fooflington/selfdefined.git
				synced 2025-10-31 06:08:33 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			232 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			232 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # 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.
 | ||
| 
 | ||
| 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
 | 
