jekyll-minify 1.1.5 → 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: '019402f7b9f78ee3b5fd9ecf3524fc77439ed63a261417ac4e2ed9a5c393fc4d'
4
- data.tar.gz: 95d346d134f1a74f2da9258d4f8fbbd4adc42d6c380a9722dbb7cca35bb7d37d
3
+ metadata.gz: cb32991092f0b2d76cbe462ef326b8e8e17bcd32412cc4d1d96b067243d86860
4
+ data.tar.gz: '0586c8ae509a614b9c0f8e155515fc4c9af03754d0d5a53ca1dd29a54bd84af5'
5
5
  SHA512:
6
- metadata.gz: '09e8585a3ac2bd941322d69c55cbd5460f6034a46cc25b715a3b4ff63031a16b2d4167d9bf5d104ab15da28b20e3974f696dbbf17ffb7fd983880f6e90c43b39'
7
- data.tar.gz: 31726f53f05f554518d3fb8cc2978b2ae7e6dede5f1ed82a2d8d567006e30e1543fe9b708abab5e2e5bf401b57595f69193ebebb3aa48887276ecd5c5f1060a1
6
+ metadata.gz: 7642d1ab27efa2bf42653839e8683dd06f042b9f202f5ad5f4298087fee430bc70e7a1482773d826fd43151e3ae6a853cc4149e5a013676c33517ed60963e93a
7
+ data.tar.gz: 0ac5b5eb5f230a58eb60f03c126502ece8d4c5cbd841c021bd30e0d3b42258d281ea06c64469b079dfae29f5b74b200bdc437f2280258ee1e72100fdd1747087
data/README.md CHANGED
@@ -9,4 +9,4 @@ For complete documentation, configuration options, and examples, visit the [docu
9
9
 
10
10
  ## License
11
11
 
12
- MIT License - see LICENSE file for details.
12
+ MIT License - see [LICENSE](LICENSE) file for details.
@@ -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.5'
9
+ VERSION = '2.0.0'
10
10
  end
11
11
  end
data/lib/jekyll-minify.rb CHANGED
@@ -43,22 +43,30 @@
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 7 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
  #
64
+ # It also sets `keep_input_type_text_attr` so `<input type="text">` survives
65
+ # minification. Dropping it is valid HTML (browsers default to type=text), but
66
+ # html-validate's `no-implicit-input-type` rule flags the omission as an error,
67
+ # so stripping it here would reintroduce a validation failure the source
68
+ # template deliberately avoids by writing the attribute explicitly.
69
+ #
62
70
  # === CSS/JavaScript Minification Note ===
63
71
  #
64
72
  # The minify-html Ruby gem only exposes HTML minification through its public API.
@@ -77,6 +85,7 @@ rescue LoadError
77
85
  end
78
86
 
79
87
  require_relative 'jekyll-minify/version'
88
+ require_relative 'jekyll-minify/configuration'
80
89
  require_relative 'jekyll-minify/instrumentation'
81
90
 
82
91
  module Jekyll
@@ -89,28 +98,21 @@ module Jekyll
89
98
  #
90
99
  # @example Disable minification in development
91
100
  # # _config-dev.yml
92
- # with_minify: false
101
+ # jekyll_minify:
102
+ # enabled: false
93
103
  #
94
104
  # @example Override minify-html options for production
95
105
  # # _config.yml
96
- # with_minify: true
97
- # with_minify_data:
106
+ # jekyll_minify:
107
+ # enabled: true
98
108
  # remove_bangs: false
99
109
  # minify_css: false
100
110
  class Minifier
101
- # Default options passed to the minify-html gem.
102
- # 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.
103
113
  #
104
114
  # @return [Hash{Symbol => Boolean}]
105
- DEFAULT_MINIFY_OPTIONS = {
106
- keep_html_and_head_opening_tags: true,
107
- keep_closing_tags: true,
108
- minify_css: true,
109
- minify_js: true,
110
- remove_bangs: true,
111
- remove_processing_instructions: true,
112
- ensure_spec_compliant_unquoted_attribute_values: true
113
- }.freeze
115
+ DEFAULT_MINIFY_OPTIONS = Configuration::DEFAULT_MINIFY_OPTIONS
114
116
 
115
117
  # Asset types processed by the minifier, in order.
116
118
  # Each entry has :type (Symbol) and :display_name (String) for logging.
@@ -128,6 +130,7 @@ module Jekyll
128
130
  html: { count: 0, original_bytes: 0, minified_bytes: 0 },
129
131
  css: { count: 0, original_bytes: 0, minified_bytes: 0 },
130
132
  js: { count: 0, original_bytes: 0, minified_bytes: 0 },
133
+ skipped: 0,
131
134
  errors: []
132
135
  }
