fhtml-macros 0.6.1

Macro implementations for `fhtml`
Documentation
use std::fmt::Write;

use quote::ToTokens;
use syn::ext::IdentExt as _;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;

use crate::{ast, lower_ast, ConcatInput, FormatArgsInput};

mod kw {
    syn::custom_keyword!(DOCTYPE);
    syn::custom_keyword!(doctype);
    syn::custom_keyword!(html);
}

impl Parse for ast::DashIdent {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        // Parse a non-empty sequence of identifiers separated by dashes.
        Ok(Self(
            Punctuated::<syn::Ident, syn::Token![-]>::parse_separated_nonempty_with(
                input,
                syn::Ident::parse_any,
            )?
        ))
    }
}

impl Parse for ast::Doctype {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        input.parse::<syn::Token![<]>()?;
        input.parse::<syn::Token![!]>()?;
        if input.peek(kw::doctype) {
            input.parse::<kw::doctype>()?;
        } else {
            input.parse::<kw::DOCTYPE>()?;
        }
        input.parse::<kw::html>()?;
        input.parse::<syn::Token![>]>()?;

        Ok(Self)
    }
}

impl<Value: Parse> Parse for ast::Tag<Value> {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        input.parse::<syn::Token![<]>()?;

        if input.parse::<Option<syn::Token![/]>>()?.is_some() {
            let name = input.parse()?;
            input.parse::<syn::Token![>]>()?;

            return Ok(Self::Closing { name });
        }

        let name = input.parse()?;

        let mut attrs = Vec::new();
        while !(input.peek(syn::Token![>])
            || (input.peek(syn::Token![/]) && input.peek2(syn::Token![>])))
        {
            attrs.push(input.parse()?);
        }

        let self_closing = input.parse::<Option<syn::Token![/]>>()?.is_some();
        input.parse::<syn::Token![>]>()?;

        Ok(Self::Opening {
            name,
            attrs,
            self_closing,
        })
    }
}

impl<Value: Parse> Parse for ast::Attr<Value> {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let name = input.parse()?;
        input.parse::<syn::Token![=]>()?;
        let value = input.parse()?;

        Ok(Self { name, value })
    }
}

impl Parse for ast::LitValue {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(syn::LitStr) {
            Ok(Self::LitStr(input.parse()?))
        } else if lookahead.peek(syn::token::Brace) {
            let content;
            syn::braced!(content in input);
            Ok(Self::Expr(content.parse()?))
        } else {
            Err(lookahead.error())
        }
    }
}

impl Parse for ast::PlaceholderValue {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(syn::LitStr) {
            Ok(Self::LitStr(input.parse()?))
        } else if lookahead.peek(syn::token::Brace) {
            let content;

            syn::braced!(content in input);

            let value = content.parse()?;
            let mut specs = None;

            if content.parse::<Option<syn::Token![:]>>()?.is_some() {
                specs = Some(content.parse()?);
            }

            Ok(Self::Expr { value, specs })
        } else {
            Err(lookahead.error())
        }
    }
}

impl<Value: Parse> Parse for ast::Node<Value> {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(syn::Token![<])
            && input.peek2(syn::Token![!])
            && (input.peek3(kw::DOCTYPE) || input.peek3(kw::doctype))
        {
            Ok(Self::Doctype(input.parse()?))
        } else if lookahead.peek(syn::Token![<]) {
            Ok(Self::Tag(input.parse()?))
        } else if lookahead.peek(syn::LitStr)
            || lookahead.peek(syn::token::Brace)
        {
            Ok(Self::Value(input.parse()?))
        } else {
            Err(lookahead.error())
        }
    }
}

impl Parse for FormatArgsInput {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut template = String::new();
        let mut values = Vec::new();

        while !input.is_empty() {
            let node = input.parse::<ast::Node<ast::PlaceholderValue>>()?;

            values.extend(node.get_all_values().into_iter().map(Into::into));

            for part in node.into_iter() {
                let _ = template.write_str(&part.to_string());
            }
        }

        Ok(Self { template, values })
    }
}

impl Parse for ConcatInput {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut segments = Vec::new();
        let mut acc = String::new();

        while !input.is_empty() {
            let node = input.parse::<ast::Node<ast::LitValue>>()?;

            for part in node.into_iter() {
                match part {
                    lower_ast::AstPart::AttrValue(v)
                    | lower_ast::AstPart::Value(v) => {
                        if let ast::LitValue::LitStr(lit) = v {
                            acc.push_str(&lit.value());
                        } else {
                            segments.push(acc.to_token_stream());
                            segments.push(v.to_token_stream());
                            acc.clear();
                        }
                    }
                    _ => {
                        let _ = write!(acc, "{}", part);
                    }
                }
            }
        }

        if !acc.is_empty() {
            segments.push(acc.to_token_stream());
        }

        Ok(Self { segments })
    }
}