css_parser 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +33 -0
- data/LICENSE +42 -0
- data/README +60 -0
- data/lib/css_parser/parser.rb +345 -0
- data/lib/css_parser/regexps.rb +46 -0
- data/lib/css_parser/rule_set.rb +381 -0
- data/lib/css_parser.rb +149 -0
- data/test/fixtures/import-circular-reference.css +4 -0
- data/test/fixtures/import-with-media-types.css +3 -0
- data/test/fixtures/import1.css +3 -0
- data/test/fixtures/simple.css +6 -0
- data/test/fixtures/subdir/import2.css +3 -0
- data/test/test_css_parser_basic.rb +56 -0
- data/test/test_css_parser_downloading.rb +81 -0
- data/test/test_css_parser_media_types.rb +71 -0
- data/test/test_css_parser_misc.rb +143 -0
- data/test/test_css_parser_regexps.rb +68 -0
- data/test/test_helper.rb +8 -0
- data/test/test_merging.rb +88 -0
- data/test/test_rule_set.rb +74 -0
- data/test/test_rule_set_creating_shorthand.rb +90 -0
- data/test/test_rule_set_expanding_shorthand.rb +178 -0
- metadata +82 -0
@@ -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,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
|