templr_parser 0.2.1

Parser for templr templates
Documentation
use core::fmt;

use proc_macro2::{Punct, Spacing, TokenStream};
use quote::{ToTokens, TokenStreamExt};
use syn::{ext::*, parse::ParseStream, spanned::Spanned, Ident, LitFloat, LitInt, LitStr, Token};

#[rustfmt::skip]
const fn is_invalid_attribute_char(ch: char) -> bool {
    matches!(
        ch,
        '\0'..='\x1F' | '\x7F'..='\u{9F}'
        | 'A'..='Z' | ' ' | '"' | '\'' | '>' | '/' | '='
        | '\u{FDD0}'..='\u{FDEF}'
        | '\u{0FFFE}' | '\u{0FFFF}' | '\u{01FFFE}' | '\u{01FFFF}' | '\u{2FFFE}'
        | '\u{2FFFF}' | '\u{3FFFE}' | '\u{03FFFF}' | '\u{04FFFE}' | '\u{4FFFF}'
        | '\u{5FFFE}' | '\u{5FFFF}' | '\u{06FFFE}' | '\u{06FFFF}' | '\u{7FFFE}'
        | '\u{7FFFF}' | '\u{8FFFE}' | '\u{08FFFF}' | '\u{09FFFE}' | '\u{9FFFF}'
        | '\u{AFFFE}' | '\u{AFFFF}' | '\u{0BFFFE}' | '\u{0BFFFF}' | '\u{CFFFE}'
        | '\u{CFFFF}' | '\u{DFFFE}' | '\u{0DFFFF}' | '\u{0EFFFE}' | '\u{EFFFF}'
        | '\u{FFFFE}' | '\u{FFFFF}' | '\u{10FFFE}' | '\u{10FFFF}'
    )
}

#[rustfmt::skip]
const fn is_invalid_tagname_char(ch: char) -> bool {
    !matches!(
        ch,
        '-' | '.' | '0'..='9' | '_' | 'a'..='z'
        | '\u{B7}' | '\u{C0}'..='\u{D6}' | '\u{D8}'..='\u{F6}' | '\u{F8}'..='\u{37D}'
        | '\u{37F}'..='\u{1FFF}' | '\u{200C}'..='\u{200D}' | '\u{203F}'..='\u{2040}'
        | '\u{2070}'..='\u{218F}' | '\u{2C00}'..='\u{2FEF}' | '\u{3001}'..='\u{D7FF}'
        | '\u{F900}'..='\u{FDCF}' | '\u{FDF0}'..='\u{FFFD}' | '\u{10000}'..='\u{EFFFF}'
    )
}

const INVALID_TAG_MSG: &str =
    "Invalid tag name (https://html.spec.whatwg.org/multipage/syntax.html#syntax-tag-name)";
const INVALID_ATTR_MSG: &str = "Invalid attribute name \
     (https://html.spec.whatwg.org/multipage/syntax.html#syntax-attribute-name)";

#[derive(Debug, Clone)]
pub enum NamePart {
    Ident(Ident),
    Int(LitInt),
    Float(LitFloat),
    Dot(Token![.], Spacing),
    Hyphen(Token![-], Spacing),
    Colon(Token![:], Spacing),
    At(Token![@], Spacing),
    Question(Token![?], Spacing),
}

impl ToTokens for NamePart {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        match self {
            Self::Ident(slf) => slf.to_tokens(tokens),
            Self::Int(slf) => slf.to_tokens(tokens),
            Self::Float(slf) => slf.to_tokens(tokens),
            Self::Dot(slf, _) => slf.to_tokens(tokens),
            Self::Hyphen(slf, _) => slf.to_tokens(tokens),
            Self::Colon(slf, _) => slf.to_tokens(tokens),
            Self::At(slf, _) => slf.to_tokens(tokens),
            Self::Question(slf, _) => slf.to_tokens(tokens),
        }
    }
}

