Skip to content

Commit fb860ef

Browse files
authored
Merge 5c22fda into 907a22b
2 parents 907a22b + 5c22fda commit fb860ef

File tree

3 files changed

+447
-25
lines changed

3 files changed

+447
-25
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@emotion/unitless": "^0.7.5",
4646
"classnames": "^2.3.1",
4747
"csstype": "^3.1.3",
48+
"defu": "^6.1.4",
4849
"rc-util": "^5.35.0",
4950
"stylis": "^4.0.13"
5051
},

src/transformers/px2rem.ts

Lines changed: 268 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@
33
*/
44
// @ts-ignore
55
import unitless from '@emotion/unitless';
6+
import { defuArrayFn } from 'defu';
67
import type { CSSObject } from '..';
78
import type { Transformer } from './interface';
89

10+
interface ConvertUnit {
11+
source: string | RegExp;
12+
target: string;
13+
}
14+
915
interface Options {
1016
/**
1117
* The root font size.
@@ -22,49 +28,286 @@ interface Options {
2228
* @default false
2329
*/
2430
mediaQuery?: boolean;
31+
/**
32+
* The selector blackList.
33+
*
34+
*/
35+
selectorBlackList?: {
36+
/**
37+
* The selector black list.
38+
*/
39+
match?: (string | RegExp)[];
40+
/**
41+
* Whether to deep into the children.
42+
* @default true
43+
*/
44+
deep?: boolean;
45+
};
46+
/**
47+
* The property list to convert.
48+
* @default ['*']
49+
* @example
50+
* ['font-size', 'margin']
51+
*/
52+
propList?: string[];
53+
/**
54+
* The minimum pixel value to transform.
55+
* @default 1
56+
*/
57+
minPixelValue?: number;
58+
/**
59+
* Convert unit on end.
60+
* @default null
61+
* @example
62+
* ```js
63+
* {
64+
* source: /px$/i,
65+
* target: 'px'
66+
* }
67+
* ```
68+
*/
69+
convertUnit?: ConvertUnit | ConvertUnit[] | false | null;
2570
}
2671

27-
const pxRegex = /url\([^)]+\)|var\([^)]+\)|(\d*\.?\d+)px/g;
72+
const pxRegex = /"[^"]+"|'[^']+'|url\([^)]+\)|--[\w-]+|(\d*\.?\d+)px/g;
73+
74+
const filterPropList = {
75+
exact(list: string[]) {
76+
return list.filter((m) => m.match(/^[^!*]+$/));
77+
},
78+
contain(list: string[]) {
79+
return list.filter((m) => m.match(/^\*.+\*$/)).map((m) => m.slice(1, -1));
80+
},
81+
endWith(list: string[]) {
82+
return list.filter((m) => m.match(/^\*[^*]+$/)).map((m) => m.slice(1));
83+
},
84+
startWith(list: string[]) {
85+
return list
86+
.filter((m) => m.match(/^[^!*]+\*$/))
87+
.map((m) => m.slice(0, Math.max(0, m.length - 1)));
88+
},
89+
notExact(list: string[]) {
90+
return list.filter((m) => m.match(/^![^*].*$/)).map((m) => m.slice(1));
91+
},
92+
notContain(list: string[]) {
93+
return list.filter((m) => m.match(/^!\*.+\*$/)).map((m) => m.slice(2, -1));
94+
},
95+
notEndWith(list: string[]) {
96+
return list.filter((m) => m.match(/^!\*[^*]+$/)).map((m) => m.slice(2));
97+
},
98+
notStartWith(list: string[]) {
99+
return list.filter((m) => m.match(/^![^*]+\*$/)).map((m) => m.slice(1, -1));
100+
},
101+
};
102+
103+
function createPropListMatcher(propList: string[]) {
104+
const hasWild = propList.includes('*');
105+
const matchAll = hasWild && propList.length === 1;
106+
const lists = {
107+
exact: filterPropList.exact(propList),
108+
contain: filterPropList.contain(propList),
109+
startWith: filterPropList.startWith(propList),
110+
endWith: filterPropList.endWith(propList),
111+
notExact: filterPropList.notExact(propList),
112+
notContain: filterPropList.notContain(propList),
113+
notStartWith: filterPropList.notStartWith(propList),
114+
notEndWith: filterPropList.notEndWith(propList),
115+
};
116+
return function (prop: string) {
117+
if (matchAll) return true;
118+
return (
119+
(hasWild ||
120+
lists.exact.includes(prop) ||
121+
lists.contain.some((m) => prop.includes(m)) ||
122+
lists.startWith.some((m) => prop.indexOf(m) === 0) ||
123+
lists.endWith.some(
124+
(m) => prop.indexOf(m) === prop.length - m.length,
125+
)) &&
126+
!(
127+
lists.notExact.includes(prop) ||
128+
lists.notContain.some((m) => prop.includes(m)) ||
129+
lists.notStartWith.some((m) => prop.indexOf(m) === 0) ||
130+
lists.notEndWith.some((m) => prop.indexOf(m) === prop.length - m.length)
131+
)
132+
);
133+
};
134+
}
135+
136+
function createPxReplace(
137+
rootValue: number,
138+
precision: NonNullable<Options['precision']>,
139+
minPixelValue: NonNullable<Options['minPixelValue']>,
140+
) {
141+
return (m: string, $1: string | null) => {
142+
if (!$1) return m;
143+
const pixels = Number.parseFloat($1);
144+
if (pixels <= minPixelValue) return m;
145+
const fixedVal = toFixed(pixels / rootValue, precision);
146+
return fixedVal === 0 ? '0' : `${fixedVal}rem`;
147+
};
148+
}
28149

