///|
/// The maximum recursion in the VM. Normally each stack frame adds one to
/// this counter (eg: every time a frame is added). However in some cases
/// more depth is pushed if the cost of the stack frame is higher.
let max_vm_recursion = 500
///|
/// A callback that formats a value to the output.
pub type FormatterFunc = (Output, State, Value) -> Unit raise TemplateError
///|
/// The signature of filters, tests and functions.
pub type NativeFunction = (State, Array[Value]) -> Value raise TemplateError
///|
/// An abstraction that holds the engine configuration.
///
/// This object holds the central configuration state for templates. It is
/// also the container for all loaded templates.
///
/// There are generally two ways to construct an environment:
///
/// * [`Environment::new`] creates an environment preconfigured with sensible
/// defaults. It will contain all built-in filters, tests and globals as
/// well as a callback for auto escaping based on file extension.
/// * [`Environment::empty`] creates a completely blank environment.
pub struct Environment {
priv templates : Map[String, CompiledTemplate]
priv mut loader : ((String) -> String? raise TemplateError)?
// the builtin maps are shared between environments until modified
// (copy-on-write, like `Arc::make_mut` upstream)
priv mut filters : Map[String, Value]
priv mut tests : Map[String, Value]
priv mut globals : Map[String, Value]
priv mut maps_shared : Bool
priv mut path_join_callback : ((String, String) -> String)?
priv mut unknown_method_callback : ((State, Value, String, Array[Value]) -> Value raise TemplateError)?
priv mut undefined_behavior : UndefinedBehavior
priv mut formatter : FormatterFunc
priv mut formatter_is_default : Bool
priv mut debug : Bool
priv mut fuel : Int64?
priv mut recursion_limit : Int
priv mut syntax_config : SyntaxConfig
priv mut ws_config : WhitespaceConfig
priv mut auto_escape_callback : (String) -> AutoEscape
}
///|
fn default_formatter(
out : Output,
state : State,
value : Value,
) -> Unit raise TemplateError {
write_escaped(out, state.auto_escape(), value)
}
///|
/// Creates a new environment with sensible defaults.
///
/// This environment does not yet contain any templates but it will have all
/// the default filters, tests and globals loaded. If you do not want any
/// default configuration you can use [`Environment::empty`].
pub fn Environment::new() -> Environment {
let env = Environment::empty()
env.auto_escape_callback = default_auto_escape_callback
env.filters = builtin_filters
env.tests = builtin_tests
env.globals = builtin_globals
env.maps_shared = true
env
}
///|
/// Creates a completely empty environment.
///
/// This environment has no filters, no templates, no globals and no default
/// logic for auto escaping configured.
pub fn Environment::empty() -> Environment {
{
templates: Map([]),
loader: None,
filters: Map([]),
tests: Map([]),
globals: Map([]),
maps_shared: false,
path_join_callback: None,
unknown_method_callback: None,
undefined_behavior: Lenient,
formatter: default_formatter,
formatter_is_default: true,
debug: true,
fuel: None,
recursion_limit: max_vm_recursion,
syntax_config: SyntaxConfig::default(),
ws_config: WhitespaceConfig::default(),
auto_escape_callback: _ => NoEscape,
}
}
///|
/// Loads a template from a string into the environment.
///
/// The `name` parameter defines the name of the template which identifies
/// it. To look up a loaded template use the [`get_template`] method.
pub fn Environment::add_template(
self : Environment,
name : String,
source : String,
) -> Unit raise TemplateError {
self.templates[name] = self.compile(name, source)
}
///|
fn Environment::compile(
self : Environment,
name : String,
source : String,
) -> CompiledTemplate raise TemplateError {
CompiledTemplate::new(
name,
source,
self.syntax_config,
self.ws_config,
(self.auto_escape_callback)(name),
)
}
///|
/// Removes a template by name.
pub fn Environment::remove_template(self : Environment, name : String) -> Unit {
self.templates.remove(name)
}
///|
/// Removes all stored templates.
pub fn Environment::clear_templates(self : Environment) -> Unit {
self.templates.clear()
}
///|
/// Registers a template loader as source of templates.
///
/// When a template loader is registered, the environment gains the ability
/// to dynamically load templates. The loader is invoked with the name of
/// the template. If this template exists `Some(source)` has to be returned,
/// otherwise `None`. Once a template has been loaded it's stored on the
/// environment.
pub fn Environment::set_loader(
self : Environment,
f : (String) -> String? raise TemplateError,
) -> Unit {
self.loader = Some(f)
}
///|
/// Preserve the trailing newline when rendering templates.
pub fn Environment::set_keep_trailing_newline(
self : Environment,
yes : Bool,
) -> Unit {
self.ws_config = { ..self.ws_config, keep_trailing_newline: yes, }
}
///|
/// Returns the value of the trailing newline preservation flag.
pub fn Environment::keep_trailing_newline(self : Environment) -> Bool {
self.ws_config.keep_trailing_newline
}
///|
/// Remove the first newline after a block.
pub fn Environment::set_trim_blocks(self : Environment, yes : Bool) -> Unit {
self.ws_config = { ..self.ws_config, trim_blocks: yes, }
}
///|
/// Returns the value of the trim blocks flag.
pub fn Environment::trim_blocks(self : Environment) -> Bool {
self.ws_config.trim_blocks
}
///|
/// Remove leading spaces and tabs from the start of a line to a block.
pub fn Environment::set_lstrip_blocks(self : Environment, yes : Bool) -> Unit {
self.ws_config = { ..self.ws_config, lstrip_blocks: yes, }
}
///|
/// Returns the value of the lstrip blocks flag.
pub fn Environment::lstrip_blocks(self : Environment) -> Bool {
self.ws_config.lstrip_blocks
}
///|
/// Sets a callback to join template paths (for relative includes).
///
/// The callback is invoked with the name of the template to load and the
/// name of the template that is loading it.
pub fn Environment::set_path_join_callback(
self : Environment,
f : (String, String) -> String,
) -> Unit {
self.path_join_callback = Some(f)
}
///|
/// Sets a callback invoked when an unknown method is called on an object.
pub fn Environment::set_unknown_method_callback(
self : Environment,
f : (State, Value, String, Array[Value]) -> Value raise TemplateError,
) -> Unit {
self.unknown_method_callback = Some(f)
}
///|
/// Fetches a template by name.
///
/// This requires that the template has been loaded with
/// [`add_template`](Environment::add_template) beforehand or that a loader
/// can produce it.
pub fn Environment::get_template(
self : Environment,
name : String,
) -> Template raise TemplateError {
match self.templates.get(name) {
Some(compiled) => { env: self, compiled, }
None => {
let source = match self.loader {
Some(loader) => loader(name)
None => None
}
match source {
Some(source) => {
let compiled = self.compile(name, source)
self.templates[name] = compiled
{ env: self, compiled, }
}
None => raise TemplateError::new_not_found(name)
}
}
}
}
///|
/// Returns the names of all loaded templates.
pub fn Environment::template_names(self : Environment) -> Array[String] {
self.templates.keys().to_array()
}
///|
/// Loads a template from a string, with name.
///
/// In some cases you really only need a template to be compiled once. This
/// creates a template that is not stored on the environment.
pub fn Environment::template_from_named_str(
self : Environment,
name : String,
source : String,
) -> Template raise TemplateError {
{ env: self, compiled: self.compile(name, source), }
}
///|
/// Loads a template from a string (named ``).
pub fn Environment::template_from_str(
self : Environment,
source : String,
) -> Template raise TemplateError {
self.template_from_named_str("", source)
}
///|
/// Parses and renders a template from a string in one go with name.
pub fn Environment::render_named_str(
self : Environment,
name : String,
source : String,
ctx : Value,
) -> String raise TemplateError {
self.template_from_named_str(name, source).render(ctx)
}
///|
/// Parses and renders a template from a string in one go.
///
/// ```mbt check
/// test {
/// let env = @minijinja.Environment::new()
/// let ctx = @minijinja.Value::from_pairs([
/// ("name", @minijinja.Value::from_string("World")),
/// ])
/// inspect(env.render_str("Hello {{ name }}!", ctx), content="Hello World!")
/// }
/// ```
pub fn Environment::render_str(
self : Environment,
source : String,
ctx : Value,
) -> String raise TemplateError {
self.template_from_str(source).render(ctx)
}
///|
/// Sets a new function to select the default auto escaping.
///
/// This function is invoked when templates are loaded into the environment
/// to determine the default auto escaping behavior. The function is
/// invoked with the name of the template.
pub fn Environment::set_auto_escape_callback(
self : Environment,
f : (String) -> AutoEscape,
) -> Unit {
self.auto_escape_callback = f
}
///|
/// Changes the undefined behavior.
pub fn Environment::set_undefined_behavior(
self : Environment,
behavior : UndefinedBehavior,
) -> Unit {
self.undefined_behavior = behavior
}
///|
/// Returns the current undefined behavior.
pub fn Environment::undefined_behavior(self : Environment) -> UndefinedBehavior {
self.undefined_behavior
}
///|
/// Sets a different formatter function.
///
/// The formatter is invoked to format the given value into the provided
/// output. The default formatter escapes values according to the auto
/// escape flag of the state.
pub fn Environment::set_formatter(
self : Environment,
f : FormatterFunc,
) -> Unit {
self.formatter = f
self.formatter_is_default = false
}
///|
/// Enable or disable the debug mode.
///
/// When the debug mode is enabled the engine will dump out some of the
/// execution state together with the source information of the executing
/// template when an error is created.
pub fn Environment::set_debug(self : Environment, enabled : Bool) -> Unit {
self.debug = enabled
}
///|
/// Returns the current value of the debug flag.
pub fn Environment::debug(self : Environment) -> Bool {
self.debug
}
///|
/// Sets the optional fuel of the engine.
///
/// When MiniJinja is compiled with fuel support (always the case in this
/// port), the engine will consume fuel on every instruction it executes.
/// Once the engine runs out of fuel, rendering fails with an `OutOfFuel`
/// error. This can be used to limit the amount of work a template can do.
pub fn Environment::set_fuel(self : Environment, fuel : Int64?) -> Unit {
self.fuel = fuel
}
///|
/// Returns the configured fuel.
pub fn Environment::fuel(self : Environment) -> Int64? {
self.fuel
}
///|
/// Sets the syntax for the environment.
///
/// Note that this only affects templates that are added after the syntax
/// was changed.
pub fn Environment::set_syntax(
self : Environment,
syntax : SyntaxConfig,
) -> Unit {
self.syntax_config = syntax
}
///|
/// Returns the current syntax config.
pub fn Environment::syntax(self : Environment) -> SyntaxConfig {
self.syntax_config
}
///|
/// Reconfigures the runtime recursion limit (capped at 500).
pub fn Environment::set_recursion_limit(
self : Environment,
level : Int,
) -> Unit {
self.recursion_limit = if level < max_vm_recursion {
level
} else {
max_vm_recursion
}
}
///|
/// Returns the current max recursion limit.
pub fn Environment::recursion_limit(self : Environment) -> Int {
self.recursion_limit
}
///|
/// Compiles an expression.
///
/// This lets one compile an expression in the template language and receive
/// the output. This lets one use the expressions of the language be used
/// as a minimal scripting language.
pub fn Environment::compile_expression(
self : Environment,
expr : String,
) -> Expression raise TemplateError {
let ast = parse_expr(expr) catch {
err => raise attach_basic_debug_info(err, expr)
}
let g = CodeGenerator::new("", expr)
g.compile_expr(ast)
{ env: self, instructions: g.finish().0, }
}
///|
/// Adds a new filter function.
///
/// Filter functions are functions that can be applied to values in
/// templates. The first argument is the value the filter is applied to.
pub fn Environment::add_filter(
self : Environment,
name : String,
f : NativeFunction,
) -> Unit {
self.unshare_maps()
self.filters[name] = Value::from_function(name, f)
}
///|
/// Removes a filter by name.
pub fn Environment::remove_filter(self : Environment, name : String) -> Unit {
self.unshare_maps()
self.filters.remove(name)
}
///|
/// Adds a new test function.
///
/// Test functions are similar to filters but perform a check on a value
/// where the return value is always considered a boolean.
pub fn Environment::add_test(
self : Environment,
name : String,
f : NativeFunction,
) -> Unit {
self.unshare_maps()
self.tests[name] = Value::from_function(name, f)
}
///|
/// Removes a test by name.
pub fn Environment::remove_test(self : Environment, name : String) -> Unit {
self.unshare_maps()
self.tests.remove(name)
}
///|
/// Adds a new global function.
pub fn Environment::add_function(
self : Environment,
name : String,
f : NativeFunction,
) -> Unit {
self.add_global(name, Value::from_function(name, f))
}
///|
/// Adds a global variable.
pub fn Environment::add_global(
self : Environment,
name : String,
value : Value,
) -> Unit {
self.unshare_maps()
self.globals[name] = value
}
///|
/// Removes a global function or variable by name.
pub fn Environment::remove_global(self : Environment, name : String) -> Unit {
self.unshare_maps()
self.globals.remove(name)
}
///|
/// Returns all globals as name/value pairs (sorted by name).
pub fn Environment::globals(self : Environment) -> Array[(String, Value)] {
sorted_keys(self.globals).map(k => (k, self.globals[k]))
}
///|
/// Creates a copy of the environment. Templates, filters, tests and
/// globals added to the copy do not affect the original and vice versa.
pub fn Environment::clone(self : Environment) -> Environment {
// both environments now share the maps until one of them modifies them
self.maps_shared = true
{
..self,
templates: Map::from_iter(self.templates.iter()),
maps_shared: true,
}
}
///|
/// Returns an empty [`State`] for testing purposes and similar.
pub fn Environment::empty_state(self : Environment) -> State {
State::new(
Context::new(self),
NoEscape,
Instructions::new("", ""),
Map([]),
)
}
///|
fn Environment::get_global(self : Environment, name : String) -> Value? {
self.globals.get(name)
}
///|
fn Environment::get_filter(self : Environment, name : String) -> Value? {
self.filters.get(name)
}
///|
fn Environment::get_test(self : Environment, name : String) -> Value? {
self.tests.get(name)
}
///|
fn Environment::format(
self : Environment,
value : Value,
state : State,
out : Output,
) -> Unit raise TemplateError {
match (self.undefined_behavior, value) {
// this intentionally does not check for SilentUndefined.
(Strict | SemiStrict, Undefined(Default)) =>
raise TemplateError::from_kind(UndefinedError)
_ =>
if self.formatter_is_default {
write_escaped(out, state.auto_escape(), value)
} else {
(self.formatter)(out, state, value)
}
}
}
///|
fn Environment::join_template_path(
self : Environment,
name : String,
parent : String,
) -> String {
match self.path_join_callback {
Some(cb) => cb(name, parent)
None => name
}
}
///|
fn sorted_keys(m : Map[String, Value]) -> Array[String] {
let keys = m.keys().to_array()
keys.sort_by((a, b) => compare_str(a, b))
keys
}
///|
fn Environment::fmt_debug(self : Environment, f : @rfmt.Formatter) -> Unit {
f
.debug_struct("Environment")
.field("globals", f => {
let m = f.debug_map()
for key in sorted_keys(self.globals) {
let value = self.globals[key]
m.entry(f => f.write_str(@rfmt.str_debug(key)), f => value.fmt_debug(f))
|> ignore
}
m.finish()
})
.field("tests", f => {
let l = f.debug_list()
for key in sorted_keys(self.tests) {
l.entry(f => f.write_str(@rfmt.str_debug(key))) |> ignore
}
l.finish()
})
.field("filters", f => {
let l = f.debug_list()
for key in sorted_keys(self.filters) {
l.entry(f => f.write_str(@rfmt.str_debug(key))) |> ignore
}
l.finish()
})
.field("templates", f => {
let l = f.debug_list()
for key in sorted_keys_of(self.templates) {
l.entry(f => f.write_str(@rfmt.str_debug(key))) |> ignore
}
l.finish()
})
.finish()
}
///|
fn[V] sorted_keys_of(m : Map[String, V]) -> Array[String] {
let keys = m.keys().to_array()
keys.sort_by((a, b) => compare_str(a, b))
keys
}
///|
/// The default logic for auto escaping based on file extension.
///
/// * `Html`: `.html`, `.htm`, `.xml`
/// * `Json`: `.json`, `.json5`, `.js`, `.yaml`, `.yml`
/// * `NoEscape`: all others
///
/// Additionally `.j2`, `.jinja2` and `.jinja` are stripped first.
pub fn default_auto_escape_callback(name : String) -> AutoEscape {
let mut name = name
for ext in [".j2", ".jinja2", ".jinja"] {
if name.has_suffix(ext) {
name = name.view(end_offset=name.length() - ext.length()).to_owned()
break
}
}
let ext = match name.rev_find(".") {
Some(idx) => name.view(start_offset=idx + 1).to_owned()
None => name
}
match ext {
"html" | "htm" | "xml" => Html
"json" | "json5" | "js" | "yaml" | "yml" => Json
_ => NoEscape
}
}
///|
fn attach_basic_debug_info(
err : TemplateError,
source : String,
) -> TemplateError {
err.attach_debug_info({
template_source: Some(source),
referenced_locals: Map([]),
})
err
}
///|
let builtin_filters : Map[String, Value] = {
let rv : Map[String, Value] = Map([])
register_builtin_filters(rv)
rv
}
///|
let builtin_tests : Map[String, Value] = {
let rv : Map[String, Value] = Map([])
register_builtin_tests(rv)
rv
}
///|
let builtin_globals : Map[String, Value] = {
let rv : Map[String, Value] = Map([])
register_builtin_globals(rv)
rv
}
///|
/// Returns one of the built-in filters by name (independent of any
/// environment). The returned value is callable with `Value::call`; the
/// first argument is the value the filter is applied to.
///
/// ```mbt check
/// test {
/// let env = @minijinja.Environment::empty()
/// let state = env.empty_state()
/// guard @minijinja.builtin_filter("upper") is Some(upper) else {
/// fail("missing")
/// }
/// inspect(
/// upper.call(state, [@minijinja.Value::from_string("hi")]),
/// content="HI",
/// )
/// }
/// ```
pub fn builtin_filter(name : String) -> Value? {
builtin_filters.get(name)
}
///|
/// Returns one of the built-in tests by name (independent of any
/// environment).
pub fn builtin_test(name : String) -> Value? {
builtin_tests.get(name)
}
///|
/// Returns one of the built-in global functions (`range`, `dict`, `debug`,
/// `namespace`) by name.
pub fn builtin_function(name : String) -> Value? {
builtin_globals.get(name)
}
///|
/// Copies the (shared) filter, test and global maps before modifying them.
fn Environment::unshare_maps(self : Environment) -> Unit {
if self.maps_shared {
self.filters = Map::from_iter(self.filters.iter())
self.tests = Map::from_iter(self.tests.iter())
self.globals = Map::from_iter(self.globals.iter())
self.maps_shared = false
}
}
///|
/// Returns all loaded templates (sorted by name).
pub fn Environment::templates(self : Environment) -> Array[(String, Template)] {
sorted_keys_of(self.templates).map(name => {
(name, { env: self, compiled: self.templates[name], })
})
}