tui-lipan-macro 0.1.0

Procedural macros for the tui-lipan TUI framework (ui!, rsx!, mockup!).
Documentation
use syn::parse::discouraged::Speculative;
use syn::parse::{Parse, ParseStream};
use syn::{Expr, Ident, Pat, Path, Result, Token, braced};

#[derive(Clone)]
pub(crate) struct RsxInput {
    pub(crate) root: NodeOrExpr,
}

impl Parse for RsxInput {
    fn parse(input: ParseStream) -> Result<Self> {
        let root = input.parse()?;
        if !input.is_empty() {
            return Err(input.error("unexpected tokens after rsx root"));
        }
        Ok(Self { root })
    }
}

#[derive(Clone)]
pub(crate) enum NodeOrExpr {
    Node(Box<Node>),
    Expr(Box<Expr>),
}

impl Parse for NodeOrExpr {
    fn parse(input: ParseStream) -> Result<Self> {
        if looks_like_node(input) {
            Ok(Self::Node(Box::new(input.parse()?)))
        } else {
            Ok(Self::Expr(Box::new(input.parse()?)))
        }
    }
}

#[derive(Clone)]
pub(crate) struct Node {
    pub(crate) path: Path,
    pub(crate) entries: Vec<Entry>,
}

#[derive(Clone)]
pub(crate) enum Entry {
    Prop(Ident, Box<NodeOrExpr>),
    Child(Box<Child>),
}

#[derive(Clone)]
pub(crate) enum Child {
    Node(Box<Node>),
    Expr(Box<Expr>),
    For(Box<ForChild>),
    If(Box<IfChild>),
}

#[derive(Clone)]
pub(crate) struct ForChild {
    pub(crate) pat: Pat,
    pub(crate) expr: Expr,
    pub(crate) body: Vec<Child>,
}

#[derive(Clone)]
pub(crate) struct IfChild {
    pub(crate) cond: Expr,
    pub(crate) then_body: Vec<Child>,
    pub(crate) else_body: Option<Vec<Child>>,
}

impl Parse for Node {
    fn parse(input: ParseStream) -> Result<Self> {
        let path: Path = input.parse()?;

        let content;
        braced!(content in input);

        let entries = parse_entries(&content)?;
        Ok(Self { path, entries })
    }
}

impl Parse for Child {
    fn parse(input: ParseStream) -> Result<Self> {
        if input.peek(Token![for]) {
            Ok(Self::For(Box::new(input.parse()?)))
        } else if input.peek(Token![if]) {
            Ok(Self::If(Box::new(input.parse()?)))
        } else if looks_like_node(input) {
            Ok(Self::Node(Box::new(input.parse()?)))
        } else {
            Ok(Self::Expr(Box::new(input.parse()?)))
        }
    }
}

impl Parse for ForChild {
    fn parse(input: ParseStream) -> Result<Self> {
        input.parse::<Token![for]>()?;
        let pat: Pat = input.call(Pat::parse_single)?;
        input.parse::<Token![in]>()?;
        let expr = input.call(Expr::parse_without_eager_brace)?;

        let content;
        braced!(content in input);
        let body = parse_children(&content)?;

        Ok(Self { pat, expr, body })
    }
}

impl Parse for IfChild {
    fn parse(input: ParseStream) -> Result<Self> {
        input.parse::<Token![if]>()?;
        let cond = input.call(Expr::parse_without_eager_brace)?;

        let then_content;
        braced!(then_content in input);
        let then_body = parse_children(&then_content)?;

        let else_body = if input.peek(Token![else]) {
            input.parse::<Token![else]>()?;

            if input.peek(Token![if]) {
                let inner_if: IfChild = input.parse()?;
                Some(vec![Child::If(Box::new(inner_if))])
            } else {
                let else_content;
                braced!(else_content in input);
                Some(parse_children(&else_content)?)
            }
        } else {
            None
        };

        Ok(Self {
            cond,
            then_body,
            else_body,
        })
    }
}

fn looks_like_prop(input: ParseStream<'_>) -> bool {
    if !input.peek(Ident) {
        return false;
    }

    let fork = input.fork();
    if fork.parse::<Ident>().is_err() {
        return false;
    }

    if fork.peek(Token![::]) {
        return false;
    }

    fork.peek(Token![:])
}

fn parse_entries(input: ParseStream) -> Result<Vec<Entry>> {
    let mut entries = Vec::new();

    while !input.is_empty() {
        if looks_like_prop(input) {
            let name: Ident = input.parse()?;
            let name_str = name.to_string();

            input.parse::<Token![:]>()?;

            if name_str == "child" || name_str == "children" {
                let _value = parse_prop_value(input)?;
                return Err(syn::Error::new(
                    name.span(),
                    format!("don't use `{}:` in rsx! - add children directly", name_str),
                ));
            }

            let value = parse_prop_value(input)?;
            entries.push(Entry::Prop(name, Box::new(value)));
        } else {
            let child: Child = input.parse()?;
            entries.push(Entry::Child(Box::new(child)));
        }

        if !input.is_empty() {
            if !input.peek(Token![,]) && !input.peek(Token![;]) {
                return Err(syn::Error::new(
                    input.span(),
                    "expected `,` or `;` between entries in rsx! macro",
                ));
            }
            let _ = input.parse::<Option<Token![,]>>()?;
            let _ = input.parse::<Option<Token![;]>>()?;
        }
    }

    Ok(entries)
}

fn parse_prop_value(input: ParseStream) -> Result<NodeOrExpr> {
    let fork = input.fork();
    if let Ok(expr) = fork.parse::<Expr>() {
        input.advance_to(&fork);
        return Ok(NodeOrExpr::Expr(Box::new(expr)));
    }

    if looks_like_node(input) {
        return Ok(NodeOrExpr::Node(Box::new(input.parse()?)));
    }

    Err(input.error("expected property value"))
}

fn parse_children(input: ParseStream) -> Result<Vec<Child>> {
    let mut out = Vec::new();

    while !input.is_empty() {
        out.push(input.parse()?);

        if !input.is_empty() {
            if !input.peek(Token![,]) && !input.peek(Token![;]) {
                return Err(syn::Error::new(
                    input.span(),
                    "expected `,` or `;` between children in rsx! macro",
                ));
            }
            let _ = input.parse::<Option<Token![,]>>()?;
            let _ = input.parse::<Option<Token![;]>>()?;
        }
    }

    Ok(out)
}

fn looks_like_node(input: ParseStream<'_>) -> bool {
    let fork = input.fork();
    if fork.parse::<Path>().is_err() {
        return false;
    }
    fork.peek(syn::token::Brace)
}