sitemap_generator 7.0.3 → 7.1.1

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +84 -64
  3. data/README.md +142 -143
  4. data/VERSION +1 -1
  5. data/lib/capistrano/sitemap_generator.rb +2 -0
  6. data/lib/sitemap_generator/adapters/active_storage_adapter.rb +15 -10
  7. data/lib/sitemap_generator/adapters/aws_sdk_adapter.rb +13 -12
  8. data/lib/sitemap_generator/adapters/file_adapter.rb +10 -13
  9. data/lib/sitemap_generator/adapters/fog_adapter.rb +2 -2
  10. data/lib/sitemap_generator/adapters/google_storage_adapter.rb +5 -4
  11. data/lib/sitemap_generator/adapters/s3_adapter.rb +15 -14
  12. data/lib/sitemap_generator/adapters/wave_adapter.rb +5 -5
  13. data/lib/sitemap_generator/application.rb +5 -3
  14. data/lib/sitemap_generator/builder/sitemap_file.rb +21 -20
  15. data/lib/sitemap_generator/builder/sitemap_index_file.rb +20 -18
  16. data/lib/sitemap_generator/builder/sitemap_index_url.rb +5 -6
  17. data/lib/sitemap_generator/builder/sitemap_url.rb +82 -36
  18. data/lib/sitemap_generator/builder.rb +4 -2
  19. data/lib/sitemap_generator/core_ext/big_decimal.rb +38 -33
  20. data/lib/sitemap_generator/core_ext/numeric.rb +50 -46
  21. data/lib/sitemap_generator/helpers/number_helper.rb +52 -46
  22. data/lib/sitemap_generator/interpreter.rb +17 -15
  23. data/lib/sitemap_generator/link_set.rb +64 -66
  24. data/lib/sitemap_generator/railtie.rb +1 -0
  25. data/lib/sitemap_generator/simple_namer.rb +3 -4
  26. data/lib/sitemap_generator/sitemap_location.rb +44 -36
  27. data/lib/sitemap_generator/tasks.rb +3 -1
  28. data/lib/sitemap_generator/templates.rb +6 -7
  29. data/lib/sitemap_generator/utilities.rb +36 -17
  30. data/lib/sitemap_generator.rb +22 -14
  31. data/lib/tasks/sitemap_generator_tasks.rake +2 -0
  32. data/rails/install.rb +2 -0
  33. data/rails/uninstall.rb +2 -0
  34. data/templates/sitemap.rb +2 -0
  35. metadata +2 -2
@@ -10,23 +10,26 @@ module SitemapGenerator
10
10
  # compressed prior to being written out. Otherwise the data will be written out
11
11
  # unchanged.
12
12
  # @param raw_data - data to write to the file.
13
- def write(location, raw_data)
13
+ def write(location, raw_data) # rubocop:disable Metrics/MethodLength
14
14
  # Ensure that the directory exists
15
15
  dir = location.directory
16
16
  if !File.exist?(dir)
17
17
  FileUtils.mkdir_p(dir)
18
18
  elsif !File.directory?(dir)
19
- raise SitemapError.new("#{dir} should be a directory!")
19
+ raise SitemapError, "#{dir} should be a directory!"
20
20
  end
21
21
 
22
- stream = open(location.path, 'wb')
23
- if location.path.to_s =~ /.gz$/
24
- gzip(stream, raw_data)
25
- else
26
- plain(stream, raw_data)
22
+ File.open(location.path, 'wb') do |stream|
23
+ if location.gzip?
24
+ gzip(stream, raw_data)
25
+ else
26
+ stream.write raw_data
27
+ end
27
28
  end
28
29
  end
29
30
 
31
+ private
32
+
30
33
  # Write `data` to a stream, passing the data through a GzipWriter
31
34
  # to compress it.
32
35
  def gzip(stream, data)
@@ -34,11 +37,5 @@ module SitemapGenerator
34
37
  gz.write data
35
38
  gz.close
36
39
  end
37
-
38
- # Write `data` to a stream as is.
39
- def plain(stream, data)
40
- stream.write data
41
- stream.close
42
- end
43
40
  end
44
41
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if !defined?(Fog::Storage)
3
+ unless defined?(Fog::Storage)
4
4
  raise LoadError, "Error: `Fog::Storage` is not defined.\n\n" \
5
- "Please `require 'fog'` - or another library that defines this class."
5
+ "Please `require 'fog'` - or another library that defines this class."
6
6
  end
7
7
 
8
8
  module SitemapGenerator
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if !defined?(Google::Cloud::Storage)
3
+ unless defined?(Google::Cloud::Storage)
4
4
  raise LoadError, "Error: `Google::Cloud::Storage` is not defined.\n\n" \
