css_parser 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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