beuvy 0.1.0

Facade crate for beuvy-runtime plus optional declarative UI authoring.
Documentation
use super::{expr_binding_path, parse_runtime_number};
use crate::ast::{
    DeclarativeBinaryOp, DeclarativeLiteral, DeclarativeNumber, DeclarativeRuntimeExpr,
};
use crate::parser::{DeclarativeUiAssetLoadError, attr_error};
use roxmltree::Node as XmlNode;

pub(crate) struct RuntimeExprParser<'a, 'input> {
    pub(crate) node: XmlNode<'a, 'input>,
    pub(crate) attr_name: &'a str,
    pub(crate) raw: &'a str,
    pub(crate) chars: Vec<char>,
    pub(crate) index: usize,
}

impl<'a, 'input> RuntimeExprParser<'a, 'input> {
    pub(crate) fn new(node: XmlNode<'a, 'input>, attr_name: &'a str, raw: &'a str) -> Self {
        Self {
            node,
            attr_name,
            raw: raw.trim(),
            chars: raw.trim().chars().collect(),
            index: 0,
        }
    }

    pub(crate) fn parse_expression(
        &mut self,
    ) -> Result<DeclarativeRuntimeExpr, DeclarativeUiAssetLoadError> {
        self.parse_conditional()
    }

    fn parse_conditional(&mut self) -> Result<DeclarativeRuntimeExpr, DeclarativeUiAssetLoadError> {
        let condition = self.parse_equality()?;
        self.skip_ws();
        if !self.consume_char('?') {
            return Ok(condition);
        }
        let then_expr = self.parse_expression()?;
        self.skip_ws();
        self.expect_char(':', "expected `:` in conditional expression")?;
        let else_expr = self.parse_expression()?;
        Ok(DeclarativeRuntimeExpr::Conditional {
            condition: Box::new(condition),
            then_expr: Box::new(then_expr),
            else_expr: Box::new(else_expr),
        })
    }

    fn parse_equality(&mut self) -> Result<DeclarativeRuntimeExpr, DeclarativeUiAssetLoadError> {
        self.parse_binary_chain(
            Self::parse_comparison,
            &[
                ("===", DeclarativeBinaryOp::Equal),
                ("!==", DeclarativeBinaryOp::NotEqual),
                ("==", DeclarativeBinaryOp::Equal),
                ("!=", DeclarativeBinaryOp::NotEqual),
            ],
        )
    }

    fn parse_comparison(&mut self) -> Result<DeclarativeRuntimeExpr, DeclarativeUiAssetLoadError> {
        self.parse_binary_chain(
            Self::parse_additive,
            &[
                ("<=", DeclarativeBinaryOp::LessThanOrEqual),
                (">=", DeclarativeBinaryOp::GreaterThanOrEqual),
                ("<", DeclarativeBinaryOp::LessThan),
                (">", DeclarativeBinaryOp::GreaterThan),
            ],
        )
    }

    fn parse_additive(&mut self) -> Result<DeclarativeRuntimeExpr, DeclarativeUiAssetLoadError> {
        self.parse_binary_chain(
            Self::parse_multiplicative,
            &[
                ("+", DeclarativeBinaryOp::Add),
                ("-", DeclarativeBinaryOp::Subtract),
            ],
        )
    }

    fn parse_multiplicative(
        &mut self,
    ) -> Result<DeclarativeRuntimeExpr, DeclarativeUiAssetLoadError> {
        self.parse_binary_chain(
            Self::parse_unary,
            &[
                ("*", DeclarativeBinaryOp::Multiply),
                ("/", DeclarativeBinaryOp::Divide),
            ],
        )
    }

    fn parse_binary_chain(
        &mut self,
        subparser: fn(&mut Self) -> Result<DeclarativeRuntimeExpr, DeclarativeUiAssetLoadError>,
        operators: &[(&str, DeclarativeBinaryOp)],
    ) -> Result<DeclarativeRuntimeExpr, DeclarativeUiAssetLoadError> {
        let mut expr = subparser(self)?;
        loop {
            self.skip_ws();
            let Some((token, op)) = operators
                .iter()
                .find(|(token, _)| self.starts_with(token))
                .copied()
            else {
                break;
            };
            self.index += token.len();
            let right = subparser(self)?;
            expr = DeclarativeRuntimeExpr::Binary {
                left: Box::new(expr),
                op,
                right: Box::new(right),
            };
        }
        Ok(expr)
    }

