diff options
| -rw-r--r-- | src/eval.ts | 61 | ||||
| -rw-r--r-- | src/parser.ts | 1 | ||||
| -rw-r--r-- | tests/calc.spec.ts | 46 | ||||
| -rw-r--r-- | tests/parser.spec.ts | 18 |
4 files changed, 122 insertions, 4 deletions
diff --git a/src/eval.ts b/src/eval.ts index 636023b..87bcd6d 100644 --- a/src/eval.ts +++ b/src/eval.ts @@ -1,4 +1,4 @@ -import { CSSUnit, Expr, parse } from './parser' +import { CSSUnit, Expr, parse, parseExpr } from './parser' import { Enum, constructors, match, matchString } from './utils/adt' export interface EvalActions { @@ -58,8 +58,10 @@ export const EvalValue = constructors<EvalValue>() export const evalExprAsString = async ( expr: Expr, actions: EvalActions, -): Promise<string | undefined> => - evalValueToString(await evalExpr(expr, actions)) +): Promise<string | undefined> => { + const evalVal = await evalExpr(expr, actions) + return evalValueToString(evalVal) +} export const evalExpr = async ( expr: Expr, @@ -72,11 +74,15 @@ export const evalExpr = async ( EvalValue.Number( matchString<number, CSSUnit>(unit, { s: () => value * 1000, + rem: () => value * 16, // TODO: get root font size + em: () => value * 16, // TODO: get parent font size + '%': () => value * 100, // TODO: Get parent width _: () => value, }), ), Identifier: async s => EvalValue.String(s), VarIdentifier: async s => EvalValue.VarIdentifier(s), + Parens: ({ expr }) => evalExpr(expr, actions), _: async _ => EvalValue.Void(), }) } @@ -119,7 +125,9 @@ const getFunctions = ( ): Promise<EvalValue> => { const getVariable = async () => { const varName = await evalExpr(args[0], actions) - const defaultValue = args[1] && (await evalExpr(args[1], actions)) + const defaultValue = args[1] + ? await evalExpr(args[1], actions) + : EvalValue.Void() return match<Promise<EvalValue>, EvalValue>(varName, { VarIdentifier: async name => { @@ -358,10 +366,55 @@ const getFunctions = ( }) }, + calc: async () => { + const result = await evalCalcExpr(args[0], actions) + return EvalValue.Number(result) + }, + _: () => Promise.reject(new Error(`Not implemented: ${name}`)), }) } +const evalBinOp = async ( + left: Expr, + right: Expr, + actions: EvalActions, + op: (a: number, b: number) => number, +): Promise<number> => + op(await evalCalcExpr(left, actions), await evalCalcExpr(right, actions)) + +export const evalCalcExpr = ( + expr: Expr, + actions: EvalActions, +): Promise<number> => + match(expr, { + BinOp: async ({ op, left, right }) => + matchString(op, { + '+': () => evalBinOp(left, right, actions, (a, b) => a + b), + '*': () => evalBinOp(left, right, actions, (a, b) => a * b), + '-': () => evalBinOp(left, right, actions, (a, b) => a - b), + '/': () => evalBinOp(left, right, actions, (a, b) => a / b), + _: () => + Promise.reject( + new Error(`Invalid operator in calc expression: ${op}`), + ), + }), + Parens: ({ expr }) => evalCalcExpr(expr, actions), + _: async () => { + if (expr.tag === 'Call' && expr.value.name === 'var') { + const value = await evalExprAsString(expr, actions) + try { + const pvalue = await evalExpr(parseExpr(value ?? ''), actions) + return evalValueToNumber(pvalue) ?? 0 + } catch (e) { + return 0 + } + } + const value = await evalExpr(expr, actions) + return evalValueToNumber(value) ?? 0 + }, + }) + export const evalArgs = ( args: Array<Expr>, count: number, diff --git a/src/parser.ts b/src/parser.ts index a2bb577..c2aa30f 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -2,6 +2,7 @@ import { Enum, constructors, match, matchString } from './utils/adt' import * as P from './utils/parser-comb' import { Result } from './utils/result' +// TODO: vh, vw export type CSSUnit = '' | 's' | 'ms' | 'px' | '%' | 'rem' | 'em' export type BinOp = '+' | '-' | '*' | '/' diff --git a/tests/calc.spec.ts b/tests/calc.spec.ts new file mode 100644 index 0000000..a4e11cd --- /dev/null +++ b/tests/calc.spec.ts @@ -0,0 +1,46 @@ +import { EvalActions, EvalValue, evalExpr } from '../src/eval' +import { parseExpr } from '../src/parser' +import { matchString } from '../src/utils/adt' + +describe('calc', () => { + const variables = (name: string) => + matchString(name, { + '--test-8rem': () => '8rem', + _: () => {}, + }) + const actions: EvalActions = { + addClass: jest.fn(), + removeClass: jest.fn(), + delay: jest.fn(), + jsEval: jest.fn(), + loadCssx: jest.fn(), + getVariable: jest.fn(variables), + updateVariable: jest.fn(), + setAttribute: jest.fn(), + getAttribute: jest.fn(), + withEvent: jest.fn(), + getFormData: jest.fn(), + sendRequest: jest.fn(), + addChildren: jest.fn(), + removeElement: jest.fn(), + callMethod: jest.fn(), + evaluateInScope: jest.fn(), + } + + describe.each([ + ['calc(8rem)', EvalValue.Number(128)], + ['calc(5 + 8)', EvalValue.Number(13)], + ['calc(5 * 8 + 1)', EvalValue.Number(41)], + ['calc(5 * (8 + 1))', EvalValue.Number(45)], + ['calc(5px * (8rem + 1))', EvalValue.Number(645)], + ['calc(5px * 8rem/2 + 1)', EvalValue.Number(321)], + ['calc(var(--test-8rem))', EvalValue.Number(128)], + ['calc(var(--test-1))', EvalValue.Number(0)], // Var not found + ['calc(5px * var(--test-8rem)/2 + 1)', EvalValue.Number(321)], + ])('when given "%s"', (expr, expected) => { + it('should evaluate the result of math', async () => { + const evalValue = await evalExpr(parseExpr(expr), actions) + expect(evalValue).toEqual(expected) + }) + }) +}) diff --git a/tests/parser.spec.ts b/tests/parser.spec.ts index 494bb51..16480e6 100644 --- a/tests/parser.spec.ts +++ b/tests/parser.spec.ts @@ -267,5 +267,23 @@ describe('parser', () => { }), ]) }) + + it('parses calc expression with vars', () => { + expect(parse(`calc(5px * var(--value))`)).toEqual([ + Expr.Call({ + name: 'calc', + args: [ + Expr.BinOp({ + op: '*', + left: Expr.LiteralNumber({ value: 5, unit: 'px' }), + right: Expr.Call({ + name: 'var', + args: [Expr.VarIdentifier('--value')], + }), + }), + ], + }), + ]) + }) }) }) |
