Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat support css if #1778

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions packages/webpack-plugin/lib/style-compiler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const loadPostcssConfig = require('./load-postcss-config')
const { MPX_ROOT_VIEW, MPX_DISABLE_EXTRACTOR_CACHE } = require('../utils/const')
const rpx = require('./plugins/rpx')
const vw = require('./plugins/vw')
const pluginCondStrip = require('./plugins/conditional-strip')
const scopeId = require('./plugins/scope-id')
const transSpecial = require('./plugins/trans-special')
const cssArrayList = require('./plugins/css-array-list')
Expand Down Expand Up @@ -58,9 +57,9 @@ module.exports = function (css, map) {
plugins.push(transSpecial({ id }))
}

plugins.push(pluginCondStrip({
defs
}))
// plugins.push(pluginCondStrip({
// defs
// }))

for (const item of transRpxRules) {
const {
Expand Down
127 changes: 127 additions & 0 deletions packages/webpack-plugin/lib/style-compiler/strip-conditional-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
class Node {
constructor (type, condition = null) {
this.type = type // 'If', 'ElseIf', 'Else' 或 'Text'
this.condition = condition // If 或 Elif 的条件
this.children = []
this.value = ''
}
}

// 提取 css string 为 token
function tokenize (cssString) {
const regex = /\/\*\s*@mpx-(if|elif|else|end)(?:\s*\((.*?)\))?\s*\*\//g
const tokens = []
let lastIndex = 0
let match

while ((match = regex.exec(cssString)) !== null) {
// 如果 token 前有普通文本,生成文本 token
if (match.index > lastIndex) {
const text = cssString.substring(lastIndex, match.index)
tokens.push({ type: 'text', content: text })
}
// match[1] 为关键字:if, elif, else, end
// match[2] 为条件(如果存在)
tokens.push({
type: match[1], // 'if'、'elif'、'else' 或 'end'
condition: match[2] ? match[2].trim() : null
})
lastIndex = regex.lastIndex
}
// 处理结尾剩余的文本
if (lastIndex < cssString.length) {
const text = cssString.substring(lastIndex)
tokens.push({ type: 'text', content: text })
}
return tokens
}

// parse:将生成的 token 数组构造成嵌套的 AST
function parse (cssString) {
const tokens = tokenize(cssString)
const ast = []
const nodeStack = []
let currentChildren = ast
tokens.forEach(token => {
if (token.type === 'text') {
const node = new Node('Text')
node.value = token.content
currentChildren.push(node)
} else if (token.type === 'if') {
const node = new Node('If', token.condition)
currentChildren.push(node)
nodeStack.push(currentChildren)
currentChildren = node.children
} else if (token.type === 'elif') {
if (nodeStack.length === 0) {
throw new Error('elif without a preceding if')
}
currentChildren = nodeStack[nodeStack.length - 1]
const node = new Node('ElseIf', token.condition)
currentChildren.push(node)
currentChildren = node.children
} else if (token.type === 'else') {
if (nodeStack.length === 0) {
throw new Error('else without a preceding if')
}
currentChildren = nodeStack[nodeStack.length - 1]
const node = new Node('Else')
currentChildren.push(node)
currentChildren = node.children
} else if (token.type === 'end') {
if (nodeStack.length > 0) {
currentChildren = nodeStack.pop()
}
}
})
return ast
}

function evaluateCondition (condition, defs) {
try {
const keys = Object.keys(defs)
const values = keys.map(key => defs[key])
/* eslint-disable no-new-func */
const func = new Function(...keys, `return (${condition});`)
return func(...values)
} catch (e) {
console.error(`Error evaluating condition: ${condition}`, e)
return false
}
}

function traverseAndEvaluate (ast, defs) {
let output = ''
let batchedIf = false
function traverse (nodes) {
for (const node of nodes) {
if (node.type === 'Text') {
output += node.value
} else if (node.type === 'If') {
// 直接判断 If 节点
batchedIf = false
if (evaluateCondition(node.condition, defs)) {
traverse(node.children)
batchedIf = true
}
} else if (node.type === 'ElseIf' && !batchedIf) {
if (evaluateCondition(node.condition, defs)) {
traverse(node.children)
batchedIf = true
}
} else if (node.type === 'Else' && !batchedIf) {
traverse(node.children)
}
}
}
traverse(ast)
return output
}

module.exports = function (css) {
this.cacheable()
const mpx = this.getMpx()
const defs = mpx.defs
const ast = parse(css)
return traverseAndEvaluate(ast, defs)
}
198 changes: 198 additions & 0 deletions packages/webpack-plugin/test/platform/common/css-if.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
const cssIfLoader = require('../../../lib/style-compiler/strip-conditional-loader')

describe('css-if webpack loader - 测试用例', () => {
// 测试简单的 if/else 条件
it('简单条件: 当 isMobile 为 true 时,保留 if 分支内容', () => {
const context = {
cacheable: jest.fn(),
getMpx: () => ({ defs: { isMobile: true } })
}
const inputCSS = `
/*@mpx-if(isMobile)*/
.mobile { display: block; }
/*@mpx-else*/
.desktop { display: block; }
/*@mpx-end*/
`
const output = cssIfLoader.call(context, inputCSS)
expect(output).toContain('.mobile')
expect(output).not.toContain('.desktop')
})

it('简单条件: 当 isMobile 为 false 时,保留 else 分支内容', () => {
const context = {
cacheable: jest.fn(),
getMpx: () => ({ defs: { isMobile: false } })
}
const inputCSS = `
/*@mpx-if(isMobile)*/
.mobile { display: block; }
/*@mpx-else*/
.desktop { display: block; }
/*@mpx-end*/
`
const output = cssIfLoader.call(context, inputCSS)
expect(output).toContain('.desktop')
expect(output).not.toContain('.mobile')
})

// 测试嵌套条件
it('嵌套条件: 外层 isMobile 为 true 内层 hasFeature 为 true, 输出嵌套 if 分支内容', () => {
const context = {
cacheable: jest.fn(),
getMpx: () => ({ defs: { isMobile: true, hasFeature: true } })
}
const inputCSS = `
body { margin: 0; }
/*@mpx-if(isMobile)*/
.mobile {
display: block;
/*@mpx-if(hasFeature)*/
.feature { color: red; }
/*@mpx-else*/
.feature { color: blue; }
/*@mpx-end*/
}
/*@mpx-else*/
.desktop { display: block; }
/*@mpx-end*/
header { color: red }
`
const output = cssIfLoader.call(context, inputCSS)
expect(output).toContain('.mobile')
expect(output).toContain('.feature { color: red; }')
expect(output).toContain('header { color: red }')
expect(output).not.toContain('.feature { color: blue; }')
expect(output).not.toContain('.desktop')
})

it('嵌套条件: 外层 isMobile 为 true 内层 hasFeature 为 false, 输出内层 else 分支内容', () => {
const context = {
cacheable: jest.fn(),
getMpx: () => ({ defs: { isMobile: true, hasFeature: false } })
}
const inputCSS = `
body { margin: 0; }
/*@mpx-if(isMobile)*/
.mobile {
display: block;
/*@mpx-if(hasFeature)*/
.feature { color: red; }
/*@mpx-else*/
.feature { color: blue; }
/*@mpx-end*/
}
/*@mpx-else*/
.desktop { display: block; }
/*@mpx-end*/
`
const output = cssIfLoader.call(context, inputCSS)
expect(output).toContain('.mobile')
expect(output).toContain('.feature { color: blue; }')
expect(output).not.toContain('.feature { color: red; }')
expect(output).not.toContain('.desktop')
})

// 测试多个条件分支:if、elif、else 的情况
it('多个条件分支: 优先匹配 if 分支,其次 elif,再到 else', () => {
// 测试1: isMobile 为 false,isTablet 为 true,匹配 elif 分支
let context = {
cacheable: jest.fn(),
getMpx: () => ({ defs: { isMobile: false, isTablet: true } })
}
const inputCSS = `
header {}
/*@mpx-if(isMobile)*/
.mobile { display: block; }
/*@mpx-elif(isTablet)*/
.tablet { display: block; }
/*@mpx-else*/
.desktop { display: block; }
/*@mpx-end*/
body {}
`
let output = cssIfLoader.call(context, inputCSS)
expect(output).not.toContain('.mobile')
expect(output).toContain('.tablet')
expect(output).not.toContain('.desktop')
expect(output).toContain('header {}')
expect(output).toContain('body {}')

// 测试2: isMobile 与 isTablet 均为 false,匹配 else 分支
context = {
cacheable: jest.fn(),
getMpx: () => ({ defs: { isMobile: false, isTablet: false } })
}
output = cssIfLoader.call(context, inputCSS)
expect(output).not.toContain('.mobile')
expect(output).not.toContain('.tablet')
expect(output).toContain('.desktop')

// 测试3: isMobile 为 true(优先匹配 if 分支),即使 isTablet 为 true
context = {
cacheable: jest.fn(),
getMpx: () => ({ defs: { isMobile: true, isTablet: true } })
}
output = cssIfLoader.call(context, inputCSS)
expect(output).toContain('.mobile')
expect(output).not.toContain('.tablet')
expect(output).not.toContain('.desktop')
})

// 测试多个 if 块在一起的情况
it('多个 if 块处理: 不同 if 条件处理各自独立', () => {
const context = {
cacheable: jest.fn(),
getMpx: () => ({ defs: { isMobile: true, showHeader: false } })
}
const inputCSS = `
/*@mpx-if(isMobile)*/
.mobile { display: block; }
/*@mpx-end*/

/*@mpx-if(showHeader)*/
.header { height: 100px; }
/*@mpx-else*/
.header { height: 50px; }
/*@mpx-end*/
`
const output = cssIfLoader.call(context, inputCSS)
// 第一个 if 块:isMobile 为 true,输出 .mobile
expect(output).toContain('.mobile')
// 第二个 if 块:showHeader 为 false,应该保留 else 分支
expect(output).not.toContain('height: 100px;')
expect(output).toContain('height: 50px;')
})
it('多个 if 嵌套 elif 块处理', () => {
const context = {
cacheable: jest.fn(),
getMpx: () => ({ defs: { isMobile: true, showHeader: true } })
}
const inputCSS = `
/*@mpx-if(isMobile)*/
.mobile { display: block; }
/*@mpx-if(false)*/
.test1 {}
/*@mpx-elif(showHeader)*/
.test2 {}
/*@mpx-end*/
/*@mpx-end*/
`
const output = cssIfLoader.call(context, inputCSS)
expect(output).toContain('.mobile')
expect(output).toContain('.test2')
})

it('错误处理: 缺少开始标签', () => {
const context = {
cacheable: jest.fn(),
getMpx: () => ({ defs: { isMobile: true } })
}
const inputCSS = `
/*@mpx-elif(isMobile)*/
.mobile { display: block; }
`
// 预期这种情况下应该抛出异常或给出警告
expect(() => cssIfLoader.call(context, inputCSS)).toThrow(new Error('elif without a preceding if'))
})
})
Loading