defmodule Icon.Parse do @moduledoc """ Icon parsing module. """ defmodule Icon do @moduledoc "Icon struct" @type t :: %__MODULE__{href: String.t(), rel: String.t(), type: String.t(), sizes: String.t()} defstruct [:href, :rel, :type, :sizes] end @doc """ Parse a html string and extract all the icons from it ## Parameters - html: String - url: a URL string ## Returns - `[Icon.Parse.Icon.t()]` - `[]` if no icon discovered ## Examples iex> Icon.Parse.parse(-s(), "http://test.com") [%Icon.Parse.Icon{href: "http://test.com/a/favicon.ico", rel: "icon"}] """ @spec parse(String.t(), String.t()) :: [Icon.t()] | [] def parse(html, url) do html |> extract_icons |> Enum.map(fn icon -> resolve_href(icon, url) end) end defp extract_icons(html) do html |> Floki.find("link") |> Enum.filter(fn {_tag, attrs, _children} -> icon_link?(attrs) end) |> Enum.map(fn {_tag, attrs, _children} -> attrs end) |> Enum.map(fn attrs -> make_icon(attrs) end) end defp icon_link?(attributes) do Enum.any?(attributes, fn {att, value} -> att == "rel" and String.contains?(value, "icon") end) end defp attr([{att, value} | tl], name) do if att == name do value else attr(tl, name) end end defp attr([], _name) do nil end defp make_icon(attrs) do %Icon{ href: attr(attrs, "href"), sizes: attr(attrs, "sizes"), rel: attr(attrs, "rel"), type: attr(attrs, "type") } end defp resolve_href(icon, url) do href = icon.href case Regex.match?(~r/^https?/, href) do true -> icon false -> uri = URI.parse(href) case uri do %URI{host: nil} -> %{icon | href: %{URI.parse(url) | path: uri.path} |> URI.to_string()} %URI{scheme: nil} -> %{icon | href: %{uri | scheme: "http"} |> URI.to_string()} _ -> icon end end end end