Add parser support for iodata and safe iodata
This commit is contained in:
parent
62652612c9
commit
b1f6c04eef
4 changed files with 168 additions and 80 deletions
|
@ -40,11 +40,20 @@ defmodule Linkify do
|
|||
* `hashtag_handler: nil` - a custom handler to validate and formart a hashtag
|
||||
* `extra: false` - link urls with rarely used schemes (magnet, ipfs, irc, etc.)
|
||||
* `validate_tld: true` - Set to false to disable TLD validation for urls/emails, also can be set to :no_scheme to validate TLDs only for urls without a scheme (e.g `example.com` will be validated, but `http://example.loki` won't)
|
||||
* `iodata` - Set to `true` to return iodata as a result, or `:safe` for iodata with linkified anchor tags wrapped in Phoenix.HTML `:safe` tuples (removes need for further sanitization)
|
||||
"""
|
||||
def link(text, opts \\ []) do
|
||||
parse(text, opts)
|
||||
end
|
||||
|
||||
def link_to_iodata(text, opts \\ []) do
|
||||
parse(text, Keyword.merge(opts, iodata: true))
|
||||
end
|
||||
|
||||
def link_safe(text, opts \\ []) do
|
||||
parse(text, Keyword.merge(opts, iodata: :safe))
|
||||
end
|
||||
|
||||
def link_map(text, acc, opts \\ []) do
|
||||
parse({text, acc}, opts)
|
||||
end
|
||||
|
|
|
@ -56,8 +56,9 @@ defmodule Linkify.Builder do
|
|||
|> strip_prefix(Map.get(opts, :strip_prefix, false))
|
||||
|> truncate(Map.get(opts, :truncate, false))
|
||||
|
||||
attrs = format_attrs(attrs)
|
||||
"<a #{attrs}>#{url}</a>"
|
||||
attrs
|
||||
|> format_attrs()
|
||||
|> format_tag(url, opts)
|
||||
end
|
||||
|
||||
defp format_attrs(attrs) do
|
||||
|
@ -123,23 +124,39 @@ defmodule Linkify.Builder do
|
|||
|> format_extra(uri, opts)
|
||||
end
|
||||
|
||||
def format_mention(attrs, name, _opts) do
|
||||
attrs = format_attrs(attrs)
|
||||
"<a #{attrs}>@#{name}</a>"
|
||||
def format_mention(attrs, name, opts) do
|
||||
attrs
|
||||
|> format_attrs()
|
||||
|> format_tag("@#{name}", opts)
|
||||
end
|
||||
|
||||
def format_hashtag(attrs, tag, _opts) do
|
||||
attrs = format_attrs(attrs)
|
||||
"<a #{attrs}>##{tag}</a>"
|
||||
def format_hashtag(attrs, tag, opts) do
|
||||
attrs
|
||||
|> format_attrs()
|
||||
|> format_tag("##{tag}", opts)
|
||||
end
|
||||
|
||||
def format_email(attrs, email, _opts) do
|
||||
attrs = format_attrs(attrs)
|
||||
~s(<a #{attrs}>#{email}</a>)
|
||||
def format_email(attrs, email, opts) do
|
||||
attrs
|
||||
|> format_attrs()
|
||||
|> format_tag(email, opts)
|
||||
end
|
||||
|
||||
def format_extra(attrs, uri, _opts) do
|
||||
attrs = format_attrs(attrs)
|
||||
~s(<a #{attrs}>#{uri}</a>)
|
||||
def format_extra(attrs, uri, opts) do
|
||||
attrs
|
||||
|> format_attrs()
|
||||
|> format_tag(uri, opts)
|
||||
end
|
||||
|
||||
def format_tag(attrs, content, %{iodata: true}) do
|
||||
["<a ", attrs, ">", content, "</a>"]
|
||||
end
|
||||
|
||||
def format_tag(attrs, content, %{iodata: :safe}) do
|
||||
[{:safe, ["<a ", attrs, ">"]}, content, {:safe, "</a>"}]
|
||||
end
|
||||
|
||||
def format_tag(attrs, content, _opts) do
|
||||
"<a #{attrs}>#{content}</a>"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -62,135 +62,142 @@ defmodule Linkify.Parser do
|
|||
|
||||
def parse(input, opts) do
|
||||
opts = Map.merge(@default_opts, opts)
|
||||
opts_list = Map.to_list(opts)
|
||||
|
||||
Enum.reduce(@types, input, fn
|
||||
type, input ->
|
||||
if {type, true} in opts_list do
|
||||
do_parse(input, opts, {"", "", :parsing}, type)
|
||||
else
|
||||
input
|
||||
end
|
||||
end)
|
||||
acc = if opts[:iodata], do: [], else: ""
|
||||
do_parse(input, opts, {"", acc, :parsing})
|
||||
end
|
||||
|
||||
defp do_parse({"", user_acc}, _opts, {"", acc, _}, _handler),
|
||||
defp accumulate(acc, buffer) when is_list(acc),
|
||||
do: [buffer | acc]
|
||||
|
||||
defp accumulate(acc, buffer) when is_binary(acc),
|
||||
do: acc <> buffer
|
||||
|
||||
defp accumulate(acc, buffer, trailing) when is_list(acc),
|
||||
do: [trailing, buffer | acc]
|
||||
|
||||
defp accumulate(acc, buffer, trailing) when is_binary(acc),
|
||||
do: acc <> buffer <> trailing
|
||||
|
||||
defp do_parse({"", user_acc}, _opts, {"", acc, _}) when is_list(acc),
|
||||
do: {Enum.reverse(acc), user_acc}
|
||||
|
||||
defp do_parse({"", user_acc}, _opts, {"", acc, _}) when is_binary(acc),
|
||||
do: {acc, user_acc}
|
||||
|
||||
defp do_parse({"@" <> text, user_acc}, opts, {buffer, acc, :skip}, type),
|
||||
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "@", :skip}, type)
|
||||
defp do_parse({"@" <> text, user_acc}, opts, {buffer, acc, :skip}),
|
||||
do: do_parse({text, user_acc}, opts, {"", accumulate(acc, buffer, "@"), :skip})
|
||||
|
||||
defp do_parse({"<a" <> text, user_acc}, opts, {buffer, acc, :parsing}, type),
|
||||
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "<a", :skip}, type)
|
||||
defp do_parse({"<a" <> text, user_acc}, opts, {buffer, acc, :parsing}),
|
||||
do: do_parse({text, user_acc}, opts, {"", accumulate(acc, buffer, "<a"), :skip})
|
||||
|
||||
defp do_parse({"<pre" <> text, user_acc}, opts, {buffer, acc, :parsing}, type),
|
||||
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "<pre", :skip}, type)
|
||||
defp do_parse({"<pre" <> text, user_acc}, opts, {buffer, acc, :parsing}),
|
||||
do: do_parse({text, user_acc}, opts, {"", accumulate(acc, buffer, "<pre"), :skip})
|
||||
|
||||
defp do_parse({"<code" <> text, user_acc}, opts, {buffer, acc, :parsing}, type),
|
||||
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "<code", :skip}, type)
|
||||
defp do_parse({"<code" <> text, user_acc}, opts, {buffer, acc, :parsing}),
|
||||
do: do_parse({text, user_acc}, opts, {"", accumulate(acc, buffer, "<code"), :skip})
|
||||
|
||||
defp do_parse({"</a>" <> text, user_acc}, opts, {buffer, acc, :skip}, type),
|
||||
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "</a>", :parsing}, type)
|
||||
defp do_parse({"</a>" <> text, user_acc}, opts, {buffer, acc, :skip}),
|
||||
do: do_parse({text, user_acc}, opts, {"", accumulate(acc, buffer, "</a>"), :parsing})
|
||||
|
||||
defp do_parse({"</pre>" <> text, user_acc}, opts, {buffer, acc, :skip}, type),
|
||||
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "</pre>", :parsing}, type)
|
||||
defp do_parse({"</pre>" <> text, user_acc}, opts, {buffer, acc, :skip}),
|
||||
do: do_parse({text, user_acc}, opts, {"", accumulate(acc, buffer, "</pre>"), :parsing})
|
||||
|
||||
defp do_parse({"</code>" <> text, user_acc}, opts, {buffer, acc, :skip}, type),
|
||||
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> "</code>", :parsing}, type)
|
||||
defp do_parse({"</code>" <> text, user_acc}, opts, {buffer, acc, :skip}),
|
||||
do: do_parse({text, user_acc}, opts, {"", accumulate(acc, buffer, "</code>"), :parsing})
|
||||
|
||||
defp do_parse({"<" <> text, user_acc}, opts, {"", acc, :parsing}, type),
|
||||
do: do_parse({text, user_acc}, opts, {"<", acc, {:open, 1}}, type)
|
||||
defp do_parse({"<" <> text, user_acc}, opts, {"", acc, :parsing}),
|
||||
do: do_parse({text, user_acc}, opts, {"<", acc, {:open, 1}})
|
||||
|
||||
defp do_parse({"<" <> text, user_acc}, opts, {"", acc, {:html, level}}, type) do
|
||||
do_parse({text, user_acc}, opts, {"<", acc, {:open, level + 1}}, type)
|
||||
defp do_parse({"<" <> text, user_acc}, opts, {"", acc, {:html, level}}) do
|
||||
do_parse({text, user_acc}, opts, {"<", acc, {:open, level + 1}})
|
||||
end
|
||||
|
||||
defp do_parse({">" <> text, user_acc}, opts, {buffer, acc, {:attrs, level}}, type),
|
||||
defp do_parse({">" <> text, user_acc}, opts, {buffer, acc, {:attrs, level}}),
|
||||
do:
|
||||
do_parse(
|
||||
{text, user_acc},
|
||||
opts,
|
||||
{"", acc <> buffer <> ">", {:html, level}},
|
||||
type
|
||||
{"", accumulate(acc, buffer, ">"), {:html, level}}
|
||||
)
|
||||
|
||||
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}}, type)
|
||||
defp do_parse({<<ch::8>> <> text, user_acc}, opts, {"", acc, {:attrs, level}}) do
|
||||
do_parse({text, user_acc}, opts, {"", accumulate(acc, <<ch::8>>), {:attrs, level}})
|
||||
end
|
||||
|
||||
defp do_parse({"</" <> text, user_acc}, opts, {buffer, acc, {:html, level}}, type) do
|
||||
{buffer, user_acc} = link(type, buffer, opts, user_acc)
|
||||
defp do_parse({"</" <> text, user_acc}, opts, {buffer, acc, {:html, level}}) do
|
||||
{buffer, user_acc} = link(buffer, opts, user_acc)
|
||||
|
||||
do_parse(
|
||||
{text, user_acc},
|
||||
opts,
|
||||
{"", acc <> buffer <> "</", {:close, level}},
|
||||
type
|
||||
{"", accumulate(acc, buffer, "</"), {:close, level}}
|
||||
)
|
||||
end
|
||||
|
||||
defp do_parse({">" <> text, user_acc}, opts, {buffer, acc, {:close, 1}}, type),
|
||||
do: do_parse({text, user_acc}, opts, {"", acc <> buffer <> ">", :parsing}, type)
|
||||
defp do_parse({">" <> text, user_acc}, opts, {buffer, acc, {:close, 1}}),
|
||||
do: do_parse({text, user_acc}, opts, {"", accumulate(acc, buffer, ">"), :parsing})
|
||||
|
||||
defp do_parse({">" <> text, user_acc}, opts, {buffer, acc, {:close, level}}, type),
|
||||
defp do_parse({">" <> text, user_acc}, opts, {buffer, acc, {:close, level}}),
|
||||
do:
|
||||
do_parse(
|
||||
{text, user_acc},
|
||||
opts,
|
||||
{"", acc <> buffer <> ">", {:html, level - 1}},
|
||||
type
|
||||
{"", accumulate(acc, buffer, ">"), {:html, level - 1}}
|
||||
)
|
||||
|
||||
defp do_parse({text, user_acc}, opts, {buffer, acc, {:open, level}}, type) do
|
||||
do_parse({text, user_acc}, opts, {"", acc <> buffer, {:attrs, level}}, type)
|
||||
defp do_parse({text, user_acc}, opts, {buffer, acc, {:open, level}}) do
|
||||
do_parse({text, user_acc}, opts, {"", acc <> buffer, {:attrs, level}})
|
||||
end
|
||||
|
||||
defp do_parse(
|
||||
{<<char::bytes-size(1), text::binary>>, user_acc},
|
||||
opts,
|
||||
{buffer, acc, state},
|
||||
type
|
||||
{buffer, acc, state}
|
||||
)
|
||||
when char in [" ", "\r", "\n"] do
|
||||
{buffer, user_acc} = link(type, buffer, opts, user_acc)
|
||||
{buffer, user_acc} = link(buffer, opts, user_acc)
|
||||
|
||||
do_parse(
|
||||
{text, user_acc},
|
||||
opts,
|
||||
{"", acc <> buffer <> char, state},
|
||||
type
|
||||
{"", accumulate(acc, buffer, char), state}
|
||||
)
|
||||
end
|
||||
|
||||
defp do_parse({<<ch::8>>, user_acc}, opts, {buffer, acc, state}, type) do
|
||||
{buffer, user_acc} = link(type, buffer <> <<ch::8>>, opts, user_acc)
|
||||
defp do_parse({<<ch::8>>, user_acc}, opts, {buffer, acc, state}) do
|
||||
{buffer, user_acc} = link(buffer <> <<ch::8>>, opts, user_acc)
|
||||
|
||||
do_parse(
|
||||
{"", user_acc},
|
||||
opts,
|
||||
{"", acc <> buffer, state},
|
||||
type
|
||||
{"", accumulate(acc, buffer), state}
|
||||
)
|
||||
end
|
||||
|
||||
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}, type)
|
||||
defp do_parse({<<ch::8>> <> text, user_acc}, opts, {buffer, acc, state}),
|
||||
do: do_parse({text, user_acc}, opts, {buffer <> <<ch::8>>, acc, state})
|
||||
|
||||
def check_and_link(:url, buffer, opts, _user_acc) do
|
||||
str = strip_parens(buffer)
|
||||
|
||||
if url?(str, opts) do
|
||||
case @match_url |> Regex.run(str, capture: [:url]) |> hd() do
|
||||
^buffer -> link_url(buffer, opts)
|
||||
url -> String.replace(buffer, url, link_url(url, opts))
|
||||
^buffer ->
|
||||
link_url(buffer, opts)
|
||||
|
||||
url ->
|
||||
buffer
|
||||
|> String.split(url)
|
||||
|> Enum.intersperse(link_url(url, opts))
|
||||
|> (if opts[:iodata], do: & &1, else: & Enum.join(&1)).()
|
||||
end
|
||||
else
|
||||
buffer
|
||||
:nomatch
|
||||
end
|
||||
end
|
||||
|
||||
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: :nomatch
|
||||
end
|
||||
|
||||
def check_and_link(:mention, buffer, opts, user_acc) do
|
||||
|
@ -210,7 +217,7 @@ defmodule Linkify.Parser do
|
|||
end
|
||||
|
||||
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: :nomatch
|
||||
end
|
||||
|
||||
defp strip_parens("(" <> buffer) do
|
||||
|
@ -272,7 +279,7 @@ defmodule Linkify.Parser do
|
|||
end
|
||||
end
|
||||
|
||||
def link_hashtag(nil, buffer, _, _user_acc), do: buffer
|
||||
def link_hashtag(nil, _buffer, _, _user_acc), do: :nomatch
|
||||
|
||||
def link_hashtag(hashtag, buffer, %{hashtag_handler: hashtag_handler} = opts, user_acc) do
|
||||
hashtag
|
||||
|
@ -286,7 +293,7 @@ defmodule Linkify.Parser do
|
|||
|> maybe_update_buffer(hashtag, buffer)
|
||||
end
|
||||
|
||||
def link_mention(nil, buffer, _, user_acc), do: {buffer, user_acc}
|
||||
def link_mention(nil, _buffer, _, _user_acc), do: :nomatch
|
||||
|
||||
def link_mention(mention, buffer, %{mention_handler: mention_handler} = opts, user_acc) do
|
||||
mention
|
||||
|
@ -326,10 +333,23 @@ defmodule Linkify.Parser do
|
|||
Builder.create_extra_link(buffer, opts)
|
||||
end
|
||||
|
||||
defp link(type, buffer, opts, user_acc) do
|
||||
defp link(buffer, opts, user_acc) do
|
||||
opts_list = Map.to_list(opts)
|
||||
|
||||
Enum.reduce_while @types, {buffer, user_acc}, fn type, _ ->
|
||||
if {type, true} in opts_list do
|
||||
check_and_link_reducer(type, buffer, opts, user_acc)
|
||||
else
|
||||
{:cont, {buffer, user_acc}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp check_and_link_reducer(type, buffer, opts, user_acc) do
|
||||
case check_and_link(type, buffer, opts, user_acc) do
|
||||
{buffer, user_acc} -> {buffer, user_acc}
|
||||
buffer -> {buffer, user_acc}
|
||||
:nomatch -> {:cont, {buffer, user_acc}}
|
||||
{buffer, user_acc} -> {:halt, {buffer, user_acc}}
|
||||
buffer -> {:halt, {buffer, user_acc}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,16 @@ defmodule LinkifyTest do
|
|||
"<a href=\"http://google.com\">google.com</a>"
|
||||
end
|
||||
|
||||
test "default link iodata" do
|
||||
assert Linkify.link_to_iodata("google.com") ==
|
||||
[["<a ", "href=\"http://google.com\"", ">", "google.com", "</a>"]]
|
||||
end
|
||||
|
||||
test "default link safe iodata" do
|
||||
assert Linkify.link_safe("google.com") ==
|
||||
[[{:safe, ["<a ", "href=\"http://google.com\"", ">"]}, "google.com", {:safe, "</a>"}]]
|
||||
end
|
||||
|
||||
test "does on link existing links" do
|
||||
text = ~s(<a href="http://google.com">google.com</a>)
|
||||
assert Linkify.link(text) == text
|
||||
|
@ -24,16 +34,38 @@ defmodule LinkifyTest do
|
|||
) == expected
|
||||
end
|
||||
|
||||
test "all kinds of links iodata" do
|
||||
text = "hello google.com https://ddg.com user@email.com irc:///mIRC"
|
||||
|
||||
expected =
|
||||
["hello", " ", ["<a ", "href=\"http://google.com\"", ">", "google.com", "</a>"], " ", ["<a ", "href=\"https://ddg.com\"", ">", "https://ddg.com", "</a>"], " ", ["<a ", "href=\"mailto:user@email.com\"", ">", "user@email.com", "</a>"], " ", ["<a ", "href=\"irc:///mIRC\"", ">", "irc:///mIRC", "</a>"]]
|
||||
|
||||
assert Linkify.link_to_iodata(text,
|
||||
email: true,
|
||||
extra: true
|
||||
) == expected
|
||||
end
|
||||
|
||||
test "class attribute" do
|
||||
assert Linkify.link("google.com", class: "linkified") ==
|
||||
"<a href=\"http://google.com\" class=\"linkified\">google.com</a>"
|
||||
end
|
||||
|
||||
test "class attribute iodata" do
|
||||
assert Linkify.link_to_iodata("google.com", class: "linkified") ==
|
||||
[["<a ", "href=\"http://google.com\" class=\"linkified\"", ">", "google.com", "</a>"]]
|
||||
end
|
||||
|
||||
test "rel attribute" do
|
||||
assert Linkify.link("google.com", rel: "noopener noreferrer") ==
|
||||
"<a href=\"http://google.com\" rel=\"noopener noreferrer\">google.com</a>"
|
||||
end
|
||||
|
||||
test "rel attribute iodata" do
|
||||
assert Linkify.link_to_iodata("google.com", rel: "noopener noreferrer") ==
|
||||
[["<a ", "href=\"http://google.com\" rel=\"noopener noreferrer\"", ">", "google.com", "</a>"]]
|
||||
end
|
||||
|
||||
test "rel as function" do
|
||||
text = "google.com"
|
||||
|
||||
|
@ -54,6 +86,16 @@ defmodule LinkifyTest do
|
|||
assert Linkify.link(text, rel: custom_rel) == expected
|
||||
end
|
||||
|
||||
test "strip parens" do
|
||||
assert Linkify.link("(google.com)") ==
|
||||
"(<a href=\"http://google.com\">google.com</a>)"
|
||||
end
|
||||
|
||||
test "strip parens iodata" do
|
||||
assert Linkify.link_to_iodata("(google.com)") ==
|
||||
[["(", ["<a ", "href=\"http://google.com\"", ">", "google.com", "</a>"], ")"]]
|
||||
end
|
||||
|
||||
test "link_map/2" do
|
||||
assert Linkify.link_map("google.com", []) ==
|
||||
{"<a href=\"http://google.com\">google.com</a>", []}
|
||||
|
|
Loading…
Reference in a new issue