aboutsummaryrefslogtreecommitdiff
path: root/src/parser.ts
blob: 9cb04057e1b7a08936aaaf7b1072428751ed294f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import { Enum, constructors, match } from './utils/adt'
import * as P from './utils/parser-comb'

export type CSSUnit = '' | 's' | 'ms'

export type Expr = Enum<{
  Call: { name: string; args: Expr[] }
  Identifier: string
  VarIdentifier: string
  LiteralString: string
  LiteralNumber: { value: number, unit: CSSUnit }
}>

export const Expr = constructors<Expr>()

const whitespace = P.regex(/^\s*/)
const consumeWhitespace = <A>(p: P.Parser<A>): P.Parser<A> =>
  P.between(whitespace, p, whitespace)
const comma = consumeWhitespace(P.string(','))
const parens = <A>(p: P.Parser<A>): P.Parser<A> =>
  P.between(P.string('('), p, P.string(')'))
const identifierParser = P.regex(/^[a-z][a-z0-9_-]*/i)
const varIdentifierParser = P.regex(/^--[a-z][a-z0-9-]*/i)
const singleQuote = P.string("'")
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 stringLiteralParser: P.Parser<Expr> = P.map(
  P.or([
    P.between(singleQuote, P.regex(/^[^']*/), singleQuote),
    P.between(doubleQuote, P.regex(/^[^"]*/), doubleQuote),
  ]),
  Expr.LiteralString
)

const numberParser = P.regex(/^[-+]?((\d*\.\d+)|\d+)/)

const numberExprParser: P.Parser<Expr> = P.map(
  P.zip2(numberParser, P.optional(P.regex(/^(s|ms)/i))),
  ([value, unit]) => Expr.LiteralNumber({ value: Number(value), unit: (unit ?? '') as CSSUnit }),
)

const exprParser: P.Parser<Expr> = P.or([
  stringLiteralParser,
  varIdentifierExprParser,
  numberExprParser,
  callExprParser,
  identifierExprParser,
])

const multiExprParser = P.many1(exprParser)

export const parse = (input: string): Array<Expr> => {
  const res = multiExprParser(input.trim())
  return match(res, {
    Ok: ({ value, input }) => {
      if (input) {
        throw new Error(`Input not consumed completely here brosky: "${input}"`)
      }
      return value
    },
    Err: ({ error, input }) => {
      throw new Error(`${error}.\n Left input: ${input.slice(0, 20)}...`)
    },
  })
}