5
- "Please `require 'google/cloud/storage'` - or another library that defines this class."
5
+ "Please `require 'google/cloud/storage'` - or another library that defines this class."
6
6
  end
7
7
 
8
8
  module SitemapGenerator
@@ -12,7 +12,8 @@ module SitemapGenerator
12
12
  #
13
13
  # @param [Hash] opts Google::Cloud::Storage configuration options.
14
14
  # @option :bucket [String] Required. Name of Google Storage Bucket where the file is to be uploaded.
15
- # @option :acl [String] Optional. Access control which is applied to the uploaded file(s). Default value is 'public'.
15
+ # @option :acl [String] Optional. Access control which is applied to the uploaded file(s).
16
+ # Default value is 'public'.
16
17
  #
17
18
  # All options other than the `:bucket` and `:acl` options are passed to the `Google::Cloud::Storage.new`
18
19
  # initializer. See https://googleapis.dev/ruby/google-cloud-storage/latest/file.AUTHENTICATION.html
@@ -25,7 +26,7 @@ module SitemapGenerator
25
26
  def initialize(opts = {})
26
27
  opts = opts.clone
27
28
  @bucket = opts.delete(:bucket)
28
- @acl = opts.has_key?(:acl) ? opts.delete(:acl) : 'public'
29
+ @acl = opts.key?(:acl) ? opts.delete(:acl) : 'public'
29
30
  @storage_options = opts
30
31
  end
31
32
 
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if !defined?(Fog::Storage)
3
+ unless defined?(Fog::Storage)
4
4
  raise LoadError, "Error: `Fog::Storage` is not defined.\n\n" \
5
- "Please `require 'fog-aws'` - or another library that defines this class."
5
+ "Please `require 'fog-aws'` - or another library that defines this class."
6
6
  end
7
7
 
8
8
  module SitemapGenerator
@@ -22,21 +22,21 @@ module SitemapGenerator
22
22
  #
23
23
  # Alternatively you can use an environment variable to configure each option (except `fog_storage_options`).
24
24
  # The environment variables have the same name but capitalized, e.g. `FOG_PATH_STYLE`.
25
- def initialize(opts = {})
26
- @aws_access_key_id = opts[:aws_access_key_id] || ENV['AWS_ACCESS_KEY_ID']
27
- @aws_secret_access_key = opts[:aws_secret_access_key] || ENV['AWS_SECRET_ACCESS_KEY']
28
- @aws_session_token = opts[:aws_session_token] || ENV['AWS_SESSION_TOKEN']
29
- @fog_provider = opts[:fog_provider] || ENV['FOG_PROVIDER']
30
- @fog_directory = opts[:fog_directory] || ENV['FOG_DIRECTORY']
31
- @fog_region = opts[:fog_region] || ENV['FOG_REGION']
32
- @fog_path_style = opts[:fog_path_style] || ENV['FOG_PATH_STYLE']
25
+ def initialize(opts = {}) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
26
+ @aws_access_key_id = opts[:aws_access_key_id] || ENV.fetch('AWS_ACCESS_KEY_ID', nil)
27
+ @aws_secret_access_key = opts[:aws_secret_access_key] || ENV.fetch('AWS_SECRET_ACCESS_KEY', nil)
28
+ @aws_session_token = opts[:aws_session_token] || ENV.fetch('AWS_SESSION_TOKEN', nil)
29
+ @fog_provider = opts[:fog_provider] || ENV.fetch('FOG_PROVIDER', nil)
30
+ @fog_directory = opts[:fog_directory] || ENV.fetch('FOG_DIRECTORY', nil)
31
+ @fog_region = opts[:fog_region] || ENV.fetch('FOG_REGION', nil)
32
+ @fog_path_style = opts[:fog_path_style] || ENV.fetch('FOG_PATH_STYLE', nil)
33
33
  @fog_storage_options = opts[:fog_storage_options] || {}
34
- fog_public = opts[:fog_public].nil? ? ENV['FOG_PUBLIC'] : opts[:fog_public]
35
- @fog_public = SitemapGenerator::Utilities.falsy?(fog_public) ? false : true
34
+ fog_public = opts[:fog_public].nil? ? ENV.fetch('FOG_PUBLIC', nil) : opts[:fog_public]
35
+ @fog_public = !SitemapGenerator::Utilities.falsy?(fog_public)
36
36
  end
37
37
 
38
38
  # Call with a SitemapLocation and string data
39
- def write(location, raw_data)
39
+ def write(location, raw_data) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
40
40
  SitemapGenerator::FileAdapter.new.write(location, raw_data)
