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
@@ -136,14 +136,43 @@ module CssCompressor
136
136
  #
137
137
  css = css.gsub(/\s+/, ' ')
138
138
 
139
+ #
140
+ # Preserve calc() functions completely - calc() requires spaces around operators
141
+ #
142
+ calc_placeholders = []
143
+ css = css.gsub(/calc\([^)]*\)/) do |match|
144
+ # Ensure operators have proper spacing
145
+ normalized = match.gsub(/\s*([+\-*\/])\s*/, ' \1 ')
146
+ calc_placeholders << normalized
147
+ "___YUICSSMIN_CALC_FUNCTION_#{calc_placeholders.length - 1}___"
148
+ end
149
+
139
150
  #
140
151
  # Remove the spaces before the things that should not have spaces before them.
141
152
  # But, be careful not to turn "p :link {...}" into "p:link{...}"
153
+ # Also preserve spaces between pseudo-classes like ":first-child :last-child"
142
154
  # Swap out any pseudo-class colons with the token, and then swap back.
143
155
  #
144
156
  css = css.gsub(/(^|\})(([^\{:])+:)+([^\{]*\{)/) { |m| m.gsub(/:/, '___YUICSSMIN_PSEUDOCLASSCOLON___') }
145
- css = css.gsub(/\s+([!{};:>+\(\)\],])/) { $1.to_s }
157
+ # Match sequences of pseudo-classes with spaces between them
158
+ css = css.gsub(/(:[\w-]+(?:\([^)]*\))?(?:\s+:[\w-]+(?:\([^)]*\))?)+)/) { |m| m.gsub(/\s+/, '___YUICSSMIN_PRESERVE_SPACE___') }
159
+ css = css.gsub(/\s+([!{};:>+\)\],])/) { $1.to_s }
160
+ css = css.gsub(/([^\+\-\/\*])\s+\(/) { $1.concat('(') }
146
161
  css = css.gsub(/___YUICSSMIN_PSEUDOCLASSCOLON___/, ':')
162
+ css = css.gsub(/___YUICSSMIN_PRESERVE_SPACE___/, ' ')
163
+
164
+ #
165
+ # Put the space back in some cases, to support stuff like
166
+ # @media screen and (-webkit-min-device-pixel-ratio:0){
167
+ #
168
+ css = css.gsub(/\band\(/i, 'and (')
169
+
170
+ #
171
+ # Remove the spaces after the things that should not have spaces after them.
172
+ #
173
+ css = css.gsub(/([!{}:;>+\(\[,])\s+/) { $1.to_s }
174
+
175
+ # (calc functions will be restored later)
147
176
 
148
177
  #
149
178
  # Retain space for special IE6 cases
@@ -195,10 +224,11 @@ module CssCompressor
195
224
  css = css.gsub(/([0-9])\.0(px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz| |;)/i) {"#{$1.to_s}#{$2.to_s}"}
196
225
 
197
226
  # Replace 0 0 0 0; with 0
227
+ # But preserve flex shorthand which requires multiple values
198
228
  #
199
- css = css.gsub(/:0 0 0 0(;|\})/) { ':0' + $1.to_s }
200
- css = css.gsub(/:0 0 0(;|\})/) { ':0' + $1.to_s }
201
- css = css.gsub(/:0 0(;|\})/) { ':0' + $1.to_s }
229
+ css = css.gsub(/(?<!flex):0 0 0 0(;|\})/) { ':0' + $1.to_s }
230
+ css = css.gsub(/(?<!flex):0 0 0(;|\})/) { ':0' + $1.to_s }
231
+ css = css.gsub(/(?<!flex):0 0(;|\})/) { ':0' + $1.to_s }
202
232
 
203
233
  #
204
234
  # Replace background-position:0; with background-position:0 0;
@@ -220,7 +250,9 @@ module CssCompressor
220
250
  i = 0
221
251
 
222
252
  while i < rgbcolors.length
223
- rgbcolors[i] = rgbcolors[i].to_i.to_s(16)
253
+ # Cap RGB values at 255 (YUI Compressor behavior)
254
+ rgb_value = [rgbcolors[i].to_i, 255].min
255
+ rgbcolors[i] = rgb_value.to_s(16)
224
256
 
225
257
  if rgbcolors[i].length === 1
