Skip to content
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

Separate interpolation from verbatim text #130

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/slime/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ defmodule Slime.Compiler do
def compile(%HTMLCommentNode{content: content}) do
"<!--" <> compile(content) <> "-->"
end
def compile({:eex, eex}), do: "<%= " <> eex <> "%>"
def compile(raw), do: raw

defp render_attribute({_, []}), do: ""
Expand Down
73 changes: 20 additions & 53 deletions lib/slime/parser/embedded_engine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ defmodule Slime.Parser.EmbeddedEngine do
@type parser_tag :: binary | {:eex | binary, Keyword.t}
@callback render(binary, Keyword.t) :: parser_tag

@embedded_engine_regex ~r/^(?<indent>\s*)(?<engine>\w+):$/
@empty_line_regex ~r/^\s*$/
import Slime.Parser.TextBlock, only: [render_content: 2]

@engines %{
javascript: Slime.Parser.EmbeddedEngine.Javascript,
Expand All @@ -19,51 +18,20 @@ defmodule Slime.Parser.EmbeddedEngine do
|> Enum.into(%{}, fn ({key, value}) -> {to_string(key), value} end)
@registered_engines Map.keys(@engines)

def parse(header, lines) do
case Regex.named_captures(@embedded_engine_regex, header) do
%{"engine" => engine, "indent" => indent} when engine in @registered_engines ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to keep this check, it will be easier to add descriptive error in case of invalid engine name

indent = String.length(indent)
{embedded_lines, rest} = split_lines(lines, indent)
{{indent, render_with_engine(engine, embedded_lines)}, rest}
_ -> nil
end
end
def parse(engine, lines) when engine in @registered_engines do
embedded_text = render_content(lines, 0)

def render_with_engine(engine, lines) when is_list(lines) do
embedded_text = case lines do
[] -> ""
[line | _] ->
strip_indent = indent(line)
lines
|> Enum.map(&strip_line(&1, strip_indent))
|> Enum.join("\n")
end

render_with_engine(engine, embedded_text)
{:ok, render_with_engine(engine, embedded_text)}
end

def render_with_engine(engine, embedded_text) do
keep_lines = Application.get_env(:slime, :keep_lines)
embedded_text = if keep_lines do
"\n" <> embedded_text
else
embedded_text
end
apply(@engines[engine], :render, [embedded_text, [keep_lines: keep_lines]])
def parse(engine, _) do
{:error, ~s(Unknown embedded engine "#{engine}")}
end

defp split_lines(lines, indent_size) do
Enum.split_while(lines, fn (line) ->
line =~ @empty_line_regex || indent_size < indent(line)
end)
end

defp indent(line) do
String.length(line) - String.length(String.lstrip(line))
end
defp render_with_engine(engine, text) do
keep_lines = Application.get_env(:slime, :keep_lines)
text = if keep_lines, do: ["\n" | text], else: text

defp strip_line(line, strip_indent) do
String.slice(line, min(strip_indent, indent(line))..-1)
apply(@engines[engine], :render, [text, [keep_lines: keep_lines]])
end
end

Expand All @@ -73,11 +41,8 @@ defmodule Slime.Parser.EmbeddedEngine.Javascript do
"""

@behaviour Slime.Parser.EmbeddedEngine
import Slime.Parser, only: [parse_eex_string: 1]

def render(text, _options) do
{"script", children: [parse_eex_string(text)]}
end
def render(text, _options), do: {"script", children: text}
end

defmodule Slime.Parser.EmbeddedEngine.Css do
Expand All @@ -86,10 +51,9 @@ defmodule Slime.Parser.EmbeddedEngine.Css do
"""

@behaviour Slime.Parser.EmbeddedEngine
import Slime.Parser, only: [parse_eex_string: 1]

def render(text, _options) do
{"style", attributes: [type: "text/css"], children: [parse_eex_string(text)]}
{"style", attributes: [type: "text/css"], children: text}
end
end

Expand All @@ -104,13 +68,18 @@ defmodule Slime.Parser.EmbeddedEngine.Elixir do

def render(text, options) do
newlines = if options[:keep_lines] do
count = text |> String.split("\n") |> length |> Kernel.-(1)
count = Enum.count(text, &Kernel.==(&1, "\n"))
[String.duplicate("\n", count)]
else
[]
end

%EExNode{content: text, children: newlines}
eex = Enum.map_join(text, fn
({:eex, interpolation}) -> ~S"#{" <> interpolation <> "}"
(text) -> text
end)

%EExNode{content: eex, children: newlines}
end
end

Expand All @@ -121,7 +90,5 @@ defmodule Slime.Parser.EmbeddedEngine.EEx do

@behaviour Slime.Parser.EmbeddedEngine

def render(text, _options) do
text
end
def render(text, _options), do: text
end
52 changes: 20 additions & 32 deletions lib/slime/parser/text_block.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ defmodule Slime.Parser.TextBlock do
Utilities for parsing text blocks.
"

import Slime.Parser.Transform, only: [wrap_in_quotes: 1]
alias Slime.Parser.Nodes.EExNode

@doc """
Given a text block and its declaration indentation level (see below),
produces a string (or a dynamic EEx tuple) that can be inserted into the tree.
Expand All @@ -19,51 +16,42 @@ defmodule Slime.Parser.TextBlock do
"""
def render_content(lines, declaration_indent) do
lines = case lines do
[{_, "", _} | rest] -> rest
[{relative_indent, first_line, is_eex} | rest] ->
[{_, []} | rest] -> rest
[{relative_indent, first_line_contents} | rest] ->
first_line_indent = relative_indent + declaration_indent
[{first_line_indent, first_line, is_eex} | rest]
[{first_line_indent, first_line_contents} | rest]
end

text_indent = Enum.find_value(lines, 0,
fn({indent, line, _}) -> line != "" && indent end)
fn({indent, line_contents}) -> !Enum.empty?(line_contents) && indent end)

