css 颜色提取分析功能
🏚️

css 颜色提取分析功能

Tags
工程化
Update
Mar 26, 2022 07:00 AM
Create
Aug 8, 2021 10:11 AM
项目中涉及到许多的样式文件,在之前的开发中没有统一规范,项目中的许多 css 颜色都是以 #fff 或者 red 的形式书写的,没有利用到 stylus 的变量功能,也不太好管理。因此,现在需要分析项目中的颜色变量。
出于通用性的考虑,一般的样式文件都会经过 postcss 处理,在此之前可能会经过其他的语法编译过程,因此最初的设想是是用 postcss 插件实现这一功能。
postcss 提供的插件机制非常强大,可以通过 walkDecls 遍历语法树种所有的声明节点(即 background: red 这种形式的),结合 postcss-value-parser 可以解析具体的变量。可以拿到所有的变量位置,变量名称,即可统计出项目中重复的颜色。
 
在最初的尝试中,我想要使用 parser 来对 stylus 进行预处理再放入 postcss, 但是其原理是先解析成 css 内容,再传入 postcss 中,丢失了原内容位置等信息。并且会遇到引用文件等问题。如果变量经过编译,就失去了统计的意义了。(我们不知道哪些颜色是未经过变量转换的)
// a.styl
$red = red
// 1.styl
@import 'a'
.a 
	color $red black

//希望解析后
@import 'a'
.a {
color: $red black
}
	
//实际上
.a {
color: red black
}
之后尝试了直接使用 stylus 的方案,其没有提供很好的外部访问机制,在查看其源码后发现,stylus 在编译的过程中,会先生成语法树(Parser),再对语法树的节点进行遍历访问处理(Evaluator),之后在编译成字符串(Normalizer)。通过 hack 的方法,我们可以修改 Evaluator 访问节点的方法:
function stylusVisitPlugin({ visitors = {} } = {}) {
    return function() {
        for(let name in visitors) {
            if(typeof visitors[name] !== 'function') {
                throw new Error(`visitor ${name} should be a function`);
            }
            const originVisitor = this.options.Evaluator.prototype[`visit${name}`];
            if(!originVisitor) {
                throw new Error(`no such visitor ${name}`);
            }
            this.options.Evaluator.prototype[`visit${name}`] = function(node) {
                const newNode = visitors[name].call(this, node) || node;
                return originVisitor.call(this, newNode);
            };
        }
    };
}
就可以使用插件以类似访问者的模式对语法树的特定节点访问拿到颜色值了。
 
但是上述做法是对 stylus 语言的特殊化处理,如果换成 sass 或者其他语言,就失效了,理想的方法还是在一个统一的地方进行处理。
受到 一文的启发, 可以放在 stylint 层面进行处理。 stylint 使用的也是 postcss。说明可能有办法让 postcss 拿到 stylus 的语法树。
查了一下 stylelint-stylus-plugin,发现它是用了 postcss-styl 对 stylus 进行 syntax(句法)解析。使用它,就可以在 postcss 插件中同时处理 css 和 stylus 样式了。
 
上述方案可以准确地拿到颜色所在位置,不需要在经过 sourcemap 的转换了。且 stylint 也使用了 postcss。 之后可以再进一步:定义 rule 使用 stylint 对这些颜色进行自动替换。
 
 

Stylelint 开发

RULE

Stylelint 基于 Postcss, 使用 Postcss Api 对 AST 节点进行访问。
(root, result) => {
// root check something
result.warn(...)

// fix
// get node by e.g. walkDecls
// mutate node
node.value = ''

}