#![warn(clippy::use_self)]
#![allow(clippy::should_implement_trait)]
mod arg;
mod directive;
mod err;
mod lexer;
mod parser;
mod traits;
use std::{
borrow::Cow,
cell::RefCell,
collections::HashMap,
fmt::{self},
};
pub use arg::*;
pub use directive::*;
pub use err::*;
pub use lexer::*;
pub use parser::*;
#[derive(Debug, Clone)]
pub enum Value {
Str(Cow<'static, str>),
Int(i64),
Float(f64),
Bool(bool),
}
impl Value {
pub fn static_str(s: &'static str) -> Self {
Self::Str(Cow::Borrowed(s))
}
pub fn owned_str(s: String) -> Self {
Self::Str(Cow::Owned(s))
}
pub fn type_name(&self) -> &str {
match self {
Self::Str(_) => "string",
Self::Int(_) => "integer",
Self::Float(_) => "float",
Self::Bool(_) => "boolean",
}
}
}
pub type Context = HashMap<&'static str, Value>;
pub struct Template<const O: char, const C: char> {
directives: Vec<Box<dyn Directive + Send + Sync>>,
}
impl<const C: char, const O: char> fmt::Debug for Template<O, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Template<'{}', '{}'>", O, C)
}
}
impl<const O: char, const C: char> Template<O, C> {
pub fn compile(input: impl AsRef<str>) -> Result<Self, TemplateError> {
Self::compile_with_parser::<DefaultParser>(input.as_ref())
}
pub fn compile_with_parser<P: Parser>(input: &str) -> Result<Self, TemplateError> {
let mut directives: Vec<Box<dyn Directive + Send + Sync>> = Vec::new();
let mut cursor = 0;
let mut chars = input.char_indices().peekable();
let arena = RefCell::new(String::new());
while let Some((idx, ch)) = chars.next() {
if ch == O {
if let Some(&(_, next_char)) = chars.peek()
&& next_char == O
{
if idx > cursor {
directives.push(Box::new(LiteralDirective(Cow::Owned(
input[cursor..idx].to_string(),
))));
}
directives.push(Box::new(LiteralDirective(Cow::Owned(O.to_string()))));
chars.next();
cursor = chars.peek().map(|(i, _)| *i).unwrap_or(input.len());
continue;
}
if idx > cursor {
directives.push(Box::new(LiteralDirective(Cow::Owned(
input[cursor..idx].to_string(),
))));
}
let start = idx + ch.len_utf8();
let mut depth = 1;
let mut end = start;
let mut found_close = false;
for (c_idx, c_char) in chars.by_ref() {
if O == C {
if c_char == C {
depth -= 1;
}
} else if c_char == O {
depth += 1;
} else if c_char == C {
depth -= 1;
}
if depth == 0 {
end = c_idx;
found_close = true;
cursor = c_idx + C.len_utf8();
break;
}
}
if !found_close {
return Err(TemplateError::MissingDelimiter(C));
}
let content = &input[start..end];
arena.borrow_mut().clear();
let tokens: Vec<Token> = TemplateLexer::new(content).collect();
match P::parse(&tokens) {
Some(directive) => directives.push(directive),
None => return Err(TemplateError::DirectiveParsing(content.to_string())),
}
} else if ch == C
&& let Some(&(_, next_char)) = chars.peek()
&& next_char == C
{
if idx > cursor {
directives.push(Box::new(LiteralDirective(Cow::Owned(
input[cursor..idx].to_string(),
))));
}
directives.push(Box::new(LiteralDirective(Cow::Owned(C.to_string()))));
chars.next();
cursor = chars.peek().map(|(i, _)| *i).unwrap_or(input.len());
continue;
}
}
if cursor < input.len() {
directives.push(Box::new(LiteralDirective(Cow::Owned(
input[cursor..].to_string(),
))));
}
Ok(Self { directives })
}
pub fn format(&self, ctx: &Context) -> Result<String, DirectiveError> {
let mut output = String::with_capacity(self.directives.len() * 8);
for directive in &self.directives {
let result = directive.exec(ctx)?;
output.push_str(&result);
}
Ok(output)
}
}