133
136
  @start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -142,21 +145,20 @@ module Jekyll
142
145
  end
143
146
 
144
147
  # Runs the minification pipeline for the given site.
145
- # Reads configuration from site.config:
146
- # - with_minify (Boolean, defaults to true): enable/disable the plugin
147
- # - 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.
148
151
  #
149
- # 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.
150
154
  #
151
155
  # @param site [Jekyll::Site] the Jekyll site object providing dest and config
152
156
  # @return [Hash, nil] the run's @stats, or nil if minification was skipped
153
157
  def run(site)
154
- unless plugin_enabled?(site.config)
155
- Jekyll.logger.info 'jekyll-minify:', 'Minification disabled (with_minify: false).'
156
- return
157
- end
158
+ settings = Configuration.for(site.config)
159
+ return unless settings['enabled']
158
160
 
159
- @minify_options = build_minify_options(site.config)
161
+ @minify_options = Configuration.minify_options(settings)
160
162
 
161
163
  dest = Pathname.new(site.dest)
162
164
  return unless dest.directory?
@@ -170,46 +172,38 @@ module Jekyll
170
172
 
171
173
  private
172
174
 
173
- # Returns false if the site config explicitly disables the plugin.
174
- # Defaults to true when the key is absent (matches with_link_decorator convention).
175
- #
176
- # @param site_config [Hash] Jekyll site configuration hash (site.config)
177
- # @return [Boolean]
178
- def plugin_enabled?(site_config)
179
- site_config.key?('with_minify') ? site_config['with_minify'] : true
180
- end
181
-
182
- # Builds effective minify-html options by merging with_minify_data over defaults.
183
- # YAML string keys from _config.yml are converted to symbols for minify-html.
184
- # Unknown keys are passed through unchanged.
185
- #
186
- # @param site_config [Hash] Jekyll site configuration hash (site.config)
187
- # @return [Hash{Symbol => Boolean}]
188
- def build_minify_options(site_config)
189
- overrides = (site_config['with_minify_data'] || {}).transform_keys(&:to_sym)
190
- DEFAULT_MINIFY_OPTIONS.merge(overrides)
191
- end
192
-
193
- # Walks the destination directory and dispatches each eligible file.
175
+ # Dispatches each file discover_files finds in the destination directory.
194
176
  #
195
177
  # @param dest [Pathname] the Jekyll site destination directory
196
178
  # @return [void]
197
179
  def minify_each_file(dest)
198
- dest.glob('**/*.{html,css,js}').each do |file_path|
180
+ discover_files(dest).each do |file_path|
199
181
  minify_file(file_path)
200
182
  end
201
183
  rescue StandardError => e
202
184
  Jekyll.logger.error 'jekyll-minify:', "Error during file discovery: #{e.message}"
203
185
  end
204
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
+
205
197
  # Dispatches a single file to the appropriate minifier based on extension.
206
- # 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).
207
201
  #
208
202
  # @param file_path [Pathname] path to the file to process
209
203
  # @return [void]
210
204
  def minify_file(file_path)
211
205
  # Skip already-minified files
212
- 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')
213
207
 
214
208
  case file_path.extname.downcase
215
209
  when '.html'
@@ -217,12 +211,28 @@ module Jekyll
217
211
  when '.css'
218
212
  minify_with_fallback(file_path, :css)
219
213
  when '.js'
220
- minify_with_fallback(file_path, :js, rename_to_min: true)
214
+ minify_js_file(file_path)
221
215
  end
222
216
  rescue StandardError => e
223
217
  Jekyll.logger.warn 'jekyll-minify:', "Unexpected error processing #{file_path}: #{e.message}"
224
218
  end
225
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
+
226
236
  # Minifies a file, restoring the original if anything goes wrong.
227
237
  # Skips non-UTF-8 files with a targeted warning.
228
238
  # Skips empty files.
@@ -236,13 +246,13 @@ module Jekyll
236
246
  original_content = file_path.read(encoding: 'UTF-8')
237
247
  rescue Encoding::InvalidByteSequenceError
238
248
  Jekyll.logger.warn 'jekyll-minify:', "Skipping #{file_path}: not valid UTF-8"
239
- return
249
+ return skip_file
240
250
  end
241
251
 
242
252
  original_bytes = original_content.bytesize
243
253
 
244
254
  # Skip empty files
245
- return if original_bytes.zero?
255
+ return skip_file if original_bytes.zero?
246
256
 
247
257
  begin
248
258
  minified = minify_by_type(original_content, type)
