Refactor parser

Refactor parser
This commit is contained in:
Egor Kislitsyn 2019-06-21 20:09:48 +07:00
parent 76cfb574a3
commit 093d2344d2
4 changed files with 68 additions and 108 deletions

View file

@ -31,7 +31,6 @@ defmodule AutoLinker do
* `strip_prefix: true` - Strip the scheme prefix * `strip_prefix: true` - Strip the scheme prefix
* `exclude_class: false` - Set to a class name when you don't want urls auto linked in the html of the give class * `exclude_class: false` - Set to a class name when you don't want urls auto linked in the html of the give class
* `exclude_id: false` - Set to an element id when you don't want urls auto linked in the html of the give element * `exclude_id: false` - Set to an element id when you don't want urls auto linked in the html of the give element
* `exclude_patterns: ["```"]` - Don't link anything between the the pattern
* `email: false` - link email links * `email: false` - link email links
* `mention: false` - link @mentions (when `true`, requires `mention_prefix` or `mention_handler` options to be set) * `mention: false` - link @mentions (when `true`, requires `mention_prefix` or `mention_handler` options to be set)
* `mention_prefix: nil` - a prefix to build a link for a mention (example: `https://example.com/user/`) * `mention_prefix: nil` - a prefix to build a link for a mention (example: `https://example.com/user/`)

View file

@ -25,7 +25,10 @@ defmodule AutoLinker.Builder do
end end
defp build_attrs(attrs, _, opts, :rel) do defp build_attrs(attrs, _, opts, :rel) do
if rel = Map.get(opts, :rel, "noopener noreferrer"), do: [{:rel, rel} | attrs], else: attrs case Map.get(opts, :rel, "noopener noreferrer") do
rel when is_binary(rel) -> [{:rel, rel} | attrs]
_ -> attrs
end
end end
defp build_attrs(attrs, _, opts, :target) do defp build_attrs(attrs, _, opts, :target) do
@ -33,7 +36,10 @@ defmodule AutoLinker.Builder do
end end
defp build_attrs(attrs, _, opts, :class) do defp build_attrs(attrs, _, opts, :class) do
if cls = Map.get(opts, :class, "auto-linker"), do: [{:class, cls} | attrs], else: attrs case Map.get(opts, :class, "auto-linker") do
cls when is_binary(cls) -> [{:class, cls} | attrs]
_ -> attrs
end
end end
defp build_attrs(attrs, url, _opts, :href) do defp build_attrs(attrs, url, _opts, :href) do

View file

