-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for HEEx. #168
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,3 +5,4 @@ erl_crash.dump | |
/bench/snapshots | ||
/src/slime_parser.peg | ||
/src/slime_parser.erl | ||
.tool-versions |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be either
or
? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, you're right. I'll add a suggestion for that, too.
|
||
``` | ||
|
||
### Attributes | ||
|
||
Attributes can be assigned in a similar fashion to regular HTML. | ||
|
@@ -114,6 +127,7 @@ body#bar | |
<body id="bar"></body> | ||
``` | ||
|
||
See [HEEx Support](#heex-support) for assigning attributes when rendering to HEEx. | ||
|
||
### Code | ||
|
||
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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 | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -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 | ||||||||
|
||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Removing a formatting change to simplify the diff. |
||||||||
eex = file |> File.read!() |> Renderer.precompile() | ||||||||
EEx.function_from_string(kind, name, eex, args, opts) | ||||||||
end | ||||||||
|
@@ -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) | ||||||||
Comment on lines
+82
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Removing a formatting change to simplify the diff. |
||||||||
EEx.function_from_string(kind, name, eex, args, opts) | ||||||||
end | ||||||||
end | ||||||||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -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 {"#" <> "{", "}"} | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Any reason not to write this way? |
||||||||
@heex_delimiters {"{", "}"} | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I can tell, the values of these globals are never used, except for comparing if the passed "delimiters" parameter matches one or the other. Given this, perhaps it would be better to pass a symbol instead? Something like |
||||||||
|
||||||||
@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([], _delimiters), do: "" | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Might (or might not!) be good to avoid breaking the public API of the module by preserving If this is done, all the changes to test/compiler_test.exs can be reverted. |
||||||||
|
||||||||
def compile(tags) when is_list(tags) 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 | ||||||||
Comment on lines
+38
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be useful for debugging to print the HEExNode here or tell the user to look for tags that begin with |
||||||||
|
||||||||
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 = | ||||||||
|
@@ -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 <> " %>" | ||||||||
|
||||||||
|
@@ -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}" | ||||||||
|
@@ -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 | ||||||||
|
@@ -109,27 +133,34 @@ 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}"} %>"] | ||||||||
|
||||||||
# 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 | ||||||||
# String with interpolation or string concatination | ||||||||
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 | ||||||||
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 | ||||||||
# When rendering to traditional EEx | ||||||||
value = if safe == :eex, do: "slim__v", else: "{:safe, slim__v}" | ||||||||
|
||||||||
""" | ||||||||
|
@@ -139,6 +170,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: "" | ||||||||
|
||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -5,6 +5,8 @@ defmodule Slime.Renderer do | |||||||||
alias Slime.Compiler | ||||||||||
alias Slime.Parser | ||||||||||
|
||||||||||
import Compiler, only: [eex_delimiters: 0, heex_delimiters: 0] | ||||||||||
|
||||||||||
@doc """ | ||||||||||
Compile Slime template to valid EEx HTML. | ||||||||||
|
||||||||||
|
@@ -15,7 +17,20 @@ defmodule Slime.Renderer do | |||||||||
def precompile(input) do | ||||||||||
input | ||||||||||
|> Parser.parse() | ||||||||||
|> Compiler.compile() | ||||||||||
|> Compiler.compile(eex_delimiters()) | ||||||||||
end | ||||||||||
|
||||||||||
@doc """ | ||||||||||
Compile Slime template to valid EEx HTML. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
||||||||||
## Examples | ||||||||||
iex> Slime.Renderer.precompile(~s(input.required type="hidden")) | ||||||||||
"<input class=\\"required\\" type=\\"hidden\\">" | ||||||||||
Comment on lines
+27
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
""" | ||||||||||
def precompile_heex(input) do | ||||||||||
input | ||||||||||
|> Parser.parse() | ||||||||||
|> Compiler.compile(heex_delimiters()) | ||||||||||
end | ||||||||||
|
||||||||||
@doc """ | ||||||||||
|
@@ -25,10 +40,13 @@ defmodule Slime.Renderer do | |||||||||
Note that this method of rendering is substantially slower than rendering | ||||||||||
precompiled templates created with Slime.function_from_file/5 and | ||||||||||
Slime.function_from_string/5. | ||||||||||
|
||||||||||
Note: A HEEx-aware version of render/4 was not included because it would require importing | ||||||||||
Phoenix.LiveView.HTMLEngine, creating a dependency on Phoenix. | ||||||||||
""" | ||||||||||
def render(slime, bindings \\ [], opts \\ []) do | ||||||||||
slime | ||||||||||
|> precompile | ||||||||||
|> precompile() | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Removing a formatting change to simplify the diff. |
||||||||||
|> EEx.eval_string(bindings, opts) | ||||||||||
end | ||||||||||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you split the tag across multiple lines you have to use
|
.