cmassimo-friendly_id 3.0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Changelog.md +277 -0
- data/Contributors.md +39 -0
- data/Guide.md +561 -0
- data/LICENSE +19 -0
- data/README.md +83 -0
- data/Rakefile +66 -0
- data/extras/README.txt +3 -0
- data/extras/bench.rb +36 -0
- data/extras/extras.rb +38 -0
- data/extras/prof.rb +14 -0
- data/extras/template-gem.rb +26 -0
- data/extras/template-plugin.rb +28 -0
- data/generators/friendly_id/friendly_id_generator.rb +30 -0
- data/generators/friendly_id/templates/create_slugs.rb +18 -0
- data/lib/friendly_id.rb +73 -0
- data/lib/friendly_id/active_record.rb +52 -0
- data/lib/friendly_id/active_record_adapter/configuration.rb +67 -0
- data/lib/friendly_id/active_record_adapter/finders.rb +156 -0
- data/lib/friendly_id/active_record_adapter/simple_model.rb +123 -0
- data/lib/friendly_id/active_record_adapter/slug.rb +66 -0
- data/lib/friendly_id/active_record_adapter/slugged_model.rb +238 -0
- data/lib/friendly_id/active_record_adapter/tasks.rb +69 -0
- data/lib/friendly_id/configuration.rb +113 -0
- data/lib/friendly_id/finders.rb +109 -0
- data/lib/friendly_id/railtie.rb +20 -0
- data/lib/friendly_id/sequel.rb +5 -0
- data/lib/friendly_id/slug_string.rb +391 -0
- data/lib/friendly_id/slugged.rb +102 -0
- data/lib/friendly_id/status.rb +35 -0
- data/lib/friendly_id/test.rb +291 -0
- data/lib/friendly_id/version.rb +9 -0
- data/lib/generators/friendly_id_generator.rb +25 -0
- data/lib/tasks/friendly_id.rake +19 -0
- data/rails/init.rb +2 -0
- data/test/active_record_adapter/ar_test_helper.rb +119 -0
- data/test/active_record_adapter/basic_slugged_model_test.rb +14 -0
- data/test/active_record_adapter/cached_slug_test.rb +61 -0
- data/test/active_record_adapter/core.rb +98 -0
- data/test/active_record_adapter/custom_normalizer_test.rb +20 -0
- data/test/active_record_adapter/custom_table_name_test.rb +22 -0
- data/test/active_record_adapter/scoped_model_test.rb +118 -0
- data/test/active_record_adapter/simple_test.rb +76 -0
- data/test/active_record_adapter/slug_test.rb +34 -0
- data/test/active_record_adapter/slugged.rb +30 -0
- data/test/active_record_adapter/slugged_status_test.rb +25 -0
- data/test/active_record_adapter/sti_test.rb +22 -0
- data/test/active_record_adapter/support/database.jdbcsqlite3.yml +2 -0
- data/test/active_record_adapter/support/database.mysql.yml +4 -0
- data/test/active_record_adapter/support/database.postgres.yml +6 -0
- data/test/active_record_adapter/support/database.sqlite3.yml +2 -0
- data/test/active_record_adapter/support/models.rb +87 -0
- data/test/active_record_adapter/tasks_test.rb +82 -0
- data/test/friendly_id_test.rb +55 -0
- data/test/slug_string_test.rb +88 -0
- data/test/test_helper.rb +15 -0
- metadata +168 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
|
4
|
+
initializer "friendly_id.configure_rails_initialization" do |app|
|
5
|
+
# Experimental Sequel support. See: http://github.com/norman/friendly_id_sequel
|
6
|
+
if app.config.generators.rails[:orm] == :sequel
|
7
|
+
require "friendly_id/sequel"
|
8
|
+
else
|
9
|
+
# Only Sequel and ActiveRecord are currently supported; AR is the default.
|
10
|
+
# Want Datamapper support? http://github.com/norman/friendly_id/issues#issue/24
|
11
|
+
require "friendly_id/active_record"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
rake_tasks do
|
16
|
+
load "tasks/friendly_id.rake"
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,391 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FriendlyId
|
3
|
+
|
4
|
+
# This class provides some string-manipulation methods specific to slugs.
|
5
|
+
# Its Unicode support is provided by ActiveSupport::Multibyte::Chars; this
|
6
|
+
# is needed primarily for Unicode encoding normalization and proper
|
7
|
+
# calculation of string lengths.
|
8
|
+
#
|
9
|
+
# Note that this class includes many "bang methods" such as {#clean!} and {#normalize!}
|
10
|
+
# that perform actions on the string in-place. Each of these methods has a
|
11
|
+
# corresponding "bangless" method (i.e., +SlugString#clean!+ and +SlugString#clean+)
|
12
|
+
# which does not appear in the documentation because it is generated dynamically.
|
13
|
+
#
|
14
|
+
# All of the bang methods return an instance of String, while the bangless
|
15
|
+
# versions return an instance of FriendlyId::SlugString, so that calls to
|
16
|
+
# methods specific to this class can be chained:
|
17
|
+
#
|
18
|
+
# string = SlugString.new("hello world")
|
19
|
+
# string.with_dashes! # => "hello-world"
|
20
|
+
# string.with_dashes # => <FriendlyId::SlugString:0x000001013e1590 @wrapped_string="hello-world">
|
21
|
+
#
|
22
|
+
# @see http://www.utf8-chartable.de/unicode-utf8-table.pl?utf8=dec Unicode character table
|
23
|
+
# @see FriendlyId::SlugString::dump_approximations
|
24
|
+
class SlugString < ActiveSupport::Multibyte::Chars
|
25
|
+
|
26
|
+
# All values are Unicode decimal characters or character arrays.
|
27
|
+
APPROXIMATIONS = {
|
28
|
+
:common => Hash[
|
29
|
+
192, 65, 193, 65, 194, 65, 195, 65, 196, 65, 197, 65, 198, [65, 69],
|
30
|
+
199, 67, 200, 69, 201, 69, 202, 69, 203, 69, 204, 73, 205, 73, 206,
|
31
|
+
73, 207, 73, 208, 68, 209, 78, 210, 79, 211, 79, 212, 79, 213, 79,
|
32
|
+
214, 79, 215, 120, 216, 79, 217, 85, 218, 85, 219, 85, 220, 85, 221,
|
33
|
+
89, 222, [84, 104], 223, [115, 115], 224, 97, 225, 97, 226, 97, 227,
|
34
|
+
97, 228, 97, 229, 97, 230, [97, 101], 231, 99, 232, 101, 233, 101,
|
35
|
+
234, 101, 235, 101, 236, 105, 237, 105, 238, 105, 239, 105, 240, 100,
|
36
|
+
241, 110, 242, 111, 243, 111, 244, 111, 245, 111, 246, 111, 248, 111,
|
37
|
+
249, 117, 250, 117, 251, 117, 252, 117, 253, 121, 254, [116, 104],
|
38
|
+
255, 121, 256, 65, 257, 97, 258, 65, 259, 97, 260, 65, 261, 97, 262,
|
39
|
+
67, 263, 99, 264, 67, 265, 99, 266, 67, 267, 99, 268, 67, 269, 99,
|
40
|
+
270, 68, 271, 100, 272, 68, 273, 100, 274, 69, 275, 101, 276, 69, 277,
|
41
|
+
101, 278, 69, 279, 101, 280, 69, 281, 101, 282, 69, 283, 101, 284, 71,
|
42
|
+
285, 103, 286, 71, 287, 103, 288, 71, 289, 103, 290, 71, 291, 103,
|
43
|
+
292, 72, 293, 104, 294, 72, 295, 104, 296, 73, 297, 105, 298, 73, 299,
|
44
|
+
105, 300, 73, 301, 105, 302, 73, 303, 105, 304, 73, 305, 105, 306,
|
45
|
+
[73, 74], 307, [105, 106], 308, 74, 309, 106, 310, 75, 311, 107, 312,
|
46
|
+
107, 313, 76, 314, 108, 315, 76, 316, 108, 317, 76, 318, 108, 319, 76,
|
47
|
+
320, 108, 321, 76, 322, 108, 323, 78, 324, 110, 325, 78, 326, 110,
|
48
|
+
327, 78, 328, 110, 329, [39, 110], 330, [78, 71], 331, [110, 103],
|
49
|
+
332, 79, 333, 111, 334, 79, 335, 111, 336, 79, 337, 111, 338, [79,
|
50
|
+
69], 339, [111, 101], 340, 82, 341, 114, 342, 82, 343, 114, 344, 82,
|
51
|
+
345, 114, 346, 83, 347, 115, 348, 83, 349, 115, 350, 83, 351, 115,
|
52
|
+
352, 83, 353, 115, 354, 84, 355, 116, 356, 84, 357, 116, 358, 84, 359,
|
53
|
+
116, 360, 85, 361, 117, 362, 85, 363, 117, 364, 85, 365, 117, 366, 85,
|
54
|
+
367, 117, 368, 85, 369, 117, 370, 85, 371, 117, 372, 87, 373, 119,
|
55
|
+
374, 89, 375, 121, 376, 89, 377, 90, 378, 122, 379, 90, 380, 122, 381,
|
56
|
+
90, 382, 122
|
57
|
+
].freeze,
|
58
|
+
:german => Hash[252, [117, 101], 246, [111, 101], 228, [97, 101]],
|
59
|
+
:spanish => Hash[209, [78, 110], 241, [110, 110]]
|
60
|
+
}
|
61
|
+
|
62
|
+
# CP-1252 decimal byte => UTF-8 approximation as an array of bytes
|
63
|
+
CP1252 = {
|
64
|
+
128 => [226, 130, 172],
|
65
|
+
129 => nil,
|
66
|
+
130 => [226, 128, 154],
|
67
|
+
131 => [198, 146],
|
68
|
+
132 => [226, 128, 158],
|
69
|
+
133 => [226, 128, 166],
|
70
|
+
134 => [226, 128, 160],
|
71
|
+
135 => [226, 128, 161],
|
72
|
+
136 => [203, 134],
|
73
|
+
137 => [226, 128, 176],
|
74
|
+
138 => [197, 160],
|
75
|
+
139 => [226, 128, 185],
|
76
|
+
140 => [197, 146],
|
77
|
+
141 => nil,
|
78
|
+
142 => [197, 189],
|
79
|
+
143 => nil,
|
80
|
+
144 => nil,
|
81
|
+
145 => [226, 128, 152],
|
82
|
+
146 => [226, 128, 153],
|
83
|
+
147 => [226, 128, 156],
|
84
|
+
148 => [226, 128, 157],
|
85
|
+
149 => [226, 128, 162],
|
86
|
+
150 => [226, 128, 147],
|
87
|
+
151 => [226, 128, 148],
|
88
|
+
152 => [203, 156],
|
89
|
+
153 => [226, 132, 162],
|
90
|
+
154 => [197, 161],
|
91
|
+
155 => [226, 128, 186],
|
92
|
+
156 => [197, 147],
|
93
|
+
157 => nil,
|
94
|
+
158 => [197, 190],
|
95
|
+
159 => [197, 184]
|
96
|
+
}
|
97
|
+
|
98
|
+
cattr_accessor :approximations
|
99
|
+
self.approximations = []
|
100
|
+
|
101
|
+
# This method can be used by developers wishing to debug the
|
102
|
+
# {APPROXIMATIONS} hashes, which are written in a hard-to-read format.
|
103
|
+
# @return Hash
|
104
|
+
# @example
|
105
|
+
#
|
106
|
+
# > ruby -rrubygems -rlib/friendly_id -e 'p FriendlyId::SlugString.dump_approximations'
|
107
|
+
#
|
108
|
+
# {:common =>
|
109
|
+
# {"À"=>"A", "Á"=>"A", "Â"=>"A", "Ã"=>"A", "Ä"=>"A", "Å"=>"A", "Æ"=>"AE",
|
110
|
+
# "Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I",
|
111
|
+
# "Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O",
|
112
|
+
# "Õ"=>"O", "Ö"=>"O", "×"=>"x", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U",
|
113
|
+
# "Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "à"=>"a", "á"=>"a", "â"=>"a",
|
114
|
+
# "ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c", "è"=>"e", "é"=>"e",
|
115
|
+
# "ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i", "ï"=>"i", "ð"=>"d",
|
116
|
+
# "ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o", "ö"=>"o", "ø"=>"o",
|
117
|
+
# "ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y", "þ"=>"th", "ÿ"=>"y",
|
118
|
+
# "Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A", "ą"=>"a", "Ć"=>"C",
|
119
|
+
# "ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c", "Č"=>"C", "č"=>"c",
|
120
|
+
# "Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E", "ē"=>"e", "Ĕ"=>"E",
|
121
|
+
# "ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e", "Ě"=>"E", "ě"=>"e",
|
122
|
+
# "Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G", "ġ"=>"g", "Ģ"=>"G",
|
123
|
+
# "ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h", "Ĩ"=>"I", "ĩ"=>"i",
|
124
|
+
# "Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I", "į"=>"i", "İ"=>"I",
|
125
|
+
# "ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j", "Ķ"=>"K", "ķ"=>"k",
|
126
|
+
# "ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l", "Ľ"=>"L", "ľ"=>"l",
|
127
|
+
# "Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N", "ń"=>"n", "Ņ"=>"N",
|
128
|
+
# "ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG", "ŋ"=>"ng",
|
129
|
+
# "Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o", "Œ"=>"OE",
|
130
|
+
# "œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R", "ř"=>"r",
|
131
|
+
# "Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s", "Š"=>"S",
|
132
|
+
# "š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T", "ŧ"=>"t",
|
133
|
+
# "Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u", "Ů"=>"U",
|
134
|
+
# "ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W", "ŵ"=>"w",
|
135
|
+
# "Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z", "ż"=>"z",
|
136
|
+
# "Ž"=>"Z", "ž"=>"z"},
|
137
|
+
# :german => {"ü"=>"ue", "ö"=>"oe", "ä"=>"ae"},
|
138
|
+
# :spanish => {"Ñ"=>"Nn", "ñ"=>"nn"}}
|
139
|
+
def self.dump_approximations
|
140
|
+
Hash[APPROXIMATIONS.map do |name, approx|
|
141
|
+
[name, Hash[approx.map {|key, value| [[key].pack("U*"), [value].flatten.pack("U*")]}]]
|
142
|
+
end]
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
# @param string [String] The string to use as the basis of the SlugString.
|
147
|
+
def initialize(string)
|
148
|
+
super string.to_s
|
149
|
+
tidy_bytes!
|
150
|
+
end
|
151
|
+
|
152
|
+
# Approximate an ASCII string. This works only for Western strings using
|
153
|
+
# characters that are Roman-alphabet characters + diacritics. Non-letter
|
154
|
+
# characters are left unmodified.
|
155
|
+
#
|
156
|
+
# string = SlugString.new "Łódź, Poland"
|
157
|
+
# string.approximate_ascii # => "Lodz, Poland"
|
158
|
+
# string = SlugString.new "日本"
|
159
|
+
# string.approximate_ascii # => "日本"
|
160
|
+
#
|
161
|
+
# You can pass any key(s) from {APPROXIMATIONS} as arguments. This allows
|
162
|
+
# for contextual approximations. By default; +:spanish+ and +:german+ are
|
163
|
+
# provided:
|
164
|
+
#
|
165
|
+
# string = SlugString.new "Jürgen Müller"
|
166
|
+
# string.approximate_ascii # => "Jurgen Muller"
|
167
|
+
# string.approximate_ascii :german # => "Juergen Mueller"
|
168
|
+
# string = SlugString.new "¡Feliz año!"
|
169
|
+
# string.approximate_ascii # => "¡Feliz ano!"
|
170
|
+
# string.approximate_ascii :spanish # => "¡Feliz anno!"
|
171
|
+
#
|
172
|
+
# You can modify the built-in approximations, or add your own:
|
173
|
+
#
|
174
|
+
# # Make Spanish use "nh" rather than "nn"
|
175
|
+
# FriendlyId::SlugString::APPROXIMATIONS[:spanish] = {
|
176
|
+
# # Ñ => "Nh"
|
177
|
+
# 209 => [78, 104],
|
178
|
+
# # ñ => "nh"
|
179
|
+
# 241 => [110, 104]
|
180
|
+
# }
|
181
|
+
#
|
182
|
+
# It's also possible to use a custom approximation for all strings:
|
183
|
+
#
|
184
|
+
# FriendlyId::SlugString.approximations << :german
|
185
|
+
#
|
186
|
+
# Notice that this method does not simply convert to ASCII; if you want
|
187
|
+
# to remove non-ASCII characters such as "¡" and "¿", use {#to_ascii!}:
|
188
|
+
#
|
189
|
+
# string.approximate_ascii!(:spanish) # => "¡Feliz anno!"
|
190
|
+
# string.to_ascii! # => "Feliz anno!"
|
191
|
+
# @param *args <Symbol>
|
192
|
+
# @return String
|
193
|
+
def approximate_ascii!(*args)
|
194
|
+
@maps = (self.class.approximations + args + [:common]).flatten.uniq
|
195
|
+
@wrapped_string = normalize_utf8(:c).unpack("U*").map { |char| approx_char(char) }.flatten.pack("U*")
|
196
|
+
end
|
197
|
+
|
198
|
+
# Removes leading and trailing spaces or dashses, and replaces multiple
|
199
|
+
# whitespace characters with a single space.
|
200
|
+
# @return String
|
201
|
+
def clean!
|
202
|
+
@wrapped_string = @wrapped_string.gsub(/\A\-|\-\z/, "").gsub(/\s+/u, " ").strip
|
203
|
+
end
|
204
|
+
|
205
|
+
# Lowercases the string. Note that this works for Unicode strings,
|
206
|
+
# though your milage may vary with Greek and Turkic strings.
|
207
|
+
# @return String
|
208
|
+
def downcase!
|
209
|
+
@wrapped_string = apply_mapping :lowercase_mapping
|
210
|
+
end
|
211
|
+
|
212
|
+
# Remove any non-word characters.
|
213
|
+
# @return String
|
214
|
+
def word_chars!
|
215
|
+
@wrapped_string = normalize_utf8(:c).unpack("U*").map { |char|
|
216
|
+
case char
|
217
|
+
# control chars
|
218
|
+
when 0..31
|
219
|
+
# punctuation; 45 is "-" (HYPHEN-MINUS) and allowed
|
220
|
+
when 33..44
|
221
|
+
# more puncuation
|
222
|
+
when 46..47
|
223
|
+
# more puncuation and other symbols
|
224
|
+
when 58..64
|
225
|
+
# brackets and other symbols
|
226
|
+
when 91..96
|
227
|
+
# braces, pipe, tilde, etc.
|
228
|
+
when 123..191
|
229
|
+
else char
|
230
|
+
end
|
231
|
+
}.compact.pack("U*")
|
232
|
+
end
|
233
|
+
|
234
|
+
# Normalize the string for a given {FriendlyId::Configuration}.
|
235
|
+
# @param config [FriendlyId::Configuration]
|
236
|
+
# @return String
|
237
|
+
def normalize_for!(config)
|
238
|
+
approximate_ascii! if config.approximate_ascii?
|
239
|
+
to_ascii! if config.strip_non_ascii?
|
240
|
+
normalize!
|
241
|
+
end
|
242
|
+
|
243
|
+
alias :normalize_utf8 :normalize rescue NoMethodError
|
244
|
+
|
245
|
+
# Normalize the string for use as a FriendlyId. Note that in
|
246
|
+
# this context, +normalize+ means, strip, remove non-letters/numbers,
|
247
|
+
# downcasing and converting whitespace to dashes.
|
248
|
+
# ActiveSupport::Multibyte::Chars#normalize is aliased to +normalize_utf8+
|
249
|
+
# in this subclass.
|
250
|
+
# @return String
|
251
|
+
def normalize!
|
252
|
+
clean!
|
253
|
+
word_chars!
|
254
|
+
downcase!
|
255
|
+
with_dashes!
|
256
|
+
end
|
257
|
+
|
258
|
+
# Attempt to replace invalid UTF-8 bytes with valid ones. This method
|
259
|
+
# naively assumes if you have invalid UTF8 bytes, they are either Windows
|
260
|
+
# CP-1252 or ISO8859-1. In practice this isn't a bad assumption, but may not
|
261
|
+
# always work.
|
262
|
+
#
|
263
|
+
# Passing +true+ will forcibly tidy all bytes, assuming that the string's
|
264
|
+
# encoding is CP-1252 or ISO-8859-1.
|
265
|
+
def tidy_bytes!(force = false)
|
266
|
+
|
267
|
+
if force
|
268
|
+
@wrapped_string = @wrapped_string.unpack("C*").map do |b|
|
269
|
+
tidy_byte(b)
|
270
|
+
end.flatten.compact.pack("C*").unpack("U*").pack("U*")
|
271
|
+
end
|
272
|
+
|
273
|
+
bytes = @wrapped_string.unpack("C*")
|
274
|
+
conts_expected = 0
|
275
|
+
last_lead = 0
|
276
|
+
|
277
|
+
bytes.each_index do |i|
|
278
|
+
|
279
|
+
byte = bytes[i]
|
280
|
+
is_ascii = byte < 128
|
281
|
+
is_cont = byte > 127 && byte < 192
|
282
|
+
is_lead = byte > 191 && byte < 245
|
283
|
+
is_unused = byte > 240
|
284
|
+
is_restricted = byte > 244
|
285
|
+
|
286
|
+
# Impossible or highly unlikely byte? Clean it.
|
287
|
+
if is_unused || is_restricted
|
288
|
+
bytes[i] = tidy_byte(byte)
|
289
|
+
elsif is_cont
|
290
|
+
# Not expecting contination byte? Clean up. Otherwise, now expect one less.
|
291
|
+
conts_expected == 0 ? bytes[i] = tidy_byte(byte) : conts_expected -= 1
|
292
|
+
else
|
293
|
+
if conts_expected > 0
|
294
|
+
# Expected continuation, but got ASCII or leading? Clean backwards up to
|
295
|
+
# the leading byte.
|
296
|
+
(1..(i - last_lead)).each {|j| bytes[i - j] = tidy_byte(bytes[i - j])}
|
297
|
+
conts_expected = 0
|
298
|
+
end
|
299
|
+
if is_lead
|
300
|
+
# Final byte is leading? Clean it.
|
301
|
+
if i == bytes.length - 1
|
302
|
+
bytes[i] = tidy_byte(bytes.last)
|
303
|
+
else
|
304
|
+
# Valid leading byte? Expect continuations determined by position of
|
305
|
+
# first zero bit, with max of 3.
|
306
|
+
conts_expected = byte < 224 ? 1 : byte < 240 ? 2 : 3
|
307
|
+
last_lead = i
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
@wrapped_string = bytes.empty? ? "" : bytes.flatten.compact.pack("C*").unpack("U*").pack("U*")
|
313
|
+
end
|
314
|
+
|
315
|
+
# Delete any non-ascii characters.
|
316
|
+
# @return String
|
317
|
+
def to_ascii!
|
318
|
+
if ">= 1.9".respond_to?(:force_encoding)
|
319
|
+
@wrapped_string.encode!("ASCII", :invalid => :replace, :undef => :replace,
|
320
|
+
:replace => "")
|
321
|
+
else
|
322
|
+
@wrapped_string = tidy_bytes.normalize_utf8(:c).unpack("U*").reject {|char| char > 127}.pack("U*")
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# Truncate the string to +max+ length.
|
327
|
+
# @return String
|
328
|
+
def truncate!(max)
|
329
|
+
@wrapped_string = self[0...max].to_s if length > max
|
330
|
+
end
|
331
|
+
|
332
|
+
# Upper-cases the string. Note that this works for Unicode strings,
|
333
|
+
# though your milage may vary with Greek and Turkic strings.
|
334
|
+
# @return String
|
335
|
+
def upcase!
|
336
|
+
@wrapped_string = apply_mapping :uppercase_mapping
|
337
|
+
end
|
338
|
+
|
339
|
+
# Validate that the slug string is not blank or reserved, and truncate
|
340
|
+
# it to the max length if necessary.
|
341
|
+
# @param config [FriendlyId::Configuration]
|
342
|
+
# @return String
|
343
|
+
# @raise FriendlyId::BlankError
|
344
|
+
# @raise FriendlyId::ReservedError
|
345
|
+
def validate_for!(config)
|
346
|
+
truncate!(config.max_length)
|
347
|
+
raise FriendlyId::BlankError if blank?
|
348
|
+
raise FriendlyId::ReservedError if config.reserved?(self)
|
349
|
+
self
|
350
|
+
end
|
351
|
+
|
352
|
+
# Replaces whitespace with dashes ("-").
|
353
|
+
# @return String
|
354
|
+
def with_dashes!
|
355
|
+
@wrapped_string = @wrapped_string.gsub(/[\s\-]+/u, "-")
|
356
|
+
end
|
357
|
+
|
358
|
+
%w[approximate_ascii clean downcase word_chars normalize normalize_for tidy_bytes
|
359
|
+
to_ascii truncate upcase with_dashes].each do |method|
|
360
|
+
class_eval(<<-EOM)
|
361
|
+
def #{method}(*args)
|
362
|
+
send_to_new_instance(:#{method}!, *args)
|
363
|
+
end
|
364
|
+
EOM
|
365
|
+
end
|
366
|
+
|
367
|
+
private
|
368
|
+
|
369
|
+
# Look up the character's approximation in the configured maps.
|
370
|
+
def approx_char(char)
|
371
|
+
@maps.each do |map|
|
372
|
+
if new_char = APPROXIMATIONS[map][char]
|
373
|
+
return new_char
|
374
|
+
end
|
375
|
+
end
|
376
|
+
char
|
377
|
+
end
|
378
|
+
|
379
|
+
# Used as the basis of the bangless methods.
|
380
|
+
def send_to_new_instance(*args)
|
381
|
+
string = SlugString.new self
|
382
|
+
string.send(*args)
|
383
|
+
string
|
384
|
+
end
|
385
|
+
|
386
|
+
def tidy_byte(byte)
|
387
|
+
byte < 160 ? CP1252[byte] : byte < 192 ? [194, byte] : [195, byte - 64]
|
388
|
+
end
|
389
|
+
|
390
|
+
end
|
391
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
module Slugged
|
3
|
+
|
4
|
+
class Status < FriendlyId::Status
|
5
|
+
|
6
|
+
attr_accessor :sequence, :slug
|
7
|
+
|
8
|
+
# Did the find operation use the best possible id? True if +id+ is
|
9
|
+
# numeric, but the model has no slug, or +id+ is friendly and current
|
10
|
+
def best?
|
11
|
+
current? || (numeric? && !record.slug)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Did the find operation use the current slug?
|
15
|
+
def current?
|
16
|
+
!! slug && slug.current?
|
17
|
+
end
|
18
|
+
|
19
|
+
# Did the find operation use a friendly id?
|
20
|
+
def friendly?
|
21
|
+
!! (name or slug)
|
22
|
+
end
|
23
|
+
|
24
|
+
def friendly_id=(friendly_id)
|
25
|
+
@name, @sequence = friendly_id.parse_friendly_id(record.friendly_id_config.sequence_separator)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Did the find operation use an outdated slug?
|
29
|
+
def outdated?
|
30
|
+
!current?
|
31
|
+
end
|
32
|
+
|
33
|
+
# The slug that was used to find the model.
|
34
|
+
def slug
|
35
|
+
@slug ||= record.find_slug(name, sequence)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
module Model
|
41
|
+
attr_accessor :slug
|
42
|
+
|
43
|
+
def find_slug
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
def friendly_id_config
|
48
|
+
self.class.friendly_id_config
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get the {FriendlyId::Status} after the find has been performed.
|
52
|
+
def friendly_id_status
|
53
|
+
@friendly_id_status ||= Status.new(:record => self)
|
54
|
+
end
|
55
|
+
|
56
|
+
# The friendly id.
|
57
|
+
def friendly_id
|
58
|
+
slug.to_friendly_id if slug?
|
59
|
+
end
|
60
|
+
|
61
|
+
# Clean up the string before setting it as the friendly_id. You can override
|
62
|
+
# this method to add your own custom normalization routines.
|
63
|
+
# @param string An instance of {FriendlyId::SlugString}.
|
64
|
+
# @return [String]
|
65
|
+
def normalize_friendly_id(string)
|
66
|
+
string.normalize_for!(friendly_id_config).to_s
|
67
|
+
end
|
68
|
+
|
69
|
+
# Does the instance have a slug?
|
70
|
+
def slug?
|
71
|
+
!! slug
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# Get the processed string used as the basis of the friendly id.
|
77
|
+
def slug_text
|
78
|
+
base = send(friendly_id_config.method)
|
79
|
+
unless base.nil? && friendly_id_config.allow_nil?
|
80
|
+
text = normalize_friendly_id(SlugString.new(base))
|
81
|
+
SlugString.new(text.to_s).validate_for!(friendly_id_config).to_s
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Has the slug text changed?
|
86
|
+
def slug_text_changed?
|
87
|
+
slug_text != slug.name
|
88
|
+
end
|
89
|
+
|
90
|
+
# Has the basis of our friendly id changed, requiring the generation of a
|
91
|
+
# new slug?
|
92
|
+
def new_slug_needed?
|
93
|
+
if friendly_id_config.allow_nil?
|
94
|
+
(!slug? && !slug_text.blank?) || (slug? && slug_text_changed?)
|
95
|
+
else
|
96
|
+
!slug? || slug_text_changed?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|