From e01cb693bc5737792e2b37abfd98d2d8f81bac4d Mon Sep 17 00:00:00 2001 From: Akshay Nair Date: Thu, 10 Aug 2023 22:45:00 +0530 Subject: feat: adds simple evaluator --- examples/api/style.css | 2 +- src/eval.ts | 40 +++++++++++++++++++++++++ src/parse-expr.ts | 66 ----------------------------------------- src/parser.ts | 66 +++++++++++++++++++++++++++++++++++++++++ src/utils/adt.ts | 5 ++++ src/utils/parser-comb.ts | 15 ++-------- src/utils/result.ts | 14 +++++++++ tests/eval.spec.ts | 29 ++++++++++++++++++ tests/parse-expr.spec.ts | 76 ------------------------------------------------ tests/parser.spec.ts | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 233 insertions(+), 156 deletions(-) create mode 100644 src/eval.ts delete mode 100644 src/parse-expr.ts create mode 100644 src/parser.ts create mode 100644 src/utils/result.ts create mode 100644 tests/eval.spec.ts delete mode 100644 tests/parse-expr.spec.ts create mode 100644 tests/parser.spec.ts diff --git a/examples/api/style.css b/examples/api/style.css index ad1dff9..d1be912 100644 --- a/examples/api/style.css +++ b/examples/api/style.css @@ -5,7 +5,7 @@ body { #load-btn { --cssx-on-click: add-class(load-btn, 'loading') - get(url('./more-style.css')) + get('./more-style.css') add-class(load-btn, 'hidden'); } #load-btn.loading { diff --git a/src/eval.ts b/src/eval.ts new file mode 100644 index 0000000..1e18457 --- /dev/null +++ b/src/eval.ts @@ -0,0 +1,40 @@ +import { Expr } from "./parser"; +import { match, matchString } from "./utils/adt"; + +export type Dependencies = { + addClass(id: string, classes: string): Promise + removeClass(id: string, classes: string): Promise + // requestGetCss(url: string): Promise + // getVarable(name: string, def?: string): Promise + // updateVariable(id: string, varName: string, value: string): Promise + // calculate ?? +} + +export const evalExpr = async (expr: Expr, deps: Dependencies): Promise => + match, Expr>(expr, { + Call: async ({ name, args }) => { + await matchString(name, { + 'add-class': async () => { + const id = await evalExpr(args[0], deps) + const classes = await evalExpr(args[1], deps) + if (id && classes) { + await deps.addClass(id, classes) + } + }, + 'remove-class': async () => { + const id = await evalExpr(args[0], deps) + const classes = await evalExpr(args[1], deps) + if (id && classes) { + await deps.removeClass(id, classes) + } + }, + _: () => Promise.reject(new Error('not supposed to be here')), + }) + return undefined + }, + LiteralString: async s => s, + Identifier: async s => s, + VarIdentifier: async s => s, + _: async _ => undefined, + }) + diff --git a/src/parse-expr.ts b/src/parse-expr.ts deleted file mode 100644 index 1c5c08f..0000000 --- a/src/parse-expr.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Enum, constructors, match } from './utils/adt' -import * as P from './utils/parser-comb' - -export type Expr = Enum<{ - Call: { name: string; args: Expr[] } - Identifier: string - VarIdentifier: string - LiteralString: string -}> - -export const Expr = constructors() - -const whitespace = P.regex(/^\s*/) -const consumeWhitespace = (p: P.Parser): P.Parser => - P.between(whitespace, p, whitespace) -const comma = consumeWhitespace(P.string(',')) -const parens = (p: P.Parser): P.Parser => - 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 = P.map( - P.or([ - P.between(singleQuote, P.regex(/^[^']*/), singleQuote), - P.between(doubleQuote, P.regex(/^[^"]*/), doubleQuote), - ]), - Expr.LiteralString -) - -const exprParser: P.Parser = P.or([ - stringLiteralParser, - varIdentifierExprParser, - callExprParser, - identifierExprParser, -]) - -const multiExprParser = P.many1(exprParser) - -export const parse = (input: string): Array => { - 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)}...`) - }, - }) -} diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..1c5c08f --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,66 @@ +import { Enum, constructors, match } from './utils/adt' +import * as P from './utils/parser-comb' + +export type Expr = Enum<{ + Call: { name: string; args: Expr[] } + Identifier: string + VarIdentifier: string + LiteralString: string +}> + +export const Expr = constructors() + +const whitespace = P.regex(/^\s*/) +const consumeWhitespace = (p: P.Parser): P.Parser => + P.between(whitespace, p, whitespace) +const comma = consumeWhitespace(P.string(',')) +const parens = (p: P.Parser): P.Parser => + 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 = P.map( + P.or([ + P.between(singleQuote, P.regex(/^[^']*/), singleQuote), + P.between(doubleQuote, P.regex(/^[^"]*/), doubleQuote), + ]), + Expr.LiteralString +) + +const exprParser: P.Parser = P.or([ + stringLiteralParser, + varIdentifierExprParser, + callExprParser, + identifierExprParser, +]) + +const multiExprParser = P.many1(exprParser) + +export const parse = (input: string): Array => { + 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)}...`) + }, + }) +} diff --git a/src/utils/adt.ts b/src/utils/adt.ts index e9a6c51..7234f67 100644 --- a/src/utils/adt.ts +++ b/src/utils/adt.ts @@ -5,6 +5,11 @@ export const match = [key in T['tag'] | '_']?: (v: TagValue) => R }): R => ((pattern as any)[tag.tag] || (pattern._ as any))(tag.value) +export const matchString = + (key: T, pattern: { + [key in T | '_']?: (key: key) => R + }): R => ((pattern as any)[key] || (pattern._ as any))(key) + type Tag = { tag: N; value: V } export type Enum = { [N in keyof T]: Tag }[keyof T] diff --git a/src/utils/parser-comb.ts b/src/utils/parser-comb.ts index 49f633e..e0665f6 100644 --- a/src/utils/parser-comb.ts +++ b/src/utils/parser-comb.ts @@ -1,16 +1,5 @@ -import { Enum, constructors, match } from './adt'; - -export type Result = Enum<{ Ok: V, Err: E }> -export const Result = constructors>() - -export const mapResult = (res: Result, fn: (_: A) => B): Result => - chainResult(res, a => Result.Ok(fn(a))) - -export const chainResult = (res: Result, fn: (_: A) => Result): Result => - match(res, { - Ok: a => fn(a), - Err: e => Result.Err(e), - }); +import { match } from './adt'; +import { Result, mapResult, chainResult } from './result'; export type ParseResult = Result<{ value: T, input: string }, { error: string, input: string }>; diff --git a/src/utils/result.ts b/src/utils/result.ts new file mode 100644 index 0000000..c0120b8 --- /dev/null +++ b/src/utils/result.ts @@ -0,0 +1,14 @@ +import { Enum, constructors, match } from "./adt"; + +export type Result = Enum<{ Ok: V, Err: E }> +export const Result = constructors>() + +export const mapResult = (res: Result, fn: (_: A) => B): Result => + chainResult(res, a => Result.Ok(fn(a))) + +export const chainResult = (res: Result, fn: (_: A) => Result): Result => + match(res, { + Ok: a => fn(a), + Err: e => Result.Err(e), + }); + diff --git a/tests/eval.spec.ts b/tests/eval.spec.ts new file mode 100644 index 0000000..63062e3 --- /dev/null +++ b/tests/eval.spec.ts @@ -0,0 +1,29 @@ +import { Dependencies, evalExpr } from '../src/eval' +import { Expr } from '../src/parser' + +describe('eval', () => { + const deps: Dependencies = { + addClass: jest.fn(), + removeClass: jest.fn(), + } + + it('should add classes', async () => { + await evalExpr(Expr.Call({ + name: 'add-class', + args: [ Expr.Identifier('element-id'), Expr.LiteralString('class-name') ], + }), deps) + + expect(deps.addClass).toHaveBeenCalledTimes(1) + expect(deps.addClass).toHaveBeenCalledWith('element-id', 'class-name') + }) + + it('should add classes', async () => { + await evalExpr(Expr.Call({ + name: 'remove-class', + args: [ Expr.Identifier('element-id'), Expr.LiteralString('class-name') ], + }), deps) + + expect(deps.removeClass).toHaveBeenCalledTimes(1) + expect(deps.removeClass).toHaveBeenCalledWith('element-id', 'class-name') + }) +}) diff --git a/tests/parse-expr.spec.ts b/tests/parse-expr.spec.ts deleted file mode 100644 index f873464..0000000 --- a/tests/parse-expr.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Expr, parse } from '../src/parse-expr' - -describe('parser', () => { - it('should parse function call', () => { - expect(parse('hello()')).toEqual([Expr.Call({ name: 'hello', args: [] })]) - expect(parse('hello ( wow , foo ) ')).toEqual([ - Expr.Call({ - name: 'hello', - args: [Expr.Identifier('wow'), Expr.Identifier('foo')], - }), - ]) - expect(parse('hello(wow,foo)')).toEqual([ - Expr.Call({ - name: 'hello', - args: [Expr.Identifier('wow'), Expr.Identifier('foo')], - }), - ]) - expect(parse('hello(wow,foo, coolio)')).toEqual([ - Expr.Call({ - name: 'hello', - args: [ - Expr.Identifier('wow'), - Expr.Identifier('foo'), - Expr.Identifier('coolio'), - ], - }), - ]) - expect(parse('hello(wow)')).toEqual([ - Expr.Call({ name: 'hello', args: [Expr.Identifier('wow')] }), - ]) - }) - - it('should parse sequential function calls', () => { - expect(parse('hello(world) foo-doo(bar, baz)')).toEqual([ - Expr.Call({ - name: 'hello', - args: [Expr.Identifier('world')], - }), - Expr.Call({ - name: 'foo-doo', - args: [Expr.Identifier('bar'), Expr.Identifier('baz')], - }), - ]) - }) - - it('should parse string literal', () => { - expect(parse(`"hello world toodles \' nice single quote there"`)).toEqual([ - Expr.LiteralString(`hello world toodles \' nice single quote there`), - ]) - - expect(parse(` 'hello world toodles \" nice double quote there' `)).toEqual( - [Expr.LiteralString(`hello world toodles \" nice double quote there`)] - ) - }) - - it('should parse var identifiers', () => { - expect(parse(`var(--hello, 'default')`)).toEqual([ - Expr.Call({ - name: 'var', - args: [Expr.VarIdentifier('--hello'), Expr.LiteralString(`default`)], - }), - ]) - - expect(parse(`calc(var(--hello))`)).toEqual([ - Expr.Call({ - name: 'calc', - args: [ - Expr.Call({ - name: 'var', - args: [Expr.VarIdentifier('--hello')], - }), - ], - }), - ]) - }) -}) diff --git a/tests/parser.spec.ts b/tests/parser.spec.ts new file mode 100644 index 0000000..48c398c --- /dev/null +++ b/tests/parser.spec.ts @@ -0,0 +1,76 @@ +import { Expr, parse } from '../src/parser' + +describe('parser', () => { + it('should parse function call', () => { + expect(parse('hello()')).toEqual([Expr.Call({ name: 'hello', args: [] })]) + expect(parse('hello ( wow , foo ) ')).toEqual([ + Expr.Call({ + name: 'hello', + args: [Expr.Identifier('wow'), Expr.Identifier('foo')], + }), + ]) + expect(parse('hello(wow,foo)')).toEqual([ + Expr.Call({ + name: 'hello', + args: [Expr.Identifier('wow'), Expr.Identifier('foo')], + }), + ]) + expect(parse('hello(wow,foo, coolio)')).toEqual([ + Expr.Call({ + name: 'hello', + args: [ + Expr.Identifier('wow'), + Expr.Identifier('foo'), + Expr.Identifier('coolio'), + ], + }), + ]) + expect(parse('hello(wow)')).toEqual([ + Expr.Call({ name: 'hello', args: [Expr.Identifier('wow')] }), + ]) + }) + + it('should parse sequential function calls', () => { + expect(parse('hello(world) foo-doo(bar, baz)')).toEqual([ + Expr.Call({ + name: 'hello', + args: [Expr.Identifier('world')], + }), + Expr.Call({ + name: 'foo-doo', + args: [Expr.Identifier('bar'), Expr.Identifier('baz')], + }), + ]) + }) + + it('should parse string literal', () => { + expect(parse(`"hello world toodles \' nice single quote there"`)).toEqual([ + Expr.LiteralString(`hello world toodles \' nice single quote there`), + ]) + + expect(parse(` 'hello world toodles \" nice double quote there' `)).toEqual( + [Expr.LiteralString(`hello world toodles \" nice double quote there`)] + ) + }) + + it('should parse var identifiers', () => { + expect(parse(`var(--hello, 'default')`)).toEqual([ + Expr.Call({ + name: 'var', + args: [Expr.VarIdentifier('--hello'), Expr.LiteralString(`default`)], + }), + ]) + + expect(parse(`calc(var(--hello))`)).toEqual([ + Expr.Call({ + name: 'calc', + args: [ + Expr.Call({ + name: 'var', + args: [Expr.VarIdentifier('--hello')], + }), + ], + }), + ]) + }) +}) -- cgit v1.3.1