defmodule Meeseeks do alias Meeseeks.{Context, Document, Error, Parser, Result, Select, Selector, TupleTree} @moduledoc """ Meeseeks is an Elixir library for parsing and extracting data from HTML and XML with CSS or XPath selectors. ```elixir import Meeseeks.CSS html = HTTPoison.get!("https://news.ycombinator.com/").body for story <- Meeseeks.all(html, css("tr.athing")) do title = Meeseeks.one(story, css(".title a")) %{title: Meeseeks.text(title), url: Meeseeks.attr(title, "href")} end #=> [%{title: "...", url: "..."}, %{title: "...", url: "..."}, ...] ``` ## Getting Started ### Parse Start by parsing a source (HTML/XML string or `Meeseeks.TupleTree`) into a `Meeseeks.Document` so that it can be queried. `Meeseeks.parse/1` parses the source as HTML, but `Meeseeks.parse/2` accepts a second argument of either `:html` or `:xml` that specifies how the source is parsed. ```elixir document = Meeseeks.parse("

1

2

3

") #=> Meeseeks.Document<{...}> ``` The selection functions accept an unparsed source, parsing it as HTML, but parsing is expensive so parse ahead of time when running multiple selections on the same document. ### Select Next, use one of Meeseeks's selection functions - `fetch_all`, `all`, `fetch_one`, or `one` - to search for nodes. All these functions accept a queryable (a source, a document, or a `Meeseeks.Result`), one or more `Meeseeks.Selector`s, and optionally an initial context. `all` returns a (possibly empty) list of results representing every node matching one of the provided selectors, while `one` returns a result representing the first node to match a selector (depth-first) or nil if there is no match. `fetch_all` and `fetch_one` work like `all` and `one` respectively, but wrap the result in `{:ok, ...}` if there is a match or return `{:error, %Meeseeks.Error{type: :select, reason: :no_match}}` if there is not. To generate selectors, use the `css` macro provided by `Meeseeks.CSS` or the `xpath` macro provided by `Meeseeks.XPath`. ```elixir import Meeseeks.CSS result = Meeseeks.one(document, css("#main p")) #=> #Meeseeks.Result<{

1

}> import Meeseeks.XPath result = Meeseeks.one(document, xpath("//*[@id='main']//p")) #=> #Meeseeks.Result<{

1

}> ``` ### Extract Retrieve information from the `Meeseeks.Result` with an extraction function. The extraction functions are `attr`, `attrs`, `data`, `dataset`, `html`, `own_text`, `tag`, `text`, `tree`. ```elixir Meeseeks.tag(result) #=> "p" Meeseeks.text(result) #=> "1" Meeseeks.tree(result) #=> {"p", [], ["1"]} ``` The extraction functions `html` and `tree` work on `Meeseeks.Document`s in addition to `Meeseeks.Result`s. ```elixir Meeseeks.html(document) #=> "

1

2

3

" ``` """ @type queryable :: Parser.source() | Document.t() | Result.t() @type extractable :: Document.t() | Result.t() | nil @type selectors :: Selector.t() | [Selector.t()] # Parse @doc """ Parses a string or `Meeseeks.TupleTree` into a `Meeseeks.Document`. `parse/1` parses as HTML, while `parse/2` accepts a second argument of either `:html`, `:xml`, or `tuple_tree` that specifies how the source is parsed. ## Examples iex> Meeseeks.parse("

Hello, Meeseeks!

") #Meeseeks.Document<{...}> iex> Meeseeks.parse("GGK", :xml) #Meeseeks.Document<{...}> iex> Meeseeks.parse({"div", [{"id", "main"}], [{"p", [], ["Hello, Meeseeks!"]}]}, :tuple_tree) #Meeseeks.Document<{...}> """ @spec parse(Parser.source()) :: Document.t() | {:error, Error.t()} def parse(source) do Parser.parse(source) end @spec parse(Parser.source(), Parser.type()) :: Document.t() | {:error, Error.t()} def parse(source, parser) do Parser.parse(source, parser) end # Select @doc """ Returns `{:ok, [Result, ...]}` if one of more nodes in the queryable match a selector, or `{:error, %Meeseeks.Error{type: :select, reason: :no_match}}` if none do. Optionally accepts a `Meeseeks.Context` map. Parses the source if it is not a `Meeseeks.Document` or `Meeseeks.Result`, and may return `{:error, %Meeseeks.Error{type: parser}` if there is a parse error. If multiple selections are being ran on the same unparsed source, parse first to avoid unnecessary computation. ## Examples iex> import Meeseeks.CSS iex> Meeseeks.fetch_all("

