Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for importing partial templates #3

Merged
merged 6 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import handles/ctx
pub fn main() {
let assert Ok(template) = handles.prepare("Hello {{name}}")
let assert Ok(string) =
handles.run(template, ctx.Dict([ctx.Prop("name", ctx.Str("Oliver"))]))
handles.run(template, ctx.Dict([ctx.Prop("name", ctx.Str("Oliver"))], []))

io.debug(string)
}
Expand All @@ -27,7 +27,7 @@ pub fn main() {

__Handles__ a is very simple templating language that consists of a single primitive, the "tag".
A tag starts with two open braces `{{`, followed by a string body, and ends with two closing braces `}}`.
There are two kinds of tags, [Property tags](#property-tags) and [Block tags](#block-tags).
There are three kinds of tags, [Property tags](#property-tags), [Block tags](#block-tags), and [Partial tags](#partial-tags).

### Property tags

Expand Down Expand Up @@ -80,7 +80,7 @@ Accessing a property which was not passed into the template engine will result i
#### each

`each` blocks are used to include a part of a templated zero or more times.
Values accessed by an `each` block must be of type `List(Dict)` or it will result in a runtime error.
Values accessed by an `each` block must be of type `List` or it will result in a runtime error.
Accessing a property which was not passed into the template engine will result in a runtime error.
The context of which properties are resolved while inside the each block will be scoped to the current item from the list.

Expand All @@ -92,6 +92,16 @@ The context of which properties are resolved while inside the each block will be

Further documentation can be found at <https://hexdocs.pm/handles>.

### Partial tags

A partial tag is used to include other templates in-place of the partial tag.
Values accessed by a partial tag can be of any type and will be passed to the partial as the context of which properties are resolved against while inside the partial template.
Accessing a property which was not passed into the template engine will result in a runtime error.

```handlebars
{{>some_template some.prop.path}}
```

## Development

```sh
Expand Down
12 changes: 11 additions & 1 deletion src/handles.gleam
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import gleam/dict
import gleam/result
import gleam/string_builder
import handles/ctx
Expand All @@ -21,7 +22,16 @@ pub fn prepare(template: String) -> Result(Template, error.TokenizerError) {
pub fn run(
template: Template,
ctx: ctx.Value,
partials: List(#(String, Template)),
) -> Result(String, error.RuntimeError) {
let Template(ast) = template
engine.run(ast, ctx, string_builder.new())
let partials_dict =
partials
|> dict.from_list
|> dict.map_values(fn(_, template) {
let Template(ast) = template
ast
})

engine.run(ast, ctx, partials_dict, string_builder.new())
}
13 changes: 8 additions & 5 deletions src/handles/error.gleam
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
pub type TokenizerError {
UnbalancedTag(index: Int)
MissingPropertyPath(index: Int)
MissingBlockArgument(index: Int)
UnexpectedBlockArgument(index: Int)
MissingArgument(index: Int)
MissingBlockKind(index: Int)
MissingPartialId(index: Int)
UnexpectedMultipleArguments(index: Int)
UnexpectedArgument(index: Int)
UnexpectedBlockKind(index: Int)
}

pub type RuntimeError {
UnexpectedTypeError(
UnexpectedType(
index: Int,
path: List(String),
got: String,
expected: List(String),
)
UnknownPropertyError(index: Int, path: List(String))
UnknownProperty(index: Int, path: List(String))
UnknownPartial(index: Int, id: String)
}
28 changes: 15 additions & 13 deletions src/handles/format.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,18 @@ pub fn format_tokenizer_error(
case error {
error.UnbalancedTag(index) ->
transform_error(template, index, "Tag is missing closing braces }}")
error.UnexpectedBlockArgument(index) ->
transform_error(
template,
index,
"Tag is a closing block, which does not take any arguments",
)
error.MissingBlockArgument(index) ->
transform_error(template, index, "Tag is missing block argument")
error.MissingPropertyPath(index) ->
transform_error(template, index, "Tag is missing property path")
error.MissingArgument(index) ->
transform_error(template, index, "Tag is missing an argument")
error.MissingBlockKind(index) ->
transform_error(template, index, "Tag is missing a block kind")
error.MissingPartialId(index) ->
transform_error(template, index, "Tag is missing a partial id")
error.UnexpectedBlockKind(index) ->
transform_error(template, index, "Tag is of unknown block kind")
transform_error(template, index, "Tag is of an unknown block kind")
error.UnexpectedMultipleArguments(index) ->
transform_error(template, index, "Tag is receiving too many arguments")
error.UnexpectedArgument(index) ->
transform_error(template, index, "Tag is receiving too many arguments")
}
}

Expand All @@ -80,7 +80,7 @@ pub fn format_runtime_error(
template: String,
) -> Result(String, Nil) {
case error {
error.UnexpectedTypeError(index, path, got, expected) ->
error.UnexpectedType(index, path, got, expected) ->
transform_error(
template,
index,
Expand All @@ -91,11 +91,13 @@ pub fn format_runtime_error(
<> " but found found "
<> got,
)
error.UnknownPropertyError(index, path) ->
error.UnknownProperty(index, path) ->
transform_error(
template,
index,
"Unable to resolve property " <> string.join(path, "."),
)
error.UnknownPartial(index, id) ->
transform_error(template, index, "Unknown partial " <> id)
}
}
97 changes: 38 additions & 59 deletions src/handles/internal/ctx_utils.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import gleam/result
import handles/ctx
import handles/error

pub fn drill_ctx(
fn drill_ctx(
path: List(String),
ctx: ctx.Value,
) -> Result(ctx.Value, error.RuntimeError) {
Expand All @@ -16,112 +16,91 @@ pub fn drill_ctx(
ctx.Dict(arr) -> {
case list.find(arr, fn(it) { it.key == key }) {
Ok(ctx.Prop(_, value)) -> drill_ctx(rest, value)
Error(_) -> Error(error.UnknownPropertyError(0, []))
Error(_) -> Error(error.UnknownProperty(0, []))
}
}
ctx.List(_) -> Error(error.UnexpectedTypeError(0, [], "List", ["Dict"]))
ctx.Str(_) -> Error(error.UnexpectedTypeError(0, [], "Str", ["Dict"]))
ctx.Int(_) -> Error(error.UnexpectedTypeError(0, [], "Int", ["Dict"]))
ctx.Float(_) ->
Error(error.UnexpectedTypeError(0, [], "Float", ["Dict"]))
ctx.Bool(_) -> Error(error.UnexpectedTypeError(0, [], "Bool", ["Dict"]))
ctx.List(_) -> Error(error.UnexpectedType(0, [], "List", ["Dict"]))
ctx.Str(_) -> Error(error.UnexpectedType(0, [], "Str", ["Dict"]))
ctx.Int(_) -> Error(error.UnexpectedType(0, [], "Int", ["Dict"]))
ctx.Float(_) -> Error(error.UnexpectedType(0, [], "Float", ["Dict"]))
ctx.Bool(_) -> Error(error.UnexpectedType(0, [], "Bool", ["Dict"]))
}
}
}

pub fn get_property(
index: Int,
path: List(String),
root_ctx: ctx.Value,
) -> Result(String, error.RuntimeError) {
drill_ctx(path, root_ctx)
pub fn get(path: List(String), ctx: ctx.Value, index: Int) {
drill_ctx(path, ctx)
|> result.map_error(fn(err) {
case err {
error.UnexpectedTypeError(_, _, got, expected) ->
error.UnexpectedTypeError(index, path, got, expected)
error.UnknownPropertyError(_, _) ->
error.UnknownPropertyError(index, path)
error.UnexpectedType(_, _, got, expected) ->
error.UnexpectedType(index, path, got, expected)
error.UnknownProperty(_, _) -> error.UnknownProperty(index, path)
err -> err
}
})
}

pub fn get_property(
path: List(String),
root_ctx: ctx.Value,
index: Int,
) -> Result(String, error.RuntimeError) {
get(path, root_ctx, index)
|> result.try(fn(it) {
case it {
ctx.Str(value) -> value |> Ok
ctx.Int(value) -> value |> int.to_string |> Ok
ctx.Float(value) -> value |> float.to_string |> Ok
ctx.List(_) ->
Error(
error.UnexpectedTypeError(index, path, "List", ["Str", "Int", "Float"]),
error.UnexpectedType(index, path, "List", ["Str", "Int", "Float"]),
)
ctx.Dict(_) ->
Error(
error.UnexpectedTypeError(index, path, "Dict", ["Str", "Int", "Float"]),
error.UnexpectedType(index, path, "Dict", ["Str", "Int", "Float"]),
)
ctx.Bool(_) ->
Error(
error.UnexpectedTypeError(index, path, "Bool", ["Str", "Int", "Float"]),
error.UnexpectedType(index, path, "Bool", ["Str", "Int", "Float"]),
)
}
})
}

pub fn get_list(
index: Int,
path: List(String),
root_ctx: ctx.Value,
index: Int,
) -> Result(List(ctx.Value), error.RuntimeError) {
drill_ctx(path, root_ctx)
|> result.map_error(fn(err) {
case err {
error.UnexpectedTypeError(_, _, got, expected) ->
error.UnexpectedTypeError(index, path, got, expected)
error.UnknownPropertyError(_, _) ->
error.UnknownPropertyError(index, path)
}
})
get(path, root_ctx, index)
|> result.try(fn(it) {
case it {
ctx.List(value) -> value |> Ok
ctx.Bool(_) ->
Error(error.UnexpectedTypeError(index, path, "Bool", ["List"]))
ctx.Str(_) ->
Error(error.UnexpectedTypeError(index, path, "Str", ["List"]))
ctx.Int(_) ->
Error(error.UnexpectedTypeError(index, path, "Int", ["List"]))
ctx.Bool(_) -> Error(error.UnexpectedType(index, path, "Bool", ["List"]))
ctx.Str(_) -> Error(error.UnexpectedType(index, path, "Str", ["List"]))
ctx.Int(_) -> Error(error.UnexpectedType(index, path, "Int", ["List"]))
ctx.Float(_) ->
Error(error.UnexpectedTypeError(index, path, "Float", ["List"]))
ctx.Dict(_) ->
Error(error.UnexpectedTypeError(index, path, "Dict", ["List"]))
Error(error.UnexpectedType(index, path, "Float", ["List"]))
ctx.Dict(_) -> Error(error.UnexpectedType(index, path, "Dict", ["List"]))
}
})
}

pub fn get_bool(
index: Int,
path: List(String),
root_ctx: ctx.Value,
index: Int,
) -> Result(Bool, error.RuntimeError) {
drill_ctx(path, root_ctx)
|> result.map_error(fn(err) {
case err {
error.UnexpectedTypeError(_, _, got, expected) ->
error.UnexpectedTypeError(index, path, got, expected)
error.UnknownPropertyError(_, _) ->
error.UnknownPropertyError(index, path)
}
})
get(path, root_ctx, index)
|> result.try(fn(it) {
case it {
ctx.Bool(value) -> value |> Ok
ctx.List(_) ->
Error(error.UnexpectedTypeError(index, path, "List", ["Bool"]))
ctx.Str(_) ->
Error(error.UnexpectedTypeError(index, path, "Str", ["Bool"]))
ctx.Int(_) ->
Error(error.UnexpectedTypeError(index, path, "Int", ["Bool"]))
ctx.List(_) -> Error(error.UnexpectedType(index, path, "List", ["Bool"]))
ctx.Str(_) -> Error(error.UnexpectedType(index, path, "Str", ["Bool"]))
ctx.Int(_) -> Error(error.UnexpectedType(index, path, "Int", ["Bool"]))
ctx.Float(_) ->
Error(error.UnexpectedTypeError(index, path, "Float", ["Bool"]))
ctx.Dict(_) ->
Error(error.UnexpectedTypeError(index, path, "Dict", ["Bool"]))
Error(error.UnexpectedType(index, path, "Float", ["Bool"]))
ctx.Dict(_) -> Error(error.UnexpectedType(index, path, "Dict", ["Bool"]))
}
})
}
33 changes: 23 additions & 10 deletions src/handles/internal/engine.gleam
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import gleam/bool
import gleam/dict
import gleam/result
import gleam/string_builder
import handles/ctx
Expand All @@ -10,13 +11,14 @@ fn run_each(
ctxs: List(ctx.Value),
ast: List(parser.AST),
builder: string_builder.StringBuilder,
partials: dict.Dict(String, List(parser.AST)),
) -> Result(String, error.RuntimeError) {
case ctxs {
[] -> builder |> string_builder.to_string |> Ok
[ctx, ..rest] ->
run(ast, ctx, string_builder.new())
run(ast, ctx, partials, string_builder.new())
|> result.try(fn(it) {
run_each(rest, ast, builder |> string_builder.append(it))
run_each(rest, ast, builder |> string_builder.append(it), partials)
})
}
}
Expand All @@ -25,39 +27,50 @@ fn run_if(
bool: Bool,
children: List(parser.AST),
ctx: ctx.Value,
partials: dict.Dict(String, List(parser.AST)),
) -> Result(String, error.RuntimeError) {
case bool {
False -> Ok("")
True -> run(children, ctx, string_builder.new())
True -> run(children, ctx, partials, string_builder.new())
}
}

pub fn run(
ast: List(parser.AST),
ctx: ctx.Value,
partials: dict.Dict(String, List(parser.AST)),
builder: string_builder.StringBuilder,
) -> Result(String, error.RuntimeError) {
case ast {
[] -> builder |> string_builder.to_string |> Ok
[node, ..rest] ->
case node {
parser.Constant(_, value) -> Ok(value)
parser.Property(index, path) -> ctx_utils.get_property(index, path, ctx)
parser.Property(index, path) -> ctx_utils.get_property(path, ctx, index)
parser.Partial(index, id, path) ->
case dict.get(partials, id) {
Error(_) -> Error(error.UnknownPartial(index, id))
Ok(partial) ->
ctx_utils.get(path, ctx, index)
|> result.try(run(partial, _, partials, string_builder.new()))
}

parser.IfBlock(index, path, children) ->
ctx_utils.get_bool(index, path, ctx)
|> result.try(run_if(_, children, ctx))
ctx_utils.get_bool(path, ctx, index)
|> result.try(run_if(_, children, ctx, partials))
parser.UnlessBlock(index, path, children) ->
ctx_utils.get_bool(index, path, ctx)
ctx_utils.get_bool(path, ctx, index)
|> result.map(bool.negate)
|> result.try(run_if(_, children, ctx))
|> result.try(run_if(_, children, ctx, partials))
parser.EachBlock(index, path, children) ->
ctx_utils.get_list(index, path, ctx)
|> result.try(run_each(_, children, string_builder.new()))
ctx_utils.get_list(path, ctx, index)
|> result.try(run_each(_, children, string_builder.new(), partials))
}
|> result.try(fn(it) {
run(
rest,
ctx,
partials,
builder
|> string_builder.append(it),
)
Expand Down
Loading