如何编写自定义语法(How to Write Custom Syntax)

¥How to Write Custom Syntax

PostCSS 可以转换任何语法的样式,而不仅限于 CSS。通过编写自定义语法,你可以将样式转换为任何所需的格式。

¥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.

编写自定义语法比编写 PostCSS 插件困难得多,但这是一次很棒的冒险。

¥Writing a custom syntax is much harder than writing a PostCSS plugin, but it is an awesome adventure.

PostCSS 语法包有 3 种类型:

¥There are 3 types of PostCSS syntax packages:

目录

¥Table of Contents

语法(Syntax)

¥Syntax

自定义语法的一个很好的例子是 SCSS。某些用户可能希望使用 PostCSS 插件转换 SCSS 源,例如,如果他们需要添加浏览器前缀或更改属性顺序。因此,此语法应从 SCSS 输入输出 SCSS。

¥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.

语法 API 是一个非常简单的普通对象,具有 parsestringify 函数:

¥The syntax API is a very simple plain object, with parse & stringify functions:

module.exports = {
  parse:     require('./parse'),
  stringify: require('./stringify')
}

解析器(Parser)

¥Parser

解析器的一个很好的例子是 安全解析器,它解析格式错误/损坏的 CSS。由于没有必要生成损坏的输出,因此该包仅提供一个解析器。

¥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.

解析器 API 是一个接收字符串并返回 RootDocument 节点的函数。第二个参数是一个函数,它接收带有 PostCSS 选项的对象。

¥The parser API is a function which receives a string & returns a Root or Document node. The second argument is a function which receives an object with PostCSS options.

const postcss = require('postcss')

module.exports = function parse (css, opts) {
  const root = postcss.root()
  // Add other nodes to root
  return root
}

对于开源解析器 npm 包必须有 postcsspeerDependencies 中,而不是直接在 dependencies 中。

¥For open source parser npm package must have postcss in peerDependencies, not in direct dependencies.

主要理论(Main Theory)

¥Main Theory

有很多关于解析器的书;但不用担心,因为 CSS 语法非常简单,因此解析器将比编程语言解析器简单得多。

¥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.

默认的 PostCSS 解析器包含两个步骤:

¥The default PostCSS parser contains two steps:

  1. 分词器 逐字符读取输入字符串并构建标记数组。例如,它将空格符号连接到 ['space', '\n '] 标记,并检测字符串到 ['string', '"\"{"'] 标记。

    ¥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.

性能(Performance)

¥Performance

解析输入通常是 CSS 处理器中最耗时的任务。所以拥有一个快速的解析器非常重要。

¥Parsing input is often the most time consuming task in CSS processors. So it is very important to have a fast parser.

优化的主要规则是没有基准就没有性能。你可以查看 PostCSS 基准测试 来构建你自己的。

¥The main rule of optimization is that there is no performance without a benchmark. You can look at PostCSS benchmarks to build your own.

在解析任务中,标记化步骤通常花费最多时间,因此应优先考虑其性能。不幸的是,类、函数和高级结构会减慢你的分词器的速度。准备好编写带有重复语句的脏代码。这就是为什么很难扩展默认的 PostCSS 分词器;复制和粘贴将是一种必要的罪恶。

¥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.

// Slow
string[i] === '{'

// Fast
const OPEN_CURLY = 123 // `{'
string.charCodeAt(i) === OPEN_CURLY

第三个优化是“快速跳跃”。如果你找到打开的引号,你可以在 indexOf 之前更快地找到下一个关闭的引号:

¥Third optimization is “fast jumps”. If you find open quotes, you can find next closing quote much faster by indexOf:

// Simple jump
next = string.indexOf('"', currentPosition + 1)

// Jump by RegExp
regexp.lastIndex = currentPosion + 1
regexp.test(string)
next = regexp.lastIndex

解析器可以是一个编写良好的类。那里不需要复制粘贴和硬核优化。你可以扩展默认的 PostCSS 解析器

¥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.

节点来源(Node Source)

¥Node Source

每个节点都应该具有 source 属性以生成正确的源映射。此属性包含带有 { line, column }startend 属性,以及带有 Input 实例的 input 属性。

¥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.

原始值(Raw Values)