    fn parse_unary(&mut self) -> Result<DeclarativeRuntimeExpr, DeclarativeUiAssetLoadError> {
        self.skip_ws();
        if self.consume_char('!') {
            return Ok(DeclarativeRuntimeExpr::UnaryNot {
                expr: Box::new(self.parse_unary()?),
            });
        }
        self.parse_postfix()
    }

    fn parse_postfix(&mut self) -> Result<DeclarativeRuntimeExpr, DeclarativeUiAssetLoadError> {
        let mut expr = self.parse_primary()?;
        loop {
            self.skip_ws();
            if self.consume_char('.') {
                let field = self.parse_identifier()?;
                self.skip_ws();
                if field == "getBoundingClientRect" && self.consume_char('(') {
                    self.skip_ws();
                    self.expect_char(')', "getBoundingClientRect() does not take arguments")?;
                    let Some(target_path) = expr_binding_path(&expr) else {
                        return Err(attr_error(
                            self.node,
                            self.attr_name,
                            self.raw,
                            "getBoundingClientRect() receiver must be a binding path",
                        ));
                    };
                    expr = DeclarativeRuntimeExpr::GetBoundingClientRect {
                        target_path: target_path.to_string(),
                    };
                    continue;
                }
                expr = DeclarativeRuntimeExpr::FieldAccess {
                    base: Box::new(expr),
                    field,
                };
                continue;
            }
            if self.consume_char('(') {
                let args = self.parse_call_args()?;
                expr = self.parse_supported_call(expr, args)?;
                continue;
            }
            break;
        }
        Ok(expr)
    }

    fn parse_supported_call(
        &self,
        callee: DeclarativeRuntimeExpr,
        args: Vec<DeclarativeRuntimeExpr>,
    ) -> Result<DeclarativeRuntimeExpr, DeclarativeUiAssetLoadError> {
        match callee {
            DeclarativeRuntimeExpr::BindingPath(name) if name == "anchorPopup" => {
                if args.len() != 6 {
                    return Err(attr_error(
                        self.node,
                        self.attr_name,
                        self.raw,
                        "expected 6 arguments for anchorPopup",
                    ));
                }
                let mut args = args.into_iter();
                Ok(DeclarativeRuntimeExpr::AnchorPopup {
                    anchor_rect: Box::new(args.next().unwrap()),
                    shell_rect: Box::new(args.next().unwrap()),
                    popup_width: Box::new(args.next().unwrap()),
                    popup_min_height: Box::new(args.next().unwrap()),
                    gap: Box::new(args.next().unwrap()),
                    margin: Box::new(args.next().unwrap()),
                })
            }
            DeclarativeRuntimeExpr::FieldAccess { base, field } => match (*base, field.as_str()) {
                (DeclarativeRuntimeExpr::BindingPath(name), "min") if name == "Math" => {
                    Ok(DeclarativeRuntimeExpr::MathMin { args })
                }
                (DeclarativeRuntimeExpr::BindingPath(name), "max") if name == "Math" => {
                    Ok(DeclarativeRuntimeExpr::MathMax { args })
                }
                _ => Err(attr_error(
                    self.node,
                    self.attr_name,
                    self.raw,
                    "unsupported runtime method or function in expression",
                )),
            },
            _ => Err(attr_error(
                self.node,
                self.attr_name,
                self.raw,
                "unsupported runtime method or function in expression",
            )),
        }
    }

    fn parse_call_args(
        &mut self,
    ) -> Result<Vec<DeclarativeRuntimeExpr>, DeclarativeUiAssetLoadError> {
        let mut args = Vec::new();
        self.skip_ws();
        if self.consume_char(')') {
            return Ok(args);
        }
        loop {
            args.push(self.parse_expression()?);
            self.skip_ws();
            if self.consume_char(')') {
                return Ok(args);
            }
            self.expect_char(',', "expected `,` or `)` in function call")?;
        }
    }

