use std::collections::VecDeque;
use crate::{
error::InternalError,
source::{Chunk, Span},
};
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Token<'src> {
Start,
Content,
Newline { indent: &'src str },
Tag(Tag, &'src str),
SetDelimiter,
}
impl Token<'_> {
pub(crate) fn is_standalone(&self) -> bool {
match self {
Self::SetDelimiter
| Self::Tag(
Tag::Partial | Tag::Section | Tag::Inverted | Tag::CloseTag | Tag::Comment,
_,
) => true,
#[cfg(feature = "ext-dynamic-names")]
Self::Tag(Tag::DynPartial, _) => true,
#[cfg(feature = "ext-inheritance")]
Self::Tag(Tag::Parent, _) => true,
#[cfg(all(feature = "ext-dynamic-names", feature = "ext-inheritance"))]
Self::Tag(Tag::DynParent, _) => true,
_ => false,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Tag {
Variable,
RawVariable,
Section,
Inverted,
Partial,
#[cfg(feature = "ext-dynamic-names")]
DynPartial,
#[cfg(feature = "ext-inheritance")]
Block,
#[cfg(feature = "ext-inheritance")]
Parent,
#[cfg(all(feature = "ext-dynamic-names", feature = "ext-inheritance"))]
DynParent,
Comment,
CloseTag,
}
impl Tag {
pub(crate) fn is_partial(&self) -> bool {
match self {
Self::Partial => true,
#[cfg(feature = "ext-dynamic-names")]
Self::DynPartial => true,
#[cfg(feature = "ext-inheritance")]
Self::Parent => true,
#[cfg(all(feature = "ext-dynamic-names", feature = "ext-inheritance"))]
Self::DynParent => true,
_ => false,
}
}
#[cfg(feature = "ext-inheritance")]
pub(crate) fn is_open_tag(&self) -> bool {
match self {
Self::Section => true,
Self::Inverted => true,
#[cfg(feature = "ext-inheritance")]
Self::Parent => true,
#[cfg(feature = "ext-inheritance")]
Self::Block => true,
#[cfg(all(feature = "ext-dynamic-names", feature = "ext-inheritance"))]
Self::DynParent => true,
_ => false,
}
}
}
#[derive(Debug)]
pub(crate) struct Lexer<'src> {
source: &'src str,
offset: usize,
open_tag: &'src str,
close_tag: &'src str,
peeked: VecDeque<<Self as Iterator>::Item>,
}
impl<'src> Lexer<'src> {
pub(crate) fn new(source: &'src str) -> Self {
let mut whitespace_before = true;
let first_line_indent = source
.char_indices()
.take_while(|(_, c)| {
if !whitespace_before {
false
} else {
whitespace_before = char::is_whitespace(*c);
true
}
})
.last()
.map_or(0, |(i, _)| i);
Self {
source: &source[first_line_indent..],
offset: first_line_indent,
open_tag: "{{",
close_tag: "}}",
peeked: vec![Ok(Chunk::new(0..first_line_indent, Token::Start))].into(),
}
}
pub(crate) fn loc(&self) -> Span {
Span::from(self.offset..self.offset + 1)
}
pub fn peek(&mut self) -> Option<&<Self as Iterator>::Item> {
self.peek_nth(0)
}
pub fn peek_nth(&mut self, n: usize) -> Option<&<Self as Iterator>::Item> {
while self.peeked.len() <= n {
let next = self.force_next()?;
self.peeked.push_back(next);
}
Some(&self.peeked[n])
}
pub fn peek_token(&mut self) -> Option<&Token<'src>> {
self.peek_nth_token(0)
}
pub fn peek_nth_token(&mut self, n: usize) -> Option<&Token<'src>> {
Some(&self.peek_nth(n)?.as_ref().ok()?.data)
}
#[allow(dead_code)]
pub fn peek_mut(&mut self) -> Option<&mut <Self as Iterator>::Item> {
self.peek_nth_mut(0)
}
pub fn peek_nth_mut(&mut self, n: usize) -> Option<&mut <Self as Iterator>::Item> {
while self.peeked.len() <= n {
let next = self.force_next()?;
self.peeked.push_back(next);
}
Some(&mut self.peeked[n])
}
#[allow(dead_code)]
pub fn peek_token_mut(&mut self) -> Option<&mut Token<'src>> {
self.peek_nth_token_mut(0)
}
#[allow(dead_code)]
pub fn peek_nth_token_mut(&mut self, n: usize) -> Option<&mut Token<'src>> {
Some(&mut self.peek_nth_mut(n)?.as_mut().ok()?.data)
}
pub fn find_standalone(&mut self) -> Option<StandaloneReport<'_>> {
let mut i = 0;
while let Some(Ok(tk)) = self.peek_nth(i) {
match &tk.data {
Token::Newline { .. } => {
if i == 0 {
return None;
} else {
let span = &mut self.peek_nth_mut(i).unwrap().as_mut().unwrap().span;
return Some(StandaloneReport { span: Some(span) });
}
}
t if t.is_standalone() => (),
_ => return None,
}
i += 1;
}
if i > 0 {
Some(StandaloneReport { span: None })
} else {
None
}
}
fn consume_tag(&mut self) -> Option<<Self as Iterator>::Item> {
let Some(end) = &self.source[self.open_tag.len()..].find(self.close_tag) else {
self.source = "";
return Some(Err(InternalError::new(
"unclosed tag",
self.offset..self.offset + self.open_tag.len(),
)));
};
let end = end + self.open_tag.len();
let (mut tag, mut rest) = self.source.split_at(end + self.close_tag.len());
let mut content = &tag[self.open_tag.len()..tag.len() - self.close_tag.len()];
let mut content_chars = content.char_indices();
macro_rules! tag_kind {
($kind:expr) => {{
content = &content[1..];
$kind
}};
($kind:expr, dyn $dyn_kind:expr) => {{
#[allow(unused)]
if let Some((i, '*')) = content_chars.filter(|(_, c)| !c.is_whitespace()).next() {
cfg_select! {
feature = "ext-dynamic-names" => {
content = &content[i + 1..];
$dyn_kind
}
_ => {
let source = self.offset..self.offset + tag.len();
self.source = rest;
self.offset += tag.len();
return Some(Err(InternalError::new("dynamic names are disabled", source)));
}
}
} else {
tag_kind!($kind)
}
}};
}
let kind = match content_chars.next().map(|(_, c)| c) {
Some('{') => {
if self.close_tag == "}}" {
if rest.chars().next() != Some('}') {
let source = Span {
start: self.offset + content.len() + 2,
end: self.offset + tag.len(),
};
self.source = rest;
self.offset += tag.len();
return Some(Err(InternalError::new(
"expected a triple mustache",
source,
)));
}
(tag, rest) = self.source.split_at(end + 3);
content = &tag[self.open_tag.len()..tag.len() - 3];
} else if content.chars().last() != Some('}') {
let source = Span {
start: self.offset + content.len() + self.open_tag.len(),
end: self.offset + tag.len(),
};
self.source = rest;
self.offset += tag.len();
return Some(Err(InternalError::new(
"expected a triple mustache",
source,
)));
} else {
content = &content[..content.len() - 1]
}
content = &content[1..];
Tag::RawVariable
}
Some('=') => {
if content.chars().last() != Some('=') {
let source = Span {
start: self.offset + content.len() + self.open_tag.len(),
end: self.offset + tag.len(),
};
self.source = rest;
self.offset += tag.len();
return Some(Err(InternalError::new("expected an equals sign", source)));
}
content = &content[1..content.len() - 1].trim();
let Some((open, close)) = content.split_once(" ") else {
self.source = rest;
self.offset += tag.len();
return Some(Err(InternalError::new(
"expected a space to separate delimiters",
Span {
start: self.offset,
end: self.offset + tag.len(),
},
)));
};
self.open_tag = open.trim();
self.close_tag = close.trim();
let span = Span {
start: self.offset,
end: self.offset + tag.len(),
};
self.source = rest;
self.offset += tag.len();
return Some(Ok(Chunk::new(span, Token::SetDelimiter)));
}
Some('&') => tag_kind!(Tag::RawVariable),
Some('#') => tag_kind!(Tag::Section),
Some('^') => tag_kind!(Tag::Inverted),
Some('>') => tag_kind!(Tag::Partial, dyn Tag::DynPartial),
#[cfg(not(feature = "ext-inheritance"))]
Some('<') | Some('$') => {
let source = Span {
start: self.offset,
end: self.offset + tag.len(),
};
self.source = rest;
self.offset += tag.len();
return Some(Err(InternalError::new("inheritance is disabled", source)));
}
#[cfg(feature = "ext-inheritance")]
Some('<') => tag_kind!(Tag::Parent, dyn Tag::DynParent),
#[cfg(feature = "ext-inheritance")]
Some('$') => tag_kind!(Tag::Block),
Some('/') => tag_kind!(Tag::CloseTag),
Some('!') => tag_kind!(Tag::Comment),
_ => Tag::Variable,
};
let span = Span {
start: self.offset,
end: self.offset + tag.len(),
};
self.source = rest;
self.offset += tag.len();
Some(Ok(Chunk::new(span, Token::Tag(kind, content.trim()))))
}
fn consume_newline(&mut self) -> Option<<Self as Iterator>::Item> {
let mut whitespace_before = true;
let len = self
.source
.char_indices()
.take_while(|(i, c)| {
if !whitespace_before {
false
} else {
whitespace_before = (*c != '\n' || *i == 0) && char::is_whitespace(*c);
true
}
})
.last()?
.0;
let indent = if len > 1 { &self.source[1..len] } else { "" };
let len = len.max(1);
let span = Span {
start: self.offset,
end: self.offset + len,
};
self.offset += len;
self.source = &self.source[len..];
Some(Ok(Chunk::new(span, Token::Newline { indent })))
}
fn force_next(&mut self) -> Option<<Self as Iterator>::Item> {
if self.source.is_empty() {
return None;
}
if self.source.starts_with('\n') {
self.consume_newline()
} else if self.source.starts_with(self.open_tag) {
self.consume_tag()
} else {
let next = match (self.source.find(self.open_tag), self.source.find('\n')) {
(Some(a), Some(b)) => a.min(b),
(Some(next), None) | (None, Some(next)) => next,
(None, None) => self.source.len(),
};
let (content, rest) = self.source.split_at(next);
let span = Span {
start: self.offset,
end: self.offset + content.len(),
};
self.source = rest;
self.offset += content.len();
Some(Ok(Chunk::new(span, Token::Content)))
}
}
}
impl<'src> Iterator for Lexer<'src> {
type Item = Result<Chunk<Token<'src>>, InternalError>;
fn next(&mut self) -> Option<Self::Item> {
self.peeked.pop_front().or_else(|| self.force_next())
}
}
pub struct StandaloneReport<'a> {
pub span: Option<&'a mut Span>,
}
#[cfg(test)]
mod test {
use crate::{
error::InternalError,
lexer::{Lexer, Tag, Token},
source::Chunk,
};
fn check(src: &str, expected: &[Result<Chunk<Token>, InternalError>]) {
let lexer = Lexer::new(src);
assert_eq!(lexer.collect::<Vec<_>>(), expected);
}
#[test]
fn variable() {
check(
"Hello, {{subject}}",
&[
Ok(Chunk::new(0..0, Token::Start)),
Ok(Chunk::new(0..7, Token::Content)),
Ok(Chunk::new(7..18, Token::Tag(Tag::Variable, "subject"))),
],
);
}
#[test]
fn single_braces() {
check(
"Hello, {Mustache}",
&[
Ok(Chunk::new(0..0, Token::Start)),
Ok(Chunk::new(0..17, Token::Content)),
],
)
}
#[test]
fn triple_mustache() {
check(
"Hello, {{{subject}}}",
&[
Ok(Chunk::new(0..0, Token::Start)),
Ok(Chunk::new(0..7, Token::Content)),
Ok(Chunk::new(7..20, Token::Tag(Tag::RawVariable, "subject"))),
],
)
}
#[test]
fn triple_mustache_recovery() {
check(
"Hello, {{{subject}}!",
&[
Ok(Chunk::new(0..0, Token::Start)),
Ok(Chunk::new(0..7, Token::Content)),
Err(InternalError::new("expected a triple mustache", 17..19)),
Ok(Chunk::new(19..20, Token::Content)),
],
)
}
#[test]
fn set_delimiter() {
check(
"{{=| |=}} |value|",
&[
Ok(Chunk::new(0..0, Token::Start)),
Ok(Chunk::new(0..9, Token::SetDelimiter)),
Ok(Chunk::new(9..10, Token::Content)),
Ok(Chunk::new(10..17, Token::Tag(Tag::Variable, "value"))),
],
)
}
#[test]
fn set_delimiter_triple() {
check(
"{{=| |=}} |{value}|",
&[
Ok(Chunk::new(0..0, Token::Start)),
Ok(Chunk::new(0..9, Token::SetDelimiter)),
Ok(Chunk::new(9..10, Token::Content)),
Ok(Chunk::new(10..19, Token::Tag(Tag::RawVariable, "value"))),
],
)
}
#[test]
fn newlines() {
check(
"Before
After",
&[
Ok(Chunk::new(0..0, Token::Start)),
Ok(Chunk::new(0..6, Token::Content)),
Ok(Chunk::new(
6..19,
Token::Newline {
indent: " ",
},
)),
Ok(Chunk::new(19..24, Token::Content)),
],
);
}
}