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

Introduce syntax for inserting string_builder values #13

Merged
merged 3 commits into from
Feb 4, 2022
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
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