diff --git a/packages/unocss-plugin/lib/index.js b/packages/unocss-plugin/lib/index.js index 74054bbad2..2a2fcc2ffd 100644 --- a/packages/unocss-plugin/lib/index.js +++ b/packages/unocss-plugin/lib/index.js @@ -14,11 +14,13 @@ const transformerVariantGroup = require('@unocss/transformer-variant-group') const { parseClasses, parseStrings, + parseMpxEscapeKeys, parseMustache, stringifyAttr, parseComments, parseCommentConfig } = require('./parser') +const { escapeKey, escapeClassName } = require('@mpxjs/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr') const { getReplaceSource, getConcatSource, getRawSource } = require('./source') const { transformStyle, @@ -358,6 +360,10 @@ class MpxUnocssPlugin { result = transformClasses(result, classNameHandler) expSource.replace(start, end, result) }) + parseMpxEscapeKeys(exp).forEach(({ result, start, end }) => { + const expanded = transformClasses(result, classNameHandler) + expSource.replace(start, end, escapeKey(escapeClassName(expanded))) + }) return expSource.source() }, str => transformClasses(str, classNameHandler)) if (replaced) { diff --git a/packages/unocss-plugin/lib/parser.js b/packages/unocss-plugin/lib/parser.js index 1ce02abc72..a27b47fce9 100644 --- a/packages/unocss-plugin/lib/parser.js +++ b/packages/unocss-plugin/lib/parser.js @@ -1,4 +1,5 @@ const { parseMustache, stringifyAttr } = require('@mpxjs/webpack-plugin/lib/template-compiler/compiler') +const { unescapeKey } = require('@mpxjs/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr') function parseClasses (content) { const output = [] @@ -78,9 +79,32 @@ function parseStrings (content) { return output } +// 匹配对象字面量中标识符形式的 key,如 { ml_da_17rpxMpxEscape: flag, a: true } +// key 前面必须是 { 或 ,(加可选空格),后面是 : +const objKeyReg = /(?:[{,]\s*)([\w-]+?)(?=\s*:)/gm + +function parseMpxEscapeKeys (content) { + const output = [] + if (!content) { return output } + let match + objKeyReg.lastIndex = 0 + while (match = objKeyReg.exec(content)) { + const raw = match[1] + const end = match.index + match[0].length - 1 + const start = end - raw.length + 1 + output.push({ + result: unescapeKey(raw), + start, + end + }) + } + return output +} + module.exports = { parseClasses, parseStrings, + parseMpxEscapeKeys, parseComments, parseCommentConfig, parseMustache, diff --git a/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js b/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js index ac61d261ed..e32a57cb49 100644 --- a/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js +++ b/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js @@ -3,8 +3,10 @@ const t = require('@babel/types') const traverse = require('@babel/traverse').default const generate = require('@babel/generator').default const isValidIdentifierStr = require('../utils/is-valid-identifier-str') -const escapeReg = /[()[\]{}#!.:,%'"+$]/g -const escapeMap = { +function escapeRegExp (str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} +const classNameEscapeMap = { '(': '_pl_', ')': '_pr_', '[': '_bl_', @@ -23,22 +25,66 @@ const escapeMap = { '+': '_a_', $: '_si_' } +const classNameEscapeReg = new RegExp('[' + Object.keys(classNameEscapeMap).map(escapeRegExp).join('') + ']', 'g') + +// classNameEscapeMap 的反向映射,用于还原 escapeClassName 编码 +const classNameDecodeMap = Object.keys(classNameEscapeMap).reduce((acc, key) => { + acc[classNameEscapeMap[key]] = key + return acc +}, {}) +const classNameDecodeReg = new RegExp(Object.keys(classNameDecodeMap).map(escapeRegExp).join('|'), 'g') -function mpEscape (str) { - return str.replace(escapeReg, function (match) { - if (escapeMap[match]) return escapeMap[match] +function escapeClassName (str) { + return str.replace(classNameEscapeReg, function (match) { + if (classNameEscapeMap[match]) return classNameEscapeMap[match] // unknown escaped return '_u_' }) } -function keyEscape (str) { - let result = str.replace(/-/g, '_da_').replace(/\s+/g, '_sp_') - if (result !== str) result += 'MpxEscape' - return result +function unescapeClassName (str) { + return str.replace(classNameDecodeReg, m => classNameDecodeMap[m] || m) +} + +const KEY_ESCAPE_SUFFIX = 'MpxEscape' +const KEY_ESCAPE_DASH = '_da_' +const KEY_ESCAPE_SPACE = '_sp_' + +const keyEscapeMap = { + '-': KEY_ESCAPE_DASH, + ' ': KEY_ESCAPE_SPACE, + '*': '_st_' } +const keyDecodeMap = Object.keys(keyEscapeMap).reduce((acc, key) => { + acc[keyEscapeMap[key]] = key + return acc +}, {}) +const keyDecodeReg = new RegExp(Object.keys(keyDecodeMap).map(escapeRegExp).join('|'), 'g') + +function escapeKey (str) { + const result = str.replace(/-/g, KEY_ESCAPE_DASH).replace(/\s+/g, KEY_ESCAPE_SPACE).replace(/\*/g, '_st_') + if (result !== str) return result + KEY_ESCAPE_SUFFIX + return str +} + +function unescapeKey (str) { + if (str.endsWith(KEY_ESCAPE_SUFFIX)) { + return unescapeClassName( + str.slice(0, -KEY_ESCAPE_SUFFIX.length).replace(keyDecodeReg, m => keyDecodeMap[m]) + ) + } + return str +} + +module.exports = transDynamicClassExpr +module.exports.KEY_ESCAPE_SUFFIX = KEY_ESCAPE_SUFFIX +module.exports.KEY_ESCAPE_DASH = KEY_ESCAPE_DASH +module.exports.KEY_ESCAPE_SPACE = KEY_ESCAPE_SPACE +module.exports.unescapeKey = unescapeKey +module.exports.escapeKey = escapeKey +module.exports.escapeClassName = escapeClassName -module.exports = function transDynamicClassExpr (expr, { error } = {}) { +function transDynamicClassExpr (expr, { error } = {}) { try { const ast = babylon.parse(expr, { plugins: [ @@ -50,7 +96,7 @@ module.exports = function transDynamicClassExpr (expr, { error } = {}) { path.node.properties.forEach((property) => { if (t.isObjectProperty(property) && !property.computed) { const rawPropertyName = property.key.name || property.key.value - const propertyName = keyEscape(mpEscape(rawPropertyName)) + const propertyName = escapeKey(escapeClassName(rawPropertyName)) if (!isValidIdentifierStr(propertyName)) { error && error(`Dynamic classname [${rawPropertyName}] can not be escaped as a valid identifier, which is not supported.`) } else {