Skip to content

Commit

Permalink
Fixes #406 (#437)
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertDober authored Oct 27, 2021
1 parent 2c6f435 commit 7e5dd49
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 54 deletions.
113 changes: 61 additions & 52 deletions lib/earmark/transform.ex
Original file line number Diff line number Diff line change
Expand Up @@ -195,14 +195,15 @@
@doc """
Transforms an AST to html, also accepts the result of `map_ast_with` for convenience
"""
def transform(ast, options \\ %{initial_indent: 0, indent: 2})
def transform(ast, options \\ %{initial_indent: 0, indent: 2, compact_output: false})
def transform({ast, _}, options), do: transform(ast, options)
def transform(ast, options) when is_list(options) do
transform(ast, options|>Enum.into(%{initial_indent: 0, indent: 2}))
transform(ast, options|>Enum.into(%{initial_indent: 0, indent: 2, compact_output: false}))
end
def transform(ast, options) when is_map(options) do
options1 = options
|> Map.put_new(:indent, 2)
|> Map.put_new(:compact_output, false)
ast
# |> IO.inspect
|> _maybe_remove_paras(options1)
Expand Down Expand Up @@ -266,61 +267,74 @@
end
end

defp maybe_add_newline(options)
defp maybe_add_newline(%Options{compact_output: true}), do: []
defp maybe_add_newline(_), do: ?\n
defp _maybe_add_newline1(options)
defp _maybe_add_newline1(%Options{compact_output: true}), do: []
defp _maybe_add_newline1(_), do: ?\n

@crlf_rgx ~r{(?:\n\r?)+}
defp _maybe_compact(element, options)
defp _maybe_compact(element, %{compact_output: false}), do: element
defp _maybe_compact(element, _options) do
String.replace(element, @crlf_rgx, " ")
end

defp to_html(ast, options) do
_to_html(ast, options, Map.get(options, :initial_indent, 0))|> IO.iodata_to_binary
_to_html(ast, options, Map.get(options, :initial_indent, 0)) |> IO.iodata_to_binary
end

defp _to_html(ast, options, level, verbatim \\ false)
defp _to_html({:comment, _, content, _}, options, _level, _verbatim) do
["<!--", Enum.intersperse(content, ?\n), "-->", maybe_add_newline(options)]
["<!--", Enum.intersperse(content, ?\n), "-->", _maybe_add_newline1(options)]
end
defp _to_html({"code", atts, children, meta}, options, level, _verbatim) do
verbatim = meta |> Map.get(:verbatim, false)
[ open_tag("code", atts),
[ _open_tag1("code", atts),
_to_html(children, Map.put(options, :smartypants, false), level, verbatim),
"</code>"]
end
defp _to_html({tag, atts, children, _}, options, level, verbatim) when tag in @compact_tags do
[open_tag(tag, atts),
[_open_tag1(tag, atts),
children
|> Enum.map(&_to_html(&1, options, level, verbatim)),
"</", tag, ?>]
end
defp _to_html({tag, atts, _, _}, options, level, _verbatim) when tag in @void_elements do
[ make_indent(options, level), open_tag(tag, atts), maybe_add_newline(options) ]
[ make_indent(options, level), _open_tag1(tag, atts), _maybe_add_newline1(options) ]
end
defp _to_html(elements, options, level, verbatim) when is_list(elements) do
elements
|> Enum.map(&_to_html(&1, options, level, verbatim))
end
defp _to_html(element, options, _level, false) when is_binary(element) do
escape(element, options)
element
|> _maybe_compact(options)
|> escape(options)
end
defp _to_html(element, options, level, true) when is_binary(element) do
[make_indent(options, level), element]
end
defp _to_html({"pre", atts, children, meta}, options, level, _verbatim) do
verbatim = meta |> Map.get(:verbatim, false)
[ make_indent(options, level),
open_tag("pre", atts),
_to_html(children, Map.put(options, :smartypants, false), level, verbatim),
"</pre>", maybe_add_newline(options)]
_open_tag1("pre", atts),
_to_html(children, Map.merge(options, %{smartypants: false, compact_output: false}), level, verbatim),
"</pre>", _maybe_add_newline1(options)]
end
defp _to_html({tag, atts, children, meta}, options, level, _verbatim) do
verbatim = meta |> Map.get(:verbatim, false)
[ make_indent(options, level),
open_tag(tag, atts),
maybe_add_newline(options),
_open_tag1(tag, atts),
_maybe_add_newline1(options),
_to_html(children, options, level+1, verbatim),
close_tag(tag, options, level)]
_close_tag1(tag, options, level)]
end

