jekyll-auto-thumbnails 2.0.1 → 2.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d593b4ae0f39bd0020f6656a021327666afa8ee4776a2147d36ce3f8283a1840
4
- data.tar.gz: af035e37d27382e9a8d1d15b20ea14e3a0994f2fe1d1ff312c09b4dac924829f
3
+ metadata.gz: ba5ecfc504a10dfd094946e47dc0062ef2c200a4650aa35694b27e389ad1dc15
4
+ data.tar.gz: 65e33707b09dbafeb8fbd5cc9d6f2dbced647e403e9ef78d84a0201466d39cb6
5
5
  SHA512:
6
- metadata.gz: 89945f98a6e83a64ec2d4f20b96e827d4e408ffdda0f3340da9e99086ef156ef146a6617db1f3cb1ba7de8f34d4790f47783924fc3e02afb5e1f9a9c90506d33
7
- data.tar.gz: 0b0a29fc0985c1a1930f2b1a644ed574fb4abae81fe52f0722b0a329c2731020e7cc915b7fdf8155b63e1767f2227ac409a5e2697152648d61cdca6816f59582
6
+ metadata.gz: 2ddbcd68c77f775d8932b7e42f101b99d164a29bed79e4c176d5606e0afb7234277353483526aa67e1eb92ce5d3fcf2c5f0ca88d091f58b105b8f4f51eb5f7d5
7
+ data.tar.gz: 4e98ebbe7ea692d0ca7b4b17e2b4c4136db49f026c8d7484ac3a18667503115e08735269df5e933912ad19d9ecddae81fb713c6dea1e9ea14a037360bb8907df
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.0](https://github.com/Texarkanine/jekyll-auto-thumbnails/compare/v2.1.0...v2.2.0) (2026-08-13)
4
+
5
+
6
+ ### Features
7
+
8
+ * **hooks:** append elapsed time to completion logs ([#54](https://github.com/Texarkanine/jekyll-auto-thumbnails/issues/54)) ([4bb9c6f](https://github.com/Texarkanine/jekyll-auto-thumbnails/commit/4bb9c6fcb9a83802e503d21d108017016c00c543))
9
+
10
+ ## [2.1.0](https://github.com/Texarkanine/jekyll-auto-thumbnails/compare/v2.0.1...v2.1.0) (2026-07-19)
11
+
12
+
13
+ ### Features
14
+
15
+ * **test:** add Mutant mutation testing with RSpec ([#50](https://github.com/Texarkanine/jekyll-auto-thumbnails/issues/50)) ([42f4de6](https://github.com/Texarkanine/jekyll-auto-thumbnails/commit/42f4de6cd4740a05d7acfa4b57c01350e7054d95))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **deps:** bump nokogiri from 1.19.3 to 1.19.4 in the production-deps group ([#42](https://github.com/Texarkanine/jekyll-auto-thumbnails/issues/42)) ([0e41fd2](https://github.com/Texarkanine/jekyll-auto-thumbnails/commit/0e41fd2f3c6c475d77ca53e73be90ff4bf536ec7))
21
+ * **deps:** bump nokogiri in the production-deps group ([0e41fd2](https://github.com/Texarkanine/jekyll-auto-thumbnails/commit/0e41fd2f3c6c475d77ca53e73be90ff4bf536ec7))
22
+
3
23
  ## [2.0.1](https://github.com/Texarkanine/jekyll-auto-thumbnails/compare/v2.0.0...v2.0.1) (2026-05-14)
4
24
 
5
25
 
@@ -35,11 +35,13 @@ Gem::Specification.new do |spec|
35
35
  spec.add_dependency "nokogiri", "~> 1.15"
36
36
 
37
37
  # Development dependencies
38
+ spec.add_development_dependency "mutant", "~> 0.16"
39
+ spec.add_development_dependency "mutant-rspec", "~> 0.16"
38
40
  spec.add_development_dependency "rake", "~> 13.3"
39
41
  spec.add_development_dependency "rspec", "~> 3.13"
40
42
  spec.add_development_dependency "rubocop", "~> 1.81"
41
43
  spec.add_development_dependency "rubocop-rake", "~> 0.7"
42
44
  spec.add_development_dependency "rubocop-rspec", "~> 3.8"
43
- spec.add_development_dependency "simplecov", "~> 0.22"
44
- spec.add_development_dependency "simplecov-cobertura", "~> 3.1"
45
+ spec.add_development_dependency "simplecov", "~> 1.0"
46
+ spec.add_development_dependency "simplecov-cobertura", "~> 4.0"
45
47
  end
@@ -19,7 +19,11 @@ module JekyllAutoThumbnails
19
19
  @enabled = config_hash.fetch("enabled", true)
20
20
  @max_width = parse_dimension(config_hash["max_width"])
21
21
  @max_height = parse_dimension(config_hash["max_height"])
22
- @quality = parse_quality(config_hash.fetch("quality", 85))
22
+ @quality = if config_hash.key?("quality")
23
+ parse_quality(config_hash.fetch("quality"))
24
+ else
25
+ 85
26
+ end
23
27
  @parser = parse_parser(config_hash.fetch("parser", "html5"))
24
28
  @cache_dir = File.join(site.source, ".jekyll-cache", "jekyll-auto-thumbnails")
25
29
  end
@@ -39,7 +43,7 @@ module JekyllAutoThumbnails
39
43
  # @return [Integer, nil] positive integer or nil
40
44
  def parse_dimension(value)
41
45
  val = value.to_i
42
- val.positive? ? val : nil
46
+ val if val.positive?
43
47
  end
44
48
 
45
49
  # Parse quality value (0-100)
@@ -63,7 +67,7 @@ module JekyllAutoThumbnails
63
67
  # @return [Symbol] :html4 or :html5
64
68
  # @raise [ArgumentError] for invalid values or JRuby + :html5
65
69
  def parse_parser(value)
66
- unless value.is_a?(String)
70
+ unless value.instance_of?(String)
67
71
  raise ArgumentError,
68
72
  "auto_thumbnails: parser must be a string (\"html4\" or \"html5\"); got #{value.inspect}"
69
73
  end
@@ -13,7 +13,7 @@ module JekyllAutoThumbnails
13
13
  # @return [String] first 6 characters of MD5 hex digest
14
14
  # @raise [Errno::ENOENT] if file doesn't exist
15
15
  def self.short_digest(file_path)
16
- Digest::MD5.file(file_path).hexdigest[0...6]
16
+ Digest::MD5.file(file_path).hexdigest.byteslice(0, 6)
17
17
  end
18
18
  end
19
19
  end
@@ -72,9 +72,7 @@ module JekyllAutoThumbnails
72
72
  # @param ext [String] file extension
73
73
  # @return [String] thumbnail filename
74
74
  def build_thumbnail_filename(basename, digest, width, height, ext)
75
- width_str = width || ""
76
- height_str = height || ""
77
- "#{basename}_thumb-#{digest}-#{width_str}x#{height_str}#{ext}"
75
+ "#{basename}_thumb-#{digest}-#{width}x#{height}#{ext}"
78
76
  end
79
77
 
80
78
  private
@@ -111,9 +109,7 @@ module JekyllAutoThumbnails
111
109
  # @param height [Integer, nil] target height
112
110
  # @return [String] geometry string (e.g., "400x300>")
113
111
  def build_geometry(width, height)
114
- width_str = width || ""
115
- height_str = height || ""
116
- "#{width_str}x#{height_str}>"
112
+ "#{width}x#{height}>"
117
113
  end
118
114
 
119
115
  # Check if quality parameter needed for image format
@@ -5,6 +5,18 @@ require_relative "html_parser"
5
5
  module JekyllAutoThumbnails
6
6
  # Jekyll hook integration
7
7
  module Hooks
8
+ # Format a duration for Jekyll log suffixes.
9
+ #
10
+ # @param seconds [Numeric] elapsed seconds
11
+ # @return [String] `in Xs` under a minute, otherwise `in X m Y s`
12
+ def self.format_elapsed(seconds)
13
+ total = seconds.round
14
+ return "in #{total}s" if total < 60
15
+
16
+ minutes, remainder = total.divmod(60)
17
+ "in #{minutes} m #{remainder} s"
18
+ end
19
+
8
20
  # Initialize optimization system
9
21
  #
10
22
  # @param site [Jekyll::Site] Jekyll site
@@ -35,6 +47,8 @@ module JekyllAutoThumbnails
35
47
  return
36
48
  end
37
49
 
50
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
51
+
38
52
  # Scan all documents and pages
39
53
  (site.documents + site.pages).each do |doc|
40
54
  next unless doc.output
@@ -77,7 +91,8 @@ module JekyllAutoThumbnails
77
91
  doc.output = replace_urls(doc.output, url_map, parser: config.parser)
78
92
  end
79
93
 
80
- Jekyll.logger.info "AutoThumbnails:", "Generated #{url_map.size} thumbnails"
94
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
95
+ Jekyll.logger.info "AutoThumbnails:", "Generated #{url_map.size} thumbnails #{format_elapsed(elapsed)}"
81
96
  end
82
97
 
83
98
  # Copy thumbnails from cache to _site
@@ -92,19 +107,22 @@ module JekyllAutoThumbnails
92
107
 
93
108
  Jekyll.logger.info "AutoThumbnails:", "Copying #{url_map.size} thumbnails to _site"
94
109
 
110
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
111
+
95
112
  url_map.each_value do |thumb_url|
96
113
  thumb_filename = File.basename(thumb_url)
97
114
  cached_path = File.join(config.cache_dir, thumb_filename)
98
115
 
99
116
  # Build destination path in _site preserving directory structure
100
- dest_path = File.join(site.dest, thumb_url.sub(%r{^/}, ""))
117
+ dest_path = File.join(site.dest, thumb_url)
101
118
  dest_dir = File.dirname(dest_path)
102
119
 
103
120
  FileUtils.mkdir_p(dest_dir)
104
121
  FileUtils.cp(cached_path, dest_path)
105
122
  end
106
123
 
107
- Jekyll.logger.info "AutoThumbnails:", "All thumbnails copied"
124
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
125
+ Jekyll.logger.info "AutoThumbnails:", "All thumbnails copied #{format_elapsed(elapsed)}"
108
126
  end
109
127
 
110
128
  # Replace image URLs in HTML.
@@ -128,8 +146,6 @@ module JekyllAutoThumbnails
128
146
  modified = false
129
147
  doc.css("article img").each do |img|
130
148
  src = img["src"]
131
- next unless src
132
-
133
149
  thumb_url = url_map[src]
134
150
  next unless thumb_url
135
151
 
@@ -148,8 +164,6 @@ module JekyllAutoThumbnails
148
164
  ext = File.extname(doc.path || doc.url || "").downcase
149
165
  [".html", ".htm", ".md", ".markdown"].include?(ext)
150
166
  end
151
-
152
- private_class_method :replace_urls, :html_document?
153
167
  end
154
168
  end
155
169
 
@@ -14,8 +14,6 @@ module JekyllAutoThumbnails
14
14
  # users who depended on libxml2's serialization quirks (notably the
15
15
  # injected `<meta http-equiv="Content-Type">`).
16
16
  module HtmlParser
17
- module_function
18
-
19
17
  # Parse an HTML string with the selected parser.
20
18
  #
21
19
  # @param html [String] HTML source
@@ -25,7 +23,7 @@ module JekyllAutoThumbnails
25
23
  # @raise [NameError] if :html5 is requested under JRuby (should be
26
24
  # prevented by Configuration validation; this is a belt-and-suspenders
27
25
  # guard)
28
- def parse(html, parser)
26
+ def self.parse(html, parser)
29
27
  case parser
30
28
  when :html5
31
29
  Nokogiri::HTML5.parse(html)
@@ -25,10 +25,8 @@ module JekyllAutoThumbnails
25
25
  case detect_version
26
26
  when :v7
27
27
  %w[magick convert]
28
- when :v6
29
- ["convert"]
30
28
  else
31
- ["convert"] # Default fallback (will fail if not available)
29
+ ["convert"] # v6 and unavailable fallback (will fail if not available)
32
30
  end
33
31
  end
34
32
 
@@ -39,10 +37,8 @@ module JekyllAutoThumbnails
39
37
  case detect_version
40
38
  when :v7
41
39
  %w[magick identify]
42
- when :v6
43
- ["identify"]
44
40
  else
45
- ["identify"] # Default fallback (will fail if not available)
41
+ ["identify"] # v6 and unavailable fallback (will fail if not available)
46
42
  end
47
43
  end
48
44
 
@@ -68,9 +64,7 @@ module JekyllAutoThumbnails
68
64
  #
69
65
  # @return [Symbol] :v6, :v7, or :none
70
66
  def self.detect_version
71
- return @detected_version if defined?(@detected_version) && @detected_version
72
-
73
- @detected_version = if command_exists?("magick")
67
+ @detect_version ||= if command_exists?("magick")
74
68
  :v7
75
69
  elsif command_exists?("convert")
76
70
  :v6
@@ -79,6 +73,16 @@ module JekyllAutoThumbnails
79
73
  end
80
74
  end
81
75
 
76
+ # Clear the memoized result of {.detect_version}.
77
+ #
78
+ # Intended for tests that mutate PATH between examples so detection can
79
+ # be re-probed through the public API rather than via reflection.
80
+ #
81
+ # @return [void]
82
+ def self.reset_detection_cache!
83
+ @detect_version = nil
84
+ end
85
+
82
86
  # Check if a command exists in PATH
83
87
  #
84
88
  # @param cmd [String] command name
@@ -17,17 +17,11 @@ module JekyllAutoThumbnails
17
17
  # @param width [Integer, nil] required width
18
18
  # @param height [Integer, nil] required height
19
19
  def register(url, width, height)
20
- existing = @entries[url]
21
-
22
- @entries[url] = if existing
23
- # Update to max dimensions
24
- {
25
- width: [existing[:width], width].compact.max,
26
- height: [existing[:height], height].compact.max
27
- }
28
- else
29
- { width: width, height: height }
30
- end
20
+ previous = @entries[url]
21
+ @entries[url] = {
22
+ width: [previous&.dig(:width), width].compact.max,
23
+ height: [previous&.dig(:height), height].compact.max
24
+ }
31
25
  end
32
26
 
33
27
  # Check if image is registered
@@ -43,7 +37,7 @@ module JekyllAutoThumbnails
43
37
  # @param url [String] image URL
44
38
  # @return [Hash, nil] {width:, height:} or nil if not registered
45
39
  def requirements_for(url)
46
- @entries[url]&.dup
40
+ @entries[url].dup
47
41
  end
48
42
 
49
43
  # Get all registered entries
@@ -28,20 +28,12 @@ module JekyllAutoThumbnails
28
28
  height = parse_dimension(img["height"])
29
29
 
30
30
  if width || height
31
- # Explicitly sized - calculate missing dimension if needed
32
- if site_source && (width.nil? || height.nil?)
33
- Jekyll.logger.debug "AutoThumbnails:", "Calculating dimensions for #{src} (#{width}x#{height})"
34
- width, height = calculate_dimensions(src, width, height, site_source)
35
- Jekyll.logger.debug "AutoThumbnails:", "Calculated: #{width}x#{height}"
36
- end
31
+ # Only identify for aspect-ratio fill when a dimension is missing
32
+ width, height = calculate_dimensions(src, width, height, site_source) if site_source && (!width || !height)
37
33
 
38
34
  # Skip if dimensions match original (no thumbnail needed)
39
- if site_source && dimensions_match_original?(src, width, height, site_source)
40
- Jekyll.logger.debug "AutoThumbnails:", "Skipping #{src} - dimensions match original"
41
- next
42
- end
35
+ next if site_source && dimensions_match_original?(src, width, height, site_source)
43
36
 
44
- Jekyll.logger.debug "AutoThumbnails:", "Registering #{src} at #{width}x#{height}"
45
37
  registry.register(src, width, height)
46
38
  elsif site_source && (config.max_width || config.max_height)
47
39
  # Unsized but max config exists - check actual dimensions
@@ -61,7 +53,7 @@ module JekyllAutoThumbnails
61
53
  return unless file_path && File.exist?(file_path)
62
54
 
63
55
  actual_width, actual_height = image_dimensions(file_path)
64
- return unless actual_width && actual_height
56
+ return unless actual_height
65
57
 
66
58
  # Check if exceeds max dimensions
67
59
  exceeds_width = config.max_width && actual_width > config.max_width
@@ -82,10 +74,10 @@ module JekyllAutoThumbnails
82
74
  # Use [0] to get only first frame (important for animated GIFs)
83
75
  # Wrapper handles both ImageMagick v6 and v7
84
76
  output, status = ImageMagickWrapper.execute_identify("-format", "%wx%h", "#{file_path}[0]")
85
- return nil unless status.success? && !output.strip.empty?
77
+ return nil unless status.success?
86
78
 
87
- width, height = output.strip.split("x").map(&:to_i)
88
- [width, height]
79
+ match = output.match(/\A\s*(\d+)x(\d+)\s*\z/)
80
+ [Integer(match[1]), Integer(match[2])]
89
81
  rescue StandardError
90
82
  nil
91
83
  end
@@ -102,7 +94,7 @@ module JekyllAutoThumbnails
102
94
  return [width, height] unless file_path && File.exist?(file_path)
103
95
 
104
96
  actual_width, actual_height = image_dimensions(file_path)
105
- return [width, height] unless actual_width && actual_height
97
+ return [width, height] unless actual_height
106
98
 
107
99
  # Calculate missing dimension preserving aspect ratio
108
100
  if width && !height
@@ -123,13 +115,11 @@ module JekyllAutoThumbnails
123
115
  # @param value [String, nil] attribute value
124
116
  # @return [Integer, nil] parsed integer or nil
125
117
  def self.parse_dimension(value)
126
- return nil if value.nil? || value.empty?
127
-
128
118
  # Strip non-numeric characters (e.g., "300px" -> 300)
129
119
  numeric = value.to_s.gsub(/[^\d]/, "")
130
120
  return nil if numeric.empty?
131
121
 
132
- numeric.to_i
122
+ Integer(numeric)
133
123
  end
134
124
 
135
125
  # Check if requested dimensions match original dimensions
@@ -144,12 +134,8 @@ module JekyllAutoThumbnails
144
134
  return false unless file_path && File.exist?(file_path)
145
135
 
146
136
  actual_width, actual_height = image_dimensions(file_path)
147
- return false unless actual_width && actual_height
148
137
 
149
138
  width == actual_width && height == actual_height
150
139
  end
151
-
152
- private_class_method :check_and_register_oversized, :calculate_dimensions, :parse_dimension,
153
- :dimensions_match_original?
154
140
  end
155
141
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pathname"
4
+
3
5
  module JekyllAutoThumbnails
4
6
  # URL resolution and path handling
5
7
  #
@@ -23,13 +25,8 @@ module JekyllAutoThumbnails
23
25
  return nil if external?(url)
24
26
  return url if url.start_with?("/")
25
27
 
26
- # Relative path - resolve against base_dir
27
- # Remove ./ prefix if present
28
- cleaned_url = url.sub(%r{^\./}, "")
29
-
30
28
  # Join with base_dir and normalize
31
- require "pathname"
32
- Pathname.new(File.join(base_dir, cleaned_url)).cleanpath.to_s
29
+ Pathname.new(File.join(base_dir, url)).cleanpath.to_s
33
30
  end
34
31
 
35
32
  # Convert site-relative URL to filesystem path
@@ -40,9 +37,7 @@ module JekyllAutoThumbnails
40
37
  def self.to_filesystem_path(url, site_source)
41
38
  return nil if external?(url)
42
39
 
43
- # Strip leading slash and join with site source
44
- cleaned_url = url.sub(%r{^/}, "")
45
- File.join(site_source, cleaned_url)
40
+ File.join(site_source, url)
46
41
  end
47
42
  end
48
43
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JekyllAutoThumbnails
4
- VERSION = "2.0.1"
4
+ VERSION = "2.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-auto-thumbnails
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Texarkanine
@@ -43,6 +43,34 @@ dependencies:
43
43
  - - "~>"
44
44
  - !ruby/object:Gem::Version
45
45
  version: '1.15'
46
+ - !ruby/object:Gem::Dependency
47
+ name: mutant
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '0.16'
53
+ type: :development
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '0.16'
60
+ - !ruby/object:Gem::Dependency
61
+ name: mutant-rspec
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '0.16'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '0.16'
46
74
  - !ruby/object:Gem::Dependency
47
75
  name: rake
48
76
  requirement: !ruby/object:Gem::Requirement
@@ -119,28 +147,28 @@ dependencies:
119
147
  requirements:
120
148
  - - "~>"
121
149
  - !ruby/object:Gem::Version
122
- version: '0.22'
150
+ version: '1.0'
123
151
  type: :development
124
152
  prerelease: false
125
153
  version_requirements: !ruby/object:Gem::Requirement
126
154
  requirements:
127
155
  - - "~>"
128
156
  - !ruby/object:Gem::Version
129
- version: '0.22'
157
+ version: '1.0'
130
158
  - !ruby/object:Gem::Dependency
131
159
  name: simplecov-cobertura
132
160
  requirement: !ruby/object:Gem::Requirement
133
161
  requirements:
134
162
  - - "~>"
135
163
  - !ruby/object:Gem::Version
136
- version: '3.1'
164
+ version: '4.0'
137
165
  type: :development
138
166
  prerelease: false
139
167
  version_requirements: !ruby/object:Gem::Requirement
140
168
  requirements:
141
169
  - - "~>"
142
170
  - !ruby/object:Gem::Version
143
- version: '3.1'
171
+ version: '4.0'
144
172
  description: Jekyll plugin that automatically generates and serves optimized image
145
173
  thumbnails for faster page loads.
146
174
  email: