jekyll-minify 1.1.6 → 2.0.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: 54325d0362b0885bd72860ee0ead6d036e59d4ad47c3c00ce804ef2b7aeacc27
4
- data.tar.gz: 563c1fa1a094a967fd1c47ff975ccbcde581bd35eca131ca6af99fb93d6acf20
3
+ metadata.gz: cb32991092f0b2d76cbe462ef326b8e8e17bcd32412cc4d1d96b067243d86860
4
+ data.tar.gz: '0586c8ae509a614b9c0f8e155515fc4c9af03754d0d5a53ca1dd29a54bd84af5'
5
5
  SHA512:
6
- metadata.gz: 50b3e6363ae9ffb636d4364698989e9f764f5d4466d1945f0bdf56445dccf37d62788199349ed4fdb01c2ecd1146245be28991d2a890b69862fc169b35acbb47
7
- data.tar.gz: 0566f097397fdaec7134ac312f16fdd950e83d067a7a40163d416fd73ba341403bc6175e51f1213db8c193dd5196fba136886fc278dac5ce66e644cd4a15b43d
6
+ metadata.gz: 7642d1ab27efa2bf42653839e8683dd06f042b9f202f5ad5f4298087fee430bc70e7a1482773d826fd43151e3ae6a853cc4149e5a013676c33517ed60963e93a
7
+ data.tar.gz: 0ac5b5eb5f230a58eb60f03c126502ece8d4c5cbd841c021bd30e0d3b42258d281ea06c64469b079dfae29f5b74b200bdc437f2280258ee1e72100fdd1747087
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'did_you_mean'
4
+
5
+ module Jekyll
6
+ module Minify
7
+ module Configuration
8
+ # The type of each setting under CONFIG_KEY, and how a value of the
9
+ # wrong type is converted or replaced by its default, with a warning.
10
+ # Configuration.resolve applies it after merging the site's settings.
11
+ module Schema
12
+ # Every setting the plugin accepts, and its type: the plugin's own
13
+ # `enabled` switch, and the Cfg options the minify_html Ruby extension
14
+ # reads. Keep the options in sync with the minify_html version the
15
+ # gemspec allows; minify_html silently ignores keys it doesn't know.
16
+ #
17
+ # @return [Hash{String => Symbol}]
18
+ TYPES = %w[
19
+ enabled
20
+ allow_noncompliant_unquoted_attribute_values
21
+ allow_optimal_entities
22
+ allow_removing_spaces_between_attributes
23
+ keep_closing_tags
24
+ keep_comments
25
+ keep_html_and_head_opening_tags
26
+ keep_input_type_text_attr
27
+ keep_ssi_comments
28
+ minify_css
29
+ minify_doctype
30
+ minify_js
31
+ preserve_brace_template_syntax
32
+ preserve_chevron_percent_template_syntax
33
+ remove_bangs
34
+ remove_processing_instructions
35
+ ].to_h { |key| [key, :boolean] }.freeze
36
+
37
+ # Per type: what the warning says to expect, which values pass as is,
38
+ # and how a near-miss converts (returns [converted], or nil when the
39
+ # value can't be converted and the default applies instead).
40
+ #
41
+ # @return [Hash{Symbol => Hash}]
42
+ COERCERS = {
43
+ # "true"/"false" in any case, surrounding spaces ignored
44
+ boolean: {
45
+ expected: 'true or false',
46
+ accept: ->(value) { [true, false].include?(value) },
47
+ convert: lambda { |value|
48
+ word = value.strip.downcase if value.is_a?(String)
49
+ [word == 'true'] if %w[true false].include?(word)
50
+ }
51
+ }
52
+ }.freeze
53
+
54
+ # Site-level keys removed in 2.0.0, mapped to where their settings
55
+ # live now.
56
+ #
57
+ # @return [Hash{String => String}]
58
+ LEGACY_KEYS = {
59
+ 'with_minify' => "#{CONFIG_KEY}.enabled",
60
+ 'with_minify_data' => "the keys directly under #{CONFIG_KEY}"
61
+ }.freeze
62
+
63
+ # Returns value if its type accepts it; otherwise its conversion,
64
+ # with a warning; otherwise the default (nil when the key has none),
65
+ # with a warning.
66
+ #
67
+ # @param value [Object] the merged value of key
68
+ # @param key [String] a TYPES key
69
+ # @return [Object]
70
+ def self.coerce(value, key)
71
+ coercer = COERCERS.fetch(TYPES.fetch(key))
72
+ return value if coercer[:accept].call(value)
73
+
74
+ key_path = "#{CONFIG_KEY}.#{key}"
75
+ converted = coercer[:convert].call(value)
76
+ if converted
77
+ warn_invalid(key_path, value, coercer[:expected], "reading it as #{converted.first.inspect}")
78
+ return converted.first
79
+ end
80
+
81
+ warn_invalid(key_path, value, coercer[:expected])
82
+ DEFAULTS[key]
83
+ end
84
+
85
+ # Warns once per LEGACY_KEYS entry present in the site config, even
86
+ # with an empty value. The old keys stay ignored.
87
+ #
88
+ # @param site_config [Hash] Jekyll site configuration hash (site.config)
89
+ # @return [void]
90
+ def self.warn_legacy_keys(site_config)
91
+ (LEGACY_KEYS.keys & site_config.keys).each do |old_key|
92
+ Jekyll.logger.warn LOG_PREFIX,
93
+ "#{old_key} was removed in 2.0.0 and is ignored; " \
94
+ "use #{LEGACY_KEYS[old_key]} instead."
95
+ end
96
+ end
97
+
98
+ # Options under CONFIG_KEY that minify-html no longer reads, mapped to
99
+ # what replaces them. minify-html 0.16 dropped
100
+ # ensure_spec_compliant_unquoted_attribute_values and made compliant
101
+ # quoting its default.
102
+ #
103
+ # @return [Hash{String => String}]
104
+ LEGACY_OPTIONS = {
105
+ 'ensure_spec_compliant_unquoted_attribute_values' =>
106
+ "#{CONFIG_KEY}.allow_noncompliant_unquoted_attribute_values, which inverts it " \
107
+ 'and defaults to false'
108
+ }.freeze
109
+
110
+ # Returns a copy of settings without the keys TYPES doesn't know.
111
+ # Warns once per dropped key.
112
+ #
113
+ # @param settings [Hash] the site's jekyll_minify mapping
114
+ # @return [Hash]
115
+ def self.without_unknown_keys(settings)
116
+ settings.select do |key, _value|
117
+ next true if TYPES.key?(key)
118
+
119
+ LEGACY_OPTIONS.key?(key) ? warn_legacy_option(key) : warn_unknown(key)
120
+ false
121
+ end
122
+ end
123
+
124
+ # Warns that a LEGACY_OPTIONS key is ignored, naming its replacement.
125
+ #
126
+ # @param key [String] a LEGACY_OPTIONS key
127
+ # @return [void]
128
+ def self.warn_legacy_option(key)
129
+ Jekyll.logger.warn LOG_PREFIX,
130
+ "#{CONFIG_KEY}.#{key} was removed in 2.0.0 and is ignored; " \
131
+ "use #{LEGACY_OPTIONS[key]}."
132
+ end
133
+
134
+ # Warns that key is ignored, suggesting the closest known key.
135
+ #
136
+ # @param key [Object] the unknown key
137
+ # @return [void]
138
+ def self.warn_unknown(key)
139
+ suggestion = DidYouMean::SpellChecker.new(dictionary: TYPES.keys).correct(key.to_s).first
140
+ hint = suggestion ? "; did you mean `#{suggestion}`?" : '.'
141
+ Jekyll.logger.warn LOG_PREFIX, "#{CONFIG_KEY}.#{key} is not a known setting and is ignored#{hint}"
142
+ end
143
+
144
+ # One warning per invalid value per build: resolve runs once per build
145
+ # (see Configuration.for), so this never repeats file by file.
146
+ #
147
+ # @param key_path [String] the setting's dotted path
148
+ # @param value [Object] the invalid value
149
+ # @param expected [String] what the setting should be
150
+ # @param outcome [String] what the plugin does instead
151
+ # @return [void]
152
+ def self.warn_invalid(key_path, value, expected, outcome = 'using the default')
153
+ Jekyll.logger.warn LOG_PREFIX,
154
+ "#{key_path} should be #{expected}, got #{value.inspect}; #{outcome}."
155
+ end
156
+ private_class_method :warn_legacy_option, :warn_unknown
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Minify
5
+ # Resolves the plugin's settings: the `jekyll_minify` hash from
6
+ # _config.yml, merged over DEFAULTS so every default key has a value.
7
+ module Configuration
8
+ # The top-level _config.yml key the plugin reads.
9
+ # @return [String]
10
+ CONFIG_KEY = 'jekyll_minify'
11
+
12
+ # Prefix for every log line the plugin writes.
13
+ # @return [String]
14
+ LOG_PREFIX = 'jekyll-minify:'
15
+
16
+ # Logged once per build when `enabled` resolves to false.
17
+ # @return [String]
18
+ DISABLED_NOTICE = "Minification disabled (#{CONFIG_KEY}.enabled: false)."
19
+
20
+ # Default options passed to the minify-html gem. The site's
21
+ # `jekyll_minify` keys (all except `enabled`) are merged over these.
22
+ # `allow_noncompliant_unquoted_attribute_values` repeats minify-html's
23
+ # own default on purpose, so W3C-compliant attribute quoting stays the
24
+ # plugin's default even if minify-html changes its own.
25
+ #
26
+ # @return [Hash{Symbol => Boolean}]
27
+ DEFAULT_MINIFY_OPTIONS = {
28
+ keep_html_and_head_opening_tags: true,
29
+ keep_closing_tags: true,
30
+ minify_css: true,
31
+ minify_js: true,
32
+ remove_bangs: true,
33
+ remove_processing_instructions: true,
34
+ allow_noncompliant_unquoted_attribute_values: false,
35
+ keep_input_type_text_attr: true
36
+ }.freeze
37
+
38
+ # Every setting's default, keyed as in _config.yml: the plugin's own
39
+ # `enabled` switch plus DEFAULT_MINIFY_OPTIONS.
40
+ #
41
+ # @return [Hash{String => Boolean}]
42
+ DEFAULTS = { 'enabled' => true }.merge(DEFAULT_MINIFY_OPTIONS.transform_keys(&:to_s)).freeze
43
+
44
+ # Returns a frozen copy of DEFAULTS with the site's own jekyll_minify
45
+ # settings merged over it. Memoized per site_config object, so a build
46
+ # resolves its settings, and logs its warnings, once.
47
+ #
48
+ # @param site_config [Hash] Jekyll site configuration hash (site.config)
49
+ # @return [Hash{String => Boolean}]
50
+ def self.for(site_config)
51
+ cache[site_config] ||= resolve(site_config)
52
+ end
53
+
54
+ # The minify-html options in resolved settings: every key except the
55
+ # plugin's own `enabled` switch, with symbol keys as minify-html expects.
56
+ #
57
+ # @param settings [Hash{String => Boolean}] a result of {.for}
58
+ # @return [Hash{Symbol => Boolean}]
59
+ def self.minify_options(settings)
60
+ settings.except('enabled').transform_keys(&:to_sym)
61
+ end
62
+
63
+ # Drops every memoized result. Registered on :site, :after_reset below,
64
+ # so each build (including each `jekyll serve --watch` rebuild) starts
65
+ # from the site config as it is then.
66
+ #
67
+ # @return [void]
68
+ def self.reset!
69
+ @cache = nil
70
+ end
71
+
72
+ # Keyed on object identity, and holds its results strongly: a weak map
73
+ # would let GC drop them mid-build and repeat every warning. reset!
74
+ # clears it at the start of each build, so it never outgrows one build.
75
+ #
76
+ # @return [Hash]
77
+ def self.cache
78
+ @cache ||= {}.compare_by_identity
79
+ end
80
+
81
+ # Warns about legacy keys, drops unknown keys, merges the rest over
82
+ # DEFAULTS, then checks each value's type.
83
+ #
84
+ # @param site_config [Hash] Jekyll site configuration hash (site.config)
85
+ # @return [Hash{String => Boolean}]
86
+ def self.resolve(site_config)
87
+ Schema.warn_legacy_keys(site_config)
88
+ user = Schema.without_unknown_keys(user_settings(site_config[CONFIG_KEY]))
89
+ # A null value means unset, like Jekyll core config.
90
+ merged = DEFAULTS.merge(user.compact)
91
+ merged.each_key { |key| merged[key] = Schema.coerce(merged[key], key) }
92
+ # An option without a default whose value was invalid: leave it to
93
+ # minify-html's own default.
94
+ merged.compact!
95
+ Jekyll.logger.info LOG_PREFIX, DISABLED_NOTICE unless merged['enabled']
96
+ merged.freeze
97
+ end
98
+
99
+ # Accepts the top-level key as a mapping, or as a bare true/false
100
+ # shorthand for `enabled`. Null means unset; anything else is warned
101
+ # about and ignored.
102
+ #
103
+ # @param value [Object] the site's jekyll_minify value
104
+ # @return [Hash]
105
+ def self.user_settings(value)
106
+ case value
107
+ when Hash then value
108
+ when true, false then { 'enabled' => value }
109
+ when nil then {}
110
+ else
111
+ Schema.warn_invalid(CONFIG_KEY, value, 'a mapping of settings, or true/false')
112
+ {}
113
+ end
114
+ end
115
+ private_class_method :cache, :resolve, :user_settings
116
+ end
117
+ end
118
+ end
119
+
120
+ # Schema's constants read CONFIG_KEY, defined above.
121
+ require_relative 'configuration/schema'
122
+
123
+ Jekyll::Hooks.register(:site, :after_reset) { Jekyll::Minify::Configuration.reset! }
@@ -48,12 +48,14 @@ module Jekyll
48
48
  span.set_attribute('minify.total_file_count', types.sum { |t| t[:count] })
