moxy-template 0.1.0

Rust quasi-quoting and token templating with explicit interpolation syntax and built-in template control flow
Documentation
use moxy_token::parser::{ParseError, ParseStream};
use moxy_token::{Delim, Group, Ident, Parse, Span, TokenStream, TokenTree};

#[doc = "A parsed `paste!` body: a token tree where each `{{ ... }}` marker is collapsed to one identifier."]
#[derive(Debug, Clone)]
pub struct Paste {
    nodes: Vec<PasteNode>,
}

#[derive(Debug, Clone)]
enum PasteNode {
    Verbatim(TokenTree),
    Group(Delim, Vec<Self>),
    Splice(Span, TokenStream),
}

impl Parse for Paste {
    fn parse(stream: &mut ParseStream) -> Result<Self, ParseError> {
        Ok(Self {
            nodes: parse_nodes(stream)?,
        })
    }
}

impl Paste {
    pub fn expand(&self) -> TokenStream {
        let mut out = TokenStream::new();

        for node in &self.nodes {
            match node.expand() {
                Ok(stream) => out.extend(stream),
                Err(e) => return e.to_compile_error(),
            }
        }

        out
    }
}

fn parse_nodes(stream: &mut ParseStream) -> Result<Vec<PasteNode>, ParseError> {
    let mut nodes = Vec::new();

    while let Some(tt) = stream.curr() {
        match tt {
            TokenTree::Group(g) if is_marker(g) => {
                let span = g.span().into();
                let outer = stream.parse_group(Delim::Brace)?;
                let mut outer_ps = outer.parse();
                let inner = outer_ps.parse_group(Delim::Brace)?;
                nodes.push(PasteNode::Splice(span, inner));
            }
            TokenTree::Group(g) => {
                let delim = g.delim();
                let body = g.stream();
                stream.advance();
                nodes.push(PasteNode::Group(delim, parse_nodes(&mut body.parse())?));
            }
            _ => nodes.push(PasteNode::Verbatim(stream.advance().unwrap().clone())),
        }
    }

    Ok(nodes)
}

fn is_marker(g: &Group) -> bool {
    if g.delim() != Delim::Brace {
        return false;
    }

    let inner = g.stream();
    inner.len() == 1 && matches!(inner.get(0), Some(TokenTree::Group(ig)) if ig.delim() == Delim::Brace)
}

impl PasteNode {
    fn expand(&self) -> Result<TokenStream, ParseError> {
        match self {
            Self::Verbatim(tt) => Ok(TokenStream::from(vec![tt.clone()])),
            Self::Group(delim, kids) => {
                let mut body = TokenStream::new();

                for kid in kids {
                    body.extend(kid.expand()?);
                }

                Ok(TokenStream::from(vec![Group::new(*delim, body).to_token_tree()]))
            }
            Self::Splice(span, inner) => {
                let mut text = String::new();

                for tt in inner.iter() {
                    text.push_str(&segment_text(tt));
                }

                match Ident::lex(&text) {
                    Ok(id) => Ok(TokenStream::from(vec![id.with_span(*span).into_token_tree()])),
                    Err(e) => Err(e.into()),
                }
            }
        }
    }
}

fn segment_text(tt: &TokenTree) -> String {
    match tt {
        TokenTree::Ident(v) => v.text().to_string(),
        TokenTree::Keyword(v) => v.as_str().to_string(),
        TokenTree::Literal(v) => unquote(v.repr()).to_string(),
        other => other.to_string(),
    }
}

fn unquote(repr: &str) -> &str {
    let bytes = repr.as_bytes();

    match (bytes.first(), bytes.last()) {
        (Some(b'"'), Some(b'"')) | (Some(b'\''), Some(b'\'')) if repr.len() >= 2 => &repr[1..repr.len() - 1],
        _ => repr,
    }
}