moxy-template 0.4.1

Rust quasi-quoting and token templating with explicit interpolation syntax and built-in template control flow
Documentation
#![allow(unused)]

use moxy_ast::{Cursor, Delimited, Parse, ParseError, Parser, Token};
use moxy_token::{Delim, Group, LexError, Span, ToTokenStream, ToTokens, TokenStream, TokenTree};

use crate::Template;

#[doc = "A template match directive: `@match (expr) { pat => { body }, … }`."]
#[derive(Clone)]
#[cfg_attr(feature = "derives", derive(Debug))]
pub struct TmplMatch {
    pub span: Span,
    pub at: Token![@],
    pub keyword: Token![match],
    pub expr: TokenStream,
    pub arms: Delimited<Vec<TmplMatchArm>>,
}

impl Parse for TmplMatch {
    fn peek(cursor: Cursor<'_>) -> bool {
        let Some(cursor) = <Token![@]>::skip(cursor) else {
            return false;
        };

        <Token![match]>::peek(cursor)
    }

    fn parse(parser: &Parser) -> Result<Self, ParseError> {
        let at: Token![@] = <_ as Parse>::parse(parser)?;
        let keyword = <_ as Parse>::parse(parser)?;
        let span = at.span();
        let expr = parser.parse_group(Delim::Paren)?.to_token_stream();
        let arms = Delimited::parse_brace(parser)?;

        Ok(Self {
            span,
            at,
            keyword,
            expr,
            arms,
        })
    }

    fn skip(cursor: Cursor<'_>) -> Option<Cursor<'_>> {
        let cursor = <Token![@]>::skip(cursor)?;
        let cursor = <Token![match]>::skip(cursor)?;
        let inner = cursor.descend(Delim::Paren)?;
        let cursor = inner.offset(inner.remaining()).is_empty().then(|| cursor.offset(1))?;
        let mut inner = cursor.descend(Delim::Brace)?;

        while !inner.is_empty() {
            inner = TmplMatchArm::skip(inner)?;
        }

        Some(cursor.offset(1))
    }
}

impl ToTokens for TmplMatch {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        self.keyword.to_tokens(tokens);
        self.expr.to_tokens(tokens);
        self.arms.to_tokens(tokens);
    }
}

#[doc = "A single arm of a `@match` directive: `pat => { body }`."]
#[derive(Clone)]
#[cfg_attr(feature = "derives", derive(Debug))]
pub struct TmplMatchArm {
    pub span: Span,
    pub pat: TokenStream,
    pub arrow: Token![=>],
    pub body: Delimited<Template>,
    pub comma: Option<Token![,]>,
}

impl Parse for TmplMatchArm {
    fn peek(cursor: Cursor<'_>) -> bool {
        Self::skip(cursor).is_some()
    }

    fn parse(parser: &Parser) -> Result<Self, ParseError> {
        let span = parser.span();
        let mut pat = TokenStream::new();

        loop {
            if <Token![=>]>::peek(parser.cursor()) {
                break;
            }

            match parser.curr() {
                None => return Err(LexError::new(span).message("unexpected end of match arm").into()),
                _ => {
                    pat.extend_one(parser.advance().unwrap().clone());
                }
            }
        }

        let arrow = <_ as Parse>::parse(parser)?;
        let body = Delimited::parse_brace(parser)?;
        let comma = <_ as Parse>::parse(parser)?;

        Ok(Self {
            span,
            pat,
            arrow,
            body,
            comma,
        })
    }

    fn skip(mut cursor: Cursor<'_>) -> Option<Cursor<'_>> {
        while !<Token![=>]>::peek(cursor) {
            cursor.curr()?;
            cursor = cursor.offset(1);
        }

        cursor = <Token![=>]>::skip(cursor)?;
        let inner = cursor.descend(Delim::Brace)?;
        let inner = Template::skip(inner)?;

        if !inner.is_empty() {
            return None;
        }

        cursor = cursor.offset(1);
        Option::<Token![,]>::skip(cursor)
    }
}

impl ToTokens for TmplMatchArm {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        self.pat.to_tokens(tokens);
        self.arrow.to_tokens(tokens);
        self.body.to_tokens(tokens);
        self.comma.to_tokens(tokens);
    }
}