Current section

Files

Jump to
robotxt lib robotxt.ex
Raw

lib/robotxt.ex

defmodule Robotxt do
@moduledoc """
Robots.txt parser.
## Installation
Add the following to the `deps` portion of your `mix.exs` file.
```
defp deps do
[
{:robotxt, "~> 0.1.2"},
]
end
```
"""
defstruct user_agent: nil, disallow: [], allow: [], sitemap: nil
@type t :: %Robotxt{
user_agent: nil | binary,
disallow: list | list(binary),
allow: list | list(binary),
sitemap: nil | binary
}
@user_agent_regex Regex.compile!("user-agent:\s", "ui")
@comments_regex Regex.compile!("#.+\n?")
@split_regex Regex.compile!(":\s")
@doc """
Returns a list of `%Robotxt{}`.
## Example
iex> Robotxt.parse("User-agent: *\\nDisallow:\\n")
[%Robotxt{user_agent: "*", allow: [], disallow: [], sitemap: nil}]
iex> Robotxt.parse("User-agent: *\\ndisallow:\\n")
[%Robotxt{user_agent: "*", allow: [], disallow: [], sitemap: nil}]
"""
@spec parse(binary) :: list(%Robotxt{})
def parse(body) when is_binary(body) do
Regex.replace(@comments_regex, body, "")
|> String.split(@user_agent_regex, trim: true)
|> Stream.map(&String.split(&1, "\n", trim: true))
|> Stream.map(&List.pop_at(&1, 0))
|> Stream.map(&transform/1)
|> Enum.to_list()
end
@doc """
Returns the `%Robotxt{}` for the given `user_agent` if it exists. Otherwise, `nil` is returned.
## Example
iex> Robotxt.get_by_user_agent([%Robotxt{user_agent: "Twitterbot"}], "Twitterbot")
%Robotxt{user_agent: "Twitterbot", disallow: [], allow: [], sitemap: nil}
iex> Robotxt.get_by_user_agent([%Robotxt{user_agent: "Twitterbot"}], "nope")
nil
"""
@spec get_by_user_agent(list(%Robotxt{}), binary) :: %Robotxt{} | nil
def get_by_user_agent(list, user_agent) when is_list(list) and is_binary(user_agent) do
Enum.filter(list, &(&1.user_agent == user_agent)) |> List.first()
end
#
## Helper functions
#
defp transform({user_agent, values}) do
user_agent = String.trim(user_agent)
values =
values
|> Stream.map(&String.split(&1, @split_regex))
|> Enum.map(&downcase/1)
transform(values, %Robotxt{user_agent: user_agent})
end
defp transform([[field, value] | tail], %Robotxt{} = state) do
new_state = update_state([field, value], state)
transform(tail, new_state)
end
defp transform([[_]], %Robotxt{} = state), do: state
defp transform([], %Robotxt{} = state), do: state
defp downcase([field, value]), do: [String.downcase(field), value]
defp downcase([field]), do: [String.downcase(field)]
defp update_state([field, value], %Robotxt{disallow: disallow, allow: allow} = state) do
case field do
"disallow" ->
%Robotxt{state | disallow: [value | disallow]}
"allow" ->
%Robotxt{state | allow: [value | allow]}
"sitemap" ->
%Robotxt{state | sitemap: value}
_ ->
state
end
end
end