Spaces:
Sleeping
Sleeping
| const LOWER_A = 0x61 | |
| const LOWER_Z = 0x7a | |
| const UPPER_A = 0x41 | |
| const UPPER_Z = 0x5a | |
| const LOWER_E = 0x65 | |
| const UPPER_E = 0x45 | |
| const ZERO = 0x30 | |
| const NINE = 0x39 | |
| const ADD = 0x2b | |
| const SUB = 0x2d | |
| const MUL = 0x2a | |
| const DIV = 0x2f | |
| const OPEN_PAREN = 0x28 | |
| const CLOSE_PAREN = 0x29 | |
| const COMMA = 0x2c | |
| const SPACE = 0x20 | |
| const PERCENT = 0x25 | |
| const MATH_FUNCTIONS = [ | |
| 'calc', | |
| 'min', | |
| 'max', | |
| 'clamp', | |
| 'mod', | |
| 'rem', | |
| 'sin', | |
| 'cos', | |
| 'tan', | |
| 'asin', | |
| 'acos', | |
| 'atan', | |
| 'atan2', | |
| 'pow', | |
| 'sqrt', | |
| 'hypot', | |
| 'log', | |
| 'exp', | |
| 'round', | |
| ] | |
| export function hasMathFn(input: string) { | |
| return input.indexOf('(') !== -1 && MATH_FUNCTIONS.some((fn) => input.includes(`${fn}(`)) | |
| } | |
| export function addWhitespaceAroundMathOperators(input: string) { | |
| // Bail early if there are no math functions in the input | |
| if (!MATH_FUNCTIONS.some((fn) => input.includes(fn))) { | |
| return input | |
| } | |
| let result = '' | |
| let formattable: boolean[] = [] | |
| let valuePos = null | |
| let lastValuePos = null | |
| for (let i = 0; i < input.length; i++) { | |
| let char = input.charCodeAt(i) | |
| // Track if we see a number followed by a unit, then we know for sure that | |
| // this is not a function call. | |
| if (char >= ZERO && char <= NINE) { | |
| valuePos = i | |
| } | |
| // If we saw a number before, and we see normal a-z character, then we | |
| // assume this is a value such as `123px` | |
| else if ( | |
| valuePos !== null && | |
| (char === PERCENT || | |
| (char >= LOWER_A && char <= LOWER_Z) || | |
| (char >= UPPER_A && char <= UPPER_Z)) | |
| ) { | |
| valuePos = i | |
| } | |
| // Once we see something else, we reset the value position | |
| else { | |
| lastValuePos = valuePos | |
| valuePos = null | |
| } | |
| // Determine if we're inside a math function | |
| if (char === OPEN_PAREN) { | |
| result += input[i] | |
| // Scan backwards to determine the function name. This assumes math | |
| // functions are named with lowercase alphanumeric characters. | |
| let start = i | |
| for (let j = i - 1; j >= 0; j--) { | |
| let inner = input.charCodeAt(j) | |
| if (inner >= ZERO && inner <= NINE) { | |
| start = j // 0-9 | |
| } else if (inner >= LOWER_A && inner <= LOWER_Z) { | |
| start = j // a-z | |
| } else { | |
| break | |
| } | |
| } | |
| let fn = input.slice(start, i) | |
| // This is a known math function so start formatting | |
| if (MATH_FUNCTIONS.includes(fn)) { | |
| formattable.unshift(true) | |
| continue | |
| } | |
| // We've encountered nested parens inside a math function, record that and | |
| // keep formatting until we've closed all parens. | |
| else if (formattable[0] && fn === '') { | |
| formattable.unshift(true) | |
| continue | |
| } | |
| // This is not a known math function so don't format it | |
| formattable.unshift(false) | |
| continue | |
| } | |
| // We've exited the function so format according to the parent function's | |
| // type. | |
| else if (char === CLOSE_PAREN) { | |
| result += input[i] | |
| formattable.shift() | |
| } | |
| // Add spaces after commas in math functions | |
| else if (char === COMMA && formattable[0]) { | |
| result += `, ` | |
| continue | |
| } | |
| // Skip over consecutive whitespace | |
| else if (char === SPACE && formattable[0] && result.charCodeAt(result.length - 1) === SPACE) { | |
| continue | |
| } | |
| // Add whitespace around operators inside math functions | |
| else if ((char === ADD || char === MUL || char === DIV || char === SUB) && formattable[0]) { | |
| let trimmed = result.trimEnd() | |
| let prev = trimmed.charCodeAt(trimmed.length - 1) | |
| let prevPrev = trimmed.charCodeAt(trimmed.length - 2) | |
| let next = input.charCodeAt(i + 1) | |
| // Do not add spaces for scientific notation, e.g.: `-3.4e-2` | |
| if ((prev === LOWER_E || prev === UPPER_E) && prevPrev >= ZERO && prevPrev <= NINE) { | |
| result += input[i] | |
| continue | |
| } | |
| // If we're preceded by an operator don't add spaces | |
| else if (prev === ADD || prev === MUL || prev === DIV || prev === SUB) { | |
| result += input[i] | |
| continue | |
| } | |
| // If we're at the beginning of an argument don't add spaces | |
| else if (prev === OPEN_PAREN || prev === COMMA) { | |
| result += input[i] | |
| continue | |
| } | |
| // Add spaces only after the operator if we already have spaces before it | |
| else if (input.charCodeAt(i - 1) === SPACE) { | |
| result += `${input[i]} ` | |
| } | |
| // Add spaces around the operator, if... | |
| else if ( | |
| // Previous is a digit | |
| (prev >= ZERO && prev <= NINE) || | |
| // Next is a digit | |
| (next >= ZERO && next <= NINE) || | |
| // Previous is end of a function call (or parenthesized expression) | |
| prev === CLOSE_PAREN || | |
| // Next is start of a parenthesized expression | |
| next === OPEN_PAREN || | |
| // Next is an operator | |
| next === ADD || | |
| next === MUL || | |
| next === DIV || | |
| next === SUB || | |
| // Previous position was a value (+ unit) | |
| (lastValuePos !== null && lastValuePos === i - 1) | |
| ) { | |
| result += ` ${input[i]} ` | |
| } | |
| // Everything else | |
| else { | |
| result += input[i] | |
| } | |
| } | |
| // Handle all other characters | |
| else { | |
| result += input[i] | |
| } | |
| } | |
| return result | |
| } | |