templating 0.1.1

Simple HTML templating for Rust.
Documentation
//! This crate provides the [`html`] macro for creating HTML templates in a
//! simple way. More information about the macro can be found in its
//! documentation.
//!
//! # Tutorial
//!
//! ## Simple templates
//! You can use the [`html`] macro to create a tamplate like this:
//! ```
//! fn home() -> String {
//!     html! {
//!         <span>
//!             Welcome to my page
//!         </span>
//!     }
//! }
//! ```
//!
//! ## Expression templates
//! You can use parentheses to insert a runtime value into the string.
//! ```
//! fn dashboard(user: &str) -> String {
//!     html! {
//!         <h3>
//!             Hello, (user)
//!         </h3>
//!     }
//! }
//! ```
//!
//! But if you try this, you will get an unexpected result:
//! ```
//! dashboard("Rust") // => <h3>Hello,Rust</h3>
//! ```
//!
//! If you want to add an space, you can use templates:
//! ```
//! fn dashboard(user: &str) -> String {
//!     html! {
//!         <h3>
//!             ("Hello, ")(user)
//!         </h3>
//!     }
//! }
//! ```
//! ```
//! dashboard("Rust") // => <h3>Hello, Rust</h3>
//! ```
//!
//! ## Block templates
//! You can use block templates to create more complex templates:
//! ```
//! fn store(items: Vec<String>) -> String {
//!     html! {
//!         <ul>
//!             {
//!                 for item in items {
//!                     html! {
//!                         <li>
//!                             (item)
//!                         </li>
//!                     };
//!                 }
//!             }
//!         </ul>
//!     }
//! }
//! ```
//!
//! [`html`]: crate::html!

extern crate proc_macro;

use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};

#[proc_macro]
/// Creates an HTML string template.
///
/// Using `()` you can display any Rust expression resulting in an value
/// implementing [`Display`] trait. Using `{}` you can use Rust's control flow
/// along with the `html` macro to insert HTML into the interpolated string.
///
/// [`Display`]: core::fmt::Display
///
/// # Notes
/// Using the `html` macro inside itself will not perform the same action as
/// the outer one. The inner one will actually work as a statement and will
/// append its content to the outer macro content.
///
/// If you really need to use it inside itself, but have the same behavior in
/// both of them, you can alias it with the `use ... as ...` declaration.
///
/// # Example
///
/// ```
/// let code = html! {
///     <ul>
///         {
///             for i in 0..10 {
///                 html! {
///                     <li>(i)</li>
///                 }
///             }
///         }
///     </ul>
/// };
/// println!("{}", code);
/// ```
pub fn html(src: TokenStream) -> TokenStream {
    html1(src, false)
}

fn html1(src: TokenStream, is_inner: bool) -> TokenStream {
    let mut tokens = Vec::new();
    if !is_inner {
        tokens.push(TokenTree::Ident(Ident::new("let", Span::call_site())));
        tokens.push(TokenTree::Ident(Ident::new("mut", Span::call_site())));
        tokens.push(TokenTree::Ident(Ident::new("__html", Span::call_site())));
        tokens.push(TokenTree::Punct(Punct::new('=', Spacing::Alone)));
        tokens.push(TokenTree::Ident(Ident::new("String", Span::call_site())));
        tokens.push(TokenTree::Punct(Punct::new(':', Spacing::Joint)));
        tokens.push(TokenTree::Punct(Punct::new(':', Spacing::Joint)));
        tokens.push(TokenTree::Ident(Ident::new("new", Span::call_site())));
        tokens.push(TokenTree::Group(Group::new(
            Delimiter::Parenthesis,
            TokenStream::new(),
        )));
        tokens.push(TokenTree::Punct(Punct::new(';', Spacing::Alone)));
    }

    let mut string = String::new();
    parse(src, &mut string, &mut tokens);

    if !string.is_empty() {
        tokens.push(TokenTree::Ident(Ident::new("__html", Span::call_site())));
        tokens.push(TokenTree::Punct(Punct::new('.', Spacing::Alone)));
        tokens.push(TokenTree::Ident(Ident::new("push_str", Span::call_site())));
        tokens.push(TokenTree::Group(Group::new(Delimiter::Parenthesis, {
            [TokenTree::Literal(Literal::string(&string))]
                .into_iter()
                .collect()
        })));
        tokens.push(TokenTree::Punct(Punct::new(';', Spacing::Alone)));
    }

    if !is_inner {
        tokens.push(TokenTree::Ident(Ident::new("__html", Span::call_site())));
    }

    [TokenTree::Group(Group::new(
        Delimiter::Brace,
        tokens.into_iter().collect(),
    ))]
    .into_iter()
    .collect()
}