41
41
 
42
42
  credentials = { provider: @fog_provider }
@@ -57,7 +57,8 @@ module SitemapGenerator
57
57
  directory.files.create(
58
58
  key: location.path_in_public,
59
59
  body: File.open(location.path),
60
- public: @fog_public
60
+ public: @fog_public,
61
+ content_type: location.content_type
61
62
  )
62
63
  end
63
64
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if !defined?(::CarrierWave::Uploader::Base)
3
+ unless defined?(CarrierWave::Uploader::Base)
4
4
  raise LoadError, "Error: `CarrierWave::Uploader::Base` is not defined.\n\n" \
5
- "Please `require 'carrierwave'` - or another library that defines this class."
5
+ "Please `require 'carrierwave'` - or another library that defines this class."
6
6
  end
7
7
 
8
8
  module SitemapGenerator
@@ -14,10 +14,10 @@ module SitemapGenerator
14
14
  def write(location, raw_data)
15
15
  SitemapGenerator::FileAdapter.new.write(location, raw_data)
16
16
  directory = File.dirname(location.path_in_public)
17
- if directory != '.'
18
- self.store_dir = directory
17
+ self.store_dir = directory if directory != '.'
18
+ File.open(location.path, 'rb') do |io|
19
+ store!(io)
19
20
  end
20
- store!(open(location.path, 'rb'))
21
21
  end
22
22
  end
23
23
  end
@@ -3,17 +3,19 @@
3
3
  require 'pathname'
4
4
 
5
5
  module SitemapGenerator
6
+ # Thin abstraction over the host application environment.
7
+ # Provides the app +root+ path and Rails version detection.
6
8
  class Application
7
- def is_rails?
9
+ def is_rails? # rubocop:disable Naming/PredicatePrefix
8
10
  !!defined?(Rails::VERSION)
9
11
  end
10
12
 
11
13
  # Returns a boolean indicating whether this environment is Rails 3
12
14
  #
13
15
  # @return [Boolean]
14
- def is_at_least_rails3?
16
+ def is_at_least_rails3? # rubocop:disable Naming/PredicatePrefix
15
17
  is_rails? && Rails.version.to_f >= 3
16
- rescue
18
+ rescue StandardError
17
19
  false # Rails.version defined in 2.1.0
18
20
  end
19
21
 
@@ -11,10 +11,12 @@ module SitemapGenerator
11
11
  #
12
12
  # sitemap = SitemapFile.new(:location => SitemapLocation.new(...))
13
13
  # sitemap.add('/', { ... }) <- add a link to the sitemap
14
- # sitemap.finalize! <- write the sitemap file and freeze the object to protect it from further modification
14
+ # sitemap.finalize! <- write the sitemap file and freeze the object
15
+ # to protect it from further modification
15
16
  #
16
17
  class SitemapFile
17
18
  include SitemapGenerator::Helpers::NumberHelper
19
+
18
20
  attr_reader :link_count, :filesize, :location, :news_count
19
21
 
20
22
  # === Options
@@ -22,7 +24,7 @@ module SitemapGenerator
22
24
  # * <tt>location</tt> - a SitemapGenerator::SitemapLocation instance or a Hash of options
23
25
  # from which a SitemapLocation will be created for you. See `SitemapGenerator::SitemapLocation` for
24
26
  # the supported list of options.
25
- def initialize(opts = {})
27
+ def initialize(opts = {}) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
26
28
  @location = opts.is_a?(Hash) ? SitemapGenerator::SitemapLocation.new(opts) : opts
27
29
  @link_count = 0
28
30
  @news_count = 0
@@ -46,33 +48,33 @@ module SitemapGenerator
46
48
  HTML
47
49
  @xml_wrapper_start.gsub!(/\s+/, ' ').gsub!(/ *> */, '>').strip!
48
50
  @xml_wrapper_end = '</urlset>'
49
- @filesize = SitemapGenerator::Utilities.bytesize(@xml_wrapper_start) + SitemapGenerator::Utilities.bytesize(@xml_wrapper_end)
51
+ @filesize = SitemapGenerator::Utilities.bytesize(@xml_wrapper_start) +
52
+ SitemapGenerator::Utilities.bytesize(@xml_wrapper_end)
50
53
  @xml_content = @xml_wrapper_start
51
54
  @written = false
52
55
  @reserved_name = nil # holds the name reserved from the namer
53
56
  @frozen = false # rather than actually freeze, use this boolean
54
57
  end
55
58
 
