Current section
Files
Jump to
Current section
Files
lib/slime/parser/preprocessor.ex
defmodule Slime.Parser.Preprocessor do
@moduledoc """
This module helps to process input and insert indents and dedents to simplify parser design.
"""
alias Slime.TemplateSyntaxError
@indent "\x0E"
@dedent "\x0F"
@tabsize Application.get_env(:slime, :tabsize, 2)
def indent_meta_symbol, do: @indent
def dedent_meta_symbol, do: @dedent
def process(input) do
input
|> convert_crlf_to_lf
|> remove_trailing_spaces
|> indent
end
@doc """
Removes trailing whitespace, leaving newlines intact.
"""
def remove_trailing_spaces(input) do
Regex.replace(~r/[ \t]+$/m, input, "")
end
@doc """
Takes an input binary and inserts virtual indent and dedent.
## Examples:
iex> Slime.Parser.Preprocessor.indent("t\\n p")
"t\\n#{@indent} p#{@dedent}"
iex> Slime.Parser.Preprocessor.indent("t\\n p\\n a\\nh")
"t\\n#{@indent} p\\n#{@indent} a#{@dedent}#{@dedent}\\nh"
"""
def indent(input) do
case String.split(input, "\n") do
[line] -> line
lines -> indent(lines, [0], [])
end
end
defp indent([], stack, result) do
dedents = case Enum.count(stack) - 1 do
0 -> ""
dedents -> String.duplicate(@dedent, dedents)
end
(result |> Enum.reverse |> Enum.join("\n")) <> dedents
end
defp indent([line | rest], [current | _] = stack, result) do
indent = indent_size(line, current, rest)
{stack, result} = cond do
current == indent -> {stack, [line | result]}
current < indent -> {[indent | stack], [@indent <> line | result]}
current > indent ->
{dedents, stack} = Enum.split_while(stack, &(&1 > indent))
if consistent_indentation?(indent, stack) do
dedents = String.duplicate(@dedent, Enum.count(dedents))
[prev_line | result] = result
{stack, [line, prev_line <> dedents | result]}
else
raise TemplateSyntaxError,
message: "Malformed indentation",
line: line,
line_number: Enum.count(result) + 1,
column: indent
end
end
{rest, result} = skip_inconsistent_indentation(line, indent, rest, result)
indent(rest, stack, result)
end
defp skip_inconsistent_indentation(line, indent, rest, result) do
cond do
embedded_engine?(line) -> skip_indented_lines(indent, rest, result, false)
broken_code_line?(line) -> skip_broken_code_lines(rest, result)
verbatim_text?(line) -> skip_indented_lines(indent, rest, result, true)
true -> {rest, result}
end
end
defp consistent_indentation?(indent, [next | _]) when next != indent, do: false
defp consistent_indentation?(_, _), do: true
@doc """
Counts indent size by indent string using :tab_size config option
## Examples:
iex> Slime.Parser.Preprocessor.indent_size(" ")
4
iex> Slime.Parser.Preprocessor.indent_size(" \t")
4
iex> Slime.Parser.Preprocessor.indent_size("")
0
"""
def indent_size(spaces) when is_binary(spaces), do: indent_size(String.codepoints(spaces), 0)
def indent_size(spaces) when is_list(spaces), do: indent_size(spaces, 0)
defp indent_size([], result), do: result
defp indent_size([symbol | rest], result) do
size = case symbol do
" " -> 1
"\t" -> @tabsize
end
indent_size(rest, result + size)
end
defp indent_size(line, current, rest) do
case indent_symbols(line) do
nil -> if rest == [], do: 0, else: current
symbols -> indent_size(symbols)
end
end
defp verbatim_text?(line), do: line =~ ~r/^[ \t]*+[|']/
defp embedded_engine?(line), do: line =~ ~r/^[ \t]*+\w+:$/
defp broken_code_line?(line), do: line =~ ~r/^[ \t]*+[=-].*(,|\\)$/
defp broken_code_line_continuation?(line), do: line =~ ~r/[^\\](,|\\)$/
defp skip_indented_lines(indent, lines, result, skip_empty) do
{indented, rest} = Enum.split_while(lines, fn (line) ->
line_empty?(line) || indent_size(line, indent, lines) > indent
end)
if skip_empty && Enum.all?(indented, &line_empty?/1) do
{lines, result}
else
[indent_first_line | indent_rest] = indented
indented = [@indent <> indent_first_line | indent_rest]
{empty_tail, indented} =
indented
|> Enum.reverse
|> Enum.split_while(&(line_empty?(&1)))
[indent_last_line | indent_rest] = indented
{rest, empty_tail ++ [indent_last_line <> @dedent | indent_rest] ++ result}
end
end
defp skip_broken_code_lines(lines, result) do
{broken_lines, [last_line | rest]} = Enum.split_while(lines, fn (line) ->
line_empty?(line) || broken_code_line_continuation?(line)
end)
{rest, [last_line | Enum.reverse(broken_lines)] ++ result}
end
defp line_empty?(line) do
line |> indent_symbols |> is_nil
end
defp indent_symbols(line) do
case Regex.run(~r/^[ \t]*+(?!$|\s)/, line) do
# NOTE: empty line
nil -> nil
[symbols] -> symbols
end
end
defp convert_crlf_to_lf(document) do
String.replace(document, ~r/\r/, "")
end
end