#[cfg(feature = "ext-inheritance")]
use std::collections::HashMap;
use crate::{
error::InternalError,
lexer::{Lexer, Tag, Token},
source::{Chunk, Span},
};
#[derive(Debug, Clone)]
pub struct Template<'src> {
pub(crate) root: Vec<Node<'src>>,
pub(crate) groups: Vec<Vec<Node<'src>>>,
}
impl<'src> Template<'src> {
pub fn parse(source: &'src str) -> Result<Self, InternalError> {
let mut p = Parser {
lexer: Lexer::new(source),
source,
groups: Vec::new(),
prev_indentation: "",
cut_indentation: 0,
};
let mut root = Vec::new();
while p.lexer.peek().is_some() {
let [fst, snd] = p.parse_node()?;
if let Some(n) = fst {
root.push(n);
}
if let Some(n) = snd {
root.push(n);
}
}
Ok(Self {
root,
groups: p.groups,
})
}
}
struct Parser<'src> {
lexer: Lexer<'src>,
source: &'src str,
groups: Vec<Vec<Node<'src>>>,
prev_indentation: &'src str,
cut_indentation: usize,
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub(crate) enum Node<'src> {
Content(&'src str),
Newline,
Variable {
key: &'src str,
raw: bool,
},
Partial {
name: &'src str,
source: Span,
indent: &'src str,
#[cfg(feature = "ext-dynamic-names")]
dynamic: bool,
},
Section {
key: &'src str,
group_id: usize,
#[cfg(feature = "ext-lambdas")]
span_inside: Span,
},
Inverted {
key: &'src str,
group_id: usize,
},
#[cfg(feature = "ext-inheritance")]
Parent {
name: &'src str,
parameters: HashMap<&'src str, usize>,
indent: &'src str,
source: Span,
#[cfg(feature = "ext-dynamic-names")]
dynamic: bool,
},
#[cfg(feature = "ext-inheritance")]
Block {
name: &'src str,
indent: &'src str,
group_id: usize,
},
}
impl<'src> Node<'src> {
pub(crate) fn get_indent(&self) -> Option<&'src str> {
match self {
Node::Partial { indent, .. } => Some(indent),
#[cfg(feature = "ext-inheritance")]
Node::Parent { indent, .. } => Some(indent),
#[cfg(feature = "ext-inheritance")]
Node::Block { indent, .. } => Some(indent),
_ => None,
}
}
}
impl<'src> Parser<'src> {
fn parse_node(&mut self) -> Result<[Option<Node<'src>>; 2], InternalError> {
let Some(t) = self.lexer.next() else {
return Err(InternalError::new("expected a node", self.lexer.loc()));
};
let t = t?;
match t.data {
Token::Start => {
#[cfg(feature = "inline-indentation")]
{
self.prev_indentation = &self.source[t.span.start..t.span.end];
}
if self.lexer.peek_token().is_some_and(Token::is_standalone)
&& let Some(standalone) = self.lexer.find_standalone()
{
#[cfg(not(feature = "inline-indentation"))]
{
self.prev_indentation = &self.source[t.span.start..t.span.end];
}
if let Some(span) = standalone.span {
span.start += 1;
}
if let Some(Token::Tag(tag, _)) = self.lexer.peek_token()
&& tag.is_partial()
{
self.parse_content(t.span).map(|c| [c, None])
} else {
Ok([const { None }; 2])
}
} else {
self.parse_content(t.span).map(|c| [c, None])
}
}
Token::Content => self.parse_content(t.span).map(|c| [c, None]),
Token::Newline { indent } => {
#[cfg(feature = "inline-indentation")]
{
self.prev_indentation = indent;
}
let mut start = t.span.start + self.cut_indentation;
let nl_node = if t.span.end > t.span.start + indent.len() {
start += 1;
Some(Node::Newline)
} else {
None
};
if let Some(standalone) = self.lexer.find_standalone() {
#[cfg(not(feature = "inline-indentation"))]
{
self.prev_indentation = indent;
}
if let Some(span) = standalone.span {
span.start += 1;
}
Ok([nl_node, None])
} else {
#[cfg(not(feature = "inline-indentation"))]
cfg_select! {
feature = "ext-inheritance" => {
if matches!(self.lexer.peek_token(), Some(Token::Tag(Tag::Block, _))) {
self.prev_indentation = indent;
} else {
self.prev_indentation = "";
}
}
_ => {
self.prev_indentation = "";
}
}
self.parse_content(Span::from(start..t.span.end))
.map(|c| [nl_node, c])
}
}
Token::Tag(tag, content) => Ok([
Some(match tag {
Tag::Variable => Node::Variable {
key: content,
raw: false,
},
Tag::RawVariable => Node::Variable {
key: content,
raw: true,
},
Tag::Section => {
#[cfg(feature = "ext-lambdas")]
let (group, span_inside) = self.parse_group_to_span(t.span, content)?;
#[cfg(not(feature = "ext-lambdas"))]
let group = self.parse_group(t.span, content)?;
let group_id = self.groups.len();
self.groups.push(group);
Node::Section {
key: content,
group_id,
#[cfg(feature = "ext-lambdas")]
span_inside,
}
}
Tag::Inverted => {
let group = self.parse_group(t.span, content)?;
let group_id = self.groups.len();
self.groups.push(group);
Node::Inverted {
key: content,
group_id,
}
}
Tag::Partial => Node::Partial {
name: content,
source: t.span,
indent: self.prev_indentation,
#[cfg(feature = "ext-dynamic-names")]
dynamic: false,
},
#[cfg(feature = "ext-dynamic-names")]
Tag::DynPartial => Node::Partial {
name: content,
source: t.span,
indent: self.prev_indentation,
dynamic: true,
},
#[cfg(feature = "ext-inheritance")]
Tag::Block => {
use std::mem;
let (cut_indentation, block_indent) = self.cut_indentation()?;
let old_ci =
mem::replace(&mut self.cut_indentation, cut_indentation.unwrap_or(0));
let group = self.parse_group(t.span, content)?;
self.cut_indentation = old_ci;
let group_id = self.groups.len();
self.groups.push(group);
let indent = if cut_indentation.is_some() {
block_indent
} else {
self.prev_indentation
};
self.prev_indentation = "";
Node::Block {
name: content,
indent,
group_id,
}
}
#[cfg(feature = "ext-inheritance")]
Tag::Parent => Node::Parent {
name: content,
parameters: self.parse_parent(t.span, content)?,
indent: self.prev_indentation,
source: t.span,
#[cfg(feature = "ext-dynamic-names")]
dynamic: false,
},
#[cfg(all(feature = "ext-dynamic-names", feature = "ext-inheritance"))]
Tag::DynParent => Node::Parent {
name: content,
parameters: self.parse_parent(t.span, content)?,
indent: self.prev_indentation,
source: t.span,
dynamic: true,
},
Tag::Comment => return Ok([None, None]),
Tag::CloseTag => {
return Err(InternalError::new("disconnected closing tag", t.span));
}
}),
None,
]),
Token::SetDelimiter => Ok([None, None]),
}
}
#[cfg(feature = "ext-inheritance")]
fn cut_indentation(&mut self) -> Result<(Option<usize>, &'src str), InternalError> {
let mut cut_indentation = None::<usize>;
let mut use_tabs = None::<bool>;
let mut tag_level = 0;
let mut block_indent = self.prev_indentation;
let mut i = 0_usize;
while let Some(Ok(Chunk { data: tk, span })) = self.lexer.peek_nth(i) {
match tk {
Token::Newline { indent } => {
let span = *span;
let indent = *indent;
if tag_level == 0
&& matches!(
self.lexer.peek_nth_token(i + 1),
Some(Token::Tag(Tag::CloseTag, _))
)
{
break;
}
if indent.contains(" ") && indent.contains("\t") {
return Err(InternalError::new(
"tabs and spaces may not be used in the same block for indentation",
span,
));
} else if let Some(use_tabs) = use_tabs {
if indent.contains("\t") != use_tabs {
return Err(InternalError::new(
"tabs and spaces may not be used in the same block for indentation",
span,
));
}
} else if indent.contains(" ") {
use_tabs = Some(false);
} else if indent.contains("\t") {
use_tabs = Some(true);
}
let len = indent.len();
let ci = cut_indentation.map_or(len, |o| o.min(len));
block_indent = &indent[0..ci.saturating_sub(self.cut_indentation)];
cut_indentation = Some(ci);
}
Token::Tag(t, _) => {
if *t == Tag::CloseTag {
if tag_level == 0 {
break;
} else {
tag_level -= 1;
}
} else if t.is_open_tag() {
tag_level += 1;
}
}
_ => (),
}
i += 1;
}
Ok((cut_indentation, block_indent))
}
fn parse_content(&mut self, t: Span) -> Result<Option<Node<'src>>, InternalError> {
let mut end = t.end;
loop {
let t = match self.lexer.peek_token() {
Some(Token::Tag(_, _))
| Some(Token::SetDelimiter)
| Some(Token::Newline { .. })
| None => break,
Some(Token::Start | Token::Content) => self.lexer.next().unwrap(),
};
end = t?.span.end;
}
Ok(if end > t.start {
Some(Node::Content(&self.source[t.start..end]))
} else {
None
})
}
fn parse_group(
&mut self,
open_tag: Span,
expected_close_tag: &str,
) -> Result<Vec<Node<'src>>, InternalError> {
self.parse_group_to_span(open_tag, expected_close_tag)
.map(|(v, _)| v)
}
fn parse_group_to_span(
&mut self,
open_tag: Span,
expected_close_tag: &str,
) -> Result<(Vec<Node<'src>>, Span), InternalError> {
let mut g = Vec::new();
while !matches!(
self.lexer.peek_token(),
Some(Token::Tag(Tag::CloseTag, _)) | None
) {
let [fst, snd] = self.parse_node()?;
if let Some(n) = fst {
g.push(n);
}
if let Some(n) = snd {
g.push(n);
}
}
let Some(close_tag) = self.lexer.next() else {
return Err(InternalError::new("unclosed group", open_tag));
};
let close_tag = close_tag?;
match close_tag.data {
Token::Tag(Tag::CloseTag, name) if name == expected_close_tag => (),
Token::Tag(Tag::CloseTag, name) => {
return Err(InternalError::new(
format!("expected '{{{{/{expected_close_tag}}}}}', but found {{{{/{name}}}}}"),
close_tag.span,
));
}
_ => return Err(InternalError::new("unclosed group", open_tag)),
}
Ok((
g,
Span {
start: open_tag.end,
end: close_tag.span.start,
},
))
}
#[cfg(feature = "ext-inheritance")]
fn parse_parent(
&mut self,
open_tag: Span,
expected_close_tag: &str,
) -> Result<HashMap<&'src str, usize>, InternalError> {
let mut params = HashMap::new();
let close_tag = loop {
match self.lexer.next() {
Some(Ok(t)) => match t.data {
Token::Tag(Tag::Block, name) => {
if let Some(Ok(Chunk {
data: Token::Newline { .. },
span,
})) = self.lexer.peek_mut()
{
span.start += 1;
}
use std::mem;
let (cut_indentation, _) = self.cut_indentation()?;
let old_ci =
mem::replace(&mut self.cut_indentation, cut_indentation.unwrap_or(0));
let group = self.parse_group(t.span, name)?;
self.cut_indentation = old_ci;
params.insert(name, self.groups.len());
self.groups.push(group);
}
Token::Tag(Tag::CloseTag, _) => break t,
Token::Newline { .. } => {
if let Some(standalone) = self.lexer.find_standalone() {
if let Some(span) = standalone.span {
span.start += 1;
}
}
}
_ => (),
},
Some(Err(e)) => return Err(e),
None => return Err(InternalError::new("unclosed parent", open_tag)),
}
};
match close_tag.data {
Token::Tag(Tag::CloseTag, name) if name == expected_close_tag => (),
Token::Tag(Tag::CloseTag, name) => {
return Err(InternalError::new(
format!("expected '{{{{/{expected_close_tag}}}}}', but found {{{{/{name}}}}}"),
close_tag.span,
));
}
_ => return Err(InternalError::new("unclosed parent", open_tag)),
}
Ok(params)
}
}
#[cfg(test)]
mod test {
use crate::parser::{Node, Template};
#[cfg(feature = "ext-lambdas")]
use crate::source::Span;
fn check(src: &str, expected: &[Node<'static>]) {
let template = Template::parse(src).unwrap();
assert_eq!(template.root, expected);
}
fn check_groups(src: &str, expected: &[Node<'static>], groups: &[Vec<Node<'static>>]) {
let template = Template::parse(src).unwrap();
assert_eq!(template.root, expected);
assert_eq!(template.groups, groups)
}
#[test]
fn variable() {
check(
"Hello, {{subject}}",
&[
Node::Content("Hello, "),
Node::Variable {
key: "subject",
raw: false,
},
],
)
}
#[test]
fn section() {
check_groups(
"{{#blog}}
hello
{{/blog}}",
&[Node::Section {
key: "blog",
group_id: 0,
#[cfg(feature = "ext-lambdas")]
span_inside: Span::from(9..44),
}],
&[vec![Node::Content(" hello"), Node::Newline]],
);
}
}