diff --git a/bench/example_parse_bench.exs b/bench/example_parse_bench.exs deleted file mode 100644 index 9689384..0000000 --- a/bench/example_parse_bench.exs +++ /dev/null @@ -1,28 +0,0 @@ -defmodule ExampleParseBench do - use Benchfella - - @slime ~S""" - doctype html - html - head - meta name="keywords" description="Slime" - title = site_title - javascript: - alert('Slime supports embedded javascript!'); - body - #id.class - ul - = Enum.map [1, 2], fn x -> - li = x - """ - - bench "new parse" do - Slime.Parser.parse(@slime) - end - - bench "old ways" do - @slime - |> Slime.Preprocessor.process - |> Slime.Parser.parse_lines - end -end diff --git a/bench/performance_bottlenecks_bench.exs b/bench/performance_bottlenecks_bench.exs deleted file mode 100644 index 8c06c06..0000000 --- a/bench/performance_bottlenecks_bench.exs +++ /dev/null @@ -1,15 +0,0 @@ -defmodule PerformanceBottlenecksBench do - use Benchfella - - @slime ~S(h2 Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do #{"eiusmod"}) - bench "interpolation performance on long lines" do - Slime.Parser.parse_line(@slime) - end - - @slime """ - a.social__link.facebook itemprop="sameAs" href="http://www.facebook.com/test" title="Facebook" target="_blank" - """ - bench "inline tag split performance on attributes with :" do - Slime.Preprocessor.process(@slime) - end -end diff --git a/lib/slime/parser.ex b/lib/slime/parser.ex index 3728da7..6e040d9 100644 --- a/lib/slime/parser.ex +++ b/lib/slime/parser.ex @@ -3,9 +3,6 @@ defmodule Slime.Parser do Build a Slime tree from a Slime document. """ - alias Slime.Doctype - alias Slime.Parser.AttributesKeyword - alias Slime.Parser.EmbeddedEngine alias Slime.Parser.Preprocessor alias Slime.TemplateSyntaxError @@ -34,83 +31,8 @@ defmodule Slime.Parser do column: column end - @content "|" - @comment "/" - @html "<" - @preserved"'" - @script "-" - @smart "=" - - @lint false - @attr_delim_regex ~r/[ ]+(?=([^"]*"[^"]*")*[^"]*$)(?=(?:[^ "']+=|(?:allowfullscreen|async|autofocus|autoplay|checked|compact|controls|declare|default|defaultchecked|defaultmuted|defaultselected|defer|disabled|draggable|enabled|formnovalidate|hidden|indeterminate|inert|ismap|itemscope|loop|multiple|muted|nohref|noresize|noshade|novalidate|nowrap|open|pauseonexit|readonly|required|reversed|scoped|seamless|selected|sortable|spellcheck|translate|truespeed|typemustmatch|visible)(?: |$)))/ - @attr_list_delims Application.get_env(:slime, :attr_list_delims, %{"{" => "}", "[" => "]", "(" => ")"}) - @attr_group_regex ~r/(?:\s*[\w-]+\s*=\s*(?:[^"'].*?(?= [^ "']+=|$)|"(?:(?\{(?:[^{}]|\g)*\})|[^"])*"|'[^']*'))*/ - - @parse_line_split_regexes @attr_list_delims - |> Map.keys - |> Enum.map(&("|\\" <> &1)) - @parse_line_split_regexes ["\\s|="] ++ @parse_line_split_regexes - @parse_line_split_regex @parse_line_split_regexes - |> Enum.join("") - |> Regex.compile! - - @tag_regex ~r/\A(?[\w-]*)?(?(?:[\.|#][\w-]*)*)?(?\<)?(?\>)?/ - @id_regex ~r/(?:#(?[\w-]*))/ r = ~r/(^|\G)(?:\\.|[^#]|#(?!\{)|(?#\{(?:[^"\}]++|"(?:\\.|[^"#]|#(?!\{)|(?&pn))*")*\}))*?\K"/u @quote_outside_interpolation_regex r - @verbatim_text_regex ~r/^(\s*)([#{@content}#{@preserved}])\s?/ - @eex_line_regex ~r/^(\s*)(-|=|==)(?\<)?(?\>)?\s*(.*?)$/ - - @merge_attrs %{class: " "} - - def parse_lines(lines, acc \\ []) - - def parse_lines([], result), do: Enum.reverse(result) - def parse_lines([head | tail], result) do - parsed_result = - EmbeddedEngine.parse(head, tail) || - parse_verbatim_text(head, tail) || - parse_eex_lines(head, tail) || - parse_line(head) - - case parsed_result do - nil -> - parse_lines(tail, result) - - {text, rest} when is_list(rest) -> - parse_lines(rest, [text | result]) - - text -> - parse_lines(tail, [text | result]) - end - end - - @inline_tag_regex ~r/\A(?(?:[\.#]?[\w-]+)++):[^\w\.#]*(?.*)/ - - def parse_line(line) do - case strip_line(line) do - {_indentation, ""} -> - if Application.get_env(:slime, :keep_lines), do: {:prev, ""}, else: nil - {indentation, line} -> - [tag, rest] = - case Regex.run(@inline_tag_regex, line, capture: :all_but_first) do - nil -> [line, nil] - match -> match - end - - parse_tag = fn (tag) -> tag |> String.first |> parse_line(tag) end - tag = parse_tag.(tag) - tag = if rest do - {0, rest} = parse_line(rest) - {tag_name, attrs} = tag - {tag_name, Keyword.put(attrs, :children, [rest])} - else - tag - end - - {indentation, tag} - end - end def parse_eex_string(input) do if String.contains?(input, "\#{") do @@ -120,238 +42,4 @@ defmodule Slime.Parser do input end end - - defp attribute_key(key), do: key |> String.strip |> String.to_atom - defp attribute_val(~s'"' <> value) do - value - |> String.strip - |> String.slice(0..-2) - |> parse_eex_string - end - defp attribute_val(value), do: parse_eex(value, true) - - defp css_classes(""), do: [] - defp css_classes(input) do - [""|t] = String.split(input, ".") - [class: t] - end - - defp html_attribute(key, value) do - key = attribute_key(key) - value = attribute_val(value) - - [{key, value}] - end - - defp html_id(""), do: [] - defp html_id(id), do: [id: id] - - defp parse_comment("!" <> comment), do: {:html_comment, children: [String.strip(comment)]} - defp parse_comment("[" <> comment) do - [h|[t|_]] = comment |> String.split("]", parts: 2) - conditions = String.strip(h) - children = t |> String.strip |> parse_inline - {:ie_comment, content: conditions, children: children} - end - defp parse_comment(_comment), do: "" - - defp parse_eex(input, inline \\ false) do - input = String.lstrip(input) - script = input - |> String.split(~r/^(-|==|=)/) - |> List.last - |> String.lstrip - inline = inline or String.starts_with?(input, "=") - {:eex, content: script, inline: inline} - end - - defp parse_attributes(""), do: {"", []} - - defp parse_attributes(line) do - delim = String.first(line) - if Map.has_key?(@attr_list_delims , delim) do - line = String.slice(line, 1..-1) - parse_wrapped_attributes(line, @attr_list_delims[delim]) - else - match = @attr_group_regex |> Regex.run(line) |> List.first - offset = String.length(match) - {attrs, rem} = line |> String.split_at(offset) - attrs = parse_attributes(attrs, []) - {rem, attrs} - end - end - - defp parse_attributes("", acc) do - acc - end - defp parse_attributes(line, acc) when is_binary(line) do - line - |> String.split(@attr_delim_regex) - |> parse_attributes(acc) - end - defp parse_attributes([], acc) do - acc - end - defp parse_attributes([head|tail], acc) do - parts = String.split(head, ~r/=/, parts: 2) - attr = case parts do - [key, value] -> html_attribute(key, value) - [key] -> html_attribute(key, "true") - _ -> [] - end - parse_attributes(tail, attr ++ acc) - end - - defp parse_inline(""), do: [] - defp parse_inline(@smart <> content) do - content - |> parse_eex(true) - |> List.wrap - end - defp parse_inline(input) do - input - |> String.strip(?") - |> parse_eex_string - |> List.wrap - end - - defp parse_line("", _line), do: "" - defp parse_line(@content, line), do: line |> String.slice(1..-1) |> String.strip |> parse_eex_string - defp parse_line(@comment, line), do: line |> String.slice(1..-1) |> parse_comment - defp parse_line(@html, line), do: line |> String.strip |> parse_eex_string - defp parse_line(@preserved, line), do: line |> String.slice(1..-1) |> parse_eex_string - defp parse_line(@script, line), do: parse_eex(line) - defp parse_line(@smart, line), do: parse_eex(line, true) - - defp parse_line(_, "doctype " <> type) do - value = Doctype.for(type) - {:doctype, value} - end - - defp parse_line(_, line) do - line = String.strip(line) - offset = case Regex.run(@parse_line_split_regex, line, return: :index) do - [{index, _}] -> index - nil -> String.length(line) - end - - {head, tail} = String.split_at(line, offset) - {tag, basics, spaces} = parse_tag(head) - - tail = String.lstrip(tail) - - {children, attributes, close} = case parse_attributes(tail) do - {"/", attributes} -> - {[], Enum.reverse(attributes), true} - {rem, attributes} -> - children = rem - |> String.strip - |> parse_inline - {children, Enum.reverse(attributes), false} - end - - attributes = AttributesKeyword.merge(basics ++ attributes, @merge_attrs) - {tag, attributes: attributes, children: children, spaces: spaces, close: close} - end - - defp parse_tag(line) do - parts = Regex.named_captures(@tag_regex, line) - - tag = case parts["tag"] do - "" -> "div" - tag -> tag - end - - spaces = %{} - spaces = if parts["leading_space"] != "", do: Map.put(spaces, :leading, true), else: spaces - spaces = if parts["trailing_space"] != "", do: Map.put(spaces, :trailing, true), else: spaces - - case Regex.named_captures(@id_regex, parts["css"]) do - nil -> - {tag, css_classes(parts["css"]) ++ html_id(""), spaces} - capture -> - css_classes = String.replace(parts["css"], "#" <> capture["id"], "") - {tag, css_classes(css_classes) ++ html_id(capture["id"]), spaces} - end - end - - defp parse_verbatim_text(head, tail) do - case Regex.run(@verbatim_text_regex, head) do - nil -> - nil - - [text_indent, indent, text_type] -> - indent = String.length(indent) - text_indent = String.length(text_indent) - {text_lines, rest} = parse_verbatim_text(indent, text_indent, head, tail) - text = Enum.join(text_lines, "\n") - text = if text_type == @preserved, do: text <> " ", else: text - {{indent, parse_eex_string(text)}, rest} - end - end - - defp parse_verbatim_text(indent, text_indent, head, tail) do - text_indent = if String.length(head) == text_indent, do: text_indent + 1, else: text_indent - {_, head_text} = String.split_at(head, text_indent) - {text_lines, rest} = Enum.split_while(tail, fn (line) -> - {line_indent, _} = strip_line(line) - indent < line_indent - end) - text_lines = Enum.map(text_lines, fn (line) -> - {_, text} = String.split_at(line, text_indent) - text - end) - text_lines = if head_text == "", do: text_lines, else: [head_text | text_lines] - {text_lines, rest} - end - - defp parse_eex_lines(head, tail) do - {indent, head} = strip_line(head) - - case Regex.run(@eex_line_regex, head, capture: :all_but_first) do - nil -> - nil - - [_, delim, leading_space, trailing_space, content] -> - {content, rest} = slurp_eex_lines("", [content | tail]) - inline? = @smart == String.first delim - - spaces = %{} - spaces = if leading_space != "", do: Map.put(spaces, :leading, true), else: spaces - spaces = if trailing_space != "", do: Map.put(spaces, :trailing, true), else: spaces - - {{indent, {:eex, content: content, inline: inline?, spaces: spaces}}, rest} - end - end - - defp slurp_eex_lines(content, [head | tail]) do - content = content <> head - - if String.last(head) in [",", "\\"] do - slurp_eex_lines(content <> "\n", tail) - else - {content, tail} - end - end - - defp parse_wrapped_attributes(line, delim) do - unless String.contains?(line, delim) do - raise Slime.TemplateSyntaxError, message: ~s(Can't find matching delimiter "#{delim}" in line "#{line}") - end - [attrs, rem] = line - |> String.strip - |> String.split(delim, parts: 2) - attributes = parse_attributes(attrs, []) - {rem, attributes} - end - - defp strip_line(line) do - orig_len = String.length(line) - trimmed = String.lstrip(line) - trim_len = String.length(trimmed) - - offset = if trimmed == "- else", do: 2, else: 0 - {orig_len - trim_len + offset, trimmed} - end - end diff --git a/lib/slime/preprocessor.ex b/lib/slime/preprocessor.ex deleted file mode 100644 index a7858e7..0000000 --- a/lib/slime/preprocessor.ex +++ /dev/null @@ -1,33 +0,0 @@ -defmodule Slime.Preprocessor do - @moduledoc """ - In order to make parsing Slime documents easier we run some simple - transformations on the document to standardise the format. - """ - - @tabsize 2 - @soft_tab String.duplicate(" ", @tabsize) - - def process(document) do - document - |> expand_tabs - |> remove_trailing_newlines - |> convert_crlf_to_lf - |> split_into_lines - end - - defp expand_tabs(document) do - String.replace(document, ~r/\t/m, @soft_tab) - end - - defp remove_trailing_newlines(document) do - String.replace(document, ~r/\n+\z/m, "") - end - - defp split_into_lines(document) do - String.split(document, "\n") - end - - defp convert_crlf_to_lf(document) do - String.replace(document, ~r/\r/, "") - end -end diff --git a/test/preprocessor_test.exs b/test/preprocessor_test.exs deleted file mode 100644 index e9c3758..0000000 --- a/test/preprocessor_test.exs +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Slime.PreprocessorTest do - use ExUnit.Case - - import Slime.Preprocessor, only: [process: 1] - - test "documents are split into lines" do - result = """ - h1 Hi - h2 Bye - """ |> process - assert result == [ - "h1 Hi", - " h2 Bye", - ] - end - - test "hard tabs are expanded" do - result = """ - h1 Hi - \th2 Bye - \t\th3 Hi - """ |> process - assert result == [ - "h1 Hi", - " h2 Bye", - " h3 Hi", - ] - end - - test " CRLF line endings are converted to LF" do - result = "h1 Hi\r\n\th2 Bye\r\n\t\th3 Hi\r\n" - |> process - assert result == [ - "h1 Hi", - " h2 Bye", - " h3 Hi", - ] - end -end diff --git a/test/renderer_test.exs b/test/renderer_test.exs index 5b48524..3fe9c3c 100644 --- a/test/renderer_test.exs +++ b/test/renderer_test.exs @@ -119,4 +119,8 @@ defmodule RendererTest do """ assert render(slime, a: 2) == "

1

" end + + test "CRLF line endings are converted to LF" do + assert render("h1 Hi\r\n\th2 Bye\r\n\t\th3 Hi\r\n") == "

Hi

Bye

Hi

" + end end