56
- # If a name has been reserved, use the last modified time from the file.
57
- # Otherwise return nil. We don't want to prematurely assign a name
58
- # for this sitemap if one has not yet been reserved, because we may
59
- # mess up the name-assignment sequence.
59
+ # Return the time the sitemap was written, or nil if it has not been written yet.
60
+ # Returning nil before write prevents prematurely reserving a name in the
61
+ # namer sequence.
60
62
  def lastmod
61
- File.mtime(location.path) if location.reserved_name?
62
- rescue
63
- nil
63
+ @lastmod if location.reserved_name?
64
64
  end
65
65
 
66
66
  def empty?
67
- @link_count == 0
67
+ @link_count.zero?
68
68
  end
69
69
 
70
70
  # Return a boolean indicating whether the sitemap file can fit another link
71
71
  # of <tt>bytes</tt> bytes in size. You can also pass a string and the
72
72
  # bytesize will be calculated for you.
73
73
  def file_can_fit?(bytes)
74
- bytes = bytes.is_a?(String) ? SitemapGenerator::Utilities.bytesize(bytes) : bytes
75
- (@filesize + bytes) < SitemapGenerator::MAX_SITEMAP_FILESIZE && @link_count < max_sitemap_links && @news_count < SitemapGenerator::MAX_SITEMAP_NEWS
74
+ bytes = SitemapGenerator::Utilities.bytesize(bytes) if bytes.is_a?(String)
75
+ (@filesize + bytes) < SitemapGenerator::MAX_SITEMAP_FILESIZE &&
76
+ @link_count < max_sitemap_links &&
77
+ @news_count < SitemapGenerator::MAX_SITEMAP_NEWS
76
78
  end
77
79
 
78
80
  # Add a link to the sitemap file.
@@ -94,7 +96,7 @@ module SitemapGenerator
94
96
  #
95
97
  # The link added to the sitemap will use the host from its location object
96
98
  # if no host has been specified.
97
- def add(link, options = {})
99
+ def add(link, options = {}) # rubocop:disable Metrics/MethodLength
98
100
  raise SitemapGenerator::SitemapFinalizedError if finalized?
99
101
 
100
102
  sitemap_url =
@@ -106,11 +108,9 @@ module SitemapGenerator
106
108
  end
107
109
 
108
110
  xml = sitemap_url.to_xml
109
- raise SitemapGenerator::SitemapFullError if !file_can_fit?(xml)
111
+ raise SitemapGenerator::SitemapFullError unless file_can_fit?(xml)
110
112
 
111
- if sitemap_url.news?
112
- @news_count += 1
113
- end
113
+ @news_count += 1 if sitemap_url.news?
114
114
 
115
115
  # Add the XML to the sitemap
116
116
  @xml_content << xml
@@ -140,12 +140,13 @@ module SitemapGenerator
140
140
  # A SitemapGenerator::SitemapError exception is raised if the file has
141
141
  # already been written.
142
142
  def write
143
- raise SitemapGenerator::SitemapError.new('Sitemap already written!') if written?
143
+ raise SitemapGenerator::SitemapError, 'Sitemap already written!' if written?
144
144
 
145
145
  finalize! unless finalized?
146
146
  reserve_name
147
147
  @xml_content << @xml_wrapper_end
148
148
  @location.write(@xml_content, link_count)
149
+ @lastmod = SitemapGenerator::Utilities.current_time
149
150
  @xml_content = @xml_wrapper_end = ''
150
151
  @written = true
151
152
  end
@@ -158,7 +159,7 @@ module SitemapGenerator
158
159
  # Reserve a name from the namer unless one has already been reserved.
159
160
  # Safe to call more than once.
160
161
  def reserve_name
161
- @reserved_name ||= @location.reserve_name
162
+ @reserved_name ||= @location.reserve_name # rubocop:disable Naming/MemoizedInstanceVariableName
162
163
  end
163
164
 
164
165
  # Return a boolean indicating whether a name has been reserved
@@ -2,13 +2,15 @@
2
2
 
3
3
  module SitemapGenerator
4
4
  module Builder
5
+ # Builds the sitemap index XML file, listing all sitemap files.
6
+ # Manages deferred naming (waits for a second sitemap before reserving the index name)
7
+ # and controls whether an index is written at all via +create_index+.
5
8
  class SitemapIndexFile < SitemapFile
6
-
7
9
  # === Options
8
10
  #
9
11
  # * <tt>location</tt> - a SitemapGenerator::SitemapIndexLocation instance or a Hash of options
10
12
  # from which a SitemapLocation will be created for you.