49
49
  span.set_attribute('minify.total_original_size_bytes', types.sum { |t| t[:original_bytes] })
50
50
  span.set_attribute('minify.total_minified_size_bytes', types.sum { |t| t[:minified_bytes] })
51
+ span.set_attribute('minify.skipped_file_count', result[:skipped])
51
52
  span.set_attribute('minify.error_count', result[:errors].length)
52
53
  }],
53
54
 
54
- ['Jekyll::Minify::Minifier', :private, :minify_each_file, 'minify.file_discovery',
55
- lambda { |span, _this, args, _result|
55
+ ['Jekyll::Minify::Minifier', :private, :discover_files, 'minify.file_discovery',
56
+ lambda { |span, _this, args, result|
56
57
  span.set_attribute('minify.dest_path', args[0].to_s)
58
+ span.set_attribute('minify.discovered_file_count', result.size)
57
59
  }],
58
60
 
59
61
  ['Jekyll::Minify::Minifier', :private, :minify_with_fallback, 'minify.file_process',
@@ -145,9 +147,13 @@ module Jekyll
145
147
 
146
148
  # Configures the OTel SDK from standard env vars if the SDK gem is available.
147
149
  # Silently no-ops when opentelemetry-sdk or opentelemetry-exporter-otlp is
148
- # not installed. Uses SimpleSpanProcessor (not the SDK default
149
- # BatchSpanProcessor) since Jekyll is a short-lived CLI process that could
150
- # exit before a batch timer fires, dropping spans.
150
+ # not installed. Uses BatchSpanProcessor, which exports from a background
151
+ # thread, so the build never waits on the collector while a span ends
152
+ # (SimpleSpanProcessor exports synchronously, inside every traced call).
153
+ # Spans still reach the collector before Jekyll exits: the post_write hook
154
+ # below runs after every :low-or-higher post_write hook, this plugin's
155
+ # minification included, and force-flushes the batch; at_exit shuts the
156
+ # provider down, exporting anything left (e.g. between serve rebuilds).
151
157
  #
152
158
  # jekyll-theme-centos loads several instrumented plugins in one process;
153
159
  # OpenTelemetry::SDK.configure overwrites the global tracer_provider on
@@ -164,7 +170,7 @@ module Jekyll
164
170
  require 'opentelemetry/sdk'
165
171
  require 'opentelemetry/exporter/otlp'
166
172
  exporter = OpenTelemetry::Exporter::OTLP::Exporter.new
167
- processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter)
173
+ processor = OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
168
174
  OpenTelemetry::SDK.configure { |c| c.add_span_processor(processor) }
169
175
 
170
176
  at_exit { OpenTelemetry.tracer_provider&.shutdown }
@@ -6,6 +6,6 @@ module Jekyll
6
6
  # Regenerated alongside the gemspec on every `make jekyll-minify.gemspec`
7
7
  # run (locally or in CI), so it always matches spec.version in the built
8
8
  # gem. Any hand edit is overwritten on the next run.
9
- VERSION = '1.1.6'
9
+ VERSION = '2.0.0'
10
10
  end
11
11
  end
data/lib/jekyll-minify.rb CHANGED
@@ -43,20 +43,22 @@
43
43
  #
44
44
  # The plugin is enabled by default. You can control it via _config.yml:
45
45
  #
46
- # with_minify: false # disable minification (defaults to true)
47
- # with_minify_data: # override minify-html options
48
- # remove_bangs: false # preserve IE conditional comments
49
- # minify_css: false # disable CSS minification
50
- # minify_js: false # disable JS minification
46
+ # jekyll_minify:
47
+ # enabled: false # disable minification (defaults to true)
48
+ # remove_bangs: false # override minify-html options: every key
49
+ # minify_css: false # other than `enabled` is passed through
50
+ # minify_js: false # to minify-html
51
51
  #
52
- # Defaults to all 8 minify-html options enabled. YAML string keys are automatically
53
- # converted to symbols for the minify-html gem.
52
+ # Jekyll::Minify::Configuration resolves these settings once per build: it
53
+ # merges them over DEFAULT_MINIFY_OPTIONS, and warns about (then ignores)
54
+ # unknown keys, non-boolean values, and the pre-2.0 `with_minify` keys.
54
55
  #
55
56
  # === W3C HTML Validation Compatibility ===
56
57
  #
57
- # This plugin uses minify-html's built-in `ensure_spec_compliant_unquoted_attribute_values`
58
- # option to maintain W3C validation compliance. This preserves attribute quotes on values
59
- # containing special characters, preventing unquoting issues with attributes like:
58
+ # The plugin sets minify-html's `allow_noncompliant_unquoted_attribute_values`
59
+ # to false (minify-html's own default since 0.16) to maintain W3C validation
60
+ # compliance. This preserves attribute quotes on values containing special
61
+ # characters, preventing unquoting issues with attributes like:
60
62
  # prefix="og: https://ogp.me/ns#"
61
63
  #
62
64
  # It also sets `keep_input_type_text_attr` so `<input type="text">` survives
@@ -83,6 +85,7 @@ rescue LoadError
83
85
  end
84
86
 
85
87
  require_relative 'jekyll-minify/version'
88
+ require_relative 'jekyll-minify/configuration'
86
89
  require_relative 'jekyll-minify/instrumentation'
87
90
 
88
91
  module Jekyll
@@ -95,29 +98,21 @@ module Jekyll
95
98
  #
96
99
  # @example Disable minification in development
97
100
  # # _config-dev.yml
98
- # with_minify: false
101
+ # jekyll_minify:
102
+ # enabled: false
99
103
  #
100
104
  # @example Override minify-html options for production
101
105
  # # _config.yml
102
- # with_minify: true
103
- # with_minify_data:
106
+ # jekyll_minify:
107
+ # enabled: true
104
108
  # remove_bangs: false
105
109
  # minify_css: false
106
110
  class Minifier
107
- # Default options passed to the minify-html gem.
108
- # User overrides from `with_minify_data` in _config.yml are merged over these.
111
+ # Default options passed to the minify-html gem; an alias of
112
+ # {Configuration::DEFAULT_MINIFY_OPTIONS}, kept for backward compatibility.
109
113
  #
110
114
  # @return [Hash{Symbol => Boolean}]
111
- DEFAULT_MINIFY_OPTIONS = {
112
- keep_html_and_head_opening_tags: true,
113
- keep_closing_tags: true,
114
- minify_css: true,
115
- minify_js: true,
116
- remove_bangs: true,
117
- remove_processing_instructions: true,
118
- ensure_spec_compliant_unquoted_attribute_values: true,
119
- keep_input_type_text_attr: true
120
- }.freeze
115
+ DEFAULT_MINIFY_OPTIONS = Configuration::DEFAULT_MINIFY_OPTIONS
121
116
 
122
117
  # Asset types processed by the minifier, in order.
123
118
  # Each entry has :type (Symbol) and :display_name (String) for logging.
@@ -135,6 +130,7 @@ module Jekyll
135
130
  html: { count: 0, original_bytes: 0, minified_bytes: 0 },
136
131
  css: { count: 0, original_bytes: 0, minified_bytes: 0 },
137
132
  js: { count: 0, original_bytes: 0, minified_bytes: 0 },
133
+ skipped: 0,
138
134
  errors: []
139
135
  }
140
136
  @start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -149,21 +145,20 @@ module Jekyll
149
145
  end
150
146
 
151
147
  # Runs the minification pipeline for the given site.
152
- # Reads configuration from site.config:
153
- # - with_minify (Boolean, defaults to true): enable/disable the plugin
154
- # - with_minify_data (Hash, optional): override minify-html options
148
+ # Reads the jekyll_minify settings from site.config through
149
+ # {Configuration.for}: `enabled` switches the plugin on or off, and every
150
+ # other key overrides a minify-html option.
155
151
  #
156
- # Returns immediately if with_minify is false or the destination directory does not exist.
152
+ # Returns immediately if jekyll_minify.enabled is false (Configuration
153
+ # logs that once per build) or the destination directory does not exist.
157
154
  #
158
155
  # @param site [Jekyll::Site] the Jekyll site object providing dest and config
159
156
  # @return [Hash, nil] the run's @stats, or nil if minification was skipped
160
157
  def run(site)
161
- unless plugin_enabled?(site.config)
162
- Jekyll.logger.info 'jekyll-minify:', 'Minification disabled (with_minify: false).'
163
- return
164
- end
158
+ settings = Configuration.for(site.config)
159
+ return unless settings['enabled']
165
160
 
166
- @minify_options = build_minify_options(site.config)
161
+ @minify_options = Configuration.minify_options(settings)
167
162
 
168
163
  dest = Pathname.new(site.dest)
169
164
  return unless dest.directory?
@@ -177,46 +172,38 @@ module Jekyll
177
172
 
178
173
  private
179
174
 
180
- # Returns false if the site config explicitly disables the plugin.
181
- # Defaults to true when the key is absent (matches with_link_decorator convention).
182
- #
183
- # @param site_config [Hash] Jekyll site configuration hash (site.config)
184
- # @return [Boolean]
185
- def plugin_enabled?(site_config)
186
- site_config.key?('with_minify') ? site_config['with_minify'] : true
187
- end
188
-
189
- # Builds effective minify-html options by merging with_minify_data over defaults.
190
- # YAML string keys from _config.yml are converted to symbols for minify-html.
191
- # Unknown keys are passed through unchanged.
192
- #
193
- # @param site_config [Hash] Jekyll site configuration hash (site.config)
194
- # @return [Hash{Symbol => Boolean}]
195
- def build_minify_options(site_config)
196
- overrides = (site_config['with_minify_data'] || {}).transform_keys(&:to_sym)
197
- DEFAULT_MINIFY_OPTIONS.merge(overrides)
198
- end
199
-
200
- # Walks the destination directory and dispatches each eligible file.
175
+ # Dispatches each file discover_files finds in the destination directory.
201
176
  #
202
177
  # @param dest [Pathname] the Jekyll site destination directory
203
178
  # @return [void]
204
179
  def minify_each_file(dest)
205
- dest.glob('**/*.{html,css,js}').each do |file_path|
180
+ discover_files(dest).each do |file_path|
206
181
  minify_file(file_path)
207
182
  end
208
183
  rescue StandardError => e
209
184
  Jekyll.logger.error 'jekyll-minify:', "Error during file discovery: #{e.message}"
210
185
  end
211
186
 
187
+ # Lists the HTML, CSS, and JS files under dest, including the ones
188
+ # minify_file then skips. Its own method so the minify.file_discovery
189
+ # span times the search alone, not the processing that follows.
190
+ #
191
+ # @param dest [Pathname] the Jekyll site destination directory
192
+ # @return [Array<Pathname>]
193
+ def discover_files(dest)
194
+ dest.glob('**/*.{html,css,js}')
195
+ end
196
+
212
197
  # Dispatches a single file to the appropriate minifier based on extension.
213
- # Skips files already minified (*.min.css, *.min.js).
198
+ # Skips files already minified (*.min.css, *.min.js), and .js sources that
199
+ # already have a minified copy next to them: minifying one would rename the
200
+ # result onto that copy, replacing a vendor build (such as Bootstrap's).
214
201
  #
215
202
  # @param file_path [Pathname] path to the file to process
216
203
  # @return [void]
217
204
  def minify_file(file_path)
218
205
  # Skip already-minified files
219
- return if file_path.to_s.end_with?('.min.css', '.min.js')
206
+ return skip_file if file_path.to_s.end_with?('.min.css', '.min.js')
220
207
 
221
208
  case file_path.extname.downcase
222
209
  when '.html'
@@ -224,12 +211,28 @@ module Jekyll
224
211
  when '.css'
225
212
  minify_with_fallback(file_path, :css)
226
213
  when '.js'
227
- minify_with_fallback(file_path, :js, rename_to_min: true)
214
+ minify_js_file(file_path)
228
215
  end
229
216
  rescue StandardError => e
230
217
  Jekyll.logger.warn 'jekyll-minify:', "Unexpected error processing #{file_path}: #{e.message}"
231
218
  end
232
219
 
220
+ # Minifies a .js file and renames it to .min.js, unless a .min.js copy
221
+ # already exists next to it: minifying it would rename the result onto
222
+ # that copy, replacing a vendor build (such as Bootstrap's).
223
+ #
224
+ # @param file_path [Pathname] path to the .js file
225
+ # @return [void]
226
+ def minify_js_file(file_path)
227
+ if minified_sibling?(file_path)
228
+ Jekyll.logger.debug 'jekyll-minify:',
229
+ "Skipping #{file_path}: #{file_path.sub_ext('.min.js').basename} already exists"
230
+ return skip_file
231
+ end
232
+
233
+ minify_with_fallback(file_path, :js, rename_to_min: true)
234
+ end
235
+
233
236
  # Minifies a file, restoring the original if anything goes wrong.
234
237
  # Skips non-UTF-8 files with a targeted warning.
235
238
  # Skips empty files.
@@ -243,13 +246,13 @@ module Jekyll
243
246
  original_content = file_path.read(encoding: 'UTF-8')
244
247
  rescue Encoding::InvalidByteSequenceError
245
248
  Jekyll.logger.warn 'jekyll-minify:', "Skipping #{file_path}: not valid UTF-8"
246
- return
249
+ return skip_file
247
250
  end
248
251
 
249
252
  original_bytes = original_content.bytesize
250
253
 
251
254
  # Skip empty files
252
- return if original_bytes.zero?
255
+ return skip_file if original_bytes.zero?
253
256
 
254
257
  begin
255
258
  minified = minify_by_type(original_content, type)
@@ -259,10 +262,20 @@ module Jekyll
259
262
  rename_to_min: rename_to_min)
