use std::{cell::RefCell, collections::HashMap};
use js_sys::Reflect;
use wasm_bindgen::{JsCast, JsValue};
pub use pocopine_expr::{parse, BinOp, Expr, Literal, ParseError, Span, Spanned};
#[doc(hidden)]
#[derive(Clone, Copy, Debug)]
pub enum StaticLiteral {
Null,
Bool(bool),
Number(f64),
String(&'static str),
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug)]
pub enum StaticBinOp {
And,
Or,
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug)]
pub enum StaticExpr {
Literal(StaticLiteral),
Path(&'static [&'static str]),
Not(&'static StaticExpr),
BinOp {
op: StaticBinOp,
lhs: &'static StaticExpr,
rhs: &'static StaticExpr,
},
}
impl StaticExpr {
#[doc(hidden)]
pub fn evaluate(&'static self, scope: &JsValue) -> JsValue {
match self {
StaticExpr::Literal(lit) => static_lit_to_js(lit),
StaticExpr::Path(segments) => resolve_static_segments(scope, segments),
StaticExpr::Not(inner) => JsValue::from_bool(inner.evaluate(scope).is_falsy()),
StaticExpr::BinOp { op, lhs, rhs } => match op {
StaticBinOp::And => {
let l = lhs.evaluate(scope);
if l.is_falsy() {
l
} else {
rhs.evaluate(scope)
}
}
StaticBinOp::Or => {
let l = lhs.evaluate(scope);
if !l.is_falsy() {
l
} else {
rhs.evaluate(scope)
}
}
StaticBinOp::Eq | StaticBinOp::Ne => {
let l = lhs.evaluate(scope);
let r = rhs.evaluate(scope);
let eq = js_strict_eq(&l, &r);
JsValue::from_bool(if matches!(op, StaticBinOp::Eq) {
eq
} else {
!eq
})
}
StaticBinOp::Lt | StaticBinOp::Le | StaticBinOp::Gt | StaticBinOp::Ge => {
let l = lhs.evaluate(scope);
let r = rhs.evaluate(scope);
match (l.as_f64(), r.as_f64()) {
(Some(a), Some(b)) => JsValue::from_bool(match op {
StaticBinOp::Lt => a < b,
StaticBinOp::Le => a <= b,
StaticBinOp::Gt => a > b,
StaticBinOp::Ge => a >= b,
_ => unreachable!(),
}),
_ => JsValue::from_bool(false),
}
}
},
}
}
}
thread_local! {
static PARSE_CACHE: RefCell<HashMap<String, Result<Spanned<Expr>, ParseError>>> =
RefCell::new(HashMap::new());
}
pub fn parse_cached(src: &str) -> Result<Spanned<Expr>, ParseError> {
PARSE_CACHE.with(|cache| {
if let Some(hit) = cache.borrow().get(src).cloned() {
return hit;
}
let parsed = parse(src);
cache.borrow_mut().insert(src.to_string(), parsed.clone());
parsed
})
}
pub fn evaluate(expr: &Spanned<Expr>, scope: &JsValue) -> JsValue {
match &expr.value {
Expr::Literal(l) => lit_to_js(l),
Expr::Path(segs) => resolve_segments(scope, segs),
Expr::Not(inner) => JsValue::from_bool(evaluate(inner, scope).is_falsy()),
Expr::BinOp(op, lhs, rhs) => match op {
BinOp::And => {
let l = evaluate(lhs, scope);
if l.is_falsy() {
l
} else {
evaluate(rhs, scope)
}
}
BinOp::Or => {
let l = evaluate(lhs, scope);
if !l.is_falsy() {
l
} else {
evaluate(rhs, scope)
}
}
BinOp::Eq | BinOp::Ne => {
let l = evaluate(lhs, scope);
let r = evaluate(rhs, scope);
let eq = js_strict_eq(&l, &r);
let out = if matches!(op, BinOp::Eq) { eq } else { !eq };
JsValue::from_bool(out)
}
BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
let l = evaluate(lhs, scope);
let r = evaluate(rhs, scope);
match (l.as_f64(), r.as_f64()) {
(Some(a), Some(b)) => JsValue::from_bool(match op {
BinOp::Lt => a < b,
BinOp::Le => a <= b,
BinOp::Gt => a > b,
BinOp::Ge => a >= b,
_ => unreachable!(),
}),
_ => JsValue::from_bool(false),
}
}
BinOp::Plus => {
let l = evaluate(lhs, scope);
let r = evaluate(rhs, scope);
if l.as_string().is_some() || r.as_string().is_some() {
let ls = js_to_string(&l);
let rs = js_to_string(&r);
JsValue::from_str(&format!("{ls}{rs}"))
} else if let (Some(a), Some(b)) = (l.as_f64(), r.as_f64()) {
JsValue::from_f64(a + b)
} else {
JsValue::from_str("")
}
}
},
Expr::Ternary(cond, then_e, else_e) => {
if !evaluate(cond, scope).is_falsy() {
evaluate(then_e, scope)
} else {
evaluate(else_e, scope)
}
}
Expr::Call(name, args) => {
let arr = js_sys::Array::new();
for a in args {
arr.push(&evaluate(a, scope));
}
if name.starts_with('$') {
if let Some(id) = scope_id_for(scope) {
let fn_val = crate::magics::resolve(name, id);
if let Some(f) = fn_val.dyn_ref::<js_sys::Function>() {
return f
.apply(&JsValue::UNDEFINED, &arr)
.unwrap_or(JsValue::UNDEFINED);
}
}
return JsValue::UNDEFINED;
}
match scope_id_for(scope) {
Some(id) => crate::scope::invoke_handler(id, name, &arr),
None => JsValue::UNDEFINED,
}
}
Expr::Assign(path, rhs) => {
let v = evaluate(rhs, scope);
write_assign_path(scope, path, &v);
v
}
Expr::Seq(stmts) => {
let mut last = JsValue::UNDEFINED;
for s in stmts {
last = evaluate(s, scope);
}
last
}
}
}
fn scope_id_for(_proxy: &JsValue) -> Option<crate::reactive::ScopeId> {
crate::scope::current_scope_id()
}
fn write_assign_path(proxy: &JsValue, segments: &[String], value: &JsValue) {
if segments.is_empty() {
return;
}
if segments.len() == 1 {
let _ = Reflect::set(proxy, &JsValue::from_str(&segments[0]), value);
return;
}
let mut cur = proxy.clone();
for seg in &segments[..segments.len() - 1] {
cur = Reflect::get(&cur, &JsValue::from_str(seg)).unwrap_or(JsValue::UNDEFINED);
if !cur.is_object() {
return;
}
}
let last = &segments[segments.len() - 1];
let _ = Reflect::set(&cur, &JsValue::from_str(last), value);
}
fn lit_to_js(l: &Literal) -> JsValue {
match l {
Literal::Null => JsValue::NULL,
Literal::Bool(b) => JsValue::from_bool(*b),
Literal::Number(n) => JsValue::from_f64(*n),
Literal::String(s) => JsValue::from_str(s),
}
}
fn static_lit_to_js(l: &StaticLiteral) -> JsValue {
match l {
StaticLiteral::Null => JsValue::NULL,
StaticLiteral::Bool(b) => JsValue::from_bool(*b),
StaticLiteral::Number(n) => JsValue::from_f64(*n),
StaticLiteral::String(s) => JsValue::from_str(s),
}
}
fn resolve_segments(root: &JsValue, segments: &[String]) -> JsValue {
let mut cur = root.clone();
for seg in segments {
cur = Reflect::get(&cur, &JsValue::from_str(seg)).unwrap_or(JsValue::UNDEFINED);
}
cur
}
fn resolve_static_segments(root: &JsValue, segments: &[&'static str]) -> JsValue {
let mut cur = root.clone();
for seg in segments {
cur = Reflect::get(&cur, &JsValue::from_str(seg)).unwrap_or(JsValue::UNDEFINED);
}
cur
}
fn js_to_string(v: &JsValue) -> String {
if let Some(s) = v.as_string() {
return s;
}
if let Some(n) = v.as_f64() {
if n.fract() == 0.0 && n.is_finite() {
return format!("{}", n as i64);
}
return n.to_string();
}
if let Some(b) = v.as_bool() {
return if b { "true".into() } else { "false".into() };
}
if v.is_null() {
return "null".into();
}
if v.is_undefined() {
return "undefined".into();
}
"[object Object]".into()
}
fn js_strict_eq(a: &JsValue, b: &JsValue) -> bool {
if let (Some(as_), Some(bs_)) = (a.as_string(), b.as_string()) {
return as_ == bs_;
}
if let (Some(an), Some(bn)) = (a.as_f64(), b.as_f64()) {
return an == bn;
}
if let (Some(ab), Some(bb)) = (a.as_bool(), b.as_bool()) {
return ab == bb;
}
if a.is_null() && b.is_null() {
return true;
}
if a.is_undefined() && b.is_undefined() {
return true;
}
js_sys::Object::is(a, b)
}
pub fn evaluate_truthy(expr: &Spanned<Expr>, scope: &JsValue) -> bool {
!evaluate(expr, scope).is_falsy()
}