defmodule Text2Html do @moduledoc """ Text2Html transform text into HTML. """ @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> Text2Html.text_to_html("Hello\n\nWorld") |> Phoenix.HTML.safe_to_string() "

Hello

\n

World

\n" iex> Text2Html.text_to_html("Hello\nWorld") |> Phoenix.HTML.safe_to_string() "

Hello
\nWorld

\n" iex> opts = [wrapper_tag: :div, attributes: [class: "p"]] ...> Text2Html.text_to_html("Hello\n\nWorld", opts) |> Phoenix.HTML.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`) * `:links` - if `true` convert URLs to links (default: `false`) * `:links_opts` - options for the links conversion (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) links? = Keyword.get(opts, :links, false) links_opts = Keyword.get(opts, :links_opts, []) string |> maybe_html_escape(escape?) |> String.split(["\n\n", "\r\n\r\n"], trim: true) |> Enum.filter(¬_blank?/1) |> Enum.map(&wrap_paragraph(&1, wrapper_tag, attributes, insert_brs?, links?, links_opts)) |> Phoenix.HTML.html_escape() end defp maybe_html_escape(string, true), do: string |> Phoenix.HTML.Engine.html_escape() |> IO.iodata_to_binary() defp maybe_html_escape(string, false), do: string 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, insert_brs?, links?, links_opts) do text = text |> insert_brs(insert_brs?) |> convert_urls_to_links(links?, links_opts) [PhoenixHTMLHelpers.Tag.content_tag(tag, text, attributes), ?\n] end defp insert_brs(text, false) do text |> split_lines() |> Enum.intersperse(?\s) |> Phoenix.HTML.raw() end defp insert_brs(text, true) do text |> split_lines() |> Enum.map(&Phoenix.HTML.raw/1) |> Enum.intersperse([PhoenixHTMLHelpers.Tag.tag(:br), ?\n]) end defp split_lines(text) do String.split(text, ["\n", "\r\n"], trim: true) end defp convert_urls_to_links(lines, false, _), do: lines defp convert_urls_to_links(lines, true, opts) do only_secure? = Keyword.get(opts, :only_secure, true) blank_target? = Keyword.get(opts, :blank_target, true) class = Keyword.get(opts, :class, nil) set_links([], lines, only_secure?, blank_target?, class) end defp url_regex(true) do ~r/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/ end defp url_regex(false) do ~r/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/ end defp set_links(acc, [], _, _, _), do: acc defp set_links(acc, [line | rest], only_secure?, blank_target?, class) when is_list(line) do set_links(acc ++ [line], rest, only_secure?, blank_target?, class) end defp set_links(acc, [line | rest], only_secure?, blank_target?, class) do line = Phoenix.HTML.safe_to_string(line) links = Regex.scan(url_regex(only_secure?), line) |> Enum.map(&List.first(&1)) |> Enum.uniq() line = links |> Enum.reduce(line, fn link, acc -> acc |> String.replace( link, "#{link}" ) end) line = Phoenix.HTML.raw(line) set_links(acc ++ [line], rest, only_secure?, blank_target?, class) end end