defmodule Floki do @moduledoc """ A HTML parser and seeker. This is a simple HTML parser that enables searching using CSS like selectors. You can search elements by class, tag name and id. ## Example Assuming that you have the following HTML:

Floki

Github page
You can perform the following queries: * Floki.find(html, "#content") : returns the section with all children; * Floki.find(html, ".headline") : returns a list with the `p` element; * Floki.find(html, "a") : returns a list with the `a` element. Each HTML node is represented by a tuple like: {tag_name, attributes, chidren_nodes} Example of node: {"p", [{"class", "headline"}], ["Floki"]} So even if the only child node is the element text, it is represented inside a list. You can write a simple HTML crawler (with support of [HTTPoison](https://github.com/edgurgel/httpoison)) with a few lines of code: html |> Floki.find(".pages") |> Floki.find("a") |> Floki.attribute("href") |> Enum.map(fn(url) -> HTTPoison.get!(url) end) It is simple as that! """ @type html_tree :: tuple | list @doc """ Parses a HTML string. ## Examples iex> Floki.parse("
hello world
") {"div", [{"class", "js-action" }], ["hello world"]} """ @spec parse(binary) :: html_tree def parse(html) do :mochiweb_html.parse(html) end @doc """ Finds elements inside a HTML tree or string. You can search by class, tag name or id. It is possible to compose searches: Floki.find(html_string, ".class") |> Floki.find(".another-class-inside-small-scope" ## Examples iex> Floki.find("

hello

", ".hint") [{"span", [{ "class", "hint" }], ["hello"]}] iex> "
Content
" |> Floki.find("#important") {"div", [{"id", "important"}], [{"div", [], ["Content"]}]} iex> Floki.find("

Google

", "a") [{"a", [{"href", "https://google.com"}], ["Google"]}] """ @spec find(binary | html_tree, binary) :: html_tree def find(html, selector) when is_binary(html) do parse(html) |> find(selector) end def find(html_tree, "." <> class) do find_by_selector(class, html_tree, &class_matcher/3, []) |> Enum.reverse end def find(html_tree, "#" <> id) do find_by_selector(id, html_tree, &id_matcher/3, []) |> List.first end def find(html_tree, tag_name) do find_by_selector(tag_name, html_tree, &tag_matcher/3, []) |> Enum.reverse end @doc """ Returns a list with attribute values for a given selector. ## Examples iex> Floki.attribute("Google", "a", "href") ["https://google.com"] """ @spec attribute(binary | html_tree, binary, binary) :: list def attribute(html, selector, attribute_name) do html |> find(selector) |> get_attribute_values(attribute_name) end @doc """ Returns a list with attribute values from elements. ## Examples iex> Floki.attribute("Google", "href") ["https://google.com"] """ @spec attribute(binary | html_tree, binary) :: list def attribute(elements, attribute_name) do elements |> get_attribute_values(attribute_name) end defp attribute_match?(attributes, attribute_name) do attributes |> Enum.find(fn({attr_name, _}) -> attr_name == attribute_name end) end defp attribute_match?(attributes, attribute_name, value) do Enum.find(attributes, fn(attribute) -> { attr_name, attr_value } = attribute attr_name == attribute_name && String.contains?(attr_value, value) end) end defp find_by_selector(_selector, {}, _, acc), do: acc defp find_by_selector(_selector, [], _, acc), do: acc defp find_by_selector(_selector, tree, _, acc) when is_binary(tree), do: acc defp find_by_selector(selector, [h|t], matcher, acc) do acc = find_by_selector(selector, h, matcher, acc) find_by_selector(selector, t, matcher, acc) end defp find_by_selector(_selector, { :comment, _comment }, _, acc), do: acc defp find_by_selector(selector, node, matcher, acc) when is_tuple(node) do { _, _, child_node } = node acc = matcher.(selector, node, acc) find_by_selector(selector, child_node, matcher, acc) end defp get_attribute_values(element, attr_name) when is_tuple(element) do get_attribute_values([element], attr_name) end defp get_attribute_values(elements, attr_name) do Enum.map(elements, fn(el) -> { _name, attributes, _childs } = el attribute_match?(attributes, attr_name) end) |> Enum.reject(fn(x) -> is_nil(x) end) |> Enum.map(fn({_attr_name, value}) -> value end) end defp class_matcher(class_name, node, acc) do { _, attributes, _ } = node if attribute_match?(attributes, "class", class_name) do acc = [node|acc] end acc end defp tag_matcher(tag_name, node, acc) do { tag, _, _ } = node if tag == tag_name do acc = [node|acc] end acc end defp id_matcher(id, node, acc) do { _, attributes, _ } = node if attribute_match?(attributes, "id", id) do acc = [node|acc] end acc end end