UNPKG

cheetah-grid

Version:

Cheetah Grid is a high performance grid engine that works on canvas

312 lines (291 loc) 7.71 kB
import { array } from "./utils"; const TYPE_PAREN = 0; const TYPE_UNIT = 1; const TYPE_OPERATOR = 2; const TYPE_NUMBER = 3; const NODE_TYPE_UNIT = 10; const NODE_TYPE_BINARY_EXPRESSION = 11; const NODE_TYPE_NUMBER = 12; type Ops = "-" | "+" | "*" | "/"; type ParenToken = { value: "(" | ")"; type: typeof TYPE_PAREN; }; type UnitToken = { unit: string; value: number; type: typeof TYPE_UNIT; }; type OpToken = { value: Ops; type: typeof TYPE_OPERATOR; }; type NumToken = { value: number; type: typeof TYPE_NUMBER; }; type Token = ParenToken | UnitToken | OpToken | NumToken; type UnitNode = { nodeType: typeof NODE_TYPE_UNIT; unit: string; value: number; }; type BinaryNode = { nodeType: typeof NODE_TYPE_BINARY_EXPRESSION; left: Node; op: OpToken; right: Node; }; type NumNode = { nodeType: typeof NODE_TYPE_NUMBER; value: number; }; type Node = UnitNode | BinaryNode | NumNode; const TABULATION = 0x09; const CARRIAGE_RETURN = 0x0d; const LINE_FEED = 0x0a; const FORM_FEED = 0x0c; const SPACE = 0x20; const PERCENT = 0x25; const FULL_STOP = 0x2e; const DIGIT_0 = 0x30; const DIGIT_9 = 0x39; const LATIN_CAPITAL_A = 0x41; const LATIN_CAPITAL_Z = 0x5a; const LATIN_SMALL_A = 0x61; const LATIN_SMALL_Z = 0x7a; function isUpperLetter(cp: number): boolean { return cp >= LATIN_CAPITAL_A && cp <= LATIN_CAPITAL_Z; } function isLowerLetter(cp: number): boolean { return cp >= LATIN_SMALL_A && cp <= LATIN_SMALL_Z; } function isLetter(cp: number): boolean { return isLowerLetter(cp) || isUpperLetter(cp); } function isWhitespace(cp: number): boolean { return ( cp === TABULATION || cp === LINE_FEED || cp === FORM_FEED || cp === CARRIAGE_RETURN || cp === SPACE ); } function isDigit(cp: number): boolean { return cp >= DIGIT_0 && cp <= DIGIT_9; } function isDot(cp: number): boolean { return cp === FULL_STOP; } function isUnit(cp: number): boolean { return isLetter(cp) || cp === PERCENT; } function createError(calc: string): Error { return new Error(`calc parse error: ${calc}`); } /** * tokenize * @param {string} calc calc expression * @returns {Array} tokens * @private */ function tokenize(calc: string): Token[] { const exp = calc.replace(/calc\(/g, "(").trim(); const tokens: Token[] = []; const len = exp.length; for (let index = 0; index < len; index++) { const c = exp[index]; const cp = c.charCodeAt(0); if (c === "(" || c === ")") { tokens.push({ value: c, type: TYPE_PAREN }); } else if (c === "*" || c === "/") { tokens.push({ value: c, type: TYPE_OPERATOR }); } else if (c === "+" || c === "-") { index = parseSign(c, index + 1) - 1; } else if (isDigit(cp) || isDot(cp)) { index = parseNum(c, index + 1) - 1; } else if (isWhitespace(cp)) { // skip } else { throw createError(calc); } } function parseSign(sign: "+" | "-", start: number): number { if (start < len) { const c = exp[start]; const cp = c.charCodeAt(0); if (isDigit(cp) || isDot(cp)) { return parseNum(sign + c, start + 1); } } tokens.push({ value: sign, type: TYPE_OPERATOR }); return start; } function parseNum(num: string, start: number): number { let index = start; for (; index < len; index++) { const c = exp[index]; const cp = c.charCodeAt(0); if (isDigit(cp)) { num += c; } else if (c === ".") { if (num.indexOf(".") >= 0) { throw createError(calc); } num += c; } else if (isUnit(cp)) { return parseUnit(num, c, index + 1); } else { break; } } if (num === ".") { throw createError(calc); } tokens.push({ value: parseFloat(num), type: TYPE_NUMBER }); return index; } function parseUnit(num: string, unit: string, start: number): number { let index = start; for (; index < len; index++) { const c = exp[index]; const cp = c.charCodeAt(0); if (isUnit(cp)) { unit += c; } else { break; } } tokens.push({ value: parseFloat(num), unit, type: TYPE_UNIT }); return index; } return tokens; } const PRECEDENCE = { "*": 3, "/": 3, "+": 2, "-": 2, }; function lex(tokens: Token[], calc: string): Node { function buildBinaryExpNode(stack: (Node | OpToken)[]): BinaryNode { const right = stack.pop() as Node; const op = stack.pop() as OpToken; const left = stack.pop() as Node; if ( !left || !left.nodeType || !op || op.type !== TYPE_OPERATOR || !right || !right.nodeType ) { throw createError(calc); } return { nodeType: NODE_TYPE_BINARY_EXPRESSION, left, op, right, }; } const stack: (Node | OpToken)[] = []; while (tokens.length) { const token = tokens.shift() as Token; if (token.type === TYPE_PAREN && token.value === "(") { let deep = 0; const closeIndex = array.findIndex(tokens, (t) => { if (t.type === TYPE_PAREN && t.value === "(") { deep++; } else if (t.type === TYPE_PAREN && t.value === ")") { if (!deep) { return true; } deep--; } return false; }); if (closeIndex === -1) { throw createError(calc); } stack.push(lex(tokens.splice(0, closeIndex), calc)); tokens.shift(); } else if (token.type === TYPE_OPERATOR) { if (stack.length >= 3) { const beforeOp = (stack[stack.length - 2] as OpToken).value; if (PRECEDENCE[token.value] <= PRECEDENCE[beforeOp]) { stack.push(buildBinaryExpNode(stack)); } } stack.push(token); } else if (token.type === TYPE_UNIT) { const { value: num, unit } = token; stack.push({ nodeType: NODE_TYPE_UNIT, value: num, unit, }); } else if (token.type === TYPE_NUMBER) { stack.push({ nodeType: NODE_TYPE_NUMBER, value: token.value, }); } } while (stack.length > 1) { stack.push(buildBinaryExpNode(stack)); } return stack[0] as Node; } function parse(calcStr: string): Node { const tokens = tokenize(calcStr); return lex(tokens, calcStr); } function calcNode(node: Node, context: CalcContext): number { if (node.nodeType === NODE_TYPE_BINARY_EXPRESSION) { const left = calcNode(node.left, context); const right = calcNode(node.right, context); switch (node.op.value) { case "+": return left + right; case "-": return left - right; case "*": return left * right; case "/": return left / right; default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`calc error. unknown operator: ${node.op.value}`); } } else if (node.nodeType === NODE_TYPE_UNIT) { switch (node.unit) { case "%": return (node.value * context.full) / 100; case "em": return node.value * context.em; case "px": return node.value; default: throw new Error(`calc error. unknown unit: ${node.unit}`); } } else if (node.nodeType === NODE_TYPE_NUMBER) { return node.value; } throw new Error("calc error."); } function toPxInternal(value: string, context: CalcContext): number { const ast = parse(value); return calcNode(ast, context); } type CalcContext = { full: number; em: number; }; export function toPx(value: string | number, context: CalcContext): number { if (typeof value === "string") { return toPxInternal(value.trim(), context); } return value - 0; }