Skip to content

Commit

Permalink
#170 - Introduce 'capture' template tag
Browse files Browse the repository at this point in the history
  • Loading branch information
ellmetha committed Apr 6, 2024
1 parent 7b00afa commit 5fbf14c
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 1 deletion.
24 changes: 24 additions & 0 deletions docs/docs/templates/reference/tags.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,30 @@ It should be noted that the `cache` template tag also supports specifying additi
{% endcache %}
```

## `capture`

The `capture` template tag allows to define that the output of a block of code should be stored in a new variable.

For example:

```html
{% capture my_var %}
Hello World, {{ name }}!
{% endcapture %}
```

Assuming the variable `name` is assigned the value "John Doe" upon rendering this snippet, the variable `my_var` will hold the string "Hello World, John Doe!".

By default, variables assigned using this template tag will overwrite any existing variables with the same name in the template context. To prevent overwriting existing variables, you can append `unless assigned` after the variable name to ensure that new variables are only assigned if there isn't already one with the same name present.

For example:

```html
{% capture my_var unless defined %}
Hello World, {{ name }}!
{% endcapture %}
```

## `csrf_token`

The `csrf_token` template tag allows to compute and insert the value of the CSRF token into a template. This tag can only be used for templates that are rendered as part of a handler (for example by leveraging [`#render`](../../handlers-and-http/introduction.md#render) or one of the [generic handlers](../../handlers-and-http/generic-handlers.md) involving rendered templates).
Expand Down
135 changes: 135 additions & 0 deletions spec/marten/template/tag/capture_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
require "./spec_helper"

describe Marten::Template::Tag::Capture do
describe "::new" do
it "can initialize a regular capture tag" do
parser = Marten::Template::Parser.new(
<<-TEMPLATE
Hello World, <b>{{ name }}</b>!
{% endcapture %}
TEMPLATE
)

tag = Marten::Template::Tag::Capture.new(parser, "capture my_var")

context = Marten::Template::Context{"name" => "John Doe"}
tag.render(context).should be_empty

context["my_var"].should eq "Hello World, <b>John Doe</b>!\n"
end

it "can initialize a capture tag making use of the 'unless defined' modifier" do
parser = Marten::Template::Parser.new(
<<-TEMPLATE
Hello World, <b>{{ name }}</b>!
{% endcapture %}
TEMPLATE
)

tag = Marten::Template::Tag::Capture.new(parser, "capture my_var unless defined")

context = Marten::Template::Context{"name" => "John Doe"}
tag.render(context).should be_empty

context["my_var"].should eq "Hello World, <b>John Doe</b>!\n"
end

it "raises a syntax error if no variable name is given" do
parser = Marten::Template::Parser.new(
<<-TEMPLATE
Hello World, <b>{{ name }}</b>!
{% endcapture %}
TEMPLATE
)

expect_raises(
Marten::Template::Errors::InvalidSyntax,
"Malformed capture tag: one variable name must be specified."
) do
Marten::Template::Tag::Capture.new(parser, "capture")
end
end

it "raises a syntax error if more than one variable name is given" do
parser = Marten::Template::Parser.new(
<<-TEMPLATE
Hello World, <b>{{ name }}</b>!
{% endcapture %}
TEMPLATE
)

expect_raises(
Marten::Template::Errors::InvalidSyntax,
"Malformed capture tag: unrecognized syntax."
) do
Marten::Template::Tag::Capture.new(parser, "capture my_var my_var2 my_var3 my_var4")
end
end
end

describe "#render" do
it "returns an empty string and assigns the captured content to the specified variable" do
parser = Marten::Template::Parser.new(
<<-TEMPLATE
Hello World, <b>{{ name }}</b>!
{% endcapture %}
TEMPLATE
)

tag = Marten::Template::Tag::Capture.new(parser, "capture my_var")

context = Marten::Template::Context{"name" => "John Doe"}
tag.render(context).should be_empty

context["my_var"].should eq "Hello World, <b>John Doe</b>!\n"
end

it "returns an empty string and assigns the captured content to the specified variable even if it already exists" do
parser = Marten::Template::Parser.new(
<<-TEMPLATE
Hello World, <b>{{ name }}</b>!
{% endcapture %}
TEMPLATE
)

tag = Marten::Template::Tag::Capture.new(parser, "capture my_var")

