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.
Files changed (56) hide show
  1. data/Changelog.md +277 -0
  2. data/Contributors.md +39 -0
  3. data/Guide.md +561 -0
  4. data/LICENSE +19 -0
  5. data/README.md +83 -0
  6. data/Rakefile +66 -0
  7. data/extras/README.txt +3 -0
  8. data/extras/bench.rb +36 -0
  9. data/extras/extras.rb +38 -0
  10. data/extras/prof.rb +14 -0
  11. data/extras/template-gem.rb +26 -0
  12. data/extras/template-plugin.rb +28 -0
  13. data/generators/friendly_id/friendly_id_generator.rb +30 -0
  14. data/generators/friendly_id/templates/create_slugs.rb +18 -0
  15. data/lib/friendly_id.rb +73 -0
  16. data/lib/friendly_id/active_record.rb +52 -0
  17. data/lib/friendly_id/active_record_adapter/configuration.rb +67 -0
  18. data/lib/friendly_id/active_record_adapter/finders.rb +156 -0
  19. data/lib/friendly_id/active_record_adapter/simple_model.rb +123 -0
  20. data/lib/friendly_id/active_record_adapter/slug.rb +66 -0
  21. data/lib/friendly_id/active_record_adapter/slugged_model.rb +238 -0
  22. data/lib/friendly_id/active_record_adapter/tasks.rb +69 -0
  23. data/lib/friendly_id/configuration.rb +113 -0
  24. data/lib/friendly_id/finders.rb +109 -0
  25. data/lib/friendly_id/railtie.rb +20 -0
  26. data/lib/friendly_id/sequel.rb +5 -0
  27. data/lib/friendly_id/slug_string.rb +391 -0
  28. data/lib/friendly_id/slugged.rb +102 -0
  29. data/lib/friendly_id/status.rb +35 -0
  30. data/lib/friendly_id/test.rb +291 -0
  31. data/lib/friendly_id/version.rb +9 -0
  32. data/lib/generators/friendly_id_generator.rb +25 -0
  33. data/lib/tasks/friendly_id.rake +19 -0
  34. data/rails/init.rb +2 -0
  35. data/test/active_record_adapter/ar_test_helper.rb +119 -0
  36. data/test/active_record_adapter/basic_slugged_model_test.rb +14 -0
  37. data/test/active_record_adapter/cached_slug_test.rb +61 -0
  38. data/test/active_record_adapter/core.rb +98 -0
  39. data/test/active_record_adapter/custom_normalizer_test.rb +20 -0
  40. data/test/active_record_adapter/custom_table_name_test.rb +22 -0
  41. data/test/active_record_adapter/scoped_model_test.rb +118 -0
  42. data/test/active_record_adapter/simple_test.rb +76 -0
  43. data/test/active_record_adapter/slug_test.rb +34 -0
  44. data/test/active_record_adapter/slugged.rb +30 -0
  45. data/test/active_record_adapter/slugged_status_test.rb +25 -0
  46. data/test/active_record_adapter/sti_test.rb +22 -0
  47. data/test/active_record_adapter/support/database.jdbcsqlite3.yml +2 -0
  48. data/test/active_record_adapter/support/database.mysql.yml +4 -0
  49. data/test/active_record_adapter/support/database.postgres.yml +6 -0
  50. data/test/active_record_adapter/support/database.sqlite3.yml +2 -0
  51. data/test/active_record_adapter/support/models.rb +87 -0
  52. data/test/active_record_adapter/tasks_test.rb +82 -0
  53. data/test/friendly_id_test.rb +55 -0
  54. data/test/slug_string_test.rb +88 -0
  55. data/test/test_helper.rb +15 -0
  56. 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,5 @@
1
+ begin
2
+ require "friendly_id_sequel"
3
+ rescue LoadError
4
+ raise "To use FriendlyId's Sequel adapter, please `gem install friendly_id_sequel`"
5
+ 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