Skip to content

Commit

Permalink
Support unescaped content, attributes and interpolation (#145)
Browse files Browse the repository at this point in the history
  • Loading branch information
Rakoth authored and doomspork committed Nov 7, 2017
1 parent 245aa21 commit fdbd42e
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 28 deletions.
27 changes: 19 additions & 8 deletions lib/slime/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ defmodule Slime.Compiler do
leading_space(spaces) <> body <> trailing_space(spaces)
end
def compile(%EExNode{content: code, spaces: spaces, output: output} = eex) do
code = if eex.safe?, do: "{:safe, " <> code <> "}", else: code
opening = (if output, do: "<%= ", else: "<% ") <> code <> " %>"
closing = if Regex.match?(~r/(fn.*->| do)\s*$/, code) do
"<% end %>"
Expand All @@ -57,21 +58,22 @@ defmodule Slime.Compiler do
"<!--" <> compile(content) <> "-->"
end
def compile({:eex, eex}), do: "<%= " <> eex <> "%>"
def compile({:safe_eex, eex}), do: "<%= {:safe, " <> eex <> "} %>"
def compile(raw), 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({name, {:eex, content}}) do
defp render_attribute({name, {safe_eex, content}}) do
case content do
"true" -> " #{name}"
"false" -> ""
"nil" -> ""
_ ->
{:ok, quoted_content} = Code.string_to_quoted(content)
render_attribute_code(name, content, quoted_content)
render_attribute_code(name, content, quoted_content, safe_eex)
end
end
defp render_attribute({name, value}) do
Expand All @@ -88,19 +90,28 @@ defmodule Slime.Compiler do
end
end

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

defp render_attribute_code(name, _cotnent, quoted, safe) when is_binary(quoted) do
value = if :eex == safe, do: quoted, else: ~s[<%= {:safe, "#{quoted}"} %>]
~s[ #{name}="#{value}"]
end

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

defp render_attribute_code(name, content, _, safe) do
value = if safe == :eex, do: "slim__v", else: "{:safe, slim__v}"
"""
<% slim__k = "#{name}"; slim__v = Slime.Compiler.hide_dialyzer_spec(#{content}) %>\
<%= if slim__v do %> <%= slim__k %><%= unless slim__v == true do %>\
="<%= slim__v %>"<% end %><% end %>\
="<%= #{value} %>"<% end %><% end %>\
"""
end

Expand Down
4 changes: 3 additions & 1 deletion lib/slime/parser/nodes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ defmodule Slime.Parser.Nodes do
* :spaces — tag whitespace, represented as a keyword list of boolean
values for :leading and :trailing,
* :children — a list of nodes.
* :safe? - mark output as safe for html-escaping engines
"""

defstruct content: "",
output: false,
spaces: %{},
children: []
children: [],
safe?: false
end

defmodule VerbatimTextNode do
Expand Down
37 changes: 28 additions & 9 deletions lib/slime/parser/transform.ex
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,20 @@ defmodule Slime.Parser.Transform do

def transform(:text_item, input, _index) do
case input do
{:dynamic, [_, expression, _]} -> {:eex, to_string(expression)}
{:dynamic, {:safe, expression}} -> {:safe_eex, expression}
{:dynamic, expression} -> {:eex, expression}
{:static, text} -> to_string(text)
end
end

def transform(:interpolation, [_, expression, _], _index) do
to_string(expression)
end

def transform(:safe_interpolation, [_, expression, _], _index) do
to_string(expression)
end

def transform(:html_comment, input, _index) do
indent = indent_size(input[:indent])
decl_indent = indent + String.length(input[:type])
Expand Down Expand Up @@ -172,16 +181,17 @@ defmodule Slime.Parser.Transform do
end

def transform(:code, input, _index) do
{output, spaces} = case input[:output] do
"-" -> {false, %{}}
[_, _, spaces] -> {true, spaces}
{output, safe, spaces} = case input[:output] do
"-" -> {false, false, %{}}
[_, safe, spaces] -> {true, safe == "=", spaces}
end

%EExNode{
content: input[:code],
output: output,
spaces: spaces,
children: input[:children] ++ input[:optional_else]
children: input[:children] ++ input[:optional_else],
safe?: safe
}
end

Expand All @@ -199,9 +209,8 @@ defmodule Slime.Parser.Transform do
def transform(:code_line, input, _index), do: to_string(input)
def transform(:code_line_with_break, input, _index), do: to_string(input)

def transform(:dynamic_content, input, _index) do
content = input |> Enum.at(3) |> to_string
%EExNode{content: content, output: true}
def transform(:dynamic_content, [_, safe, _, content], _index) do
%EExNode{content: to_string(content), output: true, safe?: safe == "="}
end

def transform(:tag_spaces, input, _index) do
Expand Down Expand Up @@ -251,7 +260,17 @@ defmodule Slime.Parser.Transform do
[head | tail]
end

def transform(:attribute, [name, _, value], _index), do: {name, value}
def transform(:attribute, [name, _, safe, value], _index) do
value = if safe == "=" do
case value do
{:eex, content} -> {:safe_eex, content}
_ -> {:safe_eex, ~s["#{value}"]}
end
else
value
end
{name, value}
end

def transform(:attribute_value, input, _index) do
case input do
Expand Down
4 changes: 2 additions & 2 deletions lib/slime/renderer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ defmodule Slime.Renderer do
precompiled templates created with Slime.function_from_file/5 and
Slime.function_from_string/5.
"""
def render(slime, bindings \\ []) do
def render(slime, bindings \\ [], opts \\ []) do
slime
|> precompile
|> EEx.eval_string(bindings)
|> EEx.eval_string(bindings, opts)
end
end
8 changes: 5 additions & 3 deletions src/slime_parser.peg.eex
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,20 @@ wrapped_attribute <- (space / eol)* (attribute:attribute / attribute_name:tag_na

plain_attributes <- head:attribute tail:(space attribute)*;

attribute <- attribute_name '=' attribute_value;
attribute <- attribute_name '=' '='? attribute_value;

attribute_value <- simple:string / dynamic:(string_with_interpolation / attribute_code);

string <- '"' ('\\' . / !('"' / '#{') .)* '"';
string_with_interpolation <- '"' (interpolation / '\\' . / !'"' .)* '"';
string_with_interpolation <- '"' (elixir_interpolation / '\\' . / !'"' .)* '"';

attribute_code <- (parentheses / brackets / braces / !(space / eol / ')' / ']' / '}') .)+;

text_item <- static:text / dynamic:interpolation;
text_item <- static:text / dynamic:(safe:safe_interpolation / interpolation);
text <- ('\\' . / !('#{' / eol) .)+;
elixir_interpolation <- '#{' (string / string_with_interpolation / !'}' .)* '}';
interpolation <- '#{' (string / string_with_interpolation / !'}' .)* '}';
safe_interpolation <- '#{{' (string / string_with_interpolation / !'}}' .)* '}}';

parentheses <- '(' (parentheses / !')' .)* ')';
brackets <- '[' (brackets / !']' .)* ']';
Expand Down
5 changes: 0 additions & 5 deletions test/rendering/elixir_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ defmodule RenderElixirTest do
assert render(slime) == "2"
end

test "== evalutes Elixir and inserts the result (unescaped version)" do
slime = "== 1 + 1"
assert render(slime) == "2"
end

test "= can be used inside an element (space before)" do
slime = "div = 1 + 1"
assert render(slime) == "<div>2</div>"
Expand Down
41 changes: 41 additions & 0 deletions test/rendering/raw_output_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule RawOutputTest do
use ExUnit.Case

alias Slime.Renderer
alias Phoenix.HTML
alias Phoenix.HTML.Engine

defp render(template, bindings \\ []) do
template
|> Renderer.render(bindings, engine: Engine)
|> HTML.safe_to_string
end

test "render raw dynamic content" do
slime = """
== "<>"
"""
assert render(slime) == "<>"
end

test "render raw attribute value" do
slime = """
a href==href
"""
assert render(slime, href: "&") == ~s[<a href="&"></a>]
end

test "render raw tag content" do
slime = """
p == "<>"
"""
assert render(slime) == ~s[<p><></p>]
end

test "render raw text interpolation" do
slime = ~S"""
| test #{{"<>"}}
"""
assert render(slime) == "test <>"
end
end

0 comments on commit fdbd42e

Please sign in to comment.