#[path = "grammar_assign.rs"]
mod grammar_assign;
#[path = "grammar_block.rs"]
mod grammar_block;
#[path = "grammar_control.rs"]
mod grammar_control;
#[path = "grammar_directive.rs"]
mod grammar_directive;
#[cfg(test)]
#[path = "grammar_directive_tests.rs"]
mod grammar_directive_tests;
#[path = "grammar_element_helpers.rs"]
mod grammar_element_helpers;
#[path = "grammar_expression.rs"]
mod grammar_expression;
#[path = "grammar_expression_args.rs"]
mod grammar_expression_args;
#[path = "grammar_helpers.rs"]
mod grammar_helpers;
#[cfg(test)]
#[path = "grammar_misc_tests.rs"]
mod grammar_misc_tests;
#[path = "grammar_stripping.rs"]
mod grammar_stripping;
#[cfg(test)]
#[path = "grammar_tests.rs"]
mod grammar_tests;
use crate::core::{Element, ElementKind, ExprKind, MacroDef};
use crate::error::{Result, TemplateError};
use crate::parser::lexer::{ExprCtx, Lexer, TagOpen, TagSyntax, TextStop, Tok};
use crate::template::{Configuration, Template};
use std::collections::HashMap;
use std::rc::Rc;
use grammar_element_helpers::{is_non_outputting, is_ws, sync_macro_defs};
use grammar_helpers::{header_bool, tok_desc, BlockStop, IterCtx};
pub fn parse(cfg: &Rc<Configuration>, name: &str, text: &str) -> Result<Template> {
let mut parser = Parser::new(cfg, name, text);
parser.parse_template()
}
pub fn parse_expression(cfg: &Rc<Configuration>, src: &str) -> Result<crate::core::Expr> {
let wrapped = format!("${{{src}}}");
let t = parse(cfg, "eval", &wrapped)?;
match t.root.first().map(|e| &e.kind) {
Some(ElementKind::Interpolation { expr, .. }) => Ok(expr.clone()),
_ => Err(crate::error::TemplateError::misc(format!(
"Failed to parse the expression: {src:?}"
))),
}
}
const END_TAG_NAMES: &[&str] = &[
"foreach",
"list",
"sep",
"items",
"switch",
"if",
"compress",
"macro",
"function",
"transform",
"escape",
"noescape",
"assign",
"global",
"local",
"attempt",
"recover",
"outputformat",
"autoesc",
"noautoesc",
"trim",
];
const SETTING_NAMES: &[&str] = &[
"boolean_format",
"c_format",
"classic_compatible",
"date_format",
"datetime_format",
"locale",
"number_format",
"output_encoding",
"sql_date_and_time_time_zone",
"time_format",
"time_zone",
"url_escaping_charset",
];
const EXPRESSION_START_PATTERNS: &[&str] = &[
"<STRING_LITERAL>",
"<RAW_STRING>",
"\"false\"",
"\"true\"",
"<INTEGER>",
"<DECIMAL>",
"\".\"",
"\"+\"",
"\"-\"",
"\"!\"",
"\"[\"",
"\"(\"",
"\"{\"",
"<ID>",
];
const MIXED_CONTENT_PATTERNS: &[&str] = &[
"<ATTEMPT>",
"<IF>",
"<ELSE_IF>",
"<LIST>",
"<ITEMS>",
"<SEP>",
"<FOREACH>",
"<SWITCH>",
"<ASSIGN>",
"<GLOBALASSIGN>",
"<LOCALASSIGN>",
"<_INCLUDE>",
"<IMPORT>",
"<FUNCTION>",
"<MACRO>",
"<TRANSFORM>",
"<VISIT>",
"<STOP>",
"<RETURN>",
"<CALL>",
"<SETTING>",
"<OUTPUTFORMAT>",
"<AUTOESC>",
"<NOAUTOESC>",
"<COMPRESS>",
"<COMMENT>",
"<TERSE_COMMENT>",
"<NOPARSE>",
"<END_IF>",
"<ELSE>",
"<BREAK>",
"<CONTINUE>",
"<SIMPLE_RETURN>",
"<HALT>",
"<FLUSH>",
"<TRIM>",
"<LTRIM>",
"<RTRIM>",
"<NOTRIM>",
"<SIMPLE_NESTED>",
"<NESTED>",
"<SIMPLE_RECURSE>",
"<RECURSE>",
"<FALLBACK>",
"<ESCAPE>",
"<NOESCAPE>",
"<UNIFIED_CALL>",
"<STATIC_TEXT_WS>",
"<STATIC_TEXT_NON_WS>",
"<STATIC_TEXT_FALSE_ALARM>",
"\"${\"",
"\"#{\"",
"\"[=\"",
];
const ROOT_MIXED_PATTERNS: &[&str] = &[
"<EOF>",
"<ATTEMPT>",
"<IF>",
"<LIST>",
"<ITEMS>",
"<SEP>",
"<FOREACH>",
"<SWITCH>",
"<ASSIGN>",
"<GLOBALASSIGN>",
"<LOCALASSIGN>",
"<_INCLUDE>",
"<IMPORT>",
"<FUNCTION>",
"<MACRO>",
"<TRANSFORM>",
"<VISIT>",
"<STOP>",
"<RETURN>",
"<CALL>",
"<SETTING>",
"<OUTPUTFORMAT>",
"<AUTOESC>",
"<NOAUTOESC>",
"<COMPRESS>",
"<COMMENT>",
"<TERSE_COMMENT>",
"<NOPARSE>",
"<BREAK>",
"<CONTINUE>",
"<SIMPLE_RETURN>",
"<HALT>",
"<FLUSH>",
"<TRIM>",
"<LTRIM>",
"<RTRIM>",
"<NOTRIM>",
"<SIMPLE_NESTED>",
"<NESTED>",
"<SIMPLE_RECURSE>",
"<RECURSE>",
"<FALLBACK>",
"<ESCAPE>",
"<NOESCAPE>",
"<UNIFIED_CALL>",
"<STATIC_TEXT_WS>",
"<STATIC_TEXT_NON_WS>",
"<STATIC_TEXT_FALSE_ALARM>",
"\"${\"",
"\"#{\"",
"\"[=\"",
];
const NOPARAM_DIRECTIVES: &[&str] = &[
"attempt",
"recover",
"sep",
"compress",
"comment",
"default",
"trim",
"autoesc",
"noautoesc",
"noescape",
"noparse",
"else",
"break",
"continue",
"flush",
"t",
"lt",
"rt",
"nt",
"fallback",
"nested",
"recurse",
"return",
"stop",
"ftl",
];
const SELF_CLOSE_DIRECTIVES: &[&str] = &[
"else", "break", "continue", "flush", "t", "lt", "rt", "nt", "fallback", "nested", "recurse",
"return", "stop",
];
struct Parser<'a> {
lexer: Lexer,
cfg: &'a Rc<Configuration>,
name: String,
macros: HashMap<String, MacroDef>,
encoding: Option<String>,
ns_prefixes: HashMap<String, String>,
strip_ws: bool,
in_macro: u32,
in_function: u32,
loop_nesting: u32,
continue_nesting: u32,
iter_stack: Vec<IterCtx>,
ctx: ExprCtx,
buf: Vec<(Tok, u32, u32, u32, u32)>,
last_tok_end: (u32, u32),
tag_pos: (u32, u32),
named_arg_depth: u32,
pending_stop: Option<BlockStop>,
}
impl<'a> Parser<'a> {
fn new(cfg: &'a Rc<Configuration>, name: &str, text: &str) -> Self {
Parser {
lexer: Lexer::new(name, text, cfg.settings.strict_syntax),
cfg,
name: name.to_string(),
macros: HashMap::new(),
encoding: None,
ns_prefixes: HashMap::new(),
strip_ws: cfg.settings.whitespace_stripping,
in_macro: 0,
in_function: 0,
loop_nesting: 0,
continue_nesting: 0,
iter_stack: Vec::new(),
ctx: ExprCtx::Tag { square: false },
buf: Vec::new(),
last_tok_end: (1, 1),
tag_pos: (1, 1),
named_arg_depth: 0,
pending_stop: None,
}
}
fn err(&self, line: u32, col: u32, details: impl Into<String>) -> TemplateError {
TemplateError::Parse {
template: self.name.clone(),
line,
col,
message: details.into(),
}
}
fn tag_square(&self) -> bool {
self.lexer.tag_syntax == Some(TagSyntax::Square)
}
fn establish_tag_syntax(&mut self, square: bool) {
if self.lexer.tag_syntax.is_none() {
self.lexer.tag_syntax = Some(if square {
TagSyntax::Square
} else {
TagSyntax::Angle
});
}
}
fn enter_tag(&mut self, square: bool) {
self.establish_tag_syntax(square);
self.ctx = ExprCtx::Tag {
square: self.tag_square(),
};
debug_assert!(self.buf.is_empty());
}
fn peek_tok(&mut self) -> Result<(Tok, u32, u32)> {
if self.buf.is_empty() {
let (t, l, c, el, ec) = self.lexer.next_expr_token(self.ctx)?;
self.buf.push((t, l, c, el, ec));
}
Ok((self.buf[0].0.clone(), self.buf[0].1, self.buf[0].2))
}
fn peek_tok2(&mut self) -> Result<(Tok, u32, u32)> {
self.peek_tok()?;
while self.buf.len() < 2 {
let (t, l, c, el, ec) = self.lexer.next_expr_token(self.ctx)?;
self.buf.push((t, l, c, el, ec));
}
Ok((self.buf[1].0.clone(), self.buf[1].1, self.buf[1].2))
}
fn next_tok(&mut self) -> Result<(Tok, u32, u32)> {
let entry = if !self.buf.is_empty() {
Some(self.buf.remove(0))
} else {
let (t, l, c, el, ec) = self.lexer.next_expr_token(self.ctx)?;
Some((t, l, c, el, ec))
};
if let Some((t, l, c, el, ec)) = entry {
self.last_tok_end = (el, ec);
Ok((t, l, c))
} else {
unreachable!()
}
}
fn expect_tok(&mut self, tok: Tok, what: &str) -> Result<(u32, u32)> {
let (t, l, c) = self.next_tok()?;
if t == tok {
Ok((l, c))
} else {
Err(self.err(
l,
c,
format!("Expected {what}, but found {}.", tok_desc(&t)),
))
}
}
fn expect_tag_end(&mut self) -> Result<(u32, u32)> {
let (t, l, c) = self.next_tok()?;
match t {
Tok::TagEnd => Ok((l, c)),
Tok::EmptyTagEnd => Err(self.err(
l,
c,
"The tag can't be self-closing (\" />\"): this directive needs a body.",
)),
other => Err(self.err(
l,
c,
format!(
"Expected \">\" to close the tag, but found {}.",
tok_desc(&other)
),
)),
}
}
fn loose_end(&mut self) -> Result<(u32, u32)> {
let (t, l, c) = self.next_tok()?;
match t {
Tok::TagEnd | Tok::EmptyTagEnd => Ok((l, c)),
other => Err(self.err(
l,
c,
format!(
"Expected \">\" or \"/>\" to close the tag, but found {}.",
tok_desc(&other)
),
)),
}
}
fn expect_tag_end_raw(&mut self) -> Result<()> {
match self.lexer.try_read_tag_end() {
Some(_) => Ok(()),
None => {
let (l, c) = self.lexer.line_col();
Err(self.err(l, c, "Expected \">\" or \"/>\" to close the tag."))
}
}
}
fn parse_template(&mut self) -> Result<Template> {
let save = self.lexer.save();
let (text, stop, _, _) = self.next_text_chunk()?;
let mut header_parsed = false;
if stop == TextStop::Tag && text.trim().is_empty() {
let open = self.lexer.read_tag_open();
if let TagOpen::Dir { square } = open {
if let Some(name) = self.lexer.read_name() {
if name.eq_ignore_ascii_case("ftl") {
self.enter_tag(square);
self.parse_ftl_header()?;
header_parsed = true;
}
}
}
}
if !header_parsed {
self.lexer.restore(&save);
}
let (root, _) = self.parse_block(&[], &[])?;
let mut root = root;
Self::mark_stripping(&mut root, self.strip_ws);
Self::remove_ignorable(&mut root, true);
sync_macro_defs(&mut root, &mut self.macros);
let mut template = Template::new(
self.name.clone(),
root,
std::mem::take(&mut self.macros),
self.cfg.clone(),
);
template.encoding = self.encoding.take();
template.ns_prefixes = std::mem::take(&mut self.ns_prefixes);
Ok(template)
}
fn remove_ignorable(els: &mut Vec<Element>, is_root: bool) {
let mut i = 0;
while i < els.len() {
let removable = match &els[i].kind {
ElementKind::TrimLineStart
| ElementKind::NoTrimLineStart
| ElementKind::TrimLineEnd
| ElementKind::LeftTrimLine => true,
ElementKind::Text { text, .. } | ElementKind::NoParse { text, .. } => {
if text.is_empty() {
true
} else if !text.chars().all(is_ws) {
false
} else {
let prev_ok =
(i == 0 && is_root) || (i > 0 && is_non_outputting(&els[i - 1]));
let next_ok = (i + 1 == els.len() && is_root)
|| (i + 1 < els.len() && is_non_outputting(&els[i + 1]));
prev_ok && next_ok
}
}
_ => false,
};
if removable {
els.remove(i);
} else {
i += 1;
}
}
}
fn parse_ftl_header(&mut self) -> Result<()> {
loop {
let (t, l, c) = self.peek_tok()?;
match t {
Tok::TagEnd | Tok::EmptyTagEnd => {
self.next_tok()?;
break;
}
Tok::Ident(key) => {
self.next_tok()?;
self.expect_tok(Tok::Eq, "\"=\" after the FTL header parameter")?;
let value = self.expression()?;
match key.to_ascii_lowercase().as_str() {
"encoding" => match &value.kind {
ExprKind::Str(s) => self.encoding = Some(s.clone()),
_ => {
return Err(self.err(
l,
c,
"Expected a string constant for \"encoding\".",
))
}
},
"strip_whitespace" | "stripwhitespace" | "strip_text" | "striptext" => {
match header_bool(&value) {
Some(b) => self.strip_ws = b,
None => {
return Err(self.err(
value.span.line,
value.span.col,
"Expecting boolean (true/false) parameter",
));
}
}
}
"strict_syntax" | "strictsyntax" => match header_bool(&value) {
Some(b) => self.lexer.strict_syntax = b,
None => {
return Err(self.err(
value.span.line,
value.span.col,
"Expecting boolean (true/false) parameter",
));
}
},
"auto_esc" | "autoesc" | "output_format" | "outputformat"
| "attributes" => {}
"ns_prefixes" | "nsprefixes" => {
match &value.kind {
ExprKind::HashLit(entries) => {
for (k, v) in entries {
let prefix = match &k.kind {
ExprKind::Str(s) => s.clone(),
ExprKind::Ident(i) => i.clone(),
_ => continue,
};
let ExprKind::Str(uri) = &v.kind else {
return Err(self.err(
l,
c,
"Expected a string constant for the namespace URI in \"ns_prefixes\".",
));
};
self.ns_prefixes.insert(prefix.clone(), uri.clone());
}
}
_ => {
return Err(self.err(
l,
c,
"Expected a hash literal for \"ns_prefixes\".",
))
}
}
}
other => {
return Err(self.err(
l,
c,
format!("Unknown FTL header parameter: {other}."),
))
}
}
}
other => {
return Err(self.err(
l,
c,
format!(
"Expected an FTL header parameter or the closing \">\", but found {}.",
tok_desc(&other)
),
))
}
}
}
loop {
match self.lexer.peek() {
Some(c) if c == ' ' || c == '\t' || c == '\r' || c == '\n' => {
if c == '\r' {
self.lexer.bump();
if self.lexer.peek() == Some('\n') {
self.lexer.bump();
}
break;
}
if c == '\n' {
self.lexer.bump();
break;
}
self.lexer.bump();
}
_ => break,
}
}
Ok(())
}
}