changed CHANGELOG.md
 
@@ -1,6 +1,12 @@
1
- # CHANGELOG
1
+ # Changelog
2
+
3
+ 1.1.0
4
+ -----
5
+
6
+ * Add `truncate` option to limit slug length to the nearest word
7
+ * Support passing codepoints (e.g. `?-`) as a separator
2
8
3
9
1.0.0
4
10
-----
5
11
6
- :tada: Initial release
12
+ Initial release :tada:
changed README.md
 
@@ -1,16 +1,13 @@
1
1
# Slugify
2
2
3
3
[![Build Status](https://travis-ci.org/jayjun/slugify.svg?branch=master)](https://travis-ci.org/jayjun/slugify)
4
+ [![Hex.pm](https://img.shields.io/hexpm/v/plug.svg)](https://hex.pm/packages/slugify)
4
5
5
- Transforms strings in any language into slugs.
6
+ Transform strings in any language into slugs.
6
7
7
- It works by transliterating any Unicode character to alphanumeric ones, and
8
- replaces whitespaces with hyphens.
9
-
10
- The goal is to generate general purpose human and machine-readable slugs. So
11
- `-`, `.`, `_` and `~` characters are stripped from input even though they are
12
- "unreserved" characters for URLs (see [RFC 3986][1]). Having said that, any
13
- character can be used as a separator, including the ones above.
8
+ It works by transliterating Unicode characters into alphanumeric strings (e.g.
9
+ `字` into `zi`). All punctuation is stripped and whitespace between words are
10
+ replaced by separators.
14
11
15
12
This package has no dependencies.
16
13
 
@@ -23,24 +20,28 @@ Slug.slugify("Hello, World!")
23
20
Slug.slugify("你好,世界")
24
21
"nihaoshijie"
25
22
26
- Slug.slugify("さようなら")
27
- "sayounara"
23
+ Slug.slugify("Wikipedia case", separator: ?_, lowercase: false)
24
+ "Wikipedia_case"
28
25
26
+ # Remember to check for nil if a valid slug is important!
29
27
Slug.slugify("🙅‍")
30
- nil # Remember nil check if a valid slug is important!
28
+ nil
31
29
```
32
30
33
31
## Options
34
32
35
- Whitespaces are replaced by separators (defaults to `-`), but you can use any
36
- string as a separator or pass `""` to have none.
33
+ Whitespaces are replaced by separators (defaults to `-`). Pass any codepoint or
34
+ string to customize the separator, or pass `""` to have none.
37
35
38
36
```elixir
39
- Slug.slugify(" How are you ? ")
37
+ Slug.slugify(" How are you? ")
40
38
"how-are-you"
41
39
42
- Slug.slugify("1 2 3", separator: " != ")
43
- "1 != 2 != 3"
40
+ Slug.slugify("John Doe", separator: ?.)
41
+ "john.doe"
42
+
43
+ Slug.slugify("Wide open spaces", separator: "%20")
44
+ "wide%20open%20spaces"
44
45
45
46
Slug.slugify("Madam, I'm Adam", separator: "")
46
47
"madamimadam"
 
@@ -53,8 +54,8 @@ Slug.slugify("StUdLy CaPs", lowercase: false)
53
54
"StUdLy-CaPs"
54
55
```
55
56
56
- Specific graphemes can be ignored if you pass a string (or a list of strings)
57
- containing characters to ignore.
57
+ To avoid transforming certain characters, pass a string (or a list of strings)
58
+ of graphemes to `ignore`.
58
59
59
60
```elixir
60
61
Slug.slugify("你好,世界", ignore: "你好")
 
@@ -76,7 +77,7 @@ Add `slugify` to your list of dependencies in `mix.exs`:
76
77
77
78
```elixir
78
79
def deps do
79
- [{:slugify, "~> 1.0.0"}]
80
+ [{:slugify, "~> 1.1"}]
80
81
end
81
82
```
changed hex_metadata.config
 
@@ -1,7 +1,7 @@
1
1
{<<"app">>,<<"slugify">>}.
2
2
{<<"build_tools">>,[<<"mix">>]}.
3
3
{<<"description">>,
4
- <<"Transforms a string in any language to slugs for URLs, filenames or fun">>}.
4
+ <<"Transform strings in any language to slugs for URLs, filenames or fun">>}.
5
5
{<<"elixir">>,<<"~> 1.4">>}.
6
6
{<<"files">>,
7
7
[<<"lib/replacements.exs">>,<<"lib/slug.ex">>,<<"mix.exs">>,<<"README.md">>,
 
@@ -11,4 +11,4 @@
11
11
{<<"maintainers">>,[<<"Tan Jay Jun">>]}.
12
12
{<<"name">>,<<"slugify">>}.
13
13
{<<"requirements">>,[]}.
14
- {<<"version">>,<<"1.0.0">>}.
14
+ {<<"version">>,<<"1.1.0">>}.
changed lib/slug.ex
 
@@ -1,15 +1,12 @@
1
1
defmodule Slug do
2
2
@moduledoc """
3
- Transforms any string into a slug.
3
+ Transform strings in any language into slugs.
4
4
5
- It works by transliterating any Unicode character to alphanumeric ones, and
6
- replacing whitespaces with hyphens.
5
+ It works by transliterating Unicode characters into alphanumeric strings (e.g.
6
+ `字` into `zi`). All punctuation is stripped and whitespace between words are
7
+ replaced by separators.
7
8
8
- The goal is to generate general purpose human and machine-readable slugs. So
9
- `-`, `.`, `_` and `~` characters are stripped from input even though they are
10
- "unreserved" characters for URLs (see
11
- [RFC 3986](https://www.ietf.org/rfc/rfc3986.txt)). Having said that, any
12
- character can be used as a separator, including the ones above.
9
+ This package has no dependencies.
13
10
"""
14
11
15
12
@doc """
 
@@ -18,9 +15,11 @@ defmodule Slug do
18
15
## Options
19
16
20
17
* `separator` - Replace whitespaces with this string. Leading, trailing or
21
- repeated whitespaces are still trimmed. Defaults to `-`.
22
- * `lowercase` - Set to `false` if you wish to retain
23
- your uppercase letters. Defaults to `true`.
18
+ repeated whitespaces are trimmed. Defaults to `-`.
19
+ * `lowercase` - Set to `false` if you wish to retain capitalization.
20
+ Defaults to `true`.
21
+ * `truncate` - Truncates slug at this character length, shortened to the
22
+ nearest word.
24
23
* `ignore` - Pass in a string (or list of strings) of characters to ignore.
25
24
26
25
## Examples
 
@@ -34,8 +33,8 @@ defmodule Slug do
34
33
iex> Slug.slugify("StUdLy CaPs", lowercase: false)
35
34
"StUdLy-CaPs"
36
35
37
- iex> Slug.slugify("你好,世界")
38
- "nihaoshijie"
36
+ iex> Slug.slugify("Call me maybe", truncate: 10)
37
+ "call-me"
39
38
40
39
iex> Slug.slugify("你好,世界", ignore: ["你", "好"])
41
40
"你好shijie"
 
@@ -43,42 +42,90 @@ defmodule Slug do
43
42
"""
44
43
@spec slugify(String.t, Keyword.t) :: String.t | nil
45
44
def slugify(string, opts \\ []) do
46
- separator = Keyword.get(opts, :separator, "-")
47
- force_lowercase = Keyword.get(opts, :lowercase, true)
48
- ignored_codepoints =
49
- opts
50
- |> Keyword.get(:ignore, "")
51
- |> case do
52
- characters when is_list(characters) ->
53
- Enum.join(characters)
54
- characters when is_binary(characters) ->
55
- characters
56
- end
57
- |> normalize_to_codepoints()
45
+ separator = get_separator(opts)
46
+ lowercase? = Keyword.get(opts, :lowercase, true)
47
+ truncate_length = get_truncate_length(opts)
48
+ ignored_codepoints = get_ignored_codepoints(opts)
58
49
59
- result =
60
- string
61
- |> String.split(~r{[\s]}, trim: true)
62
- |> Enum.map(& transliterate(&1, ignored_codepoints))
63
- |> Enum.filter(& &1 != "")
64
- |> Enum.join(separator)
50
+ string
51
+ |> String.split(~r{\s}, trim: true)
52
+ |> Enum.map(& transliterate(&1, ignored_codepoints))
53
+ |> Enum.filter(& &1 != "")
54
+ |> join(separator, truncate_length)
55
+ |> lower_case(lowercase?)
56
+ |> validate_slug()
57
+ end
58
+
59
+ def get_separator(opts) do
60
+ separator = Keyword.get(opts, :separator)
65
61
66
62
case separator do
67
- "" ->
68
- lower_case(result, force_lowercase)
69
- separator ->
70
- case String.replace(result, separator, "") do
71
- "" ->
72
- nil
73
- _ ->
74
- lower_case(result, force_lowercase)
75
- end
63
+ separator when is_integer(separator) and separator >= 0 ->
64
+ <<separator::utf8>>
65
+ separator when is_binary(separator) ->
66
+ separator
67
+ _ ->
68
+ "-"
76
69
end
77
70
end
78
71
72
+ def get_truncate_length(opts) do
73
+ length = Keyword.get(opts, :truncate)
74
+
75
+ case length do
76
+ length when is_integer(length) and length <= 0 ->
77
+ 0
78
+ length when is_integer(length) ->
79
+ length
80
+ _ ->
81
+ nil
82
+ end
83
+ end
84
+
85
+ defp get_ignored_codepoints(opts) do
86
+ characters_to_ignore = Keyword.get(opts, :ignore)
87
+
88
+ string = case characters_to_ignore do
89
+ characters when is_list(characters) ->
90
+ Enum.join(characters)
91
+ characters when is_binary(characters) ->
92
+ characters
93
+ _ ->
94
+ ""
95
+ end
96
+
97
+ normalize_to_codepoints(string)
98
+ end
99
+
100
+ defp join(words, separator, nil), do: Enum.join(words, separator)
101
+ defp join(words, separator, maximum_length) do
102
+ words
103
+ |> Enum.reduce_while({[], 0}, fn word, {result, length} ->
104
+ new_length = case length do
105
+ 0 -> String.length(word)
106
+ _ -> length + String.length(separator) + String.length(word)
107
+ end
108
+
109
+ cond do
110
+ new_length > maximum_length ->
111
+ {:halt, {result, length}}
112
+ new_length == maximum_length ->
113
+ {:halt, {[word | result], new_length}}
114
+ true ->
115
+ {:cont, {[word | result], new_length}}
116
+ end
117
+ end)
118
+ |> elem(0)
119
+ |> Enum.reverse()
120
+ |> Enum.join(separator)
121
+ end
122
+
79
123
defp lower_case(string, false), do: string
80
124
defp lower_case(string, true), do: String.downcase(string)
81
125
126
+ defp validate_slug(""), do: nil
127
+ defp validate_slug(string), do: string
128
+
82
129
defp normalize_to_codepoints(string) do
83
130
string
84
131
|> String.normalize(:nfc)
 
@@ -114,8 +161,7 @@ defmodule Slug do
114
161
@replacements "lib/replacements.exs" |> Code.eval_file() |> elem(0)
115
162
defp transliterate([codepoint | rest], acc, ignored_codepoints) do
116
163
if codepoint in ignored_codepoints do
117
- character = List.to_string([codepoint])
118
- transliterate(rest, [character | acc], ignored_codepoints)
164
+ transliterate(rest, [<<codepoint::utf8>> | acc], ignored_codepoints)
119
165
else
120
166
case Map.get(@replacements, codepoint) do
121
167
nil ->
changed mix.exs
 
@@ -5,10 +5,10 @@ defmodule Slug.Mixfile do
5
5
6
6
def project do
7
7
[app: :slugify,
8
- version: "1.0.0",
8
+ version: "1.1.0",
9
9
elixir: "~> 1.4",
10
10
name: "Slugify",
11
- description: "Transforms a string in any language to slugs for URLs, filenames or fun",
11
+ description: "Transform strings in any language to slugs for URLs, filenames or fun",
12
12
deps: deps(),
13
13
package: package(),
14
14
source_url: @github_url]