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, Parse, ParseError, Parser, Token};
use moxy_token::{Delim, Group, Span, ToTokenStream, ToTokens, TokenStream, TokenTree};

use crate::Template;

#[doc = "A template if/else-if/else directive: `@if (cond) { body } @else if (cond) { body } @else { body }`."]
#[derive(Clone)]
#[cfg_attr(feature = "derives", derive(Debug))]
pub struct TmplIf {
    pub span: Span,
    pub at_punct: Token![@],
    pub if_keyword: Token![if],
    pub branches: Vec<TmplIfBranch>,
    pub else_at_punct: Option<Token![@]>,
    pub else_keyword: Option<Token![else]>,
    pub else_body: Option<Box<Template>>,
}

#[doc = "A single branch of a `@if` or `@else if` directive."]
#[derive(Clone)]
#[cfg_attr(feature = "derives", derive(Debug))]
pub struct TmplIfBranch {
    pub span: Span,
    pub at_punct: Option<Token![@]>,
    pub else_keyword: Option<Token![else]>,
    pub if_keyword: Token![if],
    pub cond: TokenStream,
    pub body: Template,
}

impl Parse for TmplIfBranch {
    fn peek(cursor: Cursor<'_>) -> bool {
        <Token![if]>::peek(cursor)
    }

    fn parse(parser: &Parser) -> Result<Self, ParseError> {
        let if_keyword: Token![if] = <_ as Parse>::parse(parser)?;
        let span = if_keyword.span();
        let cond = parser.parse_group(Delim::Paren)?.to_token_stream();
        let body = Template::parse(&parser.parse_group(Delim::Brace)?)?;

        Ok(Self {
            span,
            at_punct: None,
            else_keyword: None,
            if_keyword,
            cond,
            body,
        })
    }

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

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

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

    fn parse(parser: &Parser) -> Result<Self, ParseError> {
        let at_punct: Token![@] = <_ as Parse>::parse(parser)?;
        let span = at_punct.span();
        let first: TmplIfBranch = <_ as Parse>::parse(parser)?;
        let mut branches = vec![first];
        let mut else_at_punct = None;
        let mut else_keyword = None;
        let mut else_body = None;

        while let Some(cursor) = <Token![@]>::skip(parser.cursor())
            && <Token![else]>::peek(cursor)
        {
            let at_punct = <_ as Parse>::parse(parser)?;
            let keyword = <_ as Parse>::parse(parser)?;

            if <Token![if]>::peek(parser.cursor()) {
                let mut branch: TmplIfBranch = <_ as Parse>::parse(parser)?;
                branch.at_punct = Some(at_punct);
                branch.else_keyword = Some(keyword);
                branches.push(branch);
            } else {
                else_at_punct = Some(at_punct);
                else_keyword = Some(keyword);
                else_body = Some(Box::new(Template::parse(&parser.parse_group(Delim::Brace)?)?));
                break;
            }
        }

        Ok(Self {
            span,
            at_punct,
            if_keyword: branches[0].if_keyword,
            branches,
            else_at_punct,
            else_keyword,
            else_body,
        })
    }

    fn skip(mut cursor: Cursor<'_>) -> Option<Cursor<'_>> {
        cursor = <Token![@]>::skip(cursor)?;
        cursor = TmplIfBranch::skip(cursor)?;

        while let Some(next) = <Token![@]>::skip(cursor)
            && <Token![else]>::peek(next)
        {
            cursor = <Token![else]>::skip(next)?;

            if <Token![if]>::peek(cursor) {
                cursor = TmplIfBranch::skip(cursor)?;
            } else {
                let inner = cursor.descend(Delim::Brace)?;
                let inner = Template::skip(inner)?;
                return inner.is_empty().then(|| cursor.offset(1));
            }
        }

        Some(cursor)
    }
}

impl ToTokens for TmplIf {
    fn to_tokens(&self, out: &mut TokenStream) {
        for (i, branch) in self.branches.iter().enumerate() {
            if i > 0 {
                branch.else_keyword.to_tokens(out);
            }

            branch.if_keyword.to_tokens(out);
            branch.cond.to_tokens(out);
            out.extend_one(TokenTree::Group(Group::new(Delim::Brace, branch.body.to_token_stream())));
        }

        if let Some(else_b) = &self.else_body {
            self.else_keyword.to_tokens(out);
            out.extend_one(TokenTree::Group(Group::new(Delim::Brace, else_b.to_token_stream())));
        }
    }
}