maomi-macro 0.1.0

A rust wasm framework for building pages with components (helper macros)
Documentation
use syn::*;
use syn::parse::*;
use syn::export::Span;

use super::template::*;
use super::template::Attribute as TemplateAttribute;

const SYSTEM_ATTRIBUTES: [&'static str; 3] = [
    "id",
    "class",
    "style",
];
const SYSTEM_EVENTS: [&'static str; 20] = [
    "click",
    "mouse_down",
    "mouse_move",
    "mouse_up",
    "touch_start",
    "touch_move",
    "touch_end",
    "touch_cancel",
    "tap",
    "long_tap",
    "cancel_tap",
    "key_down",
    "key_press",
    "key_up",
    "change",
    "submit",
    "animation_start",
    "animation_iteration",
    "animation_end",
    "transition_end",
];

fn ident_to_dashed_str(s: &Ident) -> LitStr {
    let s: String = s.to_string().chars().map(|c| {
        match c {
            '_' => '-',
            _ => c,
        }
    }).collect();
    LitStr::new(s.as_str(), Span::call_site())
}

fn ty_to_string(s: &Ident) -> String {
    let s: String = s.to_string().chars().map(|c| {
        if c >= 'A' && c <= 'Z' {
            format!("-{}", c.to_lowercase())
        } else {
            c.to_string()
        }
    }).collect();
    s
}

fn parse_children(input: ParseStream) -> Result<Vec<TemplateNode>> {
    let mut children = vec![];
    while !input.is_empty() {
        let lookahead = input.lookahead1();
        if lookahead.peek(Token![<]) {
            input.parse::<Token![<]>()?;
            let lookahead = input.lookahead1();
            if lookahead.peek(Token![/]) {
                input.parse::<Token![/]>()?;
                break;
            } else if lookahead.peek(Token![if]) {
                input.parse::<Token![if]>()?;
                children.push(parse_template_if(input)?);
            } else if lookahead.peek(Token![for]) {
                input.parse::<Token![for]>()?;
                children.push(parse_template_for(input)?);
            } else if lookahead.peek(Token![in]) {
                input.parse::<Token![in]>()?;
                children.push(parse_template_in(input)?);
            } else {
                children.push(parse_template_element_or_slot(input)?);
            }
        } else if lookahead.peek(token::Brace) || lookahead.peek(LitStr) {
            children.push(TemplateNode::TextNode(parse_template_value(&input)?));
        } else {
            return Err(lookahead.error());
        }
    }
    Ok(children)
}

fn parse_template_value(input: ParseStream) -> Result<TemplateValue> {
    let lookahead = input.lookahead1();
    if lookahead.peek(token::Brace) {
        let content;
        braced!(content in input);
        Ok(TemplateValue::from(content.parse::<Expr>()?))
    } else if lookahead.peek(LitStr) {
        let s = input.parse::<LitStr>()?;
        let expr: Expr = Expr::Lit(ExprLit {
            attrs: vec![],
            lit: Lit::Str(s),
        });
        Ok(TemplateValue::from(expr))
    } else {
        return Err(lookahead.error());
    }
}

fn parse_template_slot(input: ParseStream) -> Result<TemplateNode> {
    let lookahead = input.lookahead1();
    let name = if lookahead.peek(Token![/]) {
        None
    } else {
        Some(input.parse()?)
    };
    input.parse::<Token![/]>()?;
    input.parse::<Token![>]>()?;
    Ok(TemplateNode::VirtualNode(TemplateVirtualNode::Slot { name }))
}

fn parse_template_shadow_root(input: ParseStream) -> Result<TemplateShadowRoot> {
    let children = parse_children(input)?;
    Ok(TemplateShadowRoot { children })
}