29150
function toFixed(number: number, precision: number) {
30-
const multiplier = Math.pow(10, precision + 1),
31-
wholeNumber = Math.floor(number * multiplier);
151+
const multiplier = 10 ** (precision + 1);
152+
const wholeNumber = Math.floor(number * multiplier);
32153
return (Math.round(wholeNumber / 10) * 10) / multiplier;
33154
}
34155

156+
function is(val: unknown, type: string) {
157+
return Object.prototype.toString.call(val) === `[object ${type}]`;
158+
}
159+
160+
function isRegExp(data: unknown): data is RegExp {
161+
return is(data, 'RegExp');
162+
}
163+
164+
function isString(data: unknown): data is string {
165+
return is(data, 'String');
166+
}
167+
168+
function isObject(data: unknown): data is object {
169+
return is(data, 'Object');
170+
}
171+
172+
function isNumber(data: unknown): data is number {
173+
return is(data, 'Number');
174+
}
175+
176+
function blacklistedSelector(blacklist: (string | RegExp)[], selector: string) {
177+
if (!isString(selector)) return;
178+
return blacklist.some((t) => {
179+
if (isString(t)) {
180+
return selector.includes(t);
181+
}
182+
return selector.match(t);
183+
});
184+
}
185+
186+
const SKIP_SYMBOL = {
187+
key: Symbol('skip_symbol_key'),
188+
value: Symbol('skip_symbol_value'),
189+
};
190+
191+
function defineSkipSymbol(obj: object) {
192+
Object.defineProperty(obj, SKIP_SYMBOL.key, {
193+
value: SKIP_SYMBOL.value,
194+
enumerable: true,
195+
writable: false,
196+
configurable: false,
197+
});
198+
}
199+
200+
const uppercasePattern = /([A-Z])/g;
201+
function hyphenateStyleName(name: string): string {
202+
return name.replace(uppercasePattern, '-$1').toLowerCase();
203+
}
204+
205+
function convertUnitFn(value: string, convert: ConvertUnit) {
206+
const { source, target } = convert;
207+
if (isString(source)) {
208+
return value.replace(new RegExp(`${source}$`), target);
209+
} else if (isRegExp(source)) {
210+
return value.replace(new RegExp(source), target);
211+
}
212+
return value;
213+
}
214+
215+
const DEFAULT_OPTIONS: Required<Options> = {
216+
rootValue: 16,
217+
precision: 5,
218+
mediaQuery: false,
219+
minPixelValue: 1,
220+
propList: ['*'],
221+
selectorBlackList: { match: [], deep: true },
222+
convertUnit: null,
223+
};
224+
225+
function resolveOptions(options: Options, defaults = DEFAULT_OPTIONS) {
226+
return defuArrayFn(options, defaults);
227+
}
228+
35229
const transform = (options: Options = {}): Transformer => {
36-
const { rootValue = 16, precision = 5, mediaQuery = false } = options;
230+
const opts = resolveOptions(options);
37231

38-
const pxReplace = (m: string, $1: any) => {
39-
if (!$1) return m;
40-
const pixels = parseFloat($1);
41-
// covenant: pixels <= 1, not transform to rem @zombieJ
42-
if (pixels <= 1) return m;
43-
const fixedVal = toFixed(pixels / rootValue, precision);
44-
return `${fixedVal}rem`;
45-
};
232+
const {
233+
rootValue,
234+
precision,
235+
minPixelValue,
236+
propList,
237+
mediaQuery,
238+
convertUnit,
239+
selectorBlackList,
240+
} = opts;
241+
242+
const pxReplace = createPxReplace(rootValue, precision, minPixelValue);
243+
const satisfyPropList = createPropListMatcher(propList);
46244

47245
const visit = (cssObj: CSSObject): CSSObject => {
48246
const clone: CSSObject = { ...cssObj };
49247

50-
Object.entries(cssObj).forEach(([key, value]) => {
51-
if (typeof value === 'string' && value.includes('px')) {
52-
const newValue = value.replace(pxRegex, pxReplace);
53-
clone[key] = newValue;
248+
const skip = Object.getOwnPropertySymbols(clone).some((symbol) => {
249+
if (symbol === SKIP_SYMBOL.key) {
250+
return true;
54251
}
252+
return false;
253+
});
55254

56-
// no unit
57-
if (!unitless[key] && typeof value === 'number' && value !== 0) {
58-
clone[key] = `${value}px`.replace(pxRegex, pxReplace);
255+
if (skip) {
256+
if (selectorBlackList.deep) {
257+
Object.values(clone).forEach((value) => {
258+
if (value && isObject(value)) {
259+
defineSkipSymbol(value);
260+
}
261+
});
262+
return clone;
59263
}
264+
return clone;
265+
}
266+
267+
Object.entries(cssObj).forEach(([key, value]) => {
268+
if (!isObject(value)) {
269+
if (!satisfyPropList(hyphenateStyleName(key))) {
270+
return;
271+
}
272+
273+
if (isString(value) && value.includes('px')) {
274+
const newValue = value.replace(pxRegex, pxReplace);
275+
clone[key] = newValue;
276+
}
277+
278+
// no unit
279+
if (!unitless[key] && isNumber(value) && value !== 0) {
280+
clone[key] = `${value}px`.replace(pxRegex, pxReplace);
281+
}
282+
283+
if (convertUnit && isString(clone[key])) {
284+
const newValue = clone[key] as string;
285+
if (Array.isArray(convertUnit)) {
286+
convertUnit.forEach((conv) => {
287+
clone[key] = convertUnitFn(newValue, conv);
288+
});
289+
} else {
290+
clone[key] = convertUnitFn(newValue, convertUnit);
291+
}
292+
}
293+
} else {
294+
if (blacklistedSelector(selectorBlackList.match || [], key)) {
295+
defineSkipSymbol(value);
296+
return;
297+
}
60298

61-
// Media queries
62-
const mergedKey = key.trim();
63-
if (mergedKey.startsWith('@') && mergedKey.includes('px') && mediaQuery) {
64-
const newKey = key.replace(pxRegex, pxReplace);
299+
// Media queries
300+
const mergedKey = key.trim();
301+
if (
302+
mergedKey.startsWith('@') &&
303+
mergedKey.includes('px') &&
304+
mediaQuery
305+
) {
306+
const newKey = key.replace(pxRegex, pxReplace);
65307

66-
clone[newKey] = clone[key];
67-
delete clone[key];
308+
clone[newKey] = clone[key];
309+
delete clone[key];
310+
}
68311
}
69312
});
70313

0 commit comments

Comments
 (0)