1

2

3

", css("#main p")) |> elem(1) |> List.first() #Meeseeks.Result<{

1

}> """ @spec fetch_all(queryable, selectors) :: {:ok, [Result.t()]} | {:error, Error.t()} def fetch_all(queryable, selectors) do fetch_all(queryable, selectors, %{}) end @spec fetch_all(queryable, selectors, Context.t()) :: {:ok, [Result.t()]} | {:error, Error.t()} def fetch_all(queryable, selectors, context) def fetch_all({:error, _} = error, _selectors, _context), do: error def fetch_all(%Document{} = queryable, selectors, context) do Select.fetch_all(queryable, selectors, context) end def fetch_all(%Result{} = queryable, selectors, context) do Select.fetch_all(queryable, selectors, context) end def fetch_all(source, selectors, context) do case parse(source) do {:error, reason} -> {:error, reason} document -> Select.fetch_all(document, selectors, context) end end @doc """ Returns `[Result, ...]` if one or more nodes in the queryable match a selector, or `[]` if none do. Optionally accepts a `Meeseeks.Context` map. Parses the source if it is not a `Meeseeks.Document` or `Meeseeks.Result`, and may return `{:error, %Meeseeks.Error{type: parser}` if there is a parse error. If multiple selections are being ran on the same unparsed source, parse first to avoid unnecessary computation. ## Examples iex> import Meeseeks.CSS iex> Meeseeks.all("

1

2

3

", css("#main p")) |> List.first() #Meeseeks.Result<{

1

}> """ @spec all(queryable, selectors) :: [Result.t()] | {:error, Error.t()} def all(queryable, selectors) do all(queryable, selectors, %{}) end @spec all(queryable, selectors, Context.t()) :: [Result.t()] | {:error, Error.t()} def all(queryable, selectors, context) def all({:error, _} = error, _selectors, _context), do: error def all(%Document{} = queryable, selectors, context) do Select.all(queryable, selectors, context) end def all(%Result{} = queryable, selectors, context) do Select.all(queryable, selectors, context) end def all(source, selectors, context) do case parse(source) do {:error, reason} -> {:error, reason} document -> Select.all(document, selectors, context) end end @doc """ Returns `{:ok, Result}` for the first node in the queryable (depth-first) matching a selector, or `{:error, %Meeseeks.Error{type: :select, reason: :no_match}}` if none do. Optionally accepts a `Meeseeks.Context` map. Parses the source if it is not a `Meeseeks.Document` or `Meeseeks.Result`, and may return `{:error, %Meeseeks.Error{type: parser}` if there is a parse error. If multiple selections are being ran on the same unparsed source, parse first to avoid unnecessary computation. ## Examples iex> import Meeseeks.CSS iex> Meeseeks.fetch_one("

1

2

3

", css("#main p")) |> elem(1) #Meeseeks.Result<{

1

}> """ @spec fetch_one(queryable, selectors) :: {:ok, Result.t()} | {:error, Error.t()} def fetch_one(queryable, selectors) do fetch_one(queryable, selectors, %{}) end @spec fetch_one(queryable, selectors, Context.t()) :: {:ok, Result.t()} | {:error, Error.t()} def fetch_one(queryable, selectors, context) def fetch_one({:error, _} = error, _selectors, _context), do: error def fetch_one(%Document{} = queryable, selectors, context) do Select.fetch_one(queryable, selectors, context) end def fetch_one(%Result{} = queryable, selectors, context) do Select.fetch_one(queryable, selectors, context) end def fetch_one(source, selectors, context) do case parse(source) do {:error, reason} -> {:error, reason} document -> Select.fetch_one(document, selectors, context) end end @doc """ Returns a `Result` for the first node in the queryable (depth-first) matching a selector, or `nil` if none do. Optionally accepts a `Meeseeks.Context` map. Parses the source if it is not a `Meeseeks.Document` or `Meeseeks.Result`, and may return `{:error, %Meeseeks.Error{type: parser}` if there is a parse error. If multiple selections are being ran on the same unparsed source, parse first to avoid unnecessary computation. ## Examples iex> import Meeseeks.CSS iex> Meeseeks.one("

