Skip to content

Commit 57435c8

Browse files
committed
First working version.
1 parent 6e1903b commit 57435c8

10 files changed

+339
-0
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/_build
2+
/deps
3+
erl_crash.dump
4+
*.ez
5+
.idea/

README.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Slugger
2+
3+
This package provides a library and a protocol to create [slugs](http://en.wikipedia.org/wiki/Semantic_URL#Slug) for given strings.
4+
5+
By default, a slug will be containing _only_ chars `a-z0-9` and the default seperator `-`.
6+
7+
## Library
8+
9+
Using the library is straightforward, check out a few examples:
10+
11+
```elixir
12+
iex(1)> Slugger.slugify " a b c "
13+
"a-b-c"
14+
15+
iex(2)> Slugger.slugify "A cool title of a blog post"
16+
"a-cool-title-of-a-blog-post"
17+
18+
iex(3)> Slugger.slugify "A cool title of a blog post, wikipedia style", ?_
19+
"a_cool_title_of_a_blog_post_wikipedia_style"
20+
```
21+
22+
## Protocol
23+
24+
Next to the library, a protocol is provided to ease creating slugs for own data structures.
25+
By default, the protocol will try to run `Slugger.slugify(Kernel.to_string(your_data))`, so if your_data implents `String.Chars` that returned string will be slugified.
26+
If you want to provide a own way to create a slug, check out the following example:
27+
28+
```elixir
29+
iex(4)> defmodule User do
30+
...(4)> defstruct name: "Julius Beckmann"
31+
...(4)> end
32+
33+
iex(5)> defimpl Slugify, for: User do
34+
...(5)> def slugify(user), do: Slugger.slugify(user.name)
35+
...(5)> end
36+
37+
iex(6)> Slugify.slugify %User{}
38+
"julius-beckmann"
39+
```
40+
41+
## Replacements
42+
43+
Special chars like `äöüéáÁÉ` will be replaced by rules given in the file [lib/replacements.exs](lib/replacements.exs).
44+
45+
Modify that file if you need have own replacement rules and __recompile__.

config/config.exs

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# This file is responsible for configuring your application
2+
# and its dependencies with the aid of the Mix.Config module.
3+
use Mix.Config
4+
5+
# This configuration is loaded before any dependency and is restricted
6+
# to this project. If another project depends on this project, this
7+
# file won't be loaded nor affect the parent project. For this reason,
8+
# if you want to provide default values for your application for third-
9+
# party users, it should be done in your mix.exs file.
10+
11+
# Sample configuration:
12+
#
13+
# config :logger, :console,
14+
# level: :info,
15+
# format: "$date $time [$level] $metadata$message\n",
16+
# metadata: [:user_id]
17+
18+
# It is also possible to import configuration files, relative to this
19+
# directory. For example, you can emulate configuration per environment
20+
# by uncommenting the line below and defining dev.exs, test.exs and such.
21+
# Configuration from the imported file will override the ones defined
22+
# here (which is why it is important to import them last).
23+
#
24+
# import_config "#{Mix.env}.exs"

lib/replacements.exs

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
[
2+
# This file was generated from these two files:
3+
# https://github.com/easybook/slugger/blob/master/src/Easybook/Slugger.php
4+
# https://github.com/keyvanakbary/slugifier/blob/master/src/Slugifier.php
5+
6+
# Latin
7+
{, '0'},{, 'ae'},{, 'ae'},{, 'A'},{, 'A'},{, 'A'},{, 'A'},{, 'A'},{, 'A'},{, 'A'},
8+
{, 'A'},{, 'AE'},{, 'AE'},{, 'a'},{, 'a'},{, 'a'},{, 'a'},{, 'a'},{, 'a'},{, 'a'},
9+
{, 'a'},{, 'a'},{?@, 'at'},{, 'C'},{, 'C'},{, 'c'},{, 'c'},{, 'c'},{, 'D'},{, 'D'},
10+
{, 'd'},{, 'd'},{, 'E'},{, 'E'},{, 'E'},{, 'E'},{, 'E'},{, 'E'},{, 'e'},{, 'e'},
11+
{, 'e'},{, 'e'},{, 'e'},{, 'e'},{, 'f'},{, 'G'},{, 'G'},{, 'g'},{, 'g'},{, 'H'},
12+
{, 'H'},{, 'h'},{, 'h'},{, 'I'},{, 'I'},{, 'I'},{, 'I'},{, 'I'},{, 'I'},{, 'I'},
13+
{, 'I'},{, 'IJ'},{, 'i'},{, 'i'},{, 'i'},{, 'i'},{, 'i'},{, 'i'},{, 'i'},{, 'i'},
14+
{, 'ij'},{, 'J'},{, 'j'},{, 'L'},{, 'L'},{?Ŀ, 'L'},{, 'l'},{, 'l'},{, 'l'},{, 'N'},
15+
{, 'n'},{, 'n'},{, 'O'},{, 'O'},{, 'O'},{, 'O'},{, 'O'},{, 'O'},{, 'O'},{, 'O'},
16+
{, 'O'},{, 'O'},{, 'OE'},{, 'o'},{, 'o'},{, 'o'},{, 'o'},{, 'o'},{, 'o'},{, 'o'},
17+
{, 'o'},{, 'o'},{?ǿ, 'o'},{, 'o'},{, 'oe'},{, 'R'},{, 'R'},{, 'r'},{, 'r'},{, 'S'},
18+
{, 'S'},{, 's'},{, 's'},{?ſ, 's'},{, 'T'},{, 'T'},{, 'T'},{, 'TH'},{, 't'},{, 't'},
19+
{, 't'},{, 'th'},{, 'U'},{, 'U'},{, 'U'},{, 'U'},{, 'U'},{, 'U'},{, 'U'},{, 'U'},
20+
{, 'U'},{, 'U'},{, 'U'},{, 'U'},{, 'U'},{, 'u'},{, 'u'},{, 'u'},{, 'u'},{, 'u'},
21+
{, 'u'},{, 'u'},{, 'u'},{, 'u'},{, 'u'},{, 'u'},{, 'u'},{, 'u'},{, 'W'},{, 'w'},
22+
{, 'Y'},{, 'Y'},{, 'Y'},{, 'y'},{?ÿ, 'y'},{, 'y'},{, 'C'},{, 'O'},{, 'c'},{, 'o'},
23+
24+
# Arabic
25+
{, 'a'},{, 'b'},{, 't'},{, 'th'},{, 'g'},{, 'h'},{, 'kh'},{, 'd'},{, 'th'},{, 'r'},
26+
{, 'z'},{, 's'},{, 'sh'},{, 's'},{, 'd'},{, 't'},{, 'th'},{, 'aa'},{, 'gh'},{, 'f'},
27+
{, 'k'},{, 'k'},{, 'l'},{, 'm'},{, 'n'},{, 'h'},{, 'o'},{, 'y'},
28+
29+
# Croatian
30+
{, 'D'},{, 'd'},
31+
32+
# Czech
33+
{, 'C'},{, 'D'},{, 'E'},{, 'N'},{, 'R'},{, 'S'},{, 'T'},{, 'U'},{, 'Z'},{, 'c'},
34+
{, 'd'},{, 'e'},{, 'n'},{, 'r'},{, 's'},{, 't'},{, 'u'},{, 'z'},
35+
36+
# Esperanto
37+
{, 'C'},{, 'c'},{, 'G'},{, 'g'},{, 'H'},{, 'h'},{, 'J'},{, 'j'},{, 'S'},{, 's'},
38+
{, 'U'},{, 'u'},
39+
40+
# German
41+
{, 'AE'},{, 'OE'},{, 'UE'},{, 'ss'},{, 'ae'},{, 'oe'},{, 'ue'},
42+
43+
# Greek
44+
{, 'A'},{, 'B'},{, 'G'},{, 'D'},{, 'E'},{, 'Z'},{, 'H'},{, '8'},{, 'I'},{, 'K'},
45+
{, 'L'},{, 'M'},{, 'N'},{, '3'},{, 'O'},{, 'P'},{, 'R'},{, 'S'},{, 'T'},{, 'Y'},
46+
{, 'F'},{, 'X'},{, 'PS'},{, 'W'},{, 'I'},{, 'Y'},{, 'a'},{, 'e'},{, 'h'},{, 'i'},
47+
{, 'y'},{, 'a'},{, 'b'},{, 'g'},{, 'd'},{, 'e'},{, 'z'},{, 'h'},{, '8'},{, 'i'},
48+
{, 'k'},{, 'l'},{, 'm'},{, 'n'},{, '3'},{?ο, 'o'},{, 'p'},{, 'r'},{, 's'},{, 's'},
49+
{, 't'},{, 'y'},{, 'f'},{, 'x'},{, 'ps'},{, 'w'},{, 'i'},{, 'y'},{, 'o'},{, 'y'},
50+
{, 'w'},{, 'b'},{, 'th'},{, 'Y'},{, 'A'},{, 'E'},{, 'I'},{, 'O'},{, 'Y'},{, 'H'},
51+
{, 'W'},{, 'i'},
52+
53+
# Latvian
54+
{, 'A'},{, 'E'},{, 'G'},{, 'I'},{, 'K'},{, 'L'},{, 'N'},{, 'U'},{, 'r'},{, 'u'},
55+
{, 'a'},{, 'e'},{, 'g'},{, 'i'},{, 'k'},{, 'l'},{, 'n'},{, 'R'},{, 'O'},{, 'o'},
56+
57+
# Lithuanian
58+
{, 'E'},{, 'e'},{, 'I'},{, 'i'},{, 'I'},{, 'i'},{, 'U'},{, 'u'},
59+
60+
# Maltese
61+
{, 'C'},{, 'c'},{, 'G'},{, 'g'},{, 'H'},{, 'h'},
62+
63+
# Polish
64+
{, 'A'},{, 'C'},{, 'E'},{, 'L'},{, 'N'},{, 'O'},{, 'S'},{, 'Z'},{, 'Z'},{, 'a'},
65+
{, 'c'},{, 'e'},{, 'l'},{, 'n'},{, 'o'},{, 's'},{, 'z'},{, 'z'},
66+
67+
# Russian
68+
{, 'A'},{, 'B'},{, 'V'},{, 'G'},{, 'D'},{, 'E'},{, 'E'},{, 'Zh'},{, 'Z'},{, 'I'},
69+
{, 'J'},{, 'K'},{, 'L'},{, 'M'},{, 'N'},{, 'O'},{, 'P'},{, 'R'},{, 'S'},{, 'T'},
70+
{, 'U'},{, 'F'},{, 'H'},{, 'C'},{, 'Ch'},{, 'Sh'},{, 'Shch'},{, ''},{, 'Y'},{, ''},
71+
{, 'E'},{, 'Ju'},{, 'Ja'},{, 'a'},{, 'b'},{, 'v'},{, 'g'},{, 'd'},{, 'e'},{, 'e'},
72+
{, 'zh'},{, 'z'},{, 'i'},{, 'j'},{, 'k'},{, 'l'},{, 'm'},{, 'n'},{, 'o'},{?п, 'p'},
73+
{, 'r'},{, 's'},{, 't'},{, 'u'},{, 'f'},{, 'h'},{, 'c'},{, 'ch'},{, 'sh'},{, 'shch'},
74+
{, ''},{, 'y'},{, ''},{, 'e'},{, 'ju'},{, 'ja'},
75+
76+
# Sami
77+
{, 'N'},{, 'n'},{, 'G'},{, 'g'},{, 'K'},{, 'k'},{, 'Z'},{, 'z'},{, 'Z'},{, 'z'},
78+
79+
# Serbian
80+
{, 'dj'},{, 'j'},{, 'lj'},{, 'nj'},{, 'c'},{, 'dz'},{, 'Dj'},{, 'j'},{, 'Lj'},{, 'Nj'},
81+
{, 'C'},{, 'Dz'},
82+
83+
# Slovak
84+
{, 'L'},{, 'l'},{, 'L'},{, 'l'},{, 'R'},{, 'r'},
85+
86+
# Turkish
87+
{, 'C'},{, 'G'},{, 'I'},{, 'S'},{, 'c'},{, 'g'},{, 'i'},{, 's'},
88+
89+
# Ukrainian
90+
{, 'Ye'},{, 'I'},{, 'Ji'},{, 'G'},{, 'ye'},{, 'i'},{, 'ji'},{, 'g'},
91+
92+
# Vietnamese
93+
{?ạ, 'a'},{?ả, 'a'},{?ầ, 'a'},{?ấ, 'a'},{?ậ, 'a'},{?ẩ, 'a'},{?ẫ, 'a'},{?ằ, 'a'},{?ắ, 'a'},{?ặ, 'a'},
94+
{?ẳ, 'a'},{?ẵ, 'a'},{?ẹ, 'e'},{?ẻ, 'e'},{?ẽ, 'e'},{?ề, 'e'},{?ế, 'e'},{?ệ, 'e'},{?ể, 'e'},{?ễ, 'e'},
95+
{?ị, 'i'},{?ỉ, 'i'},{?ọ, 'o'},{?ỏ, 'o'},{?ồ, 'o'},{?ố, 'o'},{?ộ, 'o'},{?ổ, 'o'},{?ỗ, 'o'},{?ờ, 'o'},
96+
{?ớ, 'o'},{?ợ, 'o'},{?ở, 'o'},{?ỡ, 'o'},{?ụ, 'u'},{?ủ, 'u'},{?ừ, 'u'},{?ứ, 'u'},{?ự, 'u'},{?ử, 'u'},
97+
{?ữ, 'u'},{?ỳ, 'y'},{?ỵ, 'y'},{?ỷ, 'y'},{?ỹ, 'y'},{?Ạ, 'A'},{?Ả, 'A'},{?Ầ, 'A'},{?Ấ, 'A'},{?Ậ, 'A'},
98+
{?Ẩ, 'A'},{?Ẫ, 'A'},{?Ằ, 'A'},{?Ắ, 'A'},{?Ặ, 'A'},{?Ẳ, 'A'},{?Ẵ, 'A'},{?Ẹ, 'E'},{?Ẻ, 'E'},{?Ẽ, 'E'},
99+
{?Ề, 'E'},{?Ế, 'E'},{?Ệ, 'E'},{?Ể, 'E'},{?Ễ, 'E'},{?Ị, 'I'},{?Ỉ, 'I'},{?Ọ, 'O'},{?Ỏ, 'O'},{?Ồ, 'O'},
100+
{?Ố, 'O'},{?Ộ, 'O'},{?Ổ, 'O'},{?Ỗ, 'O'},{?Ờ, 'O'},{?Ớ, 'O'},{?Ợ, 'O'},{?Ở, 'O'},{?Ỡ, 'O'},{?Ụ, 'U'},
101+
{?Ủ, 'U'},{?Ừ, 'U'},{?Ứ, 'U'},{?Ự, 'U'},{?Ử, 'U'},{?Ữ, 'U'},{?Ỳ, 'Y'},{?Ỵ, 'Y'},{?Ỷ, 'Y'},{?Ỹ, 'Y'},
102+
103+
# Other
104+
{, '1'},{, '2'},{, '3'},{, 'P'}
105+
]
106+

lib/slugger.ex

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
defmodule Slugger do
2+
@moduledoc """
3+
Calcualtes a 'slug' for a given string.
4+
Such a slug can be used for reading URLs or Search Engine Optimization.
5+
"""
6+
7+
@doc """
8+
Return a string in form of a slug for a given string.
9+
10+
## Examples
11+
iex> Slugger.slugify(" Hi # there ")
12+
"hi-there"
13+
14+
iex> Slugger.slugify("Über den Wölkchen draußen im Tore")
15+
"ueber-den-woelkchen-draussen-im-tore"
16+
17+
iex> Slugger.slugify("Wikipedia Style", ?_)
18+
"wikipedia_style"
19+
20+
iex> Slugger.slugify("_trimming_and___removing_inside___")
21+
"trimming-and-removing-inside"
22+
"""
23+
def slugify(text, separator \\ ?-) do
24+
text
25+
|> replace_special_chars
26+
|> normalize
27+
|> replace_unwanted_chars(separator)
28+
end
29+
30+
defp normalize(text) do
31+
text
32+
|> String.downcase
33+
end
34+
35+
defp replace_unwanted_chars(text, separator) do
36+
text
37+
|> String.replace(~r/([^a-z0-9])+/, to_string([separator]))
38+
|> String.strip(separator)
39+
end
40+
41+
defp replace_special_chars(text) do
42+
text |> to_char_list |> replace_chars |> to_string
43+
end
44+
45+
#-- Generated function `replace_chars` below ---
46+
47+
# Generate replacement functions using pattern matching.
48+
{replacements, _} = Code.eval_file("replacements.exs", __DIR__)
49+
for {search, replace} <- replacements do
50+
defp replace_chars([unquote(search)|t]), do: unquote(replace) ++ replace_chars(t)
51+
end
52+
53+
# A unmatched char will be kept.
54+
defp replace_chars([h|t]), do: [h] ++ replace_chars(t)
55+
56+
# String has come to an end, stop recursion here.
57+
defp replace_chars([]), do: []
58+
59+
end

lib/slugify.ex

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
defprotocol Slugify do
3+
@fallback_to_any true
4+
5+
@doc "Returns the slug for the given data."
6+
def slugify(data)
7+
end
8+
9+
defimpl Slugify, for: Any do
10+
11+
@doc "Default handler for anything that implements String.Chars Protocol."
12+
def slugify(data), do: Slugger.slugify(Kernel.to_string data)
13+
end

mix.exs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
defmodule Slugger.Mixfile do
2+
use Mix.Project
3+
4+
def project do
5+
[app: :slugger,
6+
version: "0.0.1",
7+
elixir: "~> 1.0",
8+
deps: deps]
9+
end
10+
11+
# Configuration for the OTP application
12+
#
13+
# Type `mix help compile.app` for more information
14+
def application do
15+
[applications: []]
16+
end
17+
18+
# Dependencies can be Hex packages:
19+
#
20+
# {:mydep, "~> 0.3.0"}
21+
#
22+
# Or git/path repositories:
23+
#
24+
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
25+
#
26+
# Type `mix help deps` for more examples and options
27+
defp deps do
28+
[]
29+
end
30+
end

test/slugger_test.exs

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
defmodule SluggerTest do
2+
use ExUnit.Case
3+
doctest Slugger
4+
5+
test "the truth" do
6+
assert 1 + 1 == 2
7+
end
8+
9+
test "string to lower" do
10+
assert Slugger.slugify("ABC") == "abc"
11+
end
12+
13+
test "removing space at beginning" do
14+
assert Slugger.slugify(" \t \n ABC") == "abc"
15+
end
16+
17+
test "removing space at ending" do
18+
assert Slugger.slugify("ABC \n \t \n ") == "abc"
19+
end
20+
21+
test "removing space at ending and ending" do
22+
assert Slugger.slugify(" \n \t \n ABC \n \t \n ") == "abc"
23+
end
24+
25+
test "replace whitespace inside with seperator" do
26+
assert Slugger.slugify(" A B C ") == "a-b-c"
27+
assert Slugger.slugify(" A B C ", ?_) == "a_b_c"
28+
end
29+
30+
test "replace multiple seperator inside and outside" do
31+
assert Slugger.slugify("--a--b c - - - ") == "a-b-c"
32+
end
33+
34+
test "replace special char with expected string" do
35+
assert Slugger.slugify("üba") == "ueba"
36+
assert Slugger.slugify("büa") == "buea"
37+
assert Slugger.slugify("baü") == "baue"
38+
assert Slugger.slugify("büaü") == "bueaue"
39+
end
40+
41+
end

test/slugify_test.exs

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
defmodule SlugifyTest do
2+
use ExUnit.Case
3+
doctest Slugify
4+
5+
test "using protocol fallback to any" do
6+
assert Slugify.slugify(42) == "42"
7+
assert Slugify.slugify(3.1415) == "3-1415"
8+
assert Slugify.slugify("hello") == "hello"
9+
assert Slugify.slugify(:hello) == "hello"
10+
assert Slugify.slugify('hello') == "hello"
11+
assert Slugify.slugify([]) == ""
12+
assert Slugify.slugify(["hello", "world"]) == "helloworld"
13+
end
14+
15+
end

test/test_helper.exs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ExUnit.start()

0 commit comments

Comments
 (0)