Skip to content

Commit df65f1f

Browse files
Add support for HEEx.
Co-Authored-By: Topher Hunt <hunt.topher@gmail.com>
1 parent d461835 commit df65f1f

File tree

10 files changed

+253
-43
lines changed

10 files changed

+253
-43
lines changed

.tool-versions

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
elixir 1.12.2
2+
erlang 24.0.2

README.md

+38-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,19 @@ Slime.render(source, site_title: "Website Title")
6767

6868
## Reference
6969

70+
### Tags
71+
72+
Starting a line with a string followed by a space will create an html tag, as follows:
73+
74+
```slim
75+
tt
76+
Always bring a towel.
77+
```
78+
79+
```html
80+
<tt>Always bring a towel.<tt>
81+
```
82+
7083
### Attributes
7184

7285
Attributes can be assigned in a similar fashion to regular HTML.
@@ -114,6 +127,7 @@ body#bar
114127
<body id="bar"></body>
115128
```
116129

130+
See [HEEx Support](#heex-support) for assigning attributes when rendering to HEEx.
117131

118132
### Code
119133

@@ -277,6 +291,28 @@ the library after you have added new engines. You can do this by:
277291
mix deps.compile slime --force
278292
```
279293

294+
## HEEx Support
295+
296+
To output HEEx instead of HTML, see [`phoenix_slime`](https://github.com/slime-lang/phoenix_slime). This will cause slime to emit "html aware" HEEx with two differences from conventional HTML:
297+
298+
- Attribute values will be wrapped in curley-braces (`{}`) instead of escaped EEx (`#{}`):
299+
300+
- HTML Components will be prefixed with a dot. To render an HTML Component, prefix the component name with a colon (`:`). This will tell slime to render these html tags with a dot-prefix (`.`).
301+
302+
For example,
303+
304+
```slim
305+
:greet user=@current_user.name
306+
| Hello there!
307+
```
308+
would create the following output:
309+
310+
```
311+
<.greet user={@current_user.name}>Hello there!</.greet>
312+
```
313+
When using slime with Phoenix, the `phoenix_slime` package will call `precompile_heex/2` and pass the resulting valid HEEx to [`EEx`](https://hexdocs.pm/eex/EEx.html) with [`Phoenix.LiveView.HTMLEngine`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.HTMLEngine.html#handle_text/3) as the `engine:` option. This will produce the final html.
314+
315+
280316
## Precompilation
281317

282318
Templates can be compiled into module functions like EEx templates, using
@@ -285,7 +321,7 @@ functions `Slime.function_from_file/5` and
285321

286322
To use slime templates (and Slime) with
287323
[Phoenix][phoenix], please see
288-
[PhoenixSlim][phoenix-slime].
324+
[PhoenixSlime][phoenix-slime].
289325

290326
[phoenix]: http://www.phoenixframework.org/
291327
[phoenix-slime]: https://github.com/slime-lang/phoenix_slime
@@ -319,6 +355,7 @@ where Ruby Slim would do
319355
Note the `do` and the initial `=`, because we render the return value of the
320356
conditional as a whole.
321357

358+
Slime also adds support for HEEx. See the section on [HEEx Support](#heex-support).
322359

323360
## Debugging
324361

lib/slime.ex

+9-1
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,14 @@ defmodule Slime do
4646
4747
# iex
4848
Sample.sample(1, 2) #=> "3"
49+
50+
Note: A HEEx-aware version of function_from_file/5 was not included because it would require importing
51+
Phoenix.LiveView.HTMLEngine, creating a dependency on Phoenix.
4952
"""
5053
defmacro function_from_file(kind, name, file, args \\ [], opts \\ []) do
5154
quote bind_quoted: binding() do
5255
require EEx
56+
5357
eex = file |> File.read!() |> Renderer.precompile()
5458
EEx.function_from_string(kind, name, eex, args, opts)
5559
end
@@ -68,11 +72,15 @@ defmodule Slime do
6872
...> end
6973
iex> Sample.sample(1, 2)
7074
"3"
75+
76+
Note: A HEEx-aware version of function_from_string/5 was not included because it would require importing
77+
Phoenix.LiveView.HTMLEngine, creating a dependency on Phoenix.
7178
"""
7279
defmacro function_from_string(kind, name, source, args \\ [], opts \\ []) do
7380
quote bind_quoted: binding() do
7481
require EEx
75-
eex = source |> Renderer.precompile()
82+
83+
eex = Renderer.precompile(source)
7684
EEx.function_from_string(kind, name, eex, args, opts)
7785
end
7886
end

lib/slime/compiler.ex

+78-31
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,51 @@ defmodule Slime.Compiler do
55

66
alias Slime.Doctype
77

8-
alias Slime.Parser.Nodes.{DoctypeNode, EExNode, HTMLCommentNode, HTMLNode, InlineHTMLNode, VerbatimTextNode}
8+
alias Slime.Parser.Nodes.{DoctypeNode, EExNode, HEExNode, HTMLCommentNode, HTMLNode, InlineHTMLNode, VerbatimTextNode}
9+
10+
alias Slime.TemplateSyntaxError
11+
12+
@eex_delimiters {"#" <> "{", "}"}
13+
@heex_delimiters {"{", "}"}
914

1015
@void_elements ~w(
1116
area br col doctype embed hr img input link meta base param
1217
keygen source menuitem track wbr
1318
)
1419

15-
def compile([]), do: ""
20+
def eex_delimiters, do: @eex_delimiters
21+
def heex_delimiters, do: @heex_delimiters
1622

17-
def compile(tags) when is_list(tags) do
23+
def compile([], _delimiters), do: ""
24+
25+
def compile(tags, delimiters) when is_list(tags) do
1826
tags
19-
|> Enum.map(&compile(&1))
27+
|> Enum.map(&compile(&1, delimiters))
2028
|> Enum.join()
2129
|> String.replace("\r", "")
2230
end
2331

24-
def compile(%DoctypeNode{name: name}), do: Doctype.for(name)
25-
def compile(%VerbatimTextNode{content: content}), do: compile(content)
32+
def compile(%DoctypeNode{name: name}, _delimiters), do: Doctype.for(name)
33+
def compile(%VerbatimTextNode{content: content}, delimiters), do: compile(content, delimiters)
34+
35+
def compile(%HEExNode{}, @eex_delimiters) do
36+
# Raise an error if the user generates a HEEx node (by using a :) but the target is EEx
37+
38+
raise TemplateSyntaxError,
39+
line: 0,
40+
message: "I found a HEEx component, but this is not compiling to a HEEx file",
41+
line_number: 0,
42+
column: 0
43+
end
44+
45+
def compile(%HEExNode{} = tag, @heex_delimiters) do
46+
# Pass the HEExNode through to HTMLNode since it behaves identically
47+
tag = Map.put(tag, :__struct__, HTMLNode)
48+
compile(tag, @heex_delimiters)
49+
end
2650

27-
def compile(%HTMLNode{name: name, spaces: spaces} = tag) do
28-
attrs = Enum.map(tag.attributes, &render_attribute/1)
51+
def compile(%HTMLNode{name: name, spaces: spaces} = tag, delimiters) do
52+
attrs = Enum.map(tag.attributes, &render_attribute(&1, delimiters))
2953
tag_head = Enum.join([name | attrs])
3054

3155
body =
@@ -37,13 +61,13 @@ defmodule Slime.Compiler do
3761
"<" <> tag_head <> ">"
3862

3963
:otherwise ->
40-
"<" <> tag_head <> ">" <> compile(tag.children) <> "</" <> name <> ">"
64+
"<" <> tag_head <> ">" <> compile(tag.children, delimiters) <> "</" <> name <> ">"
4165
end
4266

4367
leading_space(spaces) <> body <> trailing_space(spaces)
4468
end
4569

46-
def compile(%EExNode{content: code, spaces: spaces, output: output} = eex) do
70+
def compile(%EExNode{content: code, spaces: spaces, output: output} = eex, delimiters) do
4771
code = if eex.safe?, do: "{:safe, " <> code <> "}", else: code
4872
opening = if(output, do: "<%= ", else: "<% ") <> code <> " %>"
4973

@@ -54,30 +78,30 @@ defmodule Slime.Compiler do
5478
""
5579
end
5680

57-
body = opening <> compile(eex.children) <> closing
81+
body = opening <> compile(eex.children, delimiters) <> closing
5882

5983
leading_space(spaces) <> body <> trailing_space(spaces)
6084
end
6185

62-
def compile(%InlineHTMLNode{content: content, children: children}) do
63-
compile(content) <> compile(children)
86+
def compile(%InlineHTMLNode{content: content, children: children}, delimiters) do
87+
compile(content, delimiters) <> compile(children, delimiters)
6488
end
6589

66-
def compile(%HTMLCommentNode{content: content}) do
67-
"<!--" <> compile(content) <> "-->"
90+
def compile(%HTMLCommentNode{content: content}, delimiters) do
91+
"<!--" <> compile(content, delimiters) <> "-->"
6892
end
6993

70-
def compile({:eex, eex}), do: "<%= " <> eex <> "%>"
71-
def compile({:safe_eex, eex}), do: "<%= {:safe, " <> eex <> "} %>"
72-
def compile(raw), do: raw
94+
def compile({:eex, eex}, _delimiter), do: "<%= " <> eex <> "%>"
95+
def compile({:safe_eex, eex}, _delimiter), do: "<%= {:safe, " <> eex <> "} %>"
96+
def compile(raw, _delimiter), do: raw
7397

7498
@spec hide_dialyzer_spec(any) :: any
7599
def hide_dialyzer_spec(input), do: input
76100

77-
defp render_attribute({_, []}), do: ""
78-
defp render_attribute({_, ""}), do: ""
101+
defp render_attribute({_, []}, _delimiters), do: ""
102+
defp render_attribute({_, ""}, _delimiters), do: ""
79103

80-
defp render_attribute({name, {safe_eex, content}}) do
104+
defp render_attribute({name, {safe_eex, content}}, delimiters) do
81105
case content do
82106
"true" ->
83107
" #{name}"
@@ -90,11 +114,11 @@ defmodule Slime.Compiler do
90114

91115
_ ->
92116
{:ok, quoted_content} = Code.string_to_quoted(content)
93-
render_attribute_code(name, content, quoted_content, safe_eex)
117+
render_attribute_code(name, content, quoted_content, safe_eex, delimiters)
94118
end
95119
end
96120

97-
defp render_attribute({name, value}) do
121+
defp render_attribute({name, value}, _delimiters) do
98122
if value == true do
99123
" #{name}"
100124
else
@@ -109,27 +133,45 @@ defmodule Slime.Compiler do
109133
end
110134
end
111135

112-
defp render_attribute_code(name, _content, quoted, _safe)
136+
defp render_attribute_code(name, _content, quoted, _safe, _delimiters)
113137
when is_number(quoted) or is_atom(quoted) do
114138
~s[ #{name}="#{quoted}"]
115139
end
116140

117-
defp render_attribute_code(name, _content, quoted, _) when is_list(quoted) do
141+
defp render_attribute_code(name, _content, quoted, _, _delimiters) when is_list(quoted) do
118142
quoted |> Enum.map_join(" ", &Kernel.to_string/1) |> (&~s[ #{name}="#{&1}"]).()
119143
end
120144

121-
defp render_attribute_code(name, _content, quoted, :eex) when is_binary(quoted), do: ~s[ #{name}="#{quoted}"]
145+
defp render_attribute_code(name, _content, quoted, :eex, _delimiters) when is_binary(quoted),
146+
do: ~s[ #{name}="#{quoted}"]
122147

123-
defp render_attribute_code(name, _content, quoted, _) when is_binary(quoted),
148+
defp render_attribute_code(name, _content, quoted, _, _delimiters) when is_binary(quoted),
124149
do: ~s[ #{name}="<%= {:safe, "#{quoted}"} %>"]
125150

151+
# # Topher and Jonathan are writing elixir here
152+
# defp render_attribute_code(name, content, {op, _, _}, _delimiters) when op in [:<<>>, :<>] do
153+
# # was: ~s[ #{name}="<%= #{content} %>"]
154+
# IO.inspect("WE DID IT!")
155+
# ~s[ #{name}="{#{content}}"]
156+
# end
157+
126158
# NOTE: string with interpolation or strings concatination
127-
defp render_attribute_code(name, content, {op, _, _}, safe) when op in [:<<>>, :<>] do
128-
value = if safe == :eex, do: content, else: "{:safe, #{content}}"
129-
~s[ #{name}="<%= #{value} %>"]
159+
defp render_attribute_code(name, content, {op, _, _}, safe, @heex_delimiters) when op in [:<<>>, :<>] do
160+
# IO.inspect op, label: "heex_delimiters <<>>"
161+
expression = if safe == :eex, do: content, else: "{:safe, #{content}}"
162+
~s[ #{name}={#{expression}}]
163+
end
164+
165+
defp render_attribute_code(name, content, {op, _, _}, safe, @eex_delimiters) when op in [:<<>>, :<>] do
166+
# IO.inspect op, label: "eex_delimiters <<>>"
167+
expression = if safe == :eex, do: content, else: "{:safe, #{content}}"
168+
~s[ #{name}="<%= #{expression} %>"]
130169
end
131170

132-
defp render_attribute_code(name, content, _, safe) do
171+
defp render_attribute_code(name, content, _, safe, @eex_delimiters) do
172+
# IO.inspect "EEx"
173+
174+
# When rendering to traditional EEx
133175
value = if safe == :eex, do: "slim__v", else: "{:safe, slim__v}"
134176

135177
"""
@@ -139,6 +181,11 @@ defmodule Slime.Compiler do
139181
"""
140182
end
141183

184+
defp render_attribute_code(name, content, _, _safe, @heex_delimiters) do
185+
# When rendering to html-aware HEEx
186+
~s[ #{name}={#{content}}]
187+
end
188+
142189
defp leading_space(%{leading: true}), do: " "
143190
defp leading_space(_), do: ""
144191

lib/slime/parser/nodes.ex

+20
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,26 @@ defmodule Slime.Parser.Nodes do
3838
safe?: false
3939
end
4040

41+
defmodule HEExNode do
42+
@moduledoc """
43+
An HTML node that represents a HEEx function component.
44+
45+
* :name — function component (tag) name,
46+
* :attributes — a list of {"name", :v} tuples, where :v is
47+
either a string or an {:eex, "content"} tuple,
48+
* :spaces — tag whitespace, represented as a keyword list of boolean
49+
values for :leading and :trailing,
50+
* :closed — the presence of a trailing "/", which explicitly closes the tag,
51+
* :children — a list of nodes.
52+
"""
53+
54+
defstruct name: "",
55+
attributes: [],
56+
spaces: %{},
57+
closed: false,
58+
children: []
59+
end
60+
4161
defmodule VerbatimTextNode do
4262
@moduledoc """
4363
A verbatim text node.

lib/slime/parser/transform.ex

+8-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ defmodule Slime.Parser.Transform do
88
import Slime.Parser.Preprocessor, only: [indent_size: 1]
99

1010
alias Slime.Parser.{AttributesKeyword, EmbeddedEngine, TextBlock}
11-
alias Slime.Parser.Nodes.{DoctypeNode, EExNode, HTMLCommentNode, HTMLNode, InlineHTMLNode, VerbatimTextNode}
11+
alias Slime.Parser.Nodes.{DoctypeNode, EExNode, HEExNode, HTMLCommentNode, HTMLNode, InlineHTMLNode, VerbatimTextNode}
1212

1313
alias Slime.TemplateSyntaxError
1414

@@ -214,6 +214,13 @@ defmodule Slime.Parser.Transform do
214214
%EExNode{content: to_string(content), output: true, safe?: safe == "="}
215215
end
216216

217+
def transform(:function_component, [":", name, _space, content], _index) do
218+
{attributes, children, false} = content
219+
# Match on brief function components, e.g. ".city" and explicit, e.g. "MyApp.city"
220+
leading_dot = if "." in name, do: "", else: "."
221+
%HEExNode{name: "#{leading_dot}#{name}", attributes: attributes, children: children}
222+
end
223+
217224
def transform(:tag_spaces, input, _index) do
218225
leading = input[:leading]
219226
trailing = input[:trailing]

0 commit comments

Comments
 (0)