@@ -252,10 +262,20 @@ module Jekyll
252
262
  rename_to_min: rename_to_min)
253
263
  record_minification_result(type, original_bytes, minified_bytes)
254
264
  rescue StandardError => e
255
- restore_on_error(file_path, original_content, e)
265
+ restore_on_error(file_path, original_content, e, rename_to_min: rename_to_min)
256
266
  end
257
267
  end
258
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
+
259
279
  # Validates that minification produced non-empty content.
260
280
  #
261
281
  # @param minified [String, nil] the minification output to validate
@@ -278,16 +298,55 @@ module Jekyll
278
298
  end
279
299
 
280
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.
281
304
  #
282
305
  # @param file_path [Pathname] path to the file being restored
283
306
  # @param original_content [String] the unmodified file content to write back
284
307
  # @param error [StandardError] the error that triggered the restore
308
+ # @param rename_to_min [Boolean] if true, renames .js to .min.js
285
309
  # @return [void]
286
- 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
287
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
288
315
  Jekyll.logger.warn 'jekyll-minify:', error_msg
289
316
  @stats[:errors] << error_msg
290
- 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
291
350
  end
292
351
 
293
352
  # Writes minified content to disk, optionally renaming .js to .min.js.
@@ -300,11 +359,7 @@ module Jekyll
300
359
  # @return [void]
301
360
  def write_minified_file(file_path, minified_content, original_content, rename_to_min: false)
302
361
  file_path.write(minified_content, encoding: 'UTF-8')
303
-
304
- if rename_to_min && file_path.extname == '.js'
305
- new_path = file_path.sub_ext('.min.js')
306
- file_path.rename(new_path)
307
- end
362
+ rename_to_min_js(file_path) if rename_to_min
308
363
  rescue StandardError => e
309
364
  # Restore original if write or rename fails
310
365
  file_path.write(original_content, encoding: 'UTF-8')
@@ -332,7 +387,7 @@ module Jekyll
332
387
 
333
388
  # Minifies HTML content using the minify-html gem.
334
389
  # Uses @minify_options which defaults to DEFAULT_MINIFY_OPTIONS but may be
335
- # overridden via with_minify_data in _config.yml.
390
+ # overridden via jekyll_minify in _config.yml.
336
391
  #
337
392
  # @param content [String] raw HTML string
338
393
  # @return [String] minified HTML
@@ -344,24 +399,56 @@ module Jekyll
344
399
  end
345
400
 
346
401
  # Minifies CSS or JS content by wrapping it in a temporary HTML tag.
347
- # 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.
348
409
  #
349
410
  # @param content [String] raw CSS or JS string
350
411
  # @param wrapper_tag [String] HTML tag name ("style" or "script")
351
412
  # @param minify_option [Symbol] minify-html option key (:minify_css or :minify_js)
352
413
  # @return [String] minified content without wrapper tags
353
- # @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
354
416
  def minify_wrapped_content(content, wrapper_tag, minify_option)
417
+ ensure_wrappable(content, wrapper_tag)
355
418
  wrapped = "<#{wrapper_tag}>#{content}</#{wrapper_tag}>"
356
419
  minified = Kernel.minify_html(wrapped, { minify_option => true })
357
- match = minified.match(%r{<#{wrapper_tag}>(.*?)</#{wrapper_tag}>}m)
358
- 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)
359
423
 
360
424
  match[1]
361
425
  rescue StandardError => e
362
426
  raise "#{wrapper_tag.upcase} minification failed: #{e.message}"
363
427
  end
364
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
+
365
452
  # Minifies a CSS string by wrapping it in a <style> tag.
366
453
  #
367
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.5
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
@@ -85,6 +85,20 @@ dependencies:
85
85
  - - "~>"
86
86
  - !ruby/object:Gem::Version
87
87
  version: '3.13'
88
+ - !ruby/object:Gem::Dependency
89
+ name: rspec_junit_formatter
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '0.6'
95
+ type: :development
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '0.6'
88
102
  - !ruby/object:Gem::Dependency
89
103
  name: simplecov
90
104
  requirement: !ruby/object:Gem::Requirement
@@ -193,6 +207,8 @@ files:
193
207
  - LICENSE
194
208
  - README.md
195
209
  - lib/jekyll-minify.rb
210
+ - lib/jekyll-minify/configuration.rb
211
+ - lib/jekyll-minify/configuration/schema.rb
196
212
  - lib/jekyll-minify/instrumentation.rb
197
213
  - lib/jekyll-minify/version.rb
198
214
  homepage: https://gitlab.com/CentOS/artwork/centos-web/jekyll-minify