Skip to content

Commit

Permalink
Merge pull request #12 from michaeljones/for-as
Browse files Browse the repository at this point in the history
Support `as` syntax in `for` loops to associate type with item
  • Loading branch information
michaeljones authored Feb 1, 2022
2 parents b86eaf2 + c1b7abc commit 5353fff
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 11 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ after `in`.
</ul>
```

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}
<ul>
{% for user as MyUser in users %}
<li>{{ user.name }}</li>
{% endfor %}
</ul>
```

### Import

You can use the `{>` syntax to add import statements to the template. These are used to import types
Expand Down
65 changes: 57 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,14 +363,16 @@ fn eat_spaces(iter: &mut Iter) {

type TokenIter<'a> = std::iter::Peekable<std::slice::Iter<'a, (Token, Range)>>;

type Type = String;

#[derive(Debug)]
enum Node {
Text(String),
Identifier(String),
If(String, Vec<Node>, Vec<Node>),
For(String, String, Vec<Node>),
For(String, Option<Type>, String, Vec<Node>),
Import(String),
With(String, String),
With(String, Type),
}

#[derive(Debug)]
Expand Down Expand Up @@ -501,7 +503,22 @@ fn parse_if_statement(tokens: &mut TokenIter) -> Result<Node, ParserError> {

fn parse_for_statement(tokens: &mut TokenIter) -> Result<Node, ParserError> {
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)?;
Expand All @@ -511,7 +528,12 @@ fn parse_for_statement(tokens: &mut TokenIter) -> Result<Node, ParserError> {
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<String, ParserError> {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 %}");
Expand Down Expand Up @@ -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 %}");
Expand Down Expand Up @@ -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 %}");
Expand Down
32 changes: 32 additions & 0 deletions src/snapshots/templates__test__parse_for_as_loop.snap
Original file line number Diff line number Diff line change
@@ -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",
),
]
3 changes: 2 additions & 1 deletion src/snapshots/templates__test__parse_for_loop.snap
Original file line number Diff line number Diff line change
@@ -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"

---
Expand All @@ -10,6 +10,7 @@ expression: "Hello,{% for item in list %} to {{ item }} and {% endfor %} everyon
),
For(
"item",
None,
"list",
[
Text(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
source: src/main.rs
assertion_line: 681
assertion_line: 1105
expression: "Hello {% for item in list %}{{ item }}{% endfor %}"

---
Expand All @@ -10,6 +10,7 @@ expression: "Hello {% for item in list %}{{ item }}{% endfor %}"
),
For(
"item",
None,
"list",
[
Identifier(
Expand Down
30 changes: 30 additions & 0 deletions src/snapshots/templates__test__render_for_as_loop.snap
Original file line number Diff line number Diff line change
@@ -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))
}

78 changes: 78 additions & 0 deletions src/snapshots/templates__test__scan_for_as_loop.snap
Original file line number Diff line number Diff line change
@@ -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,
),
]
4 changes: 4 additions & 0 deletions test/my_user.gleam
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
pub type User {
User(is_admin: Bool)
}

pub type NamedUser {
NamedUser(name: String)
}
3 changes: 3 additions & 0 deletions test/template/for_as_loop.gleamx
Original file line number Diff line number Diff line change
@@ -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
8 changes: 7 additions & 1 deletion test/templates_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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("<h1>My List</h1>
Expand Down

0 comments on commit 5353fff

Please sign in to comment.