fn parse_template_if(input: ParseStream) -> Result<TemplateNode> {
    let mut branches = vec![];
    let mut is_end = false;
    loop {
        let cond = if is_end {
            None
        } else {
            let content;
            braced!(content in input);
            let cond = content.parse()?;
            Some(cond)
        };
        let lookahead = input.lookahead1();
        let children = if lookahead.peek(Token![/]) {
            input.parse::<Token![/]>()?;
            input.parse::<Token![>]>()?;
            vec![]
        } else if lookahead.peek(Token![>]) {
            input.parse::<Token![>]>()?;
            let children = parse_children(input)?;
            let lookahead = input.lookahead1();
            if lookahead.peek(Token![if]) {
                input.parse::<Token![if]>()?;
            } else if lookahead.peek(Token![else]) {
                input.parse::<Token![else]>()?;
            } else {
                return Err(lookahead.error());
            }
            input.parse::<Token![>]>()?;
            children
        } else {
            return Err(lookahead.error());
        };
        branches.push((cond, children));
        let lookahead = input.lookahead1();
        if lookahead.peek(Token![<]) {
            if input.peek2(Token![else]) {
                input.parse::<Token![<]>()?;
                input.parse::<Token![else]>()?;
                let lookahead = input.lookahead1();
                if lookahead.peek(Token![if]) {
                    input.parse::<Token![if]>()?;
                } else {
                    is_end = true;
                }
            } else {
                break
            }
        } else {
            break
        }
    }
    Ok(TemplateNode::VirtualNode(TemplateVirtualNode::If { branches }))
}

fn parse_template_for(input: ParseStream) -> Result<TemplateNode> {
    let lookahead = input.lookahead1();
    let (index, item) = if lookahead.peek(Ident) {
        (Ident::new("index", Span::call_site()), input.parse::<Ident>()?)
    } else if lookahead.peek(token::Paren) {
        let index = input.parse::<Ident>()?;
        input.parse::<Token![,]>()?;
        let item = input.parse::<Ident>()?;
        (index, item)
    } else {
        return Err(lookahead.error());
    };
    input.parse::<Token![in]>()?;
    let content;
    braced!(content in input);
    let list: Expr = content.parse()?;
    let lookahead = input.lookahead1();
    let key = if lookahead.peek(Token![use]) {
        input.parse::<Token![use]>()?;
        let field: Ident = input.parse()?;
        input.parse::<Token![:]>()?;
        let ty: Path = input.parse()?;
        Some((field, ty))
    } else {
        None
    };
    let lookahead = input.lookahead1();
    let children = if lookahead.peek(Token![/]) {
        input.parse::<Token![/]>()?;
        input.parse::<Token![>]>()?;
        vec![]
    } else if lookahead.peek(Token![>]) {
        input.parse::<Token![>]>()?;
        let children = parse_children(input)?;
        input.parse::<Token![for]>()?;
        input.parse::<Token![>]>()?;
        children
    } else {
        return Err(lookahead.error());
    };
    Ok(TemplateNode::VirtualNode(TemplateVirtualNode::For { index, item, list, key, children }))
}

fn parse_template_in(input: ParseStream) -> Result<TemplateNode> {
    let name = input.parse()?;
    let children = parse_children(input)?;
    input.parse::<Token![in]>()?;
    input.parse::<Token![>]>()?;
    Ok(TemplateNode::VirtualNode(TemplateVirtualNode::InSlot { name, children }))
}

fn parse_dashed_name(input: ParseStream) -> Result<Ident> {
    let name: Ident = input.parse()?;
    let span = name.span();
    let mut s = name.to_string();
    loop {
        let lookahead = input.lookahead1();
        if lookahead.peek(Token![-]) {
            input.parse::<Token![-]>()?;
            let name: Ident = input.parse()?;
            s += "_";
            s += &name.to_string();
        } else {
            break
        }
    }
    Ok(Ident::new(&s, span))
}

