///|
priv struct Context {
scopes : Array[Map[String, Value]]
}
///|
fn Context::new(root : Map[String, Value]) -> Context {
{ scopes: [root.copy()] }
}
///|
fn Context::push_values(self : Context, values : Map[String, Value]) -> Unit {
self.scopes.push(values)
}
///|
fn Context::pop(self : Context) -> Unit {
if self.scopes.length() > 1 {
ignore(self.scopes.pop())
}
}
///|
fn Context::set(self : Context, name : String, value : Value) -> Unit {
self.scopes[self.scopes.length() - 1][name] = value
}
///|
fn Context::resolve(self : Context, path : VarPath) -> Value? {
guard path.parts is [first, .. rest] else { return None }
let mut current : Value? = None
for index = self.scopes.length() - 1; index >= 0; index = index - 1 {
match self.scopes[index].get(first) {
Some(value) => {
current = Some(value)
break
}
None => ()
}
}
for part in rest {
current = match current {
Some(MapValue(values)) => values.get(part)
_ => return None
}
}
current
}
///|
fn Context::snapshot(self : Context) -> Array[Map[String, Value]] {
self.scopes.map(scope => scope.copy())
}
///|
fn Context::copy(self : Context) -> Context {
{ scopes: self.snapshot() }
}
///|
priv struct BlockFrame {
name : String
index : Int
}
///|
priv struct MacroDefinition {
name : String
params : Array[MacroParam]
body : Array[Expr]
captured : Array[Map[String, Value]]
}
///|
priv struct EngineState {
environment : Environment
options : RenderOptions
mut fuel : Int
mut recursion : Int
mut output_size : Int
block_chains : Map[String, Array[Array[Expr]]]
block_stack : Array[BlockFrame]
macros : Map[String, MacroDefinition]
template_stack : Array[String]
}
///|
priv enum Flow {
Normal
BreakFlow
ContinueFlow
} derive(Eq)
///|
fn EngineState::consume(
self : EngineState,
amount : Int,
) -> Unit raise JinjaError {
self.fuel -= amount
if self.fuel < 0 {
raise RenderError("Template fuel exhausted")
}
}
///|
priv enum RenderOutput {
Buffered(StringBuilder)
Streaming((String) -> Unit)
}
///|
fn RenderOutput::write(self : RenderOutput, text : String) -> Unit {
match self {
Buffered(buffer) => buffer.write_string(text)
Streaming(write) => write(text)
}
}
///|
fn RenderOutput::to_string(self : RenderOutput) -> String {
match self {
Buffered(buffer) => buffer.to_string()
Streaming(_) => ""
}
}
///|
fn EngineState::enter(self : EngineState) -> Unit raise JinjaError {
if self.recursion >= self.options.max_recursion {
raise RenderError("Template recursion limit exceeded")
}
self.recursion += 1
}
///|
fn EngineState::leave(self : EngineState) -> Unit {
self.recursion -= 1
}
///|
fn write_checked(
output : RenderOutput,
text : String,
state : EngineState,
) -> Unit raise JinjaError {
state.output_size += utf8_length(text)
if state.output_size > state.options.max_output_size {
raise RenderError("Template output size limit exceeded")
}
output.write(text)
}
///|
fn render_compiled(
template : Template,
root : Map[String, Value],
environment : Environment,
name : String,
) -> String raise JinjaError {
let output = RenderOutput::Buffered(StringBuilder())
render_compiled_into(template, root, environment, name, output) catch {
RenderError(message) =>
raise RenderError("Template '" + name + "': " + message)
error => raise error
}
output.to_string()
}
///|
fn render_compiled_to(
template : Template,
root : Map[String, Value],
environment : Environment,
name : String,
write : (String) -> Unit,
) -> Unit raise JinjaError {
render_compiled_into(
template,
root,
environment,
name,
RenderOutput::Streaming(write),
) catch {
RenderError(message) =>
raise RenderError("Template '" + name + "': " + message)
error => raise error
}
}
///|
fn render_compiled_into(
template : Template,
root : Map[String, Value],
environment : Environment,
name : String,
output : RenderOutput,
) -> Unit raise JinjaError {
let (root_template, chains, prefixes) = resolve_inheritance(
template, environment,
)
let options = environment.options_for(name)
let state : EngineState = {
environment,
options,
fuel: options.fuel,
recursion: 0,
output_size: 0,
block_chains: chains,
block_stack: [],
macros: Map([]),
template_stack: [name],
}
let context = Context::new(root)
for prefix in prefixes {
ignore(render_engine_nodes(prefix, context, state, output))
}
ignore(render_engine_nodes(root_template.body, context, state, output))
}
///|
fn resolve_inheritance(
template : Template,
environment : Environment,
) -> (Template, Map[String, Array[Array[Expr]]], Array[Array[Expr]]) raise JinjaError {
let templates : Array[Template] = [template]
let mut current = template
let seen : Map[String, Bool] = Map([])
let mut inheritance_depth = 0
while current.parent is Some(parent_name) {
inheritance_depth += 1
if inheritance_depth > environment.options().max_include_depth {
raise RenderError("Template inheritance depth limit exceeded")
}
if seen.contains(parent_name) {
raise RenderError("Template inheritance cycle at: " + parent_name)
}
seen[parent_name] = true
current = environment.resolve_template(parent_name)
templates.push(current)
}
let chains : Map[String, Array[Array[Expr]]] = Map([])
let prefixes : Array[Array[Expr]] = []
for item in templates {
if !item.prefix.is_empty() {
prefixes.push(item.prefix)
}
let blocks = collect_blocks(item.body)
for name, body in blocks {
match chains.get(name) {
Some(chain) => chain.push(body)
None => chains[name] = [body]
}
}
}
(current, chains, prefixes)
}
///|
fn collect_blocks(nodes : Array[Expr]) -> Map[String, Array[Expr]] {
let blocks : Map[String, Array[Expr]] = Map([])
collect_blocks_into(nodes, blocks)
blocks
}
///|
fn collect_blocks_into(
nodes : Array[Expr],
blocks : Map[String, Array[Expr]],
) -> Unit {
for node in nodes {
match node {
BlockNode(name, body) => {
blocks[name] = body
collect_blocks_into(body, blocks)
}
IfBlock(_, then_body, else_body) => {
collect_blocks_into(then_body, blocks)
collect_blocks_into(else_body, blocks)
}
ForBlock(_, _, body, else_body) => {
collect_blocks_into(body, blocks)
collect_blocks_into(else_body, blocks)
}
WithBlock(_, body) | MacroDef(_, _, body) | CallBlock(_, body) =>
collect_blocks_into(body, blocks)
_ => ()
}
}
}
///|
fn render_engine_nodes(
nodes : Array[Expr],
context : Context,
state : EngineState,
output : RenderOutput,
) -> Flow raise JinjaError {
for node in nodes {
state.consume(1)
match node {
TextNode(text, strip_start, strip_end) => {
let mut rendered = text
if strip_start {
rendered = trim_ws_start(rendered)
}
if strip_end {
rendered = trim_ws_end(rendered)
}
write_checked(output, rendered, state)
}
RawText(text) => write_checked(output, text, state)
Literal(_)
| ListLiteral(_)
| MapLiteral(_)
| Var(_)
| Unary(_, _)
| Binary(_, _, _)
| Conditional(_, _, _)
| Index(_, _)
| TestCall(_, _, _, _, _)
| FunctionCall(_)
| FilterChain(_, _) => {
let value = eval_engine_expr(node, context, state)
reject_undefined(value, state, "output")
write_checked(
output,
display_string(value, autoescape=state.options.autoescape),
state,
)
}
IfBlock(condition, then_nodes, else_nodes) => {
let condition_value = eval_engine_expr(condition, context, state)
reject_undefined(condition_value, state, "condition")
let selected = if is_truthy(condition_value) {
then_nodes
} else {
else_nodes
}
let flow = render_nested_nodes(selected, context, state, output)
if flow != Normal {
return flow
}
}
ForBlock(name, iterable, body, else_body) =>
match eval_engine_expr(iterable, context, state) {
ListValue(items) =>
if items.is_empty() {
let flow = render_nested_nodes(else_body, context, state, output)
if flow != Normal {
return flow
}
} else {
for index, item in items {
let loop_values : Map[String, Value] = Map([])
loop_values[name] = item
loop_values["loop"] = MapValue({
"index": IntValue(index + 1),
"index0": IntValue(index),
"first": BoolValue(index == 0),
"last": BoolValue(index == items.length() - 1),
"length": IntValue(items.length()),
})
context.push_values(loop_values)
let flow = render_nested_nodes(body, context, state, output)
context.pop()
match flow {
BreakFlow => break
ContinueFlow => continue
Normal => ()
}
}
}
_ => raise RenderError("for expects a list")
}
IncludeNode(name, with_context) => {
if state.template_stack.length() >= state.options.max_include_depth {
raise RenderError("Template include depth limit exceeded")
}
if state.template_stack.contains(name) {
raise RenderError(
"Template include cycle: " +
state.template_stack.join(" -> ") +
" -> " +
name,
)
}
state.enter()
state.template_stack.push(name)
let included = state.environment.resolve_template(name)
let (included_root, included_chains, included_prefixes) = resolve_inheritance(
included,
state.environment,
)
let nested : EngineState = {
..state,
options: state.environment.options_for(name),
block_chains: included_chains,
block_stack: [],
}
let include_context = if with_context {
context.copy()
} else {
Context::new(Map([]))
}
for prefix in included_prefixes {
ignore(render_engine_nodes(prefix, include_context, nested, output))
}
let flow = render_engine_nodes(
included_root.body,
include_context,
nested,
output,
) catch {
RenderError(message) => {
ignore(state.template_stack.pop())
state.leave()
raise RenderError("Template '" + name + "': " + message)
}
error => {
ignore(state.template_stack.pop())
state.leave()
raise error
}
}
state.fuel = nested.fuel
state.output_size = nested.output_size
ignore(state.template_stack.pop())
state.leave()
if flow != Normal {
return flow
}
}
BlockNode(name, body) => {
let chain = match state.block_chains.get(name) {
Some(chain) => chain
None => [body]
}
let flow = render_block(name, 0, chain, context, state, output)
if flow != Normal {
return flow
}
}
SetStmt(name, value) =>
context.set(name, eval_engine_expr(value, context, state))
WithBlock(assignments, body) => {
let scope : Map[String, Value] = Map([])
for name, value in assignments {
scope[name] = eval_engine_expr(value, context, state)
}
context.push_values(scope)
let flow = render_nested_nodes(body, context, state, output)
context.pop()
if flow != Normal {
return flow
}
}
MacroDef(name, params, body) =>
state.macros[name] = {
name,
params,
body,
captured: context.snapshot(),
}
CallBlock(call, body) => {
let previous = state.macros.get("caller")
state.macros["caller"] = {
name: "caller",
params: [],
body,
captured: context.snapshot(),
}
let rendered = eval_engine_function(call, context, state)
match previous {
Some(caller) => state.macros["caller"] = caller
None => state.macros.remove("caller")
}
write_checked(
output,
display_string(rendered, autoescape=state.options.autoescape),
state,
)
}
ImportNode(filename, import_name) =>
register_imported_macros(filename, Some(import_name), None, state)
FromImportNode(filename, names) =>
register_imported_macros(filename, None, Some(names), state)
Break => return BreakFlow
Continue => return ContinueFlow
ExtendsNode(_) =>
raise RenderError("extends is only valid at template top level")
}
}
Normal
}
///|
fn render_nested_nodes(
nodes : Array[Expr],
context : Context,
state : EngineState,
output : RenderOutput,
) -> Flow raise JinjaError {
state.enter()
let flow = render_engine_nodes(nodes, context, state, output)
state.leave()
flow
}
///|
fn render_block(
name : String,
index : Int,
chain : Array[Array[Expr]],
context : Context,
state : EngineState,
output : RenderOutput,
) -> Flow raise JinjaError {
if index >= chain.length() {
return Normal
}
state.block_stack.push({ name, index })
let flow = render_nested_nodes(chain[index], context, state, output)
ignore(state.block_stack.pop())
flow
}
///|
fn eval_engine_expr(
expression : Expr,
context : Context,
state : EngineState,
) -> Value raise JinjaError {
state.consume(1)
match expression {
Literal(value) => value
ListLiteral(expressions) =>
ListValue(
expressions.map(expression => {
eval_engine_expr(expression, context, state)
}),
)
MapLiteral(entries) => {
let values : Map[String, Value] = Map([])
for entry in entries {
let (key, expression) = entry
values[key] = eval_engine_expr(expression, context, state)
}
MapValue(values)
}
Var(path) => context.resolve(path).unwrap_or(Undefined)
Unary(operator, operand) => {
let value = eval_engine_expr(operand, context, state)
reject_undefined(value, state, "unary expression")
match (operator, value) {
(UnOp::Not, value) => BoolValue(!is_truthy(value))
(UnOp::Neg, IntValue(number)) => IntValue(-number)
(UnOp::Neg, DoubleValue(number)) => DoubleValue(-number)
(UnOp::Pos, IntValue(_) | DoubleValue(_) as number) => number
_ => raise RenderError("Invalid unary operation")
}
}
Binary(left, operator, right) => {
if is_comparison(operator) && is_comparison_expression(left) {
return eval_comparison_chain(expression, context, state)
}
let lhs = eval_engine_expr(left, context, state)
reject_undefined(lhs, state, "binary expression")
if operator == BinOp::And && !is_truthy(lhs) {
return lhs
}
if operator == BinOp::Or && is_truthy(lhs) {
return lhs
}
let rhs = eval_engine_expr(right, context, state)
reject_undefined(rhs, state, "binary expression")
if operator == BinOp::And || operator == BinOp::Or {
rhs
} else {
apply_binop(lhs, operator, rhs)
}
}
Conditional(value, condition, fallback) => {
let condition_value = eval_engine_expr(condition, context, state)
reject_undefined(condition_value, state, "conditional expression")
if is_truthy(condition_value) {
eval_engine_expr(value, context, state)
} else {
match fallback {
Some(otherwise) => eval_engine_expr(otherwise, context, state)
None => Undefined
}
}
}
Index(base, index) => {
let value = eval_engine_expr(base, context, state)
let key = eval_engine_expr(index, context, state)
match (value, key) {
(ListValue(items), IntValue(position)) =>
if position < 0 || position >= items.length() {
Undefined
} else {
items[position]
}
(MapValue(entries), StrValue(name) | SafeStr(name)) =>
entries.get(name).unwrap_or(Undefined)
(StrValue(text) | SafeStr(text), IntValue(position)) => {
let chars = text.to_array()
if position < 0 || position >= chars.length() {
Undefined
} else {
StrValue(chars[position].to_string())
}
}
_ => raise RenderError("Index expects a list, map, or string")
}
}
TestCall(subject, name, negated, argument_exprs, keyword_exprs) => {
ensure_extension_allowed(state, name)
let value = eval_engine_expr(subject, context, state)
let arguments = eval_engine_arguments(argument_exprs, context, state)
let keywords = eval_engine_keywords(keyword_exprs, context, state)
let isolate = state.environment.restricted_extensions.contains(name)
let passed = match state.environment.context_tests.get(name) {
Some(test_function) =>
match
test_function(
extension_context(state),
if isolate {
deep_clone_value(value)
} else {
value
},
if isolate {
arguments.map(deep_clone_value)
} else {
arguments
},
if isolate {
deep_clone_map(keywords)
} else {
keywords
},
) {
Ok(result) => result
Err(message) => raise RenderError(message)
}
None =>
match state.environment.tests.get(name) {
Some(test_function) => {
if !keywords.is_empty() {
raise RenderError(
"Test does not accept keyword arguments: " + name,
)
}
match
test_function(
if isolate {
deep_clone_value(value)
} else {
value
},
if isolate {
arguments.map(deep_clone_value)
} else {
arguments
},
) {
Ok(result) => result
Err(message) => raise RenderError(message)
}
}
None => raise RenderError("Unknown test: " + name)
}
}
BoolValue(if negated { !passed } else { passed })
}
FunctionCall(call) => eval_engine_function(call, context, state)
FilterChain(base, calls) => {
let mut value = eval_engine_expr(base, context, state)
for call in calls {
ensure_extension_allowed(state, call.name)
let arguments = eval_engine_arguments(call.args, context, state)
let keywords = eval_engine_keywords(call.kwargs, context, state)
let isolate = state.environment.restricted_extensions.contains(
call.name,
)
value = match state.environment.context_filters.get(call.name) {
Some(filter) =>
match
filter(
extension_context(state),
if isolate {
deep_clone_value(value)
} else {
value
},
if isolate {
arguments.map(deep_clone_value)
} else {
arguments
},
if isolate {
deep_clone_map(keywords)
} else {
keywords
},
) {
Ok(result) =>
if isolate {
deep_clone_value(result)
} else {
result
}
Err(message) => raise RenderError(message)
}
None =>
match state.environment.filters.get(call.name) {
Some(filter) => {
if !keywords.is_empty() {
raise RenderError(
"Filter does not accept keyword arguments: " + call.name,
)
}
match
filter(
if isolate {
deep_clone_value(value)
} else {
value
},
if isolate {
arguments.map(deep_clone_value)
} else {
arguments
},
) {
Ok(result) =>
if isolate {
deep_clone_value(result)
} else {
result
}
Err(message) => raise RenderError(message)
}
}
None => raise RenderError("Unknown filter: " + call.name)
}
}
}
value
}
_ => {
let output = RenderOutput::Buffered(StringBuilder())
ignore(render_nested_nodes([expression], context, state, output))
StrValue(output.to_string())
}
}
}
///|
fn is_comparison(operator : BinOp) -> Bool {
match operator {
Eq | Ne | Lt | Le | Gt | Ge | In | NotIn => true
_ => false
}
}
///|
fn is_comparison_expression(expression : Expr) -> Bool {
match expression {
Binary(_, operator, _) => is_comparison(operator)
_ => false
}
}
///|
fn flatten_comparison(
expression : Expr,
operands : Array[Expr],
operators : Array[BinOp],
) -> Unit {
match expression {
Binary(left, operator, right) if is_comparison(operator) => {
flatten_comparison(left, operands, operators)
operators.push(operator)
operands.push(right)
}
_ => operands.push(expression)
}
}
///|
fn eval_comparison_chain(
expression : Expr,
context : Context,
state : EngineState,
) -> Value raise JinjaError {
let operands : Array[Expr] = []
let operators : Array[BinOp] = []
flatten_comparison(expression, operands, operators)
let mut previous = eval_engine_expr(operands[0], context, state)
reject_undefined(previous, state, "comparison")
for index, operator in operators {
let current = eval_engine_expr(operands[index + 1], context, state)
reject_undefined(current, state, "comparison")
match apply_binop(previous, operator, current) {
BoolValue(true) => previous = current
BoolValue(false) => return BoolValue(false)
_ => raise RenderError("Comparison did not return a boolean")
}
}
BoolValue(true)
}
///|
fn eval_engine_arguments(
expressions : Array[Expr],
context : Context,
state : EngineState,
) -> Array[Value] raise JinjaError {
let values : Array[Value] = []
for expression in expressions {
let value = eval_engine_expr(expression, context, state)
reject_undefined(value, state, "function argument")
values.push(value)
}
values
}
///|
fn eval_engine_keywords(
expressions : Map[String, Expr],
context : Context,
state : EngineState,
) -> Map[String, Value] raise JinjaError {
let values : Map[String, Value] = Map([])
for name, expression in expressions {
let value = eval_engine_expr(expression, context, state)
reject_undefined(value, state, "keyword argument")
values[name] = value
}
values
}
///|
fn extension_context(state : EngineState) -> ExtensionContext {
{
template_name: state.template_stack.last().unwrap_or(""),
autoescape: state.options.autoescape,
sandboxed: state.options.sandboxed,
}
}
///|
fn ensure_extension_allowed(
state : EngineState,
name : String,
) -> Unit raise JinjaError {
if state.options.sandboxed &&
(name == "safe" || state.environment.restricted_extensions.contains(name)) {
raise RenderError("Extension is not allowed in sandbox mode: " + name)
}
}
///|
fn deep_clone_map(values : Map[String, Value]) -> Map[String, Value] {
let cloned : Map[String, Value] = Map([])
for name, value in values {
cloned[name] = deep_clone_value(value)
}
cloned
}
///|
fn deep_clone_value(value : Value) -> Value {
match value {
ListValue(items) => ListValue(items.map(deep_clone_value))
MapValue(values) => MapValue(deep_clone_map(values))
_ => value
}
}
///|
fn reject_undefined(
value : Value,
state : EngineState,
usage : String,
) -> Unit raise JinjaError {
match value {
Undefined if state.options.strict_undefined =>
raise RenderError("Undefined value used in " + usage)
_ => ()
}
}
///|
fn eval_engine_function(
call : Call,
context : Context,
state : EngineState,
) -> Value raise JinjaError {
if call.name == "super" {
if !call.kwargs.is_empty() {
raise RenderError("super does not accept keyword arguments")
}
guard state.block_stack.last() is Some(frame) else {
raise RenderError("super() can only be used inside a block")
}
guard state.block_chains.get(frame.name) is Some(chain) else {
return SafeStr("")
}
let before_size = state.output_size
let output = RenderOutput::Buffered(StringBuilder())
ignore(
render_block(frame.name, frame.index + 1, chain, context, state, output),
)
state.output_size = before_size
return SafeStr(output.to_string())
}
if call.name.has_prefix("self.") {
if !call.args.is_empty() || !call.kwargs.is_empty() {
raise RenderError("self block calls do not accept arguments")
}
let block_name = call.name[5:].to_owned()
guard state.block_chains.get(block_name) is Some(chain) else {
raise RenderError("Unknown block: " + block_name)
}
let before_size = state.output_size
let output = RenderOutput::Buffered(StringBuilder())
ignore(render_block(block_name, 0, chain, context, state, output))
state.output_size = before_size
return SafeStr(output.to_string())
}
let arguments = eval_engine_arguments(call.args, context, state)
let keywords = eval_engine_keywords(call.kwargs, context, state)
match state.macros.get(call.name) {
Some(definition) =>
return invoke_macro(definition, arguments, keywords, state)
None => ()
}
ensure_extension_allowed(state, call.name)
let isolate = state.environment.restricted_extensions.contains(call.name)
match state.environment.context_functions.get(call.name) {
Some(function) =>
match
function(
extension_context(state),
if isolate {
arguments.map(deep_clone_value)
} else {
arguments
},
if isolate {
deep_clone_map(keywords)
} else {
keywords
},
) {
Ok(value) => if isolate { deep_clone_value(value) } else { value }
Err(message) => raise RenderError(message)
}
None =>
match state.environment.functions.get(call.name) {
Some(function) => {
if !keywords.is_empty() {
raise RenderError(
"Function does not accept keyword arguments: " + call.name,
)
}
match
function(
if isolate {
arguments.map(deep_clone_value)
} else {
arguments
},
) {
Ok(value) => if isolate { deep_clone_value(value) } else { value }
Err(message) => raise RenderError(message)
}
}
None => raise RenderError("Unknown function: " + call.name)
}
}
}
///|
fn invoke_macro(
definition : MacroDefinition,
arguments : Array[Value],
keywords : Map[String, Value],
state : EngineState,
) -> Value raise JinjaError {
state.enter()
let scopes = definition.captured.map(scope => scope.copy())
if scopes.is_empty() {
scopes.push(Map([]))
}
let context : Context = { scopes, }
let parameters : Map[String, Value] = Map([])
context.push_values(parameters)
for index, parameter in definition.params {
let value = if index < arguments.length() {
if keywords.contains(parameter.name) {
state.leave()
raise RenderError("Duplicate macro argument: " + parameter.name)
}
arguments[index]
} else {
match keywords.get(parameter.name) {
Some(value) => value
None =>
match parameter.default {
Some(default) => eval_engine_expr(default, context, state)
None => {
state.leave()
raise RenderError(
"Missing argument '" +
parameter.name +
"' for macro " +
definition.name,
)
}
}
}
}
context.set(parameter.name, value)
}
if arguments.length() > definition.params.length() {
state.leave()
raise RenderError("Too many arguments for macro " + definition.name)
}
for name, _ in keywords {
if !definition.params.any(parameter => parameter.name == name) {
state.leave()
raise RenderError("Unknown macro argument: " + name)
}
}
let before_size = state.output_size
let output = RenderOutput::Buffered(StringBuilder())
ignore(render_engine_nodes(definition.body, context, state, output))
state.output_size = before_size
state.leave()
SafeStr(output.to_string())
}
///|
fn register_imported_macros(
filename : String,
import_name : String?,
selected : Array[String]?,
state : EngineState,
) -> Unit raise JinjaError {
state.enter()
let template = state.environment.resolve_template(filename)
let found : Map[String, Bool] = Map([])
for node in template.body {
match node {
MacroDef(name, params, body) => {
let should_import = match selected {
Some(names) => names.contains(name) && !name.has_prefix("_")
None => !name.has_prefix("_")
}
if should_import {
found[name] = true
let exported_name = match import_name {
Some(import_name) => import_name + "." + name
None => name
}
state.macros[exported_name] = {
name: exported_name,
params,
body,
captured: [Map([])],
}
}
}
_ => ()
}
}
match selected {
Some(names) =>
for name in names {
if !found.contains(name) {
state.leave()
raise RenderError(
"Template '" + filename + "' does not export macro '" + name + "'",
)
}
}
None => ()
}
state.leave()
}