11
- def initialize(opts = {})
13
+ def initialize(opts = {}) # rubocop:disable Lint/MissingSuper, Metrics/MethodLength
12
14
  @location = opts.is_a?(Hash) ? SitemapGenerator::SitemapIndexLocation.new(opts) : opts
13
15
  @link_count = 0
14
16
  @sitemaps_link_count = 0
@@ -49,9 +51,9 @@ module SitemapGenerator
49
51
  # If a link is being added to the index manually as a string, then we
50
52
  # can assume that the index is required (unless create_index is false of course).
51
53
  # This seems like the logical thing to do.
52
- alias_method :super_add, :add
53
- def add(link, options = {})
54
- if file = link.is_a?(SitemapFile) && link
54
+ alias super_add add
55
+ def add(link, options = {}) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
56
+ if (file = link.is_a?(SitemapFile) && link)
55
57
  @sitemaps_link_count += file.link_count
56
58
  file.finalize! unless file.finalized?
57
59
 
@@ -62,7 +64,7 @@ module SitemapGenerator
62
64
  # first name (the index, or the sitemap). If the item is not a SitemapFile,
63
65
  # then it has been manually added and we can be sure that the user intends
64
66
  # for there to be an index.
65
- if @link_count == 0
67
+ if @link_count.zero?
66
68
  @first_sitemap = SitemapGenerator::Builder::LinkHolder.new(file, options)
67
69
  @link_count += 1 # pretend it's added, but don't add it yet
68
70
  else
@@ -90,7 +92,7 @@ module SitemapGenerator
90
92
  # of <tt>bytes</tt> bytes in size. You can also pass a string and the
91
93
  # bytesize will be calculated for you.
92
94
  def file_can_fit?(bytes)
93
- bytes = bytes.is_a?(String) ? SitemapGenerator::Utilities.bytesize(bytes) : bytes
95
+ bytes = SitemapGenerator::Utilities.bytesize(bytes) if bytes.is_a?(String)
94
96
  (@filesize + bytes) < SitemapGenerator::MAX_SITEMAP_FILESIZE && @link_count < SitemapGenerator::MAX_SITEMAP_FILES
95
97
  end
96
98
 
@@ -101,7 +103,7 @@ module SitemapGenerator
101
103
 
102
104
  def stats_summary(opts = {})
103
105
  str = "Sitemap stats: #{number_with_delimiter(@sitemaps_link_count)} links / #{@link_count} sitemaps"
104
- str += ' / %dm%02ds' % opts[:time_taken].divmod(60) if opts[:time_taken]
106
+ str + format(' / %dm%02ds', *opts[:time_taken].divmod(60)) if opts[:time_taken] # rubocop:disable Style/FormatStringToken
105
107
  end
106
108
 
107
109
  def finalize!
@@ -122,7 +124,7 @@ module SitemapGenerator
122
124
  # If a link is added manually and create_index is not false, we force index
123
125
  # creation because they obviously intend for there to be an index. False otherwise.
124
126
  def create_index?
125
- @create_index || @location.create_index == true || @location.create_index == :auto && @link_count > 1
127
+ @create_index || @location.create_index == true || (@location.create_index == :auto && @link_count > 1)
126
128
  end
127
129
 
128
130
  # Return the index file URL. If create_index is true, this is the URL
@@ -141,15 +143,15 @@ module SitemapGenerator
141
143
 
142
144
  # Make sure the first sitemap has been written out and added to the index
143
145
  def write_first_sitemap
144
- if @first_sitemap
145
- @first_sitemap.link.write unless @first_sitemap.link.written?
146
- super_add(SitemapGenerator::Builder::SitemapIndexUrl.new(@first_sitemap.link, @first_sitemap.options))
147
- @link_count -= 1 # we already counted it, don't count it twice
148
- # Store the URL because if create_index is false, this is the
149
- # "index" URL
150
- @first_sitemap_url = @first_sitemap.link.location.url
151
- @first_sitemap = nil
152
- end
146
+ return unless @first_sitemap
147
+
148
+ @first_sitemap.link.write unless @first_sitemap.link.written?
149
+ super_add(SitemapGenerator::Builder::SitemapIndexUrl.new(@first_sitemap.link, @first_sitemap.options))
150
+ @link_count -= 1 # we already counted it, don't count it twice
151
+ # Store the URL because if create_index is false, this is the
152
+ # "index" URL
153
+ @first_sitemap_url = @first_sitemap.link.location.url
154
+ @first_sitemap = nil
153
155
  end
154
156
  end
155
157
  end
@@ -4,16 +4,15 @@ require 'builder'
4
4
 
5
5
  module SitemapGenerator
6
6
  module Builder