260
263
  record_minification_result(type, original_bytes, minified_bytes)
261
264
  rescue StandardError => e
262
- restore_on_error(file_path, original_content, e)
265
+ restore_on_error(file_path, original_content, e, rename_to_min: rename_to_min)
263
266
  end
264
267
  end
265
268
 
269
+ # Counts a discovered file the plugin deliberately leaves unminified:
270
+ # already minified, a .js source with a .min.js copy, empty, or not
271
+ # UTF-8. Failed minifications are counted as errors instead.
272
+ #
273
+ # @return [void]
274
+ def skip_file
275
+ @stats[:skipped] += 1
276
+ nil
277
+ end
278
+
266
279
  # Validates that minification produced non-empty content.
267
280
  #
268
281
  # @param minified [String, nil] the minification output to validate
@@ -285,16 +298,55 @@ module Jekyll
285
298
  end
286
299
 
287
300
  # Logs a minification failure and restores the original file content.
301
+ # With rename_to_min, the original .js is also renamed to .min.js, so
302
+ # pages that link the .min.js name (as jekyll-theme-centos does) still
303
+ # load the script, unminified.
288
304
  #
289
305
  # @param file_path [Pathname] path to the file being restored
290
306
  # @param original_content [String] the unmodified file content to write back
291
307
  # @param error [StandardError] the error that triggered the restore
