1+ const LOWER_A = 0x61
2+ const LOWER_Z = 0x7a
3+ const LOWER_E = 0x65
4+ const UPPER_E = 0x45
5+ const ZERO = 0x30
6+ const NINE = 0x39
7+ const ADD = 0x2b
8+ const SUB = 0x2d
9+ const MUL = 0x2a
10+ const DIV = 0x2f
11+ const OPEN_PAREN = 0x28
12+ const CLOSE_PAREN = 0x29
13+ const COMMA = 0x2c
14+ const SPACE = 0x20
15+
116const MATH_FUNCTIONS = [
217 'calc' ,
318 'min' ,
@@ -20,9 +35,6 @@ const MATH_FUNCTIONS = [
2035 'round' ,
2136]
2237
23- const KNOWN_DASHED_FUNCTIONS = [ 'anchor-size' ]
24- const DASHED_FUNCTIONS_REGEX = new RegExp ( `(${ KNOWN_DASHED_FUNCTIONS . join ( '|' ) } )\\(` , 'g' )
25-
2638export function hasMathFn ( input : string ) {
2739 return input . indexOf ( '(' ) !== - 1 && MATH_FUNCTIONS . some ( ( fn ) => input . includes ( `${ fn } (` ) )
2840}
@@ -33,25 +45,36 @@ export function addWhitespaceAroundMathOperators(input: string) {
3345 return input
3446 }
3547
36- // Replace known functions with a placeholder
37- let hasKnownFunctions = false
38- if ( KNOWN_DASHED_FUNCTIONS . some ( ( fn ) => input . includes ( fn ) ) ) {
39- DASHED_FUNCTIONS_REGEX . lastIndex = 0
40- input = input . replace ( DASHED_FUNCTIONS_REGEX , ( _ , fn ) => {
41- hasKnownFunctions = true
42- return `$${ KNOWN_DASHED_FUNCTIONS . indexOf ( fn ) } $(`
43- } )
44- }
45-
4648 let result = ''
4749 let formattable : boolean [ ] = [ ]
4850
51+ let valuePos = null
52+ let lastValuePos = null
53+
4954 for ( let i = 0 ; i < input . length ; i ++ ) {
50- let char = input [ i ]
55+ let char = input . charCodeAt ( i )
56+
57+ // Track if we see a number followed by a unit, then we know for sure that
58+ // this is not a function call.
59+ if ( char >= ZERO && char <= NINE ) {
60+ valuePos = i
61+ }
62+
63+ // If we saw a number before, and we see normal a-z character, then we
64+ // assume this is a value such as `123px`
65+ else if ( valuePos !== null && char >= LOWER_A && char <= LOWER_Z ) {
66+ valuePos = i
67+ }
68+
69+ // Once we see something else, we reset the value position
70+ else {
71+ lastValuePos = valuePos
72+ valuePos = null
73+ }
5174
5275 // Determine if we're inside a math function
53- if ( char === '(' ) {
54- result += char
76+ if ( char === OPEN_PAREN ) {
77+ result += input [ i ]
5578
5679 // Scan backwards to determine the function name. This assumes math
5780 // functions are named with lowercase alphanumeric characters.
@@ -60,9 +83,9 @@ export function addWhitespaceAroundMathOperators(input: string) {
6083 for ( let j = i - 1 ; j >= 0 ; j -- ) {
6184 let inner = input . charCodeAt ( j )
6285
63- if ( inner >= 48 && inner <= 57 ) {
86+ if ( inner >= ZERO && inner <= NINE ) {
6487 start = j // 0-9
65- } else if ( inner >= 97 && inner <= 122 ) {
88+ } else if ( inner >= LOWER_A && inner <= LOWER_Z ) {
6689 start = j // a-z
6790 } else {
6891 break
@@ -91,76 +114,84 @@ export function addWhitespaceAroundMathOperators(input: string) {
91114
92115 // We've exited the function so format according to the parent function's
93116 // type.
94- else if ( char === ')' ) {
95- result += char
117+ else if ( char === CLOSE_PAREN ) {
118+ result += input [ i ]
96119 formattable . shift ( )
97120 }
98121
99122 // Add spaces after commas in math functions
100- else if ( char === ',' && formattable [ 0 ] ) {
123+ else if ( char === COMMA && formattable [ 0 ] ) {
101124 result += `, `
102125 continue
103126 }
104127
105128 // Skip over consecutive whitespace
106- else if ( char === ' ' && formattable [ 0 ] && result [ result . length - 1 ] === ' ' ) {
129+ else if ( char === SPACE && formattable [ 0 ] && result . charCodeAt ( result . length - 1 ) === SPACE ) {
107130 continue
108131 }
109132
110133 // Add whitespace around operators inside math functions
111- else if ( ( char === '+' || char === '*' || char === '/' || char === '-' ) && formattable [ 0 ] ) {
134+ else if ( ( char === ADD || char === MUL || char === DIV || char === SUB ) && formattable [ 0 ] ) {
112135 let trimmed = result . trimEnd ( )
113- let prev = trimmed [ trimmed . length - 1 ]
136+ let prev = trimmed . charCodeAt ( trimmed . length - 1 )
137+ let prevPrev = trimmed . charCodeAt ( trimmed . length - 2 )
138+ let next = input . charCodeAt ( i + 1 )
139+
140+ // Do not add spaces for scientific notation, e.g.: `-3.4e-2`
141+ if ( ( prev === LOWER_E || prev === UPPER_E ) && prevPrev >= ZERO && prevPrev <= NINE ) {
142+ result += input [ i ]
143+ continue
144+ }
114145
115146 // If we're preceded by an operator don't add spaces
116- if ( prev === '+' || prev === '*' || prev === '/' || prev === '-' ) {
117- result += char
147+ else if ( prev === ADD || prev === MUL || prev === DIV || prev === SUB ) {
148+ result += input [ i ]
118149 continue
119150 }
120151
121152 // If we're at the beginning of an argument don't add spaces
122- else if ( prev === '(' || prev === ',' ) {
123- result += char
153+ else if ( prev === OPEN_PAREN || prev === COMMA ) {
154+ result += input [ i ]
124155 continue
125156 }
126157
127158 // Add spaces only after the operator if we already have spaces before it
128- else if ( input [ i - 1 ] === ' ' ) {
129- result += `${ char } `
159+ else if ( input . charCodeAt ( i - 1 ) === SPACE ) {
160+ result += `${ input [ i ] } `
130161 }
131162
132- // Add spaces around the operator
133- else {
134- result += ` ${ char } `
163+ // Add spaces around the operator, if...
164+ else if (
165+ // Previous is a digit
166+ ( prev >= ZERO && prev <= NINE ) ||
167+ // Next is a digit
168+ ( next >= ZERO && next <= NINE ) ||
169+ // Previous is end of a function call (or parenthesized expression)
170+ prev === CLOSE_PAREN ||
171+ // Next is start of a parenthesized expression
172+ next === OPEN_PAREN ||
173+ // Next is an operator
174+ next === ADD ||
175+ next === MUL ||
176+ next === DIV ||
177+ next === SUB ||
178+ // Previous position was a value (+ unit)
179+ ( lastValuePos !== null && lastValuePos === i - 1 )
180+ ) {
181+ result += ` ${ input [ i ] } `
135182 }
136- }
137183
138- // Skip over `to-zero` when in a math function.
139- //
140- // This is specifically to handle this value in the round(…) function:
141- //
142- // ```
143- // round(to-zero, 1px)
144- // ^^^^^^^
145- // ```
146- //
147- // This is because the first argument is optionally a keyword and `to-zero`
148- // contains a hyphen and we want to avoid adding spaces inside it.
149- else if ( formattable [ 0 ] && input . startsWith ( 'to-zero' , i ) ) {
150- let start = i
151- i += 7
152- result += input . slice ( start , i + 1 )
184+ // Everything else
185+ else {
186+ result += input [ i ]
187+ }
153188 }
154189
155190 // Handle all other characters
156191 else {
157- result += char
192+ result += input [ i ]
158193 }
159194 }
160195
161- if ( hasKnownFunctions ) {
162- return result . replace ( / \$ ( \d + ) \$ / g, ( fn , idx ) => KNOWN_DASHED_FUNCTIONS [ idx ] ?? fn )
163- }
164-
165196 return result
166197}
0 commit comments