fn parse(src: TokenStream, string: &mut String, tokens: &mut Vec<TokenTree>) {
    let mut src = src.into_iter().peekable();

    while let Some(token) = src.next() {
        match token {
            TokenTree::Ident(_) if matches!(src.peek(), Some(TokenTree::Ident(_))) => {
                string.push_str(&token.to_string());
                string.push(' ');
            }
            TokenTree::Group(group) => match group.delimiter() {
                Delimiter::Brace => {
                    parse_rust_block(group.stream(), string, tokens);
                }
                Delimiter::None => {
                    parse(group.stream(), string, tokens);
                }
                Delimiter::Bracket => {
                    string.push('[');
                    parse(group.stream(), string, tokens);
                    string.push(']');
                }
                Delimiter::Parenthesis => {
                    parse_rust_expr(group.stream(), string, tokens);
                }
            },
            _ => {
                string.push_str(&token.to_string());
            }
        }
    }
}

fn parse_rust_block(src: TokenStream, string: &mut String, tokens: &mut Vec<TokenTree>) {
    if !string.is_empty() {
        tokens.push(TokenTree::Ident(Ident::new("__html", Span::call_site())));
        tokens.push(TokenTree::Punct(Punct::new('.', Spacing::Alone)));
        tokens.push(TokenTree::Ident(Ident::new("push_str", Span::call_site())));
        tokens.push(TokenTree::Group(Group::new(Delimiter::Parenthesis, {
            [TokenTree::Literal(Literal::string(string))]
                .into_iter()
                .collect()
        })));
        tokens.push(TokenTree::Punct(Punct::new(';', Spacing::Alone)));
        string.clear();
    }

    let mut src = src.into_iter().peekable();
    while let Some(token) = src.next() {
        match &token {
            TokenTree::Group(g) => {
                let mut content = Vec::new();
                parse_rust_block(g.stream(), string, &mut content);
                tokens.push(TokenTree::Group(Group::new(
                    g.delimiter(),
                    content.into_iter().collect(),
                )));
            }
            TokenTree::Ident(ident)
                if ident.to_string() == "html"
                    && matches!(src.peek(), Some(TokenTree::Punct(_))) =>
            {
                let next = src.next().unwrap();
                if next.to_string() == "!" {
                    if matches!(src.peek(), Some(TokenTree::Group(_))) {
                        let TokenTree::Group(group) = src.next().unwrap() else {
                            panic!();
                        };
                        let content = html1(group.stream(), true);
                        tokens.extend(content.into_iter());
                    } else {
                        tokens.push(next);
                    }
                } else {
                    tokens.push(next);
                }
            }
            _ => tokens.push(token),
        }
    }
}

fn parse_rust_expr(src: TokenStream, string: &mut String, tokens: &mut Vec<TokenTree>) {
    if !string.is_empty() {
        tokens.push(TokenTree::Ident(Ident::new("__html", Span::call_site())));
        tokens.push(TokenTree::Punct(Punct::new('.', Spacing::Alone)));
        tokens.push(TokenTree::Ident(Ident::new("push_str", Span::call_site())));
        tokens.push(TokenTree::Group(Group::new(Delimiter::Parenthesis, {
            [TokenTree::Literal(Literal::string(string))]
                .into_iter()
                .collect()
        })));
        tokens.push(TokenTree::Punct(Punct::new(';', Spacing::Alone)));
        string.clear();
    }

    tokens.push(TokenTree::Ident(Ident::new("__html", Span::call_site())));
    tokens.push(TokenTree::Punct(Punct::new('.', Spacing::Alone)));
    tokens.push(TokenTree::Ident(Ident::new("push_str", Span::call_site())));
    tokens.push(TokenTree::Group(Group::new(Delimiter::Parenthesis, {
        [
            TokenTree::Punct(Punct::new('&', Spacing::Alone)),
            TokenTree::Ident(Ident::new("format", Span::call_site())),
            TokenTree::Punct(Punct::new('!', Spacing::Alone)),
            TokenTree::Group(Group::new(Delimiter::Parenthesis, {
                [
                    TokenTree::Literal(Literal::string("{}")),
                    TokenTree::Punct(Punct::new(',', Spacing::Alone)),
                ]
                .into_iter()
                .chain(src)
                .collect()
            })),
        ]
        .into_iter()
        .collect()
    })));
    tokens.push(TokenTree::Punct(Punct::new(';', Spacing::Alone)));
}