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

Multiline comments #126

Merged
merged 6 commits into from
May 23, 2017
Merged
Show file tree
Hide file tree
Changes from all 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
49 changes: 49 additions & 0 deletions lib/slime/parser/text_block.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule Slime.Parser.TextBlock do
@moduledoc "
Utilities for parsing text blocks.
"

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

@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.

nested
| Text block
that spans over multiple lines
---
^
declaration indent
"""
def render(lines, declaration_indent, trailing_whitespace \\ "") do
lines = case lines do
[{_, "", _} | rest] -> rest
[{relative_indent, first_line, is_eex} | rest] ->
first_line_indent = relative_indent + declaration_indent
[{first_line_indent, first_line, is_eex} | rest]
end

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

{text, is_eex} = insert_line_spacing(lines, text_indent)

text = text <> trailing_whitespace

if is_eex do
{:eex, content: wrap_in_quotes(text), inline: true}
else
text
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}
end)
end
end
84 changes: 31 additions & 53 deletions lib/slime/parser/transform.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Slime.Parser.Transform do
import Slime.Parser.Preprocessor, only: [indent_size: 1]
alias Slime.Parser.AttributesKeyword
alias Slime.Parser.EmbeddedEngine
alias Slime.Parser.TextBlock
alias Slime.Doctype

@default_tag Application.get_env(:slime, :default_tag, "div")
Expand Down Expand Up @@ -72,7 +73,11 @@ defmodule Slime.Parser.Transform do
end

def transform(:html_comment, input, _index) do
{:html_comment, children: [to_string(Enum.at(input, 2))]}
indent = indent_size(input[:indent])
decl_indent = indent + String.length(input[:type])
text = TextBlock.render(input[:content], decl_indent)

{indent, {:html_comment, children: [text]}}
end

def transform(:ie_comment, input, _index) do
Expand All @@ -86,66 +91,39 @@ defmodule Slime.Parser.Transform do

def transform(:verbatim_text, input, _index) do
indent = indent_size(input[:indent])
relative_text_indent = indent_size(input[:space])
text_indent = indent + relative_text_indent + String.length(input[:type])
[{first_line, is_eex_line} | rest] = input[:content]
text_indent = text_indent + if first_line == "" && relative_text_indent == 0, do: 1, else: 0
content = [{text_indent, first_line, is_eex_line} | rest]
shift_indent = content |> Enum.map(&elem(&1, 0)) |> Enum.min
shift_indent = if text_indent == shift_indent do
relative_text_indent = if relative_text_indent == 0, do: 0, else: relative_text_indent - 1
text_indent - relative_text_indent
else
shift_indent
end
{content, is_eex} = Enum.reduce(content, {"", false},
fn ({line_indent, line, is_eex_line}, {result, is_eex}) ->
result = if result == "", do: result, else: result <> "\n"
result_line_indent = String.duplicate(" ", line_indent - shift_indent)
{
result <> result_line_indent <> line,
is_eex || is_eex_line
}
end
)

content = if input[:type] == "'", do: content <> " ", else: content
content = if is_eex do
{:eex, content: wrap_in_quotes(content), inline: true}
else
content
end
{indent, content}
decl_indent = indent + String.length(input[:type])
trailing_whitespace = if input[:type] == "'", do: " ", else: ""
text = TextBlock.render(input[:content], decl_indent, trailing_whitespace)

{indent, text}
end

def transform(:verbatim_text_lines, input, _index) do
def transform(:text_block, input, _index) do
case input do
[line, []] -> [line]
[line, nested_lines] ->
lines = nested_lines[:lines]
spaces = indent_size(nested_lines[:space])
[{first_line, is_eex} | rest] = lines
[line, {spaces, first_line, is_eex} | rest]
[line, nested_lines] -> [line | nested_lines[:lines]]
end
end

def transform(:verbatim_text_nested_lines, input, _index) do
def transform(:text_block_nested_lines, input, _index) do
case input do
[line, []] -> [line]
[line, lines] ->
lines = Enum.flat_map(lines, fn ([_, line]) ->
[{first_line, is_eex} | rest] = line[:lines]
[{indent_size(line[:space]), first_line, is_eex} | rest]
end)
[line | lines]
[line, nested_lines] ->
[line | Enum.flat_map(nested_lines, fn([_crlf, l]) ->
case l do
[_indent, nested, _dedent] -> nested
nested -> nested
end
end)]
end
end

def transform(:verbatim_text_line, input, _index) do
case input do
"" -> {"", false}
{:simple, content} -> {to_string(content), false}
{:dynamic, content} -> {to_string(content), true}
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

Expand Down Expand Up @@ -347,6 +325,10 @@ defmodule Slime.Parser.Transform do
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 All @@ -361,8 +343,4 @@ defmodule Slime.Parser.Transform do
final_attrs = Enum.concat(attrs, Map.get(spec, :additional_attrs, []))
{spec[:tag], final_attrs}
end

defp wrap_in_quotes(content) do
~s("#{String.replace(content, @quote_outside_interpolation_regex, ~S(\\"))}")
end
end
26 changes: 13 additions & 13 deletions src/slime_parser.peg.eex
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ doctype <- space? 'doctype' space (!eol .)+ eol;
% Also remove dedent from eol definition and modify transforms accordingly
tags_list <- tag (crlf indent? empty_lines:(space? crlf indent?)* tag:tag dedent*)*;

% NOTE: We have to track leading spaces for verbatim_text, so it handled separately
tag <- verbatim_text / space? tag_item / blank:(space? &eol);
% NOTE: Leading whitespace is handled separately for comments and verbatim text
tag <- comment / verbatim_text / space? tag_item / blank:(space? &eol);

tag_item <- (embedded_engine / comment / inline_html / code / html_tag);
tag_item <- (embedded_engine / inline_html / code / html_tag);

inline_html <- &'<' text_content;

Expand Down Expand Up @@ -83,22 +83,22 @@ braces <- '{' (braces / !'}' .)* '}';

interpolation <- '#{' (string / string_with_interpolation / !'}' .)* '}';

comment <- html_comment / ie_comment / code_comment;
comment <- html_comment / space? ie_comment / space? code_comment;

html_comment <- '/!' space? (!eol .)*;
html_comment <- indent:space? type:'/!' content:text_block;

ie_comment <- '/[' condition:(!']' .)+ ']' space? content:(!eol .)*;

% TODO: support multi-line
code_comment <- '/' (!eol .)*;
code_comment <- '/' text_block;

verbatim_text <- indent:space? type:[|'] space:space? content:verbatim_text_lines;
verbatim_text_lines <- verbatim_text_line (crlf indent space:space lines:verbatim_text_nested_lines dedent)?;
verbatim_text_nested_lines <- verbatim_text_line (crlf (
indent space:space lines:verbatim_text_nested_lines dedent /
space:space lines:verbatim_text_nested_lines
verbatim_text <- indent:space? type:[|'] content:text_block;

text_block <- text_block_line (crlf
indent lines:text_block_nested_lines dedent)?;
text_block_nested_lines <- text_block_line (crlf (
indent text_block_nested_lines dedent / text_block_nested_lines
))*;
verbatim_text_line <- dynamic:text_with_interpolation / simple:text / '';
text_block_line <- space? (dynamic:text_with_interpolation / simple:text);

embedded_engine <- tag_name ':' (
crlf indent lines:embedded_engine_lines dedent / empty:('' &eol)
Expand Down
36 changes: 33 additions & 3 deletions test/rendering/comments_test.exs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
defmodule RenderCommentsTest do
use ExUnit.Case, async: true

import Slime, only: [render: 1]
import Slime, only: [render: 1, render: 2]

test "lines started with / are commented out" do
test "lines indented deeper than / are commented out" do
slime = """
/ code comment
/ Code comment
that spans over multiple lines
/
Code comment started
on another line
p.test
/ One-liner
"""
assert render(slime) == ~s(<p class="test"></p>)
end
Expand All @@ -15,6 +20,31 @@ defmodule RenderCommentsTest do
assert render(~s(/! html comment)) == ~s(<!--html comment-->)
end

test "/! can span over multiple lines" do
slime = """
div
/!
HTML comments
/! Have similar semantics
to other text blocks:
they can be nested, with indentation being converted to spaces
"""
html = """
<div><!--HTML comments--><!--Have similar semantics
to other text blocks:
they can be nested, with indentation being converted to spaces--></div>
""" |> String.strip(?\n)
assert render(slime) == html
end

test "/! renders comments with interpolation" do
slime = ~S(/! html comment with #{interpolation})
html = """
<!--html comment with a-->
""" |> String.strip(?\n)
assert render(slime, interpolation: "a") == html
end

test "/![foo] renders IE comments" do
assert render(~s(/[if IE] html comment)) == ~s(<!--[if IE]>html comment<![endif]-->)
end
Expand Down
10 changes: 10 additions & 0 deletions test/rendering/text_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ defmodule RenderTextTest do
assert render(slime) == html
end

test "' without any text inserts whitespace" do
slime = """
p
| Text
'
| with space
"""
assert render(slime) == "<p>Text with space</p>"
end

test "render multiline varbatim text with empty first line" do
slime = """
p
Expand Down