7
+ # Wraps a sitemap file reference as a +<sitemap>+ XML entry for use in a sitemap index.
7
8
  class SitemapIndexUrl < SitemapUrl
8
-
9
9
  def initialize(path, options = {})
10
- if index = path.is_a?(SitemapGenerator::Builder::SitemapIndexFile) && path
11
- options = SitemapGenerator::Utilities.reverse_merge(options, host: index.location.host, lastmod: Time.now, priority: 1.0)
10
+ if (index = path.is_a?(SitemapGenerator::Builder::SitemapIndexFile) && path)
11
+ options = SitemapGenerator::Utilities.reverse_merge(options, host: index.location.host, lastmod: SitemapGenerator::Utilities.current_time,
12
+ priority: 1.0)
12
13
  path = index.location.path_in_public
13
- super(path, options)
14
- else
15
- super
16
14
  end
15
+ super
17
16
  end
18
17
 
19
18
  # Return the URL as XML
@@ -9,8 +9,7 @@ module SitemapGenerator
9
9
  module Builder
10
10
  # A Hash-like class for holding information about a sitemap URL and
11
11
  # generating an XML <url> element suitable for sitemaps.
12
- class SitemapUrl < Hash
13
-
12
+ class SitemapUrl < Hash # rubocop:disable Metrics/ClassLength
14
13
  # Return a new instance with options configured on it.
15
14
  #
16
15
  # == Arguments
@@ -31,9 +30,10 @@ module SitemapGenerator
31
30
  # * +mobile+
32
31
  # * +alternate+/+alternates+
33
32
  # * +pagemap+
34
- def initialize(path, options = {})
33
+ def initialize(path, options = {}) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
34
+ super()
35
35
  options = SitemapGenerator::Utilities.symbolize_keys(options)
