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 details summary menuitem menu
meta link base meta link base
area br col embed hr img input keygen param source track wbr area br col embed hr img input keygen param source track wbr
txt partial
animate animateMotion animateTransform circle clipPath animate animateMotion animateTransform circle clipPath
color-profile defs desc discard ellipse feBlend 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 marker mask mesh meshgradient meshpatch meshrow metadata mpath path pattern polygon
polyline radialGradient rect set solidcolor stop svg switch symbol text polyline radialGradient rect set solidcolor stop svg switch symbol text
textPath tspan unknown use view textPath tspan unknown use view
]a |> Enum.flat_map(fn e -> [{e, :*}, {:"#{e}!", :*}] end)
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)
[ [
inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"], 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. # 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!" "Alert!"
end end
@ -71,6 +71,34 @@ defmodule Temple do
end 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 ## Configuration
### Mode ### Mode
@ -151,7 +179,8 @@ defmodule Temple do
markup = markup =
block block
|> Parser.parse() |> 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() |> :erlang.iolist_to_binary()
quote location: :keep do quote location: :keep do
@ -163,7 +192,8 @@ defmodule Temple do
quote location: :keep do quote location: :keep do
unquote(block) unquote(block)
|> Parser.parse() |> 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() |> :erlang.iolist_to_binary()
end end
end end
@ -190,7 +220,8 @@ defmodule Temple do
markup = markup =
block block
|> Parser.parse() |> 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() |> :erlang.iolist_to_binary()
EEx.compile_string(markup, engine: engine, line: __CALLER__.line, file: __CALLER__.file) EEx.compile_string(markup, engine: engine, line: __CALLER__.line, file: __CALLER__.file)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -30,18 +30,23 @@ defmodule Temple.Parser.DoExpressions do
end end
defimpl Temple.Generator do 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), Macro.to_string(expression),
" do %>", " do %>",
"\n", "\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, 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: "" else: ""
), ),
"<% end %>" "\n#{Parser.Utils.indent(indent)}<% end %>"
] ]
end 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 end
defimpl Temple.Generator do defimpl Temple.Generator do
def to_eex(_) do def to_eex(_, _ \\ 0) do
[] []
end end
end end

View file

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

View file

@ -2,7 +2,7 @@ defmodule Temple.Parser.NonvoidElementsAliases do
@moduledoc false @moduledoc false
@behaviour Temple.Parser @behaviour Temple.Parser
defstruct name: nil, attrs: [], children: [] defstruct name: nil, attrs: [], children: [], meta: %{}
alias Temple.Parser alias Temple.Parser
@ -23,18 +23,34 @@ defmodule Temple.Parser.NonvoidElementsAliases do
children = Temple.Parser.parse(do_and_else[: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 end
defimpl Temple.Generator do 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, name,
Temple.Parser.Utils.compile_attrs(attrs), Temple.Parser.Utils.compile_attrs(attrs),
">\n", ">",
for(child <- children, do: Temple.Generator.to_eex(child)), Temple.Generator.to_eex(children, indent),
"\n</", "</",
name, name,
">" ">"
] ]

View file

@ -18,12 +18,12 @@ defmodule Temple.Parser.RightArrow do
end end
defimpl Temple.Generator do 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), Macro.to_string(elixir_ast),
" -> %>\n", " -> %>\n",
for(child <- children, do: Temple.Generator.to_eex(child)) for(child <- children, do: Temple.Generator.to_eex(child, indent + 1))
] ]
end end
end end

View file

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

View file

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

View file

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

View file

@ -2,6 +2,8 @@ defmodule Temple.Parser.VoidElementsAliases do
@moduledoc false @moduledoc false
@behaviour Temple.Parser @behaviour Temple.Parser
alias Temple.Parser.Utils
defstruct name: nil, attrs: [] defstruct name: nil, attrs: []
@impl Temple.Parser @impl Temple.Parser
@ -28,12 +30,12 @@ defmodule Temple.Parser.VoidElementsAliases do
end end
defimpl Temple.Generator do 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), to_string(name),
Temple.Parser.Utils.compile_attrs(attrs), Temple.Parser.Utils.compile_attrs(attrs),
">\n" ">"
] ]
end end
end end

View file

