diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c0ba3c7
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
index 7b37982..0bf11f1 100644
--- a/README.md
+++ b/README.md
@@ -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")
+"google.com"
+
+iex> AutoLinker.link("google.com", new_window: false, rel: false)
+"google.com"
+
+iex> AutoLinker.link("google.com", new_window: false, rel: false, class: false)
+"google.com"
+```
+
+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.
diff --git a/lib/auto_linker.ex b/lib/auto_linker.ex
index 993f2ce..301be8b 100644
--- a/lib/auto_linker.ex
+++ b/lib/auto_linker.ex
@@ -3,7 +3,7 @@ defmodule AutoLinker do
Create url links from text containing urls.
Turns an input string like `"Check out google.com"` into
- `Check out `"google.com"`
+ `Check out "google.com"`
## Examples
@@ -21,6 +21,27 @@ defmodule AutoLinker do
@doc """
Auto link a string.
+
+ Options:
+
+ * `class: "auto-linker"` - specify the class to be added to the generated link. false to clear
+ * `rel: "noopener noreferrer"` - override the rel attribute. false to clear
+ * `new_window: true` - set to false to remove `target='_blank'` attribute
+ * `scheme: false` - Set to true to link urls with schema `http://google`
+ * `truncate: false` - Set to a number to truncate urls longer then the number. Truncated urls will end in `..`
+ * `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_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
+
+ Each of the above options can be specified when calling `link(text, opts)`
+ or can be set in the `:auto_linker's configuration. For example:
+
+ config :auto_linker,
+ class: false,
+ new_window: false
+
+ Note that passing opts to `link/2` will override the configuration settings.
"""
def link(text, opts \\ []) do
opts =
@@ -28,7 +49,7 @@ defmodule AutoLinker do
|> Application.get_all_env()
|> Keyword.merge(opts)
- parse text, opts
+ parse text, Enum.into(opts, %{})
end
diff --git a/lib/auto_linker/builder.ex b/lib/auto_linker/builder.ex
new file mode 100644
index 0000000..b55d818
--- /dev/null
+++ b/lib/auto_linker/builder.ex
@@ -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(" ")
+ "" <> url <> ""
+ 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
diff --git a/lib/auto_linker/parser.ex b/lib/auto_linker/parser.ex
new file mode 100644
index 0000000..7cef83f
--- /dev/null
+++ b/lib/auto_linker/parser.ex
@@ -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 google.com"
+ """
+
+ 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(<> <> text, scheme, opts, {"", acc, {:attrs, level}}),
+ do: parse(text, scheme, opts, {"", acc <> <>, {: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(<> <> text, scheme, opts, {buffer, acc, state}),
+ do: parse(text, scheme, opts, {buffer <> <>, 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
diff --git a/mix.exs b/mix.exs
index df878bd..7e2780e 100644
--- a/mix.exs
+++ b/mix.exs
@@ -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
diff --git a/mix.lock b/mix.lock
new file mode 100644
index 0000000..46af344
--- /dev/null
+++ b/mix.lock
@@ -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]}]}}
diff --git a/test/parser_test.exs b/test/parser_test.exs
new file mode 100644
index 0000000..3553ddb
--- /dev/null
+++ b/test/parser_test.exs
@@ -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 google"
+ assert parse(text) == text
+ text = "Check out "
+ assert parse(text) == text
+ text = "Check out "
+ assert parse(text) == text
+ end
+
+ test "links url inside html" do
+ text = "Check out google.com
"
+ expected = "Check out "
+ assert parse(text, class: false, rel: false, new_window: false) == expected
+ end
+
+ test "excludes html with specified class" do
+ text = "```Check out google.com
```"
+ 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