impl NamePart {
    fn peek_attr(&self, input: ParseStream) -> bool {
        let Some((token, _)) = input.cursor().token_tree() else {
            return false;
        };

        if self
            .span()
            .join(token.span())
            .and_then(|span| span.source_text())
            .is_some_and(|source| source.contains(char::is_whitespace))
        {
            return false;
        }

        !matches!(self, Self::Ident(_) | Self::Int(_) | Self::Float(_))
            && (input.peek(Ident::peek_any) || input.peek(LitInt) || input.peek(LitFloat))
            || !matches!(
                self,
                Self::Dot(_, Spacing::Alone)
                    | Self::Hyphen(_, Spacing::Alone)
                    | Self::Colon(_, Spacing::Alone)
                    | Self::At(_, Spacing::Alone)
                    | Self::Question(_, Spacing::Alone)
            ) && (input.peek(Token![.])
                || input.peek(Token![-])
                || input.peek(Token![:])
                || input.peek(Token![@])
                || input.peek(Token![?]) && !input.peek2(Token![=]))
    }

    fn peek_tag(&self, input: ParseStream) -> bool {
        let Some((token, _)) = input.cursor().token_tree() else {
            return false;
        };

        if self
            .span()
            .join(token.span())
            .and_then(|span| span.source_text())
            .is_some_and(|source| source.contains(char::is_whitespace))
        {
            return false;
        }

        const _: () = assert!(is_invalid_tagname_char('@'));
        const _: () = assert!(is_invalid_tagname_char(':'));
        const _: () = assert!(is_invalid_tagname_char('?'));

        !matches!(self, Self::Ident(_) | Self::Int(_) | Self::Float(_))
            && (input.peek(Ident::peek_any) || input.peek(LitInt) || input.peek(LitFloat))
            || !matches!(
                self,
                Self::Dot(_, Spacing::Alone)
                    | Self::Hyphen(_, Spacing::Alone)
                    | Self::Colon(_, Spacing::Alone)
                    | Self::At(_, Spacing::Alone)
                    | Self::Question(_, Spacing::Alone)
            ) && (input.peek(Token![.]) || input.peek(Token![-]))
    }

    pub fn parse_attr(input: ParseStream) -> syn::Result<Self> {
        let lookahead1 = input.lookahead1();

        let slf = if lookahead1.peek(Ident::peek_any) {
            Self::Ident(Ident::parse_any(input)?)
        } else if lookahead1.peek(LitInt) {
            Self::Int(input.parse()?)
        } else if lookahead1.peek(LitFloat) {
            Self::Float(input.parse()?)
        } else if lookahead1.peek(Token![.]) {
            let punct: Punct = input.parse()?;
            let mut tk: Token![.] = Default::default();
            tk.spans[0] = punct.span();
            Self::Dot(tk, punct.spacing())
        } else if lookahead1.peek(Token![-]) {
            let punct: Punct = input.parse()?;
            let mut tk: Token![-] = Default::default();
            tk.spans[0] = punct.span();
            Self::Hyphen(tk, punct.spacing())
        } else if lookahead1.peek(Token![:]) {
            let punct: Punct = input.parse()?;
            let mut tk: Token![:] = Default::default();
            tk.spans[0] = punct.span();
            Self::Colon(tk, punct.spacing())
        } else if lookahead1.peek(Token![@]) {
            let punct: Punct = input.parse()?;
            let mut tk: Token![@] = Default::default();
            tk.spans[0] = punct.span();
            Self::At(tk, punct.spacing())
        } else if lookahead1.peek(Token![?]) {
            let punct: Punct = input.parse()?;
            let mut tk: Token![?] = Default::default();
            tk.spans[0] = punct.span();
            Self::Question(tk, punct.spacing())
        } else {
            return Err(lookahead1.error());
        };

        if slf.to_string().contains(is_invalid_attribute_char) {
            Err(syn::Error::new_spanned(slf, INVALID_ATTR_MSG))
        } else {
            Ok(slf)
        }
    }

    pub fn parse_tag_first(input: ParseStream) -> syn::Result<Self> {
        let ident = Ident::parse_any(input)?;

        let s = ident.to_string();
        if s.starts_with(|ch: char| !ch.is_ascii_lowercase()) || s.contains(is_invalid_tagname_char)
        {
            Err(syn::Error::new(ident.span(), INVALID_TAG_MSG))
        } else {
            Ok(Self::Ident(ident))
        }
    }

