Skip to content

Commit

Permalink
Add support for HEEx.
Browse files Browse the repository at this point in the history
Co-Authored-By: Topher Hunt <[email protected]>
  • Loading branch information
tensiondriven and topherhunt committed Nov 30, 2021
1 parent d461835 commit df65f1f
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 43 deletions.
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
elixir 1.12.2
erlang 24.0.2
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ Slime.render(source, site_title: "Website Title")

## Reference

### Tags

Starting a line with a string followed by a space will create an html tag, as follows:

```slim
tt
Always bring a towel.
```

```html
<tt>Always bring a towel.<tt>
```

### Attributes

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

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

### Code

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

## HEEx Support

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:

- Attribute values will be wrapped in curley-braces (`{}`) instead of escaped EEx (`#{}`):

- 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 (`.`).

For example,

```slim
:greet user=@current_user.name
| Hello there!
```
would create the following output:

```
<.greet user={@current_user.name}>Hello there!</.greet>
```
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.


## Precompilation

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

To use slime templates (and Slime) with
[Phoenix][phoenix], please see
[PhoenixSlim][phoenix-slime].
[PhoenixSlime][phoenix-slime].

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

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

## Debugging

Expand Down
10 changes: 9 additions & 1 deletion lib/slime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,14 @@ defmodule Slime do
# iex
Sample.sample(1, 2) #=> "3"
Note: A HEEx-aware version of function_from_file/5 was not included because it would require importing
Phoenix.LiveView.HTMLEngine, creating a dependency on Phoenix.
"""
defmacro function_from_file(kind, name, file, args \\ [], opts \\ []) do
quote bind_quoted: binding() do
require EEx

eex = file |> File.read!() |> Renderer.precompile()
EEx.function_from_string(kind, name, eex, args, opts)
end
Expand All @@ -68,11 +72,15 @@ defmodule Slime do
...> end
iex> Sample.sample(1, 2)
"3"
Note: A HEEx-aware version of function_from_string/5 was not included because it would require importing
Phoenix.LiveView.HTMLEngine, creating a dependency on Phoenix.
"""
defmacro function_from_string(kind, name, source, args \\ [], opts \\ []) do
quote bind_quoted: binding() do
require EEx
eex = source |> Renderer.precompile()

eex = Renderer.precompile(source)
EEx.function_from_string(kind, name, eex, args, opts)
end
end
Expand Down
109 changes: 78 additions & 31 deletions lib/slime/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,51 @@ defmodule Slime.Compiler do

alias Slime.Doctype

alias Slime.Parser.Nodes.{DoctypeNode, EExNode, HTMLCommentNode, HTMLNode, InlineHTMLNode, VerbatimTextNode}
alias Slime.Parser.Nodes.{DoctypeNode, EExNode, HEExNode, HTMLCommentNode, HTMLNode, InlineHTMLNode, VerbatimTextNode}

alias Slime.TemplateSyntaxError

@eex_delimiters {"#" <> "{", "}"}
@heex_delimiters {"{", "}"}

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

def compile([]), do: ""
def eex_delimiters, do: @eex_delimiters
def heex_delimiters, do: @heex_delimiters

def compile(tags) when is_list(tags) do
def compile([], _delimiters), do: ""

def compile(tags, delimiters) when is_list(tags) do
tags
|> Enum.map(&compile(&1))
|> Enum.map(&compile(&1, delimiters))
|> Enum.join()
|> String.replace("\r", "")
end

def compile(%DoctypeNode{name: name}), do: Doctype.for(name)
def compile(%VerbatimTextNode{content: content}), do: compile(content)
def compile(%DoctypeNode{name: name}, _delimiters), do: Doctype.for(name)
def compile(%VerbatimTextNode{content: content}, delimiters), do: compile(content, delimiters)

def compile(%HEExNode{}, @eex_delimiters) do
# Raise an error if the user generates a HEEx node (by using a :) but the target is EEx

raise TemplateSyntaxError,
line: 0,
message: "I found a HEEx component, but this is not compiling to a HEEx file",
line_number: 0,
column: 0
end

