efx-core 1.0.0

Core parser and AST for EFx proc-macro
Documentation
use crate::parser::error::ParseError;
use crate::parser::lexer::Lexer;
use crate::parser::nodes::{Element, Interpolation, Node, Text, Attr};
use crate::parser::PResult;
use crate::parser::span_range::SpanRange;
use crate::parser::tok::Tok;

pub struct Parser<'a> {
    toks: Vec<(Tok, SpanRange)>,
    i: usize,
    _src: &'a str,
}

impl<'a> Parser<'a> {
    pub fn new(src: &'a str) -> Self {
        Self { toks: Lexer::new(src).all(), i: 0, _src: src }
    }

    pub fn parse_nodes(&mut self) -> PResult<Vec<Node>> {
        let mut nodes = Vec::new();
        while !self.eof() {
            match self.cur() {
                Some((Tok::LAngle, _)) => nodes.push(self.parse_element()?),
                Some((Tok::LBrace, _)) => nodes.push(self.parse_i11n()?),
                Some((Tok::Text(_), _)) => nodes.push(self.parse_text()?),
                Some((tok, sp)) => return Err(ParseError { msg: format!("unexpected token: {:?}", tok), span: *sp }),
                None => break,
            }
        }

        Ok(nodes)
    }

    fn eof(&self) -> bool { self.i >= self.toks.len() }
    fn cur(&self) -> Option<&(Tok, SpanRange)> { self.toks.get(self.i) }
    fn bump(&mut self) { self.i += 1; }

    fn byte_end(&self) -> usize {
        self.toks.last().map(|t| (t.1.end).0).unwrap_or(0)
    }

    fn expect(&mut self, want: Tok) -> PResult<SpanRange> {
        if let Some((tok, sp)) = self.cur().cloned() {
            if tok == want {
                self.bump();
                return Ok(sp);
            }
        }

        let span = self.cur()
            .map(|t| t.1)
            .unwrap_or_else(|| SpanRange::new(self.byte_end(), self.byte_end()));
        Err(ParseError { msg: format!("expected {:?}", want), span })
    }

    fn parse_name(&mut self) -> PResult<(String, SpanRange)> {
        match self.cur().cloned() {
            Some((Tok::Text(s), sp)) => {
                let mut name_end = s.len();
                for (idx, ch) in s.char_indices() {
                    if ch.is_whitespace() || matches!(ch, '<' | '>' | '/' | '{' | '}') {
                        name_end = idx;
                        break;
                    }
                }
                if name_end == 0 {
                    return Err(ParseError { msg: "invalid tag name".into(), span: sp });
                }

                let name = s[..name_end].to_string();

                self.bump();

                let remainder = &s[name_end..];
                if !remainder.is_empty() {
                    let rem_start = sp.start.0 + name_end;
                    // Insert directly at the current position of self.i - the next call to cur() will see it
                    self.toks.insert(self.i, (Tok::Text(remainder.to_string()), SpanRange::new(rem_start, sp.end.0)));
                }

                let name_span = SpanRange::new(sp.start.0, sp.start.0 + name_end);
                Ok((name, name_span))
            }
            Some((_tok, sp)) => Err(ParseError { msg: "expected tag name".into(), span: sp }),
            None => Err(ParseError {
                msg: "unexpected EOF when reading tag name".into(),
                span: SpanRange::new(self.byte_end(), self.byte_end()),
            }),
        }
    }

    fn parse_element(&mut self) -> PResult<Node> {
        let start = self.expect(Tok::LAngle)?.start;
        let (name, _nsp) = self.parse_name()?;

        // Collect the "tail" after the name until '>' or '/>'
        let mut attrs_src = String::new();
        let mut attrs_span_start = None;
        let mut attrs_span_end = None;

        loop {
            match self.cur() {
                Some((Tok::Slash, _)) => {
                    return if let Some((Tok::RAngle, sp_gt)) = self.toks.get(self.i + 1).cloned() {
                        self.bump(); // '/'
                        self.bump(); // '>'

                        let end = sp_gt.end;
                        let attrs = parse_attrs_from_buffer(&attrs_src, SpanRange {
                            start: attrs_span_start.unwrap_or(start),
                            end: attrs_span_end.unwrap_or(start),
                        })?;
                        Ok(Node::Element(Element {
                            name,
                            attrs,
                            children: vec![],
                            span: SpanRange { start, end }
                        }))
                    } else {
                        let sp = self.cur().unwrap().1;
                        Err(ParseError { msg: "unexpected '/' in tag head".into(), span: sp })
                    }
                }
                Some((Tok::RAngle, sp_gt)) => {
                    // Closed the opening tag: now parse the children or end empty
                    let end_open = *sp_gt;
                    self.bump();

                    let attrs = parse_attrs_from_buffer(&attrs_src, SpanRange {
                        start: attrs_span_start.unwrap_or(start),
                        end: attrs_span_end.unwrap_or(end_open.end),
                    })?;

                    let mut children = Vec::new();
                    loop {
                        match self.cur() {
                            Some((Tok::LAngle, _)) => {
                                if let Some((Tok::Slash, _)) = self.toks.get(self.i + 1) {
                                    // </name>
                                    self.bump(); // '<'
                                    self.bump(); // '/'
                                    let (close_name, sp_name) = self.parse_name()?;
                                    if close_name != name {
                                        return Err(ParseError { msg: format!("unmatched closing tag: expected </{}>", name), span: sp_name });
                                    }
                                    let end_angle = self.expect(Tok::RAngle)?;
                                    return Ok(Node::Element(Element { name, attrs, children, span: SpanRange { start, end: end_angle.end } }));
                                } else {
                                    children.push(self.parse_element()?);
                                }
                            }
                            Some((Tok::LBrace, _)) => children.push(self.parse_i11n()?),
                            Some((Tok::Text(_), _)) => children.push(self.parse_text()?),
                            Some((tok, sp)) => return Err(ParseError { msg: format!("unexpected token in element body: {:?}", tok), span: *sp }),
                            None => return Err(ParseError { msg: "unexpected EOF in element body".into(), span: SpanRange::new(self.byte_end(), self.byte_end()) }),
                        }
                    }
                }
                Some((Tok::Text(s), sp)) => {
                    // There can be spaces and attributes between the name and '>'
                    if attrs_span_start.is_none() { attrs_span_start = Some(sp.start); }
                    attrs_span_end = Some(sp.end);
                    attrs_src.push_str(s);
                    self.bump();
                }
                Some((tok, sp)) => {
                    return Err(ParseError { msg: format!("unexpected token in tag head: {:?}", tok), span: *sp });
                }
                None => {
                    return Err(ParseError { msg: "unexpected EOF in tag head".into(), span: SpanRange::new(self.byte_end(), self.byte_end()) });
                }
            }
        }
    }

