在「點石成金的秘術」中,我們用正規表示式實現了「將 css 屬性值的函式呼叫字串用計算結果替換」的目標,但深入去研磨正規表示式的匹配範圍是相當花心力的,因此這篇要介紹一個工具:postcss-value-parser
,主要功能是將 css 的屬性值解析成可操作的大 json ( AST ),如此就能解決我看不懂正規表示式還要裝懂的困境了~
程式碼
import valueParser from 'postcss-value-parser'
const cssValue = 'add(sub(1, 2),3) 100px'
const parsed = valueParser(cssValue)
parsed.walk(node => {
console.log(`[ ${valueParser.stringify(node)} 解析出來的 AST node ]`)
console.dir(node, {depth: null})
if (node.type === 'function' && node.value === 'add') {
node.type = 'word'
node.value = '2px'
}
})
console.log(parsed.toString())
valueParser
: 解析 css
屬性值,返回 AST。
parsed.walk
:
node
。node
內容。node
的類型改成 word
。node
的值改成你要替換的字串。add()
換成 2px
。parsed.toString
: 將整個 AST 轉回 css
屬性值。valueParser.stringify
: 將指定的 node
轉回 string
。結果
% node ./main.js
[ add(sub(1, 2),3) 解析出來的 AST node ]
{
type: 'function',
sourceIndex: 0,
value: 'add',
before: '',
after: '',
sourceEndIndex: 16,
nodes: [
{
type: 'function',
sourceIndex: 4,
value: 'sub',
before: '',
after: '',
sourceEndIndex: 13,
nodes: [
{ type: 'word', sourceIndex: 8, sourceEndIndex: 9, value: '1' },
{
type: 'div',
sourceIndex: 9,
sourceEndIndex: 11,
value: ',',
before: '',
after: ' '
},
{
type: 'word',
sourceIndex: 11,
sourceEndIndex: 12,
value: '2'
}
]
},
{
type: 'div',
sourceIndex: 13,
sourceEndIndex: 14,
value: ',',
before: '',
after: ''
},
{ type: 'word', sourceIndex: 14, sourceEndIndex: 15, value: '3' }
]
}
[ 解析出來的 AST node ]
{ type: 'space', sourceIndex: 16, sourceEndIndex: 17, value: ' ' }
[ 100px 解析出來的 AST node ]
{ type: 'word', sourceIndex: 17, sourceEndIndex: 22, value: '100px' }
2px 100px
valueParser
將 add(sub(1, 2),3) 100px
拆成三個 node:add(sub(1, 2),3)
、
、100px
。function
類型的參數部分會被拆解後放到 nodes
中,包括參數與逗號。
div
。add(sub(1, 2),3)
換成 2px
的效果成功了。我們先梳理一下應該做些什麼:
postcss
的 Declaration
拿到 css 屬性值。type: 'function'
的 node。value
)。function
(回到 3.
),最內層的函式呼叫需先執行後替換,才能處理外層函式呼叫。type
改成 word
。value
改成運算結果。postcss
。index.html
<link rel="stylesheet" href="./normal.css">
normal.css
p {
width: calc(add(multiply(3, 3), 2)px + 3px);
padding: calc(multiply(add(multiply(1, 2), multiply(3, 4)), 5)px + 3px) multiply(3, 3)px;
}
跟「點石成金的秘術」相同的模擬情境!
package.json
{
"type": "module",
"devDependencies": {
"postcss-load-config": "^6.0.1",
"postcss-value-parser": "^4.2.0",
"vite": "^7.1.4"
}
}
postcss.config.js
import valueParser from 'postcss-value-parser'
const transformNode = (node, functions) => {
if (node.type !== 'function' || !functions[node.value]) {
return node
}
const func = functions[node.value]
const args = extractArgs(node.nodes, functions)
const invocation = func.apply(func, args)
node.type = 'word'
node.value = invocation
return node
}
const extractArgs = (nodes, functions) => {
nodes = nodes.map(node => transformNode(node, functions))
const args = []
const last = nodes.reduce((prev, node) => {
if (node.type === 'div' && node.value === ',') {
args.push(prev)
return ''
}
return prev + valueParser.stringify(node)
}, '')
if (last) {
args.push(last)
}
return args
}
const myPlugin = function (functions = {}) {
return {
postcssPlugin: 'my-plugin',
Declaration: (decl) => {
const parsed = valueParser(decl.value)
parsed.walk((node) => {
transformNode(node, functions)
})
decl.value = parsed.toString()
},
}
}
myPlugin.postcss = true
/** @type {import('postcss-load-config').Config} */
export default {
plugins: [
myPlugin({
add: (a, b) => parseFloat(a) + parseFloat(b),
multiply: (a, b) => parseFloat(a) * parseFloat(b),
}),
],
}
add
與 multiply
。postcss
的 Declaration
拿到 css 屬性值。type: 'function'
的 node。
if (node.type !== 'function') {return node}
。value
)。
if (!functions[node.value]) {return node}
。extractArgs
方法,將函式參數挑出來。
3.
跑一遍,避免參數中有函式呼叫還沒先處理。,
前的字符都拼接起來。
prev + valueParser.stringify(node)
。,
時,就等於一個完整的參數被拼接完畢,此時將參數存起來。
args.push(prev)
。,
後面的字符拼湊為最後一個參數,也將它存起來。
args.push(last)
。type
改成 word
。value
改成運算結果。postcss
。結果
% npx vite build --minify false
dist/assets/index-CkZrhj5N.css 0.07 kB │ gzip: 0.07 kB
% cat ./dist/assets/index-CkZrhj5N.css
p {
width: calc(11px + 3px);
padding: calc(70px + 3px) 9px;
}
完美搞定!以上就是用 postcss-value-parser
來避免自己寫正規表示式的作法參考~
雖然 postcss-value-parser
讓我不用寫正規表示式了,但有沒有可能有個工具能直接指定我們想要攔截的函式名,就能幫我們處理這一切?下篇將分享我工作上實際使用的工具,下篇見囉~
在「煉金工房的核心設施」中,我們補充介紹了 Lightning.css
,而 Lightning.css
本身就內建類似 postcss-value-parser
的功能,他可以直接攔截某個函式呼叫:
import {transform} from 'lightningcss'
const res = transform({
minify: true,
code: Buffer.from(`
.foo {
padding: add(sub(1, 2),3);
}
`),
visitor: {
Function: {
add(funcs) {
console.log('[ 攔截 add() ]')
console.dir(funcs, {depth: null})
return {raw: '0_0'}
},
sub(funcs) {
console.log('[ 攔截 sub() ]')
console.dir(funcs, {depth: null})
return {raw: '=_='}
},
},
},
})
console.log(res.code.toString())
// [ 攔截 add() ]
// {
// name: 'add',
// arguments: [
// {
// type: 'function',
// value: {
// name: 'sub',
// arguments: [
// { type: 'token', value: { type: 'number', value: 1 } },
// { type: 'token', value: { type: 'comma' } },
// { type: 'token', value: { type: 'number', value: 2 } }
// ]
// }
// },
// { type: 'token', value: { type: 'comma' } },
// { type: 'token', value: { type: 'number', value: 3 } }
// ]
// }
// .foo{padding:0_0}
從 console 可以看出與 postcss-value-parser
的結構相當類似~分享給你。