commit: b1f6c04eefa67c891d05f87a54ccc41b41b2d2b0
parent 62652612c9ebe1bf6a8caff8526c6761d102f306
Author: Justin Tormey <jrtormey@gmail.com>
Date: Mon, 27 Jul 2020 10:40:44 -0500
Add parser support for iodata and safe iodata
Diffstat:
4 files changed, 168 insertions(+), 80 deletions(-)
diff --git a/lib/linkify.ex b/lib/linkify.ex
@@ -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
diff --git a/lib/linkify/builder.ex b/lib/linkify/builder.ex
@@ -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()
+ |> format_tag("##{tag}", opts)
+ end
+
+ 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()
+ |> format_tag(uri, opts)
end
- def format_hashtag(attrs, tag, _opts) do
- attrs = format_attrs(attrs)
- "<a #{attrs}>##{tag}</a>"
+ def format_tag(attrs, content, %{iodata: true}) do
+ ["<a ", attrs, ">", content, "</a>"]
end
- def format_email(attrs, email, _opts) do
- attrs = format_attrs(attrs)
- ~s(<a #{attrs}>#{email}</a>)
+ def format_tag(attrs, content, %{iodata: :safe}) do
+ [{:safe, ["<a ", attrs, ">"]}, content, {:safe, "</a>"}]
end
- def format_extra(attrs, uri, _opts) do
- attrs = format_attrs(attrs)
- ~s(<a #{attrs}>#{uri}</a>)
+ def format_tag(attrs, content, _opts) do
+ "<a #{attrs}>#{content}</a>"
end
end
diff --git a/lib/linkify/parser.ex b/lib/linkify/parser.ex
@@ -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
diff --git a/test/linkify_test.exs b/test/linkify_test.exs
@@ -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>", []}