summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO.norg43
-rw-r--r--examples/todo-list/style.css3
-rw-r--r--src/declarations.ts6
-rw-r--r--src/eval.ts171
-rw-r--r--src/index.ts4
-rw-r--r--src/utils/adt.ts22
-rw-r--r--tests/eval.spec.ts11
7 files changed, 189 insertions, 71 deletions
diff --git a/TODO.norg b/TODO.norg
index b3eca07..cc4bcb0 100644
--- a/TODO.norg
+++ b/TODO.norg
@@ -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({