3
3
*/
4
4
// @ts -ignore
5
5
import unitless from '@emotion/unitless' ;
6
+ import { defuArrayFn } from 'defu' ;
6
7
import type { CSSObject } from '..' ;
7
8
import type { Transformer } from './interface' ;
8
9
10
+ interface ConvertUnit {
11
+ source : string | RegExp ;
12
+ target : string ;
13
+ }
14
+
9
15
interface Options {
10
16
/**
11
17
* The root font size.
@@ -22,49 +28,286 @@ interface Options {
22
28
* @default false
23
29
*/
24
30
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 ;
25
70
}
26
71
27
- const pxRegex = / u r l \( [ ^ ) ] + \) | v a r \( [ ^ ) ] + \) | ( \d * \. ? \d + ) p x / g;
72
+ const pxRegex = / " [ ^ " ] + " | ' [ ^ ' ] + ' | u r l \( [ ^ ) ] + \) | - - [ \w - ] + | ( \d * \. ? \d + ) p x / 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
+ }
28
149
29
150
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 ) ;
32
153
return ( Math . round ( wholeNumber / 10 ) * 10 ) / multiplier ;
33
154
}
34
155
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
+
35
229
const transform = ( options : Options = { } ) : Transformer => {
36
- const { rootValue = 16 , precision = 5 , mediaQuery = false } = options ;
230
+ const opts = resolveOptions ( options ) ;
37
231
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 ) ;
46
244
47
245
const visit = ( cssObj : CSSObject ) : CSSObject => {
48
246
const clone : CSSObject = { ...cssObj } ;
49
247
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 ;
54
251
}
252
+ return false ;
253
+ } ) ;
55
254
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 ;
59
263
}
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
+ }
60
298
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 ) ;
65
307
66
- clone [ newKey ] = clone [ key ] ;
67
- delete clone [ key ] ;
308
+ clone [ newKey ] = clone [ key ] ;
309
+ delete clone [ key ] ;
310
+ }
68
311
}
69
312
} ) ;
70
313
0 commit comments