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.
Files changed (95) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +10 -5
  3. data/CHANGELOG.md +121 -0
  4. data/Dockerfile +16 -0
  5. data/Gemfile +1 -1
  6. data/README.md +501 -42
  7. data/cssminify2.gemspec +6 -2
  8. data/docs/ADVANCED_USAGE.md +616 -0
  9. data/docs/API_REFERENCE.md +464 -0
  10. data/docs/EXAMPLES.md +844 -0
  11. data/docs/MIGRATION_GUIDE.md +586 -0
  12. data/lib/cssminify2/cssmin.rb +89 -7
  13. data/lib/cssminify2/cssmin_enhanced.rb +424 -0
  14. data/lib/cssminify2/enhanced.rb +818 -0
  15. data/lib/cssminify2/version.rb +1 -1
  16. data/lib/cssminify2.rb +53 -4
  17. data/spec/cssminify_spec.rb +49 -34
  18. data/spec/tests/README +6 -0
  19. data/spec/tests/_munge.js +8 -0
  20. data/spec/tests/_munge.js.min +1 -0
  21. data/spec/tests/_string_combo.js +5 -0
  22. data/spec/tests/_string_combo.js.min +1 -0
  23. data/spec/tests/_string_combo2.js +4 -0
  24. data/spec/tests/_string_combo2.js.min +1 -0
  25. data/spec/tests/_string_combo3.js +5 -0
  26. data/spec/tests/_string_combo3.js.min +1 -0
  27. data/spec/tests/_syntax_error.js +73 -0
  28. data/spec/tests/_syntax_error.js.min +1 -0
  29. data/spec/tests/border-none.css +6 -1
  30. data/spec/tests/border-none.css.min +1 -1
  31. data/spec/tests/bug-flex.css +3 -0
  32. data/spec/tests/bug-flex.css.min +1 -0
  33. data/spec/tests/bug-nested-pseudoclass.css +3 -0
  34. data/spec/tests/bug-nested-pseudoclass.css.min +1 -0
  35. data/spec/tests/bug-preservetoken-calc.css +8 -0
  36. data/spec/tests/bug-preservetoken-calc.css.min +1 -0
  37. data/spec/tests/color-keyword.css +1 -0
  38. data/spec/tests/color-keyword.css.min +1 -0
  39. data/spec/tests/color.css +2 -0
  40. data/spec/tests/color.css.min +1 -1
  41. data/spec/tests/concat-charset.css +2 -2
  42. data/spec/tests/concat-charset.css.min +1 -1
  43. data/spec/tests/dataurl-singlequote-font.css +1 -1
  44. data/spec/tests/dataurl-validity.html +29 -0
  45. data/spec/tests/float.js +2 -0
  46. data/spec/tests/float.js.min +1 -0
  47. data/spec/tests/hsla-issue81.css.FAIL +4 -0
  48. data/spec/tests/hsla-issue81.css.min +1 -0
  49. data/spec/tests/ie-backslash9-hack.css +2 -0
  50. data/spec/tests/ie-backslash9-hack.css.min +1 -0
  51. data/spec/tests/issue-59.css +7 -0
  52. data/spec/tests/issue-59.css.min +1 -0
  53. data/spec/tests/issue151.css +8 -0
  54. data/spec/tests/issue151.css.min +1 -0
  55. data/spec/tests/issue172.css.FAIL +4 -0
  56. data/spec/tests/issue172.css.min +1 -0
  57. data/spec/tests/issue180.css +16 -0
  58. data/spec/tests/issue180.css.min +1 -0
  59. data/spec/tests/issue205.css +2 -0
  60. data/spec/tests/issue205.css.min +1 -0
  61. data/spec/tests/issue221.css +1 -1
  62. data/spec/tests/issue221.css.min +1 -1
  63. data/spec/tests/issue222.css +2 -2
  64. data/spec/tests/issue222.css.min +1 -1
  65. data/spec/tests/issue71.js.FAIL +4 -0
  66. data/spec/tests/issue71.js.min +1 -0
  67. data/spec/tests/issue86.js +2 -0
  68. data/spec/tests/issue86.js.min +1 -0
  69. data/spec/tests/jquery-1.6.4.js +9046 -0
  70. data/spec/tests/jquery-1.6.4.js.min +23 -0
  71. data/spec/tests/lowercasing.css +63 -0
  72. data/spec/tests/lowercasing.css.min +1 -0
  73. data/spec/tests/media-test.css +2 -2
  74. data/spec/tests/old-ie-filter-matrix.css +8 -0
  75. data/spec/tests/old-ie-filter-matrix.css.min +1 -0
  76. data/spec/tests/opera-pixel-ratio.css +14 -0
  77. data/spec/tests/opera-pixel-ratio.css.min +1 -0
  78. data/spec/tests/pointzeros.css +6 -0
  79. data/spec/tests/pointzeros.css.min +1 -0
  80. data/spec/tests/preserve-important.css +1 -0
  81. data/spec/tests/preserve-important.css.min +1 -0
  82. data/spec/tests/promise-catch-finally-issue203.js +4 -0
  83. data/spec/tests/promise-catch-finally-issue203.js.min +1 -0
  84. data/spec/tests/pseudo-first.css +2 -2
  85. data/spec/tests/rgb-issue81.css.FAIL +4 -0
  86. data/spec/tests/rgb-issue81.css.min +1 -0
  87. data/spec/tests/suite.rhino +3 -0
  88. data/spec/tests/suite.sh +49 -0
  89. data/spec/tests/zeros.css +2 -2
  90. data/spec/tests/zeros.css.min +1 -1
  91. metadata +129 -14
  92. data/spec/tests/bug2528093.css +0 -3
  93. data/spec/tests/bug2528093.css.min +0 -1
  94. data/spec/tests/keyframe.css +0 -4
  95. data/spec/tests/keyframe.css.min +0 -1