308
+ # @param rename_to_min [Boolean] if true, renames .js to .min.js
292
309
  # @return [void]
293
- def restore_on_error(file_path, original_content, error)
310
+ def restore_on_error(file_path, original_content, error, rename_to_min: false)
311
+ file_path.write(original_content, encoding: 'UTF-8')
312
+ kept_as = rename_to_min ? keep_original_as_min_js(file_path) : file_path
294
313
  error_msg = "Failed to minify #{file_path}: #{error.message}"
314
+ error_msg += "; kept it unminified as #{kept_as.basename}" unless kept_as == file_path
295
315
  Jekyll.logger.warn 'jekyll-minify:', error_msg
296
316
  @stats[:errors] << error_msg
297
- file_path.write(original_content, encoding: 'UTF-8')
317
+ end
318
+
319
+ # Whether a .min.js file with the same name as file_path already exists.
320
+ #
321
+ # @param file_path [Pathname] path to a .js file
322
+ # @return [Boolean]
323
+ def minified_sibling?(file_path)
324
+ file_path.sub_ext('.min.js').exist?
325
+ end
326
+
327
+ # Renames a .js file to .min.js; leaves any other file as is.
328
+ #
329
+ # @param file_path [Pathname] path to the file to rename
330
+ # @return [Pathname] the file's path after the rename
331
+ # @raise [SystemCallError] if the rename fails
332
+ def rename_to_min_js(file_path)
333
+ return file_path unless file_path.extname == '.js'
334
+
335
+ new_path = file_path.sub_ext('.min.js')
336
+ file_path.rename(new_path)
337
+ new_path
338
+ end
339
+
340
+ # Renames a restored .js file to .min.js. A failed rename is logged and
341
+ # leaves the file as .js, so the restore itself never fails on it.
342
+ #
343
+ # @param file_path [Pathname] path to the restored file
344
+ # @return [Pathname] the file's path after the attempt
345
+ def keep_original_as_min_js(file_path)
346
+ rename_to_min_js(file_path)
347
+ rescue StandardError => e
348
+ Jekyll.logger.warn 'jekyll-minify:', "Could not rename #{file_path} to .min.js: #{e.message}"
349
+ file_path
298
350
  end