¥Raw Values

一个好的 PostCSS 解析器应该提供所有信息(包括空格符号)以生成字节到字节相等的输出。这并不困难,但尊重用户输入并允许集成冒烟测试。

¥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.

解析器应将所有附加符号保存到 node.raws 对象。它对你来说是一个开放的结构,你可以添加额外的键。例如,SCSS 解析器 将注释类型(/* *///)保存在 node.raws.inline 中。

¥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.

默认解析器会清除注释和空格中的 CSS 值。如果节点值未更改,它将带有注释的原始值保存到 node.raws.value.raw 并使用它。

¥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.

测试(Tests)

¥Tests

当然,PostCSS 生态系统中的所有解析器都必须经过测试。

¥Of course, all parsers in the PostCSS ecosystem must have tests.

如果你的解析器只是扩展 CSS 语法(如 SCSS安全解析器),你可以使用 PostCSS 解析器测试。它包含单元测试和集成测试。

¥If your parser just extends CSS syntax (like SCSS or Safe Parser), you can use the PostCSS Parser Tests. It contains unit & integration tests.

字符串化器(Stringifier)

¥Stringifier

风格指南生成器是字符串生成器的一个很好的例子。它生成包含 CSS 组件的输出 HTML。对于此用例,不需要解析器,因此包应该只包含一个字符串生成器。

¥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.

Stringifier API 比解析器 API 稍微复杂一些。PostCSS 生成源映射,因此字符串生成器不能只返回字符串。它必须将每个子字符串与其源节点链接起来。

¥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.

Stringifier 是一个接收 RootDocument 节点和构建器回调的函数。然后它使用每个节点的字符串和节点实例调用构建器。

¥A Stringifier is a function which receives Root or Document node and builder callback. Then it calls builder with every node’s string and node instance.

module.exports = function stringify (root, builder) {
  // Some magic
  const string = decl.prop + ':' + decl.value + ';'
  builder(string, decl)
  // Some science
};

主要理论(Main Theory)

¥Main Theory

PostCSS 默认字符串化器 只是一个类,其中包含每个节点类型的方法和许多检测原始属性的方法。

¥PostCSS default stringifier is just a class with a method for each node type and many methods to detect raw properties.

在大多数情况下,只需扩展此类就足够了,就像在 SCSS 字符串化器 中一样。

¥In most cases it will be enough just to extend this class, like in SCSS stringifier.

生成器函数(Builder Function)

¥Builder Function

构建器函数将作为第二个参数传递给 stringify 函数。例如,默认的 PostCSS stringifier 类将其保存到 this.builder 属性。

¥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 接收输出子字符串和源节点,以将此子字符串附加到最终输出。

¥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 }.

对于这些情况,你应该将第三个参数传递给构建器函数:'start''end' 字符串:

¥For these cases, you should pass a third argument to builder function: 'start' or 'end' string:

this.builder(rule.selector + '{', rule, 'start')
// Stringify declarations inside
this.builder('}', rule, 'end')

原始值(Raw Values)

¥Raw Values

良好的 PostCSS 自定义语法会保存所有符号,并在没有更改的情况下提供字节到字节的相等输出。

¥A good PostCSS custom syntax saves all symbols and provide byte-to-byte equal output if there were no changes.

这就是为什么每个节点都有 node.raws 个对象来存储空间符号等。

¥This is why every node has node.raws object to store space symbol, etc.

所有与源代码相关的数据而不是 CSS 结构,都应该在 Node#raws 中。例如,postcss-scss 保留内联注释的 Comment#raws.inline 布尔标记(// comment 而不是 /* comment */)。

¥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.

这就是为什么默认字符串生成器有一个 raw() 方法来自动检测其他节点的原始属性。例如,它将查看其他节点来检测缩进大小,并将其与当前节点深度相乘。

¥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)

¥Tests

字符串生成器也必须有测试。

¥A stringifier must have tests too.

你可以使用 PostCSS 解析器测试 中的单元和集成测试用例。只需将输入 CSS 与解析器和字符串生成器之后的 CSS 进行比较即可。

¥You can use unit and integration test cases from PostCSS Parser Tests. Just compare input CSS with CSS after your parser and stringifier.