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/lib/cssminify2/cssmin.rb
CHANGED
@@ -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
|
-
|
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(
|
200
|
-
css = css.gsub(
|
201
|
-
css = css.gsub(
|
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
|
-
|
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 =
|
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
|