defp close_tag(tag, options, level) do
[make_indent(options, level), "</", tag, ?>, maybe_add_newline(options)]
defp _add_trailing_nl(node)
defp _add_trailing_nl(text) when is_binary(text), do: [text, "\n"]
defp _add_trailing_nl(node), do: node

defp _close_tag1(tag, options, level) do
[make_indent(options, level), "</", tag, ?>, _maybe_add_newline1(options)]
end

defp escape(element, options)
Expand All @@ -332,7 +346,7 @@
@dbl2_rgx ~r{(^|[-–—/\(\[\{\s])\"}
defp escape(element, %{smartypants: true} = options) do
# Unfortunately these regexes still have to be left.
# It doesn't seem possible to make escape_to_iodata
# It doesn't seem possible to make _escape_to_iodata1
# transform, for example, "--'" to "–‘" without
# significantly complicating the code to the point
# it outweights the performance benefit.
Expand All @@ -342,19 +356,19 @@
|> replace(@dbl2_rgx, "\\1“")

escape = Map.get(options, :escape, true)
escape_to_iodata(element, 0, element, [], true, escape, 0)
_escape_to_iodata1(element, 0, element, [], true, escape, 0)
end

defp escape(element, %{escape: escape}) do
escape_to_iodata(element, 0, element, [], false, escape, 0)
_escape_to_iodata1(element, 0, element, [], false, escape, 0)
end

defp escape(element, _options) do
escape_to_iodata(element, 0, element, [], false, true, 0)
_escape_to_iodata1(element, 0, element, [], false, true, 0)
end

defp make_att(name_value_pair, tag)
defp make_att({name, value}, _) do
defp _make_att1(name_value_pair, tag)
defp _make_att1({name, value}, _) do
[" ", name, "=\"", value, "\""]
end

Expand All @@ -367,20 +381,12 @@
|> Enum.take(level*indent)
end

defp open_tag(tag, atts)
defp open_tag(tag, atts) when tag in @void_elements do
[?<, tag, Enum.map(atts, &make_att(&1, tag)), " />"]
end
defp open_tag(tag, atts) do
[?<, tag, Enum.map(atts, &make_att(&1, tag)), ?>]
end

# Optimized HTML escaping + smartypants, insipred by Plug.HTML
# https://github.com/elixir-plug/plug/blob/v1.11.0/lib/plug/html.ex

# Do not escape HTML entities
defp escape_to_iodata("&#x" <> rest, skip, original, acc, smartypants, escape, len) do
escape_to_iodata(rest, skip, original, acc, smartypants, escape, len + 3)
defp _escape_to_iodata1("&#x" <> rest, skip, original, acc, smartypants, escape, len) do
_escape_to_iodata1(rest, skip, original, acc, smartypants, escape, len + 3)
end

escapes = [
Expand All @@ -405,49 +411,52 @@
# Unlike HTML escape matches, smartypants matches may contain more than one character
match_length = if is_binary(match), do: byte_size(match), else: 1

defp escape_to_iodata(<<unquote(match), rest::bits>>, skip, original, acc, true, escape, 0) do
escape_to_iodata(rest, skip + unquote(match_length), original, [acc | unquote(insert)], true, escape, 0)
defp _escape_to_iodata1(<<unquote(match), rest::bits>>, skip, original, acc, true, escape, 0) do
_escape_to_iodata1(rest, skip + unquote(match_length), original, [acc | unquote(insert)], true, escape, 0)
end

defp escape_to_iodata(<<unquote(match), rest::bits>>, skip, original, acc, true, escape, len) do
defp _escape_to_iodata1(<<unquote(match), rest::bits>>, skip, original, acc, true, escape, len) do
part = binary_part(original, skip, len)
escape_to_iodata(rest, skip + len + unquote(match_length), original, [acc, part | unquote(insert)], true, escape, 0)
_escape_to_iodata1(rest, skip + len + unquote(match_length), original, [acc, part | unquote(insert)], true, escape, 0)
end
end

for {match, insert} <- escapes do
defp escape_to_iodata(<<unquote(match), rest::bits>>, skip, original, acc, smartypants, true, 0) do
escape_to_iodata(rest, skip + 1, original, [acc | unquote(insert)], smartypants, true, 0)
defp _escape_to_iodata1(<<unquote(match), rest::bits>>, skip, original, acc, smartypants, true, 0) do
_escape_to_iodata1(rest, skip + 1, original, [acc | unquote(insert)], smartypants, true, 0)
end

defp escape_to_iodata(<<unquote(match), rest::bits>>, skip, original, acc, smartypants, true, len) do
defp _escape_to_iodata1(<<unquote(match), rest::bits>>, skip, original, acc, smartypants, true, len) do
part = binary_part(original, skip, len)
escape_to_iodata(rest, skip + len + 1, original, [acc, part | unquote(insert)], smartypants, true, 0)
_escape_to_iodata1(rest, skip + len + 1, original, [acc, part | unquote(insert)], smartypants, true, 0)
end
end

defp escape_to_iodata(<<_char, rest::bits>>, skip, original, acc, smartypants, escape, len) do
escape_to_iodata(rest, skip, original, acc, smartypants, escape, len + 1)
defp _escape_to_iodata1(<<_char, rest::bits>>, skip, original, acc, smartypants, escape, len) do
_escape_to_iodata1(rest, skip, original, acc, smartypants, escape, len + 1)
end

defp escape_to_iodata(<<>>, 0, original, _acc, _smartypants, _escape, _len) do
defp _escape_to_iodata1(<<>>, 0, original, _acc, _smartypants, _escape, _len) do
original
end

defp escape_to_iodata(<<>>, skip, original, acc, _smartypants, _escape, len) do
defp _escape_to_iodata1(<<>>, skip, original, acc, _smartypants, _escape, len) do
[acc | binary_part(original, skip, len)]
end

defp _add_trailing_nl(node)
defp _add_trailing_nl(text) when is_binary(text), do: [text, "\n"]
defp _add_trailing_nl(node), do: node

defp _maybe_remove_paras(ast, options)
defp _maybe_remove_paras(ast, %Options{inner_html: true}) do
Enum.map(ast, &_remove_para/1)
end
defp _maybe_remove_paras(ast, _), do: ast

defp _open_tag1(tag, atts)
defp _open_tag1(tag, atts) when tag in @void_elements do
[?<, tag, Enum.map(atts, &_make_att1(&1, tag)), " />"]
end
defp _open_tag1(tag, atts) do
[?<, tag, Enum.map(atts, &_make_att1(&1, tag)), ?>]
end

@pop {:__end__}
defp _pop_to_pop(result, intermediate \\ [])
defp _pop_to_pop([@pop, {tag, atts, _, meta}|rest], intermediate) do
Expand Down
14 changes: 14 additions & 0 deletions test/acceptance/html/compact_output_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,19 @@ defmodule Acceptance.Html.CompactModeTest do
{:ok, html, _} = as_html(markdown, compact_output: true)
assert html == expected
end

test "does not preserve newlines in paragraphes" do
expected = "<p>\nhello world</p>\n"
result = Earmark.transform( [{"p", [], ["hello\nworld"], %{}}], compact_output: true)

assert result == expected
end

test "but does so if verbatim is true" do
expected = "<p>\n hello\nworld</p>\n"
result = Earmark.transform( [{"p", [], ["hello\nworld"], %{verbatim: true}}], compact_output: true)

assert result == expected
end
end
end
3 changes: 1 addition & 2 deletions test/acceptance/html/gfm_list_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,6 @@ defmodule Acceptance.Html.GfmListTest do
assert to_html2(markdown) == expected
end


end

end
# SPDX-License-Identifier: Apache-2.0

0 comments on commit 7e5dd49

Please sign in to comment.