def compile(%HEExNode{} = tag, @heex_delimiters) do
# Pass the HEExNode through to HTMLNode since it behaves identically
tag = Map.put(tag, :__struct__, HTMLNode)
compile(tag, @heex_delimiters)
end

def compile(%HTMLNode{name: name, spaces: spaces} = tag) do
attrs = Enum.map(tag.attributes, &render_attribute/1)
def compile(%HTMLNode{name: name, spaces: spaces} = tag, delimiters) do
attrs = Enum.map(tag.attributes, &render_attribute(&1, delimiters))
tag_head = Enum.join([name | attrs])

body =
Expand All @@ -37,13 +61,13 @@ defmodule Slime.Compiler do
"<" <> tag_head <> ">"

:otherwise ->
"<" <> tag_head <> ">" <> compile(tag.children) <> "</" <> name <> ">"
"<" <> tag_head <> ">" <> compile(tag.children, delimiters) <> "</" <> name <> ">"
end

leading_space(spaces) <> body <> trailing_space(spaces)
end

def compile(%EExNode{content: code, spaces: spaces, output: output} = eex) do
def compile(%EExNode{content: code, spaces: spaces, output: output} = eex, delimiters) do
code = if eex.safe?, do: "{:safe, " <> code <> "}", else: code
opening = if(output, do: "<%= ", else: "<% ") <> code <> " %>"

Expand All @@ -54,30 +78,30 @@ defmodule Slime.Compiler do
""
end

body = opening <> compile(eex.children) <> closing
body = opening <> compile(eex.children, delimiters) <> closing

leading_space(spaces) <> body <> trailing_space(spaces)
end

def compile(%InlineHTMLNode{content: content, children: children}) do
compile(content) <> compile(children)
def compile(%InlineHTMLNode{content: content, children: children}, delimiters) do
compile(content, delimiters) <> compile(children, delimiters)
end

def compile(%HTMLCommentNode{content: content}) do
"<!--" <> compile(content) <> "-->"
def compile(%HTMLCommentNode{content: content}, delimiters) do
"<!--" <> compile(content, delimiters) <> "-->"
end

def compile({:eex, eex}), do: "<%= " <> eex <> "%>"
def compile({:safe_eex, eex}), do: "<%= {:safe, " <> eex <> "} %>"
def compile(raw), do: raw
def compile({:eex, eex}, _delimiter), do: "<%= " <> eex <> "%>"
def compile({:safe_eex, eex}, _delimiter), do: "<%= {:safe, " <> eex <> "} %>"
def compile(raw, _delimiter), do: raw

@spec hide_dialyzer_spec(any) :: any
def hide_dialyzer_spec(input), do: input

defp render_attribute({_, []}), do: ""
defp render_attribute({_, ""}), do: ""
defp render_attribute({_, []}, _delimiters), do: ""
defp render_attribute({_, ""}, _delimiters), do: ""

defp render_attribute({name, {safe_eex, content}}) do
defp render_attribute({name, {safe_eex, content}}, delimiters) do
case content do
"true" ->
" #{name}"
Expand All @@ -90,11 +114,11 @@ defmodule Slime.Compiler do

_ ->
{:ok, quoted_content} = Code.string_to_quoted(content)
render_attribute_code(name, content, quoted_content, safe_eex)
render_attribute_code(name, content, quoted_content, safe_eex, delimiters)
end
end

defp render_attribute({name, value}) do
defp render_attribute({name, value}, _delimiters) do
if value == true do
" #{name}"
else
Expand All @@ -109,27 +133,45 @@ defmodule Slime.Compiler do
end
end

