summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--shell.nix2
-rw-r--r--src/parser.ts101
-rw-r--r--tests/parser.spec.ts64
3 files changed, 137 insertions, 30 deletions
diff --git a/shell.nix b/shell.nix
index 1571649..15a0536 100644
--- a/shell.nix
+++ b/shell.nix
@@ -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' }),
+ }),
+ }),
+ ],
+ }),
+ ])
+ })
+ })
})