css_parser_master 1.2.4

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,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
+