    fn parse_primary(&mut self) -> Result<DeclarativeRuntimeExpr, DeclarativeUiAssetLoadError> {
        self.skip_ws();
        if self.consume_char('(') {
            let expr = self.parse_expression()?;
            self.skip_ws();
            self.expect_char(')', "expected `)`")?;
            return Ok(expr);
        }
        if self.peek_char() == Some('[') {
            return self.parse_array_literal();
        }
        if self.peek_char() == Some('{') {
            return self.parse_object_literal();
        }
        if let Some(string) = self.parse_string_literal()? {
            return Ok(DeclarativeRuntimeExpr::Literal(DeclarativeLiteral::String(
                string,
            )));
        }
        if let Some(number) = self.parse_number_literal()? {
            return Ok(DeclarativeRuntimeExpr::NumberLiteral(number));
        }
        if self.starts_with("true") && self.word_boundary_after("true") {
            self.index += 4;
            return Ok(DeclarativeRuntimeExpr::Literal(DeclarativeLiteral::Bool(
                true,
            )));
        }
        if self.starts_with("false") && self.word_boundary_after("false") {
            self.index += 5;
            return Ok(DeclarativeRuntimeExpr::Literal(DeclarativeLiteral::Bool(
                false,
            )));
        }
        let identifier = self.parse_identifier()?;
        Ok(DeclarativeRuntimeExpr::BindingPath(identifier))
    }

    fn parse_object_literal(
        &mut self,
    ) -> Result<DeclarativeRuntimeExpr, DeclarativeUiAssetLoadError> {
        self.expect_char('{', "expected `{`")?;
        let mut fields = Vec::new();
        loop {
            self.skip_ws();
            if self.consume_char('}') {
                break;
            }
            let key = if let Some(string) = self.parse_string_literal()? {
                string
            } else {
                self.parse_identifier()?
            };
            self.skip_ws();
            let value = if self.consume_char(':') {
                self.parse_expression()?
            } else {
                DeclarativeRuntimeExpr::BindingPath(key.clone())
            };
            fields.push((key, value));
            self.skip_ws();
            if self.consume_char('}') {
                break;
            }
            self.expect_char(',', "expected `,` or `}` in object literal")?;
        }
        Ok(DeclarativeRuntimeExpr::ObjectLiteral(fields))
    }

    fn parse_array_literal(
        &mut self,
    ) -> Result<DeclarativeRuntimeExpr, DeclarativeUiAssetLoadError> {
        self.expect_char('[', "expected `[`")?;
        let mut items = Vec::new();
        loop {
            self.skip_ws();
            if self.consume_char(']') {
                break;
            }
            items.push(self.parse_expression()?);
            self.skip_ws();
            if self.consume_char(']') {
                break;
            }
            self.expect_char(',', "expected `,` or `]` in array literal")?;
        }
        Ok(DeclarativeRuntimeExpr::ArrayLiteral(items))
    }

    pub(crate) fn parse_string_literal(
        &mut self,
    ) -> Result<Option<String>, DeclarativeUiAssetLoadError> {
        self.skip_ws();
        let Some(quote @ ('\'' | '"')) = self.peek_char() else {
            return Ok(None);
        };
        self.index += 1;
        let mut value = String::new();
        while let Some(ch) = self.peek_char() {
            self.index += 1;
            if ch == quote {
                return Ok(Some(value));
            }
            if ch == '\\' {
                let Some(escaped) = self.peek_char() else {
                    break;
                };
                self.index += 1;
                value.push(escaped);
                continue;
            }
            value.push(ch);
        }
        Err(attr_error(
            self.node,
            self.attr_name,
            self.raw,
            "unterminated string literal",
        ))
    }

    fn parse_number_literal(
        &mut self,
    ) -> Result<Option<DeclarativeNumber>, DeclarativeUiAssetLoadError> {
        self.skip_ws();
        let start = self.index;
        let mut seen_digit = false;
        let mut seen_dot = false;
        while let Some(ch) = self.peek_char() {
            if ch.is_ascii_digit() {
                seen_digit = true;
                self.index += 1;
                continue;
            }
            if ch == '.' && !seen_dot {
                seen_dot = true;
                self.index += 1;
                continue;
            }
            break;
        }
        if !seen_digit {
            self.index = start;
            return Ok(None);
        }
        let raw = self.slice(start, self.index);
        parse_runtime_number(raw)
            .map(Some)
            .map_err(|_| attr_error(self.node, self.attr_name, raw, "invalid number literal"))
    }
}