|
| 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 | + |
1 | 16 | const MATH_FUNCTIONS = [
|
2 | 17 | 'calc',
|
3 | 18 | 'min',
|
@@ -20,9 +35,6 @@ const MATH_FUNCTIONS = [
|
20 | 35 | 'round',
|
21 | 36 | ]
|
22 | 37 |
|
23 |
| -const KNOWN_DASHED_FUNCTIONS = ['anchor-size'] |
24 |
| -const DASHED_FUNCTIONS_REGEX = new RegExp(`(${KNOWN_DASHED_FUNCTIONS.join('|')})\\(`, 'g') |
25 |
| - |
26 | 38 | export function hasMathFn(input: string) {
|
27 | 39 | return input.indexOf('(') !== -1 && MATH_FUNCTIONS.some((fn) => input.includes(`${fn}(`))
|
28 | 40 | }
|
@@ -33,25 +45,36 @@ export function addWhitespaceAroundMathOperators(input: string) {
|
33 | 45 | return input
|
34 | 46 | }
|
35 | 47 |
|
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 |
| - |
46 | 48 | let result = ''
|
47 | 49 | let formattable: boolean[] = []
|
48 | 50 |
|
| 51 | +let valuePos = null |
| 52 | +let lastValuePos = null |
| 53 | + |
49 | 54 | 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 | +} |
51 | 74 |
|
52 | 75 | // Determine if we're inside a math function
|
53 |
| -if (char === '(') { |
54 |
| -result += char |
| 76 | +if (char === OPEN_PAREN) { |
| 77 | +result += input[i] |
55 | 78 |
|
56 | 79 | // Scan backwards to determine the function name. This assumes math
|
57 | 80 | // functions are named with lowercase alphanumeric characters.
|
@@ -60,9 +83,9 @@ export function addWhitespaceAroundMathOperators(input: string) {
|
60 | 83 | for (let j = i - 1; j >= 0; j--) {
|
61 | 84 | let inner = input.charCodeAt(j)
|
62 | 85 |
|
63 |
| -if (inner >= 48 && inner <= 57) { |
| 86 | +if (inner >= ZERO && inner <= NINE) { |
64 | 87 | start = j // 0-9
|
65 |
| -} else if (inner >= 97 && inner <= 122) { |
| 88 | +} else if (inner >= LOWER_A && inner <= LOWER_Z) { |
66 | 89 | start = j // a-z
|
67 | 90 | } else {
|
68 | 91 | break
|
@@ -91,76 +114,84 @@ export function addWhitespaceAroundMathOperators(input: string) {
|
91 | 114 |
|
92 | 115 | // We've exited the function so format according to the parent function's
|
93 | 116 | // type.
|
94 |
| -else if (char === ')') { |
95 |
| -result += char |
| 117 | +else if (char === CLOSE_PAREN) { |
| 118 | +result += input[i] |
96 | 119 | formattable.shift()
|
97 | 120 | }
|
98 | 121 |
|
99 | 122 | // Add spaces after commas in math functions
|
100 |
| -else if (char === ',' && formattable[0]) { |
| 123 | +else if (char === COMMA && formattable[0]) { |
101 | 124 | result += `, `
|
102 | 125 | continue
|
103 | 126 | }
|
104 | 127 |
|
105 | 128 | // 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) { |
107 | 130 | continue
|
108 | 131 | }
|
109 | 132 |
|
110 | 133 | // 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]) { |
112 | 135 | 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 | +} |
114 | 145 |
|
115 | 146 | // 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] |
118 | 149 | continue
|
119 | 150 | }
|
120 | 151 |
|
121 | 152 | // 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] |
124 | 155 | continue
|
125 | 156 | }
|
126 | 157 |
|
127 | 158 | // 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]} ` |
130 | 161 | }
|
131 | 162 |
|
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]} ` |
135 | 182 | }
|
136 |
| -} |
137 | 183 |
|
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 | +} |
153 | 188 | }
|
154 | 189 |
|
155 | 190 | // Handle all other characters
|
156 | 191 | else {
|
157 |
| -result += char |
| 192 | +result += input[i] |
158 | 193 | }
|
159 | 194 | }
|
160 | 195 |
|
161 |
| -if (hasKnownFunctions) { |
162 |
| -return result.replace(/\$(\d+)\$/g, (fn, idx) => KNOWN_DASHED_FUNCTIONS[idx] ?? fn) |
163 |
| -} |
164 |
| - |
165 | 196 | return result
|
166 | 197 | }
|
0 commit comments