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} + +``` + ### 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