| [9af201e] | 1 | const LOWER_A = 0x61
|
|---|
| 2 | const LOWER_Z = 0x7a
|
|---|
| 3 | const UPPER_A = 0x41
|
|---|
| 4 | const UPPER_Z = 0x5a
|
|---|
| 5 | const LOWER_E = 0x65
|
|---|
| 6 | const UPPER_E = 0x45
|
|---|
| 7 | const ZERO = 0x30
|
|---|
| 8 | const NINE = 0x39
|
|---|
| 9 | const ADD = 0x2b
|
|---|
| 10 | const SUB = 0x2d
|
|---|
| 11 | const MUL = 0x2a
|
|---|
| 12 | const DIV = 0x2f
|
|---|
| 13 | const OPEN_PAREN = 0x28
|
|---|
| 14 | const CLOSE_PAREN = 0x29
|
|---|
| 15 | const COMMA = 0x2c
|
|---|
| 16 | const SPACE = 0x20
|
|---|
| 17 | const PERCENT = 0x25
|
|---|
| 18 |
|
|---|
| 19 | const MATH_FUNCTIONS = [
|
|---|
| 20 | 'calc',
|
|---|
| 21 | 'min',
|
|---|
| 22 | 'max',
|
|---|
| 23 | 'clamp',
|
|---|
| 24 | 'mod',
|
|---|
| 25 | 'rem',
|
|---|
| 26 | 'sin',
|
|---|
| 27 | 'cos',
|
|---|
| 28 | 'tan',
|
|---|
| 29 | 'asin',
|
|---|
| 30 | 'acos',
|
|---|
| 31 | 'atan',
|
|---|
| 32 | 'atan2',
|
|---|
| 33 | 'pow',
|
|---|
| 34 | 'sqrt',
|
|---|
| 35 | 'hypot',
|
|---|
| 36 | 'log',
|
|---|
| 37 | 'exp',
|
|---|
| 38 | 'round',
|
|---|
| 39 | ]
|
|---|
| 40 |
|
|---|
| 41 | export function hasMathFn(input: string) {
|
|---|
| 42 | return input.indexOf('(') !== -1 && MATH_FUNCTIONS.some((fn) => input.includes(`${fn}(`))
|
|---|
| 43 | }
|
|---|
| 44 |
|
|---|
| 45 | export function addWhitespaceAroundMathOperators(input: string) {
|
|---|
| 46 | // Bail early if there are no math functions in the input
|
|---|
| 47 | if (!MATH_FUNCTIONS.some((fn) => input.includes(fn))) {
|
|---|
| 48 | return input
|
|---|
| 49 | }
|
|---|
| 50 |
|
|---|
| 51 | let result = ''
|
|---|
| 52 | let formattable: boolean[] = []
|
|---|
| 53 |
|
|---|
| 54 | let valuePos = null
|
|---|
| 55 | let lastValuePos = null
|
|---|
| 56 |
|
|---|
| 57 | for (let i = 0; i < input.length; i++) {
|
|---|
| 58 | let char = input.charCodeAt(i)
|
|---|
| 59 |
|
|---|
| 60 | // Track if we see a number followed by a unit, then we know for sure that
|
|---|
| 61 | // this is not a function call.
|
|---|
| 62 | if (char >= ZERO && char <= NINE) {
|
|---|
| 63 | valuePos = i
|
|---|
| 64 | }
|
|---|
| 65 |
|
|---|
| 66 | // If we saw a number before, and we see normal a-z character, then we
|
|---|
| 67 | // assume this is a value such as `123px`
|
|---|
| 68 | else if (
|
|---|
| 69 | valuePos !== null &&
|
|---|
| 70 | (char === PERCENT ||
|
|---|
| 71 | (char >= LOWER_A && char <= LOWER_Z) ||
|
|---|
| 72 | (char >= UPPER_A && char <= UPPER_Z))
|
|---|
| 73 | ) {
|
|---|
| 74 | valuePos = i
|
|---|
| 75 | }
|
|---|
| 76 |
|
|---|
| 77 | // Once we see something else, we reset the value position
|
|---|
| 78 | else {
|
|---|
| 79 | lastValuePos = valuePos
|
|---|
| 80 | valuePos = null
|
|---|
| 81 | }
|
|---|
| 82 |
|
|---|
| 83 | // Determine if we're inside a math function
|
|---|
| 84 | if (char === OPEN_PAREN) {
|
|---|
| 85 | result += input[i]
|
|---|
| 86 |
|
|---|
| 87 | // Scan backwards to determine the function name. This assumes math
|
|---|
| 88 | // functions are named with lowercase alphanumeric characters.
|
|---|
| 89 | let start = i
|
|---|
| 90 |
|
|---|
| 91 | for (let j = i - 1; j >= 0; j--) {
|
|---|
| 92 | let inner = input.charCodeAt(j)
|
|---|
| 93 |
|
|---|
| 94 | if (inner >= ZERO && inner <= NINE) {
|
|---|
| 95 | start = j // 0-9
|
|---|
| 96 | } else if (inner >= LOWER_A && inner <= LOWER_Z) {
|
|---|
| 97 | start = j // a-z
|
|---|
| 98 | } else {
|
|---|
| 99 | break
|
|---|
| 100 | }
|
|---|
| 101 | }
|
|---|
| 102 |
|
|---|
| 103 | let fn = input.slice(start, i)
|
|---|
| 104 |
|
|---|
| 105 | // This is a known math function so start formatting
|
|---|
| 106 | if (MATH_FUNCTIONS.includes(fn)) {
|
|---|
| 107 | formattable.unshift(true)
|
|---|
| 108 | continue
|
|---|
| 109 | }
|
|---|
| 110 |
|
|---|
| 111 | // We've encountered nested parens inside a math function, record that and
|
|---|
| 112 | // keep formatting until we've closed all parens.
|
|---|
| 113 | else if (formattable[0] && fn === '') {
|
|---|
| 114 | formattable.unshift(true)
|
|---|
| 115 | continue
|
|---|
| 116 | }
|
|---|
| 117 |
|
|---|
| 118 | // This is not a known math function so don't format it
|
|---|
| 119 | formattable.unshift(false)
|
|---|
| 120 | continue
|
|---|
| 121 | }
|
|---|
| 122 |
|
|---|
| 123 | // We've exited the function so format according to the parent function's
|
|---|
| 124 | // type.
|
|---|
| 125 | else if (char === CLOSE_PAREN) {
|
|---|
| 126 | result += input[i]
|
|---|
| 127 | formattable.shift()
|
|---|
| 128 | }
|
|---|
| 129 |
|
|---|
| 130 | // Add spaces after commas in math functions
|
|---|
| 131 | else if (char === COMMA && formattable[0]) {
|
|---|
| 132 | result += `, `
|
|---|
| 133 | continue
|
|---|
| 134 | }
|
|---|
| 135 |
|
|---|
| 136 | // Skip over consecutive whitespace
|
|---|
| 137 | else if (char === SPACE && formattable[0] && result.charCodeAt(result.length - 1) === SPACE) {
|
|---|
| 138 | continue
|
|---|
| 139 | }
|
|---|
| 140 |
|
|---|
| 141 | // Add whitespace around operators inside math functions
|
|---|
| 142 | else if ((char === ADD || char === MUL || char === DIV || char === SUB) && formattable[0]) {
|
|---|
| 143 | let trimmed = result.trimEnd()
|
|---|
| 144 | let prev = trimmed.charCodeAt(trimmed.length - 1)
|
|---|
| 145 | let prevPrev = trimmed.charCodeAt(trimmed.length - 2)
|
|---|
| 146 | let next = input.charCodeAt(i + 1)
|
|---|
| 147 |
|
|---|
| 148 | // Do not add spaces for scientific notation, e.g.: `-3.4e-2`
|
|---|
| 149 | if ((prev === LOWER_E || prev === UPPER_E) && prevPrev >= ZERO && prevPrev <= NINE) {
|
|---|
| 150 | result += input[i]
|
|---|
| 151 | continue
|
|---|
| 152 | }
|
|---|
| 153 |
|
|---|
| 154 | // If we're preceded by an operator don't add spaces
|
|---|
| 155 | else if (prev === ADD || prev === MUL || prev === DIV || prev === SUB) {
|
|---|
| 156 | result += input[i]
|
|---|
| 157 | continue
|
|---|
| 158 | }
|
|---|
| 159 |
|
|---|
| 160 | // If we're at the beginning of an argument don't add spaces
|
|---|
| 161 | else if (prev === OPEN_PAREN || prev === COMMA) {
|
|---|
| 162 | result += input[i]
|
|---|
| 163 | continue
|
|---|
| 164 | }
|
|---|
| 165 |
|
|---|
| 166 | // Add spaces only after the operator if we already have spaces before it
|
|---|
| 167 | else if (input.charCodeAt(i - 1) === SPACE) {
|
|---|
| 168 | result += `${input[i]} `
|
|---|
| 169 | }
|
|---|
| 170 |
|
|---|
| 171 | // Add spaces around the operator, if...
|
|---|
| 172 | else if (
|
|---|
| 173 | // Previous is a digit
|
|---|
| 174 | (prev >= ZERO && prev <= NINE) ||
|
|---|
| 175 | // Next is a digit
|
|---|
| 176 | (next >= ZERO && next <= NINE) ||
|
|---|
| 177 | // Previous is end of a function call (or parenthesized expression)
|
|---|
| 178 | prev === CLOSE_PAREN ||
|
|---|
| 179 | // Next is start of a parenthesized expression
|
|---|
| 180 | next === OPEN_PAREN ||
|
|---|
| 181 | // Next is an operator
|
|---|
| 182 | next === ADD ||
|
|---|
| 183 | next === MUL ||
|
|---|
| 184 | next === DIV ||
|
|---|
| 185 | next === SUB ||
|
|---|
| 186 | // Previous position was a value (+ unit)
|
|---|
| 187 | (lastValuePos !== null && lastValuePos === i - 1)
|
|---|
| 188 | ) {
|
|---|
| 189 | result += ` ${input[i]} `
|
|---|
| 190 | }
|
|---|
| 191 |
|
|---|
| 192 | // Everything else
|
|---|
| 193 | else {
|
|---|
| 194 | result += input[i]
|
|---|
| 195 | }
|
|---|
| 196 | }
|
|---|
| 197 |
|
|---|
| 198 | // Handle all other characters
|
|---|
| 199 | else {
|
|---|
| 200 | result += input[i]
|
|---|
| 201 | }
|
|---|
| 202 | }
|
|---|
| 203 |
|
|---|
| 204 | return result
|
|---|
| 205 | }
|
|---|