diff options
| author | Akshay Nair <phenax5@gmail.com> | 2024-01-21 14:36:47 +0530 |
|---|---|---|
| committer | Akshay Nair <phenax5@gmail.com> | 2024-01-21 14:36:47 +0530 |
| commit | f7a4596469d0652f868cad2d97a9edf92f4bc4c7 (patch) | |
| tree | 6ad377459a86e5ad1f6d45e7bfaaec27403027ee | |
| parent | b31fe6447ec2e05bb9ccf3cffd85510d936c31d6 (diff) | |
| download | css-everything-f7a4596469d0652f868cad2d97a9edf92f4bc4c7.tar.gz css-everything-f7a4596469d0652f868cad2d97a9edf92f4bc4c7.zip | |
feat(parser): adds calc expression parser
| -rw-r--r-- | shell.nix | 2 | ||||
| -rw-r--r-- | src/parser.ts | 101 | ||||
| -rw-r--r-- | tests/parser.spec.ts | 64 |
3 files changed, 137 insertions, 30 deletions
@@ -1,7 +1,7 @@ with (import <nixpkgs> { }); mkShell { buildInputs = [ - nodejs-18_x + nodejs_21 nodePackages.typescript nodePackages.prettier nodePackages.eslint diff --git a/src/parser.ts b/src/parser.ts index 216c1bc..a2bb577 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,7 +1,10 @@ -import { Enum, constructors, match } from './utils/adt' +import { Enum, constructors, match, matchString } from './utils/adt' import * as P from './utils/parser-comb' +import { Result } from './utils/result' -export type CSSUnit = '' | 's' | 'ms' +export type CSSUnit = '' | 's' | 'ms' | 'px' | '%' | 'rem' | 'em' + +export type BinOp = '+' | '-' | '*' | '/' export interface Selector { tag: string | undefined @@ -21,6 +24,8 @@ export type Expr = Enum<{ VarIdentifier: string LiteralString: string LiteralNumber: { value: number; unit: CSSUnit } + BinOp: { op: BinOp; left: Expr; right: Expr } + Parens: { expr: Expr } Pair: { key: string; value: Expr } Selector: Selector @@ -42,14 +47,16 @@ const doubleQuote = P.string('"') const identifierExprParser = P.map(identifierParser, Expr.Identifier) const varIdentifierExprParser = P.map(varIdentifierParser, Expr.VarIdentifier) -const callExprParser = (input: string) => - P.map( - P.zip2( - consumeWhitespace(identifierParser), - parens(consumeWhitespace(P.sepBy(exprParser, comma))), - ), - ([name, args]) => Expr.Call({ name, args }), - )(input) +const callExprParser = + (fnParser?: P.Parser<string>, argParser?: P.Parser<Expr>) => + (input: string) => + P.map( + P.zip2( + consumeWhitespace(fnParser ?? identifierParser), + parens(consumeWhitespace(P.sepBy(argParser ?? exprParser, comma))), + ), + ([name, args]) => Expr.Call({ name, args }), + )(input) const stringLiteralParser = P.or([ P.between(singleQuote, P.regex(/^[^']*/), singleQuote), @@ -60,9 +67,10 @@ const stringLiteralExprParser: P.Parser<Expr> = P.map( Expr.LiteralString, ) +const unitParser = P.regex(/^(s|ms|%|px|rem|em)/i) const numberParser = P.regex(/^[-+]?((\d*\.\d+)|\d+)/) const numberExprParser: P.Parser<Expr> = P.map( - P.zip2(numberParser, P.optional(P.regex(/^(s|ms)/i))), + P.zip2(numberParser, P.optional(unitParser)), ([value, unit]) => Expr.LiteralNumber({ value: Number(value), unit: (unit ?? '') as CSSUnit }), ) @@ -98,15 +106,66 @@ const pairExprParser: P.Parser<Expr> = (input: string) => ([key, value]) => Expr.Pair({ key, value }), )(input) -const exprParser: P.Parser<Expr> = P.or([ - stringLiteralExprParser, - numberExprParser, - callExprParser, - pairExprParser, - varIdentifierExprParser, - selectorExprParser, - identifierExprParser, -]) +const precedence = (op: BinOp) => + matchString(op, { + '+': () => 0, + '-': () => 0, + '*': () => 1, + '/': () => 2, + _: () => -1, + }) + +const binOpWithFixitySwitchity = (op: BinOp, left: Expr, right: Expr) => + match(right, { + BinOp: binOp => { + if (precedence(op) > precedence(binOp.op)) { + return Expr.BinOp({ + op: binOp.op, + left: Expr.BinOp({ + op, + left: left, + right: binOp.left, + }), + right: binOp.right, + }) + } + return Expr.BinOp({ op, left, right }) + }, + Parens: ({ expr }) => Expr.BinOp({ op, left, right: expr }), + _: () => Expr.BinOp({ op, left, right }), + }) + +const allowParens = (p: P.Parser<Expr>): P.Parser<Expr> => + P.or([P.map(parens(p), expr => Expr.Parens({ expr })), p]) + +const binOpP = P.regex(/^[+\-*/]/) + +const binOpExprParser: P.Parser<Expr> = allowParens((input: string) => + match(exprParser(input), { + Ok: ({ value, input: rest }) => + P.map( + P.optional(P.zip2(consumeWhitespace(binOpP), binOpExprParser)), + res => + res + ? binOpWithFixitySwitchity(res[0] as BinOp, value, res[1]) + : value, + )(rest), + Err: _ => Result.Ok({ value: [], input }), + }), +) + +const exprParser: P.Parser<Expr> = allowParens( + P.or([ + stringLiteralExprParser, + numberExprParser, + callExprParser(P.string('calc'), binOpExprParser), + callExprParser(), + pairExprParser, + varIdentifierExprParser, + selectorExprParser, + identifierExprParser, + ]), +) export const parseExpr = (input: string): Expr => { return match(exprParser(input), { @@ -120,7 +179,7 @@ export const parseExpr = (input: string): Expr => { }) } -const declarationParser = P.or([callExprParser, selectorExprParser]) +const declarationParser = P.or([callExprParser(), selectorExprParser]) const multiDeclarationParser = P.sepBy(declarationParser, whitespace) diff --git a/tests/parser.spec.ts b/tests/parser.spec.ts index 825200e..494bb51 100644 --- a/tests/parser.spec.ts +++ b/tests/parser.spec.ts @@ -1,7 +1,7 @@ import { Expr, SelectorComp, parse, parseDeclarations } from '../src/parser' describe('parser', () => { - it('should parse function call', () => { + it('parses function call', () => { expect(parse('hello()')).toEqual([Expr.Call({ name: 'hello', args: [] })]) expect(parse('hello ( wow , foo ) ')).toEqual([ Expr.Call({ @@ -30,7 +30,7 @@ describe('parser', () => { ]) }) - it('should parse sequential function calls', () => { + it('parses sequential function calls', () => { expect(parse('hello(world) foo-doo(bar, baz)')).toEqual([ Expr.Call({ name: 'hello', @@ -43,7 +43,7 @@ describe('parser', () => { ]) }) - it('should parse string literal', () => { + it('parses string literal', () => { expect(parse(`"hello world toodles ' nice single quote there"`)).toEqual([ Expr.LiteralString(`hello world toodles ' nice single quote there`), ]) @@ -53,7 +53,7 @@ describe('parser', () => { ]) }) - it('should parse var identifiers', () => { + it('parses var identifiers', () => { expect(parse(`var(--hello, 'default')`)).toEqual([ Expr.Call({ name: 'var', @@ -88,7 +88,7 @@ describe('parser', () => { ]) }) - it('should parse number and css units', () => { + it('parses number and css units', () => { expect(parse(`100`)).toEqual([Expr.LiteralNumber({ value: 100, unit: '' })]) expect(parse(`100s`)).toEqual([ Expr.LiteralNumber({ value: 100, unit: 's' }), @@ -125,7 +125,7 @@ describe('parser', () => { ]) }) - it('should parse pair and map expressions', () => { + it('parses pair and map expressions', () => { expect(parse(`--hello: "foobar is here"`)).toEqual([ Expr.Pair({ key: '--hello', @@ -156,7 +156,7 @@ describe('parser', () => { }) describe('parseDeclarations', () => { - it('should parse complex selectors', () => { + it('parses complex selectors', () => { expect( parseDeclarations(`button#something.my-class[hello=world]`), ).toEqual([ @@ -194,7 +194,7 @@ describe('parser', () => { ]) }) - it('should parse declarations', () => { + it('parses declarations', () => { expect( parseDeclarations( `instance(button#something, map(--text: "wow", --color: red))`, @@ -220,4 +220,52 @@ describe('parser', () => { ]) }) }) + + describe('calc', () => { + it('parses calc expression', () => { + expect(parse(`calc(50% * 10px + 1px )`)).toEqual([ + Expr.Call({ + name: 'calc', + args: [ + Expr.BinOp({ + op: '+', + left: Expr.BinOp({ + op: '*', + left: Expr.LiteralNumber({ value: 50, unit: '%' }), + right: Expr.LiteralNumber({ value: 10, unit: 'px' }), + }), + right: Expr.LiteralNumber({ value: 1, unit: 'px' }), + }), + ], + }), + ]) + }) + + it('parses calc expression with parens', () => { + expect(parse(`calc((5))`)).toEqual([ + Expr.Call({ + name: 'calc', + args: [ + Expr.Parens({ expr: Expr.LiteralNumber({ value: 5, unit: '' }) }), + ], + }), + ]) + expect(parse(`calc(50% * (10px + 1px) )`)).toEqual([ + Expr.Call({ + name: 'calc', + args: [ + Expr.BinOp({ + op: '*', + left: Expr.LiteralNumber({ value: 50, unit: '%' }), + right: Expr.BinOp({ + op: '+', + left: Expr.LiteralNumber({ value: 10, unit: 'px' }), + right: Expr.LiteralNumber({ value: 1, unit: 'px' }), + }), + }), + ], + }), + ]) + }) + }) }) |
