defmodule Premailex do @external_resource "README.md" @moduledoc "README.md" |> File.read!() |> String.split("") |> Enum.fetch!(1) # Replace Markdown-linked HexDocs cross-references with plain # references so ExDoc can resolve links correctly. |> then(&Regex.replace(~r/\[(`[^`]+`)\]\([^)]+\)/, &1, "\\1")) # Convert Markdown OTP module references to ExDoc format. |> then(&Regex.replace(~r/`(:[a-z]+)`/, &1, "`m:\\1`")) require Logger @type html :: String.t() @type html_element :: {String.t(), [{String.t(), String.t()}], [html_node()]} @type html_node :: html_element() | {:comment, String.t()} | String.t() @type html_tree :: [html_node()] @doc """ Parses an HTML string into a `t:html_tree/0`. ## Options * `:html_parser` - HTML parser to use (see `Premailex.HTMLParser`); ## Examples iex> Premailex.parse(~s(

Hello

)) [{"p", [{"class", "lead"}], ["Hello"]}] """ @spec parse(html(), Keyword.t()) :: html_tree() def parse(html, options \\ []) do html_parser = Keyword.get_lazy(options, :html_parser, &html_parser/0) html_parser.parse(html) end defp html_parser do case Application.get_env(:premailex, :html_parser) || default_html_parser() do mod when is_atom(mod) -> mod other -> raise "Invalid `:html_parser` environment value: #{inspect(other)}" end end defp default_html_parser do cond do Code.ensure_loaded?(LazyHTML) -> Premailex.HTMLParser.LazyHTML Code.ensure_loaded?(Floki) -> Premailex.HTMLParser.Floki Code.ensure_loaded?(Meeseeks) -> Premailex.HTMLParser.Meeseeks true -> Premailex.HTMLParser.Xmerl end end @doc """ Converts a `t:html_tree/0` into an HTML string. ## Options * `:html_parser` - HTML parser to use (see `Premailex.HTMLParser`); ## Examples iex> Premailex.to_html([{"p", [{"class", "lead"}], ["Hello"]}]) ~s(

Hello

) """ @spec to_html(html_tree(), Keyword.t()) :: html() def to_html(tree, options \\ []) do html_parser = Keyword.get_lazy(options, :html_parser, &html_parser/0) html_parser.to_html(tree) end @doc """ Adds inline styles to an HTML string or `t:html_tree/0`. ## Options * `:html_parser` - HTML parser to use (see `Premailex.HTMLParser`); * `:http_adapter` - HTTP adapter to use for fetching external stylesheets (see `Premailex.HTTPAdapter`); * `:remove_style_tags` - whether to remove the ` ...> ...>

Text

...> )) ~s(

Text

) """ @spec to_inline_css(html() | html_tree(), Keyword.t()) :: html() def to_inline_css(html_or_tree, options \\ []) do options = options |> Keyword.put_new_lazy(:html_parser, &html_parser/0) |> Keyword.put_new_lazy(:http_adapter, &http_adapter/0) do_to_inline_css(html_or_tree, options) end defp http_adapter, do: Application.get_env(:premailex, :http_adapter, Premailex.HTTPAdapter.Httpc) defp do_to_inline_css(html, options) when is_binary(html) do html |> parse(options) |> do_to_inline_css(options) end defp do_to_inline_css(tree, options) do css_selector = Keyword.get(options, :css_selector, "style,link[rel=\"stylesheet\"][href]") css_rules = map_css_rules(tree, css_selector, options) options |> Keyword.get(:remove_style_tags, false) |> case do true -> Premailex.DOM.reject(tree, css_selector) false -> tree end |> Premailex.HTMLInlineStyles.process(css_rules) |> to_html(options) end defp map_css_rules(tree, css_selector, options) do tree |> Premailex.DOM.all(css_selector) |> Enum.flat_map(fn node -> node |> load_css(options) |> Premailex.CSSParser.parse() end) end defp load_css({"style", _, content}, _options) do Enum.join(content, "\n") end defp load_css({"link", attrs, _}, options) do case List.keyfind(attrs, "href", 0) do {"href", url} -> load_url(url, options) nil -> "" end end defp load_url(url, options) do {http_adapter, opts} = case Keyword.fetch!(options, :http_adapter) do {adapter, opts} -> {adapter, opts} adapter -> {adapter, nil} end case http_adapter.request(:get, url, nil, [], opts) do {:ok, %{status: status, body: body}} when status in 200..399 -> body {:ok, %{status: status}} -> Logger.warning("Ignoring #{url} styles due to unexpected HTTP response status: #{status}") "" {:error, error} -> Logger.warning( "Ignoring #{url} styles due to error in #{inspect(http_adapter)}:\n\n#{inspect(error)}" ) "" end end @doc """ Converts an HTML string or `t:html_tree/0` to plain text. ## Options * `:html_parser` - HTML parser to use (see `Premailex.HTMLParser`); ## Examples iex> Premailex.to_text( ...> ~s( ...> ...> ...>

Text

...> )) "Text" """ @spec to_text(html() | html_tree(), Keyword.t()) :: String.t() def to_text(html_or_tree, options \\ []) do options = Keyword.put_new_lazy(options, :html_parser, &html_parser/0) do_to_text(html_or_tree, options) end defp do_to_text(html, options) when is_binary(html) do html |> parse(options) |> do_to_text(options) end defp do_to_text(tree, _options) do Premailex.HTMLToPlainText.process(tree) end end