299
351
 
300
352
  # Writes minified content to disk, optionally renaming .js to .min.js.
@@ -307,11 +359,7 @@ module Jekyll
307
359
  # @return [void]
308
360
  def write_minified_file(file_path, minified_content, original_content, rename_to_min: false)
309
361
  file_path.write(minified_content, encoding: 'UTF-8')
310
-
311
- if rename_to_min && file_path.extname == '.js'
312
- new_path = file_path.sub_ext('.min.js')
313
- file_path.rename(new_path)
314
- end
362
+ rename_to_min_js(file_path) if rename_to_min
315
363
  rescue StandardError => e
316
364
  # Restore original if write or rename fails
317
365
  file_path.write(original_content, encoding: 'UTF-8')
@@ -339,7 +387,7 @@ module Jekyll
339
387
 
340
388
  # Minifies HTML content using the minify-html gem.
341
389
  # Uses @minify_options which defaults to DEFAULT_MINIFY_OPTIONS but may be
342
- # overridden via with_minify_data in _config.yml.
390
+ # overridden via jekyll_minify in _config.yml.
343
391
  #
344
392
  # @param content [String] raw HTML string
345
393
  # @return [String] minified HTML
@@ -351,24 +399,56 @@ module Jekyll
351
399
  end
352
400
 
