use crate::error::{Result, TemplateError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TagSyntax {
Angle,
Square,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ExprCtx {
Tag { square: bool },
Interp,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Tok {
Ident(String),
Number(String),
Str(String),
RawStr(String),
True,
False,
In,
As,
Using,
Lt,
Lte,
Gt,
Gte,
Plus,
Minus,
Times,
DoubleStar,
Divide,
Percent,
PlusEq,
MinusEq,
TimesEq,
DivEq,
ModEq,
PlusPlus,
MinusMinus,
Eq,
NotEq,
Exclam,
Exists,
Builtin,
And,
Or,
LambdaArrow,
Dot,
DotDot,
DotDotLess,
DotDotStar,
Ellipsis,
Comma,
Semicolon,
Colon,
OpenParen,
CloseParen,
OpenBracket,
CloseBracket,
OpenCurly,
CloseCurly,
TagEnd,
EmptyTagEnd,
InterpEnd,
Eof,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TextStop {
Eof,
Tag,
Interp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TagOpen {
Dir { square: bool },
Call { square: bool },
EndDir { square: bool },
EndCall { square: bool },
TerseComment { square: bool },
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct LexerPos {
pos: usize,
line: u32,
col: u32,
paren_depth: u32,
bracket_depth: u32,
curly_depth: u32,
}
pub(crate) struct Lexer {
chars: Vec<char>,
pos: usize,
line: u32,
col: u32,
pub(crate) paren_depth: u32,
pub(crate) bracket_depth: u32,
pub(crate) curly_depth: u32,
pub(crate) strict_syntax: bool,
pub(crate) tag_syntax: Option<TagSyntax>,
name: String,
}
#[path = "lexer_expr.rs"]
mod lexer_expr;
#[path = "lexer_scan.rs"]
mod lexer_scan;
#[cfg(test)]
#[path = "lexer_tests.rs"]
mod lexer_tests;
impl Lexer {
pub(crate) fn new(name: &str, text: &str, strict_syntax: bool) -> Self {
Lexer {
chars: text.chars().collect(),
pos: 0,
line: 1,
col: 1,
paren_depth: 0,
bracket_depth: 0,
curly_depth: 0,
strict_syntax,
tag_syntax: None,
name: name.to_string(),
}
}
pub(crate) fn save(&self) -> LexerPos {
LexerPos {
pos: self.pos,
line: self.line,
col: self.col,
paren_depth: self.paren_depth,
bracket_depth: self.bracket_depth,
curly_depth: self.curly_depth,
}
}
pub(crate) fn restore(&mut self, p: &LexerPos) {
self.pos = p.pos;
self.line = p.line;
self.col = p.col;
self.paren_depth = p.paren_depth;
self.bracket_depth = p.bracket_depth;
self.curly_depth = p.curly_depth;
}
pub(crate) fn peek(&self) -> Option<char> {
self.chars.get(self.pos).copied()
}
pub(crate) fn peek_at(&self, offset: usize) -> Option<char> {
self.chars.get(self.pos + offset).copied()
}
pub(crate) fn bump(&mut self) -> Option<char> {
let c = self.chars.get(self.pos).copied()?;
self.pos += 1;
match c {
'\n' => {
self.line += 1;
self.col = 1;
}
'\r' => {
self.col = 1;
}
_ => {
self.col += 1;
}
}
Some(c)
}
pub(crate) fn line_col(&self) -> (u32, u32) {
(self.line, self.col)
}
pub(crate) fn err(&self, line: u32, col: u32, details: impl Into<String>) -> TemplateError {
TemplateError::Parse {
template: self.name.clone(),
line,
col,
message: details.into(),
}
}
pub(crate) fn skip_ws(&mut self) {
while let Some(c) = self.peek() {
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
self.bump();
} else {
break;
}
}
}
pub(crate) fn starts_tag(&mut self) -> bool {
let save = self.save();
let r = self.starts_tag_inner();
self.restore(&save);
r
}
fn starts_tag_inner(&mut self) -> bool {
match self.peek() {
Some('<') => match self.peek_at(1) {
Some('#') => match self.peek_at(2) {
Some(c) if c.is_ascii_alphabetic() || c == '_' => true,
Some('-') => self.peek_at(3) == Some('-'),
_ => false,
},
Some('@') => true,
Some('/') => match self.peek_at(2) {
Some('#') | Some('@') => true,
_ => {
if !self.strict_syntax {
self.bump();
self.bump();
return self.non_strict_tag_name(true);
}
false
}
},
Some(c) if c.is_ascii_alphabetic() || c == '_' => {
if self.strict_syntax {
false
} else {
self.bump();
self.non_strict_tag_name(false)
}
}
_ => false,
},
Some('[') => match self.peek_at(1) {
Some('#') => match self.peek_at(2) {
Some(c) if c.is_ascii_alphabetic() || c == '_' => true,
Some('-') => self.peek_at(3) == Some('-'),
_ => false,
},
Some('@') => true,
Some('/') => matches!(self.peek_at(2), Some('#') | Some('@')),
_ => false,
},
_ => false,
}
}
fn non_strict_tag_name(&mut self, is_end: bool) -> bool {
let name = self.read_name();
let Some(name) = name else { return false };
if !DIRECTIVE_NAMES.contains(&name.as_str()) || name == "ftl" {
return false;
}
if is_end {
self.skip_ws();
return matches!(self.peek(), Some('>') | Some(']'))
|| (self.peek() == Some('/') && matches!(self.peek_at(1), Some('>') | Some(']')));
}
let param_required = PARAM_DIRECTIVES.contains(&name.as_str());
if param_required {
matches!(self.peek(), Some(c) if c == ' ' || c == '\t' || c == '\n' || c == '\r')
} else {
self.skip_ws();
match self.peek() {
Some('>') | Some(']') => true,
Some('/') => matches!(self.peek_at(1), Some('>') | Some(']')),
_ => false,
}
}
}
pub(crate) fn read_name(&mut self) -> Option<String> {
let mut s = String::new();
while let Some(c) = self.peek() {
if c.is_ascii_alphabetic() || c == '_' {
s.push(c);
self.bump();
} else {
break;
}
}
if s.is_empty() {
None
} else {
Some(s)
}
}
pub(crate) fn read_tag_open(&mut self) -> TagOpen {
let square = self.peek() == Some('[');
self.bump(); match self.peek() {
Some('#') => {
self.bump();
if self.peek() == Some('-') && self.peek_at(1) == Some('-') {
self.bump();
self.bump();
TagOpen::TerseComment { square }
} else {
TagOpen::Dir { square }
}
}
Some('@') => {
self.bump();
TagOpen::Call { square }
}
Some('/') => {
self.bump();
match self.peek() {
Some('@') => {
self.bump();
TagOpen::EndCall { square }
}
Some('#') => {
self.bump();
TagOpen::EndDir { square }
}
_ => TagOpen::EndDir { square },
}
}
Some(c) if c.is_ascii_alphabetic() || c == '_' => TagOpen::Dir { square },
_ => unreachable!("starts_tag 已保证标签开头"),
}
}
pub(crate) fn try_read_tag_end(&mut self) -> Option<bool> {
while let Some(c) = self.peek() {
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
self.bump();
} else {
break;
}
}
match self.peek() {
Some('>') => {
self.bump();
Some(false)
}
Some(']') => {
self.bump();
Some(false)
}
Some('/') => match self.peek_at(1) {
Some('>') | Some(']') => {
self.bump();
self.bump();
Some(true)
}
_ => None,
},
_ => None,
}
}
pub(crate) fn scan_text_chunk(&mut self) -> Result<(String, TextStop)> {
let mut text = String::new();
loop {
match self.peek() {
None => return Ok((text, TextStop::Eof)),
Some('<') | Some('[') => {
if self.starts_tag() {
return Ok((text, TextStop::Tag));
}
text.push(self.bump().unwrap());
}
Some('$') => {
if self.peek_at(1) == Some('{') {
return Ok((text, TextStop::Interp));
}
if self.peek_at(1) == Some('$') && self.peek_at(2) == Some('{') {
text.push(self.bump().unwrap());
continue;
}
text.push(self.bump().unwrap());
}
Some('#') => {
if self.peek_at(1) == Some('{') {
return Ok((text, TextStop::Interp));
}
text.push(self.bump().unwrap());
}
Some(c) => {
text.push(c);
self.bump();
}
}
}
}
}
fn is_ident_start(c: char) -> bool {
c.is_alphabetic() || c == '$' || c == '_' || c == '@'
}
fn is_ident_continue(c: char) -> bool {
is_ident_start(c) || c.is_ascii_digit()
}
pub(crate) const DIRECTIVE_NAMES: &[&str] = &[
"attempt",
"recover",
"if",
"elseif",
"else",
"list",
"items",
"sep",
"switch",
"case",
"default",
"assign",
"global",
"local",
"include",
"import",
"macro",
"function",
"stop",
"return",
"break",
"continue",
"nested",
"flush",
"t",
"lt",
"rt",
"nt",
"compress",
"comment",
"noparse",
"escape",
"noescape",
"trim",
"autoesc",
"noautoesc",
"outputformat",
"setting",
"call",
"foreach",
"transform",
"visit",
"recurse",
"on",
"fallback",
"ftl",
];
pub(crate) const PARAM_DIRECTIVES: &[&str] = &[
"if",
"elseif",
"list",
"items",
"switch",
"case",
"assign",
"global",
"local",
"include",
"import",
"macro",
"function",
"stop",
"return",
"nested",
"escape",
"setting",
"call",
"foreach",
"transform",
"visit",
"recurse",
"on",
"outputformat",
"ftl",
];