context = Marten::Template::Context{"name" => "John Doe", "my_var" => "Existing variable"}
tag.render(context).should be_empty

context["my_var"].should eq "Hello World, <b>John Doe</b>!\n"
end

it "does the assignment when the 'unless defined' modifier is used and the variable does not exist" do
parser = Marten::Template::Parser.new(
<<-TEMPLATE
Hello World, <b>{{ name }}</b>!
{% endcapture %}
TEMPLATE
)

tag = Marten::Template::Tag::Capture.new(parser, "capture my_var unless defined")

context = Marten::Template::Context{"name" => "John Doe"}
tag.render(context).should be_empty

context["my_var"].should eq "Hello World, <b>John Doe</b>!\n"
end

it "does not do the assignment when the 'unless defined' modifier is used and the variable already exists" do
parser = Marten::Template::Parser.new(
<<-TEMPLATE
Hello World, <b>{{ name }}</b>!
{% endcapture %}
TEMPLATE
)

tag = Marten::Template::Tag::Capture.new(parser, "capture my_var unless defined")

context = Marten::Template::Context{"name" => "John Doe", "my_var" => "Existing variable"}
tag.render(context).should be_empty

context["my_var"].should eq "Existing variable"
end
end
end
3 changes: 2 additions & 1 deletion spec/marten/template/tag_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ require "./spec_helper"
describe Marten::Template::Tag do
describe "::get" do
it "returns the right built-in tag classes for the expected tag names" do
Marten::Template::Tag.registry.size.should eq 18
Marten::Template::Tag.registry.size.should eq 19
Marten::Template::Tag.get("asset").should eq Marten::Template::Tag::Asset
Marten::Template::Tag.get("assign").should eq Marten::Template::Tag::Assign
Marten::Template::Tag.get("block").should eq Marten::Template::Tag::Block
Marten::Template::Tag.get("cache").should eq Marten::Template::Tag::Cache
Marten::Template::Tag.get("capture").should eq Marten::Template::Tag::Capture
Marten::Template::Tag.get("csrf_token").should eq Marten::Template::Tag::CsrfToken
Marten::Template::Tag.get("extend").should eq Marten::Template::Tag::Extend
Marten::Template::Tag.get("for").should eq Marten::Template::Tag::For
Expand Down
1 change: 1 addition & 0 deletions src/marten/template/tag.cr
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ module Marten
register "assign", Assign
register "block", Block
register "cache", Cache
register "capture", Capture
register "csrf_token", CsrfToken
register "extend", Extend
register "for", For
Expand Down
64 changes: 64 additions & 0 deletions src/marten/template/tag/capture.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
require "./concerns/*"

module Marten
module Template
module Tag
# The `capture` template tag.
#
# The `capture` template tags allows to define that the output of a block of code should be stored in a variable:
#
# ```
# {% capture my_var %}
# Hello World, {{ name }}!
# {% endcapture %}
# ```
#
# It is also possible to use the `unless defined` modifier to only assign the variable if it is not already
# defined in the template context. For example:
#
# ```
# {% capture my_var unless defined %}
# Hello World, {{ name }}!
# {% endcapture %}
# ```
class Capture < Base
include CanSplitSmartly

@assigned_to : String
@capture_nodes : NodeSet
@unless_defined : Bool = false

def initialize(parser : Parser, source : String)
parts = split_smartly(source)

if parts.size < 2
raise Errors::InvalidSyntax.new("Malformed capture tag: one variable name must be specified.")
elsif parts.size == 2
@assigned_to = parts[1]
elsif parts.size == 4 && parts[-2..] == UNLESS_DEFINED_PARTS
@assigned_to = parts[1]
@unless_defined = true
else
raise Errors::InvalidSyntax.new("Malformed capture tag: unrecognized syntax.")
end

# Retrieves the inner nodes, up to the `endcapture` tag.
@capture_nodes = parser.parse(up_to: {"endcapture"})
parser.shift_token
end

def render(context : Context) : String
if !unless_defined? || !context.has_key?(@assigned_to)
context[@assigned_to] = @capture_nodes.render(context)
end

""
end

private UNLESS_DEFINED_PARTS = ["unless", "defined"]

private getter? unless_defined
end
end
end
end

0 comments on commit 5fbf14c

Please sign in to comment.