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.
- 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
|