36
- if sitemap = path.is_a?(SitemapGenerator::Builder::SitemapFile) && path
36
+ if (sitemap = path.is_a?(SitemapGenerator::Builder::SitemapFile) && path)
37
37
  SitemapGenerator::Utilities.reverse_merge!(
38
38
  options,
39
39
  host: sitemap.location.host,
@@ -44,13 +44,15 @@ module SitemapGenerator
44
44
 
45
45
  SitemapGenerator::Utilities.assert_valid_keys(
46
46
  options,
47
- :priority, :changefreq, :lastmod, :expires, :host, :images, :video, :news, :videos, :mobile, :alternate, :alternates, :pagemap
47
+ :priority, :changefreq, :lastmod, :expires, :host,
48
+ :images, :video, :news, :videos, :mobile,
49
+ :alternate, :alternates, :pagemap
48
50
  )
49
51
  SitemapGenerator::Utilities.reverse_merge!(
50
52
  options,
51
53
  priority: 0.5,
52
54
  changefreq: 'weekly',
53
- lastmod: Time.now,
55
+ lastmod: SitemapGenerator::Utilities.current_time,
54
56
  images: [],
55
57
  news: {},
56
58
  videos: [],
@@ -59,16 +61,17 @@ module SitemapGenerator
59
61
  )
60
62
  raise 'Cannot generate a url without a host' unless SitemapGenerator::Utilities.present?(options[:host])
61
63
 
62
- if video = options.delete(:video)
64
+ if (video = options.delete(:video))
63
65
  options[:videos] = video.is_a?(Array) ? options[:videos].concat(video) : options[:videos] << video
64
66
  end
65
- if alternate = options.delete(:alternate)
66
- options[:alternates] = alternate.is_a?(Array) ? options[:alternates].concat(alternate) : options[:alternates] << alternate
67
+ if (alternate = options.delete(:alternate))
68
+ options[:alternates] =
69
+ alternate.is_a?(Array) ? options[:alternates].concat(alternate) : options[:alternates] << alternate
67
70
  end
68
71
 
69
- path = path.to_s.sub(/^\//, '')
70
- loc = path.empty? ? options[:host] : (options[:host].to_s.sub(/\/$/, '') + '/' + path)
71
- self.merge!(
72
+ path = encode_non_ascii(path.to_s.sub(%r{^/}, ''))
73
+ loc = path.empty? ? options[:host] : "#{options[:host].to_s.sub(%r{/$}, '')}/#{path}"
74
+ merge!(
72
75
  priority: options[:priority],
73
76
  changefreq: options[:changefreq],
74
77
  lastmod: options[:lastmod],
@@ -79,15 +82,15 @@ module SitemapGenerator
79
82
  news: prepare_news(options[:news]),
80
83
  videos: options[:videos],
81
84
  mobile: options[:mobile],
82
- alternates: options[:alternates],
85
+ alternates: prepare_alternates(options[:alternates], options[:host]),
83
86
  pagemap: options[:pagemap]
84
87
  )
85
88
  end
86
89
 
87
90
  # Return the URL as XML
88
- def to_xml(builder = nil)
91
+ def to_xml(builder = nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
89
92
  builder = ::Builder::XmlMarkup.new if builder.nil?
90
- builder.url do
93
+ builder.url do # rubocop:disable Metrics/BlockLength
91
94
  builder.loc self[:loc]
92
95
  builder.lastmod w3c_date(self[:lastmod]) if self[:lastmod]
93
96
  builder.expires w3c_date(self[:expires]) if self[:expires]
@@ -121,15 +124,17 @@ module SitemapGenerator
121
124
  end
122
125
  end
123
126
 
124
- self[:videos].each do |video|
125
- builder.video :video do
127
+ self[:videos].each do |video| # rubocop:disable Metrics/BlockLength
128
+ builder.video :video do # rubocop:disable Metrics/BlockLength
126
129
  builder.video :thumbnail_loc, video[:thumbnail_loc].to_s
127
130
  builder.video :title, video[:title].to_s
128
131
  builder.video :description, video[:description].to_s
129
132
  builder.video :content_loc, video[:content_loc].to_s if video[:content_loc]
130
133
  if video[:player_loc]
131
134
  loc_attributes = { allow_embed: yes_or_no_with_default(video[:allow_embed], true) }
132
- loc_attributes[:autoplay] = video[:autoplay].to_s if SitemapGenerator::Utilities.present?(video[:autoplay])
135
+ if SitemapGenerator::Utilities.present?(video[:autoplay])
136
+ loc_attributes[:autoplay] = video[:autoplay].to_s
137
+ end
133
138
  builder.video :player_loc, video[:player_loc].to_s, loc_attributes
134
139
  end
135
140
  builder.video :duration, video[:duration].to_s if video[:duration]
@@ -137,17 +142,30 @@ module SitemapGenerator
137
142
  builder.video :rating, format_float(video[:rating]) if video[:rating]
138
143
  builder.video :view_count, video[:view_count].to_s if video[:view_count]
139
144
  builder.video :publication_date, w3c_date(video[:publication_date]) if video[:publication_date]
140
- video[:tags].each { |tag| builder.video :tag, tag.to_s } if video[:tags]
145
+ video[:tags]&.each { |tag| builder.video :tag, tag.to_s }
141
146
  builder.video :tag, video[:tag].to_s if video[:tag]
142
147
  builder.video :category, video[:category].to_s if video[:category]
143
- builder.video :family_friendly, yes_or_no_with_default(video[:family_friendly], true) if video.has_key?(:family_friendly)
144
- builder.video :gallery_loc, video[:gallery_loc].to_s, title: video[:gallery_title].to_s if video[:gallery_loc]
145
- builder.video :price, video[:price].to_s, prepare_video_price_attribs(video) if SitemapGenerator::Utilities.present?(video[:price])
148
+ if video.key?(:family_friendly)
149
+ builder.video :family_friendly,
150
+ yes_or_no_with_default(video[:family_friendly], true)
151
+ end
152
+ if video[:gallery_loc]
153
+ builder.video :gallery_loc, video[:gallery_loc].to_s,
154
+ title: video[:gallery_title].to_s
155
+ end
156
+ if SitemapGenerator::Utilities.present?(video[:price])
157
+ builder.video :price, video[:price].to_s, prepare_video_price_attribs(video)
158
+ end
146
159
  if video[:uploader]
147
- builder.video :uploader, video[:uploader].to_s, video[:uploader_info] ? { info: video[:uploader_info].to_s } : {}
160
+ builder.video :uploader, video[:uploader].to_s,
161
+ video[:uploader_info] ? { info: video[:uploader_info].to_s } : {}
162
+ end
163
+ builder.video :live, yes_or_no_with_default(video[:live], true) if video.key?(:live)
164
+ if video.key?(:requires_subscription)
165
+ builder.video :requires_subscription,
166
+ yes_or_no_with_default(video[:requires_subscription],
167
+ true)
148
168
  end
149
- builder.video :live, yes_or_no_with_default(video[:live], true) if video.has_key?(:live)
150
- builder.video :requires_subscription, yes_or_no_with_default(video[:requires_subscription], true) if video.has_key?(:requires_subscription)
151
169
  end
152
170
  end
153
171
 
@@ -159,9 +177,7 @@ module SitemapGenerator
159
177
  builder.xhtml :link, attributes
160
178
  end
161
179
 
162
- unless SitemapGenerator::Utilities.blank?(self[:mobile])
163
- builder.mobile :mobile
164
- end
180
+ builder.mobile :mobile unless SitemapGenerator::Utilities.blank?(self[:mobile])
165
181
 
166
182
  unless SitemapGenerator::Utilities.blank?(self[:pagemap])
167
183
  builder.pagemap :PageMap do
@@ -188,18 +204,40 @@ module SitemapGenerator
188
204
  attribs = {}
189
205
  attribs[:currency] = video[:price_currency].to_s # required
190
206
  attribs[:type] = video[:price_type] if SitemapGenerator::Utilities.present?(video[:price_type])
191
- attribs[:resolution] = video[:price_resolution] if SitemapGenerator::Utilities.present?(video[:price_resolution])
207
+ if SitemapGenerator::Utilities.present?(video[:price_resolution])
208
+ attribs[:resolution] = video[:price_resolution]
209
+ end
192
210
  attribs
193
211
  end
194
212
 
195
213
  def prepare_news(news)
196
- SitemapGenerator::Utilities.assert_valid_keys(news, :publication_name, :publication_language, :publication_date, :genres, :access, :title, :keywords, :stock_tickers) unless news.empty?
214
+ unless news.empty?
215
+ SitemapGenerator::Utilities.assert_valid_keys(
216
+ news,
217
+ :publication_name, :publication_language, :publication_date,
218
+ :genres, :access, :title, :keywords, :stock_tickers
219
+ )
220
+ end
197
221
  news
198
222
  end
199
223
 
224
+ # Return an Array of alternate option Hashes with relative hrefs expanded to absolute URLs.
225
+ def prepare_alternates(alternates, host)
226
+ alternates.map do |alt|
227
+ next alt unless alt.is_a?(Hash)
228
+
229
+ href = alt[:href].to_s
230
+ if href.start_with?('/')
231
+ alt.merge(href: URI.join(host, href).to_s)
232
+ else
233
+ alt
234
+ end
235
+ end
236
+ end
237
+
200
238
  # Return an Array of image option Hashes suitable to be parsed by SitemapGenerator::Builder::SitemapFile
201
239
  def prepare_images(images, host)
202
- images.delete_if { |key, value| key[:loc] == nil }
240
+ images.delete_if { |key, _value| key[:loc].nil? }
203
241
  images.each do |r|
204
242
  SitemapGenerator::Utilities.assert_valid_keys(r, :loc, :caption, :geo_location, :title, :license)
205
243
  r[:loc] = URI.join(host, r[:loc]).to_s
@@ -207,7 +245,7 @@ module SitemapGenerator
207
245
  images[0..(SitemapGenerator::MAX_SITEMAP_IMAGES - 1)]
208
246
  end
209
247
 
210
- def w3c_date(date)
248
+ def w3c_date(date) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
211
249
  if date.is_a?(String)
212
250
  date
213
251
  elsif date.respond_to?(:iso8601)
@@ -222,8 +260,6 @@ module SitemapGenerator
222
260
  date.utc
223
261
  elsif date.is_a?(Integer)
224
262
  Time.at(date).utc
225
- else
226
- nil
227
263
  end
228
264
 
229
265
  if zulutime
@@ -239,7 +275,9 @@ module SitemapGenerator
239
275
  # value must be 'yes' or 'no'. Pass the default value as a boolean using `default`.
240
276
  def yes_or_no(value)
241
277
  if value.is_a?(String)
242
- raise ArgumentError.new("Unrecognized value for yes/no field: #{value.inspect}") unless value =~ /^(yes|no)$/i
278
+ unless /^(yes|no)$/i.match?(value)
279
+ raise ArgumentError, "Unrecognized value for yes/no field: #{value.inspect}"
280
+ end
243
281
 
244
282
  value.downcase
245
283
  else
@@ -256,7 +294,15 @@ module SitemapGenerator
256
294
  # Format a float to to one decimal precision.
257
295
  # TODO: Use rounding with precision once merged with framework_agnostic.
258
296
  def format_float(value)
259
- value.is_a?(String) ? value : ('%0.1f' % value)
297
+ value.is_a?(String) ? value : format('%0.1f', value)
298
+ end
299
+
300
+ private
301
+
302
+ # Percent-encode non-ASCII bytes in a path segment, leaving already-encoded %XX sequences untouched.
303
+ # Applied to paths only — not the host — IDN hostnames require punycode, not percent-encoding.
304
+ def encode_non_ascii(str)
305
+ str.gsub(/[^\x00-\x7F]/) { |c| c.bytes.map { |b| format('%%%02X', b) }.join }
260
306
  end
261
307
  end
262
308
  end