1

2

3

", css("#main p")) #Meeseeks.Result<{

1

}> """ @spec one(queryable, selectors) :: Result.t() | nil | {:error, Error.t()} def one(queryable, selectors) do one(queryable, selectors, %{}) end @spec one(queryable, selectors, Context.t()) :: Result.t() | nil | {:error, Error.t()} def one(queryable, selectors, context) def one({:error, _} = error, _selectors, _context), do: error def one(%Document{} = queryable, selectors, context) do Select.one(queryable, selectors, context) end def one(%Result{} = queryable, selectors, context) do Select.one(queryable, selectors, context) end def one(source, selectors, context) do case parse(source) do {:error, reason} -> {:error, reason} document -> Select.one(document, selectors, context) end end @doc """ Returns the accumulated result of walking the queryable, accumulating nodes that match a selector. Prefer `all` or `one`- `select` should only be used when a custom accumulator is required. Requires that a `Meeseeks.Accumulator` has been added to the context via `Meeseeks.Context.add_accumulator/2`, and will raise an error if it hasn't. Parses the source if it is not a `Meeseeks.Document` or `Meeseeks.Result`, and may return `{:error, %Meeseeks.Error{type: parser}` if there is a parse error. If multiple selections are being ran on the same unparsed source, parse first to avoid unnecessary computation. ## Examples iex> import Meeseeks.CSS iex> accumulator = %Meeseeks.Accumulator.One{} iex> context = Meeseeks.Context.add_accumulator(%{}, accumulator) iex> Meeseeks.select("

1

2

3

", css("#main p"), context) #Meeseeks.Result<{

