Better whitespace handling and control (#145)

* Fine tune whitespace

The EEx outut now emits more human-readable and predictable formatting.
This includes proper indenting, at least for each "root" template.

* Internal whitespace control

You can now use a bang version of any nonvoid tag to emit the markup
witout the internal whitespace. This means that there will not be a
newline emitted after the opening tag and before the closing tag.
This commit is contained in:
Mitchell Hanberg 2021-08-29 17:45:07 -04:00 committed by GitHub
parent 87ddbaa6b5
commit c965048f40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 613 additions and 148 deletions

View File

@ -16,7 +16,6 @@ locals_without_parens = ~w[
details summary menuitem menu
meta link base
area br col embed hr img input keygen param source track wbr
txt partial
animate animateMotion animateTransform circle clipPath
color-profile defs desc discard ellipse feBlend
@ -26,13 +25,7 @@ locals_without_parens = ~w[
marker mask mesh meshgradient meshpatch meshrow metadata mpath path pattern polygon
polyline radialGradient rect set solidcolor stop svg switch symbol text
textPath tspan unknown use view
form_for inputs_for
checkbox color_input checkbox color_input date_input date_select datetime_local_input
datetime_select email_input file_input hidden_input number_input password_input range_input
search_input telephone_input textarea text_input time_input time_select url_input
reset submit phx_label radio_button multiple_select select phx_link phx_button
]a |> Enum.map(fn e -> {e, :*} end)
]a |> Enum.flat_map(fn e -> [{e, :*}, {:"#{e}!", :*}] end)
[
inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"],

View File

@ -29,7 +29,7 @@ defmodule Temple do
# The class attribute also can take a keyword list of classes to conditionally render, based on the boolean result of the value.
div class: ["text-red-500": false, "text-green-500": true ] do
div class: ["text-red-500": false, "text-green-500": true] do
"Alert!"
end
@ -71,6 +71,34 @@ defmodule Temple do
end
```
## Whitespace Control
By default, Temple will emit internal whitespace into tags, something like this.
```elixir
span do
"Hello, world!"
end
```
```html
<span>
Hello, world!
</span>
```
If you need to create a "tight" tag, you can call the "bang" version of the desired tag.
```elixir
span! do
"Hello, world!"
end
```
```html
<span>Hello, world!</span>
```
## Configuration
### Mode
@ -151,7 +179,8 @@ defmodule Temple do
markup =
block
|> Parser.parse()
|> Enum.map(&Temple.Generator.to_eex/1)
|> Enum.map(fn parsed -> Temple.Generator.to_eex(parsed, 0) end)
|> Enum.intersperse("\n")
|> :erlang.iolist_to_binary()
quote location: :keep do
@ -163,7 +192,8 @@ defmodule Temple do
quote location: :keep do
unquote(block)
|> Parser.parse()
|> Enum.map(&Temple.Generator.to_eex/1)
|> Enum.map(fn parsed -> Temple.Generator.to_eex(parsed, 0) end)
|> Enum.intersperse("\n")
|> :erlang.iolist_to_binary()
end
end
@ -190,7 +220,8 @@ defmodule Temple do
markup =
block
|> Parser.parse()
|> Enum.map(&Temple.Generator.to_eex/1)
|> Enum.map(fn parsed -> Temple.Generator.to_eex(parsed, 0) end)
|> Enum.intersperse("\n")
|> :erlang.iolist_to_binary()
EEx.compile_string(markup, engine: engine, line: __CALLER__.line, file: __CALLER__.file)

View File

@ -1,5 +1,5 @@
defprotocol Temple.Generator do
@moduledoc false
def to_eex(ast)
def to_eex(ast, indent \\ 0)
end

View File

@ -58,7 +58,7 @@ defmodule Temple.Parser do
option textarea output progress meter
details summary menuitem menu
html
]a
]a |> Enum.flat_map(fn el -> [el, :"#{el}!"] end)
@nonvoid_elements_aliases Enum.map(@nonvoid_elements, fn el ->
Keyword.get(@aliases, el, el)

View File

@ -32,14 +32,14 @@ defmodule Temple.Parser.AnonymousFunctions do
end
defimpl Temple.Generator do
def to_eex(%{elixir_ast: {name, _, args}, children: children}) do
def to_eex(%{elixir_ast: {name, _, args}, children: children}, indent \\ 0) do
{_do_and_else, args} = Temple.Parser.Utils.split_args(args)
{args, {func, _, [{arrow, _, [[{arg, _, _}], _block]}]}, args2} =
Temple.Parser.Utils.split_on_fn(args, {[], nil, []})
[
"<%= ",
"#{Parser.Utils.indent(indent)}<%= ",
to_string(name),
" ",
Enum.map(args, &Macro.to_string(&1)) |> Enum.join(", "),
@ -51,16 +51,16 @@ defmodule Temple.Parser.AnonymousFunctions do
to_string(arrow),
" %>",
"\n",
for(child <- children, do: Temple.Generator.to_eex(child)),
for(child <- children, do: Temple.Generator.to_eex(child, indent + 1)),
if Enum.any?(args2) do
[
"<% end, ",
"#{Parser.Utils.indent(indent)}<% end, ",
Enum.map(args2, fn arg -> Macro.to_string(arg) end)
|> Enum.join(", "),
" %>"
]
else
["<% end %>", "\n"]
["#{Parser.Utils.indent(indent)}<% end %>", "\n"]
end
]
end

View File

@ -2,6 +2,8 @@ defmodule Temple.Parser.Components do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Parser
defstruct module: nil, assigns: [], children: [], slots: []
@impl Temple.Parser
@ -88,12 +90,12 @@ defmodule Temple.Parser.Components do
end
defimpl Temple.Generator do
def to_eex(%{module: module, assigns: assigns, children: children, slots: slots}) do
def to_eex(%{module: module, assigns: assigns, children: children, slots: slots}, indent \\ 0) do
component_function = Temple.Config.mode().component_function
renderer = Temple.Config.mode().renderer.(module)
[
"<%= #{component_function} ",
"#{Parser.Utils.indent(indent)}<%= #{component_function} ",
renderer,
", ",
Macro.to_string(assigns),
@ -102,23 +104,22 @@ defmodule Temple.Parser.Components do
" do %>\n",
if not Enum.empty?(children) do
[
"<% {:default, _} -> %>\n",
for(child <- children, do: Temple.Generator.to_eex(child)),
"\n"
"#{Parser.Utils.indent(indent + 1)}<% {:default, _} -> %>\n",
for(child <- children, do: Temple.Generator.to_eex(child, indent + 2))
]
else
""
end,
for slot <- slots do
[
"<% {:",
"#{Parser.Utils.indent(indent + 1)}<% {:",
to_string(slot.name),
", ",
"#{Macro.to_string(slot.assigns)}} -> %>\n",
for(child <- slot.content, do: Temple.Generator.to_eex(child))
for(child <- slot.content, do: Temple.Generator.to_eex(child, indent + 2))
]
end,
"<% end %>"
"\n#{Parser.Utils.indent(indent)}<% end %>"
]
else
" %>"

View File

@ -15,8 +15,8 @@ defmodule Temple.Parser.Default do
end
defimpl Temple.Generator do
def to_eex(%{elixir_ast: expression}) do
["<%= ", Macro.to_string(expression), " %>\n"]
def to_eex(%{elixir_ast: expression}, indent \\ 0) do
["#{Parser.Utils.indent(indent)}<%= ", Macro.to_string(expression), " %>"]
end
end
end

View File

@ -30,18 +30,23 @@ defmodule Temple.Parser.DoExpressions do
end
defimpl Temple.Generator do
def to_eex(%{elixir_ast: expression, children: [do_body, else_body]}) do
def to_eex(%{elixir_ast: expression, children: [do_body, else_body]}, indent \\ 0) do
[
"<%= ",
"#{Parser.Utils.indent(indent)}<%= ",
Macro.to_string(expression),
" do %>",
"\n",
for(child <- do_body, do: Temple.Generator.to_eex(child)),
for(child <- do_body, do: Temple.Generator.to_eex(child, indent + 1))
|> Enum.intersperse("\n"),
if(else_body != nil,
do: ["<% else %>\n", for(child <- else_body, do: Temple.Generator.to_eex(child))],
do: [
"#{Parser.Utils.indent(indent)}\n<% else %>\n",
for(child <- else_body, do: Temple.Generator.to_eex(child, indent + 1))
|> Enum.intersperse("\n")
],
else: ""
),
"<% end %>"
"\n#{Parser.Utils.indent(indent)}<% end %>"
]
end
end

View File

@ -0,0 +1,33 @@
defmodule Temple.Parser.ElementList do
@moduledoc false
@behaviour Temple.Parser
defstruct children: [], whitespace: :loose
@impl Temple.Parser
def applicable?(asts), do: is_list(asts)
@impl Temple.Parser
def run(asts) do
children = Enum.flat_map(asts, &Temple.Parser.parse/1)
Temple.Ast.new(__MODULE__, children: children)
end
defimpl Temple.Generator do
def to_eex(%{children: children, whitespace: whitespace}, indent \\ 0) do
child_indent = if whitespace == :loose, do: indent + 1, else: 0
self_indent = if whitespace == :loose, do: indent, else: 0
whitespace = if whitespace == :tight, do: [], else: ["\n"]
[
whitespace,
for(child <- children, do: Temple.Generator.to_eex(child, child_indent))
|> Enum.intersperse("\n"),
whitespace,
Temple.Parser.Utils.indent(self_indent)
]
end
end
end

View File

@ -16,7 +16,7 @@ defmodule Temple.Parser.Empty do
end
defimpl Temple.Generator do
def to_eex(_) do
def to_eex(_, _ \\ 0) do
[]
end
end

View File

@ -19,8 +19,8 @@ defmodule Temple.Parser.Match do
end
defimpl Temple.Generator do
def to_eex(%{elixir_ast: elixir_ast}) do
["<% ", Macro.to_string(elixir_ast), " %>"]
def to_eex(%{elixir_ast: elixir_ast}, indent \\ 0) do
["#{Parser.Utils.indent(indent)}<% ", Macro.to_string(elixir_ast), " %>"]
end
end
end

View File

@ -2,7 +2,7 @@ defmodule Temple.Parser.NonvoidElementsAliases do
@moduledoc false
@behaviour Temple.Parser
defstruct name: nil, attrs: [], children: []
defstruct name: nil, attrs: [], children: [], meta: %{}
alias Temple.Parser
@ -23,18 +23,34 @@ defmodule Temple.Parser.NonvoidElementsAliases do
children = Temple.Parser.parse(do_and_else[:do])
Temple.Ast.new(__MODULE__, name: to_string(name), attrs: args, children: children)
Temple.Ast.new(__MODULE__,
name: to_string(name) |> String.replace_suffix("!", ""),
attrs: args,
children:
Temple.Ast.new(Temple.Parser.ElementList,
children: children,
whitespace: whitespace(to_string(name))
)
)
end
defp whitespace(name) do
if String.ends_with?(name, "!") do
:tight
else
:loose
end
end
defimpl Temple.Generator do
def to_eex(%{name: name, attrs: attrs, children: children}) do
def to_eex(%{name: name, attrs: attrs, children: children}, indent \\ 0) do
[
"<",
"#{Parser.Utils.indent(indent)}<",
name,
Temple.Parser.Utils.compile_attrs(attrs),
">\n",
for(child <- children, do: Temple.Generator.to_eex(child)),
"\n</",
">",
Temple.Generator.to_eex(children, indent),
"</",
name,
">"
]

View File

@ -18,12 +18,12 @@ defmodule Temple.Parser.RightArrow do
end
defimpl Temple.Generator do
def to_eex(%{elixir_ast: elixir_ast, children: children}) do
def to_eex(%{elixir_ast: elixir_ast, children: children}, indent \\ 0) do
[
"<% ",
"#{Parser.Utils.indent(indent)}<% ",
Macro.to_string(elixir_ast),
" -> %>\n",
for(child <- children, do: Temple.Generator.to_eex(child))
for(child <- children, do: Temple.Generator.to_eex(child, indent + 1))
]
end
end

View File

@ -1,6 +1,7 @@
defmodule Temple.Parser.Slot do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Parser.Utils
defstruct name: nil, args: []
@ -26,15 +27,15 @@ defmodule Temple.Parser.Slot do
end
defimpl Temple.Generator do
def to_eex(%{name: name, args: args}) do
def to_eex(%{name: name, args: args}, indent \\ 0) do
render_block_function = Temple.Config.mode().render_block_function
[
"<%= #{render_block_function}(@inner_block, {:",
"#{Utils.indent(indent)}<%= #{render_block_function}(@inner_block, {:",
to_string(name),
", ",
Macro.to_string(quote(do: Enum.into(unquote(args), %{}))),
"}) %>"
"}) %>\n"
]
end
end

View File

@ -16,8 +16,8 @@ defmodule Temple.Parser.Text do
end
defimpl Temple.Generator do
def to_eex(%{text: text}) do
[text, "\n"]
def to_eex(%{text: text}, indent \\ 0) do
[Parser.Utils.indent(indent), text]
end
end
end

View File

@ -113,4 +113,8 @@ defmodule Temple.Parser.Utils do
def pop_compact?(args) do
Keyword.pop(args, :compact, false)
end
def indent(level) do
String.duplicate(" ", level * 2)
end
end

View File

@ -2,6 +2,8 @@ defmodule Temple.Parser.VoidElementsAliases do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Parser.Utils
defstruct name: nil, attrs: []
@impl Temple.Parser
@ -28,12 +30,12 @@ defmodule Temple.Parser.VoidElementsAliases do
end
defimpl Temple.Generator do
def to_eex(%{name: name, attrs: attrs}) do
def to_eex(%{name: name, attrs: attrs}, indent \\ 0) do
[
"<",
"#{Utils.indent(indent)}<",
to_string(name),
Temple.Parser.Utils.compile_attrs(attrs),
">\n"
">"
]
end
end

View File

@ -18,7 +18,19 @@ defmodule Temple.ComponentTest do
end
assert evaluate_template(result) ==
~s{<div class="font-bold">Hello, world</div><div><aside class="foobar">I'm a component!</aside></div>}
~s"""
<div class="font-bold">
Hello, world
</div>
<div>
<aside class="foobar">
I'm a component!
</aside>
</div>
"""
end
test "function components can accept local assigns" do
@ -34,7 +46,16 @@ defmodule Temple.ComponentTest do
end
assert evaluate_template(result) ==
~s{<div class="font-bold">Hello, world</div><div class="bg-red">I'm a component!</div>}
~s"""
<div class="font-bold">
Hello, world
</div>
<div class="bg-red">
I'm a component!
</div>
"""
end
test "function components can use other components" do
@ -50,8 +71,21 @@ defmodule Temple.ComponentTest do
end
assert evaluate_template(result) == ~s"""
<div id="inner" outer-id="from-outer">outer!</div>
<div id="inner" outer-id="set by root inner">inner!</div>
<div id="inner" outer-id="from-outer">
outer!
</div>
<div id="inner" outer-id="set by root inner">
inner!
</div>
"""
end
@ -63,7 +97,14 @@ defmodule Temple.ComponentTest do
end
end
assert evaluate_template(result) == ~s{<div class="barbarbar">doo doo</div>}
assert evaluate_template(result) == ~s"""
<div class="barbarbar">
doo doo
</div>
"""
end
test "components can be void elements" do
@ -72,7 +113,11 @@ defmodule Temple.ComponentTest do
c Temple.Components.VoidComponent, foo: :bar
end
assert evaluate_template(result) == ~s{<div class="void!!">bar</div>}
assert evaluate_template(result) == ~s"""
<div class="void!!">
bar
</div>
"""
end
test "components can have named slots" do
@ -94,6 +139,22 @@ defmodule Temple.ComponentTest do
end
assert evaluate_template(result, assigns) ==
~s{<div><div>the value is Header</div><div class="wrapped"><button class="btn" phx-click="toggle">bob</button></div></div>}
~s"""
<div>
<div>
the value is Header
</div>
<div class="wrapped">
<button class="btn" phx-click="toggle">
bob
</button>
</div>
</div>
"""
end
end

View File

@ -7,9 +7,9 @@ defmodule Temple.Parser.AnonymousFunctionsTest do
test "returns true when the node contains an anonymous function as an argument to a function" do
raw_asts = [
quote do
form_for changeset, Routes.foo_path(conn, :create), fn form ->
form_for(changeset, Routes.foo_path(conn, :create), fn form ->
Does.something!(form)
end
end)
end
]
@ -47,9 +47,9 @@ defmodule Temple.Parser.AnonymousFunctionsTest do
raw_ast =
quote do
form_for changeset, Routes.foo_path(conn, :create), fn form ->
form_for(changeset, Routes.foo_path(conn, :create), fn form ->
unquote(expected_child)
end
end)
end
ast = AnonymousFunctions.run(raw_ast)
@ -69,9 +69,9 @@ defmodule Temple.Parser.AnonymousFunctionsTest do
test "emits eex" do
raw_ast =
quote do
form_for changeset, Routes.foo_path(conn, :create), fn form ->
form_for(changeset, Routes.foo_path(conn, :create), fn form ->
Does.something!(form)
end
end)
end
result =

View File

@ -187,7 +187,12 @@ defmodule Temple.Parser.ComponentsTest do
|> Temple.Generator.to_eex()
assert result |> :erlang.iolist_to_binary() ==
~s|<%= Temple.Component.__component__ SomeModule, [foo: :bar] do %>\n<% {:default, _} -> %>\nI'm a component!\n<% end %>|
~s"""
<%= Temple.Component.__component__ SomeModule, [foo: :bar] do %>
<% {:default, _} -> %>
I'm a component!
<% end %>
"""
end
test "emits eex for void component with slots" do
@ -208,7 +213,14 @@ defmodule Temple.Parser.ComponentsTest do
|> Temple.Generator.to_eex()
assert result |> :erlang.iolist_to_binary() ==
~s|<%= Temple.Component.__component__ SomeModule, [foo: :bar] do %>\n<% {:foo, %{form: form}} -> %>\n<div>\nin the slot\n\n</div>\n<% end %>|
~s"""
<%= Temple.Component.__component__ SomeModule, [foo: :bar] do %>
<% {:foo, %{form: form}} -> %>
<div>
in the slot
</div>
<% end %>
"""
end
test "emits eex for nonvoid component with slots" do
@ -233,7 +245,18 @@ defmodule Temple.Parser.ComponentsTest do
|> Temple.Generator.to_eex()
assert result |> :erlang.iolist_to_binary() ==
~s|<%= Temple.Component.__component__ SomeModule, [foo: :bar] do %>\n<% {:default, _} -> %>\n<div>\ninner content\n\n</div>\n<% {:foo, %{form: form}} -> %><div>\nin the slot\n\n</div><% end %>|
~s"""
<%= Temple.Component.__component__ SomeModule, [foo: :bar] do %>
<% {:default, _} -> %>
<div>
inner content
</div>
<% {:foo, %{form: form}} -> %>
<div>
in the slot
</div>
<% end %>
"""
end
test "emits eex for void component" do

View File

@ -2,6 +2,7 @@ defmodule Temple.Parser.DefaultTest do
use ExUnit.Case, async: true
alias Temple.Parser.Default
alias Temple.Support.Utils
describe "applicable?/1" do
test "returns true when the node is an elixir expression" do
@ -35,8 +36,11 @@ defmodule Temple.Parser.DefaultTest do
end
|> Default.run()
|> Temple.Generator.to_eex()
|> Utils.iolist_to_binary()
assert result |> :erlang.iolist_to_binary() == ~s|<%= Foo.bar!(baz) %>\n|
assert result == ~s"""
<%= Foo.bar!(baz) %>
"""
end
end
end

View File

@ -2,6 +2,7 @@ defmodule Temple.Parser.DoExpressionsTest do
use ExUnit.Case, async: true
alias Temple.Parser.DoExpressions
alias Temple.Support.Utils
describe "applicable?/1" do
test "returns true when the node contains a do expression" do
@ -47,9 +48,14 @@ defmodule Temple.Parser.DoExpressionsTest do
end
|> DoExpressions.run()
|> Temple.Generator.to_eex()
|> Utils.iolist_to_binary()
assert result |> :erlang.iolist_to_binary() ==
~s|<%= for(big <- boys) do %>\nbob\n<% end %>|
assert result ==
~s"""
<%= for(big <- boys) do %>
bob
<% end %>
"""
end
test "emits eex for that includes in else clause" do
@ -65,9 +71,17 @@ defmodule Temple.Parser.DoExpressionsTest do
end
|> DoExpressions.run()
|> Temple.Generator.to_eex()
|> Utils.iolist_to_binary()
assert result |> :erlang.iolist_to_binary() ==
~s|<%= if(foo?) do %>\nbob\nbobby\n<% else %>\ncarol\n<% end %>|
assert result ==
~s"""
<%= if(foo?) do %>
bob
bobby
<% else %>
carol
<% end %>
"""
end
test "emits eex for a case expression" do
@ -80,9 +94,15 @@ defmodule Temple.Parser.DoExpressionsTest do
end
|> DoExpressions.run()
|> Temple.Generator.to_eex()
|> Utils.iolist_to_binary()
assert result |> :erlang.iolist_to_binary() ==
~s|<%= case(foo?) do %>\n<% :bing -> %>\n<%= :bong %>\n<% end %>|
assert result ==
~s"""
<%= case(foo?) do %>
<% :bing -> %>
<%= :bong %>
<% end %>
"""
end
end
end

View File

@ -2,6 +2,8 @@ defmodule Temple.Parser.NonvoidElementsAliasesTest do
use ExUnit.Case, async: true
alias Temple.Parser.NonvoidElementsAliases
alias Temple.Parser.ElementList
alias Temple.Support.Utils
describe "applicable?/1" do
test "returns true when the node is a nonvoid element or alias" do
@ -63,19 +65,25 @@ defmodule Temple.Parser.NonvoidElementsAliasesTest do
assert %NonvoidElementsAliases{
name: "div",
attrs: [class: "foo", id: {:var, [], _}],
children: [
%NonvoidElementsAliases{
name: "select",
children: [
%NonvoidElementsAliases{
name: "option",
children: %ElementList{
children: [
%NonvoidElementsAliases{
name: "select",
children: %ElementList{
children: [
%Temple.Parser.Text{text: "foo"}
%NonvoidElementsAliases{
name: "option",
children: %ElementList{
children: [
%Temple.Parser.Text{text: "foo"}
]
}
}
]
}
]
}
]
}
]
}
} = ast
end
end
@ -94,9 +102,44 @@ defmodule Temple.Parser.NonvoidElementsAliasesTest do
end
|> NonvoidElementsAliases.run()
|> Temple.Generator.to_eex()
|> Utils.iolist_to_binary()
assert result |> :erlang.iolist_to_binary() ==
~s|<div class="foo"<%= {:safe, Temple.Parser.Utils.build_attr("id", var)} %>>\n<select>\n<option>\nfoo\n\n</option>\n</select>\n</div>|
assert result ==
~s"""
<div class="foo"<%= {:safe, Temple.Parser.Utils.build_attr("id", var)} %>>
<select>
<option>
foo
</option>
</select>
</div>
"""
end
test "produce 'tight' markup" do
result =
quote do
div class: "foo", id: var do
select__ do
option! do
"foo"
end
end
end
end
|> NonvoidElementsAliases.run()
|> Temple.Generator.to_eex()
|> :erlang.iolist_to_binary()
|> Kernel.<>("\n")
assert result ==
~s"""
<div class="foo"<%= {:safe, Temple.Parser.Utils.build_attr("id", var)} %>>
<select>
<option>foo</option>
</select>
</div>
"""
end
end
end

View File

@ -2,6 +2,7 @@ defmodule Temple.Parser.RightArrowTest do
use ExUnit.Case, async: true
alias Temple.Parser.RightArrow
alias Temple.Support.Utils
describe "applicable?/1" do
test "returns true when the node contains a right arrow" do
@ -70,9 +71,13 @@ defmodule Temple.Parser.RightArrowTest do
|> List.first()
|> RightArrow.run()
|> Temple.Generator.to_eex()
|> Utils.iolist_to_binary()
assert result |> :erlang.iolist_to_binary() ==
~s|<% :bing -> %>\n<%= :bong %>\n|
assert result ==
~s"""
<% :bing -> %>
<%= :bong %>
"""
end
end
end

View File

@ -42,7 +42,9 @@ defmodule Temple.Parser.SlotTest do
|> Temple.Generator.to_eex()
assert result |> :erlang.iolist_to_binary() ==
~s|<%= Temple.Component.__render_block__(@inner_block, {:header, Enum.into([value: Form.form_for(changeset, action)], %{})}) %>|
~s"""
<%= Temple.Component.__render_block__(@inner_block, {:header, Enum.into([value: Form.form_for(changeset, action)], %{})}) %>
"""
end
end
end

View File

@ -3,6 +3,7 @@ defmodule Temple.Parser.TempleNamespaceNonvoidTest do
alias Temple.Parser.NonvoidElementsAliases
alias Temple.Parser.TempleNamespaceNonvoid
alias Temple.Support.Utils
describe "applicable?/1" do
test "returns true when the node is a Temple aliased nonvoid element" do
@ -50,7 +51,10 @@ defmodule Temple.Parser.TempleNamespaceNonvoidTest do
assert %NonvoidElementsAliases{
name: "div",
attrs: [class: "foo", id: {:var, [], _}],
children: [%Temple.Parser.Text{text: "foo"}]
children: %Temple.Parser.ElementList{
children: [%Temple.Parser.Text{text: "foo"}],
whitespace: :loose
}
} = ast
end
end
@ -65,9 +69,14 @@ defmodule Temple.Parser.TempleNamespaceNonvoidTest do
end
|> TempleNamespaceNonvoid.run()
|> Temple.Generator.to_eex()
|> Utils.iolist_to_binary()
assert result |> :erlang.iolist_to_binary() ==
~s|<div class="foo"<%= {:safe, Temple.Parser.Utils.build_attr("id", var)} %>>\nfoo\n\n</div>|
assert result ==
~s"""
<div class="foo"<%= {:safe, Temple.Parser.Utils.build_attr("id", var)} %>>
foo
</div>
"""
end
end
end

View File

@ -3,6 +3,7 @@ defmodule Temple.Parser.TempleNamespaceVoidTest do
alias Temple.Parser.TempleNamespaceVoid
alias Temple.Parser.VoidElementsAliases
alias Temple.Support.Utils
describe "applicable?/1" do
test "returns true when the node is a Temple aliased nonvoid element" do
@ -58,8 +59,9 @@ defmodule Temple.Parser.TempleNamespaceVoidTest do
end
|> TempleNamespaceVoid.run()
|> Temple.Generator.to_eex()
|> Utils.iolist_to_binary()
assert result |> :erlang.iolist_to_binary() == ~s|<meta content="foo">\n|
assert result == ~s|<meta content="foo">\n|
end
end
end

View File

@ -2,6 +2,7 @@ defmodule Temple.Parser.TextTest do
use ExUnit.Case, async: true
alias Temple.Parser.Text
alias Temple.Support.Utils
describe "applicable?/1" do
test "returns true when the node is a string literal" do
@ -35,8 +36,9 @@ defmodule Temple.Parser.TextTest do
"string literal"
|> Text.run()
|> Temple.Generator.to_eex()
|> Utils.iolist_to_binary()
assert result |> :erlang.iolist_to_binary() == ~s|string literal\n|
assert result == ~s|string literal\n|
end
end
end

View File

@ -2,6 +2,7 @@ defmodule Temple.Parser.VoidElementsAliasesTest do
use ExUnit.Case, async: true
alias Temple.Parser.VoidElementsAliases
alias Temple.Support.Utils
describe "applicable?/1" do
test "returns true when the node is a nonvoid element or alias" do
@ -63,8 +64,9 @@ defmodule Temple.Parser.VoidElementsAliasesTest do
end
|> VoidElementsAliases.run()
|> Temple.Generator.to_eex()
|> Utils.iolist_to_binary()
assert result |> :erlang.iolist_to_binary() == ~s|<meta content="foo">\n|
assert result == ~s|<meta content="foo">\n|
end
end
end

View File

@ -33,4 +33,20 @@ defmodule Temple.Support.Utils do
|> elem(0)
|> Phoenix.HTML.safe_to_string()
end
@doc """
Converts an iolist to a binary and appends a new line.
"""
def iolist_to_binary(iolist) do
iolist
|> :erlang.iolist_to_binary()
|> append_new_line()
end
@doc """
Appends a new line to a string.
"""
def append_new_line(string) do
string <> "\n"
end
end

View File

@ -11,7 +11,12 @@ defmodule TempleTest do
end
end
assert result == ~s{<div class="hello"><div class="hi"></div></div>}
assert result == ~s"""
<div class="hello">
<div class="hi">
</div>
</div>
"""
end
test "renders void element" do
@ -32,7 +37,12 @@ defmodule TempleTest do
end
end
assert result == ~s{<div class="hello">hifoo</div>}
assert result == ~s"""
<div class="hello">
hi
foo
</div>
"""
end
test "renders a variable text node as eex" do
@ -43,7 +53,11 @@ defmodule TempleTest do
end
end
assert result == ~s{<div class="hello"><%= foo %></div>}
assert result == ~s"""
<div class="hello">
<%= foo %>
</div>
"""
end
test "renders an assign text node as eex" do
@ -54,7 +68,11 @@ defmodule TempleTest do
end
end
assert result == ~s{<div class="hello"><%= @foo %></div>}
assert result == ~s"""
<div class="hello">
<%= @foo %>
</div>
"""
end
test "renders a match expression" do
@ -67,7 +85,12 @@ defmodule TempleTest do
end
end
assert result == ~s{<% x = 420 %><div>blaze it</div>}
assert result == ~s"""
<% x = 420 %>
<div>
blaze it
</div>
"""
end
test "renders a non-match expression" do
@ -80,7 +103,12 @@ defmodule TempleTest do
end
end
assert result == ~s{<%= IO.inspect(:foo) %><div>bar</div>}
assert result == ~s"""
<%= IO.inspect(:foo) %>
<div>
bar
</div>
"""
end
test "renders an expression in attr as eex" do
@ -102,7 +130,12 @@ defmodule TempleTest do
end
assert result ==
~s|<div<%= {:safe, Temple.Parser.Utils.build_attr("class", Enum.map([:one, :two], fn x -> x end))} %>><div class="hi"></div></div>|
~s"""
<div<%= {:safe, Temple.Parser.Utils.build_attr("class", Enum.map([:one, :two], fn x -> x end))} %>>
<div class="hi">
</div>
</div>
"""
end
test "renders a for comprehension as eex" do
@ -113,7 +146,12 @@ defmodule TempleTest do
end
end
assert result == ~s{<%= for(x <- 1..5) do %><div class="hi"></div><% end %>}
assert result == ~s"""
<%= for(x <- 1..5) do %>
<div class="hi">
</div>
<% end %>
"""
end
test "renders an if expression as eex" do
@ -124,7 +162,12 @@ defmodule TempleTest do
end
end
assert result == ~s{<%= if(true == false) do %><div class="hi"></div><% end %>}
assert result == ~s"""
<%= if(true == false) do %>
<div class="hi">
</div>
<% end %>
"""
end
test "renders an if/else expression as eex" do
@ -138,7 +181,15 @@ defmodule TempleTest do
end
assert result ==
~s{<%= if(true == false) do %><div class="hi"></div><% else %><div class="haha"></div><% end %>}
~s"""
<%= if(true == false) do %>
<div class="hi">
</div>
<% else %>
<div class="haha">
</div>
<% end %>
"""
end
test "renders an unless expression as eex" do
@ -149,7 +200,12 @@ defmodule TempleTest do
end
end
assert result == ~s{<%= unless(true == false) do %><div class="hi"></div><% end %>}
assert result == ~s"""
<%= unless(true == false) do %>
<div class="hi">
</div>
<% end %>
"""
end
test "renders a case expression as eex" do
@ -161,14 +217,12 @@ defmodule TempleTest do
end
end
expected =
~S"""
<%= case(@foo) do %>
expected = ~S"""
<%= case(@foo) do %>
<% :baz -> %>
<%= some_component(form: @form) %>
<% end %>
"""
|> String.trim()
<%= some_component(form: @form) %>
<% end %>
"""
assert result == expected
end
@ -176,70 +230,99 @@ defmodule TempleTest do
test "renders multiline anonymous function with 1 arg before the function" do
result =
temple do
form_for Routes.user_path(@conn, :create), fn f ->
form_for(Routes.user_path(@conn, :create), fn f ->
"Name: "
text_input f, :name
end
text_input(f, :name)
end)
end
assert result ==
~s{<%= form_for Routes.user_path(@conn, :create), fn f -> %>Name: <%= text_input(f, :name) %><% end %>}
~s"""
<%= form_for Routes.user_path(@conn, :create), fn f -> %>
Name:
<%= text_input(f, :name) %>
<% end %>
"""
end
test "renders multiline anonymous functions with 2 args before the function" do
result =
temple do
form_for @changeset, Routes.user_path(@conn, :create), fn f ->
form_for(@changeset, Routes.user_path(@conn, :create), fn f ->
"Name: "
text_input f, :name
end
text_input(f, :name)
end)
end
assert result ==
~s{<%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %>Name: <%= text_input(f, :name) %><% end %>}
~s"""
<%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %>
Name:
<%= text_input(f, :name) %>
<% end %>
"""
end
test "renders multiline anonymous functions with complex nested children" do
result =
temple do
form_for @changeset, Routes.user_path(@conn, :create), fn f ->
form_for(@changeset, Routes.user_path(@conn, :create), fn f ->
div do
"Name: "
text_input f, :name
text_input(f, :name)
end
end
end)
end
assert result ==
~s{<%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %><div>Name: <%= text_input(f, :name) %></div><% end %>}
~s"""
<%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %>
<div>
Name:
<%= text_input(f, :name) %>
</div>
<% end %>
"""
end
test "renders multiline anonymous function with 3 arg before the function" do
result =
temple do
form_for @changeset, Routes.user_path(@conn, :create), [foo: :bar], fn f ->
form_for(@changeset, Routes.user_path(@conn, :create), [foo: :bar], fn f ->
"Name: "
text_input f, :name
end
text_input(f, :name)
end)
end
assert result ==
~s{<%= form_for @changeset, Routes.user_path(@conn, :create), [foo: :bar], fn f -> %>Name: <%= text_input(f, :name) %><% end %>}
~s"""
<%= form_for @changeset, Routes.user_path(@conn, :create), [foo: :bar], fn f -> %>
Name:
<%= text_input(f, :name) %>
<% end %>
"""
end
test "renders multiline anonymous function with 1 arg before the function and 1 arg after" do
result =
temple do
form_for @changeset,
fn f ->
"Name: "
text_input f, :name
end,
foo: :bar
form_for(
@changeset,
fn f ->
"Name: "
text_input(f, :name)
end,
foo: :bar
)
end
assert result ==
~s{<%= form_for @changeset, fn f -> %>Name: <%= text_input(f, :name) %><% end, [foo: :bar] %>}
~s"""
<%= form_for @changeset, fn f -> %>
Name:
<%= text_input(f, :name) %>
<% end, [foo: :bar] %>
"""
end
test "tags prefixed with Temple. should be interpreted as temple tags" do
@ -252,7 +335,13 @@ defmodule TempleTest do
end
end
assert result == ~s{<div><span>bob</span></div>}
assert result == ~s"""
<div>
<span>
bob
</span>
</div>
"""
end
test "can pass do as an arg instead of a block" do
@ -267,7 +356,17 @@ defmodule TempleTest do
end
assert result ==
~s{<div class="font-bold">Hello, world</div><div class="font-bold">Hello, world</div><div>Hello, world</div>}
~s"""
<div class="font-bold">
Hello, world
</div>
<div class="font-bold">
Hello, world
</div>
<div>
Hello, world
</div>
"""
end
test "for with 2 generators" do
@ -280,7 +379,16 @@ defmodule TempleTest do
end
assert result ==
~s{<%= for(x <- 1..5, y <- 6..10) do %><div><%= x %></div><div><%= y %></div><% end %>}
~s"""
<%= for(x <- 1..5, y <- 6..10) do %>
<div>
<%= x %>
</div>
<div>
<%= y %>
</div>
<% end %>
"""
end
test "can pass an expression as assigns" do
@ -292,7 +400,15 @@ defmodule TempleTest do
end
assert result ==
~s{<fieldset<%= Temple.Parser.Utils.runtime_attrs(if(true == false) do [disabled: true]else []end) %>><input type="text"></fieldset>}
~s"""
<fieldset<%= Temple.Parser.Utils.runtime_attrs(if(true == false) do
[disabled: true]
else
[]
end) %>>
<input type="text">
</fieldset>
"""
end
test "can pass a variable as assigns" do
@ -304,7 +420,11 @@ defmodule TempleTest do
end
assert result ==
~s{<fieldset<%= Temple.Parser.Utils.runtime_attrs(foo_bar) %>><input type="text"></fieldset>}
~s"""
<fieldset<%= Temple.Parser.Utils.runtime_attrs(foo_bar) %>>
<input type="text">
</fieldset>
"""
end
test "can pass a function as assigns" do
@ -316,7 +436,11 @@ defmodule TempleTest do
end
assert result ==
~s{<fieldset<%= Temple.Parser.Utils.runtime_attrs(Foo.foo_bar()) %>><input type="text"></fieldset>}
~s"""
<fieldset<%= Temple.Parser.Utils.runtime_attrs(Foo.foo_bar()) %>>
<input type="text">
</fieldset>
"""
end
test "hr tag works" do
@ -334,7 +458,23 @@ defmodule TempleTest do
end
assert evaluate_template(result, assigns) ==
~s{<div>foo</div><hr><div>foo</div><hr class="foofoo"><div>bar</div><hr class="foofoo"><div>bar</div>}
~s"""
<div>
foo
</div>
<hr>
<div>
foo
</div>
<hr class="foofoo">
<div>
bar
</div>
<hr class="foofoo">
<div>
bar
</div>
"""
end
test "boolean attributes" do
@ -363,6 +503,10 @@ defmodule TempleTest do
end
end
assert evaluate_template(result, assigns) == ~s{<div class="text-red">\nfoo\n\n</div>}
assert evaluate_template(result, assigns) == ~s"""
<div class="text-red">
foo
</div>
"""
end
end

46
test/whitespace_test.exs Normal file
View File

@ -0,0 +1,46 @@
defmodule Temple.WhitespaceTest do
use ExUnit.Case, async: true
import Temple
alias Temple.Support.Utils
test "only emits a single new line" do
result =
temple do
div class: "hello" do
span id: "foo" do
"Howdy, "
end
div class: "hi" do
"Jim Bob"
end
c WhoaNelly, foo: "bar" do
slot :silver do
"esketit"
end
end
end
end
|> Utils.append_new_line()
expected = ~s"""
<div class="hello">
<span id="foo">
Howdy,
</span>
<div class="hi">
Jim Bob
</div>
<%= Temple.Component.__component__ WhoaNelly, [foo: "bar"] do %>
<% {:silver, %{}} -> %>
esketit
<% end %>
</div>
"""
assert result == expected
end
end