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 +4 -4
- data/lib/jekyll-minify/configuration/schema.rb +160 -0
- data/lib/jekyll-minify/configuration.rb +123 -0
- data/lib/jekyll-minify/instrumentation.rb +12 -6
- data/lib/jekyll-minify/version.rb +1 -1
- data/lib/jekyll-minify.rb +154 -74
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cb32991092f0b2d76cbe462ef326b8e8e17bcd32412cc4d1d96b067243d86860
|
|
4
|
+
data.tar.gz: '0586c8ae509a614b9c0f8e155515fc4c9af03754d0d5a53ca1dd29a54bd84af5'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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, :
|
|
55
|
-
lambda { |span, _this, args,
|
|
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
|
|
149
|
-
#
|
|
150
|
-
#
|
|
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::
|
|
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 = '
|
|
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
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# remove_bangs: false #
|
|
49
|
-
# minify_css: false #
|
|
50
|
-
# minify_js: false #
|
|
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
|
-
#
|
|
53
|
-
#
|
|
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
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
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
|
-
#
|
|
101
|
+
# jekyll_minify:
|
|
102
|
+
# enabled: false
|
|
99
103
|
#
|
|
100
104
|
# @example Override minify-html options for production
|
|
101
105
|
# # _config.yml
|
|
102
|
-
#
|
|
103
|
-
#
|
|
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
|
-
#
|
|
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
|
|
153
|
-
#
|
|
154
|
-
#
|
|
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
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
return
|
|
164
|
-
end
|
|
158
|
+
settings = Configuration.for(site.config)
|
|
159
|
+
return unless settings['enabled']
|
|
165
160
|
|
|
166
|
-
@minify_options =
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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}>(
|
|
365
|
-
raise "
|
|
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:
|
|
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:
|
|
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:
|
|
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
|