///|
pub struct RenderOptions {
priv autoescape : Bool
priv trim_blocks : Bool
priv lstrip_blocks : Bool
priv strict_undefined : Bool
priv fuel : Int
priv max_recursion : Int
priv max_output_size : Int
priv max_template_size : Int
priv max_range_size : Int
priv max_parse_depth : Int
priv max_include_depth : Int
priv max_cache_size : Int
priv sandboxed : Bool
priv enable_macros : Bool
priv enable_multi_template : Bool
priv enable_loop_controls : Bool
}
///|
/// Read-only metadata passed to context-aware extensions.
pub struct ExtensionContext {
priv template_name : String
priv autoescape : Bool
priv sandboxed : Bool
}
///|
pub fn ExtensionContext::template_name(self : ExtensionContext) -> String {
self.template_name
}
///|
pub fn ExtensionContext::autoescape(self : ExtensionContext) -> Bool {
self.autoescape
}
///|
pub fn ExtensionContext::sandboxed(self : ExtensionContext) -> Bool {
self.sandboxed
}
///|
pub fn RenderOptions::default() -> RenderOptions {
{
autoescape: false,
trim_blocks: false,
lstrip_blocks: false,
strict_undefined: true,
fuel: 100000,
max_recursion: 128,
max_output_size: 16 * 1024 * 1024,
max_template_size: 4 * 1024 * 1024,
max_range_size: 100000,
max_parse_depth: 256,
max_include_depth: 64,
max_cache_size: 400,
sandboxed: false,
enable_macros: true,
enable_multi_template: true,
enable_loop_controls: true,
}
}
///|
pub fn RenderOptions::with_autoescape(
self : RenderOptions,
enabled : Bool,
) -> RenderOptions {
{ ..self, autoescape: enabled }
}
///|
pub fn RenderOptions::with_whitespace_control(
self : RenderOptions,
trim_blocks : Bool,
lstrip_blocks : Bool,
) -> RenderOptions {
{ ..self, trim_blocks, lstrip_blocks }
}
///|
pub fn RenderOptions::with_strict_undefined(
self : RenderOptions,
enabled : Bool,
) -> RenderOptions {
{ ..self, strict_undefined: enabled }
}
///|
pub fn RenderOptions::with_limits(
self : RenderOptions,
fuel : Int,
max_recursion : Int,
max_output_size : Int,
) -> RenderOptions {
{ ..self, fuel, max_recursion, max_output_size }
}
///|
pub fn RenderOptions::with_compile_limits(
self : RenderOptions,
max_template_size : Int,
max_range_size : Int,
) -> RenderOptions {
{ ..self, max_template_size, max_range_size }
}
///|
pub fn RenderOptions::with_structural_limits(
self : RenderOptions,
max_parse_depth : Int,
max_include_depth : Int,
) -> RenderOptions {
{ ..self, max_parse_depth, max_include_depth }
}
///|
/// Set the parsed-template cache capacity. Zero disables the cache.
pub fn RenderOptions::with_cache_capacity(
self : RenderOptions,
max_cache_size : Int,
) -> RenderOptions {
{ ..self, max_cache_size, }
}
///|
/// Enable the restricted execution policy and force HTML autoescape.
pub fn RenderOptions::with_sandbox(
self : RenderOptions,
enabled : Bool,
) -> RenderOptions {
{
..self,
sandboxed: enabled,
autoescape: if enabled {
true
} else {
self.autoescape
},
}
}
///|
/// Enable or disable optional language feature groups at compile time.
pub fn RenderOptions::with_features(
self : RenderOptions,
macros : Bool,
multi_template : Bool,
loop_controls : Bool,
) -> RenderOptions {
{
..self,
enable_macros: macros,
enable_multi_template: multi_template,
enable_loop_controls: loop_controls,
}
}
///|
pub struct Environment {
priv sources : Map[String, String]
priv compiled : Map[String, Template]
priv cache_order : Array[String]
priv loader_sources : Map[String, Bool]
priv loader_versions : Map[String, String]
priv filters : Map[String, (Value, Array[Value]) -> Result[Value, String]]
priv functions : Map[String, (Array[Value]) -> Result[Value, String]]
priv tests : Map[String, (Value, Array[Value]) -> Result[Bool, String]]
priv context_filters : Map[
String,
(ExtensionContext, Value, Array[Value], Map[String, Value]) -> Result[
Value,
String,
],
]
priv context_functions : Map[
String,
(ExtensionContext, Array[Value], Map[String, Value]) -> Result[
Value,
String,
],
]
priv context_tests : Map[
String,
(ExtensionContext, Value, Array[Value], Map[String, Value]) -> Result[
Bool,
String,
],
]
priv restricted_extensions : Map[String, Bool]
priv loader : Ref[((String) -> Result[String, String])?]
priv versioned_loader : Ref[((String) -> Result[(String, String), String])?]
priv autoescape_callback : Ref[((String) -> Bool)?]
priv options : Ref[RenderOptions]
}
///|
pub struct CompiledTemplate {
priv environment : Environment
priv name : String
priv template : Template
}
///|
pub fn Environment::new() -> Environment {
let environment : Environment = {
sources: Map([]),
compiled: Map([]),
cache_order: [],
loader_sources: Map([]),
loader_versions: Map([]),
filters: Map([]),
functions: Map([]),
tests: Map([]),
context_filters: Map([]),
context_functions: Map([]),
context_tests: Map([]),
restricted_extensions: Map([]),
loader: { val: None },
versioned_loader: { val: None },
autoescape_callback: { val: None },
options: { val: RenderOptions::default() },
}
environment.install_builtins()
environment.restricted_extensions.clear()
environment
}
///|
pub fn Environment::set_options(
self : Environment,
options : RenderOptions,
) -> Unit {
self.options.val = options
// Compiled text depends on block whitespace policy.
self.compiled.clear()
self.cache_order.clear()
}
///|
fn Environment::options(self : Environment) -> RenderOptions {
self.options.val
}
///|
fn Environment::options_for(
self : Environment,
template_name : String,
) -> RenderOptions {
match self.autoescape_callback.val {
Some(select) =>
{
..self.options.val,
autoescape: self.options.val.sandboxed || select(template_name),
}
None => self.options.val
}
}
///|
/// Select autoescape per logical template name (for example by suffix).
pub fn Environment::set_autoescape_callback(
self : Environment,
select : (String) -> Bool,
) -> Unit {
self.autoescape_callback.val = Some(select)
}
///|
pub fn Environment::clear_autoescape_callback(self : Environment) -> Unit {
self.autoescape_callback.val = None
}
///|
pub fn Environment::set_loader(
self : Environment,
loader : (String) -> Result[String, String],
) -> Unit {
for name in self.loader_sources.keys().to_array() {
self.sources.remove(name)
self.compiled.remove(name)
self.remove_cache_order(name)
}
self.loader_sources.clear()
self.loader_versions.clear()
self.loader.val = Some(loader)
self.versioned_loader.val = None
}
///|
/// Install a loader that returns `(source, version)` and automatically
/// recompiles an entry when its version changes.
pub fn Environment::set_versioned_loader(
self : Environment,
loader : (String) -> Result[(String, String), String],
) -> Unit {
for name in self.loader_sources.keys().to_array() {
self.sources.remove(name)
self.compiled.remove(name)
self.remove_cache_order(name)
}
self.loader_sources.clear()
self.loader_versions.clear()
self.loader.val = None
self.versioned_loader.val = Some(loader)
}
///|
/// Clear parsed templates while preserving registered source strings.
pub fn Environment::clear_cache(self : Environment) -> Unit {
self.compiled.clear()
self.cache_order.clear()
}
///|
/// Invalidate one parsed template and any loader-owned source snapshot.
pub fn Environment::reload_template(self : Environment, name : String) -> Unit {
self.compiled.remove(name)
self.remove_cache_order(name)
if self.loader_sources.contains(name) {
self.sources.remove(name)
self.loader_sources.remove(name)
self.loader_versions.remove(name)
}
}
///|
pub fn Environment::remove_template(self : Environment, name : String) -> Unit {
self.sources.remove(name)
self.compiled.remove(name)
self.remove_cache_order(name)
self.loader_sources.remove(name)
self.loader_versions.remove(name)
}
///|
pub fn Environment::add_filter(
self : Environment,
name : String,
filter : (Value, Array[Value]) -> Result[Value, String],
) -> Unit {
self.filters[name] = filter
self.restricted_extensions[name] = true
}
///|
pub fn Environment::remove_filter(self : Environment, name : String) -> Unit {
self.filters.remove(name)
self.context_filters.remove(name)
}
///|
pub fn Environment::add_context_filter(
self : Environment,
name : String,
filter : (ExtensionContext, Value, Array[Value], Map[String, Value]) -> Result[
Value,
String,
],
) -> Unit {
self.context_filters[name] = filter
self.restricted_extensions[name] = true
}
///|
pub fn Environment::add_function(
self : Environment,
name : String,
function : (Array[Value]) -> Result[Value, String],
) -> Unit {
self.functions[name] = function
self.restricted_extensions[name] = true
}
///|
pub fn Environment::remove_function(self : Environment, name : String) -> Unit {
self.functions.remove(name)
self.context_functions.remove(name)
}
///|
pub fn Environment::add_context_function(
self : Environment,
name : String,
function : (ExtensionContext, Array[Value], Map[String, Value]) -> Result[
Value,
String,
],
) -> Unit {
self.context_functions[name] = function
self.restricted_extensions[name] = true
}
///|
pub fn Environment::add_test(
self : Environment,
name : String,
test_function : (Value, Array[Value]) -> Result[Bool, String],
) -> Unit {
self.tests[name] = test_function
self.restricted_extensions[name] = true
}
///|
pub fn Environment::remove_test(self : Environment, name : String) -> Unit {
self.tests.remove(name)
self.context_tests.remove(name)
}
///|
pub fn Environment::add_context_test(
self : Environment,
name : String,
test_function : (ExtensionContext, Value, Array[Value], Map[String, Value]) -> Result[
Bool,
String,
],
) -> Unit {
self.context_tests[name] = test_function
self.restricted_extensions[name] = true
}
///|
/// Allow a registered extension to execute while sandbox mode is enabled.
pub fn Environment::allow_extension_in_sandbox(
self : Environment,
name : String,
) -> Unit {
self.restricted_extensions.remove(name)
}
///|
pub fn Environment::add_template(
self : Environment,
name : String,
source : String,
) -> Unit raise JinjaError {
validate_template_name(name)
let template = self.compile_source(name, source)
self.sources[name] = source
self.cache_template(name, template)
self.loader_sources.remove(name)
}
///|
fn Environment::resolve_template(
self : Environment,
name : String,
) -> Template raise JinjaError {
validate_template_name(name)
if self.loader_sources.contains(name) || !self.sources.contains(name) {
match self.versioned_loader.val {
Some(loader) => return self.resolve_versioned_template(name, loader)
None => ()
}
}
match self.compiled.get(name) {
Some(template) => {
self.touch_cache(name)
template
}
None => {
let (source, from_loader) = match self.sources.get(name) {
Some(source) => (source, false)
None =>
match self.loader.val {
Some(loader) =>
match loader(name) {
Ok(source) => (source, true)
Err(message) => raise RenderError(message)
}
None => raise RenderError("Template not found: " + name)
}
}
let template = self.compile_source(name, source)
if !from_loader || self.options.val.max_cache_size > 0 {
self.sources[name] = source
}
self.cache_template(name, template)
if from_loader && self.options.val.max_cache_size > 0 {
self.loader_sources[name] = true
}
template
}
}
}
///|
fn Environment::resolve_versioned_template(
self : Environment,
name : String,
loader : (String) -> Result[(String, String), String],
) -> Template raise JinjaError {
let (source, version) = match loader(name) {
Ok(loaded) => loaded
Err(message) => raise RenderError(message)
}
match (self.loader_versions.get(name), self.compiled.get(name)) {
(Some(previous), Some(template)) if previous == version => {
self.touch_cache(name)
return template
}
_ => ()
}
let template = self.compile_source(name, source)
if self.options.val.max_cache_size > 0 {
self.sources[name] = source
self.loader_sources[name] = true
self.loader_versions[name] = version
}
self.cache_template(name, template)
template
}
///|
fn Environment::compile_source(
self : Environment,
name : String,
source : String,
) -> Template raise JinjaError {
if utf8_length(source) > self.options.val.max_template_size {
raise LexerError(
"Template '" + name + "' exceeds the configured source size limit",
)
}
parse(
tokenize(preprocess_whitespace(source, self.options.val)),
max_depth=self.options.val.max_parse_depth,
enable_macros=self.options.val.enable_macros,
enable_multi_template=self.options.val.enable_multi_template,
enable_loop_controls=self.options.val.enable_loop_controls,
) catch {
LexerError(message) =>
raise LexerError("Template '" + name + "': " + message)
ParseError(message) =>
raise ParseError("Template '" + name + "': " + message)
RenderError(message) =>
raise RenderError("Template '" + name + "': " + message)
}
}
///|
fn Environment::remove_cache_order(self : Environment, name : String) -> Unit {
for index, cached_name in self.cache_order {
if cached_name == name {
ignore(self.cache_order.remove(index))
return
}
}
}
///|
fn Environment::touch_cache(self : Environment, name : String) -> Unit {
self.remove_cache_order(name)
self.cache_order.push(name)
}
///|
fn Environment::cache_template(
self : Environment,
name : String,
template : Template,
) -> Unit {
let capacity = self.options.val.max_cache_size
if capacity <= 0 {
self.compiled.remove(name)
self.remove_cache_order(name)
return
}
self.compiled[name] = template
self.touch_cache(name)
while self.cache_order.length() > capacity {
let evicted = self.cache_order.remove(0)
self.compiled.remove(evicted)
if self.loader_sources.contains(evicted) {
self.sources.remove(evicted)
self.loader_sources.remove(evicted)
self.loader_versions.remove(evicted)
}
}
}
///|
/// Apply environment-level `trim_blocks` and `lstrip_blocks` before lexing.
/// Explicit Jinja `-` markers remain handled by the lexer/parser.
fn preprocess_whitespace(source : String, options : RenderOptions) -> String {
if !options.trim_blocks && !options.lstrip_blocks {
return source
}
let chars = source.to_array()
let out : Array[Char] = []
let mut i = 0
while i < chars.length() {
if i + 1 < chars.length() && chars[i] == '{' && chars[i + 1] == '%' {
if options.lstrip_blocks {
lstrip_block_indentation(out)
}
match match_named_block_tag(chars, i, "raw") {
Some(opening) =>
match find_named_block_tag(chars, opening.next_index, "endraw") {
Some((content_end, closing)) => {
for index in i.. ()
}
None => ()
}
let mut end = i + 2
while end + 1 < chars.length() &&
!(chars[end] == '%' && chars[end + 1] == '}') {
end += 1
}
if end + 1 >= chars.length() {
// Let the lexer produce the typed unterminated-tag error.
while i < chars.length() {
out.push(chars[i])
i += 1
}
break
}
for index in i..<(end + 2) {
out.push(chars[index])
}
i = end + 2
if options.trim_blocks {
i = skip_one_newline(chars, i)
}
} else {
out.push(chars[i])
i += 1
}
}
String::from_array(out)
}
///|
fn lstrip_block_indentation(out : Array[Char]) -> Unit {
let mut line_is_indentation = true
let mut cursor = out.length() - 1
while cursor >= 0 {
match out[cursor] {
'\n' | '\r' => break
' ' | '\t' => cursor -= 1
_ => {
line_is_indentation = false
break
}
}
}
if line_is_indentation {
while out.length() > 0 {
match out.last() {
Some(' ') | Some('\t') => ignore(out.pop())
_ => break
}
}
}
}
///|
fn skip_one_newline(chars : Array[Char], start : Int) -> Int {
let mut index = start
if index < chars.length() && chars[index] == '\r' {
index += 1
if index < chars.length() && chars[index] == '\n' {
index += 1
}
} else if index < chars.length() && chars[index] == '\n' {
index += 1
}
index
}
///|
pub fn Environment::get_template(
self : Environment,
name : String,
) -> CompiledTemplate raise JinjaError {
let template = self.resolve_template(name)
{ environment: self, name, template }
}
///|
pub fn[T : ToJson] Environment::render(
self : Environment,
name : String,
value : T,
) -> String raise JinjaError {
self.get_template(name).render(value)
}
///|
pub fn[T : ToJson] CompiledTemplate::render(
self : CompiledTemplate,
value : T,
) -> String raise JinjaError {
self.render_json(value.to_json())
}
///|
pub fn CompiledTemplate::render_json(
self : CompiledTemplate,
value : Json,
) -> String raise JinjaError {
let context = context_from_json(value)
render_compiled(self.template, context, self.environment, self.name)
}
///|
/// Render JSON data incrementally into a caller-provided sink.
pub fn CompiledTemplate::render_json_to(
self : CompiledTemplate,
value : Json,
write : (String) -> Unit,
) -> Unit raise JinjaError {
let context = context_from_json(value)
render_compiled_to(self.template, context, self.environment, self.name, write)
}
///|
/// Convert a MoonBit value with `ToJson` and render it incrementally.
pub fn[T : ToJson] CompiledTemplate::render_to(
self : CompiledTemplate,
value : T,
write : (String) -> Unit,
) -> Unit raise JinjaError {
self.render_json_to(value.to_json(), write)
}
///|
fn context_from_json(value : Json) -> Map[String, Value] {
match value {
Object(entries) => {
let context : Map[String, Value] = Map([])
for key, item in entries {
context[key] = value_from_json(item)
}
context
}
_ => { "value": value_from_json(value) }
}
}
///|
fn value_from_json(value : Json) -> Value {
match value {
Null => Value::Null
True => BoolValue(true)
False => BoolValue(false)
Number(number, repr~) => {
ignore(repr)
if number == number.trunc() {
IntValue(number.to_int())
} else {
DoubleValue(number)
}
}
String(text) => StrValue(text)
Array(items) => ListValue(items.map(value_from_json))
Object(entries) => {
let result : Map[String, Value] = Map([])
for key, item in entries {
result[key] = value_from_json(item)
}
MapValue(result)
}
}
}
///|
fn validate_template_name(name : String) -> Unit raise JinjaError {
if name == "" {
raise RenderError("Template name cannot be empty")
}
let unified = name.replace(old="\\", new="/")
if unified.has_prefix("/") {
raise RenderError("Absolute template names are not allowed")
}
if unified.length() >= 2 &&
unified[1].to_int() == ':'.to_int() &&
(
(
'a'.to_int() <= unified[0].to_int() &&
unified[0].to_int() <= 'z'.to_int()
) ||
(
'A'.to_int() <= unified[0].to_int() &&
unified[0].to_int() <= 'Z'.to_int()
)
) {
raise RenderError("Drive-letter template names are not allowed")
}
for part in unified.split("/") {
if part == ".." {
raise RenderError("Parent template paths are not allowed")
}
}
}