lines
|> insert_line_spacing(text_indent)
|> wrap_text
insert_line_spacing(lines, text_indent)
end

@doc """
Given a text block, returns the text without indentation.
"""
def render_without_indentation(lines) do
lines
|> skip_line_spacing
|> wrap_text
end

defp wrap_text({text, is_eex}) do
if is_eex do
[%EExNode{content: wrap_in_quotes(text), output: true}]
else
[text]
end
concat_lines(lines,
fn({_line_indent, line_contents}, content) ->
["\n" | line_contents ++ content]
end)
end

defp insert_line_spacing(lines, text_indent) do
lines |> Enum.reduce({"", false},
fn ({line_indent, line, is_eex_line}, {text, is_eex}) ->
text = if text == "", do: text, else: text <> "\n"
leading_space = String.duplicate(" ", line_indent - text_indent)
{text <> leading_space <> line, is_eex || is_eex_line}
concat_lines(lines,
fn({line_indent, line_contents}, content) ->
leading_space = String.duplicate(" ", max(0, line_indent - text_indent))
case leading_space do
"" -> ["\n" | line_contents ++ content]
_ -> ["\n" | [leading_space | line_contents ++ content]]
end
end)
end

defp skip_line_spacing(lines) do
lines |> Enum.reduce({"", false},
fn ({_, line, is_eex_line}, {text, is_eex}) ->
text = if text == "", do: text, else: text <> "\n"
{text <> line, is_eex || is_eex_line}
end)
defp concat_lines([], _), do: []
defp concat_lines(lines, concat_function) do
[_leading_newline | content] = List.foldr(lines, [], concat_function)
content
end
end
74 changes: 25 additions & 49 deletions lib/slime/parser/transform.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ defmodule Slime.Parser.Transform do
alias Slime.Parser.Nodes.InlineHTMLNode
alias Slime.Parser.Nodes.DoctypeNode

alias Slime.TemplateSyntaxError

@default_tag Application.get_env(:slime, :default_tag, "div")
@sort_attrs Application.get_env(:slime, :sort_attrs, true)
@merge_attrs Application.get_env(:slime, :merge_attrs, %{"class" => " "})
Expand All @@ -25,9 +27,6 @@ defmodule Slime.Parser.Transform do
"#" => %{attr: "id"}
})

