defmodule SimplexFormat do
@moduledoc """
Helpers related to formatting text.
"""
import Phoenix.HTML, only: [html_escape: 1, raw: 1]
import PhoenixHTMLHelpers.Tag, only: [content_tag: 3, tag: 1]
@doc ~S"""
Returns text transformed into HTML using simple formatting rules.
Two or more consecutive newlines `\n\n` or `\r\n\r\n` are considered as a
paragraph and text between them is wrapped in `
` tags.
One newline `\n` or `\r\n` is considered as a linebreak and a `
` tag is inserted.
## Examples
iex> text_to_html("Hello\n\nWorld") |> safe_to_string
"
Hello
\nWorld
\n"
iex> text_to_html("Hello\nWorld") |> safe_to_string
"Hello
\nWorld
\n"
iex> text_to_html("Hello, welcome to http://www.google.com", auto_link: true) |> safe_to_string
"Hello, welcome to http://www.google.com
\n"
iex> opts = [wrapper_tag: :div, attributes: [class: "p"]]
...> text_to_html("Hello\n\nWorld", opts) |> safe_to_string
"Hello
\nWorld
\n"
## Options
* `:escape` - if `false` does not html escape input (default: `true`)
* `:wrapper_tag` - tag to wrap each paragraph (default: `:p`)
* `:attributes` - html attributes of the wrapper tag (default: `[]`)
* `:insert_brs` - if `true` insert `
` for single line breaks (default: `true`)
* `:auto_link` - if `true` wrap HTTP URLs in an anchor tag (default: `false`)
* `:url_attributes` - HTML attributes of the anchor tag for auto_linked URLs (default: `[]`)
"""
@spec text_to_html(Phoenix.HTML.unsafe(), Keyword.t()) :: Phoenix.HTML.safe()
def text_to_html(string, opts \\ []) do
escape? = Keyword.get(opts, :escape, true)
wrapper_tag = Keyword.get(opts, :wrapper_tag, :p)
attributes = Keyword.get(opts, :attributes, [])
insert_brs? = Keyword.get(opts, :insert_brs, true)
auto_link? = Keyword.get(opts, :auto_link, false)
url_attrs = Keyword.get(opts, :url_attributes, [])
string
|> String.split(["\n\n", "\r\n\r\n"], trim: true)
|> Enum.filter(¬_blank?/1)
|> Enum.map(
&wrap_paragraph(&1, wrapper_tag, attributes, escape?, insert_brs?, auto_link?, url_attrs)
)
|> html_escape()
end
defp not_blank?("\r\n" <> rest), do: not_blank?(rest)
defp not_blank?("\n" <> rest), do: not_blank?(rest)
defp not_blank?(" " <> rest), do: not_blank?(rest)
defp not_blank?(""), do: false
defp not_blank?(_), do: true
defp wrap_paragraph(text, tag, attributes, escape?, insert_brs?, auto_link?, url_attrs) do
content =
text
|> split_lines()
|> Enum.map(&format_line(&1, escape?, auto_link?, url_attrs))
|> join_lines(insert_brs?)
[content_tag(tag, content, attributes), ?\n]
end
defp split_lines(text) do
String.split(text, ["\n", "\r\n"], trim: true)
end
defp join_lines(lines, true), do: Enum.intersperse(lines, [tag(:br), ?\n])
defp join_lines(lines, false), do: Enum.intersperse(lines, ?\s)
defp format_line(line, escape?, false, _url_attrs), do: escape_text(line, escape?)
defp format_line(line, escape?, true, url_attrs), do: link_urls(line, escape?, url_attrs)
# Escaping happens here, at the leaves, so that auto-linked URLs reach
# `content_tag/3` unescaped and are therefore escaped exactly once.
defp escape_text(text, true), do: html_escape(text)
defp escape_text(text, false), do: raw(text)
# The URL body runs up to the first whitespace or pipe; the final character
# class trims trailing sentence punctuation so "see http://x.com." excludes
# the period.
@url_regex ~r/((http(s)?(\:\/\/))+(www\.)?([\w\-\.\/])*(\.[a-zA-Z]{2,3}\/?))[^\s|]*[^.,;:\?\!\@\^\$ -]/
# Finds every URL in a single linear pass, interleaving the escaped text
# between matches with the anchor tags built from each (unescaped) URL.
defp link_urls(text, escape?, url_attrs) do
{segments, offset} =
@url_regex
|> Regex.scan(text, return: :index)
|> Enum.reduce({[], 0}, fn [{index, length} | _], {segments, offset} ->
leading = binary_part(text, offset, index - offset)
url = binary_part(text, index, length)
{[wrap_url(url, url_attrs), escape_text(leading, escape?) | segments], index + length}
end)
trailing = binary_part(text, offset, byte_size(text) - offset)
Enum.reverse([escape_text(trailing, escape?) | segments])
end
defp wrap_url(url, url_attributes) do
attributes = Keyword.merge(url_attributes, href: url)
content_tag(:a, url, attributes)
end
end