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

\n

World

\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
\n
World
\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