css_parser 0.9.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.
@@ -0,0 +1,381 @@
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
+ # Array of selector strings.
8
+ attr_reader :selectors
9
+
10
+ # Integer with the specificity to use for this RuleSet.
11
+ attr_accessor :specificity
12
+
13
+ def initialize(selectors, block, specificity = nil)
14
+ @selectors = []
15
+ @specificity = specificity
16
+ @declarations = {}
17
+ parse_selectors!(selectors) if selectors
18
+ parse_declarations!(block)
19
+ end
20
+
21
+
22
+ # Get the value of a property
23
+ def get_value(property)
24
+ return '' unless property and not property.empty?
25
+
26
+ property = property.downcase.strip
27
+ properties = @declarations.inject('') do |val, (key, data)|
28
+ #puts "COMPARING #{key} #{key.inspect} against #{property} #{property.inspect}"
29
+ importance = data[:is_important] ? ' !important' : ''
30
+ val << "#{data[:value]}#{importance}; " if key.downcase.strip == property
31
+ val
32
+ end
33
+ return properties ? properties.strip : ''
34
+ end
35
+ alias_method :[], :get_value
36
+
37
+ # Add a CSS declaration to the current RuleSet.
38
+ #
39
+ # rule_set.add_declaration!('color', 'blue')
40
+ #
41
+ # puts rule_set['color']
42
+ # => 'blue;'
43
+ #
44
+ # rule_set.add_declaration!('margin', '0px auto !important')
45
+ #
46
+ # puts rule_set['margin']
47
+ # => '0px auto !important;'
48
+ #
49
+ # If the property already exists its value will be over-written.
50
+ def add_declaration!(property, value)
51
+ value.gsub!(/;\Z/, '')
52
+ is_important = !value.gsub!(CssParser::IMPORTANT_IN_PROPERTY_RX, '').nil?
53
+ property = property.downcase.strip
54
+ #puts "SAVING #{property} #{value} #{is_important.inspect}"
55
+ @declarations[property] = {:value => value, :is_important => is_important}
56
+ end
57
+ alias_method :[]=, :add_declaration!
58
+
59
+ # Iterate through selectors.
60
+ #
61
+ # Options
62
+ # - +force_important+ -- boolean
63
+ #
64
+ # ==== Example
65
+ # ruleset.each_selector do |sel, dec, spec|
66
+ # ...
67
+ # end
68
+ def each_selector(options = {}) # :yields: selector, declarations, specificity
69
+ declarations = declarations_to_s(options)
70
+ if @specificity
71
+ @selectors.each { |sel| yield sel.strip, declarations, @specificity }
72
+ else
73
+ @selectors.each { |sel| yield sel.strip, declarations, CssParser.calculate_specificity(sel) }
74
+ end
75
+ end
76
+
77
+ # Iterate through declarations.
78
+ def each_declaration # :yields: property, value, is_important
79
+ @declarations.each do |property, data|
80
+ value = data[:value]
81
+ #value += ' !important' if data[:is_important]
82
+ yield property.downcase.strip, value.strip, data[:is_important]
83
+ end
84
+ end
85
+
86
+ # Return all declarations as a string.
87
+ #--
88
+ # TODO: Clean-up regexp doesn't seem to work
89
+ #++
90
+ def declarations_to_s(options = {})
91
+ options = {:force_important => false}.merge(options)
92
+ str = ''
93
+ importance = options[:force_important] ? ' !important' : ''
94
+ each_declaration { |prop, val| str += "#{prop}: #{val}#{importance}; " }
95
+ str.gsub(/^[\s]+|[\n\r\f\t]*|[\s]+$/mx, '').strip
96
+ end
97
+
98
+ # Return the CSS rule set as a string.
99
+ def to_s
100
+ decs = declarations_to_s
101
+ "#{@selectors} { #{decs} }"
102
+ end
103
+
104
+ # Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
105
+ def expand_shorthand!
106
+ expand_dimensions_shorthand!
107
+ expand_font_shorthand!
108
+ expand_background_shorthand!
109
+ end
110
+
111
+ # Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
112
+ def create_shorthand!
113
+ create_background_shorthand!
114
+ create_dimensions_shorthand!
115
+ create_font_shorthand!
116
+ end
117
+
118
+ private
119
+ def parse_declarations!(block) # :nodoc:
120
+ @declarations = {}
121
+
122
+ return unless block
123
+
124
+ block.gsub!(/(^[\s]*)|([\s]*$)/, '')
125
+
126
+ block.split(/[\;$]+/m).each do |decs|
127
+ if matches = decs.match(/(.[^:]*)\:(.[^;]*)(;|\Z)/i)
128
+ property, value, end_of_declaration = matches.captures
129
+
130
+ add_declaration!(property, value)
131
+ end
132
+ end
133
+ end
134
+
135
+ #--
136
+ # TODO: way too simplistic
137
+ #++
138
+ def parse_selectors!(selectors) # :nodoc:
139
+ @selectors = selectors.split(',')
140
+ end
141
+
142
+ # Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)
143
+ # into their constituent parts.
144
+ def expand_dimensions_shorthand! # :nodoc:
145
+ ['margin', 'padding'].each do |property|
146
+
147
+ next unless @declarations.has_key?(property)
148
+
149
+ value = @declarations[property][:value]
150
+ is_important = @declarations[property][:is_important]
151
+ t, r, b, l = nil
152
+
153
+ matches = value.scan(CssParser::BOX_MODEL_UNITS_RX)
154
+
155
+ case matches.length
156
+ when 1
157
+ t, r, b, l = matches[0][0], matches[0][0], matches[0][0], matches[0][0]
158
+ when 2
159
+ t, b = matches[0][0], matches[0][0]
160
+ r, l = matches[1][0], matches[1][0]
161
+ when 3
162
+ t = matches[0][0]
163
+ r, l = matches[1][0], matches[1][0]
164
+ b = matches[2][0]
165
+ when 4
166
+ t = matches[0][0]
167
+ r = matches[1][0]
168
+ b = matches[2][0]
169
+ l = matches[3][0]
170
+ end
171
+
172
+ @declarations["#{property}-top"] = {:value => t.to_s, :is_important => is_important}
173
+ @declarations["#{property}-right"] = {:value => r.to_s, :is_important => is_important}
174
+ @declarations["#{property}-bottom"] = {:value => b.to_s, :is_important => is_important}
175
+ @declarations["#{property}-left"] = {:value => l.to_s, :is_important => is_important}
176
+ @declarations.delete(property)
177
+ end
178
+ end
179
+
180
+ # Convert shorthand font declarations (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)
181
+ # into their constituent parts.
182
+ def expand_font_shorthand! # :nodoc:
183
+ return unless @declarations.has_key?('font')
184
+
185
+ font_props = {}
186
+
187
+ # reset properties to 'normal' per http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
188
+ ['font-style', 'font-variant', 'font-weight', 'font-size',
189
+ 'line-height'].each do |prop|
190
+ font_props[prop] = 'normal'
191
+ end
192
+
193
+ value = @declarations['font'][:value]
194
+ is_important = @declarations['font'][:is_important]
195
+
196
+ in_fonts = false
197
+
198
+ matches = value.scan(/("(.*[^"])"|'(.*[^'])'|(\w[^ ,]+))/)
199
+ matches.each do |match|
200
+ m = match[0].to_s.strip
201
+ m.gsub!(/[;]$/, '')
202
+
203
+ if in_fonts
204
+ if font_props.has_key?('font-family')
205
+ font_props['font-family'] += ', ' + m
206
+ else
207
+ font_props['font-family'] = m
208
+ end
209
+ elsif m =~ /normal|inherit/i
210
+ ['font-style', 'font-weight', 'font-variant'].each do |font_prop|
211
+ font_props[font_prop] = m unless font_props.has_key?(font_prop)
212
+ end
213
+ elsif m =~ /italic|oblique/i
214
+ font_props['font-style'] = m
215
+ elsif m =~ /small\-caps/i
216
+ font_props['font-variant'] = m
217
+ elsif m =~ /[1-9]00$|bold|bolder|lighter/i
218
+ font_props['font-weight'] = m
219
+ elsif m =~ CssParser::FONT_UNITS_RX
220
+ if m =~ /\//
221
+ font_props['font-size'], font_props['line-height'] = m.split('/')
222
+ else
223
+ font_props['font-size'] = m
224
+ end
225
+ in_fonts = true
226
+ end
227
+ end
228
+
229
+ font_props.each { |font_prop, font_val| @declarations[font_prop] = {:value => font_val, :is_important => is_important} }
230
+
231
+ @declarations.delete('font')
232
+ end
233
+
234
+
235
+ # Convert shorthand background declarations (e.g. <tt>background: url("chess.png") gray 50% repeat fixed;</tt>)
236
+ # into their constituent parts.
237
+ #
238
+ # See http://www.w3.org/TR/CSS21/colors.html#propdef-background
239
+ def expand_background_shorthand! # :nodoc:
240
+ return unless @declarations.has_key?('background')
241
+
242
+ value = @declarations['background'][:value]
243
+ is_important = @declarations['background'][:is_important]
244
+
245
+ bg_props = {}
246
+
247
+
248
+ if m = value.match(Regexp.union(CssParser::URI_RX, /none/i)).to_s
249
+ bg_props['background-image'] = m.strip unless m.empty?
250
+ value.gsub!(Regexp.union(CssParser::URI_RX, /none/i), '')
251
+ end
252
+
253
+ if m = value.match(/([\s]*^)?(scroll|fixed)([\s]*$)?/i).to_s
254
+ bg_props['background-attachment'] = m.strip unless m.empty?
255
+ end
256
+
257
+ if m = value.match(/([\s]*^)?(repeat(\-x|\-y)*|no\-repeat)([\s]*$)?/i).to_s
258
+ bg_props['background-repeat'] = m.strip unless m.empty?
259
+ end
260
+
261
+ if m = value.match(CssParser::RE_COLOUR).to_s
262
+ bg_props['background-color'] = m.strip unless m.empty?
263
+ end
264
+
265
+ value.scan(CssParser::RE_BACKGROUND_POSITION).each do |m|
266
+ if bg_props.has_key?('background-position')
267
+ bg_props['background-position'] += ' ' + m[0].to_s.strip unless m.empty?
268
+ else
269
+ bg_props['background-position'] = m[0].to_s.strip unless m.empty?
270
+ end
271
+ end
272
+
273
+
274
+ if value =~ /([\s]*^)?inherit([\s]*$)?/i
275
+ ['background-color', 'background-image', 'background-attachment', 'background-repeat', 'background-position'].each do |prop|
276
+ bg_props["#{prop}"] = 'inherit' unless bg_props.has_key?(prop) and not bg_props[prop].empty?
277
+ end
278
+ end
279
+
280
+ bg_props.each { |bg_prop, bg_val| @declarations[bg_prop] = {:value => bg_val, :is_important => is_important} }
281
+
282
+ @declarations.delete('background')
283
+ end
284
+
285
+
286
+ # Looks for long format CSS background properties (e.g. <tt>background-color</tt>) and
287
+ # converts them into a shorthand CSS <tt>background</tt> property.
288
+ def create_background_shorthand! # :nodoc:
289
+ new_value = ''
290
+ ['background-color', 'background-image', 'background-repeat',
291
+ 'background-position', 'background-attachment'].each do |property|
292
+ if @declarations.has_key?(property)
293
+ new_value += @declarations[property][:value] + ' '
294
+ @declarations.delete(property)
295
+ end
296
+ end
297
+
298
+ unless new_value.strip.empty?
299
+ @declarations['background'] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
300
+ end
301
+ end
302
+
303
+ # Looks for long format CSS dimensional properties (i.e. <tt>margin</tt> and <tt>padding</tt>) and
304
+ # converts them into shorthand CSS properties.
305
+ def create_dimensions_shorthand! # :nodoc:
306
+ # geometric
307
+ directions = ['top', 'right', 'bottom', 'left']
308
+ ['margin', 'padding'].each do |property|
309
+ values = {}
310
+
311
+ foldable = @declarations.select { |dim, val| dim == "#{property}-top" or dim == "#{property}-right" or dim == "#{property}-bottom" or dim == "#{property}-left" }
312
+ # All four dimensions must be present
313
+ if foldable.length == 4
314
+ values = {}
315
+
316
+ directions.each { |d| values[d.to_sym] = @declarations["#{property}-#{d}"][:value].downcase.strip }
317
+
318
+ if values[:left] == values[:right]
319
+ if values[:top] == values[:bottom]
320
+ if values[:top] == values[:left] # All four sides are equal
321
+ new_value = values[:top]
322
+ else # Top and bottom are equal, left and right are equal
323
+ new_value = values[:top] + ' ' + values[:left]
324
+ end
325
+ else # Only left and right are equal
326
+ new_value = values[:top] + ' ' + values[:left] + ' ' + values[:bottom]
327
+ end
328
+ else # No sides are equal
329
+ new_value = values[:top] + ' ' + values[:right] + ' ' + values[:bottom] + ' ' + values[:left]
330
+ end # done creating 'new_value'
331
+
332
+ # Save the new value
333
+ unless new_value.strip.empty?
334
+ @declarations[property] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
335
+ end
336
+
337
+ # Delete the shorthand values
338
+ directions.each { |d| @declarations.delete("#{property}-#{d}") }
339
+ end
340
+ end # done iterating through margin and padding
341
+ end
342
+
343
+
344
+ # Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and
345
+ # tries to convert them into a shorthand CSS <tt>font</tt> property. All
346
+ # font properties must be present in order to create a shorthand declaration.
347
+ def create_font_shorthand! # :nodoc:
348
+ ['font-style', 'font-variant', 'font-weight', 'font-size',
349
+ 'line-height', 'font-family'].each do |prop|
350
+ return unless @declarations.has_key?(prop)
351
+ end
352
+
353
+ new_value = ''
354
+ ['font-style', 'font-variant', 'font-weight'].each do |property|
355
+ unless @declarations[property][:value] == 'normal'
356
+ new_value += @declarations[property][:value] + ' '
357
+ end
358
+ end
359
+
360
+ new_value += @declarations['font-size'][:value]
361
+
362
+ unless @declarations['line-height'][:value] == 'normal'
363
+ new_value += '/' + @declarations['line-height'][:value]
364
+ end
365
+
366
+ new_value += ' ' + @declarations['font-family'][:value]
367
+
368
+ @declarations['font'] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
369
+
370
+ ['font-style', 'font-variant', 'font-weight', 'font-size',
371
+ 'line-height', 'font-family'].each do |prop|
372
+ @declarations.delete(prop)
373
+ end
374
+
375
+ end
376
+
377
+
378
+
379
+
380
+ end
381
+ end
data/lib/css_parser.rb ADDED
@@ -0,0 +1,149 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+ require 'uri'
3
+ require 'md5'
4
+ require 'zlib'
5
+ require 'iconv'
6
+ require 'css_parser/rule_set'
7
+ require 'css_parser/regexps'
8
+ require 'css_parser/parser'
9
+
10
+ module CssParser
11
+ # Merge multiple CSS RuleSets by cascading according to the CSS 2.1 cascading rules
12
+ # (http://www.w3.org/TR/REC-CSS2/cascade.html#cascading-order).
13
+ #
14
+ # Takes one or more RuleSet objects.
15
+ #
16
+ # Returns a RuleSet.
17
+ #
18
+ # ==== Cascading
19
+ # If a RuleSet object has its +specificity+ defined, that specificity is
20
+ # used in the cascade calculations.
21
+ #
22
+ # If no specificity is explicitly set and the RuleSet has *one* selector,
23
+ # the specificity is calculated using that selector.
24
+ #
25
+ # If no selectors or multiple selectors are present, the specificity is
26
+ # treated as 0.
27
+ #
28
+ # ==== Example #1
29
+ # rs1 = RuleSet.new(nil, 'color: black;')
30
+ # rs2 = RuleSet.new(nil, 'margin: 0px;')
31
+ #
32
+ # merged = CssParser.merge(rs1, rs2)
33
+ #
34
+ # puts merged
35
+ # => "{ margin: 0px; color: black; }"
36
+ #
37
+ # ==== Example #2
38
+ # rs1 = RuleSet.new(nil, 'background-color: black;')
39
+ # rs2 = RuleSet.new(nil, 'background-image: none;')
40
+ #
41
+ # merged = CssParser.merge(rs1, rs2)
42
+ #
43
+ # puts merged
44
+ # => "{ background: none black; }"
45
+ #--
46
+ # TODO: declaration_hashes should be able to contain a RuleSet
47
+ # this should be a Class method
48
+ def CssParser.merge(*rule_sets)
49
+ @folded_declaration_cache = {}
50
+
51
+ # in case called like CssParser.merge([rule_set, rule_set])
52
+ rule_sets.flatten! if rule_sets[0].kind_of?(Array)
53
+
54
+ unless rule_sets.all? {|rs| rs.kind_of?(CssParser::RuleSet)}
55
+ raise ArgumentError, "all parameters must be CssParser::RuleSets."
56
+ end
57
+
58
+ return rule_sets[0] if rule_sets.length == 1
59
+
60
+ # Internal storage of CSS properties that we will keep
61
+ properties = {}
62
+
63
+ rule_sets.each do |rule_set|
64
+ rule_set.expand_shorthand!
65
+
66
+ specificity = rule_set.specificity
67
+ unless specificity
68
+ if rule_set.selectors.length == 1
69
+ specificity = calculate_specificity(rule_set.selectors[0])
70
+ else
71
+ specificity = 0
72
+ end
73
+ end
74
+
75
+ rule_set.each_declaration do |property, value, is_important|
76
+ # Add the property to the list to be folded per http://www.w3.org/TR/CSS21/cascade.html#cascading-order
77
+ if not properties.has_key?(property) or
78
+ is_important or # step 2
79
+ properties[property][:specificity] < specificity or # step 3
80
+ properties[property][:specificity] == specificity # step 4
81
+ properties[property] = {:value => value, :specificity => specificity, :is_important => is_important}
82
+ end
83
+ end
84
+ end
85
+
86
+ merged = RuleSet.new(nil, nil)
87
+
88
+ # TODO: what about important
89
+ properties.each do |property, details|
90
+ merged[property.strip] = details[:value].strip
91
+ end
92
+
93
+ merged.create_shorthand!
94
+ merged
95
+ end
96
+
97
+ # Calculates the specificity of a CSS selector
98
+ # per http://www.w3.org/TR/CSS21/cascade.html#specificity
99
+ #
100
+ # Returns an integer.
101
+ #
102
+ # ==== Example
103
+ # CssParser.calculate_specificity('#content div p:first-line a:link')
104
+ # => 114
105
+ #--
106
+ # Thanks to Rafael Salazar and Nick Fitzsimons on the css-discuss list for their help.
107
+ #++
108
+ def CssParser.calculate_specificity(selector)
109
+ a = 0
110
+ b = selector.scan(/\#/).length
111
+ c = selector.scan(NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX).length
112
+ d = selector.scan(ELEMENTS_AND_PSEUDO_ELEMENTS_RX).length
113
+
114
+ (a.to_s + b.to_s + c.to_s + d.to_s).to_i
115
+ rescue
116
+ return 0
117
+ end
118
+
119
+ # Make <tt>url()</tt> links absolute.
120
+ #
121
+ # Takes a block of CSS and returns it with all relative URIs converted to absolute URIs.
122
+ #
123
+ # "For CSS style sheets, the base URI is that of the style sheet, not that of the source document."
124
+ # per http://www.w3.org/TR/CSS21/syndata.html#uri
125
+ #
126
+ # Returns a string.
127
+ #
128
+ # ==== Example
129
+ # CssParser.convert_uris("body { background: url('../style/yellow.png?abc=123') };",
130
+ # "http://example.org/style/basic.css").inspect
131
+ # => "body { background: url('http://example.org/style/yellow.png?abc=123') };"
132
+ def self.convert_uris(css, base_uri)
133
+ out = ''
134
+ base_uri = URI.parse(base_uri) unless base_uri.kind_of?(URI)
135
+
136
+ out = css.gsub(URI_RX) do |s|
137
+ uri = $1.to_s
138
+ uri.gsub!(/["']+/, '')
139
+ # Don't process URLs that are already absolute
140
+ unless uri =~ /^[a-z]+\:\/\//i
141
+ begin
142
+ uri = base_uri.merge(uri)
143
+ rescue; end
144
+ end
145
+ "url('" + uri.to_s + "')"
146
+ end
147
+ out
148
+ end
149
+ end
@@ -0,0 +1,4 @@
1
+ @import "import-circular-reference.css";
2
+
3
+ body { color: black; background: white; }
4
+ p { margin: 0px; }
@@ -0,0 +1,3 @@
1
+ @import "simple.css" print, tv, screen;
2
+
3
+ div { color: lime; }
@@ -0,0 +1,3 @@
1
+ @import 'subdir/import2.css';
2
+
3
+ div { color: lime; }
@@ -0,0 +1,6 @@
1
+ body {
2
+ color: black;
3
+ background: white;
4
+ }
5
+
6
+ p { margin: 0px; }
@@ -0,0 +1,3 @@
1
+ @import "../simple.css";
2
+
3
+ a { text-decoration: none; }
@@ -0,0 +1,56 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ # Test cases for reading and generating CSS shorthand properties
4
+ class CssParserBasicTests < Test::Unit::TestCase
5
+ include CssParser
6
+
7
+ def setup
8
+ @cp = CssParser::Parser.new
9
+ @css = <<-EOT
10
+ html, body, p { margin: 0px; }
11
+ p { padding: 0px; }
12
+ #content { font: 12px/normal sans-serif; }
13
+ EOT
14
+ end
15
+
16
+ def test_finding_by_selector
17
+ @cp.add_block!(@css)
18
+ assert_equal 'margin: 0px;', @cp.find_by_selector('body').join(' ')
19
+ assert_equal 'margin: 0px; padding: 0px;', @cp.find_by_selector('p').join(' ')
20
+ end
21
+
22
+ def test_adding_block
23
+ @cp.add_block!(@css)
24
+ assert_equal 'margin: 0px;', @cp.find_by_selector('body').join
25
+ end
26
+
27
+ def test_adding_a_rule
28
+ @cp.add_rule!('div', 'color: blue;')
29
+ assert_equal 'color: blue;', @cp.find_by_selector('div').join(' ')
30
+ end
31
+
32
+ def test_adding_a_rule_set
33
+ rs = CssParser::RuleSet.new('div', 'color: blue;')
34
+ @cp.add_rule_set!(rs)
35
+ assert_equal 'color: blue;', @cp.find_by_selector('div').join(' ')
36
+ end
37
+
38
+ def test_toggling_uri_conversion
39
+ # with conversion
40
+ cp_with_conversion = Parser.new(:absolute_paths => true)
41
+ cp_with_conversion.add_block!("body { background: url('../style/yellow.png?abc=123') };",
42
+ :base_uri => 'http://example.org/style/basic.css')
43
+
44
+ assert_equal "background: url('http://example.org/style/yellow.png?abc=123');",
45
+ cp_with_conversion['body'].join(' ')
46
+
47
+ # without conversion
48
+ cp_without_conversion = Parser.new(:absolute_paths => false)
49
+ cp_without_conversion.add_block!("body { background: url('../style/yellow.png?abc=123') };",
50
+ :base_uri => 'http://example.org/style/basic.css')
51
+
52
+ assert_equal "background: url('../style/yellow.png?abc=123');",
53
+ cp_without_conversion['body'].join(' ')
54
+ end
55
+
56
+ end
@@ -0,0 +1,81 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ # Test cases for the CssParser's downloading functions.
4
+ class CssParserDownloadingTests < Test::Unit::TestCase
5
+ include CssParser
6
+ include WEBrick
7
+
8
+ def setup
9
+ # from http://nullref.se/blog/2006/5/17/testing-with-webrick
10
+ @cp = Parser.new
11
+
12
+ @uri_base = 'http://localhost:12000'
13
+
14
+ www_root = File.dirname(__FILE__) + '/fixtures/'
15
+
16
+ @server_thread = Thread.new do
17
+ s = WEBrick::HTTPServer.new(:Port => 12000, :DocumentRoot => www_root, :Logger => Log.new(nil, BasicLog::ERROR), :AccessLog => [])
18
+ @port = s.config[:Port]
19
+ begin
20
+ s.start
21
+ ensure
22
+ s.shutdown
23
+ end
24
+ end
25
+
26
+ sleep 1 # ensure the server has time to load
27
+ end
28
+
29
+ def teardown
30
+ @server_thread.kill
31
+ @server_thread.join(5)
32
+ @server_thread = nil
33
+ end
34
+
35
+ def test_loading_a_remote_file
36
+ @cp.load_uri!("#{@uri_base}/simple.css")
37
+ assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
38
+ end
39
+
40
+ def test_following_at_import_rules
41
+ @cp.load_uri!("#{@uri_base}/import1.css")
42
+
43
+ # from '/import1.css'
44
+ assert_equal 'color: lime;', @cp.find_by_selector('div').join(' ')
45
+
46
+ # from '/subdir/import2.css'
47
+ assert_equal 'text-decoration: none;', @cp.find_by_selector('a').join(' ')
48
+
49
+ # from '/subdir/../simple.css'
50
+ assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
51
+ end
52
+
53
+ def test_importing_with_media_types
54
+ @cp.load_uri!("#{@uri_base}/import-with-media-types.css")
55
+
56
+ # from simple.css with :screen media type
57
+ assert_equal 'margin: 0px;', @cp.find_by_selector('p', :screen).join(' ')
58
+ assert_equal '', @cp.find_by_selector('p', :tty).join(' ')
59
+ end
60
+
61
+ def test_throwing_circular_reference_exception
62
+ assert_raise CircularReferenceError do
63
+ @cp.load_uri!("#{@uri_base}/import-circular-reference.css")
64
+ end
65
+ end
66
+
67
+ def test_toggling_not_found_exceptions
68
+ cp_with_exceptions = Parser.new(:io_exceptions => true)
69
+
70
+ assert_raise RemoteFileError do
71
+ cp_with_exceptions.load_uri!("#{@uri_base}/no-exist.xyz")
72
+ end
73
+
74
+ cp_without_exceptions = Parser.new(:io_exceptions => false)
75
+
76
+ assert_nothing_raised RemoteFileError do
77
+ cp_without_exceptions.load_uri!("#{@uri_base}/no-exist.xyz")
78
+ end
79
+ end
80
+
81
+ end