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
|
[](https://travis-ci.org/jayjun/slugify)
|
4
|
+ [](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]
|