use yog_ids::yui_vm as v;
use super::expr::{Ex, Scope, parse_expr};
use crate::yui::{Element, err};
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) enum Mode {
OneWay,
TwoWay,
}
#[derive(Debug, Clone)]
pub(super) enum Bound {
Expr(Ex),
Rust(String),
}
#[derive(Debug, Clone)]
pub(super) struct Binding {
pub(super) raw: String,
pub(super) bound: Bound,
pub(super) mode: Mode,
pub(super) bind_back: Option<String>,
pub(super) fallback: Option<String>,
}
impl Binding {
pub(super) fn one_way(raw: &str, bound: Bound) -> Binding {
Binding {
raw: raw.to_string(),
bound,
mode: Mode::OneWay,
bind_back: None,
fallback: None,
}
}
pub(super) fn expr(&self) -> Option<&Ex> {
match &self.bound {
Bound::Expr(e) => Some(e),
Bound::Rust(_) => None,
}
}
pub(super) fn has_opt(&self) -> bool {
self.expr().is_some_and(Ex::has_opt)
}
pub(super) fn rust(&self, scope: Scope, el: &Element) -> Result<String, String> {
match &self.bound {
Bound::Rust(r) => Ok(r.clone()),
Bound::Expr(e) => e
.emit(scope, false)
.map_err(|e| format!("line {}: {e}", el.line)),
}
}
pub(super) fn emit(&self, scope: Scope, el: &Element) -> Result<String, String> {
match self.expr() {
Some(e) => e
.emit(scope, false)
.map_err(|e| format!("line {}: {e}", el.line)),
None => err(el, format_args!("`{}` needs {{x:Bind ...}}", self.raw)),
}
}
pub(super) fn write_back(&self, arg: &str, value: &str) -> Result<WriteBack, String> {
Ok(match (&self.bind_back, self.mode, self.expr()) {
(Some(m), _, _) => WriteBack::Stmt(format!("vm.{m}({arg});")),
(None, Mode::TwoWay, Some(e)) if e.is_place() => {
WriteBack::Stmt(format!("{} = {value};", e.emit(Scope::Screen, false)?))
}
(None, Mode::TwoWay, _) => WriteBack::Nowhere,
_ => WriteBack::OneWay,
})
}
}
pub(super) enum WriteBack {
OneWay,
Stmt(String),
Nowhere,
}
fn split_top(s: &str) -> Vec<&str> {
let (mut depth, mut quote, mut start) = (0i32, false, 0);
let mut out = Vec::new();
for (i, c) in s.char_indices() {
match c {
'\'' => quote = !quote,
'(' if !quote => depth += 1,
')' if !quote => depth -= 1,
',' if !quote && depth == 0 => {
out.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
out.push(&s[start..]);
out
}
const OPTION_IS: char = '=';
pub(super) fn parse_binding(value: &str) -> Option<Result<Binding, String>> {
let inner = value
.trim()
.strip_prefix(yog_ids::yui_markup::BIND)?
.strip_suffix(yog_ids::yui_markup::BIND_CLOSE)?;
Some(read_binding(value, inner))
}
fn read_binding(value: &str, inner: &str) -> Result<Binding, String> {
let mut parts = split_top(inner).into_iter().map(str::trim);
let expr = parts
.next()
.filter(|p| !p.is_empty())
.ok_or("empty binding — write {x:Bind expression}")?;
let mut b = Binding::one_way(
value,
Bound::Expr(parse_expr(expr).map_err(|e| format!("{value}: {e}"))?),
);
for part in parts {
let Some((k, o)) = part.split_once(OPTION_IS) else {
return Err(format!(
"{value}: expected Mode=, BindBack= or Fallback=, got `{part}`"
));
};
let o = o.trim();
match k.trim() {
v::MODE => {
b.mode = match o {
v::ONE_WAY | v::ONE_TIME => Mode::OneWay,
v::TWO_WAY => Mode::TwoWay,
_ => return Err(format!("{value}: Mode is OneWay, OneTime or TwoWay")),
}
}
v::BIND_BACK => b.bind_back = Some(o.to_string()),
v::FALLBACK => b.fallback = Some(o.trim_matches('\'').to_string()),
other => return Err(format!("{value}: unknown binding option `{other}`")),
}
}
Ok(b)
}
pub(crate) fn compile_bind(value: &str) -> Option<Result<String, String>> {
Some(parse_binding(value)?.and_then(|b| match b.bound {
Bound::Expr(e) => e.emit(Scope::Screen, false),
Bound::Rust(r) => Ok(r),
}))
}