aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/eval.ts113
-rw-r--r--src/index.ts157
-rw-r--r--src/parser.ts78
-rw-r--r--src/utils/adt.ts32
-rw-r--r--src/utils/parser-comb.ts120
-rw-r--r--src/utils/result.ts18
6 files changed, 518 insertions, 0 deletions
diff --git a/src/eval.ts b/src/eval.ts
new file mode 100644
index 0000000..5e99282
--- /dev/null
+++ b/src/eval.ts
@@ -0,0 +1,113 @@
+import { CSSUnit, Expr } from './parser'
+import { match, matchString } from './utils/adt'
+
+export type EvalActions = {
+ addClass(id: string, classes: string): Promise<void>
+ removeClass(id: string, classes: string): Promise<void>
+ delay(num: number): Promise<void>
+ jsEval(js: string): Promise<any>
+ loadCssx(id: string, url: string): Promise<string>
+ getVariable(name: string): Promise<string | undefined>
+ updateVariable(id: string, varName: string, value: string): Promise<void>
+ setAttribute(name: string, value: string): Promise<void>
+ withEvent(fn: (e: any) => void): Promise<void>
+ getFormData(): Promise<FormData | undefined>
+ sendRequest(_: {
+ method: string
+ url: string
+ data: FormData | undefined
+ }): Promise<void>
+ // calculate ??
+}
+
+type EvalValue = string | undefined | void
+
+export const evalExpr = async (
+ expr: Expr,
+ actions: EvalActions,
+): Promise<EvalValue> =>
+ match<Promise<EvalValue>, Expr>(expr, {
+ Call: async ({ name, args }) => getFunctions(name, args, actions),
+ LiteralString: async s => s,
+ LiteralNumber: async ({ value, unit }) =>
+ matchString<number, CSSUnit>(unit, {
+ s: () => value * 1000,
+ _: () => value,
+ }).toString(),
+ Identifier: async s => s,
+ VarIdentifier: async s => s,
+ _: async _ => undefined,
+ })
+
+const getFunctions = (name: string, args: Expr[], actions: EvalActions) =>
+ matchString<Promise<EvalValue>>(name, {
+ 'add-class': async () => {
+ const id = await evalExpr(args[0], actions)
+ const classes = await evalExpr(args[1], actions)
+ if (id && classes) {
+ await actions.addClass(id, classes)
+ }
+ },
+ 'remove-class': async () => {
+ const id = await evalExpr(args[0], actions)
+ const classes = await evalExpr(args[1], actions)
+ if (id && classes) {
+ await actions.removeClass(id, classes)
+ }
+ },
+
+ delay: async () => {
+ const num = await evalExpr(args[0], actions)
+ console.log(num)
+ num && (await actions.delay(parseInt(num, 10)))
+ },
+ 'js-eval': async () => {
+ const js = await evalExpr(args[0], actions)
+ js && (await actions.jsEval(js))
+ },
+
+ 'load-cssx': async () => {
+ const id = await evalExpr(args[0], actions)
+ const url = await evalExpr(args[1], actions)
+ if (id && url) {
+ await actions.loadCssx(id, url)
+ }
+ },
+
+ var: async () => {
+ const varName = await evalExpr(args[0], actions)
+ const defaultValue = await evalExpr(args[1], actions)
+ return varName && (actions.getVariable(varName) ?? defaultValue)
+ },
+ update: async () => {
+ const id = await evalExpr(args[0], actions)
+ const varName = await evalExpr(args[1], actions)
+ const value = await evalExpr(args[2], actions)
+ if (id && varName && value) {
+ actions.updateVariable(id, varName, value)
+ }
+ },
+
+ 'set-attr': async () => {
+ const name = await evalExpr(args[0], actions)
+ const value = await evalExpr(args[1], actions)
+ if (name && value) {
+ actions.setAttribute(name, value)
+ }
+ },
+ 'prevent-default': async () => actions.withEvent(e => e.preventDefault()),
+
+ request: async () => {
+ const url = await evalExpr(args[0], actions)
+ const method = args[1]
+ ? (await evalExpr(args[1], actions)) ?? 'post'
+ : 'post'
+
+ if (url) {
+ const data = await actions.getFormData()
+ await actions.sendRequest({ method, url, data })
+ }
+ },
+
+ _: () => Promise.reject(new Error('not supposed to be here')),
+ })
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..2d70c3f
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,157 @@
+import { EvalActions, evalExpr } from './eval'
+import { parse } from './parser'
+
+const UNSET_PROPERTY_VALUE = '<unset>'
+const EVENT_HANDLERS = {
+ click: '--cssx-on-click',
+ load: '--cssx-on-load',
+ mount: '--cssx-on-mount',
+ submit: '--cssx-on-submit',
+}
+
+const PROPERTIES = [
+ '--cssx-children',
+ '--cssx-text',
+ '--cssx-disgustingly-set-innerhtml',
+]
+
+const injectStyles = () => {
+ const STYLE_TAG_CLASS = 'cssx-style-root'
+ if (document.querySelector(`.${STYLE_TAG_CLASS}`)) return
+
+ const $style = document.createElement('style')
+ $style.className = STYLE_TAG_CLASS
+
+ const properties = [...PROPERTIES, ...Object.values(EVENT_HANDLERS)]
+
+ $style.textContent = `.cssx-layer {
+ ${properties.map(p => `${p}: ${UNSET_PROPERTY_VALUE};`).join(' ')}
+ }`
+
+ document.body.appendChild($style)
+}
+
+const getPropertyValue = ($element: Element, prop: string) => {
+ const value = `${getComputedStyle($element).getPropertyValue(prop)}`.trim()
+ return !value || value === UNSET_PROPERTY_VALUE ? '' : value
+}
+
+const getChildrenIds = ($element: Element) => {
+ const value = getPropertyValue($element, '--cssx-children')
+ return value.split(/(\s*,\s*)|\s+/g).filter(Boolean)
+}
+
+const getEvalActions = ($element: Element, event: any): EvalActions => ({
+ addClass: async (id, cls) => document.getElementById(id)?.classList.add(cls),
+ removeClass: async (id, cls) =>
+ document.getElementById(id)?.classList.remove(cls),
+ delay: delay => new Promise(res => setTimeout(res, delay)),
+ jsEval: async js => (0, eval)(js),
+ loadCssx: async (id, url) =>
+ new Promise((resolve, reject) => {
+ const $link = Object.assign(document.createElement('link'), {
+ href: url,
+ rel: 'stylesheet',
+ })
+ $link.onload = () => {
+ const $el = document.getElementById(id)
+ // NOTE: Maybe create and append to body if no root?
+ if ($el) {
+ manageElement($el)
+ resolve(id)
+ } else {
+ console.error(`[CSSX] Unable to find root for ${id}`)
+ reject(`[CSSX] Unable to find root for ${id}`)
+ }
+ }
+ document.body.appendChild($link)
+ }),
+ getVariable: async varName => getPropertyValue($element, varName),
+ updateVariable: async (targetId, varName, value) => {
+ const $el = document.getElementById(targetId)
+ if ($el) {
+ $el.style.setProperty(varName, JSON.stringify(value))
+ }
+ },
+ setAttribute: async (name, value) => {
+ $element.setAttribute(name, value)
+ },
+ withEvent: async fn => fn(event),
+ getFormData: async () =>
+ $element.nodeName === 'FORM'
+ ? new FormData($element as HTMLFormElement)
+ : undefined,
+ sendRequest: async ({ url, method, data }) => {
+ await fetch(url, { method, body: data })
+ // TODO: Handle response?
+ },
+})
+
+const handleEvents = async (
+ $element: Element,
+ isNewElement: boolean = false,
+) => {
+ for (const [eventType, property] of Object.entries(EVENT_HANDLERS)) {
+ const handlerExpr = getPropertyValue($element, property)
+
+ if (handlerExpr) {
+ const eventHandler = async (event: any) => {
+ console.log(`Triggered event: ${eventType}`)
+ const exprs = parse(handlerExpr)
+ for (const expr of exprs) {
+ await evalExpr(expr, getEvalActions($element, event))
+ }
+ }
+
+ if (eventType === 'mount') {
+ if (isNewElement) setTimeout(eventHandler)
+ } else {
+ ;($element as any)[`on${eventType}`] = eventHandler
+ }
+ }
+ }
+}
+
+const manageElement = async (
+ $element: Element,
+ isNewElement: boolean = false,
+) => {
+ await handleEvents($element, isNewElement)
+
+ const text = getPropertyValue($element, '--cssx-text')
+ if (text) $element.textContent = text
+
+ const html = getPropertyValue($element, '--cssx-disgustingly-set-innerhtml')
+ if (html) $element.innerHTML = html.replace(/(^'|")|('|"$)/g, '')
+
+ const childrenIds = getChildrenIds($element)
+ if (childrenIds.length > 0) {
+ const LAYER_CLASS = 'cssx-layer'
+ const $childrenRoot =
+ $element.querySelector(`:scope > .${LAYER_CLASS}`) ??
+ Object.assign(document.createElement('div'), { className: LAYER_CLASS })
+ $element.appendChild($childrenRoot)
+
+ for (const childId of childrenIds) {
+ const selector = childId.split('#')
+ const [tag, id] = selector.length >= 2 ? selector : ['div', ...selector]
+ let $child = $childrenRoot.querySelector(`:scope > #${id}`)
+ const isNewElement = !$child
+ if (!$child) {
+ $child = Object.assign(document.createElement(tag || 'div'), { id })
+ }
+ $childrenRoot.appendChild($child)
+ await manageElement($child, isNewElement)
+ }
+ }
+}
+
+interface Options {
+ root?: HTMLElement
+}
+const render = async ({ root = document.body }: Options = {}) => {
+ injectStyles()
+ await manageElement(root)
+}
+
+render() \ No newline at end of file
diff --git a/src/parser.ts b/src/parser.ts
new file mode 100644
index 0000000..3279f57
--- /dev/null
+++ b/src/parser.ts
@@ -0,0 +1,78 @@
+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)}...`)
+ },
+ })
+}
diff --git a/src/utils/adt.ts b/src/utils/adt.ts
new file mode 100644
index 0000000..45d9e50
--- /dev/null
+++ b/src/utils/adt.ts
@@ -0,0 +1,32 @@
+type TagValue<T, N> = T extends Tag<N, infer V> ? V : never
+
+export const match = <R, T extends Tag<string, any>>(
+ tag: T,
+ pattern: {
+ [key in T['tag'] | '_']?: (v: TagValue<T, key>) => R
+ },
+): R => ((pattern as any)[tag.tag] || (pattern._ as any))(tag.value)
+
+export const matchString = <R, T extends string = string>(
+ key: T,
+ pattern: {
+ [key in T | '_']?: (key: key) => R
+ },
+): R => ((pattern as any)[key] || (pattern._ as any))(key)
+
+type Tag<N, V> = { tag: N; value: V }
+export type Enum<T> = { [N in keyof T]: Tag<N, T[N]> }[keyof T]
+
+export const constructors = <T extends Tag<string, any>>(): {
+ [N in T['tag']]: TagValue<T, N> extends null | never
+ ? (value?: null | never) => T
+ : (value: TagValue<T, N>) => T
+} =>
+ new Proxy(
+ {},
+ {
+ get(_, k) {
+ return (value: any) => ({ tag: k, value })
+ },
+ },
+ ) as any
diff --git a/src/utils/parser-comb.ts b/src/utils/parser-comb.ts
new file mode 100644
index 0000000..3f171b8
--- /dev/null
+++ b/src/utils/parser-comb.ts
@@ -0,0 +1,120 @@
+import { match } from './adt'
+import { Result, mapResult, chainResult } from './result'
+
+export type ParseResult<T> = Result<
+ { value: T; input: string },
+ { error: string; input: string }
+>
+
+export type Parser<T> = (input: string) => ParseResult<T>
+
+export const regex =
+ (re: RegExp): Parser<string> =>
+ input => {
+ if (input.length === 0) return Result.Err({ error: 'fuckedinput', input })
+ const res = input.match(re)
+ if (!res) return Result.Err({ error: 'fucked', input })
+ return Result.Ok({ value: res[0], input: input.replace(re, '') })
+ }
+
+export const string =
+ (str: string): Parser<string> =>
+ input => {
+ if (input.length === 0) return Result.Err({ error: 'fuckedinput', input })
+ if (!input.startsWith(str))
+ return Result.Err({ error: 'fuckedstring', input })
+ return Result.Ok({ value: str, input: input.slice(str.length) })
+ }
+
+export const or =
+ <T>([parser, ...rest]: Array<Parser<T>>): Parser<T> =>
+ input => {
+ if (rest.length === 0) return parser(input)
+ const result = parser(input)
+ return match(result, {
+ Ok: () => result,
+ Err: _ => or(rest)(input),
+ })
+ }
+
+export const mapParseResult =
+ <T, R>(
+ parser: Parser<T>,
+ fn: (_: { value: T; input: string }) => { value: R; input: string },
+ ): Parser<R> =>
+ input =>
+ mapResult(parser(input), fn)
+
+export const map = <T, R>(parser: Parser<T>, fn: (_: T) => R): Parser<R> =>
+ mapParseResult(parser, ({ value, ...rest }) => ({
+ ...rest,
+ value: fn(value),
+ }))
+
+export const zip2 =
+ <A, B>(parserA: Parser<A>, parserB: Parser<B>): Parser<readonly [A, B]> =>
+ input => {
+ // TODO: refactor please. shit code
+ const resa: Result<
+ { value: A; input: string },
+ { error: string; input: string }
+ > = parserA(input)
+ return chainResult(resa, ({ value: a, input: inputB }) => {
+ const res: Result<
+ { value: readonly [A, B]; input: string },
+ { error: string; input: string }
+ > = map(parserB, b => [a, b] as const)(inputB)
+ return res
+ })
+ }
+
+export const prefixed = <A>(
+ parserPrefix: Parser<any>,
+ parser: Parser<A>,
+): Parser<A> => map(zip2(parserPrefix, parser), ([_, a]) => a)
+
+export const suffixed = <A>(
+ parser: Parser<A>,
+ parserSuffix: Parser<any>,
+): Parser<A> => map(zip2(parser, parserSuffix), ([a, _]) => a)
+
+export const between = <A>(
+ prefix: Parser<any>,
+ parser: Parser<A>,
+ suffix: Parser<any>,
+): Parser<A> => suffixed(prefixed(prefix, parser), suffix)
+
+export const many0 =
+ <A>(parser: Parser<A>): Parser<Array<A>> =>
+ originalInput =>
+ match(parser(originalInput), {
+ Ok: ({ value, input }) => map(many0(parser), ls => [value, ...ls])(input),
+ Err: ({ input }) => Result.Ok({ value: [], input }),
+ })
+
+export const many1 =
+ <A>(parser: Parser<A>): Parser<Array<A>> =>
+ originalInput =>
+ match(parser(originalInput), {
+ Ok: ({ value, input }) => map(many0(parser), ls => [value, ...ls])(input),
+ Err: err => Result.Err(err),
+ })
+
+export const sepBy =
+ <A>(parser: Parser<A>, sepP: Parser<any>): Parser<Array<A>> =>
+ originalInput =>
+ match(parser(originalInput), {
+ Ok: ({ value, input }) =>
+ map(many0(prefixed(sepP, parser)), ls => [value, ...ls])(input),
+ Err: _ => Result.Ok({ value: [], input: originalInput }),
+ })
+
+export const optional =
+ <A>(parser: Parser<A>): Parser<undefined | A> =>
+ input => {
+ const result = parser(input)
+ return match(result, {
+ Ok: _ => result,
+ Err: _ => Result.Ok({ value: undefined, input }),
+ })
+ }
diff --git a/src/utils/result.ts b/src/utils/result.ts
new file mode 100644
index 0000000..39a7fe6
--- /dev/null
+++ b/src/utils/result.ts
@@ -0,0 +1,18 @@
+import { Enum, constructors, match } from './adt'
+
+export type Result<V, E> = Enum<{ Ok: V; Err: E }>
+export const Result = constructors<Result<any, any>>()
+
+export const mapResult = <A, B, E>(
+ res: Result<A, E>,
+ fn: (_: A) => B,
+): Result<B, E> => chainResult(res, a => Result.Ok(fn(a)))
+
+export const chainResult = <A, B, E>(
+ res: Result<A, E>,
+ fn: (_: A) => Result<B, E>,
+): Result<B, E> =>
+ match(res, {
+ Ok: a => fn(a),
+ Err: e => Result.Err(e),
+ })