# TODO: separate dynamic elixir blocks by parser
@quote_outside_interpolation_regex ~r/(^|\G)(?:\\.|[^#]|#(?!\{)|(?<pn>#\{(?:[^"}]++|"(?:\\.|[^"#]|#(?!\{)|(?&pn))*")*\}))*?\K"/u
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RIP


@type ast :: term
@type index :: {{:line, non_neg_integer}, {:column, non_neg_integer}}

Expand Down Expand Up @@ -99,6 +98,13 @@ defmodule Slime.Parser.Transform do
content: TextBlock.render_without_indentation(text)}
end

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

def transform(:html_comment, input, _index) do
indent = indent_size(input[:indent])
decl_indent = indent + String.length(input[:type])
Expand Down Expand Up @@ -138,40 +144,30 @@ defmodule Slime.Parser.Transform do
end
end

def transform(:text_block_line, input, _index) do
[space, line] = input
indent = indent_size(space)
case line do
{:simple, content} -> {indent, to_string(content), false}
{:dynamic, content} -> {indent, to_string(content), true}
end
end

def transform(:embedded_engine, [engine, _, lines], _index) do
lines = case lines do
{:empty, _} -> ""
_ -> List.flatten(lines[:lines])
end
case EmbeddedEngine.render_with_engine(engine, lines) do
{tag, content} -> %HTMLNode{name: tag,
attributes: (content[:attributes] || []),
children: content[:children]}
content -> content
def transform(:embedded_engine, [engine, _, content], index) do
case EmbeddedEngine.parse(engine, content[:lines]) do
{:ok, {tag, content}} ->
%HTMLNode{name: tag,
attributes: (content[:attributes] || []),
children: content[:children]}
{:ok, content} -> content
{:error, message} ->
{{:line, line_number}, {:column, column}} = index
raise TemplateSyntaxError, message: message,
line: "", line_number: line_number, column: column
end
end

def transform(:embedded_engine_lines, input, _index) do
[line, rest] = input
lines = Enum.map(rest, fn ([_, lines]) -> lines end)
[line | lines]
def transform(:embedded_engine_lines, [first_line, rest], _index) do
[first_line | Enum.map(rest, fn ([_, lines]) -> lines end)]
end

def transform(:embedded_engine_line, input, _index) do
to_string(input)
def transform(:indented_text_line, [space, content], _index) do
{indent_size(space), content}
end

def transform(:inline_html, [_, content, children], _index) do
%InlineHTMLNode{content: [content], children: children}
%InlineHTMLNode{content: content, children: children}
end

def transform(:code, input, _index) do
Expand Down Expand Up @@ -202,14 +198,6 @@ 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(:text_content, input, _index) do
case input do
{:dynamic, content} ->
%EExNode{content: content |> to_string |> wrap_in_quotes, output: true}
{:simple, content} -> content
end
end

def transform(:dynamic_content, input, _index) do
content = input |> Enum.at(3) |> to_string
%EExNode{content: content, output: true}
Expand Down Expand Up @@ -275,30 +263,18 @@ defmodule Slime.Parser.Transform do
end
end

def transform(:text, input, _index), do: to_string(input)
def transform(:tag_name, input, _index), do: to_string(input)
def transform(:attribute_name, input, _index), do: to_string(input)
def transform(:crlf, input, _index), do: to_string(input)
def transform(_symdol, input, _index), do: input

def remove_empty_lines(lines) do
Enum.filter(lines, fn
({0, ""}) -> false
(_) -> true
end)
end

def expand_tag_shortcut(tag) do
case Map.fetch(@shortcut, tag) do
:error -> {tag, []}
{:ok, spec} -> expand_shortcut(spec, tag)
end
end

def wrap_in_quotes(content) do
~s("#{String.replace(content, @quote_outside_interpolation_regex, ~S(\\"))}")
end

defp expand_attr_shortcut(type, value) do
spec = Map.fetch!(@shortcut, type)
expand_shortcut(spec, value)
Expand Down
Loading