Skip to content

Commit

Permalink
Merge pull request #13 from michaeljones/insert-string-builder
Browse files Browse the repository at this point in the history
Introduce syntax for inserting string_builder values
  • Loading branch information
michaeljones authored Feb 4, 2022
2 parents fb5937e + b127cf8 commit ddc0eff
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 4 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
# Changelog

## Unreleased

- Added support for '{[ name ]}' syntax to allow for inserting string builder values into the
template using the underlying `append_builder` call instead of the `append` functon that we use
for strings.

Thank you to @michallepicki for the suggestion.

## 0.6.0

- Added support for 'for .. as .. in ..' syntax to allow associating a type with the items in the
list being iterated.

Thank you to @lpil for the suggestion.

## 0.5.0

- Added --version flag to executable to print out current version.
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,29 @@ Alpha. The error messages are poor and there will be plenty of flaws and oversig

The syntax is inspired by [Jinja](https://jinja.palletsprojects.com/).

### Value
### String Value

You can use `{{ name }}` syntax to insert the value of name into the rendered template. The
You can use `{{ name }}` syntax to insert the value of `name` into the rendered template. The
function generated from the template will have a labelled argument matching the identifier used.

```jinja
{{ name }}
```

### String Builder Value

You can use `{[ name ]}` syntax to insert a string builder value into the rendered template. This
has the advantage of using
[string_builder.append_builder](https://hexdocs.pm/gleam_stdlib/gleam/string_builder.html#append_builder)
in the rendered template and so it more efficient for inserting content that is already in a
`string_builder`. This can be used to insert content from another template.

The function generated from the template will have a labelled argument matching the identifier used.

```jinja
{[ name ]}
```

### If

You can use `{% %}` blocks to create an if-statement using the `if`, `else` and `endif` keywords.
Expand Down
76 changes: 74 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ enum Token {
CloseLine,
OpenValue,
CloseValue,
OpenBuilder,
CloseBuilder,
Identifier(String),
Import,
ImportDetails(String),
Expand All @@ -60,6 +62,8 @@ impl std::fmt::Display for Token {
Token::CloseLine => "\n",
Token::OpenValue => "{{",
Token::CloseValue => "}}",
Token::OpenBuilder => "{[",
Token::CloseBuilder => "]}",
Token::OpenStmt => "{%",
Token::CloseStmt => "%}",
Token::Identifier(name) => name,
Expand Down Expand Up @@ -146,6 +150,29 @@ fn scan_plain(iter: &mut Iter, mut tokens: Tokens) -> Result<Tokens, ScanError>
));
iter.next();

tokens = scan_identifiers(iter, tokens)?;
} else if let Some((second_index, "[")) = iter.peek() {
if !buffer.is_empty() {
tokens.push((
Token::Text(buffer),
Range {
start: buffer_start_index.unwrap_or(0),
end: first_index,
},
));
buffer = String::new();
}

tokens.push((
Token::OpenBuilder,
Range {
start: first_index,
end: *second_index,
},
));

iter.next();

tokens = scan_identifiers(iter, tokens)?;
} else if let Some((second_index, ">")) = iter.peek() {
tokens.push((
Expand Down Expand Up @@ -195,6 +222,21 @@ fn scan_plain(iter: &mut Iter, mut tokens: Tokens) -> Result<Tokens, ScanError>
buffer.push('%');
}
}
Some((first_index, "]")) => {
if let Some((second_index, "}")) = iter.peek() {
tokens.push((
Token::CloseBuilder,
Range {
start: first_index,
end: *second_index,
},
));
buffer_start_index = None;
iter.next();
} else {
buffer.push(']');
}
}
Some((index, grapheme)) => {
buffer.push_str(grapheme);
buffer_start_index = buffer_start_index.or(Some(index));
Expand All @@ -220,7 +262,8 @@ fn scan_identifiers(iter: &mut Iter, mut tokens: Tokens) -> Result<Tokens, ScanE
log::trace!("scan_identifiers");
loop {
match iter.peek() {
Some((_index, "}")) | Some((_index, "%")) | Some((_index, "\n")) => {
Some((_index, "}")) | Some((_index, "%")) | Some((_index, "]"))
| Some((_index, "\n")) => {
break;
}
None => break,
Expand All @@ -245,7 +288,7 @@ fn scan_identifier_or_keyword(iter: &mut Iter) -> (Token, Range) {
loop {
match iter.peek() {
Some((_index, "}")) | Some((_index, " ")) | Some((_index, "\n"))
| Some((_index, "%")) => {
| Some((_index, "%")) | Some((_index, "]")) => {
break;
}
Some((index, grapheme)) => {
Expand Down Expand Up @@ -369,6 +412,7 @@ type Type = String;
enum Node {
Text(String),
Identifier(String),
Builder(String),
If(String, Vec<Node>, Vec<Node>),
For(String, Option<Type>, String, Vec<Node>),
Import(String),
Expand Down Expand Up @@ -413,6 +457,11 @@ fn parse_inner(tokens: &mut TokenIter, in_statement: bool) -> Result<Vec<Node>,
ast.push(Node::Identifier(name.clone()));
consume_token(tokens, Token::CloseValue)?;
}
Some((Token::OpenBuilder, _)) => {
let name = extract_identifier(tokens)?;
ast.push(Node::Builder(name.clone()));
consume_token(tokens, Token::CloseBuilder)?;
}
Some((Token::OpenStmt, _)) => {
if let Some((Token::Else, _)) | Some((Token::EndIf, _)) | Some((Token::EndFor, _)) =
tokens.peek()
Expand Down Expand Up @@ -661,6 +710,14 @@ fn render_lines(
));
params.push(name.clone());
}
Some(Node::Builder(name)) => {
iter.next();
builder_lines.push_str(&format!(
" let builder = string_builder.append_builder(builder, {})\n",
name
));
params.push(name.clone());
}
Some(Node::Import(import_details)) => {
iter.next();
imports.push(import_details.clone());
Expand Down Expand Up @@ -1063,6 +1120,11 @@ mod test {
assert_scan!("{> with user as User\n{{ user }}");
}

#[test]
fn test_scan_builder_block() {
assert_scan!("Hello {[ builder ]}, good to meet you");
}

// Parse

#[test]
Expand Down Expand Up @@ -1134,6 +1196,11 @@ mod test {
assert_parse!("{> with user as User\n{{ user }}");
}

#[test]
fn test_parse_builder_block() {
assert_parse!("Hello {[ name ]}, good to meet you");
}

// Render

#[test]
Expand Down Expand Up @@ -1226,6 +1293,11 @@ mod test {
assert_render!(r#"<div class="my-class">{{ name }}</div>"#);
}

#[test]
fn test_render_builder_block() {
assert_render!("Hello {[ name ]}, good to meet you");
}

// Errors

#[test]
Expand Down
17 changes: 17 additions & 0 deletions src/snapshots/templates__test__parse_builder_block.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
source: src/main.rs
assertion_line: 1201
expression: "Hello {[ name ]}, good to meet you"

---
[
Text(
"Hello ",
),
Builder(
"name",
),
Text(
", good to meet you",
),
]
24 changes: 24 additions & 0 deletions src/snapshots/templates__test__render_builder_block.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
source: src/main.rs
assertion_line: 1298
expression: "Hello {[ name ]}, good to meet you"

---
import gleam/string_builder.{StringBuilder}
import gleam/list



pub fn render_builder(name name) -> StringBuilder {
let builder = string_builder.from_string("")
let builder = string_builder.append(builder, "Hello ")
let builder = string_builder.append_builder(builder, name)
let builder = string_builder.append(builder, ", good to meet you")

builder
}

pub fn render(name name) -> String {
string_builder.to_string(render_builder(name: name))
}

34 changes: 34 additions & 0 deletions src/snapshots/templates__test__scan_builder_block.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
source: src/main.rs
assertion_line: 1092
expression: "Hello {[ builder ]}, good to meet you"

---
[
(
Text(
"Hello ",
),
0..6,
),
(
OpenBuilder,
6..7,
),
(
Identifier(
"builder",
),
9..16,
),
(
CloseBuilder,
17..18,
),
(
Text(
", good to meet you",
),
19..36,
),
]
1 change: 1 addition & 0 deletions test/template/builder.gleamx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello {[ name_builder ]}, good to meet you
9 changes: 9 additions & 0 deletions test/templates_test.gleam
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import gleam/string_builder

import gleeunit
import gleeunit/should

Expand All @@ -14,6 +16,7 @@ import template/multiline
import template/value_in_for_loop
import template/value_in_if_else
import template/quote
import template/builder

import my_user.{User, NamedUser}

Expand Down Expand Up @@ -42,6 +45,12 @@ pub fn double_identifier_usage_test() {
|> should.equal("Double usage, Double usage\n")
}

pub fn builder_block_test() {
let name_builder = string_builder.from_strings(["Anna", " ", "Bandana"])
builder.render(name_builder)
|> should.equal("Hello Anna Bandana, good to meet you\n")
}

pub fn if_statement_test() {
if_statement.render(True)
|> should.equal("Hello User\n")
Expand Down

0 comments on commit ddc0eff

Please sign in to comment.