如何编写自定义语法

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

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

有 3 种类型的 PostCSS 语法包

语法

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

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

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

解析器

解析器的良好示例是 Safe Parser,它解析格式错误/损坏的 CSS。因为生成损坏的输出毫无意义,所以此包仅提供解析器。

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

const postcss = require('postcss')

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

对于开源解析器,npm 包必须在 peerDependencies 中具有 postcss,而不能在直接 dependencies 中具有。

主要理论

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

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

  1. 标记化器逐个字符读取输入字符串并构建一个标记数组。例如,它将空格符号连接到一个 ['space', '\n '] 标记,并将字符串检测到一个 ['string', '"\"{"'] 标记。
  2. 解析器读取标记数组,创建节点实例并构建一个树。

性能

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

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

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

第二个优化是使用字符代码而不是字符串。

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

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

第三个优化是“快速跳转”。如果你找到引号,你可以通过 indexOf 更快地找到下一个引号。

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

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

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

Node 源

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

你的标记器应该保存原始位置,以便你可以将值传播到解析器,以确保源映射得到正确更新。

原始值

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

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

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

测试

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

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

字符串化器

样式指南生成器是字符串化器的良好示例。它生成包含 CSS 组件的输出 HTML。对于此用例,解析器不是必需的,因此软件包应仅包含一个字符串化器。

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

字符串化器是一个函数,它接收 RootDocument 节点和生成器回调。然后,它使用每个节点的字符串和节点实例调用生成器。

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

主要理论

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

在大多数情况下,仅仅扩展此类就足够了,例如在 SCSS 字符串化器 中。

构建器函数

生成器函数将作为第二个参数传递给 stringify 函数。例如,默认 PostCSS 字符串化器类会将其保存到 this.builder 属性。

生成器接收输出子字符串和源节点,以将此子字符串追加到最终输出。

一些节点在中间包含其他节点。例如,规则在开头有一个 {,内部有许多声明,并在结尾有一个 }

对于这些情况,你应向生成器函数传递第三个参数:'start''end' 字符串

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

原始值

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

这就是每个节点都有 node.raws 对象来存储空格符号等原因。

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

请小心,因为有时这些原始属性不会存在;某些节点可能是手动构建的,或者在移动到其他父节点时可能会丢失其缩进。

这就是默认字符串化器具有 raw() 方法通过其他节点自动检测原始属性的原因。例如,它将查看其他节点以检测缩进大小,然后将其乘以当前节点深度。

测试

字符串化器也必须有测试。

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