slug 4.0.1 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b26d7fca3befe175649b0bdbe3b1e145e775ea77191fbe023f017d907cdfd64c
4
- data.tar.gz: '06786f698b25c0e2d061ca77cbafa382e774bb96ded86a7b246c858909e8a1b5'
3
+ metadata.gz: 5d86b48722b18d45cf288be1a320bff1550270cd61a0ab2c03dba7cbb67ab9ef
4
+ data.tar.gz: 1ffb50fcca8eb72cbedb0f002104ce472fc35abf6aaf9635229605b14cb88580
5
5
  SHA512:
6
- metadata.gz: 248ca7e65bc8ccc47a9cec02c3843f4c1871aa743078627677078c4a52dbde1666226002033c0d5e4a107b27fdb32feb1f51c3d877c815bdb455cdf8dbe87bbf
7
- data.tar.gz: dfab8ccf8d2daf51720cbc7ecfb44b9fb3ad5862f85e5e7a44b236170cbb1ac7caeaff52434764d80ed5f62bd0d11b2e104699a1011fe5ca3349e63db868cc16
6
+ metadata.gz: 49a6637967aae4804dfe30f1829bbd7cd3dd2d9075337e053d93a23489af88b894275f63787596243b01905eb27b90c7ca0a88ffe60146c780a7eead7aedd451
7
+ data.tar.gz: 3693e899c785498624a302f92c3bedc6a9432c236c98f3eee924d9aba63c108191b3ab0decb44d1852ecb4093e34c187a5584d5bd082cc9125086c3fec51af8d
@@ -0,0 +1,123 @@
1
+ # slug
2
+
3
+ Slug provides simple, straightforward slugging for your ActiveRecord models.
4
+
5
+ Slug is based on code from Norman Clarke's fantastic [friendly_id](https://github.com/norman/friendly_id) project and Nick Zadrozny's [friendly_identifier](http://code.google.com/p/friendly-identifier/).
6
+
7
+ What's different:
8
+
9
+ * Unlike friendly_id's more advanced modes, slugs are stored directly in your model's table. friendly_id stores its data in a separate sluggable table, which enables cool things like slug versioning—but forces yet another join when trying to do complex find_by_slugs.
10
+ * Like friendly_id, diacritics (accented characters) are stripped from slug strings.
11
+ * The number of options is manageable.
12
+
13
+ ## Installation
14
+
15
+ Add the gem to your Gemfile
16
+
17
+ ```
18
+ gem 'slug'
19
+ ```
20
+
21
+ of your rails project.
22
+
23
+ This is tested with Rails 5.1.4, MRI Ruby 2.4.1
24
+
25
+ ## Usage
26
+
27
+ ### Creating the database column
28
+
29
+ It's up to you to set up the appropriate column in your model. By default, slug saves the slug to a column called 'slug', so in most cases you'll just want to add
30
+
31
+ ```ruby
32
+ add_column :my_table, :slug, :string
33
+ ```
34
+
35
+ in a migration. You should also add a unque index on the slug field in your migration
36
+
37
+ ```ruby
38
+ add_index :model_name, :slug, unique: true
39
+ ```
40
+
41
+ Though Slug uses `validates_uniqueness_of` to ensue the uniqueness of your slug, two concurrent INSERTs could try to set the same slug.
42
+
43
+ ### Model setup
44
+
45
+ Once your table is migrated, just add
46
+
47
+ ```ruby
48
+ slug :source_field
49
+ ```
50
+
51
+ to your ActiveRecord model. `:source_field` is the column you'd like to base the slug on. For example, it might be `:headline`.
52
+
53
+ #### Using an instance method as the source column
54
+
55
+ The source column isn't limited to just database attributes—you can specify any instance method. This is handy if you need special formatting on your source column before it's slugged, or if you want to base the slug on several attributes.
56
+
57
+ For example:
58
+
59
+ ```ruby
60
+ class Article < ActiveRecord::Base
61
+ slug :title_for_slug
62
+
63
+ def title_for_slug
64
+ "#{headline}-#{publication_date.year}-#{publication_date.month}"
65
+ end
66
+ end
67
+ ```
68
+
69
+ would use `headline-pub year-pub month` as the slug source.
70
+
71
+ From here, you can work with your slug as if it were a normal column. `find_by_slug` and named scopes will work as they do for any other column.
72
+
73
+ ### Options
74
+
75
+ There are two options:
76
+
77
+ #### Column
78
+
79
+ If you want to save the slug in a database column that isn't called
80
+ `slug`, just pass the `:column` option. For example:
81
+
82
+ ```
83
+ slug :headline, column: :web_slug
84
+ ```
85
+
86
+ would generate a slug based on `headline` and save it to `web_slug`.
87
+
88
+ #### Generic Default
89
+
90
+ If the source column is empty, blank, or only contains filtered
91
+ characters, you can avoid `ActiveRecord::ValidationError` exceptions
92
+ by setting `generic_default: true`. For example:
93
+
94
+ ```ruby
95
+ slug :headline, generic_default: true
96
+ ```
97
+
98
+ will generate a slug based on your model name if the headline is blank.
99
+
100
+ This is useful if the source column (e.g. headline) is based on user-generated
101
+ input or can be blank (nil or empty).
102
+
103
+ Some prefer to get the exception in this case. Others want to get a good
104
+ slug and move on.
105
+
106
+ ## Notes
107
+
108
+ * Slug validates presence and uniqueness of the slug column. If you pass something that isn't sluggable as the source (for example, say you set the headline to '---'), a validation error will be set. To avoid this, use the `:generic_default` option.
109
+ * Slug doesn't update the slug if the source column changes. If you really need to regenerate the slug, call `@model.set_slug` before save.
110
+ * If a slug already exists, Slug will automatically append a '-n' suffix to your slug to make it unique. The second instance of a slug is '-1'.
111
+ * If you don't like the slug formatting or the accented character stripping doesn't work for you, it's easy to override Slug's formatting functions. Check the source for details.
112
+
113
+ ## Authors
114
+
115
+ Ben Koski, ben.koski@gmail.com
116
+
117
+ With generous contributions from:
118
+ * [Derek Willis](http://thescoop.org/)
119
+ * [Douglas Lovell](https://github.com/wbreeze)
120
+ * [Paul Battley](https://github.com/threedaymonk)
121
+ * [Yura Omelchuk](https://github.com/jurgens)
122
+ * others listed in the
123
+ [GitHub contributor list](https://github.com/bkoski/slug/graphs/contributors).
@@ -1,10 +1,6 @@
1
1
  require File.join(File.dirname(__FILE__), 'slug', 'slug')
2
- require File.join(File.dirname(__FILE__), 'slug', 'ascii_approximations')
3
2
 
4
- if defined?(ActiveRecord)
5
- ActiveRecord::Base.instance_eval { extend Slug::ClassMethods }
3
+ ActiveRecord::Base.instance_eval { include Slug }
4
+ if defined?(Rails) && Rails.version.to_i < 4
5
+ raise "This version of slug requires Rails 4 or higher"
6
6
  end
7
-
8
- if defined?(Rails) && Rails.version.to_i < 3
9
- raise "This version of slug requires Rails 3 or higher"
10
- end
@@ -1,5 +1,9 @@
1
+ require 'active_support/concern'
2
+
1
3
  module Slug
2
- module ClassMethods
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
3
7
 
4
8
  # Call this to set up slug handling on an ActiveRecord model.
5
9
  #
@@ -17,7 +21,6 @@ module Slug
17
21
  # If you'd like to update the slug later on, call <tt>@model.set_slug</tt>
18
22
  def slug source, opts={}
19
23
  class_attribute :slug_source, :slug_column, :generic_default
20
- include InstanceMethods
21
24
 
22
25
  self.slug_source = source
23
26
  self.slug_column = opts.fetch(:column, :slug)
@@ -36,100 +39,60 @@ module Slug
36
39
  end
37
40
  end
38
41
 
39
- module InstanceMethods
40
-
41
- # Sets the slug. Called before create.
42
- # By default, set_slug won't change slug if one already exists. Pass :force => true to overwrite.
43
- def set_slug(opts={})
44
- validate_slug_columns
45
- return unless self[self.slug_column].blank? || opts[:force] == true
46
-
47
- original_slug = self[self.slug_column]
48
- self[self.slug_column] = self.send(self.slug_source)
49
-
50
- strip_diacritics_from_slug
51
- normalize_slug
52
- genericize_slug if generic_default
53
- assign_slug_sequence unless self[self.slug_column] == original_slug # don't try to increment seq if slug hasn't changed
54
- end
42
+ # Sets the slug. Called before create.
43
+ # By default, set_slug won't change slug if one already exists. Pass :force => true to overwrite.
44
+ def set_slug(opts={})
45
+ validate_slug_columns
46
+ return if self[self.slug_column].present? && !opts[:force]
55
47
 
56
- # Overwrite existing slug based on current contents of source column.
57
- def reset_slug
58
- set_slug(:force => true)
59
- end
48
+ self[self.slug_column] = normalize_slug(self.send(self.slug_source))
60
49
 
61
- # Overrides to_param to return the model's slug.
62
- def to_param
63
- self[self.slug_column]
50
+ # if normalize_slug returned a blank string, try the generic_default handling
51
+ if generic_default && self[self.slug_column].blank?
52
+ self[self.slug_column] = self.class.to_s.demodulize.underscore.dasherize
64
53
  end
65
54
 
66
- def self.included(klass)
67
- klass.extend(ClassMethods)
68
- end
69
-
70
- private
71
- # Validates that source and destination methods exist. Invoked at runtime to allow definition
72
- # of source/slug methods after <tt>slug</tt> setup in class.
73
- def validate_slug_columns
74
- raise ArgumentError, "Source column '#{self.slug_source}' does not exist!" if !self.respond_to?(self.slug_source)
75
- raise ArgumentError, "Slug column '#{self.slug_column}' does not exist!" if !self.respond_to?("#{self.slug_column}=")
76
- end
55
+ assign_slug_sequence if self.changed_attributes.include?(self.slug_column)
56
+ end
77
57
 
78
- # Takes the slug, downcases it and replaces non-word characters with a -.
79
- # Feel free to override this method if you'd like different slug formatting.
80
- def normalize_slug
81
- return if self[self.slug_column].blank?
82
- s = ActiveSupport::Multibyte.proxy_class.new(self[self.slug_column]).normalize(:kc)
83
- s.downcase!
84
- s.strip!
85
- s.gsub!(/[^a-z0-9\s-]/, '') # Remove non-word characters
86
- s.gsub!(/\s+/, '-') # Convert whitespaces to dashes
87
- s.gsub!(/-\z/, '') # Remove trailing dashes
88
- s.gsub!(/-+/, '-') # get rid of double-dashes
89
- self[self.slug_column] = s.to_s
90
- end
58
+ # Overwrite existing slug based on current contents of source column.
59
+ def reset_slug
60
+ set_slug(:force => true)
61
+ end
91
62
 
92
- def genericize_slug
93
- if self[self.slug_column].blank?
94
- self[self.slug_column] = self.class.to_s.demodulize.underscore.dasherize
95
- end
96
- end
63
+ # Overrides to_param to return the model's slug.
64
+ def to_param
65
+ self[self.slug_column]
66
+ end
97
67
 
98
- # Converts accented characters to their ASCII equivalents and removes them if they have no equivalent.
99
- # Override this with a void function if you don't want accented characters to be stripped.
100
- def strip_diacritics_from_slug
101
- return if self[self.slug_column].blank?
102
- s = ActiveSupport::Multibyte.proxy_class.new(self[self.slug_column])
103
- s = s.normalize(:kd).unpack('U*')
104
- s = s.inject([]) do |a,u|
105
- if Slug::ASCII_APPROXIMATIONS[u]
106
- a += Slug::ASCII_APPROXIMATIONS[u].unpack('U*')
107
- elsif (u < 0x300 || u > 0x036F)
108
- a << u
109
- end
110
- a
111
- end
112
- s = s.pack('U*')
113
- self[self.slug_column] = s.to_s
114
- end
68
+ private
69
+ # Validates that source and destination methods exist. Invoked at runtime to allow definition
70
+ # of source/slug methods after <tt>slug</tt> setup in class.
71
+ def validate_slug_columns
72
+ raise ArgumentError, "Source column '#{self.slug_source}' does not exist!" if !self.respond_to?(self.slug_source)
73
+ raise ArgumentError, "Slug column '#{self.slug_column}' does not exist!" if !self.respond_to?("#{self.slug_column}=")
74
+ end
115
75
 
116
- # If a slug of the same name already exists, this will append '-n' to the end of the slug to
117
- # make it unique. The second instance gets a '-1' suffix.
118
- def assign_slug_sequence
119
- return if self[self.slug_column].blank?
120
- self[self.slug_column] = next_slug_sequence
121
- end
76
+ # Takes the slug, downcases it and replaces non-word characters with a -.
77
+ # Feel free to override this method if you'd like different slug formatting.
78
+ def normalize_slug(str)
79
+ return if str.blank?
80
+ str.gsub!(/[\p{Pc}\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Po}]/, '') # Remove punctuation
81
+ str.parameterize
82
+ end
122
83
 
123
- # Returns the next unique index for a slug.
124
- def next_slug_sequence
125
- assoc = self.class.base_class
126
- base_val = slug_val = self[self.slug_column]
127
- next_id = 1
128
- until assoc.where(self.slug_column => slug_val).first.nil? do
129
- slug_val = base_val + "-#{next_id}"
130
- next_id = next_id + 1
131
- end
132
- slug_val
84
+ # If a slug of the same name already exists, this will append '-n' to the end of the slug to
85
+ # make it unique. The second instance gets a '-1' suffix.
86
+ def assign_slug_sequence
87
+ return if self[self.slug_column].blank?
88
+ assoc = self.class.base_class
89
+ base_slug = self[self.slug_column]
90
+ seq = 0
91
+
92
+ while assoc.where(self.slug_column => self[self.slug_column]).exists? do
93
+ seq += 1
94
+ self[self.slug_column] = "#{base_slug}-#{seq}"
133
95
  end
134
96
  end
135
- end
97
+
98
+ end
@@ -3,25 +3,23 @@
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "slug"
6
- s.version = "4.0.1"
6
+ s.version = "4.1.0"
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib"]
10
10
  s.authors = ["Ben Koski"]
11
- s.date = "2018-11-11"
11
+ s.date = "2018-11-17"
12
12
  s.description = "Simple, straightforward slugs for your ActiveRecord models."
13
13
  s.email = "ben.koski@gmail.com"
14
14
  s.extra_rdoc_files = [
15
15
  "LICENSE",
16
- "README.rdoc"
16
+ "README.md"
17
17
  ]
18
18
  s.files = [
19
19
  "LICENSE",
20
- "README.rdoc",
20
+ "README.md",
21
21
  "Rakefile",
22
- "VERSION.yml",
23
22
  "lib/slug.rb",
24
- "lib/slug/ascii_approximations.rb",
25
23
  "lib/slug/slug.rb",
26
24
  "slug.gemspec",
27
25
  "test/models.rb",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slug
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.1
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Koski
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-11 00:00:00.000000000 Z
11
+ date: 2018-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -86,14 +86,12 @@ executables: []
86
86
  extensions: []
87
87
  extra_rdoc_files:
88
88
  - LICENSE
89
- - README.rdoc
89
+ - README.md
90
90
  files:
91
91
  - LICENSE
92
- - README.rdoc
92
+ - README.md
93
93
  - Rakefile
94
- - VERSION.yml
95
94
  - lib/slug.rb
96
- - lib/slug/ascii_approximations.rb
97
95
  - lib/slug/slug.rb
98
96
  - slug.gemspec
99
97
  - test/models.rb
@@ -1,104 +0,0 @@
1
- = slug
2
-
3
- Slug provides simple, straightforward slugging for your ActiveRecord models.
4
-
5
- Slug is based on code from Norman Clarke's fantastic {friendly_id}[http://friendly-id.rubyforge.org] project and Nick Zadrozny's {friendly_identifier}[http://code.google.com/p/friendly-identifier/].
6
-
7
- What's different:
8
-
9
- * Unlike friendly_id, slugs are stored directly in your model's table. friendly_id stores its data in a separate sluggable table, which enables cool things like slug versioning--but forces yet another join when trying to do complex find_by_slugs.
10
- * Like friendly_id, diacritics (accented characters) are stripped from slug strings.
11
- * The number of options is manageable
12
-
13
- == INSTALLATION
14
-
15
- Add the gem to your Gemfile
16
-
17
- gem 'slug'
18
-
19
- of your rails project.
20
-
21
- This is tested with Rails 5.1.4, MRI Ruby 2.4.1
22
-
23
- == USAGE
24
-
25
- It's up to you to set up the appropriate column in your model. By default, Slug saves the slug to a column called 'slug', so in most cases you'll just want to add
26
-
27
- t.string :slug
28
-
29
- to your create migration.
30
-
31
- I'd also suggest adding a unique index on the slug field in your migration
32
-
33
- add_index :model_name, :slug, :unique => true
34
-
35
- Though Slug uses validates_uniqueness_of to ensue the uniqueness of your slug, two concurrent INSERTs could conceivably try to set the same slug. Unlikely, but it's worth adding a unique index as a backstop.
36
-
37
- Once your table is migrated, just add
38
-
39
- slug :source_field
40
-
41
- to your ActiveRecord model. <tt>:source_field</tt> is the column you'd like to base the slug on--for example, it might be <tt>:headline</tt>.
42
-
43
- === INSTANCE METHOD AS SOURCE COLUMN
44
-
45
- The source column isn't limited to just database attributes--you can specify any instance method. This is handy if you need special formatting on your source column before it's slugged, or if you want to base the slug on several attributes.
46
-
47
- For example:
48
-
49
- class Article < ActiveRecord::Base
50
- slug :title_for_slug
51
-
52
- def title_for_slug
53
- "#{headline}-#{publication_date.year}-#{publication_date.month}"
54
- end
55
- end
56
-
57
- would use headline-pub year-pub month as the slug source.
58
-
59
- From here, you can work with your slug as if it were a normal column--<tt>find_by_slug</tt> and named scopes will work as they do for any other column.
60
-
61
- == OPTIONS
62
- There are two options:
63
- - <tt>:column</tt> is the slug_column, if for some reason you don't want
64
- your slugs stored in a column called "slug"
65
- - <tt>:generic_default</tt> is a boolean which, if true, will help you
66
- avoid <tt>ActiveRecord::ValidationError</tt> exceptions on the slug column.
67
-
68
- === COLUMN OPTION
69
- If you want to save the slug in a database column that isn't called
70
- <tt>slug</tt>, just pass the <tt>:column</tt> option. For example
71
-
72
- slug :headline, :column => :web_slug
73
-
74
- would generate a slug based on <tt>headline</tt> and save it to <tt>web_slug</tt>.
75
-
76
- === GENERIC DEFAULT OPTION
77
- You can avoid <tt>ActiveRecord::ValidationError</tt> exceptions that can
78
- occur if the source column is empty, blank, or only contains filtered
79
- characters. Simply set <tt>generic_default: true</tt>. For example
80
-
81
- slug :headline, generic_default: true
82
-
83
- will generate a slug based on your model name if the headline is blank.
84
-
85
- This is useful if the source column (e.g. headline) is based on user-generated
86
- input or can be blank (nil or empty).
87
-
88
- Some prefer to get the exception in this case. Others want to get a good
89
- slug and move on.
90
-
91
- == NOTES
92
- * Slug validates presence and uniqueness of the slug column. If you pass something that isn't sluggable as the source (for example, say you set the headline to '---'), a validation error will be set. (See the <tt>:generic_default</tt> option for avoiding this.
93
- * Slug doesn't update the slug if the source column changes. If you really need to regenerate the slug, call <tt>@model.set_slug</tt> before save.
94
- * If a slug already exists, Slug will automatically append a '-n' suffix to your slug to make it unique. The second instance of a slug is '-1'.
95
- * If you don't like the slug formatting or the accented character stripping doesn't work for you, it's easy to override Slug's formatting functions. Check the source for details.
96
-
97
- == AUTHOR
98
-
99
- Ben Koski, ben.koski@gmail.com
100
-
101
- with generous contributions from:
102
- - Derek Willis, @derekwillis
103
- - others listed in the commit history, which can be found in the
104
- {GitHub contributor list}[https://github.com/wbreeze/slug/graphs/contributors]
@@ -1,4 +0,0 @@
1
- ---
2
- :major: 4
3
- :minor: 0
4
- :patch: 1
@@ -1,14 +0,0 @@
1
- module Slug
2
- # ASCII approximations for accented characters.
3
- ASCII_APPROXIMATIONS = {
4
- 198 => "AE",
5
- 208 => "D",
6
- 216 => "O",
7
- 222 => "Th",
8
- 223 => "ss",
9
- 230 => "ae",
10
- 240 => "d",
11
- 248 => "o",
12
- 254 => "th"
13
- }.freeze
14
- end