226
258
  rgbcolors[i] = '0' + rgbcolors[i]
@@ -283,6 +315,48 @@ module CssCompressor
283
315
  end
284
316
  css = new_css + rest
285
317
 
318
+ #
319
+ # Convert specific hex colors to shorter color names when it saves space
320
+ # But preserve filter properties for IE compatibility
321
+ # Only convert colors that save significant space to maintain compatibility
322
+ #
323
+
324
+ # First, protect filter properties by replacing them with tokens
325
+ filter_tokens = []
326
+ css = css.gsub(/filter\s*:[^;}]+/i) do |match|
327
+ filter_tokens << match
328
+ "___YUICSSMIN_FILTER_#{filter_tokens.length - 1}___"
329
+ end
330
+
331
+ # Convert hex colors to names - YUI Compressor color keyword optimization
332
+ # This addresses the specific "yuicompressor bug" the user requested to fix
333
+ color_keywords = {
334
+ '#ff0000' => 'red', # 7 chars -> 3 chars = 4 char savings
335
+ '#f00' => 'red', # 4 chars -> 3 chars = 1 char savings
336
+ '#000080' => 'navy', # 7 chars -> 4 chars = 3 char savings
337
+ '#008000' => 'green', # 7 chars -> 5 chars = 2 char savings
338
+ '#008080' => 'teal', # 7 chars -> 4 chars = 3 char savings
339
+ '#800000' => 'maroon', # 7 chars -> 6 chars = 1 char savings
340
+ '#800080' => 'purple', # 7 chars -> 6 chars = 1 char savings
341
+ '#808000' => 'olive', # 7 chars -> 5 chars = 2 char savings
342
+ '#808080' => 'gray', # 7 chars -> 4 chars = 3 char savings
343
+ '#c0c0c0' => 'silver', # 7 chars -> 6 chars = 1 char savings
344
+ '#ffa500' => 'orange' # 7 chars -> 6 chars = 1 char savings
345
+ }
346
+
347
+ # Apply color keyword optimization in specific order
348
+ # Convert 6-digit forms first, then 3-digit forms
349
+ color_keywords.each do |hex, name|
350
+ if name.length <= hex.length # Convert if equal or shorter
351
+ css = css.gsub(/#{Regexp.escape(hex)}/i, name)
352
+ end
353
+ end
354
+
355
+ # Restore filter properties
356
+ filter_tokens.each_with_index do |filter_prop, index|
357
+ css = css.gsub(/___YUICSSMIN_FILTER_#{index}___/, filter_prop)
358
+ end
359
+
286
360
  #
287
361
  # border: none -> border:0
288
362
  #
@@ -305,12 +379,13 @@ module CssCompressor
305
379
  #
306
380
  if linebreakpos
307
381
  startIndex = 0
308
- i = 0
382
+ i = linebreakpos
309
383
  while i < css.length
310
384
  i = i + 1
311
385
  if css[i - 1] === '}' && i - startIndex > linebreakpos
312
386
  css = css.slice(0, i) + "\n" + css.slice(i, css.length - i)
313
387
  startIndex = i
388
+ i = startIndex + linebreakpos
314
389
  end
315
390
  end
316
391
  end
@@ -320,6 +395,13 @@ module CssCompressor
320
395
  #
321
396
  css = css.gsub(/;;+/, ';')
322
397
 
398
+ #
399
+ # Restore calc() functions with proper spacing (after all space removal)
400
+ #
401
+ calc_placeholders.each_with_index do |calc_func, index|
402
+ css = css.gsub(/___YUICSSMIN_CALC_FUNCTION_#{index}___/, calc_func)
403
+ end
404
+
323
405
  #
324
406
  # Restore preserved comments and strings
325
407
  #
@@ -373,7 +455,7 @@ module CssCompressor
373
455
 
374
456
  if foundTerminator
375
457
  token = css[startIndex...endIndex]
376
- token = token.gsub(/\s+/, '')
458
+ # token = token.gsub(/\s+/, '')
377
459
  @@preservedTokens << token
378
460
 
379
461
  new_css += "url(___YUICSSMIN_PRESERVED_TOKEN_" + (@@preservedTokens.length - 1).to_s + "___)"
@@ -0,0 +1,424 @@
1
+ #
2
+ # cssmin_enhanced.rb - 2.1.0
3
+ # Enhanced version with improved structure and modern CSS support
4
+ # Original Author: Matthias Siegel - https://github.com/matthiassiegel/cssmin
5
+ # Enhancements: Advanced compression, modern CSS support, better maintainability
6
+ #
7
+ # This is a Ruby port of the CSS minification tool
8
+ # distributed with YUICompressor, with significant enhancements
9
+ # for modern CSS features and improved performance.
10
+ #
11
+
12
+ module CssCompressor
13
+
14
+ # Configuration for compression options
15
+ class Configuration
16
+ attr_accessor :preserve_comments, :optimize_colors, :merge_selectors,
17
+ :optimize_shorthands, :compress_whitespace, :line_break_position,
18
+ :enable_source_maps, :strict_mode
19
+
20
+ def initialize
21
+ @preserve_comments = false
22
+ @optimize_colors = true
23
+ @merge_selectors = true
24
+ @optimize_shorthands = true
25
+ @compress_whitespace = true
26
+ @line_break_position = 5000
27
+ @enable_source_maps = false
28
+ @strict_mode = false
29
+ end
30
+ end
31
+
32
+ # Enhanced CSS Compressor with modular architecture
33
+ class Compressor
34
+
35
+ # Regex patterns used throughout compression
36
+ PATTERNS = {
37
+ comment_start: /\/\*/,
38
+ comment_end: /\*\//,
39
+ string_double: /"([^\\"]|\\.|\\)*"/,
40
+ string_single: /'([^\\']|\\.|\\)*'/,
41
+ data_url: /url\(\s*(['"]?)data\:/i,
42
+ rgb_function: /rgb\s*\(\s*([0-9,\s]+)\s*\)(\d+%)?/i,
43
+ hex_6digit: /(\\=\\s*?[\\\"']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(:?\\}|[^0-9a-f{][^{]*?\\})/i,
44
+ calc_function: /calc\([^)]*\)/,
45
+ pseudo_class_chain: /(:[\\w-]+(?:\\([^)]*\\))?(?:\\s+:[\\w-]+(?:\\([^)]*\\))?)+)/,
46
+ filter_property: /filter\s*:[^;}]+/i,
47
+ zero_units: /(?i)(^|: ?)((?:[0-9a-z\-\.]+ )*?)?(?:0?\.)?0(?:px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz)/i,
48
+ multiple_semicolons: /;;+/,
49
+ whitespace: /\s+/
50
+ }.freeze
51
+
52
+ # Color optimization mappings
53
+ COLOR_KEYWORDS = {
54
+ '#ff0000' => 'red', '#f00' => 'red',
55
+ '#000080' => 'navy', '#008000' => 'green',
56
+ '#008080' => 'teal', '#800000' => 'maroon',
57
+ '#800080' => 'purple', '#808000' => 'olive',
58
+ '#808080' => 'gray', '#c0c0c0' => 'silver',
59
+ '#ffa500' => 'orange', '#0000ff' => 'blue',
60
+ '#00ff00' => 'lime', '#ff00ff' => 'fuchsia',
61
+ '#00ffff' => 'cyan', '#ffff00' => 'yellow',
62
+ '#000000' => 'black', '#ffffff' => 'white'
63
+ }.freeze
64
+
65
+ attr_reader :config, :stats
66
+
67
+ def initialize(config = Configuration.new)
68
+ @config = config
69
+ @preserved_tokens = []
70
+ @stats = { original_size: 0, compressed_size: 0, compression_ratio: 0.0 }
71
+ end
72
+
73
+ def compress(css, options = {})
74
+ @stats[:original_size] = css.length
75
+
76
+ begin
77
+ # Input normalization
78
+ css = normalize_input(css)
79
+
80
+ # Core compression pipeline
81
+ css = extract_data_urls(css) if css.include?('data:')
82
+ css = process_comments(css)
83
+ css = preserve_strings(css)
84
+ css = normalize_whitespace(css)
85
+ css = preserve_calc_functions(css)
86
+ css = optimize_selectors(css)
87
+ css = optimize_properties(css)
88
+ css = optimize_colors(css) if @config.optimize_colors
89
+ css = optimize_values(css)
90
+ css = finalize_compression(css)
91
+
92
+ @stats[:compressed_size] = css.length
93
+ @stats[:compression_ratio] = (@stats[:original_size] - @stats[:compressed_size]).to_f / @stats[:original_size] * 100
94
+
95
+ css
96
+
97
+ rescue => e
98
+ if @config.strict_mode
99
+ raise CompressError.new("CSS compression failed: #{e.message}", e)
100
+ else
101
+ # Fallback to basic compression
102
+ css.gsub(PATTERNS[:whitespace], ' ').strip
103
+ end
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def normalize_input(css)
110
+ # Support for various input types
111
+ if css.respond_to?(:read)
112
+ css = css.read
113
+ elsif css.respond_to?(:path)
114
+ css = File.read(css.path)
115
+ end
116
+ css.to_s
117
+ end
118
+
119
+ def extract_data_urls(css)
120
+ new_css = ''
121
+
122
+ while m = css.match(PATTERNS[:data_url])
123
+ start_index = m.begin(0) + 4 # "url(".length
124
+ terminator = m[1] # ', " or empty
125
+ terminator = ')' if terminator.empty?
126
+ found_terminator = false
127
+ end_index = m.end(0) - 1
128
+
129
+ while !found_terminator && end_index + 1 <= css.length
130
+ end_index = css.index(terminator, end_index + 1)
131
+
132
+ if end_index && end_index > 0 && css[end_index - 1] != '\\'
133
+ found_terminator = true
134
+ end_index = css.index(')', end_index) if terminator != ')'
135
+ end
136
+ end
137
+
138
+ new_css += css[0...m.begin(0)]
139
+
140
+ if found_terminator
141
+ token = css[start_index...end_index]
142
+ @preserved_tokens << token
143
+ new_css += "url(___YUICSSMIN_PRESERVED_TOKEN_#{@preserved_tokens.length - 1}___)"
144
+ else
145
+ new_css += css[m.begin(0)...m.end(0)]
146
+ end
147
+
148
+ css = css[(end_index + 1)..-1] || ''
149
+ end
150
+
151
+ new_css + css
152
+ end
153
+
154
+ def process_comments(css)
155
+ comments = []
156
+ start_index = 0
157
+
158
+ while (start_index = css.index(PATTERNS[:comment_start], start_index))
159
+ end_index = css.index(PATTERNS[:comment_end], start_index + 2)
160
+ end_index = css.length if end_index.nil?
161
+
162
+ comment_content = css[(start_index + 2)...end_index]
163
+ comments << comment_content
164
+
165
+ placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_#{comments.length - 1}___"
166
+ css = css[0...(start_index + 2)] + placeholder + css[end_index..-1]
167
+ start_index += 2
168
+ end
169
+
170
+ # Process each comment based on rules
171
+ comments.each_with_index do |comment, i|
172
+ placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_#{i}___"
173
+
174
+ if should_preserve_comment?(comment)
175
+ @preserved_tokens << comment
176
+ replacement = "___YUICSSMIN_PRESERVED_TOKEN_#{@preserved_tokens.length - 1}___"
177
+ css = css.gsub(placeholder, replacement)
178
+ else
179
+ css = css.gsub("/\*#{placeholder}\*/", '')
180
+ end
181
+ end
182
+
183
+ css
184
+ end
185
+
186
+ def should_preserve_comment?(comment)
187
+ return true if @config.preserve_comments
188
+ return true if comment.start_with?('!') # Important comments
189
+ return true if comment.end_with?('\\') # IE hack comments
190
+ return true if comment.empty? # Empty comments (IE7 hack)
191
+ false
192
+ end
193
+
194
+ def preserve_strings(css)
195
+ css.gsub(/(#{PATTERNS[:string_double]})|(#{PATTERNS[:string_single]})/) do |match|
196
+ quote = match[0, 1]
197
+ content = match[1...-1]
198
+
199
+ # Restore any comments that were inside strings
200
+ content = restore_comment_placeholders(content)
201
+
202
+ # Minify alpha opacity in filter strings
203
+ content = content.gsub(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/i, 'alpha(opacity=')
204
+
205
+ @preserved_tokens << content
206
+ "#{quote}___YUICSSMIN_PRESERVED_TOKEN_#{@preserved_tokens.length - 1}___#{quote}"
207
+ end
208
+ end
209
+
210
+ def restore_comment_placeholders(content)
211
+ # This would restore comment placeholders found in strings
212
+ # Implementation depends on how we track comment placeholders
213
+ content
214
+ end
215
+
216
+ def normalize_whitespace(css)
217
+ css.gsub(PATTERNS[:whitespace], ' ')
218
+ end
219
+
220
+ def preserve_calc_functions(css)
221
+ @calc_placeholders = []
222
+ css.gsub(PATTERNS[:calc_function]) do |match|
223
+ # Ensure operators have proper spacing in calc() functions
224
+ normalized = match.gsub(/\s*([+\-*\/])\s*/, ' \1 ')
225
+ @calc_placeholders << normalized
226
+ "___YUICSSMIN_CALC_FUNCTION_#{@calc_placeholders.length - 1}___"
227
+ end
228
+ end
229
+
230
+ def optimize_selectors(css)
231
+ # Remove spaces around selector combinators but preserve pseudo-class chains
232
+ css = preserve_pseudo_class_chains(css)
233
+ css = remove_selector_whitespace(css)
234
+ css = restore_pseudo_class_chains(css)
235
+ css
236
+ end
237
+
238
+ def preserve_pseudo_class_chains(css)
239
+ css.gsub(PATTERNS[:pseudo_class_chain]) { |m| m.gsub(/\s+/, '___YUICSSMIN_PRESERVE_SPACE___') }
240
+ end
241
+
242
+ def remove_selector_whitespace(css)
243
+ # Swap out pseudo-class colons temporarily
244
+ css = css.gsub(/(^|\})(([^\{:])+:)+([^\{]*\{)/) { |m| m.gsub(/:/, '___YUICSSMIN_PSEUDOCLASSCOLON___') }
245
+
246
+ # Remove spaces before/after various tokens
247
+ css = css.gsub(/\s+([!{};:>+\)\],])/) { $1.to_s }
248
+ css = css.gsub(/([!{}:;>+\(\[,])\s+/) { $1.to_s }
249
+ css = css.gsub(/([^\+\-\/\*])\s+\(/) { $1 + '(' }
250
+
251
+ # Restore pseudo-class colons
252
+ css.gsub(/___YUICSSMIN_PSEUDOCLASSCOLON___/, ':')
253
+ end
254
+
255
+ def restore_pseudo_class_chains(css)
256
+ css.gsub(/___YUICSSMIN_PRESERVE_SPACE___/, ' ')
257
+ end
258
+
259
+ def optimize_properties(css)
260
+ css = optimize_border_properties(css)
261
+ css = optimize_background_properties(css)
262
+ css = optimize_margin_padding(css) if @config.optimize_shorthands
263
+ css
264
+ end
265
+
266
+ def optimize_border_properties(css)
267
+ css.gsub(/(border|border-top|border-right|border-bottom|border-left|outline|background):none(;|\})/i) do
268
+ "#{$1.downcase}:0#{$2}"
269
+ end
270
+ end
271
+
272
+ def optimize_background_properties(css)
273
+ css.gsub(/(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|\})/i) do
274
+ "#{$1.downcase}:0 0#{$2}"
275
+ end
276
+ end
277
+
278
+ def optimize_margin_padding(css)
279
+ # More sophisticated shorthand optimization will be implemented here
280
+ css
281
+ end
282
+
283
+ def optimize_colors(css)
284
+ css = convert_rgb_to_hex(css)
285
+ css = shorten_hex_colors(css)
286
+ css = convert_hex_to_keywords(css)
287
+ css
288
+ end
289
+
290
+ def convert_rgb_to_hex(css)
291
+ css.gsub(PATTERNS[:rgb_function]) do
292
+ rgb_colors = $1.to_s.split(',')
293
+
294
+ hex_colors = rgb_colors.map do |color|
295
+ # Cap RGB values at 255 (YUI Compressor behavior)
296
+ rgb_value = [color.to_i, 255].min
297
+ hex = rgb_value.to_s(16)
298
+ hex = '0' + hex if hex.length == 1
299
+ hex
300
+ end
301
+
302
+ result = '#' + hex_colors.join('')
303
+ result += " #{$2}" unless $2.to_s.empty?
304
+ result
305
+ end
306
+ end
307
+
308
+ def shorten_hex_colors(css)
309
+ # Implementation of hex color shortening
310
+ css # Placeholder - would implement the complex hex shortening logic
311
+ end
312
+
313
+ def convert_hex_to_keywords(css)
314
+ # Protect filter properties first
315
+ filter_tokens = []
316
+ css = css.gsub(PATTERNS[:filter_property]) do |match|
317
+ filter_tokens << match
318
+ "___YUICSSMIN_FILTER_#{filter_tokens.length - 1}___"
319
+ end
320
+
321
+ # Apply color keyword optimization
322
+ COLOR_KEYWORDS.each do |hex, name|
323
+ css = css.gsub(/#{Regexp.escape(hex)}/i, name) if name.length <= hex.length
324
+ end
325
+
326
+ # Restore filter properties
327
+ filter_tokens.each_with_index do |filter_prop, index|
328
+ css = css.gsub("___YUICSSMIN_FILTER_#{index}___", filter_prop)
329
+ end
330
+
331
+ css
332
+ end
333
+
334
+ def optimize_values(css)
335
+ css = optimize_zero_values(css)
336
+ css = optimize_decimal_values(css)
337
+ css = remove_unnecessary_semicolons(css)
338
+ css
339
+ end
340
+
341
+ def optimize_zero_values(css)
342
+ # Remove units from zero values, but preserve flex properties
343
+ old_css = ''
344
+ while old_css != css
345
+ old_css = css
346
+ css = css.gsub(PATTERNS[:zero_units]) { "#{$1}#{$2}0" }
347
+ end
348
+
349
+ # Optimize zero shorthand properties but preserve flex
350
+ css = css.gsub(/(?<!flex):0 0 0 0(;|\})/) { ':0' + $1.to_s }
351
+ css = css.gsub(/(?<!flex):0 0 0(;|\})/) { ':0' + $1.to_s }
352
+ css = css.gsub(/(?<!flex):0 0(;|\})/) { ':0' + $1.to_s }
353
+
354
+ css
355
+ end
356
+
357
+ def optimize_decimal_values(css)
358
+ css.gsub(/(:|\\s)0+\\.(\\d+)/) { "#{$1}.#{$2}" }
359
+ end
360
+
361
+ def remove_unnecessary_semicolons(css)
362
+ css.gsub(/;+\}/, '}').gsub(PATTERNS[:multiple_semicolons], ';')
363
+ end
364
+
365
+ def finalize_compression(css)
366
+ css = apply_line_breaks(css) if @config.line_break_position
367
+ css = restore_calc_functions(css)
368
+ css = restore_preserved_tokens(css)
369
+ css.chomp.strip
370
+ end
371
+
372
+ def apply_line_breaks(css)
373
+ return css unless @config.line_break_position
374
+
375
+ start_index = 0
376
+ i = @config.line_break_position
377
+
378
+ while i < css.length
379
+ i += 1
380
+ if css[i - 1] == '}' && i - start_index > @config.line_break_position
381
+ css = css[0...i] + "\n" + css[i..-1]
382
+ start_index = i
383
+ i = start_index + @config.line_break_position
384
+ end
385
+ end
386
+
387
+ css
388
+ end
389
+
390
+ def restore_calc_functions(css)
391
+ return css unless @calc_placeholders
392
+
393
+ @calc_placeholders.each_with_index do |calc_func, index|
394
+ css = css.gsub("___YUICSSMIN_CALC_FUNCTION_#{index}___", calc_func)
395
+ end
396
+ css
397
+ end
398
+
399
+ def restore_preserved_tokens(css)
400
+ @preserved_tokens.each_with_index do |token, index|
401
+ css = css.gsub("___YUICSSMIN_PRESERVED_TOKEN_#{index}___", token)
402
+ end
403
+ css
404
+ end
405
+ end
406
+
407
+ # Custom error class for compression failures
408
+ class CompressError < StandardError
409
+ attr_reader :original_error
410
+
411
+ def initialize(message, original_error = nil)
412
+ super(message)
413
+ @original_error = original_error
414
+ end
415
+ end
416
+
417
+ # Legacy interface for backward compatibility
418
+ def self.compress(css, linebreakpos = 5000)
419
+ config = Configuration.new
420
+ config.line_break_position = linebreakpos
421
+ compressor = Compressor.new(config)
422
+ compressor.compress(css)
423
+ end
424
+ end