defp render_attribute_code(name, _content, quoted, _safe)
defp render_attribute_code(name, _content, quoted, _safe, _delimiters)
when is_number(quoted) or is_atom(quoted) do
~s[ #{name}="#{quoted}"]
end

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

defp render_attribute_code(name, _content, quoted, :eex) when is_binary(quoted), do: ~s[ #{name}="#{quoted}"]
defp render_attribute_code(name, _content, quoted, :eex, _delimiters) when is_binary(quoted),
do: ~s[ #{name}="#{quoted}"]

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

# # Topher and Jonathan are writing elixir here
# defp render_attribute_code(name, content, {op, _, _}, _delimiters) when op in [:<<>>, :<>] do
# # was: ~s[ #{name}="<%= #{content} %>"]
# IO.inspect("WE DID IT!")
# ~s[ #{name}="{#{content}}"]
# end

# NOTE: string with interpolation or strings concatination
defp render_attribute_code(name, content, {op, _, _}, safe) when op in [:<<>>, :<>] do
value = if safe == :eex, do: content, else: "{:safe, #{content}}"
~s[ #{name}="<%= #{value} %>"]
defp render_attribute_code(name, content, {op, _, _}, safe, @heex_delimiters) when op in [:<<>>, :<>] do
# IO.inspect op, label: "heex_delimiters <<>>"
expression = if safe == :eex, do: content, else: "{:safe, #{content}}"
~s[ #{name}={#{expression}}]
end

defp render_attribute_code(name, content, {op, _, _}, safe, @eex_delimiters) when op in [:<<>>, :<>] do
# IO.inspect op, label: "eex_delimiters <<>>"
expression = if safe == :eex, do: content, else: "{:safe, #{content}}"
~s[ #{name}="<%= #{expression} %>"]
end

defp render_attribute_code(name, content, _, safe) do
defp render_attribute_code(name, content, _, safe, @eex_delimiters) do
# IO.inspect "EEx"

# When rendering to traditional EEx
value = if safe == :eex, do: "slim__v", else: "{:safe, slim__v}"

"""
Expand All @@ -139,6 +181,11 @@ defmodule Slime.Compiler do
"""
end

defp render_attribute_code(name, content, _, _safe, @heex_delimiters) do
# When rendering to html-aware HEEx
~s[ #{name}={#{content}}]
end

defp leading_space(%{leading: true}), do: " "
defp leading_space(_), do: ""

Expand Down
20 changes: 20 additions & 0 deletions lib/slime/parser/nodes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,26 @@ defmodule Slime.Parser.Nodes do
safe?: false
end

defmodule HEExNode do
@moduledoc """
An HTML node that represents a HEEx function component.
* :name — function component (tag) name,
* :attributes — a list of {"name", :v} tuples, where :v is
either a string or an {:eex, "content"} tuple,
* :spaces — tag whitespace, represented as a keyword list of boolean
values for :leading and :trailing,
* :closed — the presence of a trailing "/", which explicitly closes the tag,
* :children — a list of nodes.
"""

defstruct name: "",
attributes: [],
spaces: %{},
closed: false,
children: []
end

defmodule VerbatimTextNode do
@moduledoc """
A verbatim text node.
Expand Down
9 changes: 8 additions & 1 deletion lib/slime/parser/transform.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule Slime.Parser.Transform do
import Slime.Parser.Preprocessor, only: [indent_size: 1]

alias Slime.Parser.{AttributesKeyword, EmbeddedEngine, TextBlock}
alias Slime.Parser.Nodes.{DoctypeNode, EExNode, HTMLCommentNode, HTMLNode, InlineHTMLNode, VerbatimTextNode}
alias Slime.Parser.Nodes.{DoctypeNode, EExNode, HEExNode, HTMLCommentNode, HTMLNode, InlineHTMLNode, VerbatimTextNode}

alias Slime.TemplateSyntaxError

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

def transform(:function_component, [":", name, _space, content], _index) do
{attributes, children, false} = content
# Match on brief function components, e.g. ".city" and explicit, e.g. "MyApp.city"
leading_dot = if "." in name, do: "", else: "."
%HEExNode{name: "#{leading_dot}#{name}", attributes: attributes, children: children}
end

def transform(:tag_spaces, input, _index) do
leading = input[:leading]
trailing = input[:trailing]
Expand Down
Loading

0 comments on commit df65f1f

Please sign in to comment.