use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, quote_spanned};
use syn::{
parse::{Parse, ParseStream},
parse_macro_input,
spanned::Spanned,
Expr, Ident, LitStr, Result, Token,
};
#[proc_macro]
pub fn view(input: TokenStream) -> TokenStream {
let nodes = parse_macro_input!(input as NodeList);
let expanded = codegen_screen(nodes);
TokenStream::from(expanded)
}
struct NodeList {
nodes: Vec<Node>,
}
impl Parse for NodeList {
fn parse(input: ParseStream) -> Result<Self> {
let mut nodes = Vec::new();
while !input.is_empty() {
nodes.push(input.parse::<Node>()?);
}
Ok(NodeList { nodes })
}
}
enum Node {
Element(ParsedElement),
Text(LitStr),
}
impl Parse for Node {
fn parse(input: ParseStream) -> Result<Self> {
if input.peek(LitStr) {
Ok(Node::Text(input.parse()?))
} else {
Ok(Node::Element(input.parse()?))
}
}
}
struct ParsedElement {
span: Span,
tag: Ident,
attrs: Vec<ParsedAttr>,
children: Vec<Node>,
}
impl Parse for ParsedElement {
fn parse(input: ParseStream) -> Result<Self> {
let lt: Token![<] = input.parse().map_err(|e| {
syn::Error::new(e.span(), "Expected `<` to open a UI element.\n\nTip: every element starts with `<`, like `<button>` or `<h1>`.")
})?;
let span = lt.span();
let tag: Ident = input.parse().map_err(|e| {
syn::Error::new(e.span(), "Expected a tag name after `<`.\n\nSupported tags: h1, h2, h3, p, button, img, input, div, span, a")
})?;
let mut attrs = Vec::new();
while !input.peek(Token![>]) && !input.peek(Token![/]) {
attrs.push(input.parse::<ParsedAttr>()?);
}
if input.peek(Token![/]) {
input.parse::<Token![/]>()?;
input.parse::<Token![>]>()?;
return Ok(ParsedElement { span, tag, attrs, children: vec![] });
}
input.parse::<Token![>]>()?;
let mut children = Vec::new();
loop {
if input.peek(Token![<]) && input.peek2(Token![/]) {
input.parse::<Token![<]>()?;
input.parse::<Token![/]>()?;
let closing_tag: Ident = input.parse().map_err(|e| {
syn::Error::new(e.span(), "Expected closing tag name.")
})?;
input.parse::<Token![>]>()?;
if closing_tag != tag {
return Err(syn::Error::new(
closing_tag.span(),
format!(
"Mismatched tags: opened `<{}>` but closed with `</{}>`.\n\nTip: every opening tag needs a matching closing tag.",
tag, closing_tag
),
));
}
break;
}
if input.is_empty() {
return Err(syn::Error::new(
span,
format!("Unclosed `<{}>` — missing `</{}>`.\n\nTip: add `</{}>` after the children.", tag, tag, tag),
));
}
children.push(input.parse::<Node>()?);
}
Ok(ParsedElement { span, tag, attrs, children })
}
}
struct ParsedAttr {
name: Ident,
value: AttrValue,
}
enum AttrValue {
Str(LitStr),
Expr(Expr),
}
impl Parse for ParsedAttr {
fn parse(input: ParseStream) -> Result<Self> {
let name: Ident = input.parse().map_err(|e| {
syn::Error::new(e.span(), "Expected an attribute name (e.g. `class`, `onclick`, `src`).")
})?;
input.parse::<Token![=]>().map_err(|e| {
syn::Error::new(e.span(), format!("Attribute `{}` needs a value: `{}=\"...\"` or `{}=expr`.", name, name, name))
})?;
let value = if input.peek(LitStr) {
AttrValue::Str(input.parse()?)
} else {
let expr = parse_event_expr(input).map_err(|e| {
syn::Error::new(
e.span(),
format!(
"Could not parse value for `{}`.\n\nExamples:\n {}=\"some-class\"\n {}=alert(\"message\")\n {}=navigate(ScreenName)",
name, name, name, name
),
)
})?;
AttrValue::Expr(expr)
};
Ok(ParsedAttr { name, value })
}
}
fn parse_event_expr(input: ParseStream) -> Result<Expr> {
if input.peek(Token![|]) {
return input.parse::<Expr>();
}
let path: syn::ExprPath = input.parse()?;
if input.peek(syn::token::Paren) {
let args_content;
let paren = syn::parenthesized!(args_content in input);
let args: syn::punctuated::Punctuated<Expr, Token![,]> =
args_content.parse_terminated(Expr::parse, Token![,])?;
Ok(Expr::Call(syn::ExprCall {
attrs: vec![],
func: Box::new(Expr::Path(path)),
paren_token: paren,
args,
}))
} else {
Ok(Expr::Path(path))
}
}
fn codegen_screen(nodes: NodeList) -> TokenStream2 {
let element_stmts: Vec<TokenStream2> = nodes.nodes.iter().map(codegen_node_as_child).collect();
quote! {
{
let mut __bubba_root = ::bubba_core::ui::Element::div();
#(#element_stmts)*
::bubba_core::ui::Screen::new(__bubba_root)
}
}
}
fn codegen_node_as_child(node: &Node) -> TokenStream2 {
match node {
Node::Text(lit) => {
quote! {
__bubba_root = __bubba_root.child(
::bubba_core::ui::Element::span().text(#lit)
);
}
}
Node::Element(el) => {
let el_expr = codegen_element(el);
quote! {
__bubba_root = __bubba_root.child(#el_expr);
}
}
}
}
fn codegen_element(el: &ParsedElement) -> TokenStream2 {
let tag = &el.tag;
let tag_str = tag.to_string();
let span = el.span;
let mut builder = quote_spanned! { span =>
::bubba_core::ui::Element::new(#tag_str)
};
for attr in &el.attrs {
let attr_name = attr.name.to_string();
match &attr.value {
AttrValue::Str(s) => {
match attr_name.as_str() {
"class" => {
builder = quote! { #builder.class(#s) };
}
_ => {
builder = quote! { #builder.attr(#attr_name, #s) };
}
}
}
AttrValue::Expr(expr) => {
let event_name = match attr_name.as_str() {
"onclick" => Some("click"),
"oninput" => Some("input"),
"onkeypress" => Some("keypress"),
"onfocus" => Some("focus"),
"onblur" => Some("blur"),
"onchange" => Some("change"),
other => {
let msg = format!(
"Unknown event attribute `{}`. Did you mean `onclick`, `oninput`, or `onkeypress`?",
other
);
builder = quote! {
#builder
compile_error!(#msg)
};
None
}
};
if let Some(ev) = event_name {
let handler_expr = codegen_event_expr(expr, ev);
builder = quote! { #builder.on(#handler_expr) };
}
}
}
}
for child in &el.children {
match child {
Node::Text(lit) => {
builder = quote! { #builder.text(#lit) };
}
Node::Element(child_el) => {
let child_expr = codegen_element(child_el);
builder = quote! { #builder.child(#child_expr) };
}
}
}
builder
}
fn codegen_event_expr(expr: &Expr, event: &str) -> TokenStream2 {
match expr {
Expr::Call(call) => {
if let Expr::Path(path) = call.func.as_ref() {
let name = path.path.segments.last().map(|s| s.ident.to_string());
match name.as_deref() {
Some("alert") => {
let args = &call.args;
return quote! {
::bubba_core::events::EventHandler::new(#event, move |_| {
::bubba_core::runtime::alert(#args);
})
};
}
Some("log") => {
let args = &call.args;
return quote! {
::bubba_core::events::EventHandler::new(#event, move |_| {
::bubba_core::runtime::log_msg(#args);
})
};
}
Some("navigate") => {
if let Some(screen_arg) = call.args.first() {
return quote! {
::bubba_core::events::EventHandler::new(#event, move |_| {
::bubba_core::navigation::navigate_to(
stringify!(#screen_arg),
#screen_arg,
);
})
};
}
}
_ => {}
}
}
quote! {
::bubba_core::events::EventHandler::new(#event, move |_| { #expr; })
}
}
Expr::Closure(closure) => {
quote! {
::bubba_core::events::EventHandler::new(#event, #closure)
}
}
_ => {
quote! {
::bubba_core::events::EventHandler::new(#event, move |_| { #expr; })
}
}
}
}