Browse Source

prep for first release

merge-requests/1/head
Stephen M. Pallen 3 years ago
parent
commit
59d41265fc
8 changed files with 355 additions and 26 deletions
  1. +21
    -0
      LICENSE
  2. +35
    -7
      README.md
  3. +23
    -2
      lib/auto_linker.ex
  4. +59
    -0
      lib/auto_linker/builder.ex
  5. +96
    -0
      lib/auto_linker/parser.ex
  6. +27
    -17
      mix.exs
  7. +2
    -0
      mix.lock
  8. +92
    -0
      test/parser_test.exs

+ 21
- 0
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.

+ 35
- 7
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")
"<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.

+ 23
- 2
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 `"<a href='http://google.com' target='_blank' rel='noopener noreferrer'>google.com</a>"`
`Check out "<a href='http://google.com' target='_blank' rel='noopener noreferrer'>google.com</a>"`
## 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

+ 59
- 0
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(" ")
"<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

+ 96
- 0
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 <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

+ 27
- 17
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

+ 2
- 0
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]}]}}

+ 92
- 0
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 <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

Loading…
Cancel
Save