353
401
  # Minifies CSS or JS content by wrapping it in a temporary HTML tag.
354
- # Extracts the minified content between the first pair of wrapper tags.
402
+ # Extracts the minified content from the single wrapper element.
403
+ #
404
+ # The HTML parser ends the wrapper at the first closing-tag sequence, so
405
+ # content containing one would come back truncated. Such content is
406
+ # refused before minification (see #ensure_wrappable), and output whose
407
+ # extracted body still contains one (minify-html can unescape `<\/style`
408
+ # in CSS strings) is rejected, so the caller keeps the original file.
355
409
  #
356
410
  # @param content [String] raw CSS or JS string
357
411
  # @param wrapper_tag [String] HTML tag name ("style" or "script")
358
412
  # @param minify_option [Symbol] minify-html option key (:minify_css or :minify_js)
359
413
  # @return [String] minified content without wrapper tags
360
- # @raise [RuntimeError] if the minification result is empty or the tag is not found
414
+ # @raise [RuntimeError] if the content cannot be wrapped safely, or the
415
+ # output is not exactly one wrapper element
361
416
  def minify_wrapped_content(content, wrapper_tag, minify_option)
417
+ ensure_wrappable(content, wrapper_tag)
362
418
  wrapped = "<#{wrapper_tag}>#{content}</#{wrapper_tag}>"
