cssminify2 2.0.1 → 2.1.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 +5 -5
- data/.travis.yml +10 -5
- data/CHANGELOG.md +121 -0
- data/Dockerfile +16 -0
- data/Gemfile +1 -1
- data/README.md +501 -42
- data/cssminify2.gemspec +6 -2
- data/docs/ADVANCED_USAGE.md +616 -0
- data/docs/API_REFERENCE.md +464 -0
- data/docs/EXAMPLES.md +844 -0
- data/docs/MIGRATION_GUIDE.md +586 -0
- data/lib/cssminify2/cssmin.rb +89 -7
- data/lib/cssminify2/cssmin_enhanced.rb +424 -0
- data/lib/cssminify2/enhanced.rb +818 -0
- data/lib/cssminify2/version.rb +1 -1
- data/lib/cssminify2.rb +53 -4
- data/spec/cssminify_spec.rb +49 -34
- data/spec/tests/README +6 -0
- data/spec/tests/_munge.js +8 -0
- data/spec/tests/_munge.js.min +1 -0
- data/spec/tests/_string_combo.js +5 -0
- data/spec/tests/_string_combo.js.min +1 -0
- data/spec/tests/_string_combo2.js +4 -0
- data/spec/tests/_string_combo2.js.min +1 -0
- data/spec/tests/_string_combo3.js +5 -0
- data/spec/tests/_string_combo3.js.min +1 -0
- data/spec/tests/_syntax_error.js +73 -0
- data/spec/tests/_syntax_error.js.min +1 -0
- data/spec/tests/border-none.css +6 -1
- data/spec/tests/border-none.css.min +1 -1
- data/spec/tests/bug-flex.css +3 -0
- data/spec/tests/bug-flex.css.min +1 -0
- data/spec/tests/bug-nested-pseudoclass.css +3 -0
- data/spec/tests/bug-nested-pseudoclass.css.min +1 -0
- data/spec/tests/bug-preservetoken-calc.css +8 -0
- data/spec/tests/bug-preservetoken-calc.css.min +1 -0
- data/spec/tests/color-keyword.css +1 -0
- data/spec/tests/color-keyword.css.min +1 -0
- data/spec/tests/color.css +2 -0
- data/spec/tests/color.css.min +1 -1
- data/spec/tests/concat-charset.css +2 -2
- data/spec/tests/concat-charset.css.min +1 -1
- data/spec/tests/dataurl-singlequote-font.css +1 -1
- data/spec/tests/dataurl-validity.html +29 -0
- data/spec/tests/float.js +2 -0
- data/spec/tests/float.js.min +1 -0
- data/spec/tests/hsla-issue81.css.FAIL +4 -0
- data/spec/tests/hsla-issue81.css.min +1 -0
- data/spec/tests/ie-backslash9-hack.css +2 -0
- data/spec/tests/ie-backslash9-hack.css.min +1 -0
- data/spec/tests/issue-59.css +7 -0
- data/spec/tests/issue-59.css.min +1 -0
- data/spec/tests/issue151.css +8 -0
- data/spec/tests/issue151.css.min +1 -0
- data/spec/tests/issue172.css.FAIL +4 -0
- data/spec/tests/issue172.css.min +1 -0
- data/spec/tests/issue180.css +16 -0
- data/spec/tests/issue180.css.min +1 -0
- data/spec/tests/issue205.css +2 -0
- data/spec/tests/issue205.css.min +1 -0
- data/spec/tests/issue221.css +1 -1
- data/spec/tests/issue221.css.min +1 -1
- data/spec/tests/issue222.css +2 -2
- data/spec/tests/issue222.css.min +1 -1
- data/spec/tests/issue71.js.FAIL +4 -0
- data/spec/tests/issue71.js.min +1 -0
- data/spec/tests/issue86.js +2 -0
- data/spec/tests/issue86.js.min +1 -0
- data/spec/tests/jquery-1.6.4.js +9046 -0
- data/spec/tests/jquery-1.6.4.js.min +23 -0
- data/spec/tests/lowercasing.css +63 -0
- data/spec/tests/lowercasing.css.min +1 -0
- data/spec/tests/media-test.css +2 -2
- data/spec/tests/old-ie-filter-matrix.css +8 -0
- data/spec/tests/old-ie-filter-matrix.css.min +1 -0
- data/spec/tests/opera-pixel-ratio.css +14 -0
- data/spec/tests/opera-pixel-ratio.css.min +1 -0
- data/spec/tests/pointzeros.css +6 -0
- data/spec/tests/pointzeros.css.min +1 -0
- data/spec/tests/preserve-important.css +1 -0
- data/spec/tests/preserve-important.css.min +1 -0
- data/spec/tests/promise-catch-finally-issue203.js +4 -0
- data/spec/tests/promise-catch-finally-issue203.js.min +1 -0
- data/spec/tests/pseudo-first.css +2 -2
- data/spec/tests/rgb-issue81.css.FAIL +4 -0
- data/spec/tests/rgb-issue81.css.min +1 -0
- data/spec/tests/suite.rhino +3 -0
- data/spec/tests/suite.sh +49 -0
- data/spec/tests/zeros.css +2 -2
- data/spec/tests/zeros.css.min +1 -1
- metadata +129 -14
- data/spec/tests/bug2528093.css +0 -3
- data/spec/tests/bug2528093.css.min +0 -1
- data/spec/tests/keyframe.css +0 -4
- data/spec/tests/keyframe.css.min +0 -1
data/docs/EXAMPLES.md
ADDED
@@ -0,0 +1,844 @@
|
|
1
|
+
# Examples & Usage Patterns
|
2
|
+
|
3
|
+
Real-world examples and common usage patterns for CSSminify2.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Basic Usage Examples](#basic-usage-examples)
|
8
|
+
- [Enhanced Feature Examples](#enhanced-feature-examples)
|
9
|
+
- [Framework Integration Examples](#framework-integration-examples)
|
10
|
+
- [Build Tool Examples](#build-tool-examples)
|
11
|
+
- [Error Handling Examples](#error-handling-examples)
|
12
|
+
- [Performance Optimization Examples](#performance-optimization-examples)
|
13
|
+
|
14
|
+
## Basic Usage Examples
|
15
|
+
|
16
|
+
### Simple CSS Compression
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
require 'cssminify2'
|
20
|
+
|
21
|
+
# Basic string compression
|
22
|
+
css = "body { margin: 0; padding: 0; }"
|
23
|
+
result = CSSminify2.compress(css)
|
24
|
+
puts result
|
25
|
+
# Output: "body{margin:0;padding:0}"
|
26
|
+
|
27
|
+
# File compression
|
28
|
+
original_css = File.read('styles.css')
|
29
|
+
compressed_css = CSSminify2.compress(original_css)
|
30
|
+
File.write('styles.min.css', compressed_css)
|
31
|
+
```
|
32
|
+
|
33
|
+
### With Line Length Control
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
long_css = ".very-long-selector { background: url('very-long-url.png'); }"
|
37
|
+
|
38
|
+
# Default line length (5000 characters)
|
39
|
+
default_result = CSSminify2.compress(long_css)
|
40
|
+
|
41
|
+
# Custom line length (80 characters)
|
42
|
+
short_lines = CSSminify2.compress(long_css, 80)
|
43
|
+
puts "Short lines result:\n#{short_lines}"
|
44
|
+
```
|
45
|
+
|
46
|
+
### Instance-Based Usage
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
compressor = CSSminify2.new
|
50
|
+
|
51
|
+
css_files = ['main.css', 'components.css', 'utilities.css']
|
52
|
+
css_files.each do |file|
|
53
|
+
css = File.read(file)
|
54
|
+
compressed = compressor.compress(css)
|
55
|
+
File.write(file.gsub('.css', '.min.css'), compressed)
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
## Enhanced Feature Examples
|
60
|
+
|
61
|
+
### Duplicate Selector Merging
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
css_with_duplicates = <<-CSS
|
65
|
+
.btn { color: red; }
|
66
|
+
.card { background: white; }
|
67
|
+
.btn { background: blue; }
|
68
|
+
.btn { color: green; } /* This will override the red */
|
69
|
+
.card { border: 1px solid gray; }
|
70
|
+
CSS
|
71
|
+
|
72
|
+
result = CSSminify2.compress_enhanced(css_with_duplicates, {
|
73
|
+
merge_duplicate_selectors: true
|
74
|
+
})
|
75
|
+
|
76
|
+
puts result
|
77
|
+
# Output: ".btn{color:green;background:blue}.card{background:white;border:1px solid gray}"
|
78
|
+
|
79
|
+
# Note: Properties are merged correctly, with later declarations taking precedence
|
80
|
+
```
|
81
|
+
|
82
|
+
### Shorthand Property Optimization
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
css_with_longhand = <<-CSS
|
86
|
+
.container {
|
87
|
+
margin: 10px 10px 10px 10px;
|
88
|
+
padding: 20px 20px;
|
89
|
+
border: 1px solid red 1px;
|
90
|
+
background: none repeat scroll 0 0 #ffffff;
|
91
|
+
}
|
92
|
+
|
93
|
+
.grid {
|
94
|
+
flex: 1 1 auto;
|
95
|
+
grid-gap: 15px 15px;
|
96
|
+
}
|
97
|
+
CSS
|
98
|
+
|
99
|
+
result = CSSminify2.compress_enhanced(css_with_longhand, {
|
100
|
+
optimize_shorthand_properties: true
|
101
|
+
})
|
102
|
+
|
103
|
+
puts result
|
104
|
+
# Output includes optimizations like:
|
105
|
+
# margin: 10px (instead of 10px 10px 10px 10px)
|
106
|
+
# padding: 20px (instead of 20px 20px)
|
107
|
+
# gap: 15px (instead of grid-gap: 15px 15px)
|
108
|
+
```
|
109
|
+
|
110
|
+
### CSS Variables Optimization
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
css_with_variables = <<-CSS
|
114
|
+
:root {
|
115
|
+
--primary-color: #3366CC;
|
116
|
+
--secondary-color: #FF6633;
|
117
|
+
--unused-variable: #DEAD00;
|
118
|
+
--single-use-spacing: 8px;
|
119
|
+
--very-long-variable-name-for-padding: 16px;
|
120
|
+
}
|
121
|
+
|
122
|
+
.component-one {
|
123
|
+
color: var(--primary-color);
|
124
|
+
background: var(--secondary-color);
|
125
|
+
margin: var(--single-use-spacing); /* Only used once */
|
126
|
+
padding: var(--very-long-variable-name-for-padding);
|
127
|
+
}
|
128
|
+
|
129
|
+
.component-two {
|
130
|
+
color: var(--primary-color);
|
131
|
+
padding: var(--very-long-variable-name-for-padding);
|
132
|
+
}
|
133
|
+
CSS
|
134
|
+
|
135
|
+
result = CSSminify2.compress_enhanced(css_with_variables, {
|
136
|
+
compress_css_variables: true,
|
137
|
+
advanced_color_optimization: true
|
138
|
+
})
|
139
|
+
|
140
|
+
puts result
|
141
|
+
# Output optimizes variables:
|
142
|
+
# - Removes --unused-variable (never used)
|
143
|
+
# - Inlines --single-use-spacing (used only once)
|
144
|
+
# - Shortens --very-long-variable-name-for-padding to --v1
|
145
|
+
# - Converts #3366CC to #36C, #FF6633 to #f63
|
146
|
+
```
|
147
|
+
|
148
|
+
### Modern CSS Layout Optimization
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
modern_css = <<-CSS
|
152
|
+
.layout {
|
153
|
+
display: grid;
|
154
|
+
grid-template-columns: repeat(12, 1fr);
|
155
|
+
grid-gap: 20px 20px;
|
156
|
+
justify-content: flex-start;
|
157
|
+
align-items: flex-start;
|
158
|
+
}
|
159
|
+
|
160
|
+
.flex-container {
|
161
|
+
display: flex;
|
162
|
+
flex: 1 1 auto;
|
163
|
+
justify-content: flex-end;
|
164
|
+
align-items: center;
|
165
|
+
transform: translate(0, 0) scale(1, 1) rotate(0deg);
|
166
|
+
}
|
167
|
+
CSS
|
168
|
+
|
169
|
+
result = CSSminify2.compress_enhanced(modern_css, {
|
170
|
+
optimize_shorthand_properties: true
|
171
|
+
})
|
172
|
+
|
173
|
+
puts result
|
174
|
+
# Output includes modern optimizations:
|
175
|
+
# gap: 20px (instead of grid-gap: 20px 20px)
|
176
|
+
# justify-content: start (instead of flex-start)
|
177
|
+
# flex: 1 (instead of 1 1 auto)
|
178
|
+
# transform: translate(0) scale(1) (removing unnecessary values)
|
179
|
+
```
|
180
|
+
|
181
|
+
### Color Optimization
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
css_with_colors = <<-CSS
|
185
|
+
.colors {
|
186
|
+
color: #FF0000;
|
187
|
+
background: #00FF00;
|
188
|
+
border-color: #0000FF;
|
189
|
+
box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
|
190
|
+
}
|
191
|
+
|
192
|
+
.ie-filter {
|
193
|
+
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FF0000', endColorstr='#00FF00');
|
194
|
+
}
|
195
|
+
CSS
|
196
|
+
|
197
|
+
result = CSSminify2.compress_enhanced(css_with_colors, {
|
198
|
+
advanced_color_optimization: true
|
199
|
+
})
|
200
|
+
|
201
|
+
puts result
|
202
|
+
# Output:
|
203
|
+
# - #FF0000 → red (shorter)
|
204
|
+
# - #00FF00 → lime
|
205
|
+
# - #0000FF → blue
|
206
|
+
# - IE filter properties are protected from color conversion
|
207
|
+
```
|
208
|
+
|
209
|
+
## Framework Integration Examples
|
210
|
+
|
211
|
+
### Rails Application
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
# app/lib/enhanced_css_compressor.rb
|
215
|
+
class EnhancedCSSCompressor
|
216
|
+
def initialize(options = {})
|
217
|
+
@base_config = {
|
218
|
+
merge_duplicate_selectors: true,
|
219
|
+
optimize_shorthand_properties: true,
|
220
|
+
advanced_color_optimization: true
|
221
|
+
}
|
222
|
+
|
223
|
+
# Environment-specific overrides
|
224
|
+
@config = case Rails.env
|
225
|
+
when 'development'
|
226
|
+
@base_config.merge(
|
227
|
+
strict_error_handling: true # Catch issues early
|
228
|
+
)
|
229
|
+
when 'production'
|
230
|
+
@base_config.merge(
|
231
|
+
compress_css_variables: true, # More aggressive in production
|
232
|
+
strict_error_handling: false # Graceful fallbacks
|
233
|
+
)
|
234
|
+
else
|
235
|
+
@base_config
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def compress(css)
|
240
|
+
stats = CSSminify2.compress_with_stats(css, @config)
|
241
|
+
|
242
|
+
# Log compression metrics
|
243
|
+
Rails.logger.info "CSS compressed: #{stats[:statistics][:compression_ratio].round(2)}% reduction"
|
244
|
+
|
245
|
+
if stats[:statistics][:fallback_used]
|
246
|
+
Rails.logger.warn "CSS compression used fallback mode"
|
247
|
+
end
|
248
|
+
|
249
|
+
stats[:compressed_css]
|
250
|
+
rescue => e
|
251
|
+
Rails.logger.error "CSS compression failed: #{e.message}"
|
252
|
+
# Ultimate fallback to basic compression
|
253
|
+
CSSminify2.compress(css)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# config/application.rb
|
258
|
+
config.assets.css_compressor = EnhancedCSSCompressor.new
|
259
|
+
```
|
260
|
+
|
261
|
+
### Sinatra Application
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
require 'sinatra'
|
265
|
+
require 'cssminify2'
|
266
|
+
|
267
|
+
class CSSMinifyApp < Sinatra::Base
|
268
|
+
configure :production do
|
269
|
+
# Enable CSS compression for production
|
270
|
+
set :compress_css, true
|
271
|
+
set :css_compressor_options, {
|
272
|
+
merge_duplicate_selectors: true,
|
273
|
+
optimize_shorthand_properties: true,
|
274
|
+
compress_css_variables: true
|
275
|
+
}
|
276
|
+
end
|
277
|
+
|
278
|
+
get '/styles.css' do
|
279
|
+
content_type 'text/css'
|
280
|
+
|
281
|
+
css = File.read('public/styles.css')
|
282
|
+
|
283
|
+
if settings.compress_css?
|
284
|
+
stats = CSSminify2.compress_with_stats(css, settings.css_compressor_options)
|
285
|
+
|
286
|
+
# Add compression info as comment in development
|
287
|
+
if development?
|
288
|
+
compression_info = "/* Compressed: #{stats[:statistics][:compression_ratio].round(2)}% reduction */"
|
289
|
+
"#{compression_info}\n#{stats[:compressed_css]}"
|
290
|
+
else
|
291
|
+
stats[:compressed_css]
|
292
|
+
end
|
293
|
+
else
|
294
|
+
css
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
```
|
299
|
+
|
300
|
+
### Jekyll Site
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
# _plugins/css_optimizer.rb
|
304
|
+
require 'cssminify2'
|
305
|
+
|
306
|
+
module Jekyll
|
307
|
+
class CSSOptimizer < Generator
|
308
|
+
safe true
|
309
|
+
priority :low
|
310
|
+
|
311
|
+
def generate(site)
|
312
|
+
return unless site.config['css_compression']
|
313
|
+
|
314
|
+
config = site.config['css_compression']
|
315
|
+
options = {
|
316
|
+
merge_duplicate_selectors: config['merge_selectors'] || false,
|
317
|
+
optimize_shorthand_properties: config['optimize_shorthand'] || true,
|
318
|
+
compress_css_variables: config['compress_variables'] || false,
|
319
|
+
advanced_color_optimization: config['advanced_colors'] || true
|
320
|
+
}
|
321
|
+
|
322
|
+
# Process CSS files
|
323
|
+
site.static_files.select { |f| f.extname == '.css' }.each do |file|
|
324
|
+
optimize_css_file(file, options, site)
|
325
|
+
end
|
326
|
+
|
327
|
+
# Process CSS in pages and posts
|
328
|
+
(site.pages + site.posts.docs).each do |page|
|
329
|
+
optimize_inline_css(page, options) if page.content.include?('<style>')
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
private
|
334
|
+
|
335
|
+
def optimize_css_file(file, options, site)
|
336
|
+
css = File.read(file.path)
|
337
|
+
|
338
|
+
stats = CSSminify2.compress_with_stats(css, options)
|
339
|
+
File.write(file.path, stats[:compressed_css])
|
340
|
+
|
341
|
+
Jekyll.logger.info "CSS:", "#{file.relative_path} compressed #{stats[:statistics][:compression_ratio].round(1)}%"
|
342
|
+
end
|
343
|
+
|
344
|
+
def optimize_inline_css(page, options)
|
345
|
+
page.content = page.content.gsub(/<style[^>]*>(.*?)<\/style>/m) do |match|
|
346
|
+
style_tag = match
|
347
|
+
css_content = $1
|
348
|
+
|
349
|
+
begin
|
350
|
+
compressed = CSSminify2.compress_enhanced(css_content, options)
|
351
|
+
style_tag.sub(css_content, compressed)
|
352
|
+
rescue => e
|
353
|
+
Jekyll.logger.warn "CSS:", "Failed to compress inline CSS in #{page.relative_path}: #{e.message}"
|
354
|
+
match # Return original on error
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
```
|
361
|
+
|
362
|
+
## Build Tool Examples
|
363
|
+
|
364
|
+
### Rake Task
|
365
|
+
|
366
|
+
```ruby
|
367
|
+
# lib/tasks/css_compression.rake
|
368
|
+
namespace :css do
|
369
|
+
desc "Compress CSS files with enhanced features"
|
370
|
+
task :compress do
|
371
|
+
require 'cssminify2'
|
372
|
+
|
373
|
+
css_files = Dir.glob('app/assets/stylesheets/**/*.css')
|
374
|
+
total_original = 0
|
375
|
+
total_compressed = 0
|
376
|
+
|
377
|
+
css_files.each do |file|
|
378
|
+
next if file.end_with?('.min.css') # Skip already minified files
|
379
|
+
|
380
|
+
css = File.read(file)
|
381
|
+
|
382
|
+
stats = CSSminify2.compress_with_stats(css, {
|
383
|
+
merge_duplicate_selectors: true,
|
384
|
+
optimize_shorthand_properties: true,
|
385
|
+
compress_css_variables: true,
|
386
|
+
advanced_color_optimization: true
|
387
|
+
})
|
388
|
+
|
389
|
+
# Create minified version
|
390
|
+
min_file = file.sub('.css', '.min.css')
|
391
|
+
File.write(min_file, stats[:compressed_css])
|
392
|
+
|
393
|
+
# Track totals
|
394
|
+
total_original += stats[:statistics][:original_size]
|
395
|
+
total_compressed += stats[:statistics][:compressed_size]
|
396
|
+
|
397
|
+
puts "#{file} → #{min_file} (#{stats[:statistics][:compression_ratio].round(2)}%)"
|
398
|
+
end
|
399
|
+
|
400
|
+
overall_ratio = ((total_original - total_compressed).to_f / total_original * 100).round(2)
|
401
|
+
puts "\nOverall compression: #{overall_ratio}% (#{total_original} → #{total_compressed} bytes)"
|
402
|
+
end
|
403
|
+
|
404
|
+
desc "Validate CSS compression results"
|
405
|
+
task :validate do
|
406
|
+
require 'cssminify2'
|
407
|
+
|
408
|
+
css_files = Dir.glob('app/assets/stylesheets/**/*.css')
|
409
|
+
issues = []
|
410
|
+
|
411
|
+
css_files.each do |file|
|
412
|
+
css = File.read(file)
|
413
|
+
|
414
|
+
begin
|
415
|
+
# Test with strict validation
|
416
|
+
CSSminify2.compress_enhanced(css, {
|
417
|
+
merge_duplicate_selectors: true,
|
418
|
+
optimize_shorthand_properties: true,
|
419
|
+
strict_error_handling: true
|
420
|
+
})
|
421
|
+
puts "✓ #{file}"
|
422
|
+
rescue => e
|
423
|
+
issues << "✗ #{file}: #{e.message}"
|
424
|
+
puts "✗ #{file}: #{e.message}"
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
if issues.any?
|
429
|
+
puts "\n#{issues.length} files have issues:"
|
430
|
+
issues.each { |issue| puts " #{issue}" }
|
431
|
+
exit 1
|
432
|
+
else
|
433
|
+
puts "\n✓ All CSS files validated successfully"
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
```
|
438
|
+
|
439
|
+
### Guard Integration
|
440
|
+
|
441
|
+
```ruby
|
442
|
+
# Guardfile
|
443
|
+
require 'cssminify2'
|
444
|
+
|
445
|
+
guard :shell do
|
446
|
+
watch(%r{^app/assets/stylesheets/(.+\.css)$}) do |m|
|
447
|
+
input_file = m[0]
|
448
|
+
output_file = input_file.sub('.css', '.min.css')
|
449
|
+
|
450
|
+
css = File.read(input_file)
|
451
|
+
|
452
|
+
begin
|
453
|
+
stats = CSSminify2.compress_with_stats(css, {
|
454
|
+
merge_duplicate_selectors: true,
|
455
|
+
optimize_shorthand_properties: true,
|
456
|
+
compress_css_variables: ENV['RAILS_ENV'] == 'production'
|
457
|
+
})
|
458
|
+
|
459
|
+
File.write(output_file, stats[:compressed_css])
|
460
|
+
|
461
|
+
puts "CSS compressed: #{input_file} → #{output_file} (#{stats[:statistics][:compression_ratio].round(2)}%)"
|
462
|
+
|
463
|
+
if stats[:statistics][:fallback_used]
|
464
|
+
puts " ⚠️ Fallback compression was used"
|
465
|
+
end
|
466
|
+
|
467
|
+
rescue => e
|
468
|
+
puts "CSS compression failed for #{input_file}: #{e.message}"
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
472
|
+
```
|
473
|
+
|
474
|
+
### Docker Build Integration
|
475
|
+
|
476
|
+
```dockerfile
|
477
|
+
# Dockerfile
|
478
|
+
FROM ruby:3.1
|
479
|
+
|
480
|
+
WORKDIR /app
|
481
|
+
COPY Gemfile Gemfile.lock ./
|
482
|
+
RUN bundle install
|
483
|
+
|
484
|
+
COPY . .
|
485
|
+
|
486
|
+
# Compress CSS as part of build process
|
487
|
+
RUN ruby -e "
|
488
|
+
require 'cssminify2'
|
489
|
+
|
490
|
+
Dir.glob('public/css/**/*.css').each do |file|
|
491
|
+
next if file.end_with?('.min.css')
|
492
|
+
|
493
|
+
css = File.read(file)
|
494
|
+
stats = CSSminify2.compress_with_stats(css, {
|
495
|
+
merge_duplicate_selectors: true,
|
496
|
+
optimize_shorthand_properties: true,
|
497
|
+
compress_css_variables: true,
|
498
|
+
advanced_color_optimization: true
|
499
|
+
})
|
500
|
+
|
501
|
+
File.write(file.sub('.css', '.min.css'), stats[:compressed_css])
|
502
|
+
puts \"Compressed: #{file} (#{stats[:statistics][:compression_ratio].round(2)}%)\"
|
503
|
+
end
|
504
|
+
"
|
505
|
+
|
506
|
+
CMD ['ruby', 'app.rb']
|
507
|
+
```
|
508
|
+
|
509
|
+
## Error Handling Examples
|
510
|
+
|
511
|
+
### Graceful Fallbacks
|
512
|
+
|
513
|
+
```ruby
|
514
|
+
def safe_css_compression(css, options = {})
|
515
|
+
begin
|
516
|
+
# Try enhanced compression first
|
517
|
+
stats = CSSminify2.compress_with_stats(css, options.merge(
|
518
|
+
strict_error_handling: true
|
519
|
+
))
|
520
|
+
|
521
|
+
{
|
522
|
+
css: stats[:compressed_css],
|
523
|
+
method: 'enhanced',
|
524
|
+
stats: stats[:statistics],
|
525
|
+
success: true
|
526
|
+
}
|
527
|
+
rescue CSSminify2Enhanced::MalformedCSSError => e
|
528
|
+
puts "CSS validation failed: #{e.message}"
|
529
|
+
puts "Errors: #{e.css_errors.join(', ')}"
|
530
|
+
|
531
|
+
# Try enhanced compression without strict validation
|
532
|
+
begin
|
533
|
+
result = CSSminify2.compress_enhanced(css, options.merge(
|
534
|
+
strict_error_handling: false
|
535
|
+
))
|
536
|
+
|
537
|
+
{
|
538
|
+
css: result,
|
539
|
+
method: 'enhanced_fallback',
|
540
|
+
warnings: ['CSS validation failed, used non-strict mode'],
|
541
|
+
success: true
|
542
|
+
}
|
543
|
+
rescue => fallback_error
|
544
|
+
puts "Enhanced compression failed: #{fallback_error.message}"
|
545
|
+
|
546
|
+
# Final fallback to basic compression
|
547
|
+
basic_result = CSSminify2.compress(css)
|
548
|
+
|
549
|
+
{
|
550
|
+
css: basic_result,
|
551
|
+
method: 'basic_fallback',
|
552
|
+
warnings: ['Enhanced compression failed, used basic compression'],
|
553
|
+
success: true
|
554
|
+
}
|
555
|
+
end
|
556
|
+
rescue => e
|
557
|
+
puts "Unexpected error: #{e.message}"
|
558
|
+
|
559
|
+
# Always ensure we return something
|
560
|
+
{
|
561
|
+
css: css, # Return original CSS if all else fails
|
562
|
+
method: 'none',
|
563
|
+
error: e.message,
|
564
|
+
success: false
|
565
|
+
}
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
# Usage
|
570
|
+
result = safe_css_compression(problematic_css, {
|
571
|
+
merge_duplicate_selectors: true,
|
572
|
+
optimize_shorthand_properties: true
|
573
|
+
})
|
574
|
+
|
575
|
+
puts "Compression method used: #{result[:method]}"
|
576
|
+
puts "Result: #{result[:css]}"
|
577
|
+
puts "Warnings: #{result[:warnings].join(', ')}" if result[:warnings]
|
578
|
+
```
|
579
|
+
|
580
|
+
### Development vs Production Error Handling
|
581
|
+
|
582
|
+
```ruby
|
583
|
+
class EnvironmentAwareCSSCompressor
|
584
|
+
def initialize(environment = Rails.env)
|
585
|
+
@environment = environment
|
586
|
+
@config = compression_config_for_environment
|
587
|
+
end
|
588
|
+
|
589
|
+
def compress(css)
|
590
|
+
if development_or_test?
|
591
|
+
# Strict validation in development to catch issues early
|
592
|
+
compress_with_strict_validation(css)
|
593
|
+
else
|
594
|
+
# Graceful fallbacks in production
|
595
|
+
compress_with_fallbacks(css)
|
596
|
+
end
|
597
|
+
end
|
598
|
+
|
599
|
+
private
|
600
|
+
|
601
|
+
def development_or_test?
|
602
|
+
%w[development test].include?(@environment)
|
603
|
+
end
|
604
|
+
|
605
|
+
def compression_config_for_environment
|
606
|
+
base_config = {
|
607
|
+
optimize_shorthand_properties: true,
|
608
|
+
merge_duplicate_selectors: true
|
609
|
+
}
|
610
|
+
|
611
|
+
case @environment
|
612
|
+
when 'development'
|
613
|
+
base_config.merge(strict_error_handling: true)
|
614
|
+
when 'test'
|
615
|
+
base_config.merge(
|
616
|
+
strict_error_handling: true,
|
617
|
+
compress_css_variables: true # Test more aggressive compression
|
618
|
+
)
|
619
|
+
when 'production'
|
620
|
+
base_config.merge(
|
621
|
+
compress_css_variables: true,
|
622
|
+
advanced_color_optimization: true,
|
623
|
+
strict_error_handling: false
|
624
|
+
)
|
625
|
+
else
|
626
|
+
base_config
|
627
|
+
end
|
628
|
+
end
|
629
|
+
|
630
|
+
def compress_with_strict_validation(css)
|
631
|
+
CSSminify2.compress_enhanced(css, @config)
|
632
|
+
rescue CSSminify2Enhanced::MalformedCSSError => e
|
633
|
+
error_message = "CSS Validation Error:\n"
|
634
|
+
error_message += "#{e.message}\n"
|
635
|
+
error_message += "Specific issues:\n"
|
636
|
+
e.css_errors.each { |error| error_message += " - #{error}\n" }
|
637
|
+
error_message += "\nPlease fix these CSS issues before proceeding."
|
638
|
+
|
639
|
+
raise StandardError, error_message
|
640
|
+
end
|
641
|
+
|
642
|
+
def compress_with_fallbacks(css)
|
643
|
+
# Production: try enhanced, fallback gracefully
|
644
|
+
CSSminify2.compress_enhanced(css, @config)
|
645
|
+
rescue => e
|
646
|
+
Rails.logger.warn "Enhanced CSS compression failed: #{e.message}"
|
647
|
+
|
648
|
+
# Log the error but continue with basic compression
|
649
|
+
Rails.logger.warn "Falling back to basic CSS compression"
|
650
|
+
CSSminify2.compress(css)
|
651
|
+
end
|
652
|
+
end
|
653
|
+
```
|
654
|
+
|
655
|
+
## Performance Optimization Examples
|
656
|
+
|
657
|
+
### Benchmarking and Profiling
|
658
|
+
|
659
|
+
```ruby
|
660
|
+
require 'benchmark'
|
661
|
+
require 'cssminify2'
|
662
|
+
|
663
|
+
def benchmark_css_compression(css_files)
|
664
|
+
puts "CSS Compression Benchmarks"
|
665
|
+
puts "=" * 50
|
666
|
+
|
667
|
+
css_files.each do |file|
|
668
|
+
css = File.read(file)
|
669
|
+
puts "\nFile: #{file} (#{css.length} characters)"
|
670
|
+
|
671
|
+
Benchmark.bm(20) do |x|
|
672
|
+
basic_result = nil
|
673
|
+
enhanced_result = nil
|
674
|
+
|
675
|
+
x.report("Basic compression:") do
|
676
|
+
basic_result = CSSminify2.compress(css)
|
677
|
+
end
|
678
|
+
|
679
|
+
x.report("Enhanced (safe):") do
|
680
|
+
enhanced_result = CSSminify2.compress_enhanced(css, {
|
681
|
+
optimize_shorthand_properties: true
|
682
|
+
})
|
683
|
+
end
|
684
|
+
|
685
|
+
x.report("Enhanced (full):") do
|
686
|
+
CSSminify2.compress_enhanced(css, {
|
687
|
+
merge_duplicate_selectors: true,
|
688
|
+
optimize_shorthand_properties: true,
|
689
|
+
compress_css_variables: true,
|
690
|
+
advanced_color_optimization: true
|
691
|
+
})
|
692
|
+
end
|
693
|
+
|
694
|
+
# Show compression ratios
|
695
|
+
basic_ratio = ((css.length - basic_result.length).to_f / css.length * 100).round(2)
|
696
|
+
enhanced_ratio = ((css.length - enhanced_result.length).to_f / css.length * 100).round(2)
|
697
|
+
|
698
|
+
puts " Basic ratio: #{basic_ratio}%"
|
699
|
+
puts " Enhanced ratio: #{enhanced_ratio}%"
|
700
|
+
puts " Improvement: #{(enhanced_ratio - basic_ratio).round(2)}%"
|
701
|
+
end
|
702
|
+
end
|
703
|
+
end
|
704
|
+
|
705
|
+
# Run benchmarks
|
706
|
+
css_files = Dir.glob('test/fixtures/**/*.css')
|
707
|
+
benchmark_css_compression(css_files)
|
708
|
+
```
|
709
|
+
|
710
|
+
### Memory-Efficient Processing
|
711
|
+
|
712
|
+
```ruby
|
713
|
+
class MemoryEfficientCSSProcessor
|
714
|
+
def self.process_large_file(input_file, output_file, options = {})
|
715
|
+
# Read file size
|
716
|
+
file_size = File.size(input_file)
|
717
|
+
puts "Processing #{input_file} (#{file_size} bytes)"
|
718
|
+
|
719
|
+
if file_size > 1_000_000 # 1MB threshold
|
720
|
+
process_in_chunks(input_file, output_file, options)
|
721
|
+
else
|
722
|
+
process_normally(input_file, output_file, options)
|
723
|
+
end
|
724
|
+
end
|
725
|
+
|
726
|
+
private
|
727
|
+
|
728
|
+
def self.process_in_chunks(input_file, output_file, options)
|
729
|
+
css = File.read(input_file)
|
730
|
+
|
731
|
+
# Split CSS into logical sections (at rule boundaries)
|
732
|
+
sections = css.split(/(?<=\})\s*(?=\.|#|@|\w)/)
|
733
|
+
|
734
|
+
compressed_sections = []
|
735
|
+
|
736
|
+
sections.each_slice(50) do |section_batch| # Process 50 rules at a time
|
737
|
+
batch_css = section_batch.join('')
|
738
|
+
|
739
|
+
begin
|
740
|
+
compressed = CSSminify2.compress_enhanced(batch_css, options)
|
741
|
+
compressed_sections << compressed
|
742
|
+
rescue => e
|
743
|
+
puts "Warning: Batch compression failed, using basic compression: #{e.message}"
|
744
|
+
compressed = CSSminify2.compress(batch_css)
|
745
|
+
compressed_sections << compressed
|
746
|
+
end
|
747
|
+
end
|
748
|
+
|
749
|
+
# Combine all compressed sections
|
750
|
+
final_result = compressed_sections.join('')
|
751
|
+
File.write(output_file, final_result)
|
752
|
+
|
753
|
+
puts "Processed in #{sections.length} sections, #{compressed_sections.length} batches"
|
754
|
+
end
|
755
|
+
|
756
|
+
def self.process_normally(input_file, output_file, options)
|
757
|
+
css = File.read(input_file)
|
758
|
+
|
759
|
+
stats = CSSminify2.compress_with_stats(css, options)
|
760
|
+
File.write(output_file, stats[:compressed_css])
|
761
|
+
|
762
|
+
puts "Compressed: #{stats[:statistics][:compression_ratio].round(2)}% reduction"
|
763
|
+
end
|
764
|
+
end
|
765
|
+
|
766
|
+
# Usage
|
767
|
+
MemoryEfficientCSSProcessor.process_large_file(
|
768
|
+
'large-framework.css',
|
769
|
+
'large-framework.min.css',
|
770
|
+
{
|
771
|
+
merge_duplicate_selectors: true,
|
772
|
+
optimize_shorthand_properties: true,
|
773
|
+
compress_css_variables: true
|
774
|
+
}
|
775
|
+
)
|
776
|
+
```
|
777
|
+
|
778
|
+
### Caching and Persistent Compressors
|
779
|
+
|
780
|
+
```ruby
|
781
|
+
class CachingCSSCompressor
|
782
|
+
def initialize(cache_dir = 'tmp/css_cache')
|
783
|
+
@cache_dir = cache_dir
|
784
|
+
@compressor = CSSminify2Enhanced::Compressor.new(
|
785
|
+
CSSminify2Enhanced::Configuration.aggressive
|
786
|
+
)
|
787
|
+
|
788
|
+
FileUtils.mkdir_p(@cache_dir)
|
789
|
+
end
|
790
|
+
|
791
|
+
def compress(css)
|
792
|
+
# Generate cache key from CSS content
|
793
|
+
cache_key = Digest::SHA256.hexdigest(css)
|
794
|
+
cache_file = File.join(@cache_dir, "#{cache_key}.css")
|
795
|
+
|
796
|
+
# Return cached result if available
|
797
|
+
if File.exist?(cache_file)
|
798
|
+
puts "Using cached compression for #{cache_key[0..7]}..."
|
799
|
+
return File.read(cache_file)
|
800
|
+
end
|
801
|
+
|
802
|
+
# Compress and cache result
|
803
|
+
result = @compressor.compress(css)
|
804
|
+
File.write(cache_file, result)
|
805
|
+
|
806
|
+
puts "Compressed and cached #{cache_key[0..7]}..."
|
807
|
+
result
|
808
|
+
end
|
809
|
+
|
810
|
+
def cache_stats
|
811
|
+
cached_files = Dir.glob(File.join(@cache_dir, '*.css'))
|
812
|
+
total_size = cached_files.sum { |f| File.size(f) }
|
813
|
+
|
814
|
+
{
|
815
|
+
cached_files: cached_files.length,
|
816
|
+
total_cache_size: total_size,
|
817
|
+
cache_directory: @cache_dir
|
818
|
+
}
|
819
|
+
end
|
820
|
+
|
821
|
+
def clear_cache
|
822
|
+
FileUtils.rm_rf(@cache_dir)
|
823
|
+
FileUtils.mkdir_p(@cache_dir)
|
824
|
+
puts "CSS compression cache cleared"
|
825
|
+
end
|
826
|
+
end
|
827
|
+
|
828
|
+
# Usage
|
829
|
+
compressor = CachingCSSCompressor.new
|
830
|
+
|
831
|
+
# Process multiple files with caching
|
832
|
+
css_files = Dir.glob('assets/**/*.css')
|
833
|
+
css_files.each do |file|
|
834
|
+
css = File.read(file)
|
835
|
+
compressed = compressor.compress(css) # Will use cache on repeat calls
|
836
|
+
|
837
|
+
output_file = file.sub('.css', '.min.css')
|
838
|
+
File.write(output_file, compressed)
|
839
|
+
end
|
840
|
+
|
841
|
+
puts "Cache stats: #{compressor.cache_stats}"
|
842
|
+
```
|
843
|
+
|
844
|
+
These examples demonstrate the flexibility and power of CSSminify2's enhanced features across different use cases, from simple compression to complex build pipeline integration.
|