|
| 1 | +<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta property="og:type" content="article"><meta property="og:url" content="http://postcss.org/"><meta name="twitter:card" content="summary"><meta name="twitter:site" content="@postcss"><meta name="twitter:creator" content="@postcss"><meta name="description" content="Transform CSS with the power of JavaScript. Auto-prefixing, future CSS syntaxes, modules, linting and more are possible with hundreds of PostCSS plugins."><link rel="icon" type="image/x-icon" sizes="any" href="/docs/assets/favicon-DbGqNhKa.ico"><link rel="icon" href="/docs/assets/logo-C2ryQugb.svg" type="image/svg+xml"><meta property="og:image" content="./base/og.jpg"><title>How to Write Custom Syntax</title> <script type="module" crossorigin src="/docs/assets/docs-dwWlJbYc.js"></script> |
| 2 | + <link rel="stylesheet" crossorigin href="/docs/assets/docs-hQTQJUj-.css"> |
| 3 | +</head><body><main><div class="hero is-small"><a class="hero_home" rel="home" href="/"><img class="hero_logo" src="/docs/assets/postcss-CsElRNOW.svg" alt="PostCSS logo"></a></div><nav class="nav"><ul class="nav_items"><li class="nav_item"><a class="nav_link" href="https://github.com/postcss/postcss#usage">Setup</a></li><li class="nav_item"><a class="nav_link" href="/docs/">Docs</a></li><li class="nav_item"><a class="nav_link" href="/docs/postcss-plugins">Plugins</a></li><li class="nav_item"><a class="nav_link" href="/api/">API</a></li><li class="nav_item"><a class="nav_link" href="https://github.com/postcss/brand">Logo</a></li></ul></nav><nav class="sidemenu"><ul><a href="how-to-write-custom-syntax#syntax" class="sidemenu_section">Syntax</a><li class="sidemenu_item"><div class="sidemenu_bar"><a href="how-to-write-custom-syntax#parser" class="sidemenu_section">Parser</a><button class="sidemenu_controller"></button></div><ul class="sidemenu_children"><li><a href="how-to-write-custom-syntax#main-theory" class="sidemenu_child">Main Theory</a></li><li><a href="how-to-write-custom-syntax#performance" class="sidemenu_child">Performance</a></li><li><a href="how-to-write-custom-syntax#node-source" class="sidemenu_child">Node Source</a></li><li><a href="how-to-write-custom-syntax#raw-values" class="sidemenu_child">Raw Values</a></li><li><a href="how-to-write-custom-syntax#tests" class="sidemenu_child">Tests</a></li></ul></li><li class="sidemenu_item"><div class="sidemenu_bar"><a href="how-to-write-custom-syntax#stringifier" class="sidemenu_section">Stringifier</a><button class="sidemenu_controller"></button></div><ul class="sidemenu_children"><li><a href="how-to-write-custom-syntax#main-theory" class="sidemenu_child">Main Theory</a></li><li><a href="how-to-write-custom-syntax#builder-function" class="sidemenu_child">Builder Function</a></li><li><a href="how-to-write-custom-syntax#raw-values" class="sidemenu_child">Raw Values</a></li><li><a href="how-to-write-custom-syntax#tests" class="sidemenu_child">Tests</a></li></ul></li></ul></nav><article class="doc"><h1 class="doc_title">How to Write Custom Syntax</h1><p>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.</p><p>Writing a custom syntax is much harder than writing a PostCSS plugin, but |
| 5 | +it is an awesome adventure.</p><p>There are 3 types of PostCSS syntax packages:</p><ul> |
| 6 | +<li><strong>Parser</strong> to parse input string to node’s tree.</li> |
| 7 | +<li><strong>Stringifier</strong> to generate output string by node’s tree.</li> |
| 8 | +<li><strong>Syntax</strong> contains both parser and stringifier.</li> |
| 9 | +</ul><h2 class="doc_subtitle" id="syntax">Syntax</h2><p>A good example of a custom syntax is <a href="https://github.com/postcss/postcss-scss">SCSS</a>. Some users may want to transform |
| 10 | +SCSS sources with PostCSS plugins, for example if they need to add vendor |
| 11 | +prefixes or change the property order. So this syntax should output SCSS from |
| 12 | +an SCSS input.</p><p>The syntax API is a very simple plain object, with <code>parse</code> & <code>stringify</code> |
| 13 | +functions:</p><pre><code class="code language-js"><span class="code-variable language_">module</span>.<span class="code-property">exports</span> = { |
| 14 | + <span class="code-attr">parse</span>: <span class="code-built_in">require</span>(<span class="code-string">'./parse'</span>), |
| 15 | + <span class="code-attr">stringify</span>: <span class="code-built_in">require</span>(<span class="code-string">'./stringify'</span>) |
| 16 | +} |
| 17 | +</code></pre><h2 class="doc_subtitle" id="parser">Parser</h2><p>A good example of a parser is <a href="https://github.com/postcss/postcss-safe-parser">Safe Parser</a>, which parses malformed/broken CSS. |
| 18 | +Because there is no point to generate broken output, this package only provides |
| 19 | +a parser.</p><p>The parser API is a function which receives a string & returns a <a href="https://postcss.org/api/#root"><code>Root</code></a> |
| 20 | +or <a href="https://postcss.org/api/#document"><code>Document</code></a> node. The second argument is a function which receives |
| 21 | +an object with PostCSS options.</p><pre><code class="code language-js"><span class="code-keyword">const</span> postcss = <span class="code-built_in">require</span>(<span class="code-string">'postcss'</span>) |
| 22 | + |
| 23 | +<span class="code-variable language_">module</span>.<span class="code-property">exports</span> = <span class="code-keyword">function</span> <span class="code-title function_">parse</span> (css, opts) { |
| 24 | + <span class="code-keyword">const</span> root = postcss.<span class="code-title function_">root</span>() |
| 25 | + <span class="code-comment">// Add other nodes to root</span> |
| 26 | + <span class="code-keyword">return</span> root |
| 27 | +} |
| 28 | +</code></pre><p>For open source parser npm package must have <code>postcss</code> in <code>peerDependencies</code>, |
| 29 | +not in direct <code>dependencies</code>.</p><h3 class="doc_subtitle" id="main-theory">Main Theory</h3><p>There are many books about parsers; but do not worry because CSS syntax is |
| 30 | +very easy, and so the parser will be much simpler than a programming language |
| 31 | +parser.</p><p>The default PostCSS parser contains two steps:</p><ol> |
| 32 | +<li><a href="https://github.com/postcss/postcss/blob/main/lib/tokenize.js">Tokenizer</a> which reads input string character by character and builds a |
| 33 | +tokens array. For example, it joins space symbols to a <code>['space', '\n ']</code> |
| 34 | +token, and detects strings to a <code>['string', '"\"{"']</code> token.</li> |
| 35 | +<li><a href="https://github.com/postcss/postcss/blob/main/lib/parser.js">Parser</a> which reads the tokens array, creates node instances and |
| 36 | +builds a tree.</li> |
| 37 | +</ol><h3 class="doc_subtitle" id="performance">Performance</h3><p>Parsing input is often the most time consuming task in CSS processors. So it |
| 38 | +is very important to have a fast parser.</p><p>The main rule of optimization is that there is no performance without a |
| 39 | +benchmark. You can look at <a href="https://github.com/postcss/benchmark">PostCSS benchmarks</a> to build your own.</p><p>Of parsing tasks, the tokenize step will often take the most time, so its |
| 40 | +performance should be prioritized. Unfortunately, classes, functions and |
| 41 | +high level structures can slow down your tokenizer. Be ready to write dirty |
| 42 | +code with repeated statements. This is why it is difficult to extend the |
| 43 | +default <a href="https://github.com/postcss/postcss/blob/main/lib/tokenize.js">PostCSS tokenizer</a>; copy & paste will be a necessary evil.</p><p>Second optimization is using character codes instead of strings.</p><pre><code class="code language-js"><span class="code-comment">// Slow</span> |
| 44 | +string[i] === <span class="code-string">'{'</span> |
| 45 | + |
| 46 | +<span class="code-comment">// Fast</span> |
| 47 | +<span class="code-keyword">const</span> <span class="code-variable constant_">OPEN_CURLY</span> = <span class="code-number">123</span> <span class="code-comment">// `{'</span> |
| 48 | +string.<span class="code-title function_">charCodeAt</span>(i) === <span class="code-variable constant_">OPEN_CURLY</span> |
| 49 | +</code></pre><p>Third optimization is “fast jumps”. If you find open quotes, you can find |
| 50 | +next closing quote much faster by <code>indexOf</code>:</p><pre><code class="code language-js"><span class="code-comment">// Simple jump</span> |
| 51 | +next = string.<span class="code-title function_">indexOf</span>(<span class="code-string">'"'</span>, currentPosition + <span class="code-number">1</span>) |
| 52 | + |
| 53 | +<span class="code-comment">// Jump by RegExp</span> |
| 54 | +regexp.<span class="code-property">lastIndex</span> = currentPosion + <span class="code-number">1</span> |
| 55 | +regexp.<span class="code-title function_">test</span>(string) |
| 56 | +next = regexp.<span class="code-property">lastIndex</span> |
| 57 | +</code></pre><p>The parser can be a well written class. There is no need in copy-paste and |
| 58 | +hardcore optimization there. You can extend the default <a href="https://github.com/postcss/postcss/blob/main/lib/parser.js">PostCSS parser</a>.</p><h3 class="doc_subtitle" id="node-source">Node Source</h3><p>Every node should have <code>source</code> property to generate correct source map. |
| 59 | +This property contains <code>start</code> and <code>end</code> properties with <code>{ line, column }</code>, |
| 60 | +and <code>input</code> property with an <a href="https://github.com/postcss/postcss/blob/main/lib/input.js"><code>Input</code></a> instance.</p><p>Your tokenizer should save the original position so that you can propagate |
| 61 | +the values to the parser, to ensure that the source map is correctly updated.</p><h3 class="doc_subtitle" id="raw-values">Raw Values</h3><p>A good PostCSS parser should provide all information (including spaces symbols) |
| 62 | +to generate byte-to-byte equal output. It is not so difficult, but respectful |
| 63 | +for user input and allow integration smoke tests.</p><p>A parser should save all additional symbols to <code>node.raws</code> object. |
| 64 | +It is an open structure for you, you can add additional keys. |
| 65 | +For example, <a href="https://github.com/postcss/postcss-scss">SCSS parser</a> saves comment types (<code>/* */</code> or <code>//</code>) |
| 66 | +in <code>node.raws.inline</code>.</p><p>The default parser cleans CSS values from comments and spaces. |
| 67 | +It saves the original value with comments to <code>node.raws.value.raw</code> and uses it, |
| 68 | +if the node value was not changed.</p><h3 class="doc_subtitle" id="tests">Tests</h3><p>Of course, all parsers in the PostCSS ecosystem must have tests.</p><p>If your parser just extends CSS syntax (like <a href="https://github.com/postcss/postcss-scss">SCSS</a> or <a href="https://github.com/postcss/postcss-safe-parser">Safe Parser</a>), |
| 69 | +you can use the <a href="https://github.com/postcss/postcss-parser-tests">PostCSS Parser Tests</a>. It contains unit & integration tests.</p><h2 class="doc_subtitle" id="stringifier">Stringifier</h2><p>A style guide generator is a good example of a stringifier. It generates output |
| 70 | +HTML which contains CSS components. For this use case, a parser isn't necessary, |
| 71 | +so the package should just contain a stringifier.</p><p>The Stringifier API is little bit more complicated, than the parser API. |
| 72 | +PostCSS generates a source map, so a stringifier can’t just return a string. |
| 73 | +It must link every substring with its source node.</p><p>A Stringifier is a function which receives <a href="https://postcss.org/api/#root"><code>Root</code></a> or <a href="https://postcss.org/api/#document"><code>Document</code></a> node and builder callback. |
| 74 | +Then it calls builder with every node’s string and node instance.</p><pre><code class="code language-js"><span class="code-variable language_">module</span>.<span class="code-property">exports</span> = <span class="code-keyword">function</span> <span class="code-title function_">stringify</span> (root, builder) { |
| 75 | + <span class="code-comment">// Some magic</span> |
| 76 | + <span class="code-keyword">const</span> string = decl.<span class="code-property">prop</span> + <span class="code-string">':'</span> + decl.<span class="code-property">value</span> + <span class="code-string">';'</span> |
| 77 | + <span class="code-title function_">builder</span>(string, decl) |
| 78 | + <span class="code-comment">// Some science</span> |
| 79 | +}; |
| 80 | +</code></pre><h3 class="doc_subtitle" id="main-theory-1">Main Theory</h3><p>PostCSS <a href="https://github.com/postcss/postcss/blob/main/lib/stringifier.js">default stringifier</a> is just a class with a method for each node type |
| 81 | +and many methods to detect raw properties.</p><p>In most cases it will be enough just to extend this class, |
| 82 | +like in <a href="https://github.com/postcss/postcss-scss/blob/main/lib/scss-stringifier.js">SCSS stringifier</a>.</p><h3 class="doc_subtitle" id="builder-function">Builder Function</h3><p>A builder function will be passed to <code>stringify</code> function as second argument. |
| 83 | +For example, the default PostCSS stringifier class saves it |
| 84 | +to <code>this.builder</code> property.</p><p>Builder receives output substring and source node to append this substring |
| 85 | +to the final output.</p><p>Some nodes contain other nodes in the middle. For example, a rule has a <code>{</code> |
| 86 | +at the beginning, many declarations inside and a closing <code>}</code>.</p><p>For these cases, you should pass a third argument to builder function: |
| 87 | +<code>'start'</code> or <code>'end'</code> string:</p><pre><code class="code language-js"><span class="code-variable language_">this</span>.<span class="code-title function_">builder</span>(rule.<span class="code-property">selector</span> + <span class="code-string">'{'</span>, rule, <span class="code-string">'start'</span>) |
| 88 | +<span class="code-comment">// Stringify declarations inside</span> |
| 89 | +<span class="code-variable language_">this</span>.<span class="code-title function_">builder</span>(<span class="code-string">'}'</span>, rule, <span class="code-string">'end'</span>) |
| 90 | +</code></pre><h3 class="doc_subtitle" id="raw-values">Raw Values</h3><p>A good PostCSS custom syntax saves all symbols and provide byte-to-byte equal |
| 91 | +output if there were no changes.</p><p>This is why every node has <code>node.raws</code> object to store space symbol, etc.</p><p>All data related to source code and not CSS structure, should be in <code>Node#raws</code>. For instance, <code>postcss-scss</code> keep in <code>Comment#raws.inline</code> boolean marker of inline comment (<code>// comment</code> instead of <code>/* comment */</code>).</p><p>Be careful, because sometimes these raw properties will not be present; some |
| 92 | +nodes may be built manually, or may lose their indentation when they are moved |
| 93 | +to another parent node.</p><p>This is why the default stringifier has a <code>raw()</code> method to autodetect raw |
| 94 | +properties by other nodes. For example, it will look at other nodes to detect |
| 95 | +indent size and them multiply it with the current node depth.</p><h3 class="doc_subtitle" id="tests">Tests</h3><p>A stringifier must have tests too.</p><p>You can use unit and integration test cases from <a href="https://github.com/postcss/postcss-parser-tests">PostCSS Parser Tests</a>. |
| 96 | +Just compare input CSS with CSS after your parser and stringifier.</p></article><aside class="socials"><ul class="socials_items"><li class="socials_item"><a class="socials_link is-open-collective" href="https://opencollective.com/postcss/" rel="me">Open Collective</a></li><li class="socials_item"><a class="socials_link is-twitter" href="https://twitter.com/postcss" rel="me">Twitter</a></li><li class="socials_item"><a class="socials_link is-github" href="https://github.com/postcss/postcss" rel="me">GitHub</a></li></ul></aside><footer class="footer"><div class="footer_inner"><div class="footer_info"><p class="footer_license">Distributed under the MIT License.</p><p class="footer_issue">Found an issue?<a class="footer_report" href="https://github.com/postcss/postcss.org/issues">Report it!</a></p></div><div><a href="https://evilmartians.com/?utm_source=postcss&utm_campaign=homepage"><img alt="Evil Martians" src="/docs/assets/evilmartians-bzYkprTm.svg"></a></div></div></footer></main></body></html> |
0 commit comments