1

}> """ @spec select(queryable, selectors, Context.t()) :: any | {:error, Error.t()} def select(queryable, selectors, context) def select({:error, _} = error, _selectors, _context), do: error def select(%Document{} = queryable, selectors, context) do Select.select(queryable, selectors, context) end def select(%Result{} = queryable, selectors, context) do Select.select(queryable, selectors, context) end def select(source, selectors, context) do case parse(source) do {:error, reason} -> {:error, reason} document -> Select.select(document, selectors, context) end end # Extract @doc """ Returns the value of an attribute in a result, or nil if there isn't one. Nil input returns `nil`. ## Examples iex> import Meeseeks.CSS iex> result = Meeseeks.one("
Hi
", css("#example")) #Meeseeks.Result<{
Hi
}> iex> Meeseeks.attr(result, "id") "example" """ @spec attr(extractable, String.t()) :: String.t() | nil def attr(extractable, attribute) def attr(nil, _), do: nil def attr(%Result{} = result, attribute), do: Result.attr(result, attribute) def attr(x, _attribute), do: raise_cannot_extract(x, "attr/2") @doc """ Returns a result's attributes list, which may be empty, or nil if the result represents a node without attributes. Nil input returns `nil`. ## Examples iex> import Meeseeks.CSS iex> result = Meeseeks.one("
Hi
", css("#example")) #Meeseeks.Result<{
Hi
}> iex> Meeseeks.attrs(result) [{"id", "example"}] """ @spec attrs(extractable) :: [{String.t(), String.t()}] | nil def attrs(extractable) def attrs(nil), do: nil def attrs(%Result{} = result), do: Result.attrs(result) def attrs(x), do: raise_cannot_extract(x, "attrs/1") @doc """ Returns the combined data of a result or the result's children, which may be an empty string. Once the data has been combined the whitespace is compacted by replacing all instances of more than one whitespace character with a single space and then trimmed. Data is the content of `", css("#example")) #Meeseeks.Result<{ }> iex> Meeseeks.data(result2) "Hi" """ @spec data(extractable, Keyword.t()) :: String.t() | nil def data(extractable, opts \\ []) def data(nil, _), do: nil def data(%Result{} = result, opts), do: Result.data(result, opts) def data(x, _), do: raise_cannot_extract(x, "data/1") @doc """ Returns a map of a result's data attributes, or nil if the result represents a node without attributes. Behaves like HTMLElement.dataset; only valid data attributes are included, and attribute names have "data-" removed and are converted to camelCase. See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset Nil input returns `nil`. ## Examples iex> import Meeseeks.CSS iex> result = Meeseeks.one("
", css("#example")) #Meeseeks.Result<{
}> iex> Meeseeks.dataset(result) %{"xVal" => "1", "yVal" => "2"} """ @spec dataset(extractable) :: %{optional(String.t()) => String.t()} | nil def dataset(extractable) def dataset(nil), do: nil def dataset(%Result{} = result), do: Result.dataset(result) def dataset(x), do: raise_cannot_extract(x, "dataset/1") @doc """ Returns a string representing the combined HTML of a document or result and its descendants. Nil input returns `nil`. ## Examples iex> import Meeseeks.CSS iex> document = Meeseeks.parse("
Hi
") iex> Meeseeks.html(document) "
Hi
" iex> result = Meeseeks.one(document, css("#example")) #Meeseeks.Result<{
Hi
}> iex> Meeseeks.html(result) "
Hi
" """ @spec html(extractable) :: String.t() | nil def html(extractable) def html(nil), do: nil def html(%Document{} = document), do: Document.html(document) def html(%Result{} = result), do: Result.html(result) def html(x), do: raise_cannot_extract(x, "html/1") @doc """ Returns the combined text of a result or the result's children, which may be an empty string. Once the text has been combined the whitespace is compacted by replacing all instances of more than one whitespace character with a single space and then trimmed. Nil input returns `nil`. ## Options * `:collapse_whitespace` - Boolean determining whether or not to replace blocks of whitespace with a single space character. Defaults to `true`. * `:trim` - Boolean determining whether or not to trim the resulting text. Defaults to `true`. ## Examples iex> import Meeseeks.CSS iex> result = Meeseeks.one("
Hello, World!
", css("div")) #Meeseeks.Result<{
Hello, World!
}> iex> Meeseeks.own_text(result) "Hello," """ @spec own_text(extractable, Keyword.t()) :: String.t() | nil def own_text(extractable, opts \\ []) def own_text(nil, _), do: nil def own_text(%Result{} = result, opts), do: Result.own_text(result, opts) def own_text(x, _), do: raise_cannot_extract(x, "own_text/1") @doc """ Returns a result's tag, or `nil` if the result represents a node without a tag. Nil input returns `nil`. ## Examples iex> import Meeseeks.CSS iex> result = Meeseeks.one("
Hi
", css("#example")) #Meeseeks.Result<{
Hi
}> iex> Meeseeks.tag(result) "div" """ @spec tag(extractable) :: String.t() | nil def tag(extractable) def tag(nil), do: nil def tag(%Result{} = result), do: Result.tag(result) def tag(x), do: raise_cannot_extract(x, "tag/1") @doc """ Returns the combined text of a result or the result's descendants, which may be an empty string. Once the text has been combined the whitespace is compacted by replacing all instances of more than one whitespace character with a single space and then trimmed. Nil input returns `nil`. ## Options * `:collapse_whitespace` - Boolean determining whether or not to replace blocks of whitespace with a single space character. Defaults to `true`. * `:trim` - Boolean determining whether or not to trim the resulting text. Defaults to `true`. ## Examples iex> import Meeseeks.CSS iex> result = Meeseeks.one("
Hello, World!
", css("div")) #Meeseeks.Result<{
Hello, World!
}> iex> Meeseeks.text(result) "Hello, World!" """ @spec text(extractable, Keyword.t()) :: String.t() | nil def text(extractable, opts \\ []) def text(nil, _), do: nil def text(%Result{} = result, opts), do: Result.text(result, opts) def text(x, _), do: raise_cannot_extract(x, "text/1") @doc """ Returns the `Meeseeks.TupleTree` of a document or result and its descendants. Nil input returns `nil`. ## Examples iex> import Meeseeks.CSS iex> document = Meeseeks.parse("
Hi
") iex> Meeseeks.tree(document) [{"html", [], [{"head", [], []}, {"body", [], [{"div", [{"id", "example"}], ["Hi"]}]}]}] iex> result = Meeseeks.one(document, css("#example")) #Meeseeks.Result<{
Hi
}> iex> Meeseeks.tree(result) {"div", [{"id", "example"}], ["Hi"]} """ @spec tree(extractable) :: TupleTree.t() | nil def tree(extractable) def tree(nil), do: nil def tree(%Document{} = document), do: Document.tree(document) def tree(%Result{} = result), do: Result.tree(result) def tree(x), do: raise_cannot_extract(x, "tree/1") defp raise_cannot_extract(target, extractor) do raise "Cannot run Meeseeks.#{extractor} on #{inspect(target)}" end end