206 lines
5.2 KiB
TypeScript
206 lines
5.2 KiB
TypeScript
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
|
|
}
|