fn parse_attributes(input: ParseStream, is_component: bool) -> Result<(Vec<TemplateAttribute>, bool)> {
    let mut props = vec![];
    let mut self_closed = false;
    loop {
        let lookahead = input.lookahead1();
        if lookahead.peek(Token![/]) {
            input.parse::<Token![/]>()?;
            input.parse::<Token![>]>()?;
            self_closed = true;
            break;
        } else if lookahead.peek(Token![>]) {
            input.parse::<Token![>]>()?;
            break;
        } else if lookahead.peek(Token![@]) {
            input.parse::<Token![@]>()?;
            let lookahead = input.lookahead1();
            let mut is_custom = false;
            if lookahead.peek(Token![#]) {
                input.parse::<Token![#]>()?;
                is_custom = true;
            } else if lookahead.peek(Ident) {
                // empty
            } else {
                return Err(lookahead.error());
            }
            let name: Ident = parse_dashed_name(input)?;
            let name_str = name.to_string();
            if !is_custom && SYSTEM_EVENTS.iter().position(|s| s == &name_str).is_none() {
                is_custom = true;
            }
            input.parse::<Token![=]>()?;
            let content;
            braced!(content in input);
            let value: Expr = content.parse()?;
            if is_custom {
                if !is_component {
                    return Err(Error::new(name.span(), "custom events are not supported on this node"));
                }
                props.push(TemplateAttribute::Ev { name, value: TemplateValue::from(value) });
            } else {
                props.push(TemplateAttribute::SystemEv { name, value: TemplateValue::from(value) });
            }
        } else if lookahead.peek(Token![#]) {
            input.parse::<Token![#]>()?;
            let name: Ident = parse_dashed_name(input)?;
            input.parse::<Token![=]>()?;
            let value = parse_template_value(input)?;
            if !is_component {
                return Err(Error::new(name.span(), "component properties are not supported on this node"));
            }
            props.push(TemplateAttribute::Prop { name, value });
        } else if lookahead.peek(Ident) {
            let name: Ident = parse_dashed_name(input)?;
            let name_str = name.to_string();
            input.parse::<Token![=]>()?;
            let value = parse_template_value(input)?;
            if name_str == "mark" {
                props.push(TemplateAttribute::Mark { value });
            } else if is_component && SYSTEM_ATTRIBUTES.iter().position(|s| s == &name_str).is_none() {
                props.push(TemplateAttribute::Prop { name, value });
            } else {
                props.push(TemplateAttribute::Common { name: ident_to_dashed_str(&name), value });
            }
        } else {
            return Err(lookahead.error());
        }
    }
    Ok((props, self_closed))
}

fn parse_template_element_or_slot(input: ParseStream) -> Result<TemplateNode> {
    let component: Path = input.parse()?;
    let name = &component.segments[0].ident;
    let name_s = name.to_string();
    if name_s == "slot" {
        return parse_template_slot(input);
    }
    let is_component = component.leading_colon.is_some() || name_s.chars().next().unwrap().is_uppercase();
    let (props, self_closed) = parse_attributes(input, is_component)?;
    let children = if !self_closed {
        let children = parse_children(input)?;
        let component_end: Ident = input.parse()?;
        if component.segments[0].ident != component_end {
            return Err(Error::new(component_end.span(), "end tag does not match the start tag"));
        }
        input.parse::<Token![>]>()?;
        children
    } else {
        vec![]
    };
    if is_component {
        Ok(TemplateNode::Component(TemplateComponent {
            tag_name: LitStr::new(format!("maomi{}", ty_to_string(&name)).as_str(), Span::call_site()),
            component,
            property_values: props,
            children,
        }))
    } else {
        Ok(TemplateNode::NativeNode(TemplateNativeNode {
            tag_name: ident_to_dashed_str(name),
            attributes: props,
            children,
        }))
    }
}

pub(crate) fn parse_template(input: ParseStream) -> Result<TemplateShadowRoot> {
    let content;
    braced!(content in input);
    parse_template_shadow_root(&content)
}