@ -0,0 +1,21 @@ | |||||
The MIT License (MIT) | |||||
Copyright (c) 2017 E-MetroTel | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights | |||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
copies of the Software, and to permit persons to whom the Software is | |||||
furnished to do so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in all | |||||
copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. |
@ -1,19 +1,47 @@ | |||||
# AutoLinker | # AutoLinker | ||||
**TODO: Add description** | |||||
[![Build Status](https://travis-ci.org/smpallen99/coherence.png?branch=master)](https://travis-ci.org/smpallen99/coherence) [![Hex Version][hex-img]][hex] [![License][license-img]][license] | |||||
[hex-img]: https://img.shields.io/hexpm/v/coherence.svg | |||||
[hex]: https://hex.pm/packages/coherence | |||||
[license-img]: http://img.shields.io/badge/license-MIT-brightgreen.svg | |||||
[license]: http://opensource.org/licenses/MIT | |||||
AutoLinker is a basic package for turning website names into links. | |||||
Use this package in your web view to convert web references into click-able links. | |||||
This is a very early version. Some of the described options are not yet functional. | |||||
## Installation | ## Installation | ||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed | |||||
by adding `auto_linker` to your list of dependencies in `mix.exs`: | |||||
The package can be installed by adding `auto_linker` to your list of dependencies in `mix.exs`: | |||||
```elixir | ```elixir | ||||
def deps do | def deps do | ||||
[{:auto_linker, "~> 0.1.0"}] | |||||
[{:auto_linker, "~> 0.1"}] | |||||
end | end | ||||
``` | ``` | ||||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) | |||||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can | |||||
be found at [https://hexdocs.pm/auto_linker](https://hexdocs.pm/auto_linker). | |||||
## Usage | |||||
``` | |||||
iex> AutoLinker.link("google.com") | |||||
"<a href='http://google.com' class='auto-linker' target='_blank' rel='noopener noreferrer'>google.com</a>" | |||||
iex> AutoLinker.link("google.com", new_window: false, rel: false) | |||||
"<a href='http://google.com' class='auto-linker'>google.com</a>" | |||||
iex> AutoLinker.link("google.com", new_window: false, rel: false, class: false) | |||||
"<a href='http://google.com'>google.com</a>" | |||||
``` | |||||
See the docs for more examples | |||||
## License | |||||
`auto_linker` is Copyright (c) 2017 E-MetroTel | |||||
The source is released under the MIT License. | |||||
Check [LICENSE](LICENSE) for more information. |
@ -0,0 +1,59 @@ | |||||
defmodule AutoLinker.Builder do | |||||
@moduledoc """ | |||||
Module for building the auto generated link. | |||||
""" | |||||
@doc """ | |||||
Create a link. | |||||
""" | |||||
def create_link(url, opts) do | |||||
[] | |||||
|> build_attrs(url, opts, :rel) | |||||
|> build_attrs(url, opts, :target) | |||||
|> build_attrs(url, opts, :class) | |||||
|> build_attrs(url, opts, :scheme) | |||||
|> format_url(url, opts) | |||||
end | |||||
defp build_attrs(attrs, _, opts, :rel) do | |||||
if rel = Map.get(opts, :rel, "noopener noreferrer"), | |||||
do: [{:rel, rel} | attrs], else: attrs | |||||
end | |||||
defp build_attrs(attrs, _, opts, :target) do | |||||
if Map.get(opts, :new_window, true), | |||||
do: [{:target, :_blank} | attrs], else: attrs | |||||
end | |||||
defp build_attrs(attrs, _, opts, :class) do | |||||
if cls = Map.get(opts, :class, "auto-linker"), | |||||
do: [{:class, cls} | attrs], else: attrs | |||||
end | |||||
defp build_attrs(attrs, url, _opts, :scheme) do | |||||
if String.starts_with?(url, ["http://", "https://"]), | |||||
do: [{:href, url} | attrs], else: [{:href, "http://" <> url} | attrs] | |||||
end | |||||
defp format_url(attrs, url, opts) do | |||||
url = | |||||
url | |||||
|> strip_prefix(Map.get(opts, :strip_prefix, true)) | |||||
|> truncate(Map.get(opts, :truncate, false)) | |||||
attrs = | |||||
attrs | |||||
|> Enum.map(fn {key, value} -> ~s(#{key}='#{value}') end) | |||||
|> Enum.join(" ") | |||||
"<a #{attrs}>" <> url <> "</a>" | |||||
end | |||||
defp truncate(url, false), do: url | |||||
defp truncate(url, len) when len < 3, do: url | |||||
defp truncate(url, len) do | |||||
if String.length(url) > len, do: String.slice(url, 0, len - 2) <> "..", else: url | |||||
end | |||||
defp strip_prefix(url, true) do | |||||
url | |||||
|> String.replace(~r/^https?:\/\//, "") | |||||
|> String.replace(~r/^www\./, "") | |||||
end | |||||
defp strip_prefix(url, _), do: url | |||||
end |
@ -0,0 +1,96 @@ | |||||
defmodule AutoLinker.Parser do | |||||
@moduledoc """ | |||||
Module to handle parsing the the input string. | |||||
""" | |||||
alias AutoLinker.Builder | |||||
@doc """ | |||||
Parse the given string. | |||||
Parses the string, replacing the matching urls with an html link. | |||||
## Examples | |||||
iex> AutoLinker.Parser.parse("Check out google.com") | |||||
"Check out <a href='http://google.com' class='auto-linker' target='_blank' rel='noopener noreferrer'>google.com</a>" | |||||
""" | |||||
def parse(text, opts \\ %{}) | |||||
def parse(text, list) when is_list(list), do: parse(text, Enum.into(list, %{})) | |||||
def parse(text, opts) do | |||||
if (exclude = Map.get(opts, :exclude_pattern, false)) && String.starts_with?(text, exclude) do | |||||
text | |||||
else | |||||
parse(text, Map.get(opts, :scheme, false), opts, {"", "", :parsing}) | |||||
end | |||||
end | |||||
# state = {buffer, acc, state} | |||||
defp parse("", _scheme, _opts ,{"", acc, _}), | |||||
do: acc | |||||
defp parse("", scheme, opts ,{buffer, acc, _}), | |||||
do: acc <> check_and_link(buffer, scheme, opts) | |||||
defp parse("<" <> text, scheme, opts, {"", acc, :parsing}), | |||||
do: parse(text, scheme, opts, {"<", acc, {:open, 1}}) | |||||
defp parse(">" <> text, scheme, opts, {buffer, acc, {:attrs, level}}), | |||||
do: parse(text, scheme, opts, {"", acc <> buffer <> ">", {:html, level}}) | |||||
defp parse(<<ch::8>> <> text, scheme, opts, {"", acc, {:attrs, level}}), | |||||
do: parse(text, scheme, opts, {"", acc <> <<ch::8>>, {:attrs, level}}) | |||||
defp parse("</" <> text, scheme, opts, {buffer, acc, {:html, level}}), | |||||
do: parse(text, scheme, opts, | |||||
{"", acc <> check_and_link(buffer, scheme, opts) <> "</", {:close, level}}) | |||||
defp parse(">" <> text, scheme, opts, {buffer, acc, {:close, 1}}), | |||||
do: parse(text, scheme, opts, {"", acc <> buffer <> ">", :parsing}) | |||||
defp parse(">" <> text, scheme, opts, {buffer, acc, {:close, level}}), | |||||
do: parse(text, scheme, opts, {"", acc <> buffer <> ">", {:html, level - 1}}) | |||||
defp parse(" " <> text, scheme, opts, {buffer, acc, {:open, level}}), | |||||
do: parse(text, scheme, opts, {"", acc <> buffer <> " ", {:attrs, level}}) | |||||
defp parse("\n" <> text, scheme, opts, {buffer, acc, {:open, level}}), | |||||
do: parse(text, scheme, opts, {"", acc <> buffer <> "\n", {:attrs, level}}) | |||||
# default cases where state is not important | |||||
defp parse(" " <> text, scheme, opts, {buffer, acc, state}), | |||||
do: parse(text, scheme, opts, | |||||
{"", acc <> check_and_link(buffer, scheme, opts) <> " ", state}) | |||||
defp parse("\n" <> text, scheme, opts, {buffer, acc, state}), | |||||
do: parse(text, scheme, opts, | |||||
{"", acc <> check_and_link(buffer, scheme, opts) <> "\n", state}) | |||||
defp parse(<<ch::8>> <> text, scheme, opts, {buffer, acc, state}), | |||||
do: parse(text, scheme, opts, {buffer <> <<ch::8>>, acc, state}) | |||||
defp check_and_link(buffer, scheme, opts) do | |||||
buffer | |||||
|> is_url?(scheme) | |||||
|> link_url(buffer, opts) | |||||
end | |||||
@doc false | |||||
def is_url?(buffer, true) do | |||||
re = ~r{^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$} | |||||
Regex.match? re, buffer | |||||
end | |||||
def is_url?(buffer, _) do | |||||
re = ~r{^[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$} | |||||
Regex.match? re, buffer | |||||
end | |||||
@doc false | |||||
def link_url(true, buffer, opts) do | |||||
Builder.create_link(buffer, opts) | |||||
end | |||||
def link_url(_, buffer, _opts), do: buffer | |||||
end |
@ -1,33 +1,43 @@ | |||||
defmodule AutoLinker.Mixfile do | defmodule AutoLinker.Mixfile do | ||||
use Mix.Project | use Mix.Project | ||||
@version "0.1.0" | |||||
def project do | def project do | ||||
[app: :auto_linker, | |||||
version: "0.1.0", | |||||
elixir: "~> 1.4", | |||||
build_embedded: Mix.env == :prod, | |||||
start_permanent: Mix.env == :prod, | |||||
deps: deps()] | |||||
[ | |||||
app: :auto_linker, | |||||
version: @version, | |||||
elixir: "~> 1.4", | |||||
build_embedded: Mix.env == :prod, | |||||
start_permanent: Mix.env == :prod, | |||||
deps: deps(), | |||||
docs: [extras: ["README.md"]], | |||||
package: package(), | |||||
name: "AutoLinker", | |||||
description: """ | |||||
AutoLinker is a basic package for turning website names into links. | |||||
""" | |||||
] | |||||
end | end | ||||
# Configuration for the OTP application | # Configuration for the OTP application | ||||
# | |||||
# Type "mix help compile.app" for more information | |||||
def application do | def application do | ||||
# Specify extra applications you'll use from Erlang/Elixir | # Specify extra applications you'll use from Erlang/Elixir | ||||
[extra_applications: [:logger]] | [extra_applications: [:logger]] | ||||
end | end | ||||
# Dependencies can be Hex packages: | # Dependencies can be Hex packages: | ||||
# | |||||
# {:my_dep, "~> 0.3.0"} | |||||
# | |||||
# Or git/path repositories: | |||||
# | |||||
# {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} | |||||
# | |||||
# Type "mix help deps" for more examples and options | |||||
defp deps do | defp deps do | ||||
[] | |||||
[ | |||||
{:ex_doc, "~> 0.15", only: :dev}, | |||||
{:earmark, "~> 1.2", only: :dev, override: true}, | |||||
] | |||||
end | |||||
defp package do | |||||
[ maintainers: ["Stephen Pallen"], | |||||
licenses: ["MIT"], | |||||
links: %{ "Github" => "https://github.com/smpallen99/auto_linker" }, | |||||
files: ~w(lib priv web README.md mix.exs LICENSE)] | |||||
end | end | ||||
end | end |
@ -0,0 +1,2 @@ | |||||
%{"earmark": {:hex, :earmark, "1.2.0", "bf1ce17aea43ab62f6943b97bd6e3dc032ce45d4f787504e3adf738e54b42f3a", [:mix], []}, | |||||
"ex_doc": {:hex, :ex_doc, "0.15.0", "e73333785eef3488cf9144a6e847d3d647e67d02bd6fdac500687854dd5c599f", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}} |
@ -0,0 +1,92 @@ | |||||
defmodule AutoLinker.ParserTest do | |||||
use ExUnit.Case | |||||
doctest AutoLinker.Parser | |||||
import AutoLinker.Parser | |||||
describe "is_url" do | |||||
test "valid scheme true" do | |||||
valid_scheme_urls() | |||||
|> Enum.each(fn url -> | |||||
assert is_url?(url, true) | |||||
end) | |||||
end | |||||
test "invalid scheme true" do | |||||
invalid_scheme_urls() | |||||
|> Enum.each(fn url -> | |||||
refute is_url?(url, true) | |||||
end) | |||||
end | |||||
test "valid scheme false" do | |||||
valid_non_scheme_urls() | |||||
|> Enum.each(fn url -> | |||||
assert is_url?(url, false) | |||||
end) | |||||
end | |||||
test "invalid scheme false" do | |||||
invalid_non_scheme_urls() | |||||
|> Enum.each(fn url -> | |||||
refute is_url?(url, false) | |||||
end) | |||||
end | |||||
end | |||||
describe "parse" do | |||||
test "does not link attributes" do | |||||
text = "Check out <a href='google.com'>google</a>" | |||||
assert parse(text) == text | |||||
text = "Check out <img src='google.com' alt='google.com'/>" | |||||
assert parse(text) == text | |||||
text = "Check out <span><img src='google.com' alt='google.com'/></span>" | |||||
assert parse(text) == text | |||||
end | |||||
test "links url inside html" do | |||||
text = "Check out <div class='section'>google.com</div>" | |||||
expected = "Check out <div class='section'><a href='http://google.com'>google.com</a></div>" | |||||
assert parse(text, class: false, rel: false, new_window: false) == expected | |||||
end | |||||
test "excludes html with specified class" do | |||||
text = "```Check out <div class='section'>google.com</div>```" | |||||
assert parse(text, exclude_pattern: "```") == text | |||||
end | |||||
end | |||||
def valid_scheme_urls, do: [ | |||||
"https://www.example.com", | |||||
"http://www2.example.com", | |||||
"http://home.example-site.com", | |||||
"http://blog.example.com", | |||||
"http://www.example.com/product", | |||||
"http://www.example.com/products?id=1&page=2", | |||||
"http://www.example.com#up", | |||||
"http://255.255.255.255", | |||||
"http://www.site.com:8008" | |||||
] | |||||
def invalid_scheme_urls, do: [ | |||||
"http://invalid.com/perl.cgi?key= | http://web-site.com/cgi-bin/perl.cgi?key1=value1&key2", | |||||
] | |||||
def valid_non_scheme_urls, do: [ | |||||
"www.example.com", | |||||
"www2.example.com", | |||||
"www.example.com:2000", | |||||
"www.example.com?abc=1", | |||||
"example.example-site.com", | |||||
"example.com", | |||||
"example.ca", | |||||
"example.tv", | |||||
"example.com:999?one=one", | |||||
"255.255.255.255", | |||||
"255.255.255.255:3000?one=1&two=2", | |||||
] | |||||
def invalid_non_scheme_urls, do: [ | |||||
"invalid.com/perl.cgi?key= | web-site.com/cgi-bin/perl.cgi?key1=value1&key2", | |||||
"invalid." | |||||
] | |||||
end |