@ -18,7 +18,19 @@ defmodule Temple.ComponentTest do
end end
assert evaluate_template(result) == 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 end
test "function components can accept local assigns" do test "function components can accept local assigns" do
@ -34,7 +46,16 @@ defmodule Temple.ComponentTest do
end end
assert evaluate_template(result) == 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 end
test "function components can use other components" do test "function components can use other components" do
@ -50,8 +71,21 @@ defmodule Temple.ComponentTest do
end end
assert evaluate_template(result) == ~s""" assert evaluate_template(result) == ~s"""
<div id="inner" outer-id="from-outer">outer!</div> <div id="inner" outer-id="from-outer">
<div id="inner" outer-id="set by root inner">inner!</div>
outer!
</div>
<div id="inner" outer-id="set by root inner">
inner!
</div>
""" """
end end
@ -63,7 +97,14 @@ defmodule Temple.ComponentTest do
end end
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 end
test "components can be void elements" do test "components can be void elements" do
@ -72,7 +113,11 @@ defmodule Temple.ComponentTest do
c Temple.Components.VoidComponent, foo: :bar c Temple.Components.VoidComponent, foo: :bar
end end
assert evaluate_template(result) == ~s{<div class="void!!">bar</div>} assert evaluate_template(result) == ~s"""
<div class="void!!">
bar
</div>
"""
end end
test "components can have named slots" do test "components can have named slots" do
@ -94,6 +139,22 @@ defmodule Temple.ComponentTest do
end end
assert evaluate_template(result, assigns) == 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
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 test "returns true when the node contains an anonymous function as an argument to a function" do
raw_asts = [ raw_asts = [
quote do 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) Does.something!(form)
end end)
end end
] ]
@ -47,9 +47,9 @@ defmodule Temple.Parser.AnonymousFunctionsTest do
raw_ast = raw_ast =
quote do 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) unquote(expected_child)
end end)
end end
ast = AnonymousFunctions.run(raw_ast) ast = AnonymousFunctions.run(raw_ast)
@ -69,9 +69,9 @@ defmodule Temple.Parser.AnonymousFunctionsTest do
test "emits eex" do test "emits eex" do
raw_ast = raw_ast =
quote do 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) Does.something!(form)
end end)
end end
result = result =

View file

@ -187,7 +187,12 @@ defmodule Temple.Parser.ComponentsTest do
|> Temple.Generator.to_eex() |> Temple.Generator.to_eex()
assert result |> :erlang.iolist_to_binary() == 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 end
test "emits eex for void component with slots" do test "emits eex for void component with slots" do
@ -208,7 +213,14 @@ defmodule Temple.Parser.ComponentsTest do
|> Temple.Generator.to_eex() |> Temple.Generator.to_eex()
assert result |> :erlang.iolist_to_binary() == 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 end
test "emits eex for nonvoid component with slots" do test "emits eex for nonvoid component with slots" do
@ -233,7 +245,18 @@ defmodule Temple.Parser.ComponentsTest do
|> Temple.Generator.to_eex() |> Temple.Generator.to_eex()
assert result |> :erlang.iolist_to_binary() == 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 end
test "emits eex for void component" do test "emits eex for void component" do

View file

@ -2,6 +2,7 @@ defmodule Temple.Parser.DefaultTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Temple.Parser.Default alias Temple.Parser.Default
alias Temple.Support.Utils
describe "applicable?/1" do describe "applicable?/1" do
test "returns true when the node is an elixir expression" do test "returns true when the node is an elixir expression" do
@ -35,8 +36,11 @@ defmodule Temple.Parser.DefaultTest do
end end
|> Default.run() |> Default.run()
|> Temple.Generator.to_eex() |> 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 end
end end

View file

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

View file

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

View file

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

View file

@ -42,7 +42,9 @@ defmodule Temple.Parser.SlotTest do
|> Temple.Generator.to_eex() |> Temple.Generator.to_eex()
assert result |> :erlang.iolist_to_binary() == 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 end
end end

View file

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

View file

@ -3,6 +3,7 @@ defmodule Temple.Parser.TempleNamespaceVoidTest do
alias Temple.Parser.TempleNamespaceVoid alias Temple.Parser.TempleNamespaceVoid
alias Temple.Parser.VoidElementsAliases alias Temple.Parser.VoidElementsAliases
alias Temple.Support.Utils
describe "applicable?/1" do describe "applicable?/1" do
test "returns true when the node is a Temple aliased nonvoid element" do test "returns true when the node is a Temple aliased nonvoid element" do
@ -58,8 +59,9 @@ defmodule Temple.Parser.TempleNamespaceVoidTest do
end end
|> TempleNamespaceVoid.run() |> TempleNamespaceVoid.run()
|> Temple.Generator.to_eex() |> 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 end
end end

View file

@ -2,6 +2,7 @@ defmodule Temple.Parser.TextTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Temple.Parser.Text alias Temple.Parser.Text
alias Temple.Support.Utils
describe "applicable?/1" do describe "applicable?/1" do
test "returns true when the node is a string literal" do test "returns true when the node is a string literal" do
@ -35,8 +36,9 @@ defmodule Temple.Parser.TextTest do
"string literal" "string literal"
|> Text.run() |> Text.run()
|> Temple.Generator.to_eex() |> 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 end
end end

View file

@ -2,6 +2,7 @@ defmodule Temple.Parser.VoidElementsAliasesTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Temple.Parser.VoidElementsAliases alias Temple.Parser.VoidElementsAliases
alias Temple.Support.Utils
describe "applicable?/1" do describe "applicable?/1" do
test "returns true when the node is a nonvoid element or alias" do test "returns true when the node is a nonvoid element or alias" do
@ -63,8 +64,9 @@ defmodule Temple.Parser.VoidElementsAliasesTest do
end end
|> VoidElementsAliases.run() |> VoidElementsAliases.run()
|> Temple.Generator.to_eex() |> 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 end
end end

View file

@ -33,4 +33,20 @@ defmodule Temple.Support.Utils do
|> elem(0) |> elem(0)
|> Phoenix.HTML.safe_to_string() |> Phoenix.HTML.safe_to_string()
end 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 end

View file

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