@@ -0,0 +1,818 @@
1
+ # Enhanced CSS Compression Features (Optional)
2
+ #
3
+ # This file provides optional enhanced features for CSS compression
4
+ # while maintaining 100% backward compatibility with the original API.
5
+ #
6
+ # Usage:
7
+ # # Original API (unchanged)
8
+ # CSSminify2.compress(css)
9
+ #
10
+ # # Enhanced API (new, optional)
11
+ # CSSminify2Enhanced.compress(css, options)
12
+ # CSSminify2Enhanced.new(config).compress(css)
13
+
14
+ module CSSminify2Enhanced
15
+
16
+ # Configuration class for enhanced features
17
+ class Configuration
18
+ attr_accessor :merge_duplicate_selectors, :optimize_shorthand_properties,
19
+ :advanced_color_optimization, :preserve_ie_hacks,
20
+ :compress_css_variables, :strict_error_handling,
21
+ :generate_source_map, :statistics_enabled
22
+
23
+ def initialize
24
+ # All new features are opt-in by default for compatibility
25
+ @merge_duplicate_selectors = false
26
+ @optimize_shorthand_properties = false
27
+ @advanced_color_optimization = false
28
+ @preserve_ie_hacks = true
29
+ @compress_css_variables = false
30
+ @strict_error_handling = false
31
+ @generate_source_map = false
32
+ @statistics_enabled = false
33
+ end
34
+
35
+ # Preset configurations
36
+ def self.conservative
37
+ new # All features disabled
38
+ end
39
+
40
+ def self.aggressive
41
+ config = new
42
+ config.merge_duplicate_selectors = true
43
+ config.optimize_shorthand_properties = true
44
+ config.advanced_color_optimization = true
45
+ config.compress_css_variables = true
46
+ config
47
+ end
48
+
49
+ def self.modern
50
+ config = aggressive
51
+ config.generate_source_map = true
52
+ config.statistics_enabled = true
53
+ config
54
+ end
55
+ end
56
+
57
+ # Enhanced compressor with new features
58
+ class Compressor
59
+ attr_reader :config, :statistics
60
+
61
+ def initialize(config = Configuration.new)
62
+ @config = config
63
+ @statistics = {
64
+ original_size: 0,
65
+ compressed_size: 0,
66
+ compression_ratio: 0.0,
67
+ selectors_merged: 0,
68
+ properties_optimized: 0,
69
+ colors_converted: 0
70
+ }
71
+ end
72
+
73
+ def compress(css, linebreakpos = 5000)
74
+ @statistics[:original_size] = css.length
75
+
76
+ begin
77
+ # Validate CSS structure before processing
78
+ validate_css_structure(css) if @config.strict_error_handling
79
+
80
+ # Start with the original compression to maintain compatibility
81
+ result = CssCompressor.compress(css, linebreakpos)
82
+
83
+ # Apply enhanced optimizations if enabled
84
+ if any_enhancements_enabled?
85
+ result = apply_enhanced_optimizations_safely(result)
86
+ end
87
+
88
+ @statistics[:compressed_size] = result.length
89
+ @statistics[:compression_ratio] = calculate_compression_ratio
90
+
91
+ result
92
+
93
+ rescue => e
94
+ if @config.strict_error_handling
95
+ raise EnhancedCompressionError.new("Enhanced compression failed: #{e.message}", e)
96
+ else
97
+ # Graceful fallback to original compressor
98
+ fallback_result = safe_fallback_compression(css, linebreakpos)
99
+ @statistics[:compressed_size] = fallback_result.length
100
+ @statistics[:compression_ratio] = calculate_compression_ratio
101
+ @statistics[:fallback_used] = true
102
+ fallback_result
103
+ end
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def any_enhancements_enabled?
110
+ @config.merge_duplicate_selectors ||
111
+ @config.optimize_shorthand_properties ||
112
+ @config.advanced_color_optimization ||
113
+ @config.compress_css_variables
114
+ end
115
+
116
+ def apply_enhanced_optimizations(css)
117
+ css = merge_duplicate_selectors(css) if @config.merge_duplicate_selectors
118
+ css = optimize_shorthand_properties(css) if @config.optimize_shorthand_properties
119
+ css = enhance_zero_value_optimization(css) if @config.optimize_shorthand_properties
120
+ css = optimize_modern_layout_properties(css) if @config.optimize_shorthand_properties
121
+ css = compress_css_variables(css) if @config.compress_css_variables
122
+ css = advanced_color_optimization(css) if @config.advanced_color_optimization
123
+ css
124
+ end
125
+
126
+ def apply_enhanced_optimizations_safely(css)
127
+ # Apply optimizations with individual error handling
128
+ optimizations = [
129
+ [:merge_duplicate_selectors, @config.merge_duplicate_selectors],
130
+ [:optimize_shorthand_properties, @config.optimize_shorthand_properties],
131
+ [:enhance_zero_value_optimization, @config.optimize_shorthand_properties],
132
+ [:optimize_modern_layout_properties, @config.optimize_shorthand_properties],
133
+ [:compress_css_variables, @config.compress_css_variables],
134
+ [:advanced_color_optimization, @config.advanced_color_optimization]
135
+ ]
136
+
137
+ optimizations.each do |method_name, enabled|
138
+ next unless enabled
139
+
140
+ begin
141
+ css = send(method_name, css)
142
+ rescue => e
143
+ if @config.strict_error_handling
144
+ raise e
145
+ else
146
+ # Log error but continue with other optimizations
147
+ warn "Warning: #{method_name} optimization failed: #{e.message}"
148
+ end
149
+ end
150
+ end
151
+
152
+ css
153
+ end
154
+
155
+ def validate_css_structure(css)
156
+ # Basic CSS validation to catch major structural issues
157
+ errors = []
158
+
159
+ # Check for balanced braces
160
+ open_braces = css.count('{')
161
+ close_braces = css.count('}')
162
+ if open_braces != close_braces
163
+ errors << "Unbalanced braces: #{open_braces} opening vs #{close_braces} closing"
164
+ end
165
+
166
+ # Check for balanced quotes
167
+ double_quotes = css.scan(/"/).length
168
+ single_quotes = css.scan(/'/).length
169
+ if double_quotes % 2 != 0
170
+ errors << "Unmatched double quotes"
171
+ end
172
+ if single_quotes % 2 != 0
173
+ errors << "Unmatched single quotes"
174
+ end
175
+
176
+ # Check for valid CSS structure patterns
177
+ if css.match(/\{[^{}]*\{/) # Nested braces outside of media queries/keyframes
178
+ unless css.match(/@(?:media|supports|keyframes|container)/)
179
+ errors << "Potentially invalid nested braces"
180
+ end
181
+ end
182
+
183
+ # Check for common syntax errors
184
+ if css.match(/[^;{}]\s*\}/) && !css.match(/@/)
185
+ errors << "Missing semicolon before closing brace"
186
+ end
187
+
188
+ if errors.any?
189
+ raise MalformedCSSError.new("CSS validation failed: #{errors.join(', ')}")
190
+ end
191
+ end
192
+
193
+ def safe_fallback_compression(css, linebreakpos)
194
+ # Safe fallback with minimal error handling
195
+ begin
196
+ CssCompressor.compress(css, linebreakpos)
197
+ rescue => e
198
+ # Last resort: basic whitespace compression
199
+ warn "Warning: Fallback to basic compression due to: #{e.message}"
200
+ basic_compression_fallback(css)
201
+ end
202
+ end
203
+
204
+ def basic_compression_fallback(css)
205
+ # Ultra-safe basic compression as last resort
206
+ css
207
+ .gsub(/\/\*.*?\*\//m, '') # Remove comments
208
+ .gsub(/\s+/, ' ') # Compress whitespace
209
+ .gsub(/\s*{\s*/, '{') # Clean braces
210
+ .gsub(/\s*}\s*/, '}')
211
+ .gsub(/\s*;\s*/, ';') # Clean semicolons
212
+ .gsub(/:\s+/, ':') # Clean colons
213
+ .strip
214
+ end
215
+
216
+ def merge_duplicate_selectors(css)
217
+ # Advanced duplicate selector merging with proper CSS parsing
218
+ # Handles media queries, keyframes, and preserves cascade order
219
+
220
+ selectors_merged = 0
221
+
222
+ # Split CSS into blocks (rules, at-rules, etc.)
223
+ css = css.gsub(/@media[^{]*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/m) do |media_block|
224
+ # Process media queries recursively
225
+ process_media_block(media_block)
226
+ end
227
+
228
+ # Process regular CSS rules outside of media queries
229
+ css = merge_regular_selectors(css)
230
+
231
+ @statistics[:selectors_merged] = selectors_merged
232
+ css
233
+ end
234
+
235
+ private
236
+
237
+ def process_media_block(media_block)
238
+ # Extract media query and content
239
+ if media_block.match(/@media([^{]*)\{(.*)\}/m)
240
+ media_query = $1.strip
241
+ content = $2
242
+
243
+ # Merge selectors within this media query
244
+ merged_content = merge_regular_selectors(content)
245
+
246
+ "@media#{media_query}{#{merged_content}}"
247
+ else
248
+ media_block
249
+ end
250
+ end
251
+
252
+ def merge_regular_selectors(css)
253
+ # Parse CSS rules more carefully
254
+ rules = parse_css_rules(css)
255
+ merged_rules = merge_parsed_rules(rules)
256
+ rebuild_css_from_rules(merged_rules)
257
+ end
258
+
259
+ def parse_css_rules(css)
260
+ rules = []
261
+ current_pos = 0
262
+
263
+ while current_pos < css.length
264
+ # Find next rule
265
+ rule_match = css.match(/([^{}]+)\{([^{}]*)\}/m, current_pos)
266
+ break unless rule_match
267
+
268
+ selector = rule_match[1].strip
269
+ declarations = rule_match[2].strip
270
+ position = rule_match.begin(0)
271
+
272
+ # Skip if this looks like an at-rule we shouldn't merge
273
+ unless selector.match(/^@(?:keyframes|font-face|page|supports|document)/)
274
+ rules << {
275
+ selector: normalize_selector(selector),
276
+ original_selector: selector,
277
+ declarations: declarations,
278
+ position: position
279
+ }
280
+ end
281
+
282
+ current_pos = rule_match.end(0)
283
+ end
284
+
285
+ rules
286
+ end
287
+
288
+ def normalize_selector(selector)
289
+ # Normalize selector for comparison (remove extra whitespace, etc.)
290
+ selector.gsub(/\s+/, ' ').strip
291
+ end
292
+
293
+ def merge_parsed_rules(rules)
294
+ # Group rules by selector, maintaining order
295
+ selector_groups = {}
296
+ merged_rules = []
297
+
298
+ rules.each do |rule|
299
+ selector = rule[:selector]
300
+
301
+ if selector_groups[selector]
302
+ # Merge with existing rule
303
+ existing_rule = selector_groups[selector]
304
+ existing_rule[:declarations] = merge_declarations(
305
+ existing_rule[:declarations],
306
+ rule[:declarations]
307
+ )
308
+ @statistics[:selectors_merged] += 1
309
+ else
310
+ # First occurrence of this selector
311
+ new_rule = rule.dup
312
+ selector_groups[selector] = new_rule
313
+ merged_rules << new_rule
314
+ end
315
+ end
316
+
317
+ merged_rules
318
+ end
319
+
320
+ def merge_declarations(existing_declarations, new_declarations)
321
+ # Parse declarations and merge, with later declarations overriding earlier ones
322
+ existing_props = parse_declarations(existing_declarations)
323
+ new_props = parse_declarations(new_declarations)
324
+
325
+ # Merge properties, with new_props taking precedence
326
+ merged_props = existing_props.merge(new_props)
327
+
328
+ # Rebuild declaration string
329
+ merged_props.map { |prop, value| "#{prop}:#{value}" }.join(';')
330
+ end
331
+
332
+ def parse_declarations(declarations)
333
+ properties = {}
334
+ return properties if declarations.empty?
335
+
336
+ declarations.split(';').each do |declaration|
337
+ if declaration.match(/^\s*([^:]+):\s*(.+)\s*$/)
338
+ property = $1.strip
339
+ value = $2.strip
340
+ properties[property] = value
341
+ end
342
+ end
343
+
344
+ properties
345
+ end
346
+
347
+ def rebuild_css_from_rules(rules)
348
+ rules.map do |rule|
349
+ declarations = rule[:declarations]
350
+ declarations = declarations.empty? ? '' : declarations
351
+ "#{rule[:original_selector]}{#{declarations}}"
352
+ end.join('')
353
+ end
354
+
355
+ def optimize_shorthand_properties(css)
356
+ original_length = css.length
357
+
358
+ # Advanced margin/padding optimization with units flexibility
359
+ css = optimize_box_model_properties(css, 'margin')
360
+ css = optimize_box_model_properties(css, 'padding')
361
+
362
+ # Background shorthand optimizations
363
+ css = optimize_background_properties(css)
364
+
365
+ # Border shorthand optimizations
366
+ css = optimize_border_properties(css)
367
+
368
+ # Font shorthand optimizations
369
+ css = optimize_font_properties(css)
370
+
371
+ # List-style optimizations
372
+ css = optimize_list_properties(css)
373
+
374
+ @statistics[:properties_optimized] += ((original_length - css.length) / 10).to_i # Rough estimate
375
+ css
376
+ end
377
+
378
+ private
379
+
380
+ def optimize_box_model_properties(css, property)
381
+ # Handle all units, not just px
382
+ unit_pattern = '(?:px|em|rem|%|vh|vw|pt|pc|in|cm|mm|ex|ch|vmin|vmax|0)'
383
+
384
+ # Four identical values: margin: 10px 10px 10px 10px → margin: 10px
385
+ css = css.gsub(/#{property}:\s*([+-]?\d*\.?\d+#{unit_pattern})\s+\1\s+\1\s+\1/i) { "#{property}:#{$1}" }
386
+
387
+ # Vertical/horizontal pairs: margin: 10px 20px 10px 20px → margin: 10px 20px
388
+ css = css.gsub(/#{property}:\s*([+-]?\d*\.?\d+#{unit_pattern})\s+([+-]?\d*\.?\d+#{unit_pattern})\s+\1\s+\2/i) { "#{property}:#{$1} #{$2}" }
389
+
390
+ # Three values where first and third are same: margin: 10px 20px 10px → margin: 10px 20px
391
+ css = css.gsub(/#{property}:\s*([+-]?\d*\.?\d+#{unit_pattern})\s+([+-]?\d*\.?\d+#{unit_pattern})\s+\1/i) { "#{property}:#{$1} #{$2}" }
392
+
393
+ css
394
+ end
395
+
396
+ def optimize_background_properties(css)
397
+ # background: none repeat scroll 0 0 color → background: color
398
+ css = css.gsub(/background:\s*none\s+repeat\s+scroll\s+0\s+0\s+([^;}]+)/i) { "background:#{$1}" }
399
+
400
+ # background-position: 0 center → background-position: 0
401
+ css = css.gsub(/background-position:\s*0\s+(?:center|50%)/i) { "background-position:0" }
402
+
403
+ # background-repeat: repeat repeat → background-repeat: repeat
404
+ css = css.gsub(/background-repeat:\s*repeat\s+repeat/i) { "background-repeat:repeat" }
405
+
406
+ css
407
+ end
408
+
409
+ def optimize_border_properties(css)
410
+ # Remove redundant border-width when already specified in border shorthand
411
+ css = css.gsub(/border:\s*(\d+\w*)\s+([^;}]+);\s*border-width:\s*\1/i) { "border:#{$1} #{$2}" }
412
+
413
+ # Remove redundant border-style when already specified
414
+ css = css.gsub(/border:\s*([^;}]*?)\s+(solid|dashed|dotted|double)([^;}]*?);\s*border-style:\s*\2/i) { "border:#{$1} #{$2}#{$3}" }
415
+
416
+ # Remove redundant border-color when already specified
417
+ css = css.gsub(/border:\s*([^;}]*?)\s+(#[0-9a-f]{3,6}|[a-z]+)([^;}]*?);\s*border-color:\s*\2/i) { "border:#{$1} #{$2}#{$3}" }
418
+
419
+ css
420
+ end
421
+
422
+ def optimize_font_properties(css)
423
+ # Font weight optimizations
424
+ css = css.gsub(/font-weight:\s*normal/i) { "font-weight:400" }
425
+ css = css.gsub(/font-weight:\s*bold/i) { "font-weight:700" }
426
+
427
+ # Font style optimizations
428
+ css = css.gsub(/font-style:\s*normal/i) { "font-style:normal" } # Normalize case
429
+
430
+ css
431
+ end
432
+
433
+ def optimize_list_properties(css)
434
+ # list-style: none inside → list-style: none (inside is default for none)
435
+ css = css.gsub(/list-style:\s*none\s+inside/i) { "list-style:none" }
436
+
437
+ css
438
+ end
439
+
440
+ def enhance_zero_value_optimization(css)
441
+ # Advanced zero value and unit optimizations beyond basic YUI compressor
442
+ original_length = css.length
443
+
444
+ # Remove unnecessary zeros in decimal values
445
+ css = optimize_decimal_zeros(css)
446
+
447
+ # Optimize calc() expressions with zeros
448
+ css = optimize_calc_zeros(css)
449
+
450
+ # Advanced unit optimizations
451
+ css = optimize_modern_units(css)
452
+
453
+ # Transform property optimizations
454
+ css = optimize_transform_zeros(css)
455
+
456
+ # Advanced background position optimizations
457
+ css = optimize_position_zeros(css)
458
+
459
+ # Box-shadow and text-shadow optimizations
460
+ css = optimize_shadow_zeros(css)
461
+
462
+ # Update statistics
463
+ chars_saved = original_length - css.length
464
+ @statistics[:properties_optimized] += (chars_saved / 3).to_i # Rough estimate
465
+
466
+ css
467
+ end
468
+
469
+ def optimize_decimal_zeros(css)
470
+ # More aggressive decimal optimization
471
+ css = css.gsub(/(\d)\.0+(?!\d)/) { $1 } # 1.0 → 1
472
+ css = css.gsub(/0+\.(\d+)/) { ".#{$1}" } # 0.5 → .5
473
+ css = css.gsub(/(\d+)\.0*(\d*?)0+(?!\d)/) { "#{$1}.#{$2}".gsub(/\.$/, '') } # 1.500 → 1.5
474
+ css
475
+ end
476
+
477
+ def optimize_calc_zeros(css)
478
+ # Optimize calc() expressions with zeros
479
+ css = css.gsub(/calc\([^)]*\)/) do |calc_expr|
480
+ # Simplify addition/subtraction with zero
481
+ calc_expr = calc_expr.gsub(/\+\s*0\w*/, '') # + 0px → nothing
482
+ calc_expr = calc_expr.gsub(/0\w*\s*\+/, '') # 0px + → nothing
483
+ calc_expr = calc_expr.gsub(/-\s*0\w*/, '') # - 0px → nothing
484
+ calc_expr = calc_expr.gsub(/\*\s*1(?:\.\d*)?/, '') # * 1 → nothing
485
+ calc_expr = calc_expr.gsub(/1(?:\.\d*)?\s*\*/, '') # 1 * → nothing
486
+
487
+ # Clean up extra spaces
488
+ calc_expr.gsub(/\s+/, ' ').strip
489
+ end
490
+ css
491
+ end
492
+
493
+ def optimize_modern_units(css)
494
+ # Optimize newer CSS units where possible
495
+ css = css.gsub(/0(?:ch|rem|em|vw|vh|vmin|vmax|fr)(?!\w)/, '0')
496
+
497
+ # Convert some units where it saves space (commented out for safety)
498
+ # css = css.gsub(/16px/, '1rem') # Only if 1rem saves space in context
499
+
500
+ css
501
+ end
502
+
503
+ def optimize_transform_zeros(css)
504
+ # Transform property optimizations
505
+ css = css.gsub(/translate\(0,\s*0\)/, 'translate(0)') # translate(0, 0) → translate(0)
506
+ css = css.gsub(/translate3d\(0,\s*0,\s*0\)/, 'translate3d(0)') # translate3d(0,0,0) → translate3d(0)
507
+ css = css.gsub(/scale\(1,\s*1\)/, 'scale(1)') # scale(1, 1) → scale(1)
508
+ css = css.gsub(/rotate\(0(?:deg|rad|turn)?\)/, '') # rotate(0deg) → remove
509
+ css = css.gsub(/skew\(0,\s*0\)/, '') # skew(0, 0) → remove
510
+
511
+ css
512
+ end
513
+
514
+ def optimize_position_zeros(css)
515
+ # Advanced background-position and object-position optimizations
516
+ css = css.gsub(/background-position:\s*0\s+0/, 'background-position:0')
517
+ css = css.gsub(/object-position:\s*0\s+0/, 'object-position:0')
518
+ css = css.gsub(/transform-origin:\s*0\s+0/, 'transform-origin:0')
519
+
520
+ css
521
+ end
522
+
523
+ def optimize_shadow_zeros(css)
524
+ # Optimize box-shadow and text-shadow with zeros
525
+ css = css.gsub(/(box-shadow|text-shadow):\s*0\s+0\s+0\s+([^;,}]+)/) { "#{$1}:0 0 #{$2}" }
526
+ css = css.gsub(/(box-shadow|text-shadow):\s*0\s+0\s+([^;,}]+)/) { "#{$1}:0 #{$2}" }
527
+ css = css.gsub(/(box-shadow|text-shadow):\s*0\s+0\s+0\s*(?:;|})/) { "#{$1}:0" }
528
+
529
+ css
530
+ end
531
+
532
+ def optimize_modern_layout_properties(css)
533
+ # Advanced CSS Grid and Flexbox optimizations
534
+ original_length = css.length
535
+
536
+ css = optimize_flexbox_properties(css)
537
+ css = optimize_grid_properties(css)
538
+ css = optimize_alignment_properties(css)
539
+ css = optimize_gap_properties(css)
540
+
541
+ # Update statistics
542
+ chars_saved = original_length - css.length
543
+ @statistics[:properties_optimized] += (chars_saved / 4).to_i # Rough estimate
544
+
545
+ css
546
+ end
547
+
548
+ def optimize_flexbox_properties(css)
549
+ # Flex shorthand optimizations
550
+ css = css.gsub(/flex:\s*1\s+1\s+auto/i, 'flex:1') # flex: 1 1 auto → flex: 1
551
+ css = css.gsub(/flex:\s*0\s+0\s+auto/i, 'flex:none') # flex: 0 0 auto → flex: none
552
+ css = css.gsub(/flex:\s*0\s+1\s+auto/i, 'flex:auto') # flex: 0 1 auto → flex: auto
553
+ css = css.gsub(/flex:\s*(\d+)\s+\1\s+0/i) { "flex:#{$1}" } # flex: 2 2 0 → flex: 2
554
+
555
+ # Flex-direction optimizations
556
+ css = css.gsub(/flex-direction:\s*row/i, 'flex-direction:row') # Normalize case
557
+
558
+ # Justify-content optimizations (use shorter values when supported)
559
+ css = css.gsub(/justify-content:\s*flex-start/i, 'justify-content:start')
560
+ css = css.gsub(/justify-content:\s*flex-end/i, 'justify-content:end')
561
+ css = css.gsub(/align-items:\s*flex-start/i, 'align-items:start')
562
+ css = css.gsub(/align-items:\s*flex-end/i, 'align-items:end')
563
+ css = css.gsub(/align-self:\s*flex-start/i, 'align-self:start')
564
+ css = css.gsub(/align-self:\s*flex-end/i, 'align-self:end')
565
+
566
+ css
567
+ end
568
+
569
+ def optimize_grid_properties(css)
570
+ # Grid shorthand optimizations
571
+ css = css.gsub(/grid-template-columns:\s*repeat\((\d+),\s*1fr\)/i) { "grid-template-columns:repeat(#{$1},1fr)" }
572
+
573
+ # Grid-area optimizations
574
+ css = css.gsub(/grid-area:\s*(\d+)\s*\/\s*(\d+)\s*\/\s*(\d+)\s*\/\s*(\d+)/i) do |match|
575
+ row_start, col_start, row_end, col_end = $1, $2, $3, $4
576
+
577
+ # Optimize common patterns
578
+ if row_start == row_end.to_i - 1 && col_start == col_end.to_i - 1
579
+ # Single cell: grid-area: 1 / 1 / 2 / 2 → grid-area: 1 / 1
580
+ "grid-area:#{row_start}/#{col_start}"
581
+ else
582
+ "grid-area:#{row_start}/#{col_start}/#{row_end}/#{col_end}"
583
+ end
584
+ end
585
+
586
+ # Grid-template optimizations
587
+ css = css.gsub(/grid-template:\s*none\s*\/\s*none/i, 'grid-template:none')
588
+
589
+ # Grid-auto-flow optimizations
590
+ css = css.gsub(/grid-auto-flow:\s*row/i, 'grid-auto-flow:row') # Default, can sometimes be omitted
591
+
592
+ css
593
+ end
594
+
595
+ def optimize_alignment_properties(css)
596
+ # Place-items and place-content shortcuts
597
+ css = css.gsub(/align-items:\s*([^;]+);\s*justify-items:\s*\1/i) { "place-items:#{$1}" }
598
+ css = css.gsub(/align-content:\s*([^;]+);\s*justify-content:\s*\1/i) { "place-content:#{$1}" }
599
+ css = css.gsub(/align-self:\s*([^;]+);\s*justify-self:\s*\1/i) { "place-self:#{$1}" }
600
+
601
+ # Center shorthand
602
+ css = css.gsub(/place-items:\s*center\s+center/i, 'place-items:center')
603
+ css = css.gsub(/place-content:\s*center\s+center/i, 'place-content:center')
604
+
605
+ css
606
+ end
607
+
608
+ def optimize_gap_properties(css)
609
+ # Gap property optimizations
610
+ css = css.gsub(/grid-gap:\s*(\d+\w*)\s+\1/i) { "grid-gap:#{$1}" } # grid-gap: 10px 10px → grid-gap: 10px
611
+ css = css.gsub(/gap:\s*(\d+\w*)\s+\1/i) { "gap:#{$1}" } # gap: 10px 10px → gap: 10px
612
+ css = css.gsub(/row-gap:\s*(\d+\w*);\s*column-gap:\s*\1/i) { "gap:#{$1}" } # Combine identical row/column gaps
613
+
614
+ # Use gap instead of grid-gap (modern syntax)
615
+ css = css.gsub(/grid-gap:/i, 'gap:') # grid-gap → gap (shorter and modern)
616
+ css = css.gsub(/grid-row-gap:/i, 'row-gap:') # grid-row-gap → row-gap
617
+ css = css.gsub(/grid-column-gap:/i, 'column-gap:') # grid-column-gap → column-gap
618
+
619
+ css
620
+ end
621
+
622
+ def compress_css_variables(css)
623
+ # Advanced CSS custom property optimization
624
+ original_length = css.length
625
+
626
+ # Parse variable declarations and usage
627
+ variable_data = analyze_css_variables(css)
628
+
629
+ # Apply optimizations based on analysis
630
+ css = inline_single_use_variables(css, variable_data)
631
+ css = remove_unused_variables(css, variable_data)
632
+ css = optimize_variable_names(css, variable_data)
633
+
634
+ # Update statistics
635
+ chars_saved = original_length - css.length
636
+ @statistics[:properties_optimized] += (chars_saved / 5).to_i # Rough estimate
637
+
638
+ css
639
+ end
640
+
641
+ def analyze_css_variables(css)
642
+ variables = {}
643
+
644
+ # Find all variable declarations with their values
645
+ css.scan(/(--[\w-]+):\s*([^;]+)/) do |var_name, var_value|
646
+ variables[var_name] ||= {
647
+ value: var_value.strip,
648
+ declarations: 0,
649
+ usages: 0,
650
+ total_value_length: 0
651
+ }
652
+ variables[var_name][:declarations] += 1
653
+ variables[var_name][:total_value_length] += var_value.length
654
+ end
655
+
656
+ # Count variable usages
657
+ css.scan(/var\((--[\w-]+)(?:,([^)]*))?\)/) do |var_name, fallback|
658
+ if variables[var_name]
659
+ variables[var_name][:usages] += 1
660
+ variables[var_name][:fallback] = fallback&.strip
661
+ end
662
+ end
663
+
664
+ variables
665
+ end
666
+
667
+ def inline_single_use_variables(css, variable_data)
668
+ # Inline variables that are used only once or twice and have short values
669
+ variables_to_inline = variable_data.select do |var_name, data|
670
+ data[:usages] <= 2 &&
671
+ data[:declarations] == 1 &&
672
+ data[:value].length <= 20 && # Only inline short values
673
+ !data[:value].include?('calc(') && # Don't inline complex calc expressions
674
+ !data[:value].include?('var(') # Don't inline variables that reference other variables
675
+ end
676
+
677
+ variables_to_inline.each do |var_name, data|
678
+ value = data[:value]
679
+
680
+ # Replace var() usages with the actual value
681
+ css = css.gsub(/var\(#{Regexp.escape(var_name)}(?:,[^)]*)?\)/, value)
682
+
683
+ # Remove the variable declaration
684
+ css = css.gsub(/#{Regexp.escape(var_name)}:\s*#{Regexp.escape(value)};?/, '')
685
+ end
686
+
687
+ css
688
+ end
689
+
690
+ def remove_unused_variables(css, variable_data)
691
+ # Remove variables that are declared but never used
692
+ unused_variables = variable_data.select { |var_name, data| data[:usages] == 0 }
693
+
694
+ unused_variables.each do |var_name, data|
695
+ # Remove unused variable declarations
696
+ css = css.gsub(/#{Regexp.escape(var_name)}:\s*[^;]+;?/, '')
697
+ end
698
+
699
+ css
700
+ end
701
+
702
+ def optimize_variable_names(css, variable_data)
703
+ # For frequently used variables with long names, consider shorter aliases
704
+ # This is more conservative - only optimize very long names that are used frequently
705
+
706
+ frequent_long_variables = variable_data.select do |var_name, data|
707
+ data[:usages] >= 3 && var_name.length > 15
708
+ end
709
+
710
+ frequent_long_variables.each_with_index do |(var_name, data), index|
711
+ # Create a shorter name (be careful not to conflict with existing names)
712
+ short_name = "--v#{index + 1}"
713
+
714
+ # Make sure the short name doesn't already exist
715
+ next if css.include?(short_name)
716
+
717
+ # Replace all occurrences of the long variable name
718
+ css = css.gsub(var_name, short_name)
719
+ end
720
+
721
+ css
722
+ end
723
+
724
+ def advanced_color_optimization(css)
725
+ # More aggressive color optimization beyond basic YUI compressor
726
+ color_count_before = css.scan(/#[0-9a-f]{3,6}|rgb\([^)]+\)/i).length
727
+
728
+ # Add HSL to RGB conversion
729
+ css = css.gsub(/hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/i) do
730
+ h, s, l = $1.to_i, $2.to_i / 100.0, $3.to_i / 100.0
731
+ rgb = hsl_to_rgb(h, s, l)
732
+ "rgb(#{rgb.join(',')})"
733
+ end
734
+
735
+ color_count_after = css.scan(/#[0-9a-f]{3,6}|rgb\([^)]+\)/i).length
736
+ @statistics[:colors_converted] += color_count_before - color_count_after
737
+
738
+ css
739
+ end
740
+
741
+ def hsl_to_rgb(h, s, l)
742
+ h = h / 360.0
743
+
744
+ if s == 0
745
+ r = g = b = l
746
+ else
747
+ hue2rgb = lambda do |p, q, t|
748
+ t += 1 if t < 0
749
+ t -= 1 if t > 1
750
+ return p + (q - p) * 6 * t if t < 1.0/6
751
+ return q if t < 1.0/2
752
+ return p + (q - p) * (2.0/3 - t) * 6 if t < 2.0/3
753
+ p
754
+ end
755
+
756
+ q = l < 0.5 ? l * (1 + s) : l + s - l * s
757
+ p = 2 * l - q
758
+ r = hue2rgb.call(p, q, h + 1.0/3)
759
+ g = hue2rgb.call(p, q, h)
760
+ b = hue2rgb.call(p, q, h - 1.0/3)
761
+ end
762
+
763
+ [(r * 255).round, (g * 255).round, (b * 255).round]
764
+ end
765
+
766
+ def calculate_compression_ratio
767
+ return 0.0 if @statistics[:original_size] == 0
768
+ ((@statistics[:original_size] - @statistics[:compressed_size]).to_f / @statistics[:original_size]) * 100
769
+ end
770
+ end
771
+
772
+ # Convenience class methods for enhanced compression
773
+ def self.compress(css, options = {})
774
+ if options.is_a?(Hash) && !options.empty?
775
+ config = Configuration.new
776
+ options.each { |key, value| config.send("#{key}=", value) if config.respond_to?("#{key}=") }
777
+ Compressor.new(config).compress(css, options[:linebreakpos] || 5000)
778
+ else
779
+ # Fallback to original API for backward compatibility
780
+ linebreakpos = options.is_a?(Integer) ? options : 5000
781
+ CssCompressor.compress(css, linebreakpos)
782
+ end
783
+ end
784
+
785
+ def self.compress_with_stats(css, options = {})
786
+ config = Configuration.new
787
+ options.each { |key, value| config.send("#{key}=", value) if config.respond_to?("#{key}=") }
788
+ config.statistics_enabled = true
789
+
790
+ compressor = Compressor.new(config)
791
+ result = compressor.compress(css, options[:linebreakpos] || 5000)
792
+
793
+ {
794
+ compressed_css: result,
795
+ statistics: compressor.statistics
796
+ }
797
+ end
798
+
799
+ # Error class for enhanced features
800
+ class EnhancedCompressionError < StandardError
801
+ attr_reader :original_error
802
+
803
+ def initialize(message, original_error = nil)
804
+ super(message)
805
+ @original_error = original_error
806
+ end
807
+ end
808
+
809
+ # Error class for malformed CSS
810
+ class MalformedCSSError < StandardError
811
+ attr_reader :css_errors
812
+
813
+ def initialize(message, css_errors = [])
814
+ super(message)
815
+ @css_errors = css_errors
816
+ end
817
+ end
818
+ end