@ -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 | |||
**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 | |||
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 | |||
def deps do | |||
[{:auto_linker, "~> 0.1.0"}] | |||
[{:auto_linker, "~> 0.1"}] | |||
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 | |||
use Mix.Project | |||
@version "0.1.0" | |||
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 | |||
# Configuration for the OTP application | |||
# | |||
# Type "mix help compile.app" for more information | |||
def application do | |||
# Specify extra applications you'll use from Erlang/Elixir | |||
[extra_applications: [:logger]] | |||
end | |||
# 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 | |||
[] | |||
[ | |||
{: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 |
@ -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 |