363
419
  minified = Kernel.minify_html(wrapped, { minify_option => true })
364
- match = minified.match(%r{<#{wrapper_tag}>(.*?)</#{wrapper_tag}>}m)
365
- raise "#{wrapper_tag.upcase} minification resulted in empty content" unless match
420
+ match = minified.match(%r{\A<#{wrapper_tag}>(.*)</#{wrapper_tag}>\z}m)
421
+ raise "minified output is not a single <#{wrapper_tag}> element" unless match
422
+ raise "minified output contains a closing </#{wrapper_tag}> sequence" if closing_tag?(match[1], wrapper_tag)
366
423
 
367
424
  match[1]
368
425
  rescue StandardError => e
369
426
  raise "#{wrapper_tag.upcase} minification failed: #{e.message}"
370
427
  end
371
428
 
429
+ # Raises if content contains a closing-tag sequence for wrapper_tag (any
430
+ # letter case), which would make minify-html end the wrapper early.
431
+ #
432
+ # @param content [String] raw CSS or JS string
433
+ # @param wrapper_tag [String] HTML tag name ("style" or "script")
434
+ # @raise [RuntimeError] if the content cannot be wrapped safely
435
+ # @return [void]
436
+ def ensure_wrappable(content, wrapper_tag)
437
+ return unless closing_tag?(content, wrapper_tag)
438
+
439
+ raise "content contains a closing </#{wrapper_tag}> sequence; left unminified to avoid truncation"
440
+ end
441
+
442
+ # Whether text contains the start of a closing tag for tag, in any
443
+ # letter case, as the HTML parser would recognize it.
444
+ #
445
+ # @param text [String] the text to search
446
+ # @param tag [String] HTML tag name
447
+ # @return [Boolean]
448
+ def closing_tag?(text, tag)
449
+ text.match?(%r{</#{tag}}i)
450
+ end
451
+
372
452
  # Minifies a CSS string by wrapping it in a <style> tag.
373
453
  #
374
454
  # @param content [String] raw CSS string
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-minify
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.6
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alain Reguera Delgado
@@ -35,14 +35,14 @@ dependencies:
35
35
  requirements:
36
36
  - - "~>"
37
37
  - !ruby/object:Gem::Version
38
- version: '0.15'
38
+ version: 0.18.1
39
39
  type: :runtime
40
40
  prerelease: false
41
41
  version_requirements: !ruby/object:Gem::Requirement
42
42
  requirements:
43
43
  - - "~>"
44
44
  - !ruby/object:Gem::Version
45
- version: '0.15'
45
+ version: 0.18.1
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: opentelemetry-api
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -207,6 +207,8 @@ files:
207
207
  - LICENSE
208
208
  - README.md
209
209
  - lib/jekyll-minify.rb
210
+ - lib/jekyll-minify/configuration.rb
211
+ - lib/jekyll-minify/configuration/schema.rb
210
212
  - lib/jekyll-minify/instrumentation.rb
211
213
  - lib/jekyll-minify/version.rb
212
214
  homepage: https://gitlab.com/CentOS/artwork/centos-web/jekyll-minify