diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc8161d..b974b0d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+## Unreleased
+
+- Added support for 'for .. as .. in ..' syntax to allow associating a type with the items in the
+ list being iterated.
+
## 0.5.0
- Added --version flag to executable to print out current version.
diff --git a/README.md b/README.md
index feff675..6880c61 100644
--- a/README.md
+++ b/README.md
@@ -79,6 +79,18 @@ after `in`.
```
+Additionally you can use the `as` keyword to associate a type with the items being iterated over.
+This is necessary if you're using a complex object.
+
+```html+jinja
+{> import my_user.{MyUser}
+
+{% for user as MyUser in users %}
+ - {{ user.name }}
+{% endfor %}
+
+```
+
### Import
You can use the `{>` syntax to add import statements to the template. These are used to import types
diff --git a/src/main.rs b/src/main.rs
index 08a0709..3da78bb 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -363,14 +363,16 @@ fn eat_spaces(iter: &mut Iter) {
type TokenIter<'a> = std::iter::Peekable>;
+type Type = String;
+
#[derive(Debug)]
enum Node {
Text(String),
Identifier(String),
If(String, Vec, Vec),
- For(String, String, Vec),
+ For(String, Option, String, Vec),
Import(String),
- With(String, String),
+ With(String, Type),
}
#[derive(Debug)]
@@ -501,7 +503,22 @@ fn parse_if_statement(tokens: &mut TokenIter) -> Result {
fn parse_for_statement(tokens: &mut TokenIter) -> Result {
let entry_identifier = extract_identifier(tokens)?;
- consume_token(tokens, Token::In)?;
+ let entry_type = match tokens.next() {
+ Some((Token::As, _)) => {
+ let type_identifier = extract_identifier(tokens)?;
+ consume_token(tokens, Token::In)?;
+ Some(type_identifier)
+ }
+ Some((Token::In, _)) => None,
+ Some((matched_token, range)) => {
+ return Err(ParserError::UnexpectedToken(
+ matched_token.clone(),
+ range.clone(),
+ vec![Token::As, Token::In],
+ ));
+ }
+ None => return Err(ParserError::UnexpectedEnd),
+ };
let list_identifier = extract_identifier(tokens)?;
consume_token(tokens, Token::CloseStmt)?;
@@ -511,7 +528,12 @@ fn parse_for_statement(tokens: &mut TokenIter) -> Result {
consume_token(tokens, Token::EndFor)?;
consume_token(tokens, Token::CloseStmt)?;
- Ok(Node::For(entry_identifier, list_identifier, loop_nodes))
+ Ok(Node::For(
+ entry_identifier,
+ entry_type,
+ list_identifier,
+ loop_nodes,
+ ))
}
fn extract_identifier(tokens: &mut TokenIter) -> Result {
@@ -670,22 +692,30 @@ fn render_lines(
params.append(&mut if_params);
params.append(&mut else_params);
}
- Some(Node::For(entry_identifier, list_identifier, loop_nodes)) => {
+ Some(Node::For(entry_identifier, entry_type, list_identifier, loop_nodes)) => {
iter.next();
+
+ let entry_type = entry_type
+ .as_ref()
+ .map(|value| format!(": {}", value))
+ .unwrap_or("".to_string());
+
let (loop_lines, mut loop_params, _, _) =
render_lines(&mut loop_nodes.iter().peekable());
builder_lines.push_str(&format!(
- r#" let builder = list.fold({}, builder, fn(builder, {}) {{
+ r#" let builder = list.fold({}, builder, fn(builder, {}{}) {{
{}
builder
}})
"#,
- list_identifier, entry_identifier, loop_lines
+ list_identifier, entry_identifier, entry_type, loop_lines
));
params.push(list_identifier.clone());
// Remove the for-loop identifier which will have been detected as a 'param'
- loop_params.retain(|value| value != entry_identifier);
+ // We split any discovered identifiers on '.' and compare the first part so that if
+ // the entry_identifier is 'user' and we find 'user.name' we still rule it out
+ loop_params.retain(|value| value.split(".").next() != Some(entry_identifier));
params.append(&mut loop_params);
}
None => break,
@@ -1013,6 +1043,11 @@ mod test {
assert_scan!("Hello {% for item in list %}{{ item }}{% endfor %}");
}
+ #[test]
+ fn test_scan_for_as_loop() {
+ assert_scan!("Hello {% for item as Item in list %}{{ item }}{% endfor %}");
+ }
+
#[test]
fn test_scan_dot_access() {
assert_scan!("Hello{% if user.is_admin %} Admin{% endif %}");
@@ -1077,6 +1112,13 @@ mod test {
assert_parse!("Hello,{% for item in list %} to {{ item }} and {% endfor %} everyone else");
}
+ #[test]
+ fn test_parse_for_as_loop() {
+ assert_parse!(
+ "Hello,{% for item as Item in list %} to {{ item }} and {% endfor %} everyone else"
+ );
+ }
+
#[test]
fn test_parse_dot_access() {
assert_parse!("Hello{% if user.is_admin %} Admin{% endif %}");
@@ -1141,6 +1183,13 @@ mod test {
assert_render!("Hello,{% for item in list %} to {{ item }} and {% endfor %} everyone else");
}
+ #[test]
+ fn test_render_for_as_loop() {
+ assert_render!(
+ "Hello,{% for item as Item in list %} to {{ item }} and {% endfor %} everyone else"
+ );
+ }
+
#[test]
fn test_render_dot_access() {
assert_render!("Hello{% if user.is_admin %} Admin{% endif %}");
diff --git a/src/snapshots/templates__test__parse_for_as_loop.snap b/src/snapshots/templates__test__parse_for_as_loop.snap
new file mode 100644
index 0000000..2ab5684
--- /dev/null
+++ b/src/snapshots/templates__test__parse_for_as_loop.snap
@@ -0,0 +1,32 @@
+---
+source: src/main.rs
+assertion_line: 1115
+expression: "Hello,{% for item as Item in list %} to {{ item }} and {% endfor %} everyone else"
+
+---
+[
+ Text(
+ "Hello,",
+ ),
+ For(
+ "item",
+ Some(
+ "Item",
+ ),
+ "list",
+ [
+ Text(
+ " to ",
+ ),
+ Identifier(
+ "item",
+ ),
+ Text(
+ " and ",
+ ),
+ ],
+ ),
+ Text(
+ " everyone else",
+ ),
+]
diff --git a/src/snapshots/templates__test__parse_for_loop.snap b/src/snapshots/templates__test__parse_for_loop.snap
index 224fdf5..ac17928 100644
--- a/src/snapshots/templates__test__parse_for_loop.snap
+++ b/src/snapshots/templates__test__parse_for_loop.snap
@@ -1,6 +1,6 @@
---
source: src/main.rs
-assertion_line: 686
+assertion_line: 1110
expression: "Hello,{% for item in list %} to {{ item }} and {% endfor %} everyone else"
---
@@ -10,6 +10,7 @@ expression: "Hello,{% for item in list %} to {{ item }} and {% endfor %} everyon
),
For(
"item",
+ None,
"list",
[
Text(
diff --git a/src/snapshots/templates__test__parse_for_loop_with_single_identifier.snap b/src/snapshots/templates__test__parse_for_loop_with_single_identifier.snap
index d2898ee..5bb8472 100644
--- a/src/snapshots/templates__test__parse_for_loop_with_single_identifier.snap
+++ b/src/snapshots/templates__test__parse_for_loop_with_single_identifier.snap
@@ -1,6 +1,6 @@
---
source: src/main.rs
-assertion_line: 681
+assertion_line: 1105
expression: "Hello {% for item in list %}{{ item }}{% endfor %}"
---
@@ -10,6 +10,7 @@ expression: "Hello {% for item in list %}{{ item }}{% endfor %}"
),
For(
"item",
+ None,
"list",
[
Identifier(
diff --git a/src/snapshots/templates__test__render_for_as_loop.snap b/src/snapshots/templates__test__render_for_as_loop.snap
new file mode 100644
index 0000000..0b362c9
--- /dev/null
+++ b/src/snapshots/templates__test__render_for_as_loop.snap
@@ -0,0 +1,30 @@
+---
+source: src/main.rs
+assertion_line: 1186
+expression: "Hello,{% for item as Item in list %} to {{ item }} and {% endfor %} everyone else"
+
+---
+import gleam/string_builder.{StringBuilder}
+import gleam/list
+
+
+
+pub fn render_builder(list list) -> StringBuilder {
+ let builder = string_builder.from_string("")
+ let builder = string_builder.append(builder, "Hello,")
+ let builder = list.fold(list, builder, fn(builder, item: Item) {
+ let builder = string_builder.append(builder, " to ")
+ let builder = string_builder.append(builder, item)
+ let builder = string_builder.append(builder, " and ")
+
+ builder
+})
+ let builder = string_builder.append(builder, " everyone else")
+
+ builder
+}
+
+pub fn render(list list) -> String {
+ string_builder.to_string(render_builder(list: list))
+}
+
diff --git a/src/snapshots/templates__test__scan_for_as_loop.snap b/src/snapshots/templates__test__scan_for_as_loop.snap
new file mode 100644
index 0000000..4865681
--- /dev/null
+++ b/src/snapshots/templates__test__scan_for_as_loop.snap
@@ -0,0 +1,78 @@
+---
+source: src/main.rs
+assertion_line: 1018
+expression: "Hello {% for item as Item in list %}{{ item }}{% endfor %}"
+
+---
+[
+ (
+ Text(
+ "Hello ",
+ ),
+ 0..6,
+ ),
+ (
+ OpenStmt,
+ 6..7,
+ ),
+ (
+ For,
+ 9..12,
+ ),
+ (
+ Identifier(
+ "item",
+ ),
+ 13..17,
+ ),
+ (
+ As,
+ 18..20,
+ ),
+ (
+ Identifier(
+ "Item",
+ ),
+ 21..25,
+ ),
+ (
+ In,
+ 26..28,
+ ),
+ (
+ Identifier(
+ "list",
+ ),
+ 29..33,
+ ),
+ (
+ CloseStmt,
+ 34..35,
+ ),
+ (
+ OpenValue,
+ 36..37,
+ ),
+ (
+ Identifier(
+ "item",
+ ),
+ 39..43,
+ ),
+ (
+ CloseValue,
+ 44..45,
+ ),
+ (
+ OpenStmt,
+ 46..47,
+ ),
+ (
+ EndFor,
+ 49..55,
+ ),
+ (
+ CloseStmt,
+ 56..57,
+ ),
+]
diff --git a/test/my_user.gleam b/test/my_user.gleam
index 62286ad..201b6b3 100644
--- a/test/my_user.gleam
+++ b/test/my_user.gleam
@@ -1,3 +1,7 @@
pub type User {
User(is_admin: Bool)
}
+
+pub type NamedUser {
+ NamedUser(name: String)
+}
diff --git a/test/template/for_as_loop.gleamx b/test/template/for_as_loop.gleamx
new file mode 100644
index 0000000..352a120
--- /dev/null
+++ b/test/template/for_as_loop.gleamx
@@ -0,0 +1,3 @@
+{> import my_user.{NamedUser}
+{> with users as List(NamedUser)
+Hello,{% for user as NamedUser in users %} to {{ user.name }} and{% endfor %} everyone else
diff --git a/test/templates_test.gleam b/test/templates_test.gleam
index c706349..f6eff39 100644
--- a/test/templates_test.gleam
+++ b/test/templates_test.gleam
@@ -8,13 +8,14 @@ import template/if_statement
import template/if_else_statement
import template/nested_if_statement
import template/for_loop
+import template/for_as_loop
import template/dot_access
import template/multiline
import template/value_in_for_loop
import template/value_in_if_else
import template/quote
-import my_user.{User}
+import my_user.{User, NamedUser}
pub fn main() {
gleeunit.main()
@@ -84,6 +85,11 @@ pub fn for_loop_test() {
|> should.equal("Hello, everyone else\n")
}
+pub fn for_as_loop_test() {
+ for_as_loop.render(users: [NamedUser("Anna"), NamedUser("Bill")])
+ |> should.equal("Hello, to Anna and to Bill and everyone else\n")
+}
+
pub fn value_in_for_loop_test() {
value_in_for_loop.render(greeting: "Hello", my_list: ["Anna", "Bill", "Christine"])
|> should.equal("My List