Initial commit
This commit is contained in:
commit
c2117bc6ff
19 changed files with 1926 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
*.beam
|
||||||
|
*.ez
|
||||||
|
build
|
||||||
|
erl_crash.dump
|
2
.tool-versions
Normal file
2
.tool-versions
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
erlang 25.2.2
|
||||||
|
gleam 0.26.2
|
4
CHANGELOG
Normal file
4
CHANGELOG
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
1.0.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
Initial release.
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2023 Mikko Ahlroth
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
61
README.md
Normal file
61
README.md
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# glemplate
|
||||||
|
|
||||||
|
[![Package Version](https://img.shields.io/hexpm/v/glemplate)](https://hex.pm/packages/glemplate)
|
||||||
|
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/glemplate/)
|
||||||
|
|
||||||
|
Glemplate is a simplistic template engine for Gleam. The aim was to learn how to make such a thing
|
||||||
|
and fill simple use cases. At this stage, it can be used for simple plain text and HTML templates,
|
||||||
|
and for other contexts by supplying a custom encoding function.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```gleam
|
||||||
|
import gleam/map
|
||||||
|
import glemplate/parser
|
||||||
|
import glemplate/html
|
||||||
|
import glemplate/assigns
|
||||||
|
|
||||||
|
let template = "<b>Welcome, <%= name %>!</b>"
|
||||||
|
|
||||||
|
assert Ok(tpl) = parser.parse_to_template(template, "input.html.glemp")
|
||||||
|
let assigns = assigns.from_list([#("name", assigns.String("<Nicd>"))])
|
||||||
|
let template_cache = map.new()
|
||||||
|
html.render(tpl, assigns, template_cache) // "<b>Welcome, <Nicd>!</b>"
|
||||||
|
```
|
||||||
|
|
||||||
|
The main modules:
|
||||||
|
|
||||||
|
- [glemplate/parser](glemplate/parser.html): The parser implements and documents the string
|
||||||
|
template syntax.
|
||||||
|
- [glemplate/ast](glemplate/ast.html): The abstract syntax tree (AST) is the data format of the
|
||||||
|
parsed templates.
|
||||||
|
- [glemplate/renderer](glemplate/renderer.html): The renderer takes in the parsed AST, input data,
|
||||||
|
and a cache of templates, and renders the template into an output string builder.
|
||||||
|
- [glemplate/html](glemplate/html.html) and [glemplate/text](glemplate/text.html) have helper
|
||||||
|
functions for rendering HTML and plain text templates.
|
||||||
|
|
||||||
|
Using templates with Glemplate has two main things to deal with:
|
||||||
|
|
||||||
|
1. parsing templates into AST with the parser, and
|
||||||
|
2. rendering that AST into some output with assigns (input data).
|
||||||
|
|
||||||
|
Since Gleam has no metaprogramming, the parsing of templates needs to be done at startup time (or
|
||||||
|
whenever deemed necessary when templates have changed). The resulting AST should be stored for
|
||||||
|
later use in e.g. ETS, process state, persistent_term…
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Static values in templates in place of variables aren't supported.
|
||||||
|
- It's possible to create infinite loops with child templates, it's not recommended to give access
|
||||||
|
to writing templates to untrusted users.
|
||||||
|
- Calling functions from templates is not supported.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
This package can be added to your Gleam project:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gleam add glemplate
|
||||||
|
```
|
||||||
|
|
||||||
|
and its documentation can be found at <https://hexdocs.pm/glemplate>.
|
18
gleam.toml
Normal file
18
gleam.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
name = "glemplate"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "A Gleam project"
|
||||||
|
|
||||||
|
# Fill out these fields if you intend to generate HTML documentation or publish
|
||||||
|
# your project to the Hex package manager.
|
||||||
|
#
|
||||||
|
licences = ["MIT"]
|
||||||
|
repository = { type = "gitlab", user = "Nicd", repo = "glemplate" }
|
||||||
|
links = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gleam_stdlib = "~> 0.26"
|
||||||
|
nibble = "~> 0.2"
|
||||||
|
glentities = "~> 1.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gleeunit = "~> 0.10"
|
15
manifest.toml
Normal file
15
manifest.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# This file was generated by Gleam
|
||||||
|
# You typically do not need to edit this file
|
||||||
|
|
||||||
|
packages = [
|
||||||
|
{ name = "gleam_stdlib", version = "0.26.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "B17BBE8A78F3909D93BCC6C24F531673A7E328A61F24222EB1E58D0A7552B1FE" },
|
||||||
|
{ name = "gleeunit", version = "0.10.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "ECEA2DE4BE6528D36AFE74F42A21CDF99966EC36D7F25DEB34D47DD0F7977BAF" },
|
||||||
|
{ name = "glentities", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glentities", source = "hex", outer_checksum = "9F602AB9A9DB0B4905BCDEC05B8586A77C17021210B85034AE3D182BD8024FE2" },
|
||||||
|
{ name = "nibble", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "nibble", source = "hex", outer_checksum = "B5D0E11DADEB4EC2F4ACD04DEB3CEBE3F0762B0BD9FF37805948FFC17017F791" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[requirements]
|
||||||
|
gleam_stdlib = "~> 0.26"
|
||||||
|
gleeunit = "~> 0.10"
|
||||||
|
glentities = "~> 1.0"
|
||||||
|
nibble = "~> 0.2"
|
59
src/glemplate/assigns.gleam
Normal file
59
src/glemplate/assigns.gleam
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
//// Assigns are used to insert dynamic data into a template. This module
|
||||||
|
//// contains convenience functions for creating assigns, but you may also
|
||||||
|
//// create the map manually with Gleam's map functions if you so wish.
|
||||||
|
|
||||||
|
import gleam/map.{Map}
|
||||||
|
import glemplate/ast.{VarName}
|
||||||
|
|
||||||
|
/// Data in an assign. Note that only `String` and `Int` are stringifiable
|
||||||
|
/// values.
|
||||||
|
pub type AssignData {
|
||||||
|
String(String)
|
||||||
|
Int(Int)
|
||||||
|
|
||||||
|
/// A function to execute that returns assign data on demand.
|
||||||
|
Lazy(LazyFn)
|
||||||
|
Bool(Bool)
|
||||||
|
Map(Map(VarName, AssignData))
|
||||||
|
List(List(AssignData))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lazy function that should return assign data when executed.
|
||||||
|
pub type LazyFn =
|
||||||
|
fn() -> AssignData
|
||||||
|
|
||||||
|
/// Assigns given to a template for rendering dynamic content.
|
||||||
|
pub type Assigns =
|
||||||
|
Map(VarName, AssignData)
|
||||||
|
|
||||||
|
pub fn new() -> Assigns {
|
||||||
|
map.new()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_list(list: List(#(VarName, AssignData))) -> Assigns {
|
||||||
|
map.from_list(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_string(assigns: Assigns, name: VarName, value: String) {
|
||||||
|
map.insert(assigns, name, String(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_int(assigns: Assigns, name: VarName, value: Int) {
|
||||||
|
map.insert(assigns, name, Int(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_lazy(assigns: Assigns, name: VarName, value: LazyFn) {
|
||||||
|
map.insert(assigns, name, Lazy(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_bool(assigns: Assigns, name: VarName, value: Bool) {
|
||||||
|
map.insert(assigns, name, Bool(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_list(assigns: Assigns, name: VarName, value: List(AssignData)) {
|
||||||
|
map.insert(assigns, name, List(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_map(assigns: Assigns, name: VarName, value: Map(VarName, AssignData)) {
|
||||||
|
map.insert(assigns, name, Map(value))
|
||||||
|
}
|
55
src/glemplate/ast.gleam
Normal file
55
src/glemplate/ast.gleam
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
//// The abstract syntax tree that templates are stored as in memory. This AST
|
||||||
|
//// can be rendered with the renderer module.
|
||||||
|
|
||||||
|
pub type Node {
|
||||||
|
Text(String)
|
||||||
|
Dynamic(DynamicNode)
|
||||||
|
Nodes(nodes: NodeList)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type NodeList =
|
||||||
|
List(Node)
|
||||||
|
|
||||||
|
pub type Template {
|
||||||
|
Template(name: TemplateName, nodes: NodeList)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A dynamic node is used for dynamic behaviour in a template.
|
||||||
|
pub type DynamicNode {
|
||||||
|
/// Output the variable, encoded.
|
||||||
|
Output(Var)
|
||||||
|
|
||||||
|
/// Output the variable, without encoding.
|
||||||
|
RawOutput(Var)
|
||||||
|
|
||||||
|
/// Render a node list based on if the assign is truthy (non-false).
|
||||||
|
If(Var, if_true: NodeList, if_false: NodeList)
|
||||||
|
|
||||||
|
/// Iterate over the variable, bind it to a new name, and render the node list
|
||||||
|
/// for every item.
|
||||||
|
Iter(over: Var, binding: VarName, NodeList)
|
||||||
|
|
||||||
|
/// Render the given template, using the assigns mapping. The assigns mapping
|
||||||
|
/// is a list of tuples `(from, to)`, where the `from` named assigns are
|
||||||
|
/// available with `to` names in the child template.
|
||||||
|
Render(tpl: TemplateName, assigns_map: List(#(Var, VarName)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reference to a template with the given name.
|
||||||
|
pub type TemplateName =
|
||||||
|
String
|
||||||
|
|
||||||
|
/// A reference to a variable input.
|
||||||
|
pub type Var {
|
||||||
|
/// Reference to an assign with the given name.
|
||||||
|
Assign(name: VarName)
|
||||||
|
|
||||||
|
/// Accessing a field of an associative container.
|
||||||
|
FieldAccess(container: Var, field: String)
|
||||||
|
|
||||||
|
/// Accessing an item in an indexable container.
|
||||||
|
IndexAccess(container: Var, index: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type VarName =
|
||||||
|
String
|
26
src/glemplate/html.gleam
Normal file
26
src/glemplate/html.gleam
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
//// Utilities for rendering HTML templates.
|
||||||
|
|
||||||
|
import gleam/string_builder.{StringBuilder}
|
||||||
|
import glentities
|
||||||
|
import glemplate/renderer
|
||||||
|
import glemplate/ast
|
||||||
|
import glemplate/assigns
|
||||||
|
|
||||||
|
/// Encode content as HTML entities so it's safe in an HTML template.
|
||||||
|
pub fn encode(text: String) -> StringBuilder {
|
||||||
|
glentities.encode(text, glentities.Named)
|
||||||
|
|> string_builder.from_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render an HTML template.
|
||||||
|
pub fn render(
|
||||||
|
template: ast.Template,
|
||||||
|
assigns: assigns.Assigns,
|
||||||
|
template_cache: renderer.TemplateCache,
|
||||||
|
) -> renderer.RenderResult {
|
||||||
|
renderer.render(
|
||||||
|
template,
|
||||||
|
assigns,
|
||||||
|
renderer.RenderOptions(encoder: encode, template_cache: template_cache),
|
||||||
|
)
|
||||||
|
}
|
486
src/glemplate/parser.gleam
Normal file
486
src/glemplate/parser.gleam
Normal file
|
@ -0,0 +1,486 @@
|
||||||
|
//// The parser transforms a string input into a template AST.
|
||||||
|
////
|
||||||
|
//// ## Template language description
|
||||||
|
////
|
||||||
|
//// The template language uses `<%` and `%>` as the tag delimiters. For
|
||||||
|
//// outputting, the start delimiter `<%=` is used.
|
||||||
|
////
|
||||||
|
//// Comments can be inserted with `<%!-- Comment --%>`, they are not emitted to
|
||||||
|
//// the AST.
|
||||||
|
////
|
||||||
|
//// Assigns can be referenced using their name (key in the assigns map). After
|
||||||
|
//// an assign name, the contents of the assign can be accessed using field or
|
||||||
|
//// index based access:
|
||||||
|
////
|
||||||
|
//// * `foo.bar` accesses field `bar` in assign `foo`.
|
||||||
|
//// * `foo.0` accesses the first element in assign `foo`.
|
||||||
|
////
|
||||||
|
//// The accesses can be chained, e.g. `foo.bar.0.baz`. These access methods
|
||||||
|
//// work in any place where an assign reference is expected.
|
||||||
|
////
|
||||||
|
//// Any other content in the template is emitted as text nodes.
|
||||||
|
////
|
||||||
|
//// ### Tags
|
||||||
|
////
|
||||||
|
//// #### Output
|
||||||
|
////
|
||||||
|
//// ```text
|
||||||
|
//// <%= foo %>
|
||||||
|
//// ```
|
||||||
|
////
|
||||||
|
//// Emits an output node of the assign `foo`. The contents are encoded using
|
||||||
|
//// the encoder given in the render call.
|
||||||
|
////
|
||||||
|
//// #### Raw output
|
||||||
|
////
|
||||||
|
//// ```text
|
||||||
|
//// <%= raw foo %>
|
||||||
|
//// ```
|
||||||
|
////
|
||||||
|
//// Emits an output node of the assign `foo`. The contents are not encoded.
|
||||||
|
////
|
||||||
|
//// #### If
|
||||||
|
////
|
||||||
|
//// ```text
|
||||||
|
//// <% if foo %>
|
||||||
|
//// ...
|
||||||
|
//// <% else %>
|
||||||
|
//// ...
|
||||||
|
//// <% end %>
|
||||||
|
//// ```
|
||||||
|
////
|
||||||
|
//// Emits an if node. If the assign `foo` is truthy (not `False`), the first
|
||||||
|
//// nodes are rendered, otherwise the nodes in the `else` block. The `else`
|
||||||
|
//// block is optional.
|
||||||
|
////
|
||||||
|
//// #### For
|
||||||
|
////
|
||||||
|
//// ```text
|
||||||
|
//// <% for bar in foo %>
|
||||||
|
//// ...
|
||||||
|
//// <% end %>
|
||||||
|
//// ```
|
||||||
|
////
|
||||||
|
//// Emits a for node. Each item of `foo` is looped through, making it available
|
||||||
|
//// as the assign `bar` inside the block. The contents of the block are
|
||||||
|
//// rendered for each item.
|
||||||
|
////
|
||||||
|
//// #### Render
|
||||||
|
////
|
||||||
|
//// ```text
|
||||||
|
//// <% render tpl.txt.glemp %>
|
||||||
|
//// <% render tpl.txt.glemp name: user.name, level: user.level %>
|
||||||
|
//// ```
|
||||||
|
////
|
||||||
|
//// Emits a render node. This will render another template in this place. The
|
||||||
|
//// listed assigns will be the assigns given to the template. The name of the
|
||||||
|
//// assign in the child template is on the left, the value is on the right.
|
||||||
|
|
||||||
|
import gleam/string_builder
|
||||||
|
import gleam/list
|
||||||
|
import gleam/option.{None, Option, Some}
|
||||||
|
import gleam/result
|
||||||
|
import gleam/int
|
||||||
|
import nibble
|
||||||
|
import nibble/predicates
|
||||||
|
import glemplate/ast
|
||||||
|
|
||||||
|
const tag_start = "<%"
|
||||||
|
|
||||||
|
const tag_end = "%>"
|
||||||
|
|
||||||
|
const tag_escape_char = "%"
|
||||||
|
|
||||||
|
const tag_comment_start = "!--"
|
||||||
|
|
||||||
|
const tag_comment_end = "--"
|
||||||
|
|
||||||
|
const tag_output = "="
|
||||||
|
|
||||||
|
const end_token = "end"
|
||||||
|
|
||||||
|
const if_token = "if"
|
||||||
|
|
||||||
|
const else_token = "else"
|
||||||
|
|
||||||
|
const iter_token = "for"
|
||||||
|
|
||||||
|
const iter_oper_token = "in"
|
||||||
|
|
||||||
|
const output_raw_token = "raw"
|
||||||
|
|
||||||
|
const render_token = "render"
|
||||||
|
|
||||||
|
const render_binding_separator = ":"
|
||||||
|
|
||||||
|
const render_pair_separator = ","
|
||||||
|
|
||||||
|
const access_separator = "."
|
||||||
|
|
||||||
|
type IncompleteTag {
|
||||||
|
If(var: ast.Var, if_true: Option(ast.NodeList))
|
||||||
|
Iter(over: ast.Var, binding: ast.VarName)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IncompleteAccess {
|
||||||
|
FieldAccess(field: String)
|
||||||
|
IndexAccess(index: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
type State {
|
||||||
|
State(
|
||||||
|
emitted: ast.NodeList,
|
||||||
|
current_text: string_builder.StringBuilder,
|
||||||
|
current_nodes: ast.NodeList,
|
||||||
|
current_tag: Option(IncompleteTag),
|
||||||
|
outer_state: Option(State),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the string into a template with the given name.
|
||||||
|
pub fn parse_to_template(template: String, name: String) {
|
||||||
|
try parsed = parse(template)
|
||||||
|
|
||||||
|
Ok(ast.Template(name: name, nodes: parsed))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the string into a node list.
|
||||||
|
pub fn parse(template: String) {
|
||||||
|
nibble.run(template, base_parser(empty_state()))
|
||||||
|
|> result.map(fn(state: State) { state.emitted })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_state() {
|
||||||
|
State(
|
||||||
|
emitted: [],
|
||||||
|
current_nodes: [],
|
||||||
|
current_text: string_builder.new(),
|
||||||
|
current_tag: None,
|
||||||
|
outer_state: None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_parser(old_state: State) {
|
||||||
|
nibble.loop(
|
||||||
|
old_state,
|
||||||
|
fn(state) {
|
||||||
|
nibble.one_of([
|
||||||
|
tag_parser(state)
|
||||||
|
|> nibble.map(nibble.Continue),
|
||||||
|
nibble.any()
|
||||||
|
|> nibble.map(fn(c) {
|
||||||
|
State(
|
||||||
|
..state,
|
||||||
|
current_text: string_builder.append(state.current_text, c),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|> nibble.map(nibble.Continue),
|
||||||
|
nibble.eof()
|
||||||
|
|> nibble.replace(stringify_current_text(state))
|
||||||
|
|> nibble.map(nibble.Break),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|> nibble.map(fn(state) {
|
||||||
|
State(
|
||||||
|
..state,
|
||||||
|
emitted: [ast.Nodes(state.emitted), ..list.reverse(state.current_nodes)],
|
||||||
|
current_nodes: [],
|
||||||
|
current_text: string_builder.new(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag_parser(state: State) {
|
||||||
|
use _ <- nibble.then(nibble.string(tag_start))
|
||||||
|
let state = stringify_current_text(state)
|
||||||
|
|
||||||
|
nibble.one_of([
|
||||||
|
escaped_tag(state),
|
||||||
|
comment_tag(state),
|
||||||
|
output_tag(state),
|
||||||
|
other_tag(state),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escaped_tag(state: State) {
|
||||||
|
nibble.string(tag_escape_char)
|
||||||
|
|> nibble.replace(
|
||||||
|
State(..state, current_nodes: [ast.Text(tag_start), ..state.current_nodes]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn comment_tag(state: State) {
|
||||||
|
use _ <- nibble.then(nibble.string(tag_comment_start))
|
||||||
|
|
||||||
|
nibble.loop(
|
||||||
|
Nil,
|
||||||
|
fn(_) {
|
||||||
|
nibble.one_of([
|
||||||
|
nibble.string(tag_comment_end <> tag_end)
|
||||||
|
|> nibble.map(nibble.Break),
|
||||||
|
nibble.any()
|
||||||
|
|> nibble.replace(nibble.Continue(Nil)),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|> nibble.replace(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output_tag(state: State) {
|
||||||
|
use _ <- nibble.then(nibble.string(tag_output))
|
||||||
|
|
||||||
|
nibble.one_of([
|
||||||
|
{
|
||||||
|
// Raw output
|
||||||
|
use _ <- nibble.then(nibble.whitespace())
|
||||||
|
use _ <- nibble.then(nibble.string(output_raw_token))
|
||||||
|
use _ <- nibble.then(nibble.whitespace())
|
||||||
|
use var <- nibble.then(variable())
|
||||||
|
|
||||||
|
nibble.succeed(
|
||||||
|
State(
|
||||||
|
..state,
|
||||||
|
current_nodes: [
|
||||||
|
ast.Dynamic(ast.RawOutput(var)),
|
||||||
|
..state.current_nodes
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Normal output
|
||||||
|
use _ <- nibble.then(nibble.whitespace())
|
||||||
|
use var <- nibble.then(variable())
|
||||||
|
|
||||||
|
nibble.succeed(
|
||||||
|
State(
|
||||||
|
..state,
|
||||||
|
current_nodes: [ast.Dynamic(ast.Output(var)), ..state.current_nodes],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|> nibble.drop(nibble.whitespace())
|
||||||
|
|> nibble.drop(nibble.string(tag_end))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn other_tag(state: State) {
|
||||||
|
use _ <- nibble.then(nibble.whitespace())
|
||||||
|
|
||||||
|
nibble.one_of([
|
||||||
|
if_tag(state),
|
||||||
|
for_tag(state),
|
||||||
|
render_tag(state),
|
||||||
|
else_tag(state),
|
||||||
|
end_tag(state),
|
||||||
|
])
|
||||||
|
|> nibble.drop(nibble.whitespace())
|
||||||
|
|> nibble.drop(nibble.string(tag_end))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn if_tag(state: State) {
|
||||||
|
use _ <- nibble.then(nibble.string(if_token))
|
||||||
|
use _ <- nibble.then(nibble.whitespace())
|
||||||
|
use var <- nibble.then(variable())
|
||||||
|
|
||||||
|
nibble.succeed(
|
||||||
|
State(
|
||||||
|
..empty_state(),
|
||||||
|
current_tag: Some(If(var, None)),
|
||||||
|
outer_state: Some(state),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn for_tag(state: State) {
|
||||||
|
use _ <- nibble.then(nibble.string(iter_token))
|
||||||
|
use _ <- nibble.then(nibble.whitespace())
|
||||||
|
use binding <- nibble.then(variable_name())
|
||||||
|
use _ <- nibble.then(nibble.whitespace())
|
||||||
|
use _ <- nibble.then(nibble.string(iter_oper_token))
|
||||||
|
use _ <- nibble.then(nibble.whitespace())
|
||||||
|
use over <- nibble.then(variable())
|
||||||
|
|
||||||
|
nibble.succeed(
|
||||||
|
State(
|
||||||
|
..empty_state(),
|
||||||
|
current_tag: Some(Iter(over, binding)),
|
||||||
|
outer_state: Some(state),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_tag(state: State) {
|
||||||
|
use _ <- nibble.then(nibble.string(render_token))
|
||||||
|
use _ <- nibble.then(nibble.whitespace())
|
||||||
|
use filename <- nibble.then(filename())
|
||||||
|
use _ <- nibble.then(nibble.whitespace())
|
||||||
|
|
||||||
|
nibble.many(render_binding(), render_separator())
|
||||||
|
|> nibble.map(fn(bindings) {
|
||||||
|
State(
|
||||||
|
..state,
|
||||||
|
current_nodes: [
|
||||||
|
ast.Dynamic(ast.Render(filename, bindings)),
|
||||||
|
..state.current_nodes
|
||||||
|
],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_tag(state: State) {
|
||||||
|
use _ <- nibble.then(nibble.string(end_token))
|
||||||
|
|
||||||
|
let outer_state =
|
||||||
|
option.lazy_unwrap(state.outer_state, fn() { empty_state() })
|
||||||
|
|
||||||
|
case state.current_tag {
|
||||||
|
Some(If(var, if_true)) -> {
|
||||||
|
let #(if_true, state) = case if_true {
|
||||||
|
Some(if_true_nodes) -> #(if_true_nodes, state)
|
||||||
|
None -> #(
|
||||||
|
list.reverse(state.current_nodes),
|
||||||
|
State(..state, current_nodes: []),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let if_false = list.reverse(state.current_nodes)
|
||||||
|
nibble.succeed(
|
||||||
|
State(
|
||||||
|
..outer_state,
|
||||||
|
current_nodes: [
|
||||||
|
ast.Dynamic(ast.If(var, if_true, if_false)),
|
||||||
|
..outer_state.current_nodes
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Iter(over, binding)) ->
|
||||||
|
nibble.succeed(
|
||||||
|
State(
|
||||||
|
..outer_state,
|
||||||
|
current_nodes: [
|
||||||
|
ast.Dynamic(ast.Iter(
|
||||||
|
over,
|
||||||
|
binding,
|
||||||
|
list.reverse(state.current_nodes),
|
||||||
|
)),
|
||||||
|
..outer_state.current_nodes
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
_else -> nibble.fail("Expected `if` or `for` tag for `end` tag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn else_tag(state: State) {
|
||||||
|
use _ <- nibble.then(nibble.string(else_token))
|
||||||
|
|
||||||
|
case state.current_tag {
|
||||||
|
Some(If(var, None)) ->
|
||||||
|
nibble.succeed(
|
||||||
|
State(
|
||||||
|
..state,
|
||||||
|
current_tag: Some(If(var, Some(list.reverse(state.current_nodes)))),
|
||||||
|
current_nodes: [],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
_else -> nibble.fail("Expected `if` to match `else` tag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stringify_current_text(state: State) {
|
||||||
|
State(
|
||||||
|
..state,
|
||||||
|
current_nodes: [
|
||||||
|
ast.Text(string_builder.to_string(state.current_text)),
|
||||||
|
..state.current_nodes
|
||||||
|
],
|
||||||
|
current_text: string_builder.new(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn variable_name() {
|
||||||
|
nibble.take_if_and_while(
|
||||||
|
fn(grapheme) { predicates.is_alphanum(grapheme) || grapheme == "_" },
|
||||||
|
"a variable name",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_access() {
|
||||||
|
nibble.take_if_and_while(
|
||||||
|
fn(grapheme) { predicates.is_alphanum(grapheme) || grapheme == "_" },
|
||||||
|
"a field accessor",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index_access() {
|
||||||
|
nibble.take_if_and_while(predicates.is_digit, "an index accessor")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accessors() {
|
||||||
|
use accessors <- nibble.map(nibble.many(
|
||||||
|
nibble.one_of([field_access(), index_access()]),
|
||||||
|
nibble.string(access_separator),
|
||||||
|
))
|
||||||
|
|
||||||
|
list.map(
|
||||||
|
accessors,
|
||||||
|
fn(accessor) {
|
||||||
|
case int.parse(accessor) {
|
||||||
|
Ok(data) -> IndexAccess(data)
|
||||||
|
Error(_) -> FieldAccess(accessor)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn variable() {
|
||||||
|
use name <- nibble.then(variable_name())
|
||||||
|
use accessors <- nibble.then(nibble.one_of([
|
||||||
|
{
|
||||||
|
use _ <- nibble.then(nibble.string(access_separator))
|
||||||
|
use accs <- nibble.then(accessors())
|
||||||
|
nibble.succeed(accs)
|
||||||
|
},
|
||||||
|
nibble.succeed([]),
|
||||||
|
]))
|
||||||
|
|
||||||
|
nibble.succeed(list.fold(
|
||||||
|
accessors,
|
||||||
|
ast.Assign(name),
|
||||||
|
fn(acc, accessor) {
|
||||||
|
case accessor {
|
||||||
|
FieldAccess(field) -> ast.FieldAccess(acc, field)
|
||||||
|
IndexAccess(index) -> ast.IndexAccess(acc, index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filename() {
|
||||||
|
nibble.take_if_and_while(
|
||||||
|
fn(grapheme) {
|
||||||
|
predicates.is_alphanum(grapheme) || grapheme == "_" || grapheme == "-" || grapheme == "."
|
||||||
|
},
|
||||||
|
"a filename without spaces",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_binding() {
|
||||||
|
use to <- nibble.then(variable_name())
|
||||||
|
use _ <- nibble.then(nibble.string(render_binding_separator))
|
||||||
|
use _ <- nibble.then(nibble.whitespace())
|
||||||
|
use from <- nibble.then(variable())
|
||||||
|
|
||||||
|
nibble.succeed(#(from, to))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_separator() {
|
||||||
|
use _ <- nibble.then(nibble.string(render_pair_separator))
|
||||||
|
use _ <- nibble.then(nibble.whitespace())
|
||||||
|
|
||||||
|
nibble.succeed(Nil)
|
||||||
|
}
|
294
src/glemplate/renderer.gleam
Normal file
294
src/glemplate/renderer.gleam
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
//// The renderer is given the template AST and some assigns, which it will
|
||||||
|
//// render into a string output.
|
||||||
|
|
||||||
|
import gleam/map.{Map}
|
||||||
|
import gleam/int
|
||||||
|
import gleam/string_builder.{StringBuilder}
|
||||||
|
import gleam/result
|
||||||
|
import gleam/list
|
||||||
|
import glemplate/assigns.{AssignData, Assigns}
|
||||||
|
import glemplate/result as result_helpers
|
||||||
|
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)
|
||||||
|
|
||||||
|
AssignFieldNotFound(assign: ast.Var, field: String, assigns: Assigns)
|
||||||
|
|
||||||
|
AssignNotFieldAccessible(assign: ast.Var, assigns: Assigns)
|
||||||
|
|
||||||
|
AssignIndexOutOfBounds(assign: ast.Var, index: Int, assigns: Assigns)
|
||||||
|
|
||||||
|
AssignNotIndexable(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(StringBuilder, 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) -> StringBuilder
|
||||||
|
|
||||||
|
/// A cache of templates to use for rendering child templates.
|
||||||
|
pub type TemplateCache =
|
||||||
|
Map(ast.TemplateName, ast.Template)
|
||||||
|
|
||||||
|
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_builder.new(), opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_nodes(
|
||||||
|
nodes: ast.NodeList,
|
||||||
|
assigns: Assigns,
|
||||||
|
acc: StringBuilder,
|
||||||
|
options: RenderOptions,
|
||||||
|
) -> RenderResult {
|
||||||
|
result_helpers.fold(
|
||||||
|
nodes,
|
||||||
|
acc,
|
||||||
|
fn(acc, node) { render_node(node, assigns, acc, options) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_node(
|
||||||
|
node: ast.Node,
|
||||||
|
assigns: Assigns,
|
||||||
|
acc: StringBuilder,
|
||||||
|
options: RenderOptions,
|
||||||
|
) -> RenderResult {
|
||||||
|
case node {
|
||||||
|
ast.Text(str) -> Ok(string_builder.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: StringBuilder,
|
||||||
|
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: StringBuilder,
|
||||||
|
options: RenderOptions,
|
||||||
|
) -> RenderResult {
|
||||||
|
let str_result = stringify(data)
|
||||||
|
|
||||||
|
case str_result {
|
||||||
|
Ok(str) -> {
|
||||||
|
let out = case raw {
|
||||||
|
True -> string_builder.append(acc, str)
|
||||||
|
False -> string_builder.append_builder(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: StringBuilder,
|
||||||
|
options: RenderOptions,
|
||||||
|
) -> RenderResult {
|
||||||
|
case data {
|
||||||
|
assigns.Bool(False) -> render_nodes(if_false, assigns, acc, options)
|
||||||
|
_truthy -> render_nodes(if_true, assigns, acc, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_iter(
|
||||||
|
data: AssignData,
|
||||||
|
over: ast.Var,
|
||||||
|
binding: ast.VarName,
|
||||||
|
nodes: ast.NodeList,
|
||||||
|
assigns: Assigns,
|
||||||
|
acc: StringBuilder,
|
||||||
|
options: RenderOptions,
|
||||||
|
) -> RenderResult {
|
||||||
|
case data {
|
||||||
|
assigns.List(items) ->
|
||||||
|
result_helpers.fold(
|
||||||
|
items,
|
||||||
|
acc,
|
||||||
|
fn(output, item) {
|
||||||
|
let new_assigns = map.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: StringBuilder,
|
||||||
|
options: RenderOptions,
|
||||||
|
) -> RenderResult {
|
||||||
|
try template =
|
||||||
|
map.get(options.template_cache, template_name)
|
||||||
|
|> result.replace_error(ChildTemplateNotFound(tpl_name: template_name))
|
||||||
|
|
||||||
|
try new_assigns =
|
||||||
|
result_helpers.fold(
|
||||||
|
assigns_map,
|
||||||
|
map.new(),
|
||||||
|
fn(acc, mapping) {
|
||||||
|
let #(from, to) = mapping
|
||||||
|
try assign = get_assign(assigns, from)
|
||||||
|
Ok(map.insert(acc, to, assign))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
render_nodes(template.nodes, new_assigns, acc, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_assign(assigns: Assigns, var: ast.Var) -> AssignResult {
|
||||||
|
try data = case var {
|
||||||
|
ast.Assign(name) -> {
|
||||||
|
let get_result = map.get(assigns, name)
|
||||||
|
case get_result {
|
||||||
|
Ok(data) -> Ok(data)
|
||||||
|
Error(_) -> Error(AssignNotFound(assign: var, assigns: assigns))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ast.FieldAccess(container, field) -> {
|
||||||
|
try data = get_assign(assigns, container)
|
||||||
|
access_field(data, field, var, assigns)
|
||||||
|
}
|
||||||
|
|
||||||
|
ast.IndexAccess(container, index) -> {
|
||||||
|
try data = get_assign(assigns, container)
|
||||||
|
access_index(data, index, 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.Map(map) ->
|
||||||
|
case map.get(map, field) {
|
||||||
|
Ok(data) -> Ok(data)
|
||||||
|
Error(_) ->
|
||||||
|
Error(AssignFieldNotFound(
|
||||||
|
assign: accessor,
|
||||||
|
field: field,
|
||||||
|
assigns: assigns,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
Error(AssignNotFieldAccessible(assign: accessor, assigns: assigns))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn access_index(
|
||||||
|
container: AssignData,
|
||||||
|
index: Int,
|
||||||
|
accessor: ast.Var,
|
||||||
|
assigns: Assigns,
|
||||||
|
) -> AssignResult {
|
||||||
|
case container {
|
||||||
|
assigns.List(l) ->
|
||||||
|
case list.at(l, index) {
|
||||||
|
Ok(data) -> Ok(data)
|
||||||
|
Error(_) ->
|
||||||
|
Error(AssignIndexOutOfBounds(
|
||||||
|
assign: accessor,
|
||||||
|
index: index,
|
||||||
|
assigns: assigns,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
_other -> Error(AssignNotIndexable(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
|
||||||
|
}
|
||||||
|
}
|
24
src/glemplate/result.gleam
Normal file
24
src/glemplate/result.gleam
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
//// Helper utilities for result types.
|
||||||
|
|
||||||
|
import gleam/list.{Continue, Stop}
|
||||||
|
|
||||||
|
/// Fold over a list, stopping at the first error encountered.
|
||||||
|
pub fn fold(
|
||||||
|
items: List(a),
|
||||||
|
init: b,
|
||||||
|
folder: fn(b, a) -> Result(b, c),
|
||||||
|
) -> Result(b, c) {
|
||||||
|
list.fold_until(
|
||||||
|
items,
|
||||||
|
Ok(init),
|
||||||
|
fn(acc, item) {
|
||||||
|
assert Ok(acc) = acc
|
||||||
|
let res = folder(acc, item)
|
||||||
|
|
||||||
|
case res {
|
||||||
|
Ok(next_acc) -> Continue(Ok(next_acc))
|
||||||
|
Error(error) -> Stop(Error(error))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
24
src/glemplate/text.gleam
Normal file
24
src/glemplate/text.gleam
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
//// Utilities for rendering plain text templates.
|
||||||
|
|
||||||
|
import gleam/string_builder
|
||||||
|
import glemplate/renderer
|
||||||
|
import glemplate/ast
|
||||||
|
import glemplate/assigns
|
||||||
|
|
||||||
|
/// An encoder that just returns the content as-is.
|
||||||
|
pub fn encode(str: String) {
|
||||||
|
string_builder.from_string(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a plain text template.
|
||||||
|
pub fn render(
|
||||||
|
template: ast.Template,
|
||||||
|
assigns: assigns.Assigns,
|
||||||
|
template_cache: renderer.TemplateCache,
|
||||||
|
) -> renderer.RenderResult {
|
||||||
|
renderer.render(
|
||||||
|
template,
|
||||||
|
assigns,
|
||||||
|
renderer.RenderOptions(encoder: encode, template_cache: template_cache),
|
||||||
|
)
|
||||||
|
}
|
5
test/glemplate_test.gleam
Normal file
5
test/glemplate_test.gleam
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import gleeunit
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
gleeunit.main()
|
||||||
|
}
|
24
test/html_test.gleam
Normal file
24
test/html_test.gleam
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import gleam/map
|
||||||
|
import gleam/string_builder
|
||||||
|
import gleeunit/should
|
||||||
|
import glemplate/parser
|
||||||
|
import glemplate/html
|
||||||
|
import glemplate/assigns
|
||||||
|
|
||||||
|
pub fn encoding_test() {
|
||||||
|
let template = "<b>Welcome, <%= name %>!</b> <%= raw name %>"
|
||||||
|
|
||||||
|
assert Ok(tpl) = parser.parse_to_template(template, "input.html.glemp")
|
||||||
|
|
||||||
|
let template_cache = map.new()
|
||||||
|
let assigns =
|
||||||
|
assigns.new()
|
||||||
|
|> assigns.add_string("name", "||<xXx_KillaBoy_69>||")
|
||||||
|
|
||||||
|
assert Ok(result) = html.render(tpl, assigns, template_cache)
|
||||||
|
|
||||||
|
should.equal(
|
||||||
|
string_builder.to_string(result),
|
||||||
|
"<b>Welcome, ||<xXx_KillaBoy_69>||!</b> ||<xXx_KillaBoy_69>||",
|
||||||
|
)
|
||||||
|
}
|
362
test/parser_test.gleam
Normal file
362
test/parser_test.gleam
Normal file
|
@ -0,0 +1,362 @@
|
||||||
|
import gleeunit/should
|
||||||
|
import glemplate/parser
|
||||||
|
import glemplate/ast.{
|
||||||
|
Assign, Dynamic, FieldAccess, If, IndexAccess, Iter, Nodes, Output, RawOutput,
|
||||||
|
Render, Text,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output_test() {
|
||||||
|
let template = "<%= aurora %>"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[Nodes([]), Text(""), Dynamic(Output(Assign("aurora"))), Text("")],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn raw_output_test() {
|
||||||
|
let template = "<%= raw aurora %>"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[Nodes([]), Text(""), Dynamic(RawOutput(Assign("aurora"))), Text("")],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn if_test() {
|
||||||
|
let template = "<% if martin %>Hello, I'm Martin<% end %>"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[
|
||||||
|
Nodes([]),
|
||||||
|
Text(""),
|
||||||
|
Dynamic(If(Assign("martin"), [Text("Hello, I'm Martin")], [])),
|
||||||
|
Text(""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn if_else_test() {
|
||||||
|
let template =
|
||||||
|
"<% if martin %>Hello, I'm Martin<% else %>Sorry, I think you've mistaken me for someone else<% end %>"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[
|
||||||
|
Nodes([]),
|
||||||
|
Text(""),
|
||||||
|
Dynamic(If(
|
||||||
|
Assign("martin"),
|
||||||
|
[Text("Hello, I'm Martin")],
|
||||||
|
[Text("Sorry, I think you've mistaken me for someone else")],
|
||||||
|
)),
|
||||||
|
Text(""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_test() {
|
||||||
|
let template = "<% for item in shopping %>- <%= item %><% end %>"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[
|
||||||
|
Nodes([]),
|
||||||
|
Text(""),
|
||||||
|
Dynamic(Iter(
|
||||||
|
Assign("shopping"),
|
||||||
|
"item",
|
||||||
|
[Text("- "), Dynamic(Output(Assign("item"))), Text("")],
|
||||||
|
)),
|
||||||
|
Text(""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_test() {
|
||||||
|
let template = "<% render foo %>"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[Nodes([]), Text(""), Dynamic(Render("foo", [])), Text("")],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_1_binding_test() {
|
||||||
|
let template = "<% render foo a: b %>"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[
|
||||||
|
Nodes([]),
|
||||||
|
Text(""),
|
||||||
|
Dynamic(Render("foo", [#(Assign("b"), "a")])),
|
||||||
|
Text(""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_n_bindings_test() {
|
||||||
|
let template = "<% render foo a: b, c: d, e: f %>"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[
|
||||||
|
Nodes([]),
|
||||||
|
Text(""),
|
||||||
|
Dynamic(Render(
|
||||||
|
"foo",
|
||||||
|
[#(Assign("b"), "a"), #(Assign("d"), "c"), #(Assign("f"), "e")],
|
||||||
|
)),
|
||||||
|
Text(""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn field_access_test() {
|
||||||
|
let template = "<%= aurora.conqueror %>"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[
|
||||||
|
Nodes([]),
|
||||||
|
Text(""),
|
||||||
|
Dynamic(Output(FieldAccess(Assign("aurora"), "conqueror"))),
|
||||||
|
Text(""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index_access_test() {
|
||||||
|
let template = "<%= aurora.0 %>"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[
|
||||||
|
Nodes([]),
|
||||||
|
Text(""),
|
||||||
|
Dynamic(Output(IndexAccess(Assign("aurora"), 0))),
|
||||||
|
Text(""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chained_access_test() {
|
||||||
|
let template = "<%= aurora.albums.0.publish_year %>"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[
|
||||||
|
Nodes([]),
|
||||||
|
Text(""),
|
||||||
|
Dynamic(Output(FieldAccess(
|
||||||
|
IndexAccess(FieldAccess(Assign("aurora"), "albums"), 0),
|
||||||
|
"publish_year",
|
||||||
|
))),
|
||||||
|
Text(""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn raw_field_access_test() {
|
||||||
|
let template = "<%= raw aurora.conqueror %>"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[
|
||||||
|
Nodes([]),
|
||||||
|
Text(""),
|
||||||
|
Dynamic(RawOutput(FieldAccess(Assign("aurora"), "conqueror"))),
|
||||||
|
Text(""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn if_field_access_test() {
|
||||||
|
let template = "<% if user.is_martin %>Hello, I'm Martin<% end %>"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[
|
||||||
|
Nodes([]),
|
||||||
|
Text(""),
|
||||||
|
Dynamic(If(
|
||||||
|
FieldAccess(Assign("user"), "is_martin"),
|
||||||
|
[Text("Hello, I'm Martin")],
|
||||||
|
[],
|
||||||
|
)),
|
||||||
|
Text(""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_access_test() {
|
||||||
|
let template = "<% for item in user.shopping_lists.0 %>- <%= item %><% end %>"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[
|
||||||
|
Nodes([]),
|
||||||
|
Text(""),
|
||||||
|
Dynamic(Iter(
|
||||||
|
IndexAccess(FieldAccess(Assign("user"), "shopping_lists"), 0),
|
||||||
|
"item",
|
||||||
|
[Text("- "), Dynamic(Output(Assign("item"))), Text("")],
|
||||||
|
)),
|
||||||
|
Text(""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complex_test() {
|
||||||
|
let template =
|
||||||
|
"<h1>HIGH SCORES:</h1>
|
||||||
|
<ul>
|
||||||
|
<% for score in scores %>
|
||||||
|
<li>
|
||||||
|
<% if score %>
|
||||||
|
<% render score_tpl.html.glemp score: score %>
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
You can create comments by using comment tags: <%%!-- Comment --%>
|
||||||
|
|
||||||
|
<%!-- The pyramid: --%>
|
||||||
|
<% if user %>
|
||||||
|
<% if other_user %>
|
||||||
|
<% if third_user %>
|
||||||
|
<% for item in user.items %>
|
||||||
|
- User's item: <%= item %>
|
||||||
|
<% for ou_item in other_user.items %>
|
||||||
|
- Other user's item: <%= ou_item %>
|
||||||
|
<% for tu_item in third_user.items %>
|
||||||
|
- Third user's item: <%= tu_item %>
|
||||||
|
|
||||||
|
<% render item_combos.html.glemp item_1: item, item_2: ou_item, item_3: tu_item %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
Third user not found!
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
Second user not found!
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
User not found!
|
||||||
|
<% end %>
|
||||||
|
"
|
||||||
|
|
||||||
|
assert Ok(parsed) = parser.parse(template)
|
||||||
|
should.equal(
|
||||||
|
parsed,
|
||||||
|
[
|
||||||
|
Nodes([]),
|
||||||
|
Text("<h1>HIGH SCORES:</h1>\n<ul>\n "),
|
||||||
|
Dynamic(Iter(
|
||||||
|
Assign("scores"),
|
||||||
|
"score",
|
||||||
|
[
|
||||||
|
Text("\n <li>\n "),
|
||||||
|
Dynamic(If(
|
||||||
|
Assign("score"),
|
||||||
|
[
|
||||||
|
Text("\n "),
|
||||||
|
Dynamic(Render(
|
||||||
|
"score_tpl.html.glemp",
|
||||||
|
[#(Assign("score"), "score")],
|
||||||
|
)),
|
||||||
|
Text("\n "),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
)),
|
||||||
|
Text("\n </li>\n "),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
Text("\n</ul>\n\nYou can create comments by using comment tags: "),
|
||||||
|
Text("<%"),
|
||||||
|
Text("!-- Comment --%>\n\n"),
|
||||||
|
Text("\n"),
|
||||||
|
Dynamic(If(
|
||||||
|
Assign("user"),
|
||||||
|
[
|
||||||
|
Text("\n "),
|
||||||
|
Dynamic(If(
|
||||||
|
Assign("other_user"),
|
||||||
|
[
|
||||||
|
Text("\n "),
|
||||||
|
Dynamic(If(
|
||||||
|
Assign("third_user"),
|
||||||
|
[
|
||||||
|
Text("\n "),
|
||||||
|
Dynamic(Iter(
|
||||||
|
FieldAccess(Assign("user"), "items"),
|
||||||
|
"item",
|
||||||
|
[
|
||||||
|
Text("\n - User's item: "),
|
||||||
|
Dynamic(Output(Assign("item"))),
|
||||||
|
Text("\n "),
|
||||||
|
Dynamic(Iter(
|
||||||
|
FieldAccess(Assign("other_user"), "items"),
|
||||||
|
"ou_item",
|
||||||
|
[
|
||||||
|
Text("\n - Other user's item: "),
|
||||||
|
Dynamic(Output(Assign("ou_item"))),
|
||||||
|
Text("\n "),
|
||||||
|
Dynamic(Iter(
|
||||||
|
FieldAccess(Assign("third_user"), "items"),
|
||||||
|
"tu_item",
|
||||||
|
[
|
||||||
|
Text("\n - Third user's item: "),
|
||||||
|
Dynamic(Output(Assign("tu_item"))),
|
||||||
|
Text("\n\n "),
|
||||||
|
Dynamic(Render(
|
||||||
|
"item_combos.html.glemp",
|
||||||
|
[
|
||||||
|
#(Assign("item"), "item_1"),
|
||||||
|
#(Assign("ou_item"), "item_2"),
|
||||||
|
#(Assign("tu_item"), "item_3"),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
Text("\n "),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
Text("\n "),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
Text("\n "),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
Text("\n "),
|
||||||
|
],
|
||||||
|
[Text("\n Third user not found!\n ")],
|
||||||
|
)),
|
||||||
|
Text("\n "),
|
||||||
|
],
|
||||||
|
[Text("\n Second user not found!\n ")],
|
||||||
|
)),
|
||||||
|
Text("\n"),
|
||||||
|
],
|
||||||
|
[Text("\n User not found!\n")],
|
||||||
|
)),
|
||||||
|
Text("\n"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
420
test/renderer_test.gleam
Normal file
420
test/renderer_test.gleam
Normal file
|
@ -0,0 +1,420 @@
|
||||||
|
import gleam/map
|
||||||
|
import gleam/string_builder
|
||||||
|
import gleeunit/should
|
||||||
|
import glemplate/assigns.{Assigns, Bool, Int, Lazy, List, Map, String}
|
||||||
|
import glemplate/ast.{
|
||||||
|
Assign, Dynamic, FieldAccess, If, IndexAccess, Iter, NodeList, Nodes, Output,
|
||||||
|
RawOutput, Render, Template, Text,
|
||||||
|
}
|
||||||
|
import glemplate/renderer
|
||||||
|
|
||||||
|
pub fn empty_test() {
|
||||||
|
should.equal(render_to_string([], map.new()), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text_test() {
|
||||||
|
should.equal(
|
||||||
|
render_to_string([Text("foo"), Text(" bar "), Text("baz")], map.new()),
|
||||||
|
"foo bar baz",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nested_test() {
|
||||||
|
should.equal(
|
||||||
|
render_to_string(
|
||||||
|
[Nodes([Nodes([Text("foo")])]), Nodes([Nodes([Nodes([])])])],
|
||||||
|
map.new(),
|
||||||
|
),
|
||||||
|
"foo",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn var_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.new()
|
||||||
|
|> map.insert("foo", String("bar"))
|
||||||
|
should.equal(
|
||||||
|
render_to_string(
|
||||||
|
[Text("<b>"), Dynamic(Output(Assign("foo"))), Text("</b>")],
|
||||||
|
assigns,
|
||||||
|
),
|
||||||
|
"<b>bar</b>",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encoder_test() {
|
||||||
|
assert Ok(builder) =
|
||||||
|
renderer.render(
|
||||||
|
Template(
|
||||||
|
name: "TestTpl",
|
||||||
|
nodes: [
|
||||||
|
Dynamic(Output(Assign("foo"))),
|
||||||
|
Dynamic(RawOutput(Assign("foo"))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
map.new()
|
||||||
|
|> map.insert("foo", String("🙂")),
|
||||||
|
renderer.RenderOptions(
|
||||||
|
encoder: fn(_content) { string_builder.from_string("🙃") },
|
||||||
|
template_cache: map.new(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
should.equal(string_builder.to_string(builder), "🙃🙂")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn int_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.new()
|
||||||
|
|> map.insert("foo", Int(1_000_000))
|
||||||
|
should.equal(
|
||||||
|
render_to_string([Dynamic(Output(Assign("foo")))], assigns),
|
||||||
|
"1000000",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lazy_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.new()
|
||||||
|
|> map.insert("foo", Lazy(fn() { String("awesome") }))
|
||||||
|
should.equal(
|
||||||
|
render_to_string([Dynamic(Output(Assign("foo")))], assigns),
|
||||||
|
"awesome",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn if_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.new()
|
||||||
|
|> map.insert("foo", Bool(True))
|
||||||
|
should.equal(
|
||||||
|
render_to_string(
|
||||||
|
[Dynamic(If(Assign("foo"), [Text("yes")], [Text("no")]))],
|
||||||
|
assigns,
|
||||||
|
),
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy function should not be called in code path that is not entered
|
||||||
|
pub fn if_lazy_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.from_list([
|
||||||
|
#("foo", Bool(True)),
|
||||||
|
#(
|
||||||
|
"lazy",
|
||||||
|
Lazy(fn() {
|
||||||
|
should.fail()
|
||||||
|
Bool(False)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
should.equal(
|
||||||
|
render_to_string(
|
||||||
|
[
|
||||||
|
Dynamic(If(
|
||||||
|
Assign("foo"),
|
||||||
|
[Text("yes")],
|
||||||
|
[Dynamic(Output(Assign("lazy")))],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
assigns,
|
||||||
|
),
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.new()
|
||||||
|
|> map.insert("foo", List([String("first"), Int(2), String("third")]))
|
||||||
|
should.equal(
|
||||||
|
render_to_string(
|
||||||
|
[
|
||||||
|
Dynamic(Iter(
|
||||||
|
Assign("foo"),
|
||||||
|
"bar",
|
||||||
|
[Text("* "), Dynamic(Output(Assign("bar"))), Text("\n")],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
assigns,
|
||||||
|
),
|
||||||
|
"* first\n* 2\n* third\n",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.new()
|
||||||
|
|> map.insert("parent_assign", Bool(False))
|
||||||
|
should.equal(
|
||||||
|
render_to_string(
|
||||||
|
[
|
||||||
|
Text("child tpl -- "),
|
||||||
|
Dynamic(Render("ChildTpl", [#(Assign("parent_assign"), "child_assign")])),
|
||||||
|
Text(" -- end child tpl"),
|
||||||
|
],
|
||||||
|
assigns,
|
||||||
|
),
|
||||||
|
"child tpl -- child_assign is false -- end child tpl",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn field_access_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.new()
|
||||||
|
|> map.insert("foo", Map(map.from_list([#("bar", String("baz"))])))
|
||||||
|
should.equal(
|
||||||
|
render_to_string(
|
||||||
|
[
|
||||||
|
Text("<b>"),
|
||||||
|
Dynamic(Output(FieldAccess(Assign("foo"), "bar"))),
|
||||||
|
Text("</b>"),
|
||||||
|
],
|
||||||
|
assigns,
|
||||||
|
),
|
||||||
|
"<b>baz</b>",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index_access_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.new()
|
||||||
|
|> map.insert("foo", List([String("bar"), String("baz")]))
|
||||||
|
should.equal(
|
||||||
|
render_to_string(
|
||||||
|
[
|
||||||
|
Text("<b>"),
|
||||||
|
Dynamic(Output(IndexAccess(Assign("foo"), 1))),
|
||||||
|
Text("</b>"),
|
||||||
|
],
|
||||||
|
assigns,
|
||||||
|
),
|
||||||
|
"<b>baz</b>",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complex_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.from_list([
|
||||||
|
#(
|
||||||
|
"user",
|
||||||
|
Map(map.from_list([
|
||||||
|
#("name", String("Nicd")),
|
||||||
|
#("registered", Lazy(fn() { String("2023-01-31") })),
|
||||||
|
#("points", Int(13_992_355_986)),
|
||||||
|
#("level", Int(32)),
|
||||||
|
#("paid", Bool(True)),
|
||||||
|
#(
|
||||||
|
"top_languages",
|
||||||
|
List([
|
||||||
|
String("Gleam"),
|
||||||
|
String("Elixir"),
|
||||||
|
String("TypeScript"),
|
||||||
|
String("JavaScript"),
|
||||||
|
String("Python"),
|
||||||
|
String("YaBasic"),
|
||||||
|
String("VHDL"),
|
||||||
|
String("GTLT"),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
])),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
let str =
|
||||||
|
render_to_string(
|
||||||
|
[
|
||||||
|
Text("User information:\n"),
|
||||||
|
Text("\n"),
|
||||||
|
Nodes([
|
||||||
|
Dynamic(Output(FieldAccess(Assign("user"), "name"))),
|
||||||
|
Text(" - "),
|
||||||
|
Dynamic(Render(
|
||||||
|
"points.txt.glemp",
|
||||||
|
assigns_map: [
|
||||||
|
#(FieldAccess(Assign("user"), "level"), "level"),
|
||||||
|
#(FieldAccess(Assign("user"), "points"), "points"),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
Text("\n"),
|
||||||
|
]),
|
||||||
|
Text("Registered on "),
|
||||||
|
Dynamic(Output(FieldAccess(Assign("user"), "registered"))),
|
||||||
|
Dynamic(If(
|
||||||
|
FieldAccess(Assign("user"), "paid"),
|
||||||
|
[Text(" (awesome supporter).")],
|
||||||
|
[Text(".")],
|
||||||
|
)),
|
||||||
|
Text("\n\n"),
|
||||||
|
Text("User's top languages\n"),
|
||||||
|
Text("--------------------\n"),
|
||||||
|
Dynamic(Iter(
|
||||||
|
FieldAccess(Assign("user"), "top_languages"),
|
||||||
|
"lang",
|
||||||
|
[Text("* "), Dynamic(Output(Assign("lang"))), Text("\n")],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
assigns,
|
||||||
|
)
|
||||||
|
|
||||||
|
should.equal(
|
||||||
|
str,
|
||||||
|
"User information:
|
||||||
|
|
||||||
|
Nicd - 13992355986 XP (level 32)
|
||||||
|
Registered on 2023-01-31 (awesome supporter).
|
||||||
|
|
||||||
|
User's top languages
|
||||||
|
--------------------
|
||||||
|
* Gleam
|
||||||
|
* Elixir
|
||||||
|
* TypeScript
|
||||||
|
* JavaScript
|
||||||
|
* Python
|
||||||
|
* YaBasic
|
||||||
|
* VHDL
|
||||||
|
* GTLT
|
||||||
|
",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hayleigh_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.from_list([
|
||||||
|
#("my_var", String(":)")),
|
||||||
|
#("pekka", Bool(False)),
|
||||||
|
#("some_items", List([String("first"), Int(2), String("third")])),
|
||||||
|
#("user", String("Nicd")),
|
||||||
|
])
|
||||||
|
|
||||||
|
let str =
|
||||||
|
render_to_string(
|
||||||
|
[
|
||||||
|
Text("<% <-- Escaped\n\n\n\nJotain tämmöstä <b>"),
|
||||||
|
Dynamic(Output(Assign("my_var"))),
|
||||||
|
Text("</b>.\nRaw variable: "),
|
||||||
|
Dynamic(RawOutput(Assign("my_var"))),
|
||||||
|
Text(".\n\n"),
|
||||||
|
Dynamic(If(
|
||||||
|
Assign("pekka"),
|
||||||
|
[Text("Olen pekka.\n")],
|
||||||
|
[Text("En ole pekka.\n")],
|
||||||
|
)),
|
||||||
|
Text("\n\n"),
|
||||||
|
Dynamic(Iter(
|
||||||
|
Assign("some_items"),
|
||||||
|
"item",
|
||||||
|
[Text(" * "), Dynamic(Output(Assign("item"))), Text("\n")],
|
||||||
|
)),
|
||||||
|
Text("\n\n\n"),
|
||||||
|
Dynamic(Render("some_template", [#(Assign("user"), "user")])),
|
||||||
|
],
|
||||||
|
assigns,
|
||||||
|
)
|
||||||
|
|
||||||
|
should.equal(
|
||||||
|
str,
|
||||||
|
"<% <-- Escaped\n\n\n\nJotain tämmöstä <b>:)</b>.\nRaw variable: :).\n\nEn ole pekka.\n\n\n * first\n * 2\n * third\n\n\n\n",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unstringifiable_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.new()
|
||||||
|
|> map.insert("foo", Bool(True))
|
||||||
|
should.be_error(render_tpl([Dynamic(Output(Assign("foo")))], assigns))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn missing_assign_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.new()
|
||||||
|
|> map.insert("foo2", String("bar"))
|
||||||
|
should.be_error(render_tpl([Dynamic(Output(Assign("foo")))], assigns))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn not_iterable_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.new()
|
||||||
|
|> map.insert("foo", String("bar"))
|
||||||
|
should.be_error(render_tpl(
|
||||||
|
[Dynamic(Iter(Assign("foo"), "binding", []))],
|
||||||
|
assigns,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn missing_child_test() {
|
||||||
|
let assigns: Assigns = map.new()
|
||||||
|
should.be_error(render_tpl([Dynamic(Render("no", []))], assigns))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_to_string(nodes: NodeList, assigns: Assigns) {
|
||||||
|
assert Ok(builder) = render_tpl(nodes, assigns)
|
||||||
|
|
||||||
|
string_builder.to_string(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_tpl(nodes: NodeList, assigns: Assigns) {
|
||||||
|
renderer.render(
|
||||||
|
Template(name: "TestTpl", nodes: nodes),
|
||||||
|
assigns,
|
||||||
|
renderer.RenderOptions(
|
||||||
|
encoder: fn(content) { string_builder.from_string(content) },
|
||||||
|
template_cache: map.from_list([
|
||||||
|
#(
|
||||||
|
"ChildTpl",
|
||||||
|
Template(
|
||||||
|
name: "ChildTpl",
|
||||||
|
nodes: [
|
||||||
|
Dynamic(If(
|
||||||
|
Assign("child_assign"),
|
||||||
|
[],
|
||||||
|
[Text("child_assign is false")],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
#(
|
||||||
|
"points.txt.glemp",
|
||||||
|
Template(
|
||||||
|
name: "points.txt.glemp",
|
||||||
|
nodes: [
|
||||||
|
Dynamic(Output(Assign("points"))),
|
||||||
|
Text(" XP (level "),
|
||||||
|
Dynamic(Output(Assign("level"))),
|
||||||
|
Text(")"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
#("some_template", Template(name: "some_template", nodes: [])),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn missing_field_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.new()
|
||||||
|
|> map.insert("foo", Map(map.from_list([#("bar", String("baz"))])))
|
||||||
|
should.be_error(render_tpl(
|
||||||
|
[
|
||||||
|
Text("<b>"),
|
||||||
|
Dynamic(Output(FieldAccess(Assign("foo"), "noooooo"))),
|
||||||
|
Text("</b>"),
|
||||||
|
],
|
||||||
|
assigns,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn out_of_bounds_test() {
|
||||||
|
let assigns: Assigns =
|
||||||
|
map.new()
|
||||||
|
|> map.insert("foo", List([String("bar"), String("baz")]))
|
||||||
|
should.be_error(render_tpl(
|
||||||
|
[
|
||||||
|
Text("<b>"),
|
||||||
|
Dynamic(Output(IndexAccess(Assign("foo"), 100_000))),
|
||||||
|
Text("</b>"),
|
||||||
|
],
|
||||||
|
assigns,
|
||||||
|
))
|
||||||
|
}
|
24
test/text_test.gleam
Normal file
24
test/text_test.gleam
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import gleam/map
|
||||||
|
import gleam/string_builder
|
||||||
|
import gleeunit/should
|
||||||
|
import glemplate/parser
|
||||||
|
import glemplate/text
|
||||||
|
import glemplate/assigns
|
||||||
|
|
||||||
|
pub fn encoding_test() {
|
||||||
|
let template = "**Welcome, <%= name %>!** <%= raw name %>"
|
||||||
|
|
||||||
|
assert Ok(tpl) = parser.parse_to_template(template, "input.txt.glemp")
|
||||||
|
|
||||||
|
let template_cache = map.new()
|
||||||
|
let assigns =
|
||||||
|
assigns.new()
|
||||||
|
|> assigns.add_string("name", "||<xXx_KillaBoy_69>||")
|
||||||
|
|
||||||
|
assert Ok(result) = text.render(tpl, assigns, template_cache)
|
||||||
|
|
||||||
|
should.equal(
|
||||||
|
string_builder.to_string(result),
|
||||||
|
"**Welcome, ||<xXx_KillaBoy_69>||!** ||<xXx_KillaBoy_69>||",
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in a new issue