use crate::ast::Node;
use crate::errors::{Result, RunjucksError};
use crate::extension::{
register_extension_inner, remove_extension_inner, CustomExtensionHandler, ExtensionTagMeta,
};
use crate::globals::{default_globals_map, value_is_callable, RJ_CALLABLE};
use crate::lexer::{LexerOptions, Tags};
use crate::loader::TemplateLoader;
use crate::parser::is_reserved_tag_keyword;
use crate::value::{is_undefined_value, undefined_value, value_to_string};
use crate::{lexer, parser, renderer};
use serde_json::{Map, Value};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::sync::{Arc, Mutex};
#[derive(Clone, Debug, PartialEq, Eq)]
struct ParseSignature {
trim_blocks: bool,
lstrip_blocks: bool,
tags: Option<Tags>,
extension_tag_keys: Vec<String>,
extension_closing_names: Vec<String>,
}
struct CachedParse {
sig: ParseSignature,
ast: Arc<Node>,
source: Option<String>,
}
fn hash_source(s: &str) -> u64 {
use std::collections::hash_map::DefaultHasher;
let mut h = DefaultHasher::new();
s.hash(&mut h);
h.finish()
}
pub type CustomFilter = Arc<dyn Fn(&Value, &[Value]) -> Result<Value> + Send + Sync>;
pub type CustomTest = Arc<dyn Fn(&Value, &[Value]) -> Result<bool> + Send + Sync>;
pub type CustomGlobalFn = Arc<dyn Fn(&[Value], &[(String, Value)]) -> Result<Value> + Send + Sync>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExtensionDescriptor {
pub name: String,
pub tags: Vec<String>,
pub blocks: HashMap<String, String>,
}
#[derive(Clone)]
pub struct Environment {
pub autoescape: bool,
pub dev: bool,
pub loader: Option<Arc<dyn TemplateLoader + Send + Sync>>,
pub globals: HashMap<String, Value>,
pub throw_on_undefined: bool,
pub random_seed: Option<u64>,
pub trim_blocks: bool,
pub lstrip_blocks: bool,
pub tags: Option<Tags>,
pub(crate) custom_filters: HashMap<String, CustomFilter>,
pub(crate) custom_tests: HashMap<String, CustomTest>,
pub(crate) custom_globals: HashMap<String, CustomGlobalFn>,
pub(crate) extension_tags: HashMap<String, ExtensionTagMeta>,
pub(crate) extension_closing_tag_names: HashSet<String>,
pub(crate) custom_extensions: HashMap<String, CustomExtensionHandler>,
inline_parse_cache: Arc<Mutex<HashMap<u64, CachedParse>>>,
named_parse_cache: Arc<Mutex<HashMap<String, CachedParse>>>,
}
impl std::fmt::Debug for Environment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Environment")
.field("autoescape", &self.autoescape)
.field("dev", &self.dev)
.field("loader", &self.loader.is_some())
.field("globals_len", &self.globals.len())
.field("custom_filters_len", &self.custom_filters.len())
.field("custom_tests_len", &self.custom_tests.len())
.field("custom_globals_len", &self.custom_globals.len())
.field("extension_tags_len", &self.extension_tags.len())
.field("throw_on_undefined", &self.throw_on_undefined)
.field("random_seed", &self.random_seed)
.finish()
}
}
fn is_truthy_value(v: &Value) -> bool {
if is_undefined_value(v) {
return false;
}
match v {
Value::Null | Value::Bool(false) => false,
Value::Bool(true) => true,
Value::Number(n) => n.as_f64().map(|x| x != 0.0 && !x.is_nan()).unwrap_or(true),
Value::String(s) => !s.is_empty(),
Value::Array(_) | Value::Object(_) => true,
}
}
fn as_is_test_integer(v: &Value) -> Result<i64> {
v.as_i64()
.or_else(|| v.as_f64().map(|x| x as i64))
.ok_or_else(|| RunjucksError::new("test expected a number"))
}
impl Default for Environment {
fn default() -> Self {
Self {
autoescape: true,
dev: false,
loader: None,
globals: default_globals_map(),
throw_on_undefined: false,
random_seed: None,
trim_blocks: false,
lstrip_blocks: false,
tags: None,
custom_filters: HashMap::new(),
custom_tests: HashMap::new(),
custom_globals: HashMap::new(),
extension_tags: HashMap::new(),
extension_closing_tag_names: HashSet::new(),
custom_extensions: HashMap::new(),
inline_parse_cache: Arc::new(Mutex::new(HashMap::new())),
named_parse_cache: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl Environment {
fn current_parse_signature(&self) -> ParseSignature {
let mut keys: Vec<_> = self.extension_tags.keys().cloned().collect();
keys.sort();
let mut closing: Vec<_> = self.extension_closing_tag_names.iter().cloned().collect();
closing.sort();
ParseSignature {
trim_blocks: self.trim_blocks,
lstrip_blocks: self.lstrip_blocks,
tags: self.tags.clone(),
extension_tag_keys: keys,
extension_closing_names: closing,
}
}
fn parse_source_to_ast(&self, src: &str) -> Result<Node> {
let tokens = lexer::tokenize_with_options(src, self.lexer_options())?;
parser::parse_with_env(
&tokens,
&self.extension_tags,
&self.extension_closing_tag_names,
)
}
pub fn parse_or_cached_inline(&self, src: &str) -> Result<Arc<Node>> {
let sig = self.current_parse_signature();
let key = hash_source(src);
{
let cache = self.inline_parse_cache.lock().unwrap();
if let Some(c) = cache.get(&key) {
if c.sig == sig && c.source.as_deref() == Some(src) {
return Ok(Arc::clone(&c.ast));
}
}
}
let node = self.parse_source_to_ast(src)?;
let arc = Arc::new(node);
let mut cache = self.inline_parse_cache.lock().unwrap();
cache.insert(
key,
CachedParse {
sig,
ast: Arc::clone(&arc),
source: Some(src.to_string()),
},
);
Ok(arc)
}
pub(crate) fn load_and_parse_named(
&self,
name: &str,
loader: &(dyn TemplateLoader + Send + Sync),
) -> Result<Arc<Node>> {
let src = loader.load(name)?;
self.parse_with_named_cache(name, loader, &src)
}
fn parse_with_named_cache(
&self,
name: &str,
loader: &(dyn TemplateLoader + Send + Sync),
src: &str,
) -> Result<Arc<Node>> {
let sig = self.current_parse_signature();
if let Some(ref key) = loader.cache_key(name) {
{
let cache = self.named_parse_cache.lock().unwrap();
if let Some(c) = cache.get(key) {
if c.sig == sig && c.source.as_deref() == Some(src) {
return Ok(Arc::clone(&c.ast));
}
}
}
let node = self.parse_source_to_ast(src)?;
let arc = Arc::new(node);
let mut cache = self.named_parse_cache.lock().unwrap();
cache.insert(
key.clone(),
CachedParse {
sig,
ast: Arc::clone(&arc),
source: Some(src.to_string()),
},
);
Ok(arc)
} else {
let node = self.parse_source_to_ast(src)?;
Ok(Arc::new(node))
}
}
pub fn clear_named_parse_cache(&self) {
self.named_parse_cache.lock().unwrap().clear();
}
pub fn invalidate_cache(&self) {
self.named_parse_cache.lock().unwrap().clear();
self.inline_parse_cache.lock().unwrap().clear();
}
pub fn render_parsed(&self, ast: &Node, context: Value) -> Result<String> {
let root = match context {
Value::Object(m) => m,
_ => Map::new(),
};
let mut stack = renderer::CtxStack::from_root(root);
let loader = self.loader.as_ref().map(|arc| arc.as_ref());
renderer::render(self, loader, ast, &mut stack)
}
pub fn add_global(&mut self, name: impl Into<String>, value: Value) -> &mut Self {
let name = name.into();
self.custom_globals.remove(&name);
self.globals.insert(name, value);
self
}
pub fn add_global_callable(&mut self, name: impl Into<String>, f: CustomGlobalFn) -> &mut Self {
let name = name.into();
let mut m = Map::new();
m.insert(RJ_CALLABLE.to_string(), Value::Bool(true));
self.globals.insert(name.clone(), Value::Object(m));
self.custom_globals.insert(name, f);
self
}
pub fn add_filter(&mut self, name: impl Into<String>, filter: CustomFilter) -> &mut Self {
self.custom_filters.insert(name.into(), filter);
self
}
pub fn add_test(&mut self, name: impl Into<String>, test: CustomTest) -> &mut Self {
self.custom_tests.insert(name.into(), test);
self
}
pub fn register_extension(
&mut self,
extension_name: impl Into<String>,
tag_specs: Vec<(String, Option<String>)>,
handler: CustomExtensionHandler,
) -> Result<()> {
let extension_name = extension_name.into();
register_extension_inner(
&mut self.extension_tags,
&mut self.extension_closing_tag_names,
&mut self.custom_extensions,
extension_name,
tag_specs,
handler,
is_reserved_tag_keyword,
)
}
pub fn has_extension(&self, name: &str) -> bool {
self.custom_extensions.contains_key(name)
}
pub fn get_extension_descriptor(&self, name: &str) -> Option<ExtensionDescriptor> {
if !self.custom_extensions.contains_key(name) {
return None;
}
let mut tags = Vec::new();
let mut blocks = HashMap::new();
for (tag, meta) in &self.extension_tags {
if meta.extension_name == name {
tags.push(tag.clone());
if let Some(end) = &meta.end_tag {
blocks.insert(tag.clone(), end.clone());
}
}
}
tags.sort();
Some(ExtensionDescriptor {
name: name.to_string(),
tags,
blocks,
})
}
pub fn remove_extension(&mut self, name: &str) -> bool {
remove_extension_inner(
&mut self.extension_tags,
&mut self.extension_closing_tag_names,
&mut self.custom_extensions,
name,
)
}
pub fn validate_lex_parse(&self, src: &str) -> Result<()> {
let tokens = lexer::tokenize_with_options(src, self.lexer_options())?;
let _ = parser::parse_with_env(
&tokens,
&self.extension_tags,
&self.extension_closing_tag_names,
)?;
Ok(())
}
pub(crate) fn eval_user_is_test(
&self,
name: &str,
value: &Value,
args: &[Value],
) -> Result<bool> {
match self.custom_tests.get(name) {
Some(t) => t(value, args),
None => Err(RunjucksError::new(format!("unknown test: `{name}`"))),
}
}
pub(crate) fn apply_is_test(
&self,
test_name: &str,
value: &Value,
arg_vals: &[Value],
) -> Result<bool> {
match test_name {
"equalto" => Ok(arg_vals.first().map(|a| a == value).unwrap_or(false)),
"sameas" => Ok(match arg_vals.first() {
Some(a) => match (value, a) {
(Value::Object(_), Value::Object(_)) | (Value::Array(_), Value::Array(_)) => {
false
}
_ => a == value,
},
None => false,
}),
"null" | "none" => Ok(value.is_null()),
"falsy" => Ok(!is_truthy_value(value)),
"truthy" => Ok(is_truthy_value(value)),
"number" => Ok(value.is_number()),
"string" => Ok(value.is_string()),
"lower" => Ok(match value {
Value::String(s) => s.chars().all(|c| !c.is_uppercase()),
_ => false,
}),
"upper" => Ok(match value {
Value::String(s) => s.chars().all(|c| !c.is_lowercase()),
_ => false,
}),
"callable" => Ok(value_is_callable(value)),
"defined" => Ok(!is_undefined_value(value)),
"odd" => {
let n = as_is_test_integer(value)?;
Ok(n.rem_euclid(2) != 0)
}
"even" => {
let n = as_is_test_integer(value)?;
Ok(n.rem_euclid(2) == 0)
}
"divisibleby" => {
let denom = arg_vals
.first()
.and_then(|a| {
a.as_i64()
.or_else(|| a.as_f64().map(|x| x as i64))
.or_else(|| value_to_string(a).parse().ok())
})
.ok_or_else(|| RunjucksError::new("`divisibleby` test expects a divisor"))?;
if denom == 0 {
return Ok(false);
}
let n = as_is_test_integer(value)?;
Ok(n.rem_euclid(denom) == 0)
}
_ => self.eval_user_is_test(test_name, value, arg_vals),
}
}
pub fn resolve_variable_ref<'a>(
&'a self,
stack: &'a renderer::CtxStack,
name: &str,
) -> Result<Cow<'a, Value>> {
if stack.defined(name) {
Ok(Cow::Borrowed(stack.get_ref(name).expect(
"internal error: variable marked defined but missing from stack",
)))
} else if let Some(v) = self.globals.get(name) {
Ok(Cow::Borrowed(v))
} else if self.throw_on_undefined {
Err(RunjucksError::new(format!("undefined variable: `{name}`")))
} else {
Ok(Cow::Owned(undefined_value()))
}
}
pub fn resolve_variable(&self, stack: &renderer::CtxStack, name: &str) -> Result<Value> {
self.resolve_variable_ref(stack, name)
.map(|c| c.into_owned())
}
pub fn lexer_options(&self) -> LexerOptions {
LexerOptions {
trim_blocks: self.trim_blocks,
lstrip_blocks: self.lstrip_blocks,
tags: self.tags.clone(),
}
}
pub fn render_string(&self, template: String, context: Value) -> Result<String> {
let ast = self.parse_or_cached_inline(&template)?;
self.render_parsed(ast.as_ref(), context)
}
pub fn render_template(&self, name: &str, context: Value) -> Result<String> {
let loader = self
.loader
.as_ref()
.ok_or_else(|| RunjucksError::new("no template loader configured"))?;
let ast = self.load_and_parse_named(name, loader.as_ref())?;
let root = match context {
Value::Object(m) => m,
_ => Map::new(),
};
let mut stack = renderer::CtxStack::from_root(root);
let mut state = renderer::RenderState::new(Some(loader.as_ref()), self.random_seed);
state.push_template(name)?;
renderer::scan_literal_extends_graph(self, &mut state, ast.as_ref(), loader.as_ref())?;
let out = renderer::render_entry(self, &mut state, ast.as_ref(), &mut stack)?;
state.pop_template();
Ok(out)
}
}