css2class 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy-docs.yml +53 -0
- package/.head_config.mjs +68 -0
- package/CONFIG.md +38 -1
- package/README.md +595 -633
- package/bin/class2css.js +32 -4
- package/common.css +1 -1
- package/configs/typography.config.js +1 -0
- package/docs/.vitepress/config.mjs +68 -65
- package/docs/guide/cli.md +97 -97
- package/docs/guide/config-template.md +16 -1
- package/docs/guide/config.md +129 -64
- package/docs/guide/faq.md +202 -202
- package/docs/guide/getting-started.md +86 -83
- package/docs/guide/incremental.md +164 -162
- package/docs/guide/rules-reference.md +73 -1
- package/docs/index.md +71 -68
- package/examples/weapp/README.md +15 -0
- package/examples/weapp/class2css.config.js +70 -0
- package/examples/weapp/src/placeholder.wxml +0 -0
- package/examples/weapp/styles.config.js +201 -0
- package/examples/web/README.md +25 -0
- package/examples/web/class2css.config.js +70 -0
- package/examples/web/demo.html +105 -0
- package/examples/web/src/placeholder.html +5 -0
- package/examples/web/styles.config.js +201 -0
- package/package.json +7 -2
- package/src/README.md +99 -0
- package/src/core/ConfigManager.js +440 -431
- package/src/core/FullScanManager.js +438 -430
- package/src/generators/DynamicClassGenerator.js +270 -72
- package/src/index.js +1091 -1046
- package/src/parsers/ClassParser.js +8 -2
- package/src/utils/CssFormatter.js +400 -47
- package/src/utils/UnitProcessor.js +4 -3
- package/src/watchers/ConfigWatcher.js +413 -413
- package/src/watchers/FileWatcher.js +148 -133
- package/src/writers/FileWriter.js +444 -302
- package/src/writers/UnifiedWriter.js +413 -370
- package/class2css.config.js +0 -124
- package/styles.config.js +0 -250
|
@@ -51,8 +51,12 @@ class ClassParser {
|
|
|
51
51
|
// 智能类名预处理和验证(使用 base class 进行分析)
|
|
52
52
|
const processedClass = this.preprocessClassName(className, cleanName);
|
|
53
53
|
|
|
54
|
+
const isStaticHit = this.userStaticClassSet.has(baseClass);
|
|
55
|
+
// 如果命中静态类定义,则不再把它当成动态类候选,避免同一 selector 重复生成两份规则
|
|
56
|
+
const isDynamicCandidate = baseClass.includes('-') && !isStaticHit;
|
|
57
|
+
|
|
54
58
|
// 先检查是否是静态类(使用 base class 检查)
|
|
55
|
-
if (
|
|
59
|
+
if (isStaticHit) {
|
|
56
60
|
userStaticList.add(processedClass.original); // 保存原始 class 名(如 sm:flex)
|
|
57
61
|
this.eventBus.emit('parser:static:found', {
|
|
58
62
|
className: processedClass.original,
|
|
@@ -63,7 +67,7 @@ class ClassParser {
|
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
// 再检查是否是动态类(使用 base class 检查)
|
|
66
|
-
if (
|
|
70
|
+
if (isDynamicCandidate) {
|
|
67
71
|
classList.add(processedClass.original); // 保存原始 class 名(如 sm:w-100)
|
|
68
72
|
this.eventBus.emit('parser:dynamic:found', {
|
|
69
73
|
className: processedClass.original,
|
|
@@ -173,6 +177,7 @@ class ClassParser {
|
|
|
173
177
|
'text',
|
|
174
178
|
'font',
|
|
175
179
|
'leading',
|
|
180
|
+
'lh',
|
|
176
181
|
'tracking',
|
|
177
182
|
'spacing',
|
|
178
183
|
'top',
|
|
@@ -219,6 +224,7 @@ class ClassParser {
|
|
|
219
224
|
text: 'font-size',
|
|
220
225
|
font: 'font-weight',
|
|
221
226
|
leading: 'line-height',
|
|
227
|
+
lh: 'line-height',
|
|
222
228
|
tracking: 'letter-spacing',
|
|
223
229
|
top: 'top',
|
|
224
230
|
right: 'right',
|
|
@@ -3,6 +3,36 @@ class CssFormatter {
|
|
|
3
3
|
this.format = format;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
// 将 states 变体转换为 CSS 伪类选择器片段
|
|
7
|
+
// 例如:['hover','first','odd'] -> ':hover:first-child:nth-child(odd)'
|
|
8
|
+
buildStatePseudoSelectors(stateVariants = []) {
|
|
9
|
+
if (!Array.isArray(stateVariants) || stateVariants.length === 0) {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const map = {
|
|
14
|
+
hover: 'hover',
|
|
15
|
+
focus: 'focus',
|
|
16
|
+
active: 'active',
|
|
17
|
+
disabled: 'disabled',
|
|
18
|
+
first: 'first-child',
|
|
19
|
+
last: 'last-child',
|
|
20
|
+
odd: 'nth-child(odd)',
|
|
21
|
+
even: 'nth-child(even)',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const pseudos = [];
|
|
25
|
+
for (const v of stateVariants) {
|
|
26
|
+
if (!v || typeof v !== 'string') continue;
|
|
27
|
+
const key = v.trim();
|
|
28
|
+
if (!key) continue;
|
|
29
|
+
const mapped = map[key] || key; // 允许用户扩展为标准伪类(如 visited、focus-within 等)
|
|
30
|
+
pseudos.push(mapped);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return pseudos.length > 0 ? ':' + pseudos.join(':') : '';
|
|
34
|
+
}
|
|
35
|
+
|
|
6
36
|
// 转义 CSS 选择器中的特殊字符
|
|
7
37
|
// 确保生成的 CSS 选择器能正确匹配 HTML/WXML 中的 class 名
|
|
8
38
|
escapeSelector(selector) {
|
|
@@ -31,26 +61,30 @@ class CssFormatter {
|
|
|
31
61
|
}
|
|
32
62
|
|
|
33
63
|
// 格式化单个CSS规则
|
|
34
|
-
|
|
64
|
+
// stateVariants: 状态变体数组(如 ['hover', 'focus']),用于生成伪类选择器
|
|
65
|
+
formatRule(selector, properties, format = null, stateVariants = []) {
|
|
35
66
|
const targetFormat = format || this.format;
|
|
36
67
|
|
|
68
|
+
// 构建带伪类的选择器
|
|
69
|
+
const escapedSelector = this.escapeSelector(selector);
|
|
70
|
+
const pseudoSelectors = this.buildStatePseudoSelectors(stateVariants);
|
|
71
|
+
const fullSelector = escapedSelector + pseudoSelectors;
|
|
72
|
+
|
|
37
73
|
if (targetFormat === 'compressed') {
|
|
38
|
-
return this.formatRuleCompressed(
|
|
74
|
+
return this.formatRuleCompressed(fullSelector, properties);
|
|
39
75
|
} else if (targetFormat === 'singleLine') {
|
|
40
|
-
return this.formatRuleSingleLine(
|
|
76
|
+
return this.formatRuleSingleLine(fullSelector, properties);
|
|
41
77
|
} else {
|
|
42
|
-
return this.formatRuleMultiLine(
|
|
78
|
+
return this.formatRuleMultiLine(fullSelector, properties);
|
|
43
79
|
}
|
|
44
80
|
}
|
|
45
81
|
|
|
46
82
|
// 多行格式(默认)
|
|
83
|
+
// 注意:selector 参数已经包含转义和伪类(由 formatRule 处理)
|
|
47
84
|
formatRuleMultiLine(selector, properties) {
|
|
48
|
-
// 转义选择器中的特殊字符
|
|
49
|
-
const escapedSelector = this.escapeSelector(selector);
|
|
50
|
-
|
|
51
85
|
if (Array.isArray(properties)) {
|
|
52
86
|
// 属性数组格式:[{property: 'margin', value: '10rpx'}, ...]
|
|
53
|
-
let css = `\n.${
|
|
87
|
+
let css = `\n.${selector} {\n`;
|
|
54
88
|
properties.forEach(({ property, value }) => {
|
|
55
89
|
css += ` ${property}: ${value};\n`;
|
|
56
90
|
});
|
|
@@ -58,36 +92,32 @@ class CssFormatter {
|
|
|
58
92
|
return css;
|
|
59
93
|
} else if (typeof properties === 'string') {
|
|
60
94
|
// 字符串格式:'margin: 10rpx;'
|
|
61
|
-
return `\n.${
|
|
95
|
+
return `\n.${selector} {\n ${properties}\n}\n`;
|
|
62
96
|
}
|
|
63
97
|
return '';
|
|
64
98
|
}
|
|
65
99
|
|
|
66
100
|
// 单行格式
|
|
101
|
+
// 注意:selector 参数已经包含转义和伪类(由 formatRule 处理)
|
|
67
102
|
formatRuleSingleLine(selector, properties) {
|
|
68
|
-
// 转义选择器中的特殊字符
|
|
69
|
-
const escapedSelector = this.escapeSelector(selector);
|
|
70
|
-
|
|
71
103
|
if (Array.isArray(properties)) {
|
|
72
104
|
// 属性数组格式
|
|
73
105
|
const propsStr = properties.map(({ property, value }) => `${property}: ${value}`).join('; ');
|
|
74
|
-
return `.${
|
|
106
|
+
return `.${selector} { ${propsStr}; }\n`;
|
|
75
107
|
} else if (typeof properties === 'string') {
|
|
76
108
|
// 字符串格式
|
|
77
|
-
return `.${
|
|
109
|
+
return `.${selector} { ${properties}; }\n`;
|
|
78
110
|
}
|
|
79
111
|
return '';
|
|
80
112
|
}
|
|
81
113
|
|
|
82
114
|
// 压缩格式
|
|
115
|
+
// 注意:selector 参数已经包含转义和伪类(由 formatRule 处理)
|
|
83
116
|
formatRuleCompressed(selector, properties) {
|
|
84
|
-
// 转义选择器中的特殊字符
|
|
85
|
-
const escapedSelector = this.escapeSelector(selector);
|
|
86
|
-
|
|
87
117
|
if (Array.isArray(properties)) {
|
|
88
118
|
// 属性数组格式
|
|
89
119
|
const propsStr = properties.map(({ property, value }) => `${property}:${value}`).join(';');
|
|
90
|
-
return `.${
|
|
120
|
+
return `.${selector}{${propsStr}}`;
|
|
91
121
|
} else if (typeof properties === 'string') {
|
|
92
122
|
// 字符串格式,移除空格但保留分号(除了最后一个分号)
|
|
93
123
|
let cleanProps = properties.replace(/\s+/g, ''); // 移除所有空格
|
|
@@ -95,7 +125,7 @@ class CssFormatter {
|
|
|
95
125
|
cleanProps = cleanProps.replace(/;+$/, ''); // 移除末尾的分号
|
|
96
126
|
// 确保分号后面没有空格(虽然已经移除了空格,但为了安全)
|
|
97
127
|
cleanProps = cleanProps.replace(/;+/g, ';'); // 多个分号合并为一个
|
|
98
|
-
return `.${
|
|
128
|
+
return `.${selector}{${cleanProps}}`;
|
|
99
129
|
}
|
|
100
130
|
return '';
|
|
101
131
|
}
|
|
@@ -118,8 +148,29 @@ class CssFormatter {
|
|
|
118
148
|
}
|
|
119
149
|
|
|
120
150
|
// 压缩CSS(去除所有空格、换行、注释)
|
|
151
|
+
// 但保留 CLASS2CSS 分区标记注释(用于 appendDelta 模式)
|
|
121
152
|
compressCSS(cssString) {
|
|
122
|
-
|
|
153
|
+
// 先提取并临时替换分区标记,避免被移除
|
|
154
|
+
const partitionMarkers = {
|
|
155
|
+
baseStart: '/* CLASS2CSS:BASE_START */',
|
|
156
|
+
baseEnd: '/* CLASS2CSS:BASE_END */',
|
|
157
|
+
mediaStart: '/* CLASS2CSS:MEDIA_START */',
|
|
158
|
+
mediaEnd: '/* CLASS2CSS:MEDIA_END */',
|
|
159
|
+
deltaStart: '/* CLASS2CSS:DELTA_START */',
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const placeholders = {};
|
|
163
|
+
let tempCss = cssString;
|
|
164
|
+
for (const [key, marker] of Object.entries(partitionMarkers)) {
|
|
165
|
+
if (tempCss.includes(marker)) {
|
|
166
|
+
const placeholder = `__CLASS2CSS_${key.toUpperCase()}__`;
|
|
167
|
+
placeholders[placeholder] = marker;
|
|
168
|
+
tempCss = tempCss.replace(new RegExp(marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), placeholder);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 压缩 CSS(移除注释等)
|
|
173
|
+
let compressed = tempCss
|
|
123
174
|
.replace(/\/\*[\s\S]*?\*\//g, '') // 移除注释
|
|
124
175
|
.replace(/\s+/g, ' ') // 多个空格合并为一个
|
|
125
176
|
.replace(/\s*{\s*/g, '{') // 移除大括号周围的空格
|
|
@@ -130,6 +181,13 @@ class CssFormatter {
|
|
|
130
181
|
.replace(/;\s*}/g, '}') // 移除结束大括号前的分号(CSS规则结束不需要分号)
|
|
131
182
|
.replace(/\s+/g, '') // 移除所有剩余空格
|
|
132
183
|
.trim();
|
|
184
|
+
|
|
185
|
+
// 恢复分区标记
|
|
186
|
+
for (const [placeholder, marker] of Object.entries(placeholders)) {
|
|
187
|
+
compressed = compressed.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), marker);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return compressed;
|
|
133
191
|
}
|
|
134
192
|
|
|
135
193
|
// 单行CSS(每个规则单行,但规则之间换行)
|
|
@@ -191,69 +249,364 @@ class CssFormatter {
|
|
|
191
249
|
return '';
|
|
192
250
|
}
|
|
193
251
|
|
|
194
|
-
//
|
|
195
|
-
|
|
252
|
+
// 规范化响应式规则顺序:
|
|
253
|
+
// - 保证普通规则(base)在前
|
|
254
|
+
// - 保证 @media 块在后(并按 min-width 升序稳定排序)
|
|
255
|
+
// 该步骤不做“字母排序”,只解决媒体查询被覆盖导致的失效问题。
|
|
256
|
+
normalizeResponsiveOrder(cssString) {
|
|
196
257
|
if (!cssString || typeof cssString !== 'string') {
|
|
197
258
|
return '';
|
|
198
259
|
}
|
|
199
260
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
let
|
|
261
|
+
const blocks = [];
|
|
262
|
+
let prefix = '';
|
|
263
|
+
let suffix = '';
|
|
264
|
+
|
|
265
|
+
let i = 0;
|
|
203
266
|
let braceCount = 0;
|
|
204
|
-
let
|
|
267
|
+
let blockStart = -1;
|
|
268
|
+
let cursor = 0;
|
|
269
|
+
|
|
270
|
+
while (i < cssString.length) {
|
|
271
|
+
const char = cssString[i];
|
|
272
|
+
|
|
273
|
+
if (char === '{') {
|
|
274
|
+
if (braceCount === 0) {
|
|
275
|
+
const preSelector = cssString.slice(cursor, i);
|
|
276
|
+
|
|
277
|
+
// 尽量把无大括号语句(常见:@import/@charset)保留在 prefix
|
|
278
|
+
const lastSemicolon = preSelector.lastIndexOf(';');
|
|
279
|
+
if (lastSemicolon !== -1) {
|
|
280
|
+
const before = preSelector.slice(0, lastSemicolon + 1);
|
|
281
|
+
if (blocks.length === 0) {
|
|
282
|
+
prefix += before;
|
|
283
|
+
}
|
|
284
|
+
blockStart = cursor + lastSemicolon + 1;
|
|
285
|
+
} else {
|
|
286
|
+
blockStart = cursor;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
braceCount++;
|
|
290
|
+
} else if (char === '}') {
|
|
291
|
+
braceCount--;
|
|
292
|
+
if (braceCount === 0 && blockStart !== -1) {
|
|
293
|
+
const fullRule = cssString.slice(blockStart, i + 1);
|
|
294
|
+
const openBraceIdx = fullRule.indexOf('{');
|
|
295
|
+
const selectorRaw = openBraceIdx === -1 ? '' : fullRule.slice(0, openBraceIdx);
|
|
296
|
+
const selectorTrim = selectorRaw.trim();
|
|
297
|
+
const cleanSelector = selectorTrim.replace(/^\./, '').trim();
|
|
298
|
+
|
|
299
|
+
if (cleanSelector) {
|
|
300
|
+
blocks.push({
|
|
301
|
+
selectorTrim,
|
|
302
|
+
fullRule,
|
|
303
|
+
index: blocks.length,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
cursor = i + 1;
|
|
308
|
+
blockStart = -1;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
i++;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (cursor < cssString.length) {
|
|
316
|
+
suffix = cssString.slice(cursor);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (blocks.length === 0) {
|
|
320
|
+
return cssString;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const isMediaBlock = (b) => typeof b.selectorTrim === 'string' && b.selectorTrim.startsWith('@media');
|
|
324
|
+
const isAtRuleBlock = (b) => typeof b.selectorTrim === 'string' && b.selectorTrim.startsWith('@');
|
|
325
|
+
|
|
326
|
+
const parseMinWidth = (selectorTrim) => {
|
|
327
|
+
if (!selectorTrim) return Number.POSITIVE_INFINITY;
|
|
328
|
+
const m = selectorTrim.match(/min-width\s*:\s*([^)]+)\)/i);
|
|
329
|
+
if (!m) return Number.POSITIVE_INFINITY;
|
|
330
|
+
const raw = String(m[1] || '').trim();
|
|
331
|
+
const num = parseFloat(raw);
|
|
332
|
+
return Number.isFinite(num) ? num : Number.POSITIVE_INFINITY;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const baseBlocks = [];
|
|
336
|
+
const mediaBlocks = [];
|
|
337
|
+
const otherAtBlocks = [];
|
|
338
|
+
|
|
339
|
+
for (const b of blocks) {
|
|
340
|
+
if (isMediaBlock(b)) {
|
|
341
|
+
mediaBlocks.push(b);
|
|
342
|
+
} else if (isAtRuleBlock(b)) {
|
|
343
|
+
otherAtBlocks.push(b);
|
|
344
|
+
} else {
|
|
345
|
+
baseBlocks.push(b);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 不改变 base 的原始顺序(稳定)
|
|
350
|
+
baseBlocks.sort((a, b) => a.index - b.index);
|
|
351
|
+
otherAtBlocks.sort((a, b) => a.index - b.index);
|
|
352
|
+
|
|
353
|
+
// media 按 min-width 升序(同断点保持稳定)
|
|
354
|
+
mediaBlocks.sort((a, b) => {
|
|
355
|
+
const wa = parseMinWidth(a.selectorTrim);
|
|
356
|
+
const wb = parseMinWidth(b.selectorTrim);
|
|
357
|
+
if (wa < wb) return -1;
|
|
358
|
+
if (wa > wb) return 1;
|
|
359
|
+
return a.index - b.index;
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
prefix +
|
|
364
|
+
otherAtBlocks.map((b) => b.fullRule).join('') +
|
|
365
|
+
baseBlocks.map((b) => b.fullRule).join('') +
|
|
366
|
+
mediaBlocks.map((b) => b.fullRule).join('') +
|
|
367
|
+
suffix
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 拆分 CSS 为 base 规则和 @media 规则(用于 appendDelta 分区插入)
|
|
372
|
+
// 返回 { baseCss, mediaCss, otherAtRulesPrefix, suffix }
|
|
373
|
+
splitBaseAndMedia(cssString) {
|
|
374
|
+
if (!cssString || typeof cssString !== 'string') {
|
|
375
|
+
return { baseCss: '', mediaCss: '', otherAtRulesPrefix: '', suffix: '' };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const blocks = [];
|
|
379
|
+
let prefix = '';
|
|
380
|
+
let suffix = '';
|
|
381
|
+
|
|
205
382
|
let i = 0;
|
|
383
|
+
let braceCount = 0;
|
|
384
|
+
let blockStart = -1;
|
|
385
|
+
let cursor = 0;
|
|
206
386
|
|
|
207
387
|
while (i < cssString.length) {
|
|
208
388
|
const char = cssString[i];
|
|
209
389
|
|
|
210
390
|
if (char === '{') {
|
|
211
391
|
if (braceCount === 0) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
392
|
+
const preSelector = cssString.slice(cursor, i);
|
|
393
|
+
|
|
394
|
+
// 尽量把无大括号语句(常见:@import/@charset)保留在 prefix
|
|
395
|
+
const lastSemicolon = preSelector.lastIndexOf(';');
|
|
396
|
+
if (lastSemicolon !== -1) {
|
|
397
|
+
const before = preSelector.slice(0, lastSemicolon + 1);
|
|
398
|
+
if (blocks.length === 0) {
|
|
399
|
+
prefix += before;
|
|
400
|
+
}
|
|
401
|
+
blockStart = cursor + lastSemicolon + 1;
|
|
402
|
+
} else {
|
|
403
|
+
blockStart = cursor;
|
|
404
|
+
}
|
|
217
405
|
}
|
|
218
406
|
braceCount++;
|
|
219
407
|
} else if (char === '}') {
|
|
220
|
-
currentRule += char;
|
|
221
408
|
braceCount--;
|
|
409
|
+
if (braceCount === 0 && blockStart !== -1) {
|
|
410
|
+
const fullRule = cssString.slice(blockStart, i + 1);
|
|
411
|
+
const openBraceIdx = fullRule.indexOf('{');
|
|
412
|
+
const selectorRaw = openBraceIdx === -1 ? '' : fullRule.slice(0, openBraceIdx);
|
|
413
|
+
const selectorTrim = selectorRaw.trim();
|
|
414
|
+
|
|
415
|
+
if (selectorTrim) {
|
|
416
|
+
blocks.push({
|
|
417
|
+
selectorTrim,
|
|
418
|
+
fullRule,
|
|
419
|
+
index: blocks.length,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
cursor = i + 1;
|
|
424
|
+
blockStart = -1;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
i++;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (cursor < cssString.length) {
|
|
432
|
+
suffix = cssString.slice(cursor);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (blocks.length === 0) {
|
|
436
|
+
return { baseCss: cssString, mediaCss: '', otherAtRulesPrefix: prefix, suffix };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const isMediaBlock = (b) => typeof b.selectorTrim === 'string' && b.selectorTrim.startsWith('@media');
|
|
440
|
+
const isAtRuleBlock = (b) => typeof b.selectorTrim === 'string' && b.selectorTrim.startsWith('@');
|
|
441
|
+
|
|
442
|
+
const baseBlocks = [];
|
|
443
|
+
const mediaBlocks = [];
|
|
444
|
+
const otherAtBlocks = [];
|
|
445
|
+
|
|
446
|
+
for (const b of blocks) {
|
|
447
|
+
if (isMediaBlock(b)) {
|
|
448
|
+
mediaBlocks.push(b);
|
|
449
|
+
} else if (isAtRuleBlock(b)) {
|
|
450
|
+
otherAtBlocks.push(b);
|
|
451
|
+
} else {
|
|
452
|
+
baseBlocks.push(b);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 保持原始顺序(不排序)
|
|
457
|
+
const baseCss = baseBlocks.map((b) => b.fullRule).join('');
|
|
458
|
+
const mediaCss = mediaBlocks.map((b) => b.fullRule).join('');
|
|
459
|
+
const otherAtRulesPrefix = prefix + otherAtBlocks.map((b) => b.fullRule).join('');
|
|
460
|
+
|
|
461
|
+
return { baseCss, mediaCss, otherAtRulesPrefix, suffix };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 对CSS规则进行字母排序
|
|
465
|
+
sortCSSRules(cssString) {
|
|
466
|
+
if (!cssString || typeof cssString !== 'string') {
|
|
467
|
+
return '';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 解析顶层块(支持 @media 嵌套),并按语义分组排序。
|
|
471
|
+
// 目标:避免 @media 块被排到普通规则前面导致覆盖失效(base 覆盖 media)。
|
|
472
|
+
// 同时尽量保留非块语句(如 @import/@charset)在输出中的存在。
|
|
473
|
+
|
|
474
|
+
const blocks = [];
|
|
475
|
+
let prefix = '';
|
|
476
|
+
let suffix = '';
|
|
477
|
+
|
|
478
|
+
let i = 0;
|
|
479
|
+
let braceCount = 0;
|
|
480
|
+
let blockStart = -1;
|
|
481
|
+
let selectorStart = 0;
|
|
482
|
+
let cursor = 0;
|
|
483
|
+
|
|
484
|
+
while (i < cssString.length) {
|
|
485
|
+
const char = cssString[i];
|
|
486
|
+
|
|
487
|
+
if (char === '{') {
|
|
222
488
|
if (braceCount === 0) {
|
|
223
|
-
//
|
|
489
|
+
// 顶层块开始:selector 在 cursor..i 之间
|
|
490
|
+
const preSelector = cssString.slice(cursor, i);
|
|
491
|
+
|
|
492
|
+
// 尽量把无大括号的 at-rule 语句(常见:@import/@charset)留在 prefix 中
|
|
493
|
+
// 规则:如果 preSelector 中有分号,将最后一个分号之前的内容视为 prefix
|
|
494
|
+
const lastSemicolon = preSelector.lastIndexOf(';');
|
|
495
|
+
if (lastSemicolon !== -1) {
|
|
496
|
+
const before = preSelector.slice(0, lastSemicolon + 1);
|
|
497
|
+
const after = preSelector.slice(lastSemicolon + 1);
|
|
498
|
+
// prefix 只在第一个块之前累积
|
|
499
|
+
if (blocks.length === 0) {
|
|
500
|
+
prefix += before;
|
|
501
|
+
}
|
|
502
|
+
selectorStart = cursor + lastSemicolon + 1;
|
|
503
|
+
// 把分号后的空白等内容归到块里,避免丢失格式
|
|
504
|
+
blockStart = selectorStart;
|
|
505
|
+
// after 这段会包含在 blockStart.. 中,不需要单独处理
|
|
506
|
+
} else {
|
|
507
|
+
selectorStart = cursor;
|
|
508
|
+
blockStart = selectorStart;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
braceCount++;
|
|
512
|
+
} else if (char === '}') {
|
|
513
|
+
braceCount--;
|
|
514
|
+
if (braceCount === 0 && blockStart !== -1) {
|
|
515
|
+
// 顶层块结束
|
|
516
|
+
const fullRule = cssString.slice(blockStart, i + 1);
|
|
517
|
+
const openBraceIdx = fullRule.indexOf('{');
|
|
518
|
+
const selectorRaw = openBraceIdx === -1 ? '' : fullRule.slice(0, openBraceIdx);
|
|
519
|
+
const selectorTrim = selectorRaw.trim();
|
|
520
|
+
|
|
224
521
|
// 清理选择器:移除点号前缀用于排序(注意:转义字符不需要处理,排序时保持原样)
|
|
225
|
-
const cleanSelector =
|
|
522
|
+
const cleanSelector = selectorTrim.replace(/^\./, '').trim();
|
|
523
|
+
|
|
226
524
|
if (cleanSelector) {
|
|
227
|
-
|
|
525
|
+
blocks.push({
|
|
228
526
|
selector: cleanSelector,
|
|
229
|
-
|
|
527
|
+
selectorTrim,
|
|
528
|
+
fullRule,
|
|
529
|
+
index: blocks.length, // 用于稳定排序
|
|
230
530
|
});
|
|
231
531
|
}
|
|
232
|
-
|
|
233
|
-
|
|
532
|
+
|
|
533
|
+
cursor = i + 1;
|
|
534
|
+
blockStart = -1;
|
|
535
|
+
selectorStart = cursor;
|
|
234
536
|
}
|
|
235
|
-
} else {
|
|
236
|
-
currentRule += char;
|
|
237
537
|
}
|
|
538
|
+
|
|
238
539
|
i++;
|
|
239
540
|
}
|
|
240
541
|
|
|
241
|
-
//
|
|
242
|
-
if (
|
|
542
|
+
// 保留尾部非块内容(例如末尾注释/换行)
|
|
543
|
+
if (cursor < cssString.length) {
|
|
544
|
+
suffix = cssString.slice(cursor);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// 如果没有解析到块,直接返回原字符串
|
|
548
|
+
if (blocks.length === 0) {
|
|
243
549
|
return cssString;
|
|
244
550
|
}
|
|
245
551
|
|
|
246
|
-
|
|
247
|
-
|
|
552
|
+
const isMediaBlock = (b) => typeof b.selectorTrim === 'string' && b.selectorTrim.startsWith('@media');
|
|
553
|
+
const isAtRuleBlock = (b) => typeof b.selectorTrim === 'string' && b.selectorTrim.startsWith('@');
|
|
554
|
+
|
|
555
|
+
const parseMinWidth = (selectorTrim) => {
|
|
556
|
+
if (!selectorTrim) return Number.POSITIVE_INFINITY;
|
|
557
|
+
// 兼容:@media(min-width:640px) / @media (min-width: 640px)
|
|
558
|
+
const m = selectorTrim.match(/min-width\s*:\s*([^)]+)\)/i);
|
|
559
|
+
if (!m) return Number.POSITIVE_INFINITY;
|
|
560
|
+
const raw = String(m[1] || '').trim();
|
|
561
|
+
const num = parseFloat(raw);
|
|
562
|
+
return Number.isFinite(num) ? num : Number.POSITIVE_INFINITY;
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const baseBlocks = [];
|
|
566
|
+
const mediaBlocks = [];
|
|
567
|
+
const otherAtBlocks = [];
|
|
568
|
+
|
|
569
|
+
for (const b of blocks) {
|
|
570
|
+
if (isMediaBlock(b)) {
|
|
571
|
+
mediaBlocks.push(b);
|
|
572
|
+
} else if (isAtRuleBlock(b)) {
|
|
573
|
+
otherAtBlocks.push(b);
|
|
574
|
+
} else {
|
|
575
|
+
baseBlocks.push(b);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// base:按选择器字母排序(不区分大小写)
|
|
580
|
+
baseBlocks.sort((a, b) => {
|
|
248
581
|
const selectorA = a.selector.toLowerCase();
|
|
249
582
|
const selectorB = b.selector.toLowerCase();
|
|
250
583
|
if (selectorA < selectorB) return -1;
|
|
251
584
|
if (selectorA > selectorB) return 1;
|
|
252
|
-
return
|
|
585
|
+
return a.index - b.index; // 稳定
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// media:按 min-width 升序(同断点保持稳定)
|
|
589
|
+
mediaBlocks.sort((a, b) => {
|
|
590
|
+
const wa = parseMinWidth(a.selectorTrim);
|
|
591
|
+
const wb = parseMinWidth(b.selectorTrim);
|
|
592
|
+
if (wa < wb) return -1;
|
|
593
|
+
if (wa > wb) return 1;
|
|
594
|
+
return a.index - b.index;
|
|
253
595
|
});
|
|
254
596
|
|
|
255
|
-
//
|
|
256
|
-
|
|
597
|
+
// 组合顺序:
|
|
598
|
+
// - prefix(@import/@charset 等)
|
|
599
|
+
// - 其他 at-rule(保持原顺序)
|
|
600
|
+
// - base 规则(排序后)
|
|
601
|
+
// - @media(最后,保证覆盖方向正确)
|
|
602
|
+
// - suffix
|
|
603
|
+
return (
|
|
604
|
+
prefix +
|
|
605
|
+
otherAtBlocks.map((b) => b.fullRule).join('') +
|
|
606
|
+
baseBlocks.map((b) => b.fullRule).join('') +
|
|
607
|
+
mediaBlocks.map((b) => b.fullRule).join('') +
|
|
608
|
+
suffix
|
|
609
|
+
);
|
|
257
610
|
}
|
|
258
611
|
}
|
|
259
612
|
|
|
@@ -54,7 +54,8 @@ class UnitProcessor {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// 智能解析数值和单位
|
|
57
|
-
|
|
57
|
+
// options.skipSpecialProcessing: true 时跳过特殊属性处理(如 line-height 的倍数转换),直接附加单位
|
|
58
|
+
parseValue(value, property, defaultUnit = null, options = {}) {
|
|
58
59
|
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
59
60
|
return value;
|
|
60
61
|
}
|
|
@@ -78,8 +79,8 @@ class UnitProcessor {
|
|
|
78
79
|
return valueStr; // 无法解析,保持原值
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
//
|
|
82
|
-
if (this.isSpecialProperty(property, numericValue)) {
|
|
82
|
+
// 处理特殊属性(skipSpecialProcessing 为 true 时跳过,直接走单位转换)
|
|
83
|
+
if (!options.skipSpecialProcessing && this.isSpecialProperty(property, numericValue)) {
|
|
83
84
|
return this.processSpecialProperty(property, numericValue);
|
|
84
85
|
}
|
|
85
86
|
|