@ -46,7 +46,7 @@ defmodule AutoLinker.Parser do
@doc """ @doc """
Parse the given string, identifying items to link. Parse the given string, identifying items to link.
Parses the string, replacing the matching urls and phone numbers with an html link. Parses the string, replacing the matching urls with an html link.
## Examples ## Examples
@ -54,6 +54,8 @@ defmodule AutoLinker.Parser do
~s{Check out <a href="http://google.com" class="auto-linker" target="_blank" rel="noopener noreferrer">google.com</a>} ~s{Check out <a href="http://google.com" class="auto-linker" target="_blank" rel="noopener noreferrer">google.com</a>}
""" """
@types [:url, :email, :hashtag, :mention, :extra]
def parse(input, opts \\ %{}) def parse(input, opts \\ %{})
def parse(input, opts) when is_binary(input), do: {input, %{}} |> parse(opts) |> elem(0) def parse(input, opts) when is_binary(input), do: {input, %{}} |> parse(opts) |> elem(0)
def parse(input, list) when is_list(list), do: parse(input, Enum.into(list, %{})) def parse(input, list) when is_list(list), do: parse(input, Enum.into(list, %{}))
@ -61,157 +63,115 @@ defmodule AutoLinker.Parser do
def parse(input, opts) do def parse(input, opts) do
opts = Map.merge(@default_opts, opts) opts = Map.merge(@default_opts, opts)
Enum.reduce(opts, input, fn
{type, true}, input when type in @types ->
do_parse(input, opts, {"", "", :parsing}, type)
do_parse(input, Map.merge(config, opts)) _, input ->
end
defp do_parse(input, %{url: false} = opts), do: do_parse(input, Map.delete(opts, :url))
defp do_parse(input, %{hashtag: true} = opts) do
input input
|> do_parse(opts, {"", "", :parsing}, &check_and_link_hashtag/3) end)
|> do_parse(Map.delete(opts, :hashtag))
end end
defp do_parse(input, %{extra: true} = opts) do
input
|> do_parse(opts, {"", "", :parsing}, &check_and_link_extra/3)
|> do_parse(Map.delete(opts, :extra))
end
defp do_parse(input, %{email: true} = opts) do
input
|> do_parse(opts, {"", "", :parsing}, &check_and_link_email/3)
|> do_parse(Map.delete(opts, :email))
end
defp do_parse({text, user_acc}, %{url: _} = opts) do
input =
with exclude <- Map.get(opts, :exclude_patterns),
true <- is_list(exclude),
true <- String.starts_with?(text, exclude) do
{text, user_acc}
else
_ ->
do_parse(
{text, user_acc},
opts,
{"", "", :parsing},
&check_and_link/3
)
end
do_parse(input, Map.delete(opts, :url))
end
defp do_parse(input, %{mention: true} = opts) do
input
|> do_parse(opts, {"", "", :parsing}, &check_and_link_mention/3)
|> do_parse(Map.delete(opts, :mention))
end
defp do_parse(input, _), do: input
defp do_parse({"", user_acc}, _opts, {"", acc, _}, _handler), defp do_parse({"", user_acc}, _opts, {"", acc, _}, _handler),
do: {acc, user_acc} do: {acc, user_acc}
defp do_parse({"<a" <> text, user_acc}, opts, {buffer, acc, :parsing}, handler), defp do_parse({"<a" <> text, user_acc}, opts, {buffer, acc, :parsing}, type),
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "<a", :skip}, handler) do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "<a", :skip}, type)
defp do_parse({"<pre" <> text, user_acc}, opts, {buffer, acc, :parsing}, handler), defp do_parse({"<pre" <> text, user_acc}, opts, {buffer, acc, :parsing}, type),
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "<pre", :skip}, handler) do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "<pre", :skip}, type)
defp do_parse({"<code" <> text, user_acc}, opts, {buffer, acc, :parsing}, handler), defp do_parse({"<code" <> text, user_acc}, opts, {buffer, acc, :parsing}, type),
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "<code", :skip}, handler) do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "<code", :skip}, type)
defp do_parse({"</a>" <> text, user_acc}, opts, {buffer, acc, :skip}, handler), defp do_parse({"</a>" <> text, user_acc}, opts, {buffer, acc, :skip}, type),
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "</a>", :parsing}, handler) do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "</a>", :parsing}, type)
defp do_parse({"</pre>" <> text, user_acc}, opts, {buffer, acc, :skip}, handler), defp do_parse({"</pre>" <> text, user_acc}, opts, {buffer, acc, :skip}, type),
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "</pre>", :parsing}, handler) do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "</pre>", :parsing}, type)
defp do_parse({"</code>" <> text, user_acc}, opts, {buffer, acc, :skip}, handler), defp do_parse({"</code>" <> text, user_acc}, opts, {buffer, acc, :skip}, type),
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "</code>", :parsing}, handler) do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "</code>", :parsing}, type)
defp do_parse({"<" <> text, user_acc}, opts, {"", acc, :parsing}, handler), defp do_parse({"<" <> text, user_acc}, opts, {"", acc, :parsing}, type),
do: do_parse({text, user_acc}, opts, {"<", acc, {:open, 1}}, handler) do: do_parse({text, user_acc}, opts, {"<", acc, {:open, 1}}, type)
defp do_parse({"<" <> text, user_acc}, opts, {"", acc, {:html, level}}, handler) do defp do_parse({"<" <> text, user_acc}, opts, {"", acc, {:html, level}}, type) do
do_parse({text, user_acc}, opts, {"<", acc, {:open, level + 1}}, handler) do_parse({text, user_acc}, opts, {"<", acc, {:open, level + 1}}, type)
end end
defp do_parse({">" <> text, user_acc}, opts, {buffer, acc, {:attrs, level}}, handler), defp do_parse({">" <> text, user_acc}, opts, {buffer, acc, {:attrs, level}}, type),
do: do:
do_parse( do_parse(
{text, user_acc}, {text, user_acc},
opts, opts,
{"", acc <> buffer <> ">", {:html, level}}, {"", acc <> buffer <> ">", {:html, level}},
handler type
) )
defp do_parse({<<ch::8>> <> text, user_acc}, opts, {"", acc, {:attrs, level}}, handler) do defp do_parse({<<ch::8>> <> text, user_acc}, opts, {"", acc, {:attrs, level}}, type) do
do_parse({text, user_acc}, opts, {"", acc <> <<ch::8>>, {:attrs, level}}, handler) do_parse({text, user_acc}, opts, {"", acc <> <<ch::8>>, {:attrs, level}}, type)
end end
defp do_parse({"</" <> text, user_acc}, opts, {buffer, acc, {:html, level}}, handler) do defp do_parse({"</" <> text, user_acc}, opts, {buffer, acc, {:html, level}}, type) do
{buffer, user_acc} = run_handler(handler, buffer, opts, user_acc) {buffer, user_acc} = link(type, buffer, opts, user_acc)
do_parse( do_parse(
{text, user_acc}, {text, user_acc},
opts, opts,
{"", acc <> buffer <> "</", {:close, level}}, {"", acc <> buffer <> "</", {:close, level}},
handler type
) )
end end
defp do_parse({">" <> text, user_acc}, opts, {buffer, acc, {:close, 1}}, handler), defp do_parse({">" <> text, user_acc}, opts, {buffer, acc, {:close, 1}}, type),
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> ">", :parsing}, handler) do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> ">", :parsing}, type)
defp do_parse({">" <> text, user_acc}, opts, {buffer, acc, {:close, level}}, handler), defp do_parse({">" <> text, user_acc}, opts, {buffer, acc, {:close, level}}, type),
do: do:
do_parse( do_parse(
{text, user_acc}, {text, user_acc},
opts, opts,
{"", acc <> buffer <> ">", {:html, level - 1}}, {"", acc <> buffer <> ">", {:html, level - 1}},
handler type
) )
defp do_parse({text, user_acc}, opts, {buffer, acc, {:open, level}}, handler) do defp do_parse({text, user_acc}, opts, {buffer, acc, {:open, level}}, type) do
do_parse({text, user_acc}, opts, {"", acc <> buffer, {:attrs, level}}, handler) do_parse({text, user_acc}, opts, {"", acc <> buffer, {:attrs, level}}, type)
end end
defp do_parse( defp do_parse(
{<<char::bytes-size(1), text::binary>>, user_acc}, {<<char::bytes-size(1), text::binary>>, user_acc},
opts, opts,
{buffer, acc, state}, {buffer, acc, state},
handler type
) )
when char in [" ", "\r", "\n"] do when char in [" ", "\r", "\n"] do
{buffer, user_acc} = run_handler(handler, buffer, opts, user_acc) {buffer, user_acc} = link(type, buffer, opts, user_acc)
do_parse( do_parse(
{text, user_acc}, {text, user_acc},
opts, opts,
{"", acc <> buffer <> char, state}, {"", acc <> buffer <> char, state},
handler type
) )
end end
defp do_parse({<<ch::8>>, user_acc}, opts, {buffer, acc, state}, handler) do defp do_parse({<<ch::8>>, user_acc}, opts, {buffer, acc, state}, type) do
{buffer, user_acc} = run_handler(handler, buffer <> <<ch::8>>, opts, user_acc) {buffer, user_acc} = link(type, buffer <> <<ch::8>>, opts, user_acc)
do_parse( do_parse(
{"", user_acc}, {"", user_acc},
opts, opts,
{"", acc <> buffer, state}, {"", acc <> buffer, state},
handler type
) )
end end
defp do_parse({<<ch::8>> <> text, user_acc}, opts, {buffer, acc, state}, handler), defp do_parse({<<ch::8>> <> text, user_acc}, opts, {buffer, acc, state}, type),
do: do_parse({text, user_acc}, opts, {buffer <> <<ch::8>>, acc, state}, handler) do: do_parse({text, user_acc}, opts, {buffer <> <<ch::8>>, acc, state}, type)
def check_and_link(buffer, opts, _user_acc) do def check_and_link(:url, buffer, opts, _user_acc) do
str = strip_parens(buffer) str = strip_parens(buffer)
if url?(str, opts) do if url?(str, opts) do
@ -224,36 +184,36 @@ defmodule AutoLinker.Parser do
end end
end end
defp strip_parens("(" <> buffer) do def check_and_link(:email, buffer, opts, _user_acc) do
~r/[^\)]*/ |> Regex.run(buffer) |> hd()
end
defp strip_parens(buffer), do: buffer
def check_and_link_email(buffer, opts, _user_acc) do
if email?(buffer, opts), do: link_email(buffer, opts), else: buffer if email?(buffer, opts), do: link_email(buffer, opts), else: buffer
end end
def check_and_link_mention(buffer, opts, user_acc) do def check_and_link(:mention, buffer, opts, user_acc) do
buffer buffer
|> match_mention |> match_mention
|> link_mention(buffer, opts, user_acc) |> link_mention(buffer, opts, user_acc)
end end
def check_and_link_hashtag(buffer, opts, user_acc) do def check_and_link(:hashtag, buffer, opts, user_acc) do
buffer buffer
|> match_hashtag |> match_hashtag
|> link_hashtag(buffer, opts, user_acc) |> link_hashtag(buffer, opts, user_acc)
end end
def check_and_link_extra("xmpp:" <> handle, opts, _user_acc) do def check_and_link(:extra, "xmpp:" <> handle, opts, _user_acc) do
if email?(handle, opts), do: link_extra("xmpp:" <> handle, opts), else: handle if email?(handle, opts), do: link_extra("xmpp:" <> handle, opts), else: handle
end end
def check_and_link_extra(buffer, opts, _user_acc) do def check_and_link(:extra, buffer, opts, _user_acc) do
if String.starts_with?(buffer, @prefix_extra), do: link_extra(buffer, opts), else: buffer if String.starts_with?(buffer, @prefix_extra), do: link_extra(buffer, opts), else: buffer
end end
defp strip_parens("(" <> buffer) do
~r/[^\)]*/ |> Regex.run(buffer) |> hd()
end
defp strip_parens(buffer), do: buffer
# @doc false # @doc false
def url?(buffer, opts) do def url?(buffer, opts) do
@ -363,8 +323,8 @@ defmodule AutoLinker.Parser do
Builder.create_extra_link(buffer, opts) Builder.create_extra_link(buffer, opts)
end end
defp run_handler(handler, buffer, opts, user_acc) do defp link(type, buffer, opts, user_acc) do
case handler.(buffer, opts, user_acc) do case check_and_link(type, buffer, opts, user_acc) do
{buffer, user_acc} -> {buffer, user_acc} {buffer, user_acc} -> {buffer, user_acc}
buffer -> {buffer, user_acc} buffer -> {buffer, user_acc}
end end

View file

@ -157,11 +157,6 @@ defmodule AutoLinker.ParserTest do
assert parse(text, class: false, rel: false, new_window: false) == expected assert parse(text, class: false, rel: false, new_window: false) == expected
end end
test "excludes html with specified class" do
text = "```Check out <div class='section'>google.com</div>```"
assert parse(text, exclude_patterns: ["```"]) == text
end
test "do not link parens" do test "do not link parens" do
text = " foo (https://example.com/path/folder/), bar" text = " foo (https://example.com/path/folder/), bar"