Current section

Files

Jump to
glemplate src glemplate renderer.gleam
Raw

src/glemplate/renderer.gleam

//// The renderer is given the template AST and some assigns, which it will
//// render into a string output.
import gleam/dict.{type Dict}
import gleam/int
import gleam/list
import gleam/result
import gleam/string_tree.{type StringTree}
import glemplate/assigns.{type AssignData, type Assigns}
import glemplate/ast
pub type RenderError {
/// A referenced assign was not found.
AssignNotFound(assign: ast.Var, assigns: Assigns)
/// A referenced assign was not iterable.
AssignNotIterable(assign: ast.Var, assigns: Assigns)
/// A referenced assign could not be stringified.
AssignNotStringifiable(assign: ast.Var, assigns: Assigns)
/// A referenced assign didn't have the requested field.
AssignFieldNotFound(assign: ast.Var, field: String, assigns: Assigns)
/// A referenced assign couldn't be accessed with field access.
AssignNotFieldAccessible(assign: ast.Var, assigns: Assigns)
/// A referenced child template was not found in the template cache.
ChildTemplateNotFound(tpl_name: ast.TemplateName)
}
pub type StringifyError {
StringifyError
}
type AssignResult =
Result(AssignData, RenderError)
pub type RenderResult =
Result(StringTree, RenderError)
/// Function to encode potentially dangerous contents into the template in a
/// safe way. E.g. encode text with HTML entities in an HTML template.
pub type EncoderFn =
fn(String) -> StringTree
/// A cache of templates to use for rendering child templates.
pub type TemplateCache =
Dict(ast.TemplateName, ast.Template)
/// Rendering options:
///
/// - `encoder`: Encoder function to use, it should encode dynamic content in a
/// safe way for this template type.
/// - `template_cache`: A cache of templates that child templates are looked up
/// from, when using Render nodes.
pub type RenderOptions {
RenderOptions(encoder: EncoderFn, template_cache: TemplateCache)
}
/// Render given template with the assigns and options.
pub fn render(
template: ast.Template,
assigns: Assigns,
opts: RenderOptions,
) -> RenderResult {
render_nodes(template.nodes, assigns, string_tree.new(), opts)
}
fn render_nodes(
nodes: ast.NodeList,
assigns: Assigns,
acc: StringTree,
options: RenderOptions,
) -> RenderResult {
list.try_fold(nodes, acc, fn(acc, node) {
render_node(node, assigns, acc, options)
})
}
fn render_node(
node: ast.Node,
assigns: Assigns,
acc: StringTree,
options: RenderOptions,
) -> RenderResult {
case node {
ast.Text(str) -> Ok(string_tree.append(acc, str))
ast.Dynamic(dynamic) -> render_dynamic(dynamic, assigns, acc, options)
ast.Nodes(nodes) -> render_nodes(nodes, assigns, acc, options)
}
}
fn render_dynamic(
node: ast.DynamicNode,
assigns: Assigns,
acc: StringTree,
options: RenderOptions,
) -> RenderResult {
case node {
ast.Output(var) ->
get_assign(assigns, var)
|> result.then(fn(data) {
render_output(data, False, var, assigns, acc, options)
})
ast.RawOutput(var) ->
get_assign(assigns, var)
|> result.then(fn(data) {
render_output(data, True, var, assigns, acc, options)
})
ast.If(var, if_true, if_false) ->
get_assign(assigns, var)
|> result.then(fn(data) {
render_if(data, if_true, if_false, assigns, acc, options)
})
ast.Iter(over, binding, nodes) ->
get_assign(assigns, over)
|> result.then(fn(data) {
render_iter(data, over, binding, nodes, assigns, acc, options)
})
ast.Render(tpl, assigns_map) ->
render_child(tpl, assigns, assigns_map, acc, options)
}
}
fn render_output(
data: AssignData,
raw: Bool,
var: ast.Var,
assigns: Assigns,
acc: StringTree,
options: RenderOptions,
) -> RenderResult {
let str_result = stringify(data)
case str_result {
Ok(str) -> {
let out = case raw {
True -> string_tree.append(acc, str)
False -> string_tree.append_tree(acc, options.encoder(str))
}
Ok(out)
}
Error(_) -> Error(AssignNotStringifiable(assign: var, assigns: assigns))
}
}
fn render_if(
data: AssignData,
if_true: ast.NodeList,
if_false: ast.NodeList,
assigns: Assigns,
acc: StringTree,
options: RenderOptions,
) -> RenderResult {
let nodes = case data {
assigns.Bool(False) | assigns.String("") | assigns.Int(0) | assigns.List([]) ->
if_false
assigns.Dict(d) ->
case !dict.is_empty(d) {
True -> if_true
False -> if_false
}
_truthy -> if_true
}
render_nodes(nodes, assigns, acc, options)
}
fn render_iter(
data: AssignData,
over: ast.Var,
binding: ast.VarName,
nodes: ast.NodeList,
assigns: Assigns,
acc: StringTree,
options: RenderOptions,
) -> RenderResult {
case data {
assigns.List(items) ->
list.try_fold(items, acc, fn(output, item) {
let new_assigns = dict.insert(assigns, binding, item)
render_nodes(nodes, new_assigns, output, options)
})
_other -> Error(AssignNotIterable(assign: over, assigns: assigns))
}
}
fn render_child(
template_name: ast.TemplateName,
assigns: Assigns,
assigns_map: List(#(ast.Var, ast.VarName)),
acc: StringTree,
options: RenderOptions,
) -> RenderResult {
use template <- result.then(
dict.get(options.template_cache, template_name)
|> result.replace_error(ChildTemplateNotFound(tpl_name: template_name)),
)
use new_assigns <- result.then(
list.try_fold(assigns_map, dict.new(), fn(acc, mapping) {
let #(from, to) = mapping
use assign <- result.then(get_assign(assigns, from))
Ok(dict.insert(acc, to, assign))
}),
)
render_nodes(template.nodes, new_assigns, acc, options)
}
fn get_assign(assigns: Assigns, var: ast.Var) -> AssignResult {
use data <- result.then(case var {
ast.Assign(name) -> {
let get_result = dict.get(assigns, name)
case get_result {
Ok(data) -> Ok(data)
Error(_) -> Error(AssignNotFound(assign: var, assigns: assigns))
}
}
ast.FieldAccess(container, field) -> {
use data <- result.then(get_assign(assigns, container))
access_field(data, field, var, assigns)
}
})
// In case the data is a lazy function, it must be resolved
Ok(resolve_lazy(data))
}
fn access_field(
container: AssignData,
field: String,
accessor: ast.Var,
assigns: Assigns,
) -> AssignResult {
case container {
assigns.Dict(d) ->
case dict.get(d, field) {
Ok(data) -> Ok(data)
Error(_) ->
Error(AssignFieldNotFound(
assign: accessor,
field: field,
assigns: assigns,
))
}
_other ->
Error(AssignNotFieldAccessible(assign: accessor, assigns: assigns))
}
}
fn stringify(data: AssignData) -> Result(String, StringifyError) {
case data {
assigns.String(s) -> Ok(s)
assigns.Int(i) -> Ok(int.to_string(i))
_other -> Error(StringifyError)
}
}
fn resolve_lazy(data: AssignData) -> AssignData {
case data {
assigns.Lazy(lazy_fn) -> resolve_lazy(lazy_fn())
other -> other
}
}