css_parser_master 1.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,388 @@
1
+ module CssParserMaster
2
+ # Exception class used for any errors encountered while downloading remote files.
3
+ class RemoteFileError < IOError; end
4
+
5
+ # Exception class used if a request is made to load a CSS file more than once.
6
+ class CircularReferenceError < StandardError; end
7
+
8
+
9
+ # == Parser class
10
+ #
11
+ # All CSS is converted to UTF-8.
12
+ #
13
+ # When calling Parser#new there are some configuaration options:
14
+ # [<tt>absolute_paths</tt>] Convert relative paths to absolute paths (<tt>href</tt>, <tt>src</tt> and <tt>url('')</tt>. Boolean, default is <tt>false</tt>.
15
+ # [<tt>import</tt>] Follow <tt>@import</tt> rules. Boolean, default is <tt>true</tt>.
16
+ # [<tt>io_exceptions</tt>] Throw an exception if a link can not be found. Boolean, default is <tt>true</tt>.
17
+ class Parser
18
+ USER_AGENT = "Ruby CSS Parser/#{CssParserMaster::VERSION} (http://code.dunae.ca/css_parser/)"
19
+
20
+ STRIP_CSS_COMMENTS_RX = /\/\*.*?\*\//m
21
+ STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m
22
+
23
+ # Initial parsing
24
+ RE_AT_IMPORT_RULE = /\@import[\s]+(url\()?["''"]?(.[^'"\s"']*)["''"]?\)?([\w\s\,^\])]*)\)?;?/
25
+
26
+ #--
27
+ # 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
28
+ #++
29
+
30
+ # Array of CSS files that have been loaded.
31
+ attr_reader :loaded_uris
32
+
33
+ #attr_reader :rules
34
+
35
+ #--
36
+ # Class variable? see http://www.oreillynet.com/ruby/blog/2007/01/nubygems_dont_use_class_variab_1.html
37
+ #++
38
+ @folded_declaration_cache = {}
39
+ class << self; attr_reader :folded_declaration_cache; end
40
+
41
+ def initialize(options = {})
42
+ @options = {:absolute_paths => false,
43
+ :import => true,
44
+ :io_exceptions => true}.merge(options)
45
+
46
+ # array of RuleSets
47
+ @rules = []
48
+
49
+
50
+ @loaded_uris = []
51
+
52
+ # unprocessed blocks of CSS
53
+ @blocks = []
54
+ reset!
55
+ end
56
+
57
+ # Get declarations by selector.
58
+ #
59
+ # +media_types+ are optional, and can be a symbol or an array of symbols.
60
+ # The default value is <tt>:all</tt>.
61
+ #
62
+ # ==== Examples
63
+ # find_by_selector('#content')
64
+ # => 'font-size: 13px; line-height: 1.2;'
65
+ #
66
+ # find_by_selector('#content', [:screen, :handheld])
67
+ # => 'font-size: 13px; line-height: 1.2;'
68
+ #
69
+ # find_by_selector('#content', :print)
70
+ # => 'font-size: 11pt; line-height: 1.2;'
71
+ #
72
+ # Returns an array of declarations.
73
+ def find_by_selector(selector, media_types = :all)
74
+ out = []
75
+ each_selector(media_types) do |sel|
76
+ # puts "selector declaration: #{sel.declarations_to_s}"
77
+ out << sel.declarations_to_s if sel.selector.strip == selector.strip
78
+ end
79
+ out
80
+ end
81
+ alias_method :[], :find_by_selector
82
+
83
+
84
+ # Add a raw block of CSS.
85
+ #
86
+ # In order to follow +@import+ rules you must supply either a
87
+ # +:base_dir+ or +:base_uri+ option.
88
+ #
89
+ # ==== Example
90
+ # css = <<-EOT
91
+ # body { font-size: 10pt }
92
+ # p { margin: 0px; }
93
+ # @media screen, print {
94
+ # body { line-height: 1.2 }
95
+ # }
96
+ # EOT
97
+ #
98
+ # parser = CssParserMaster::Parser.new
99
+ # parser.add_block!(css)
100
+ #--
101
+ # TODO: add media_type
102
+ #++
103
+ def add_block!(block, options = {})
104
+ options = {:base_uri => nil, :base_dir => nil, :charset => nil, :media_types => :all}.merge(options)
105
+
106
+ block = cleanup_block(block)
107
+
108
+ if options[:base_uri] and @options[:absolute_paths]
109
+ block = CssParserMaster.convert_uris(block, options[:base_uri])
110
+ end
111
+
112
+ # Load @imported CSS
113
+ block.scan(RE_AT_IMPORT_RULE).each do |import_rule|
114
+ media_types = []
115
+ if media_string = import_rule[import_rule.length-1]
116
+ media_string.split(/\s|\,/).each do |t|
117
+ media_types << t.to_sym unless t.empty?
118
+ end
119
+ end
120
+
121
+ import_path = import_rule[1].to_s.gsub(/['"]*/, '').strip
122
+
123
+ if options[:base_uri]
124
+ import_uri = URI.parse(options[:base_uri].to_s).merge(import_path)
125
+ load_uri!(import_uri, options[:base_uri], media_types)
126
+ elsif options[:base_dir]
127
+ load_file!(import_path, options[:base_dir], media_types)
128
+ end
129
+ end
130
+
131
+ # Remove @import declarations
132
+ block.gsub!(RE_AT_IMPORT_RULE, '')
133
+ parse_block_into_rule_sets!(block, options)
134
+
135
+ end
136
+
137
+ # Add a CSS rule by setting the +selectors+, +declarations+ and +media_types+.
138
+ #
139
+ # +media_types+ can be a symbol or an array of symbols.
140
+ def add_rule!(selectors, declarations, media_types = :all)
141
+ rule_set = RuleSet.new(selectors, declarations)
142
+ # rule_set = RuleSet.new(Selectors.new(selectors), declarations)
143
+ add_rule_set!(rule_set, media_types)
144
+ end
145
+
146
+ # Add a CssParserMaster RuleSet object.
147
+ #
148
+ # +media_types+ can be a symbol or an array of symbols.
149
+ def add_rule_set!(ruleset, media_types = :all)
150
+ raise ArgumentError unless ruleset.kind_of?(CssParserMaster::RuleSet)
151
+
152
+ media_types = [media_types] if media_types.kind_of?(Symbol)
153
+
154
+ @rules << {:media_types => media_types, :rules => ruleset}
155
+ end
156
+
157
+ # Iterate through RuleSet objects.
158
+ #
159
+ # +media_types+ can be a symbol or an array of symbols.
160
+ def each_rule_set(media_types = :all) # :yields: rule_set
161
+ media_types = [:all] if media_types.nil?
162
+ media_types = [media_types] if media_types.kind_of?(Symbol)
163
+
164
+ @rules.each do |block|
165
+ if media_types.include?(:all) or block[:media_types].any? { |mt| media_types.include?(mt) }
166
+ yield block[:rules]
167
+ end
168
+ end
169
+ end
170
+
171
+ # go through all selector of the parser in order of specificity
172
+ # parse declarations for each selector
173
+ def selector_declarations(&block)
174
+ each_selector_sorted(:all, :order => :asc) do |sel|
175
+ sel.declarations.each do |dec|
176
+ yield sel, dec
177
+ end
178
+ end
179
+ end
180
+
181
+ # Iterate through CSS selectors.
182
+ #
183
+ # +media_types+ can be a symbol or an array of symbols.
184
+ # See RuleSet#each_selector for +options+.
185
+ def each_selector(media_types = :all, options = {}) # :yields: selectors, declarations, specificity
186
+ each_rule_set(media_types) do |rule_set|
187
+ #puts rule_set
188
+ rule_set.each_selector(options) do |selector|
189
+ # puts "selector: #{selector.inspect}"
190
+ yield selector
191
+ end
192
+ end
193
+ end
194
+
195
+ # Iterate through CSS selectors.
196
+ #
197
+ # +media_types+ can be a symbol or an array of symbols.
198
+ # See RuleSet#each_selector for +options+.
199
+ def each_selector_sorted(media_types = :all, options = {}) # :yields: selectors, declarations, specificity
200
+ order = options.delete(:order) || :desc
201
+ sels = []
202
+ each_selector(media_types, options) do |sel|
203
+ sels << sel
204
+ end
205
+ sorted_sels = sels.sort_by(&:specificity).reverse
206
+ sorted_sels.reverse! if order == :asc
207
+ sorted_sels.each{|s| yield s}
208
+ end
209
+
210
+
211
+ # Output all CSS rules as a single stylesheet.
212
+ def to_s(media_types = :all)
213
+ out = ''
214
+ each_selector(media_types) do |selector|
215
+ out << selector.to_s
216
+ end
217
+ out
218
+ end
219
+
220
+ # Merge declarations with the same selector.
221
+ def compact! # :nodoc:
222
+ compacted = []
223
+
224
+ compacted
225
+ end
226
+
227
+ def parse_block_into_rule_sets!(block, options = {}) # :nodoc:
228
+ options = {:media_types => :all}.merge(options)
229
+ media_types = options[:media_types]
230
+
231
+ in_declarations = false
232
+
233
+ block_depth = 0
234
+
235
+ # @charset is ignored for now
236
+ in_charset = false
237
+ in_string = false
238
+ in_at_media_rule = false
239
+
240
+ current_selectors = ''
241
+ current_declarations = ''
242
+
243
+ block.scan(/([\\]?[{}\s"]|(.[^\s"{}\\]*))/).each do |matches|
244
+ #block.scan(/((.[^{}"\n\r\f\s]*)[\s]|(.[^{}"\n\r\f]*)\{|(.[^{}"\n\r\f]*)\}|(.[^{}"\n\r\f]*)\"|(.*)[\s]+)/).each do |matches|
245
+ token = matches[0]
246
+ #puts "TOKEN: #{token}" unless token =~ /^[\s]*$/
247
+ if token =~ /\A"/ # found un-escaped double quote
248
+ in_string = !in_string
249
+ end
250
+
251
+ if in_declarations
252
+ current_declarations += token
253
+
254
+ if token =~ /\}/ and not in_string
255
+ current_declarations.gsub!(/\}[\s]*$/, '')
256
+
257
+ in_declarations = false
258
+
259
+ unless current_declarations.strip.empty?
260
+ #puts "SAVING #{current_selectors} -> #{current_declarations}"
261
+ add_rule!(current_selectors, current_declarations, media_types)
262
+ end
263
+
264
+ current_selectors = ''
265
+ current_declarations = ''
266
+ end
267
+ elsif token =~ /@media/i
268
+ # found '@media', reset current media_types
269
+ in_at_media_rule = true
270
+ media_types = []
271
+ elsif in_at_media_rule
272
+ if token =~ /\{/
273
+ block_depth = block_depth + 1
274
+ in_at_media_rule = false
275
+ else
276
+ token.gsub!(/[,\s]*/, '')
277
+ media_types << token.strip.downcase.to_sym unless token.empty?
278
+ end
279
+ elsif in_charset or token =~ /@charset/i
280
+ # iterate until we are out of the charset declaration
281
+ in_charset = (token =~ /;/ ? false : true)
282
+ else
283
+ if token =~ /\}/ and not in_string
284
+ block_depth = block_depth - 1
285
+ else
286
+ if token =~ /\{/ and not in_string
287
+ current_selectors.gsub!(/^[\s]*/, '')
288
+ current_selectors.gsub!(/[\s]*$/, '')
289
+ in_declarations = true
290
+ else
291
+ current_selectors += token
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
297
+
298
+ # Load a remote CSS file.
299
+ def load_uri!(uri, base_uri = nil, media_types = :all)
300
+ base_uri = uri if base_uri.nil?
301
+ src, charset = read_remote_file(uri)
302
+
303
+ add_block!(src, {:media_types => media_types, :base_uri => base_uri})
304
+ end
305
+
306
+ # Load a local CSS file.
307
+ def load_file!(file_name, base_dir = nil, media_types = :all)
308
+ file_name = File.expand_path(file_name, base_dir)
309
+ return unless File.readable?(file_name)
310
+
311
+ src = IO.read(file_name)
312
+ base_dir = File.dirname(file_name)
313
+
314
+ add_block!(src, {:media_types => media_types, :base_dir => base_dir})
315
+ end
316
+
317
+
318
+
319
+ protected
320
+ # Strip comments and clean up blank lines from a block of CSS.
321
+ #
322
+ # Returns a string.
323
+ def cleanup_block(block) # :nodoc:
324
+ # Strip CSS comments
325
+ block.gsub!(STRIP_CSS_COMMENTS_RX, '')
326
+
327
+ # Strip HTML comments - they shouldn't really be in here but
328
+ # some people are just crazy...
329
+ block.gsub!(STRIP_HTML_COMMENTS_RX, '')
330
+
331
+ # Strip lines containing just whitespace
332
+ block.gsub!(/^\s+$/, "")
333
+
334
+ block
335
+ end
336
+
337
+ # Download a file into a string.
338
+ #
339
+ # Returns the file's data and character set in an array.
340
+ #--
341
+ # TODO: add option to fail silently or throw and exception on a 404
342
+ #++
343
+ def read_remote_file(uri) # :nodoc:
344
+ raise CircularReferenceError, "can't load #{uri.to_s} more than once" if @loaded_uris.include?(uri.to_s)
345
+ @loaded_uris << uri.to_s
346
+
347
+ begin
348
+ #fh = open(uri, 'rb')
349
+ fh = open(uri, 'rb', 'User-Agent' => USER_AGENT, 'Accept-Encoding' => 'gzip')
350
+
351
+ if fh.content_encoding.include?('gzip')
352
+ remote_src = Zlib::GzipReader.new(fh).read
353
+ else
354
+ remote_src = fh.read
355
+ end
356
+
357
+ #puts "reading #{uri} (#{fh.charset})"
358
+
359
+ ic = Iconv.new('UTF-8//IGNORE', fh.charset)
360
+ src = ic.iconv(remote_src)
361
+
362
+ fh.close
363
+ return src, fh.charset
364
+ rescue
365
+ raise RemoteFileError if @options[:io_exceptions]
366
+ return '', nil
367
+ end
368
+ end
369
+
370
+ private
371
+ # Save a folded declaration block to the internal cache.
372
+ def save_folded_declaration(block_hash, folded_declaration) # :nodoc:
373
+ @folded_declaration_cache[block_hash] = folded_declaration
374
+ end
375
+
376
+ # Retrieve a folded declaration block from the internal cache.
377
+ def get_folded_declaration(block_hash) # :nodoc:
378
+ return @folded_declaration_cache[block_hash] ||= nil
379
+ end
380
+
381
+ def reset! # :nodoc:
382
+ @folded_declaration_cache = {}
383
+ @css_source = ''
384
+ @css_rules = []
385
+ @css_warnings = []
386
+ end
387
+ end
388
+ end
@@ -0,0 +1,46 @@
1
+ module CssParserMaster
2
+ # :stopdoc:
3
+ # Base types
4
+ RE_NL = Regexp.new('(\n|\r\n|\r|\f)')
5
+ RE_NON_ASCII = Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE, 'n') #[^\0-\177]
6
+ RE_UNICODE = Regexp.new('(\\\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])*)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n')
7
+ RE_ESCAPE = Regexp.union(RE_UNICODE, '|(\\\\[^\n\r\f0-9a-f])')
8
+ RE_IDENT = Regexp.new("[\-]?([_a-z]|#{RE_NON_ASCII}|#{RE_ESCAPE})([_a-z0-9\-]|#{RE_NON_ASCII}|#{RE_ESCAPE})*", Regexp::IGNORECASE, 'n')
9
+
10
+ # General strings
11
+ RE_STRING1 = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')
12
+ RE_STRING2 = Regexp.new('(\'(.[^\n\r\f\\\']*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\')')
13
+ RE_STRING = Regexp.union(RE_STRING1, RE_STRING2)
14
+
15
+ 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')
16
+ URI_RX = /url\(("([^"]*)"|'([^']*)'|([^)]*))\)/im
17
+
18
+ # Initial parsing
19
+ RE_AT_IMPORT_RULE = /\@import[\s]+(url\()?["''"]?(.[^'"\s"']*)["''"]?\)?([\w\s\,^\])]*)\)?;?/
20
+
21
+ #--
22
+ #RE_AT_MEDIA_RULE = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')
23
+
24
+ #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
25
+ #++
26
+ IMPORTANT_IN_PROPERTY_RX = /[\s]*\!important[\s]*/i
27
+ STRIP_CSS_COMMENTS_RX = /\/\*.*?\*\//m
28
+ STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m
29
+
30
+ # Special units
31
+ BOX_MODEL_UNITS_RX = /(auto|inherit|0|([\-]*([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%)))([\s;]|\Z)/imx
32
+ RE_LENGTH_OR_PERCENTAGE = Regexp.new('([\-]*(([0-9]*\.[0-9]+)|[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%))', Regexp::IGNORECASE)
33
+ RE_BACKGROUND_POSITION = Regexp.new("((#{RE_LENGTH_OR_PERCENTAGE})|left|center|right|top|bottom)", Regexp::IGNORECASE | Regexp::EXTENDED)
34
+ 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
35
+
36
+ # Patterns for specificity calculations
37
+ ELEMENTS_AND_PSEUDO_ELEMENTS_RX = /((^|[\s\+\>]+)[\w]+|\:(first\-line|first\-letter|before|after))/i
38
+ NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = /(\.[\w]+)|(\[[\w]+)|(\:(link|first\-child|lang))/i
39
+
40
+ # Colours
41
+ RE_COLOUR_RGB = Regexp.new('(rgb[\s]*\([\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*\))', Regexp::IGNORECASE)
42
+ RE_COLOUR_HEX = /(#([0-9a-f]{6}|[0-9a-f]{3})([\s;]|$))/i
43
+ RE_COLOUR_NAMED = /([\s]*^)?(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|transparent)([\s]*$)?/i
44
+ RE_COLOUR = Regexp.union(RE_COLOUR_RGB, RE_COLOUR_HEX, RE_COLOUR_NAMED)
45
+ # :startdoc:
46
+ end
@@ -0,0 +1,337 @@
1
+ require 'css_parser_master/selector'
2
+ require 'css_parser_master/selectors'
3
+ require 'css_parser_master/declaration'
4
+ require 'css_parser_master/declaration_api'
5
+ require 'css_parser_master/declarations'
6
+
7
+ module CssParserMaster
8
+ class RuleSet
9
+ # Patterns for specificity calculations
10
+ RE_ELEMENTS_AND_PSEUDO_ELEMENTS = /((^|[\s\+\>]+)[\w]+|\:(first\-line|first\-letter|before|after))/i
11
+ RE_NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES = /(\.[\w]+)|(\[[\w]+)|(\:(link|first\-child|lang))/i
12
+
13
+ include DeclarationAPI
14
+
15
+ # Array of selector strings.
16
+ attr_reader :selectors
17
+
18
+ # Integer with the specificity to use for this RuleSet.
19
+ attr_accessor :specificity
20
+
21
+ def initialize(selectors, block, specificity = nil)
22
+ @selectors = []
23
+ @specificity = specificity
24
+ @declarations = {}
25
+ @order = 0
26
+ parse_selectors!(selectors) if selectors
27
+ parse_declarations!(block)
28
+ end
29
+
30
+
31
+ # Get the value of a property
32
+ def get_value(property)
33
+ return '' unless property and not property.empty?
34
+
35
+ property = property.downcase.strip
36
+ properties = @declarations.inject('') do |val, (key, data)|
37
+ #puts "COMPARING #{key} #{key.inspect} against #{property} #{property.inspect}"
38
+ importance = data[:is_important] ? ' !important' : ''
39
+ val << "#{data[:value]}#{importance}; " if key.downcase.strip == property
40
+ val
41
+ end
42
+ return properties ? properties.strip : ''
43
+ end
44
+ alias_method :[], :get_value
45
+
46
+ # Iterate through selectors.
47
+ #
48
+ # Options
49
+ # - +force_important+ -- boolean
50
+ #
51
+ # ==== Example
52
+ # ruleset.each_selector do |sel, dec, spec|
53
+ # ...
54
+ # end
55
+ def each_selector(options = {}) # :yields: selector, declarations, specificity
56
+ declarations = declarations_to_s(options)
57
+ # puts "declarations: #{declarations.inspect}"
58
+ if @specificity
59
+ @selectors.each { |sel| yield Selector.new sel.strip, declarations, @specificity }
60
+ else
61
+ @selectors.each { |sel| yield Selector.new sel.strip, declarations, CssParserMaster.calculate_specificity(sel) }
62
+ end
63
+ end
64
+
65
+ # Return the CSS rule set as a string.
66
+ def to_s
67
+ decs = declarations_to_s
68
+ "#{@selectors} { #{decs} }"
69
+ end
70
+
71
+ # Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
72
+ def expand_shorthand!
73
+ expand_dimensions_shorthand!
74
+ expand_font_shorthand!
75
+ expand_background_shorthand!
76
+ end
77
+
78
+ # Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
79
+ def create_shorthand!
80
+ create_background_shorthand!
81
+ create_dimensions_shorthand!
82
+ create_font_shorthand!
83
+ end
84
+
85
+ private
86
+
87
+
88
+ #--
89
+ # TODO: way too simplistic
90
+ #++
91
+ def parse_selectors!(selectors) # :nodoc:
92
+ @selectors = selectors.split(',')
93
+ end
94
+
95
+ public
96
+ # Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)
97
+ # into their constituent parts.
98
+ def expand_dimensions_shorthand! # :nodoc:
99
+ ['margin', 'padding'].each do |property|
100
+
101
+ next unless @declarations.has_key?(property)
102
+
103
+ value = @declarations[property][:value]
104
+ is_important = @declarations[property][:is_important]
105
+ order = @declarations[property][:order]
106
+ t, r, b, l = nil
107
+
108
+ matches = value.scan(CssParserMaster::BOX_MODEL_UNITS_RX)
109
+
110
+ case matches.length
111
+ when 1
112
+ t, r, b, l = matches[0][0], matches[0][0], matches[0][0], matches[0][0]
113
+ when 2
114
+ t, b = matches[0][0], matches[0][0]
115
+ r, l = matches[1][0], matches[1][0]
116
+ when 3
117
+ t = matches[0][0]
118
+ r, l = matches[1][0], matches[1][0]
119
+ b = matches[2][0]
120
+ when 4
121
+ t = matches[0][0]
122
+ r = matches[1][0]
123
+ b = matches[2][0]
124
+ l = matches[3][0]
125
+ end
126
+
127
+ values = { :is_important => is_important, :order => order }
128
+ @declarations["#{property}-top"] = values.merge(:value => t.to_s)
129
+ @declarations["#{property}-right"] = values.merge(:value => r.to_s)
130
+ @declarations["#{property}-bottom"] = values.merge(:value => b.to_s)
131
+ @declarations["#{property}-left"] = values.merge(:value => l.to_s)
132
+ @declarations.delete(property)
133
+ end
134
+ end
135
+
136
+ # Convert shorthand font declarations (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)
137
+ # into their constituent parts.
138
+ def expand_font_shorthand! # :nodoc:
139
+ return unless @declarations.has_key?('font')
140
+
141
+ font_props = {}
142
+
143
+ # reset properties to 'normal' per http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
144
+ ['font-style', 'font-variant', 'font-weight', 'font-size',
145
+ 'line-height'].each do |prop|
146
+ font_props[prop] = 'normal'
147
+ end
148
+
149
+ value = @declarations['font'][:value]
150
+ is_important = @declarations['font'][:is_important]
151
+ order = @declarations['font'][:order]
152
+
153
+ in_fonts = false
154
+
155
+ matches = value.scan(/("(.*[^"])"|'(.*[^'])'|(\w[^ ,]+))/)
156
+ matches.each do |match|
157
+ m = match[0].to_s.strip
158
+ m.gsub!(/[;]$/, '')
159
+
160
+ if in_fonts
161
+ if font_props.has_key?('font-family')
162
+ font_props['font-family'] += ', ' + m
163
+ else
164
+ font_props['font-family'] = m
165
+ end
166
+ elsif m =~ /normal|inherit/i
167
+ ['font-style', 'font-weight', 'font-variant'].each do |font_prop|
168
+ font_props[font_prop] = m unless font_props.has_key?(font_prop)
169
+ end
170
+ elsif m =~ /italic|oblique/i
171
+ font_props['font-style'] = m
172
+ elsif m =~ /small\-caps/i
173
+ font_props['font-variant'] = m
174
+ elsif m =~ /[1-9]00$|bold|bolder|lighter/i
175
+ font_props['font-weight'] = m
176
+ elsif m =~ CssParserMaster::FONT_UNITS_RX
177
+ if m =~ /\//
178
+ font_props['font-size'], font_props['line-height'] = m.split('/')
179
+ else
180
+ font_props['font-size'] = m
181
+ end
182
+ in_fonts = true
183
+ end
184
+ end
185
+
186
+ font_props.each { |font_prop, font_val| @declarations[font_prop] = {:value => font_val, :is_important => is_important, :order => order} }
187
+
188
+ @declarations.delete('font')
189
+ end
190
+
191
+
192
+ # Convert shorthand background declarations (e.g. <tt>background: url("chess.png") gray 50% repeat fixed;</tt>)
193
+ # into their constituent parts.
194
+ #
195
+ # See http://www.w3.org/TR/CSS21/colors.html#propdef-background
196
+ def expand_background_shorthand! # :nodoc:
197
+ return unless @declarations.has_key?('background')
198
+
199
+ value = @declarations['background'][:value]
200
+ is_important = @declarations['background'][:is_important]
201
+ order = @declarations['background'][:order]
202
+
203
+ bg_props = {}
204
+
205
+
206
+ if m = value.match(Regexp.union(CssParserMaster::URI_RX, /none/i)).to_s
207
+ bg_props['background-image'] = m.strip unless m.empty?
208
+ value.gsub!(Regexp.union(CssParserMaster::URI_RX, /none/i), '')
209
+ end
210
+
211
+ if m = value.match(/([\s]*^)?(scroll|fixed)([\s]*$)?/i).to_s
212
+ bg_props['background-attachment'] = m.strip unless m.empty?
213
+ end
214
+
215
+ if m = value.match(/([\s]*^)?(repeat(\-x|\-y)*|no\-repeat)([\s]*$)?/i).to_s
216
+ bg_props['background-repeat'] = m.strip unless m.empty?
217
+ end
218
+
219
+ if m = value.match(CssParserMaster::RE_COLOUR).to_s
220
+ bg_props['background-color'] = m.strip unless m.empty?
221
+ end
222
+
223
+ value.scan(CssParserMaster::RE_BACKGROUND_POSITION).each do |m|
224
+ if bg_props.has_key?('background-position')
225
+ bg_props['background-position'] += ' ' + m[0].to_s.strip unless m.empty?
226
+ else
227
+ bg_props['background-position'] = m[0].to_s.strip unless m.empty?
228
+ end
229
+ end
230
+
231
+
232
+ if value =~ /([\s]*^)?inherit([\s]*$)?/i
233
+ ['background-color', 'background-image', 'background-attachment', 'background-repeat', 'background-position'].each do |prop|
234
+ bg_props["#{prop}"] = 'inherit' unless bg_props.has_key?(prop) and not bg_props[prop].empty?
235
+ end
236
+ end
237
+
238
+ bg_props.each { |bg_prop, bg_val| @declarations[bg_prop] = {:value => bg_val, :is_important => is_important, :order => order} }
239
+
240
+ @declarations.delete('background')
241
+ end
242
+
243
+
244
+ # Looks for long format CSS background properties (e.g. <tt>background-color</tt>) and
245
+ # converts them into a shorthand CSS <tt>background</tt> property.
246
+ def create_background_shorthand! # :nodoc:
247
+ new_value = ''
248
+ ['background-color', 'background-image', 'background-repeat',
249
+ 'background-position', 'background-attachment'].each do |property|
250
+ if @declarations.has_key?(property)
251
+ new_value += @declarations[property][:value] + ' '
252
+ @declarations.delete(property)
253
+ end
254
+ end
255
+
256
+ unless new_value.strip.empty?
257
+ @declarations['background'] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
258
+ end
259
+ end
260
+
261
+ # Looks for long format CSS dimensional properties (i.e. <tt>margin</tt> and <tt>padding</tt>) and
262
+ # converts them into shorthand CSS properties.
263
+ def create_dimensions_shorthand! # :nodoc:
264
+ # geometric
265
+ directions = ['top', 'right', 'bottom', 'left']
266
+ ['margin', 'padding'].each do |property|
267
+ values = {}
268
+
269
+ foldable = @declarations.select { |dim, val| dim == "#{property}-top" or dim == "#{property}-right" or dim == "#{property}-bottom" or dim == "#{property}-left" }
270
+ # All four dimensions must be present
271
+ if foldable.length == 4
272
+ values = {}
273
+
274
+ directions.each { |d| values[d.to_sym] = @declarations["#{property}-#{d}"][:value].downcase.strip }
275
+
276
+ if values[:left] == values[:right]
277
+ if values[:top] == values[:bottom]
278
+ if values[:top] == values[:left] # All four sides are equal
279
+ new_value = values[:top]
280
+ else # Top and bottom are equal, left and right are equal
281
+ new_value = values[:top] + ' ' + values[:left]
282
+ end
283
+ else # Only left and right are equal
284
+ new_value = values[:top] + ' ' + values[:left] + ' ' + values[:bottom]
285
+ end
286
+ else # No sides are equal
287
+ new_value = values[:top] + ' ' + values[:right] + ' ' + values[:bottom] + ' ' + values[:left]
288
+ end # done creating 'new_value'
289
+
290
+ # Save the new value
291
+ unless new_value.strip.empty?
292
+ @declarations[property] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
293
+ end
294
+
295
+ # Delete the shorthand values
296
+ directions.each { |d| @declarations.delete("#{property}-#{d}") }
297
+ end
298
+ end # done iterating through margin and padding
299
+ end
300
+
301
+
302
+ # Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and
303
+ # tries to convert them into a shorthand CSS <tt>font</tt> property. All
304
+ # font properties must be present in order to create a shorthand declaration.
305
+ def create_font_shorthand! # :nodoc:
306
+ ['font-style', 'font-variant', 'font-weight', 'font-size',
307
+ 'line-height', 'font-family'].each do |prop|
308
+ return unless @declarations.has_key?(prop)
309
+ end
310
+
311
+ new_value = ''
312
+ ['font-style', 'font-variant', 'font-weight'].each do |property|
313
+ unless @declarations[property][:value] == 'normal'
314
+ new_value += @declarations[property][:value] + ' '
315
+ end
316
+ end
317
+
318
+ new_value += @declarations['font-size'][:value]
319
+
320
+ unless @declarations['line-height'][:value] == 'normal'
321
+ new_value += '/' + @declarations['line-height'][:value]
322
+ end
323
+
324
+ new_value += ' ' + @declarations['font-family'][:value]
325
+
326
+ @declarations['font'] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
327
+
328
+ ['font-style', 'font-variant', 'font-weight', 'font-size',
329
+ 'line-height', 'font-family'].each do |prop|
330
+ @declarations.delete(prop)
331
+ end
332
+
333
+ end
334
+ end
335
+ end
336
+
337
+