kajabi-css_parser 1.2.7

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.
@@ -0,0 +1,92 @@
1
+ module CssParser
2
+
3
+
4
+ def self.regex_possible_values *values
5
+ Regexp.new("([\s]*^)?(#{values.join('|')})([\s]*$)?", 'i')
6
+ end
7
+
8
+ # :stopdoc:
9
+ # Base types
10
+ RE_NL = Regexp.new('(\n|\r\n|\r|\f)')
11
+ RE_NON_ASCII = Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE, 'n') #[^\0-\177]
12
+ RE_UNICODE = Regexp.new('(\\\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])*)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n')
13
+ RE_ESCAPE = Regexp.union(RE_UNICODE, '|(\\\\[^\n\r\f0-9a-f])')
14
+ RE_IDENT = Regexp.new("[\-]?([_a-z]|#{RE_NON_ASCII}|#{RE_ESCAPE})([_a-z0-9\-]|#{RE_NON_ASCII}|#{RE_ESCAPE})*", Regexp::IGNORECASE, 'n')
15
+
16
+ # General strings
17
+ RE_STRING1 = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')
18
+ RE_STRING2 = Regexp.new('(\'(.[^\n\r\f\\\']*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\')')
19
+ RE_STRING = Regexp.union(RE_STRING1, RE_STRING2)
20
+
21
+ RE_INHERIT = regex_possible_values 'inherit'
22
+
23
+ RE_URI = Regexp.new('(url\([\s]*([\s]*' + RE_STRING.to_s + '[\s]*)[\s]*\))|(url\([\s]*([!#$%&*\-~]|' + RE_NON_ASCII.to_s + '|' + RE_ESCAPE.to_s + ')*[\s]*)\)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n')
24
+ URI_RX = /url\(("([^"]*)"|'([^']*)'|([^)]*))\)/im
25
+
26
+ # Initial parsing
27
+ RE_AT_IMPORT_RULE = /\@import[\s]+(url\()?["''"]?(.[^'"\s"']*)["''"]?\)?([\w\s\,^\])]*)\)?;?/
28
+
29
+ #--
30
+ #RE_AT_MEDIA_RULE = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')
31
+
32
+ #RE_AT_IMPORT_RULE = Regexp.new('@import[\s]*(' + RE_STRING.to_s + ')([\w\s\,]*)[;]?', Regexp::IGNORECASE) -- should handle url() even though it is not allowed
33
+ #++
34
+ IMPORTANT_IN_PROPERTY_RX = /[\s]*!important[\s]*/i
35
+
36
+ RE_INSIDE_OUTSIDE = regex_possible_values 'inside', 'outside'
37
+ RE_SCROLL_FIXED = regex_possible_values 'scroll', 'fixed'
38
+ RE_REPEAT = regex_possible_values 'repeat(\-x|\-y)*|no\-repeat'
39
+ RE_LIST_STYLE_TYPE = regex_possible_values 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', 'lower-roman',
40
+ 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha',
41
+ 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana',
42
+ 'hira-gana-iroha', 'katakana-iroha', 'katakana', 'none'
43
+
44
+ STRIP_CSS_COMMENTS_RX = /\/\*.*?\*\//m
45
+ STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m
46
+
47
+ # Special units
48
+ BOX_MODEL_UNITS_RX = /(auto|inherit|0|([\-]*([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%)))([\s;]|\Z)/imx
49
+ RE_LENGTH_OR_PERCENTAGE = Regexp.new('([\-]*(([0-9]*\.[0-9]+)|[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%))', Regexp::IGNORECASE)
50
+ RE_BACKGROUND_POSITION = Regexp.new("((((#{RE_LENGTH_OR_PERCENTAGE})|left|center|right|top|bottom)[\s]*){1,2})", Regexp::IGNORECASE | Regexp::EXTENDED)
51
+ FONT_UNITS_RX = /(([x]+\-)*small|medium|large[r]*|auto|inherit|([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%)*)/i
52
+ RE_BORDER_STYLE = /([\s]*^)?(none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset)([\s]*$)?/imx
53
+ RE_BORDER_UNITS = Regexp.union(BOX_MODEL_UNITS_RX, /(thin|medium|thick)/i)
54
+
55
+
56
+ # Patterns for specificity calculations
57
+ NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX= /
58
+ (\.[\w]+) # classes
59
+ |
60
+ \[(\w+) # attributes
61
+ |
62
+ (\:( # pseudo classes
63
+ link|visited|active
64
+ |hover|focus
65
+ |lang
66
+ |target
67
+ |enabled|disabled|checked|indeterminate
68
+ |root
69
+ |nth-child|nth-last-child|nth-of-type|nth-last-of-type
70
+ |first-child|last-child|first-of-type|last-of-type
71
+ |only-child|only-of-type
72
+ |empty|contains
73
+ ))
74
+ /ix
75
+ ELEMENTS_AND_PSEUDO_ELEMENTS_RX = /
76
+ ((^|[\s\+\>\~]+)[\w]+ # elements
77
+ |
78
+ \:{1,2}( # pseudo-elements
79
+ after|before
80
+ |first-letter|first-line
81
+ |selection
82
+ )
83
+ )/ix
84
+
85
+ # Colours
86
+ RE_COLOUR_NUMERIC = Regexp.new('((hsl|rgb)[\s]*\([\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*\))', Regexp::IGNORECASE)
87
+ RE_COLOUR_NUMERIC_ALPHA = Regexp.new('((hsla|rgba)[\s]*\([\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*\))', Regexp::IGNORECASE)
88
+ RE_COLOUR_HEX = /(#([0-9a-f]{6}|[0-9a-f]{3})([\s;]|$))/i
89
+ RE_COLOUR_NAMED = /([\s]*^)?(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|transparent)([\s]*$)?/i
90
+ RE_COLOUR = Regexp.union(RE_COLOUR_NUMERIC, RE_COLOUR_NUMERIC_ALPHA, RE_COLOUR_HEX, RE_COLOUR_NAMED)
91
+ # :startdoc:
92
+ end
@@ -0,0 +1,486 @@
1
+ module CssParser
2
+ class RuleSet
3
+ # Patterns for specificity calculations
4
+ RE_ELEMENTS_AND_PSEUDO_ELEMENTS = /((^|[\s\+\>]+)[\w]+|\:(first\-line|first\-letter|before|after))/i
5
+ RE_NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES = /(\.[\w]+)|(\[[\w]+)|(\:(link|first\-child|lang))/i
6
+
7
+ BACKGROUND_PROPERTIES = ['background-color', 'background-image', 'background-repeat', 'background-position', 'background-attachment']
8
+ LIST_STYLE_PROPERTIES = ['list-style-type', 'list-style-position', 'list-style-image']
9
+
10
+ # Array of selector strings.
11
+ attr_reader :selectors
12
+
13
+ # Integer with the specificity to use for this RuleSet.
14
+ attr_accessor :specificity
15
+
16
+ def initialize(selectors, block, specificity = nil)
17
+ @selectors = []
18
+ @specificity = specificity
19
+ @declarations = {}
20
+ @order = 0
21
+ parse_selectors!(selectors) if selectors
22
+ parse_declarations!(block)
23
+ end
24
+
25
+
26
+ # Get the value of a property
27
+ def get_value(property)
28
+ return '' unless property and not property.empty?
29
+
30
+ property = property.downcase.strip
31
+ properties = @declarations.inject('') do |val, (key, data)|
32
+ #puts "COMPARING #{key} #{key.inspect} against #{property} #{property.inspect}"
33
+ importance = data[:is_important] ? ' !important' : ''
34
+ val << "#{data[:value]}#{importance}; " if key.downcase.strip == property
35
+ val
36
+ end
37
+ return properties ? properties.strip : ''
38
+ end
39
+ alias_method :[], :get_value
40
+
41
+ # Add a CSS declaration to the current RuleSet.
42
+ #
43
+ # rule_set.add_declaration!('color', 'blue')
44
+ #
45
+ # puts rule_set['color']
46
+ # => 'blue;'
47
+ #
48
+ # rule_set.add_declaration!('margin', '0px auto !important')
49
+ #
50
+ # puts rule_set['margin']
51
+ # => '0px auto !important;'
52
+ #
53
+ # If the property already exists its value will be over-written.
54
+ def add_declaration!(property, value)
55
+ if value.nil? or value.empty?
56
+ @declarations.delete(property)
57
+ return
58
+ end
59
+
60
+ value.gsub!(/;\Z/, '')
61
+ is_important = !value.gsub!(CssParser::IMPORTANT_IN_PROPERTY_RX, '').nil?
62
+ property = property.downcase.strip
63
+ #puts "SAVING #{property} #{value} #{is_important.inspect}"
64
+ @declarations[property] = {
65
+ :value => value, :is_important => is_important, :order => @order += 1
66
+ }
67
+ end
68
+ alias_method :[]=, :add_declaration!
69
+
70
+ # Remove CSS declaration from the current RuleSet.
71
+ #
72
+ # rule_set.remove_declaration!('color')
73
+ def remove_declaration!(property)
74
+ @declarations.delete(property)
75
+ end
76
+
77
+ # Iterate through selectors.
78
+ #
79
+ # Options
80
+ # - +force_important+ -- boolean
81
+ #
82
+ # ==== Example
83
+ # ruleset.each_selector do |sel, dec, spec|
84
+ # ...
85
+ # end
86
+ def each_selector(options = {}) # :yields: selector, declarations, specificity
87
+ declarations = declarations_to_s(options)
88
+ if @specificity
89
+ @selectors.each { |sel| yield sel.strip, declarations, @specificity }
90
+ else
91
+ @selectors.each { |sel| yield sel.strip, declarations, CssParser.calculate_specificity(sel) }
92
+ end
93
+ end
94
+
95
+ # Iterate through declarations.
96
+ def each_declaration # :yields: property, value, is_important
97
+ decs = @declarations.sort { |a,b| a[1][:order].nil? || b[1][:order].nil? ? 0 : a[1][:order] <=> b[1][:order] }
98
+ decs.each do |property, data|
99
+ value = data[:value]
100
+ yield property.downcase.strip, value.strip, data[:is_important]
101
+ end
102
+ end
103
+
104
+ # Return all declarations as a string.
105
+ #--
106
+ # TODO: Clean-up regexp doesn't seem to work
107
+ #++
108
+ def declarations_to_s(options = {})
109
+ options = {:force_important => false}.merge(options)
110
+ str = ''
111
+ each_declaration do |prop, val, is_important|
112
+ importance = (options[:force_important] || is_important) ? ' !important' : ''
113
+ str += "#{prop}: #{val}#{importance}; "
114
+ end
115
+ str.gsub(/^[\s^(\{)]+|[\n\r\f\t]*|[\s]+$/mx, '').strip
116
+ end
117
+
118
+ # Return the CSS rule set as a string.
119
+ def to_s
120
+ decs = declarations_to_s
121
+ "#{@selectors.join(',')} { #{decs} }"
122
+ end
123
+
124
+ # Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
125
+ def expand_shorthand!
126
+ # border must be expanded before dimensions
127
+ expand_border_shorthand!
128
+ expand_dimensions_shorthand!
129
+ expand_font_shorthand!
130
+ expand_background_shorthand!
131
+ expand_list_style_shorthand!
132
+ end
133
+
134
+ # Convert shorthand background declarations (e.g. <tt>background: url("chess.png") gray 50% repeat fixed;</tt>)
135
+ # into their constituent parts.
136
+ #
137
+ # See http://www.w3.org/TR/CSS21/colors.html#propdef-background
138
+ def expand_background_shorthand! # :nodoc:
139
+ return unless @declarations.has_key?('background')
140
+
141
+ value = @declarations['background'][:value]
142
+
143
+ if value =~ CssParser::RE_INHERIT
144
+ BACKGROUND_PROPERTIES.each do |prop|
145
+ split_declaration('background', prop, 'inherit')
146
+ end
147
+ end
148
+
149
+ split_declaration('background', 'background-image', value.slice!(Regexp.union(CssParser::URI_RX, /none/i)))
150
+ split_declaration('background', 'background-attachment', value.slice!(CssParser::RE_SCROLL_FIXED))
151
+ split_declaration('background', 'background-repeat', value.slice!(CssParser::RE_REPEAT))
152
+ split_declaration('background', 'background-color', value.slice!(CssParser::RE_COLOUR))
153
+ split_declaration('background', 'background-position', value.slice(CssParser::RE_BACKGROUND_POSITION))
154
+
155
+ @declarations.delete('background')
156
+ end
157
+
158
+ # Split shorthand border declarations (e.g. <tt>border: 1px red;</tt>)
159
+ # Additional splitting happens in expand_dimensions_shorthand!
160
+ def expand_border_shorthand! # :nodoc:
161
+ ['border', 'border-left', 'border-right', 'border-top', 'border-bottom'].each do |k|
162
+ next unless @declarations.has_key?(k)
163
+
164
+ value = @declarations[k][:value]
165
+
166
+ split_declaration(k, "#{k}-width", value.slice!(CssParser::RE_BORDER_UNITS))
167
+ split_declaration(k, "#{k}-color", value.slice!(CssParser::RE_COLOUR))
168
+ split_declaration(k, "#{k}-style", value.slice!(CssParser::RE_BORDER_STYLE))
169
+
170
+ @declarations.delete(k)
171
+ end
172
+ end
173
+
174
+ # Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)
175
+ # into their constituent parts. Handles margin, padding, border-color, border-style and border-width.
176
+ def expand_dimensions_shorthand! # :nodoc:
177
+ {'margin' => 'margin-%s',
178
+ 'padding' => 'padding-%s',
179
+ 'border-color' => 'border-%s-color',
180
+ 'border-style' => 'border-%s-style',
181
+ 'border-width' => 'border-%s-width'}.each do |property, expanded|
182
+
183
+ next unless @declarations.has_key?(property)
184
+
185
+ value = @declarations[property][:value]
186
+
187
+ # RGB and HSL values in borders are the only units that can have spaces (within params).
188
+ # We cheat a bit here by stripping spaces after commas in RGB and HSL values so that we
189
+ # can split easily on spaces.
190
+ #
191
+ # TODO: rgba, hsl, hsla
192
+ value.gsub!(RE_COLOUR) { |c| c.gsub(/[\s]+/, '') }
193
+
194
+ matches = value.strip.split(/[\s]+/)
195
+
196
+ t, r, b, l = nil
197
+
198
+ case matches.length
199
+ when 1
200
+ t, r, b, l = matches[0], matches[0], matches[0], matches[0]
201
+ when 2
202
+ t, b = matches[0], matches[0]
203
+ r, l = matches[1], matches[1]
204
+ when 3
205
+ t = matches[0]
206
+ r, l = matches[1], matches[1]
207
+ b = matches[2]
208
+ when 4
209
+ t = matches[0]
210
+ r = matches[1]
211
+ b = matches[2]
212
+ l = matches[3]
213
+ end
214
+
215
+ split_declaration(property, expanded % 'top', t)
216
+ split_declaration(property, expanded % 'right', r)
217
+ split_declaration(property, expanded % 'bottom', b)
218
+ split_declaration(property, expanded % 'left', l)
219
+
220
+ @declarations.delete(property)
221
+ end
222
+ end
223
+
224
+ # Convert shorthand font declarations (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)
225
+ # into their constituent parts.
226
+ def expand_font_shorthand! # :nodoc:
227
+ return unless @declarations.has_key?('font')
228
+
229
+ font_props = {}
230
+
231
+ # reset properties to 'normal' per http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
232
+ ['font-style', 'font-variant', 'font-weight', 'font-size',
233
+ 'line-height'].each do |prop|
234
+ font_props[prop] = 'normal'
235
+ end
236
+
237
+ value = @declarations['font'][:value]
238
+ is_important = @declarations['font'][:is_important]
239
+ order = @declarations['font'][:order]
240
+
241
+ in_fonts = false
242
+
243
+ matches = value.scan(/("(.*[^"])"|'(.*[^'])'|(\w[^ ,]+))/)
244
+ matches.each do |match|
245
+ m = match[0].to_s.strip
246
+ m.gsub!(/[;]$/, '')
247
+
248
+ if in_fonts
249
+ if font_props.has_key?('font-family')
250
+ font_props['font-family'] += ', ' + m
251
+ else
252
+ font_props['font-family'] = m
253
+ end
254
+ elsif m =~ /normal|inherit/i
255
+ ['font-style', 'font-weight', 'font-variant'].each do |font_prop|
256
+ font_props[font_prop] = m unless font_props.has_key?(font_prop)
257
+ end
258
+ elsif m =~ /italic|oblique/i
259
+ font_props['font-style'] = m
260
+ elsif m =~ /small\-caps/i
261
+ font_props['font-variant'] = m
262
+ elsif m =~ /[1-9]00$|bold|bolder|lighter/i
263
+ font_props['font-weight'] = m
264
+ elsif m =~ CssParser::FONT_UNITS_RX
265
+ if m =~ /\//
266
+ font_props['font-size'], font_props['line-height'] = m.split('/')
267
+ else
268
+ font_props['font-size'] = m
269
+ end
270
+ in_fonts = true
271
+ end
272
+ end
273
+
274
+ font_props.each { |font_prop, font_val| @declarations[font_prop] = {:value => font_val, :is_important => is_important, :order => order} }
275
+
276
+ @declarations.delete('font')
277
+ end
278
+
279
+ # Convert shorthand list-style declarations (e.g. <tt>list-style: lower-alpha outside;</tt>)
280
+ # into their constituent parts.
281
+ #
282
+ # See http://www.w3.org/TR/CSS21/generate.html#lists
283
+ def expand_list_style_shorthand! # :nodoc:
284
+ return unless @declarations.has_key?('list-style')
285
+
286
+ value = @declarations['list-style'][:value]
287
+
288
+ if value =~ CssParser::RE_INHERIT
289
+ LIST_STYLE_PROPERTIES.each do |prop|
290
+ split_declaration('list-style', prop, 'inherit')
291
+ end
292
+ end
293
+
294
+ split_declaration('list-style', 'list-style-type', value.slice!(CssParser::RE_LIST_STYLE_TYPE))
295
+ split_declaration('list-style', 'list-style-position', value.slice!(CssParser::RE_INSIDE_OUTSIDE))
296
+ split_declaration('list-style', 'list-style-image', value.slice!(Regexp.union(CssParser::URI_RX, /none/i)))
297
+
298
+ @declarations.delete('list-style')
299
+ end
300
+
301
+ # Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
302
+ def create_shorthand!
303
+ create_background_shorthand!
304
+ create_dimensions_shorthand!
305
+ # border must be shortened after dimensions
306
+ create_border_shorthand!
307
+ create_font_shorthand!
308
+ create_list_style_shorthand!
309
+ end
310
+
311
+ # Combine several properties into a shorthand one
312
+ def create_shorthand_properties! properties, shorthand_property # :nodoc:
313
+ values = []
314
+ properties.each do |property|
315
+ if @declarations.has_key?(property) and not @declarations[property][:is_important]
316
+ values << @declarations[property][:value]
317
+ @declarations.delete(property)
318
+ end
319
+ end
320
+
321
+ unless values.empty?
322
+ @declarations[shorthand_property] = {:value => values.join(' ')}
323
+ end
324
+ end
325
+
326
+ # Looks for long format CSS background properties (e.g. <tt>background-color</tt>) and
327
+ # converts them into a shorthand CSS <tt>background</tt> property.
328
+ #
329
+ # Leaves properties declared !important alone.
330
+ def create_background_shorthand! # :nodoc:
331
+ create_shorthand_properties! BACKGROUND_PROPERTIES, 'background'
332
+ end
333
+
334
+ # Combine border-color, border-style and border-width into border
335
+ # Should be run after create_dimensions_shorthand!
336
+ #
337
+ # TODO: this is extremely similar to create_background_shorthand! and should be combined
338
+ def create_border_shorthand! # :nodoc:
339
+ values = []
340
+
341
+ ['border-width', 'border-style', 'border-color'].each do |property|
342
+ if @declarations.has_key?(property) and not @declarations[property][:is_important]
343
+ # can't merge if any value contains a space (i.e. has multiple values)
344
+ # we temporarily remove any spaces after commas for the check (inside rgba, etc...)
345
+ return if @declarations[property][:value].gsub(/\,[\s]/, ',').strip =~ /[\s]/
346
+ values << @declarations[property][:value]
347
+ end
348
+ end
349
+
350
+ @declarations.delete('border-width')
351
+ @declarations.delete('border-style')
352
+ @declarations.delete('border-color')
353
+
354
+ unless values.empty?
355
+ @declarations['border'] = {:value => values.join(' ')}
356
+ end
357
+ end
358
+
359
+ # Looks for long format CSS dimensional properties (margin, padding, border-color, border-style and border-width)
360
+ # and converts them into shorthand CSS properties.
361
+ def create_dimensions_shorthand! # :nodoc:
362
+ directions = ['top', 'right', 'bottom', 'left']
363
+
364
+ {'margin' => 'margin-%s',
365
+ 'padding' => 'padding-%s',
366
+ 'border-color' => 'border-%s-color',
367
+ 'border-style' => 'border-%s-style',
368
+ 'border-width' => 'border-%s-width'}.each do |property, expanded|
369
+
370
+ foldable = @declarations.select do |dim, val|
371
+ dim == expanded % 'top' or dim == expanded % 'right' or dim == expanded % 'bottom' or dim == expanded % 'left'
372
+ end
373
+ # All four dimensions must be present
374
+ if foldable.length == 4
375
+ values = {}
376
+
377
+ directions.each { |d| values[d.to_sym] = @declarations[expanded % d][:value].downcase.strip }
378
+
379
+ if values[:left] == values[:right]
380
+ if values[:top] == values[:bottom]
381
+ if values[:top] == values[:left] # All four sides are equal
382
+ new_value = values[:top]
383
+ else # Top and bottom are equal, left and right are equal
384
+ new_value = values[:top] + ' ' + values[:left]
385
+ end
386
+ else # Only left and right are equal
387
+ new_value = values[:top] + ' ' + values[:left] + ' ' + values[:bottom]
388
+ end
389
+ else # No sides are equal
390
+ new_value = values[:top] + ' ' + values[:right] + ' ' + values[:bottom] + ' ' + values[:left]
391
+ end
392
+
393
+ new_value.strip!
394
+ @declarations[property] = {:value => new_value.strip} unless new_value.empty?
395
+
396
+ # Delete the longhand values
397
+ directions.each { |d| @declarations.delete(expanded % d) }
398
+ end
399
+ end
400
+ end
401
+
402
+
403
+ # Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and
404
+ # tries to convert them into a shorthand CSS <tt>font</tt> property. All
405
+ # font properties must be present in order to create a shorthand declaration.
406
+ def create_font_shorthand! # :nodoc:
407
+ ['font-style', 'font-variant', 'font-weight', 'font-size',
408
+ 'line-height', 'font-family'].each do |prop|
409
+ return unless @declarations.has_key?(prop)
410
+ end
411
+
412
+ new_value = ''
413
+ ['font-style', 'font-variant', 'font-weight'].each do |property|
414
+ unless @declarations[property][:value] == 'normal'
415
+ new_value += @declarations[property][:value] + ' '
416
+ end
417
+ end
418
+
419
+ new_value += @declarations['font-size'][:value]
420
+
421
+ unless @declarations['line-height'][:value] == 'normal'
422
+ new_value += '/' + @declarations['line-height'][:value]
423
+ end
424
+
425
+ new_value += ' ' + @declarations['font-family'][:value]
426
+
427
+ @declarations['font'] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
428
+
429
+ ['font-style', 'font-variant', 'font-weight', 'font-size',
430
+ 'line-height', 'font-family'].each do |prop|
431
+ @declarations.delete(prop)
432
+ end
433
+
434
+ end
435
+
436
+ # Looks for long format CSS list-style properties (e.g. <tt>list-style-type</tt>) and
437
+ # converts them into a shorthand CSS <tt>list-style</tt> property.
438
+ #
439
+ # Leaves properties declared !important alone.
440
+ def create_list_style_shorthand! # :nodoc:
441
+ create_shorthand_properties! LIST_STYLE_PROPERTIES, 'list-style'
442
+ end
443
+
444
+ private
445
+
446
+ # utility method for re-assign shorthand elements to longhand versions
447
+ def split_declaration(src, dest, v) # :nodoc:
448
+ return unless v and not v.empty?
449
+
450
+ if @declarations.has_key?(dest)
451
+ #puts "dest #{dest} already exists"
452
+
453
+ if @declarations[dest][:order] > @declarations[src][:order]
454
+ #puts "skipping #{dest}:#{v} due to order "
455
+ return
456
+ else
457
+ @declarations[dest] = {}
458
+ end
459
+ end
460
+ @declarations[dest] = @declarations[src].merge({:value => v.to_s.strip})
461
+ end
462
+
463
+ def parse_declarations!(block) # :nodoc:
464
+ @declarations = {}
465
+
466
+ return unless block
467
+
468
+ block.gsub!(/(^[\s]*)|([\s]*$)/, '')
469
+
470
+ block.split(/[\;$]+/m).each do |decs|
471
+ if matches = decs.match(/(.[^:]*)\:(.[^;]*)(;|\Z)/i)
472
+ property, value, end_of_declaration = matches.captures
473
+
474
+ add_declaration!(property, value)
475
+ end
476
+ end
477
+ end
478
+
479
+ #--
480
+ # TODO: way too simplistic
481
+ #++
482
+ def parse_selectors!(selectors) # :nodoc:
483
+ @selectors = selectors.split(',')
484
+ end
485
+ end
486
+ end