    pub fn parse_tag(input: ParseStream) -> syn::Result<Self> {
        let lookahead1 = input.lookahead1();

        let slf = if lookahead1.peek(Ident::peek_any) {
            Self::Ident(Ident::parse_any(input)?)
        } else if lookahead1.peek(LitInt) {
            Self::Int(input.parse()?)
        } else if lookahead1.peek(LitFloat) {
            Self::Float(input.parse()?)
        } else if lookahead1.peek(Token![.]) {
            let punct: Punct = input.parse()?;
            let mut tk: Token![.] = Default::default();
            tk.spans[0] = punct.span();
            Self::Dot(tk, punct.spacing())
        } else if lookahead1.peek(Token![-]) {
            let punct: Punct = input.parse()?;
            let mut tk: Token![-] = Default::default();
            tk.spans[0] = punct.span();
            Self::Hyphen(tk, punct.spacing())
        } else {
            return Err(lookahead1.error());
        };

        if slf.to_string().contains(is_invalid_tagname_char) {
            Err(syn::Error::new_spanned(slf, INVALID_TAG_MSG))
        } else {
            Ok(slf)
        }
    }
}

impl fmt::Display for NamePart {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            NamePart::Ident(part) => fmt::Display::fmt(part, f),
            NamePart::Int(part) => fmt::Display::fmt(part, f),
            NamePart::Float(part) => fmt::Display::fmt(part, f),
            NamePart::Dot(_, _) => f.write_str("."),
            NamePart::Hyphen(_, _) => f.write_str("-"),
            NamePart::Colon(_, _) => f.write_str(":"),
            NamePart::At(_, _) => f.write_str("@"),
            NamePart::Question(_, _) => f.write_str("?"),
        }
    }
}

#[derive(Debug, Clone)]
pub enum Name {
    Str(LitStr),
    Parts(Vec<NamePart>),
}

impl fmt::Display for Name {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Str(s) => fmt::Display::fmt(&s.value(), f),
            Self::Parts(parts) => {
                for part in parts {
                    fmt::Display::fmt(part, f)?;
                }
                Ok(())
            }
        }
    }
}

impl Name {
    pub fn parse_tag(input: ParseStream) -> syn::Result<Self> {
        if input.peek(LitStr) {
            let lit: LitStr = input.parse()?;
            let value = lit.value();
            if value.starts_with(|ch: char| !ch.is_ascii_lowercase())
                || value.contains(is_invalid_tagname_char)
            {
                return Err(syn::Error::new(lit.span(), INVALID_TAG_MSG));
            }
            Ok(Self::Str(lit))
        } else {
            let mut parts = vec![NamePart::parse_tag_first(input)?];
            while parts.last().unwrap().peek_tag(input) {
                parts.push(NamePart::parse_tag(input)?);
            }
            Ok(Self::Parts(parts))
        }
    }
    pub fn parse_attr(input: ParseStream) -> syn::Result<Self> {
        if input.peek(LitStr) {
            let lit: LitStr = input.parse()?;
            let value = lit.value();
            if value.contains(is_invalid_attribute_char) {
                return Err(syn::Error::new(lit.span(), INVALID_ATTR_MSG));
            }
            Ok(Self::Str(lit))
        } else {
            let mut parts = vec![NamePart::parse_attr(input)?];
            while parts.last().unwrap().peek_attr(input) {
                parts.push(NamePart::parse_attr(input)?);
            }
            Ok(Self::Parts(parts))
        }
    }
}

impl ToTokens for Name {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        match self {
            Self::Str(slf) => slf.to_tokens(tokens),
            Self::Parts(parts) => {
                tokens.append_all(parts);
            }
        }
    }
}

impl PartialEq for Name {
    fn eq(&self, other: &Self) -> bool {
        self.to_string() == other.to_string()
    }
}
impl Eq for Name {}

#[test]
fn test_name() {
    use quote::quote;
    use syn::parse::Parser;

    let name = Name::parse_attr
        .parse2(quote! {
            @on::hello-world.camel
        })
        .unwrap();
    let name2 = Name::parse_attr
        .parse2(quote! {
            "@on::hello-world.camel"
        })
        .unwrap();
    assert_eq!(name, name2);
}