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) <> "" <> 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 google.com" + assert parse(text) == text + text = "Check out google.com" + 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