    fn parse_text(&mut self) -> PResult<Node> {
        let mut start = None;
        let mut end = None;
        let mut buf = String::new();

        while let Some((Tok::Text(s), sp)) = self.cur().cloned() {
            if start.is_none() { start = Some(sp.start); }
            end = Some(sp.end);
            self.bump();
            buf.push_str(&s);
        }

        if let (Some(st), Some(en)) = (start, end) {
            Ok(Node::Text(Text { value: buf, span: SpanRange { start: st, end: en } }))
        } else {
            let span = self.cur()
                .map(|t| t.1)
                .unwrap_or_else(|| SpanRange::new(self.byte_end(), self.byte_end()));
            Err(ParseError { msg: "expected text".into(), span })
        }
    }

    fn parse_i11n(&mut self) -> PResult<Node> {
        let start = self.expect(Tok::LBrace)?.start;
        // Read everything up to a single RBrace (}} was already converted to Text("}"))
        let mut expr_src = String::new();

        loop {
            match self.cur().cloned() {
                Some((Tok::RBrace, sp)) => {
                    self.bump();
                    return Ok(Node::I11n(Interpolation { expr_src, span: SpanRange { start, end: sp.end } }));
                }
                Some((Tok::Text(s), _)) => { expr_src.push_str(&s); self.bump(); }
                Some((Tok::LBrace, sp)) => {
                    // Nested { } are not supported (let's simplify the first version)
                    return Err(ParseError { msg: "nested '{' in interpolation".into(), span: sp });
                }
                Some((_tok, sp)) => return Err(ParseError { msg: "unexpected token inside { }".into(), span: sp }),
                None => return Err(ParseError { msg: "unexpected EOF in { }".into(), span: SpanRange::new(self.byte_end(), self.byte_end()) }),
            }
        }
    }
}

fn parse_attrs_from_buffer(buf: &str, span: SpanRange) -> PResult<Vec<Attr>> {
    let mut attrs = Vec::new();
    let bytes = buf.as_bytes();
    let mut i = 0usize;

    let skip_ws = |b: &[u8], mut j: usize| { while j < b.len() && b[j].is_ascii_whitespace() { j += 1; } j };
    let is_name_char = |c: u8| -> bool { c.is_ascii_alphanumeric() || c == b'_' || c == b'-' || c == b':' };

    i = skip_ws(bytes, i);
    while i < bytes.len() {
        let name_start = i;
        while i < bytes.len() && is_name_char(bytes[i]) { i += 1; }
        if i == name_start {
            // no name - maybe only spaces are left
            i = skip_ws(bytes, i);
            if i >= bytes.len() { break; }

            return Err(ParseError { msg: "invalid attribute name".into(), span });
        }

        let name = &buf[name_start..i];
        i = skip_ws(bytes, i);
        if i >= bytes.len() || bytes[i] != b'=' {
            return Err(ParseError { msg: format!("expected '=' after attribute '{}'", name), span });
        }
        i += 1;
        i = skip_ws(bytes, i);

        if i >= bytes.len() || (bytes[i] != b'"' && bytes[i] != b'\'') {
            return Err(ParseError { msg: format!("expected quote after '=' in attribute '{}'", name), span });
        }
        let quote = bytes[i];
        i += 1;
        let val_start = i;
        while i < bytes.len() && bytes[i] != quote { i += 1; }
        if i >= bytes.len() {
            return Err(ParseError { msg: format!("unterminated quoted value for attribute '{}'", name), span });
        }
        let value = &buf[val_start..i];
        i += 1; // closing quote

        attrs.push(Attr {
            name: name.to_string(),
            value: value.to_string(),
            span,
        });

        i = skip_ws(bytes, i);
    }

    Ok(attrs)
}