diff options
Diffstat (limited to '')
| -rw-r--r-- | TODO.norg | 43 | ||||
| -rw-r--r-- | examples/todo-list/style.css | 3 | ||||
| -rw-r--r-- | src/declarations.ts | 6 | ||||
| -rw-r--r-- | src/eval.ts | 171 | ||||
| -rw-r--r-- | src/index.ts | 4 | ||||
| -rw-r--r-- | src/utils/adt.ts | 22 | ||||
| -rw-r--r-- | tests/eval.spec.ts | 11 |
7 files changed, 189 insertions, 71 deletions
@@ -1,4 +1,28 @@ * Now + - (x) Refactor eval to return EvalValue + - ( ) Functions. `--my-func: func(if(get-var(--x), get-var(--y), ""))`. + - ( ) Function calls. `call(get-var(--my-func), --x: 6, --y: 2)` + - ( ) Evaluate `calc` + - ( ) string concatenation + - ( ) analog + digital clock example + +* Later + - ( ) Update `--cssx-text` on update + - ( ) `request` error handling + - ( ) `has-class` + - ( ) `add-class` & `remove-class` should use self if id is not specified? + - ( ) keyboard events + - ( ) Code cleanup + - ( ) Additional events + - ( ) Improve parser error messages + - ( ) Improve eval error messages + - ( ) filter for on update on specific properties + +* Maybe + - ( ) `list` & `tuple` data structures? + - ( ) *server-side css*? Why the fuck not!? + +* Done - (x) Hydrate existing elements instead of re-creating - (x) `load-cssx` functions - (x) `get-variable` @@ -22,23 +46,4 @@ - (x) access child from an instance (update checkbox) - (x) access instance from child (delete task) - (x) focus blur events - - ( ) string concatenation - - ( ) eval - - ( ) Update --cssx-text on update - - ( ) keyboard events - - ( ) Code cleanup - - ( ) `request` error handling - -* Later - - ( ) Evaluate `calc` - - ( ) `add-class` & `remove-class` should use self if id is not specified? - - ( ) access an instance of component - - ( ) Additional events - - ( ) Improve error messages - - ( ) filter for on update on specific properties - - ( ) update property on child on update of parent - -* Maybe - - ( ) `list` & `tuple` data structures? - - ( ) server-side css? Why the fuck not!? diff --git a/examples/todo-list/style.css b/examples/todo-list/style.css index efe888f..66ec5f0 100644 --- a/examples/todo-list/style.css +++ b/examples/todo-list/style.css @@ -102,7 +102,7 @@ body * { box-sizing: border-box; } if(get-var(--is-editing), 'none', 'block') ) if(get-var(--is-editing), - call(':scope [data-element=edit-task-input]', focus), + call-method(':scope [data-element=edit-task-input]', focus), "") ; @@ -122,6 +122,7 @@ body * { box-sizing: border-box; } [data-element=task-text] { padding: .2rem .8rem; flex: 2; + --cssx-on-click: update(get-var(--task-item-id), --is-editing, "true"); } [data-element=task-text]::after { diff --git a/src/declarations.ts b/src/declarations.ts index 0571116..5c2f9a3 100644 --- a/src/declarations.ts +++ b/src/declarations.ts @@ -1,4 +1,4 @@ -import { EvalActions, evalExpr } from './eval' +import { EvalActions, evalExprAsString } from './eval' import { Expr, Selector, SelectorComp, parseDeclarations } from './parser' import { match, matchString } from './utils/adt' @@ -22,7 +22,9 @@ export const evaluateDeclaration = async ( const props = await Promise.all( [...properties.entries()].map(async ([key, expr]) => { // Ignore errors? - const result = await evalExpr(expr, actions).catch(e => console.warn(e)) + const result = await evalExprAsString(expr, actions).catch((e: any) => + console.warn(e), + ) return [key, result ?? ''] as const }), ) diff --git a/src/eval.ts b/src/eval.ts index 01d29c6..26b3df0 100644 --- a/src/eval.ts +++ b/src/eval.ts @@ -1,5 +1,5 @@ import { CSSUnit, Expr } from './parser' -import { match, matchString } from './utils/adt' +import { Enum, constructors, match, matchString } from './utils/adt' export interface EvalActions { addClass(id: string, classes: string): Promise<void> @@ -34,12 +34,27 @@ export interface EvalActions { callMethod( id: string | undefined, method: string, - args: EvalValue[], + args: (string | undefined)[], ): Promise<void> // calculate ?? } -type EvalValue = string | undefined | void +export type EvalValue = Enum<{ + String: string + Number: number + Boolean: boolean + Lazy: Expr[] + Void: never + VarIdentifier: string + // Object: Record<any, any> +}> +export const EvalValue = constructors<EvalValue>() + +export const evalExprAsString = async ( + expr: Expr, + actions: EvalActions, +): Promise<string | undefined> => + evalValueToString(await evalExpr(expr, actions)) export const evalExpr = async ( expr: Expr, @@ -47,64 +62,104 @@ export const evalExpr = async ( ): Promise<EvalValue> => match<Promise<EvalValue>, Expr>(expr, { Call: async ({ name, args }) => getFunctions(name, args, actions), - LiteralString: async s => s, + LiteralString: async s => EvalValue.String(s), LiteralNumber: async ({ value, unit }) => - matchString<number, CSSUnit>(unit, { - s: () => value * 1000, - _: () => value, - }).toString(), - Identifier: async s => s, - VarIdentifier: async s => s, - _: async _ => undefined, + EvalValue.Number( + matchString<number, CSSUnit>(unit, { + s: () => value * 1000, + _: () => value, + }), + ), + Identifier: async s => EvalValue.String(s), + VarIdentifier: async s => EvalValue.VarIdentifier(s), + _: async _ => EvalValue.Void(), + }) + +const evalValueToString = (val: EvalValue): string | undefined => + match<string | undefined, EvalValue>(val, { + String: s => s.replace(/(^'|")|('|"$)/g, ''), + Boolean: b => `${b}`, + Number: n => `${n}`, + _: () => undefined, + }) + +const evalValueToNumber = (val: EvalValue): number | undefined => + match<number | undefined, EvalValue>(val, { + String: s => parseInt(s, 10), + Boolean: b => (b ? 1 : 0), + Number: n => n, + _: () => undefined, }) -const getFunctions = (name: string, args: Expr[], actions: EvalActions) => { +const evalValueToBoolean = (val: EvalValue): boolean => + match<boolean, EvalValue>(val, { + String: s => !['false', '', '0'].includes(s.replace(/(^'|")|('|"$)/g, '')), + Boolean: b => b, + Number: n => !!n, + _: () => false, + }) + +const getFunctions = ( + name: string, + args: Expr[], + actions: EvalActions, +): Promise<EvalValue> => { const getVariable = async () => { const varName = await evalExpr(args[0], actions) const defaultValue = args[1] && (await evalExpr(args[1], actions)) - return varName && (actions.getVariable(varName) ?? defaultValue) + + return match<Promise<EvalValue>, EvalValue>(varName, { + VarIdentifier: async name => { + const value = await actions.getVariable(name) + return value === undefined ? defaultValue : EvalValue.String(value) + }, + _: async () => EvalValue.Void(), + }) } return matchString<Promise<EvalValue>>(name, { 'add-class': async () => { - const id = await evalExpr(args[0], actions) - const classes = await evalExpr(args[1], actions) + const id = evalValueToString(await evalExpr(args[0], actions)) + const classes = evalValueToString(await evalExpr(args[1], actions)) if (id && classes) { await actions.addClass(id, classes) } + return EvalValue.Void() }, 'remove-class': async () => { - const id = await evalExpr(args[0], actions) - const classes = await evalExpr(args[1], actions) + const id = evalValueToString(await evalExpr(args[0], actions)) + const classes = evalValueToString(await evalExpr(args[1], actions)) if (id && classes) { await actions.removeClass(id, classes) } + return EvalValue.Void() }, if: async () => { - const cond = await evalExpr(args[0], actions) - const FALSEY = ['0', 'false'] - if (cond && !FALSEY.includes(cond.replace(/(^'|")|('|"$)/g, ''))) { + const cond = evalValueToBoolean(await evalExpr(args[0], actions)) + if (cond) { return evalExpr(args[1], actions) } else { return evalExpr(args[2], actions) } }, delay: async () => { - const num = await evalExpr(args[0], actions) - num && (await actions.delay(parseInt(num, 10))) + const num = evalValueToNumber(await evalExpr(args[0], actions)) + num !== undefined ? await actions.delay(num) : undefined + return EvalValue.Void() }, 'js-eval': async () => { - const js = await evalExpr(args[0], actions) + const js = evalValueToString(await evalExpr(args[0], actions)) return js && (await actions.jsEval(js)) }, 'load-cssx': async () => { - const id = await evalExpr(args[0], actions) - const url = await evalExpr(args[1], actions) + const id = evalValueToString(await evalExpr(args[0], actions)) + const url = evalValueToString(await evalExpr(args[1], actions)) if (id && url) { await actions.loadCssx(id, url) } + return EvalValue.Void() }, var: getVariable, @@ -113,59 +168,81 @@ const getFunctions = (name: string, args: Expr[], actions: EvalActions) => { update: async () => { const [id, name, value] = args.length >= 3 - ? await evalArgs(args, 3, actions) - : [undefined, ...(await evalArgs(args, 2, actions))] + ? (await evalArgs(args, 3, actions)).map(evalValueToString) + : [ + undefined, + ...(await evalArgs(args, 2, actions)).map(evalValueToString), + ] if (name) { - actions.updateVariable(id ?? undefined, name, value ?? '') + await actions.updateVariable(id ?? undefined, name, value ?? '') } + return EvalValue.Void() }, 'set-attr': async () => { const [id, name, value] = args.length >= 3 - ? await evalArgs(args, 3, actions) - : [undefined, ...(await evalArgs(args, 2, actions))] + ? (await evalArgs(args, 3, actions)).map(evalValueToString) + : [ + undefined, + ...(await evalArgs(args, 2, actions)).map(evalValueToString), + ] if (name) { actions.setAttribute(id ?? undefined, name, value ?? '') } + return EvalValue.Void() }, attr: async () => { const [id, name] = args.length >= 2 - ? await evalArgs(args, 2, actions) - : [undefined, await evalExpr(args[0], actions)] + ? (await evalArgs(args, 2, actions)).map(evalValueToString) + : [undefined, evalValueToString(await evalExpr(args[0], actions))] if (name) { - return actions.getAttribute(id as string | undefined, name) + const val = await actions.getAttribute(id as string | undefined, name) + return val === undefined ? EvalValue.Void() : EvalValue.String(val) } + return EvalValue.Void() }, - 'prevent-default': async () => actions.withEvent(e => e.preventDefault()), + 'prevent-default': async () => { + await actions.withEvent(e => e.preventDefault()) + return EvalValue.Void() + }, request: async () => { - const url = await evalExpr(args[0], actions) - const method = (args[1] && (await evalExpr(args[1], actions))) ?? 'post' + const url = evalValueToString(await evalExpr(args[0], actions)) + const method = + (args[1] && evalValueToString(await evalExpr(args[1], actions))) || + 'post' if (url) { const data = await actions.getFormData() await actions.sendRequest({ method, url, data }) } + return EvalValue.Void() }, 'add-children': async () => { - const id = await evalExpr(args[0], actions) - if (id) actions.addChildren(id, args.slice(1)) + const id = evalValueToString(await evalExpr(args[0], actions)) + if (id) await actions.addChildren(id, args.slice(1)) + return EvalValue.Void() }, - 'remove-element': async () => - actions.removeElement( - (args[0] && (await evalExpr(args[0], actions))) ?? undefined, - ), - call: async () => { - const [id, method, ...methodArgs] = await Promise.all( - args.map(a => evalExpr(a, actions)), - ) + 'remove-element': async () => { + const selector = + (args[0] && evalValueToString(await evalExpr(args[0], actions))) ?? + undefined + if (selector) await actions.removeElement(selector) + return EvalValue.Void() + }, + + 'call-method': async () => { + const [id, method, ...methodArgs] = ( + await Promise.all(args.map(a => evalExpr(a, actions))) + ).map(evalValueToString) if (id && method) { - actions.callMethod(id, method, methodArgs) + await actions.callMethod(id, method, methodArgs) } + return EvalValue.Void() }, _: () => Promise.reject(new Error('not supposed to be here')), diff --git a/src/index.ts b/src/index.ts index a448924..52f3a1b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { EvalActions, evalExpr } from './eval' +import { EvalActions, evalExpr, evalExprAsString } from './eval' import { extractDeclaration, DeclarationEval, @@ -274,7 +274,7 @@ export const manageElement = async ( try { const exprs = parse(text) $element.textContent = - (exprs[0] ? await evalExpr(exprs[0], actions) : text) ?? text + (exprs[0] ? await evalExprAsString(exprs[0], actions) : text) ?? text } catch (e) { $element.textContent = text } diff --git a/src/utils/adt.ts b/src/utils/adt.ts index 45d9e50..3ae387c 100644 --- a/src/utils/adt.ts +++ b/src/utils/adt.ts @@ -7,6 +7,28 @@ export const match = <R, T extends Tag<string, any>>( }, ): R => ((pattern as any)[tag.tag] || (pattern._ as any))(tag.value) +// type TagValues< +// T extends Tag<any, string>, +// Keys extends Array<string>, +// Values extends Array<any> = [], +// > = Keys extends [] +// ? Values +// : Keys extends [ +// infer key extends string, +// ...infer restOfKeys extends string[], +// ] +// ? TagValues<T, restOfKeys, [...Values, TagValue<T, key>]> +// : never +// +// export const ifLet = <T extends Tag<string, any>, Keys extends Array<T['tag']>>( +// tag: T, +// kinds: Keys, +// cb: (...values: TagValues<T, Keys>) => void, +// ): void => { +// const values = kinds.map(k => (tag.tag === k ? tag.value : undefined)) +// ;(cb as any)(...values) +// } + export const matchString = <R, T extends string = string>( key: T, pattern: { diff --git a/tests/eval.spec.ts b/tests/eval.spec.ts index 1d8b161..660f0b9 100644 --- a/tests/eval.spec.ts +++ b/tests/eval.spec.ts @@ -11,8 +11,19 @@ describe('eval', () => { getVariable: jest.fn(), updateVariable: jest.fn(), setAttribute: jest.fn(), + getAttribute: jest.fn(), + withEvent: jest.fn(), + getFormData: jest.fn(), + sendRequest: jest.fn(), + addChildren: jest.fn(), + removeElement: jest.fn(), + callMethod: jest.fn(), } + fit('should do stuff', () => { + console.log('yo') + }) + it('should add classes', async () => { await evalExpr( Expr.Call({ |
