css_parser 1.1.9 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,4 @@
1
+ require 'addressable/uri'
1
2
  require 'uri'
2
3
  require 'net/https'
3
4
  require 'open-uri'
@@ -7,7 +8,7 @@ require 'stringio'
7
8
  require 'iconv'
8
9
 
9
10
  module CssParser
10
- VERSION = '1.1.9'
11
+ VERSION = '1.2.0'
11
12
 
12
13
  # Merge multiple CSS RuleSets by cascading according to the CSS 2.1 cascading rules
13
14
  # (http://www.w3.org/TR/REC-CSS2/cascade.html#cascading-order).
@@ -139,21 +140,19 @@ module CssParser
139
140
  # "http://example.org/style/basic.css").inspect
140
141
  # => "body { background: url('http://example.org/style/yellow.png?abc=123') };"
141
142
  def self.convert_uris(css, base_uri)
142
- out = ''
143
- base_uri = URI.parse(base_uri) unless base_uri.kind_of?(URI)
143
+ base_uri = Addressable::URI.parse(base_uri) unless base_uri.kind_of?(Addressable::URI)
144
144
 
145
- out = css.gsub(URI_RX) do |s|
145
+ css.gsub(URI_RX) do
146
146
  uri = $1.to_s
147
147
  uri.gsub!(/["']+/, '')
148
148
  # Don't process URLs that are already absolute
149
149
  unless uri =~ /^[a-z]+\:\/\//i
150
150
  begin
151
- uri = base_uri.merge(uri)
151
+ uri = base_uri + uri
152
152
  rescue; end
153
153
  end
154
- "url('" + uri.to_s + "')"
154
+ "url('#{uri.to_s}')"
155
155
  end
156
- out
157
156
  end
158
157
  end
159
158
 
@@ -121,7 +121,7 @@ module CssParser
121
121
  import_path = import_rule[0].to_s.gsub(/['"]*/, '').strip
122
122
 
123
123
  if options[:base_uri]
124
- import_uri = URI.parse(options[:base_uri].to_s).merge(import_path)
124
+ import_uri = Addressable::URI.parse(options[:base_uri].to_s) + Addressable::URI.parse(import_path)
125
125
  load_uri!(import_uri, options[:base_uri], media_types)
126
126
  elsif options[:base_dir]
127
127
  load_file!(import_path, options[:base_dir], media_types)
@@ -293,7 +293,7 @@ module CssParser
293
293
  #
294
294
  # Deprecated: originally accepted three params: `uri`, `base_uri` and `media_types`
295
295
  def load_uri!(uri, options = {}, deprecated = nil)
296
- uri = URI.parse(uri) unless uri.respond_to? :scheme
296
+ uri = Addressable::URI.parse(uri) unless uri.respond_to? :scheme
297
297
  #base_uri = nil, media_types = :all, options = {}
298
298
 
299
299
  opts = {:base_uri => nil, :media_types => :all}
@@ -324,6 +324,7 @@ module CssParser
324
324
  def load_file!(file_name, base_dir = nil, media_types = :all)
325
325
  file_name = File.expand_path(file_name, base_dir)
326
326
  return unless File.readable?(file_name)
327
+ return unless circular_reference_check(file_name)
327
328
 
328
329
  src = IO.read(file_name)
329
330
  base_dir = File.dirname(file_name)
@@ -334,6 +335,21 @@ module CssParser
334
335
 
335
336
 
336
337
  protected
338
+ # Check that a path hasn't been loaded already
339
+ #
340
+ # Raises a CircularReferenceError exception if io_exceptions are on,
341
+ # otherwise returns true/false.
342
+ def circular_reference_check(path)
343
+ path = path.to_s
344
+ if @loaded_uris.include?(path)
345
+ raise CircularReferenceError, "can't load #{path} more than once" if @options[:io_exceptions]
346
+ return false
347
+ else
348
+ @loaded_uris << path
349
+ return true
350
+ end
351
+ end
352
+
337
353
  # Strip comments and clean up blank lines from a block of CSS.
338
354
  #
339
355
  # Returns a string.
@@ -358,18 +374,12 @@ module CssParser
358
374
  # TODO: add option to fail silently or throw and exception on a 404
359
375
  #++
360
376
  def read_remote_file(uri) # :nodoc:
361
- if @loaded_uris.include?(uri.to_s)
362
- raise CircularReferenceError, "can't load #{uri.to_s} more than once" if @options[:io_exceptions]
363
- return '', nil
364
- end
365
-
366
- @loaded_uris << uri.to_s
377
+ return nil, nil unless circular_reference_check(uri.to_s)
367
378
 
368
379
  src = '', charset = nil
369
380
 
370
381
  begin
371
- uri = URI.parse(uri.to_s)
372
- http = Net::HTTP.new(uri.host, uri.port)
382
+ uri = Addressable::URI.parse(uri.to_s)
373
383
 
374
384
  if uri.scheme == 'file'
375
385
  # local file
@@ -379,8 +389,12 @@ module CssParser
379
389
  else
380
390
  # remote file
381
391
  if uri.scheme == 'https'
392
+ uri.port = 443 unless uri.port
393
+ http = Net::HTTP.new(uri.host, uri.port)
382
394
  http.use_ssl = true
383
395
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
396
+ else
397
+ http = Net::HTTP.new(uri.host, uri.port)
384
398
  end
385
399
 
386
400
  res, src = http.get(uri.path, {'User-Agent' => USER_AGENT, 'Accept-Encoding' => 'gzip'})
@@ -1,4 +1,10 @@
1
1
  module CssParser
2
+
3
+
4
+ def self.regex_possible_values *values
5
+ Regexp.new("([\s]*^)?(#{values.join('|')})([\s]*$)?", 'i')
6
+ end
7
+
2
8
  # :stopdoc:
3
9
  # Base types
4
10
  RE_NL = Regexp.new('(\n|\r\n|\r|\f)')
@@ -12,6 +18,8 @@ module CssParser
12
18
  RE_STRING2 = Regexp.new('(\'(.[^\n\r\f\\\']*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\')')
13
19
  RE_STRING = Regexp.union(RE_STRING1, RE_STRING2)
14
20
 
21
+ RE_INHERIT = regex_possible_values 'inherit'
22
+
15
23
  RE_URI = Regexp.new('(url\([\s]*([\s]*' + RE_STRING.to_s + '[\s]*)[\s]*\))|(url\([\s]*([!#$%&*\-~]|' + RE_NON_ASCII.to_s + '|' + RE_ESCAPE.to_s + ')*[\s]*)\)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n')
16
24
  URI_RX = /url\(("([^"]*)"|'([^']*)'|([^)]*))\)/im
17
25
 
@@ -23,24 +31,62 @@ module CssParser
23
31
 
24
32
  #RE_AT_IMPORT_RULE = Regexp.new('@import[\s]*(' + RE_STRING.to_s + ')([\w\s\,]*)[;]?', Regexp::IGNORECASE) -- should handle url() even though it is not allowed
25
33
  #++
26
- IMPORTANT_IN_PROPERTY_RX = /[\s]*\!important[\s]*/i
34
+ IMPORTANT_IN_PROPERTY_RX = /[\s]*!important[\s]*/i
35
+
36
+ RE_INSIDE_OUTSIDE = regex_possible_values 'inside', 'outside'
37
+ RE_SCROLL_FIXED = regex_possible_values 'scroll', 'fixed'
38
+ RE_REPEAT = regex_possible_values 'repeat(\-x|\-y)*|no\-repeat'
39
+ RE_LIST_STYLE_TYPE = regex_possible_values 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', 'lower-roman',
40
+ 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha',
41
+ 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana',
42
+ 'hira-gana-iroha', 'katakana-iroha', 'katakana', 'none'
43
+
27
44
  STRIP_CSS_COMMENTS_RX = /\/\*.*?\*\//m
28
45
  STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m
29
46
 
30
47
  # Special units
31
48
  BOX_MODEL_UNITS_RX = /(auto|inherit|0|([\-]*([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%)))([\s;]|\Z)/imx
32
49
  RE_LENGTH_OR_PERCENTAGE = Regexp.new('([\-]*(([0-9]*\.[0-9]+)|[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%))', Regexp::IGNORECASE)
33
- RE_BACKGROUND_POSITION = Regexp.new("((#{RE_LENGTH_OR_PERCENTAGE})|left|center|right|top|bottom)", Regexp::IGNORECASE | Regexp::EXTENDED)
50
+ RE_BACKGROUND_POSITION = Regexp.new("((((#{RE_LENGTH_OR_PERCENTAGE})|left|center|right|top|bottom)[\s]*){1,2})", Regexp::IGNORECASE | Regexp::EXTENDED)
34
51
  FONT_UNITS_RX = /(([x]+\-)*small|medium|large[r]*|auto|inherit|([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%)*)/i
52
+ RE_BORDER_STYLE = /([\s]*^)?(none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset)([\s]*$)?/imx
53
+ RE_BORDER_UNITS = Regexp.union(BOX_MODEL_UNITS_RX, /(thin|medium|thick)/i)
54
+
35
55
 
36
56
  # Patterns for specificity calculations
37
- ELEMENTS_AND_PSEUDO_ELEMENTS_RX = /((^|[\s\+\>]+)[\w]+|\:(first\-line|first\-letter|before|after))/i
38
- NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = /(\.[\w]+)|(\[[\w]+)|(\:(link|first\-child|lang))/i
57
+ NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX= /
58
+ (\.[\w]+) # classes
59
+ |
60
+ \[(\w+) # attributes
61
+ |
62
+ (\:( # pseudo classes
63
+ link|visited|active
64
+ |hover|focus
65
+ |lang
66
+ |target
67
+ |enabled|disabled|checked|indeterminate
68
+ |root
69
+ |nth-child|nth-last-child|nth-of-type|nth-last-of-type
70
+ |first-child|last-child|first-of-type|last-of-type
71
+ |only-child|only-of-type
72
+ |empty|contains
73
+ ))
74
+ /ix
75
+ ELEMENTS_AND_PSEUDO_ELEMENTS_RX = /
76
+ ((^|[\s\+\>\~]+)[\w]+ # elements
77
+ |
78
+ \:{1,2}( # pseudo-elements
79
+ after|before
80
+ |first-letter|first-line
81
+ |selection
82
+ )
83
+ )/ix
39
84
 
40
85
  # Colours
41
- RE_COLOUR_RGB = Regexp.new('(rgb[\s]*\([\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*\))', Regexp::IGNORECASE)
86
+ RE_COLOUR_NUMERIC = Regexp.new('((hsl|rgb)[\s]*\([\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*\))', Regexp::IGNORECASE)
87
+ RE_COLOUR_NUMERIC_ALPHA = Regexp.new('((hsla|rgba)[\s]*\([\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*\))', Regexp::IGNORECASE)
42
88
  RE_COLOUR_HEX = /(#([0-9a-f]{6}|[0-9a-f]{3})([\s;]|$))/i
43
89
  RE_COLOUR_NAMED = /([\s]*^)?(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|transparent)([\s]*$)?/i
44
- RE_COLOUR = Regexp.union(RE_COLOUR_RGB, RE_COLOUR_HEX, RE_COLOUR_NAMED)
90
+ RE_COLOUR = Regexp.union(RE_COLOUR_NUMERIC, RE_COLOUR_NUMERIC_ALPHA, RE_COLOUR_HEX, RE_COLOUR_NAMED)
45
91
  # :startdoc:
46
92
  end
@@ -4,6 +4,9 @@ module CssParser
4
4
  RE_ELEMENTS_AND_PSEUDO_ELEMENTS = /((^|[\s\+\>]+)[\w]+|\:(first\-line|first\-letter|before|after))/i
5
5
  RE_NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES = /(\.[\w]+)|(\[[\w]+)|(\:(link|first\-child|lang))/i
6
6
 
7
+ BACKGROUND_PROPERTIES = ['background-color', 'background-image', 'background-repeat', 'background-position', 'background-attachment']
8
+ LIST_STYLE_PROPERTIES = ['list-style-type', 'list-style-position', 'list-style-image']
9
+
7
10
  # Array of selector strings.
8
11
  attr_reader :selectors
9
12
 
@@ -109,65 +112,111 @@ module CssParser
109
112
  importance = (options[:force_important] || is_important) ? ' !important' : ''
110
113
  str += "#{prop}: #{val}#{importance}; "
111
114
  end
112
- str.gsub(/^[\s]+|[\n\r\f\t]*|[\s]+$/mx, '').strip
115
+ str.gsub(/^[\s^(\{)]+|[\n\r\f\t]*|[\s]+$/mx, '').strip
113
116
  end
114
117
 
115
118
  # Return the CSS rule set as a string.
116
119
  def to_s
117
120
  decs = declarations_to_s
118
- "#{@selectors} { #{decs} }"
121
+ "#{@selectors.join} { #{decs} }"
119
122
  end
120
123
 
121
124
  # Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
122
125
  def expand_shorthand!
126
+ # border must be expanded before dimensions
127
+ expand_border_shorthand!
123
128
  expand_dimensions_shorthand!
124
129
  expand_font_shorthand!
125
130
  expand_background_shorthand!
131
+ expand_list_style_shorthand!
126
132
  end
127
133
 
128
- # Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
129
- def create_shorthand!
130
- create_background_shorthand!
131
- create_dimensions_shorthand!
132
- create_font_shorthand!
134
+ # Convert shorthand background declarations (e.g. <tt>background: url("chess.png") gray 50% repeat fixed;</tt>)
135
+ # into their constituent parts.
136
+ #
137
+ # See http://www.w3.org/TR/CSS21/colors.html#propdef-background
138
+ def expand_background_shorthand! # :nodoc:
139
+ return unless @declarations.has_key?('background')
140
+
141
+ value = @declarations['background'][:value]
142
+
143
+ if value =~ CssParser::RE_INHERIT
144
+ BACKGROUND_PROPERTIES.each do |prop|
145
+ split_declaration('background', prop, 'inherit')
146
+ end
147
+ end
148
+
149
+ split_declaration('background', 'background-image', value.slice!(Regexp.union(CssParser::URI_RX, /none/i)))
150
+ split_declaration('background', 'background-attachment', value.slice!(CssParser::RE_SCROLL_FIXED))
151
+ split_declaration('background', 'background-repeat', value.slice!(CssParser::RE_REPEAT))
152
+ split_declaration('background', 'background-color', value.slice!(CssParser::RE_COLOUR))
153
+ split_declaration('background', 'background-position', value.slice(CssParser::RE_BACKGROUND_POSITION))
154
+
155
+ @declarations.delete('background')
156
+ end
157
+
158
+ # Split shorthand border declarations (e.g. <tt>border: 1px red;</tt>)
159
+ # Additional splitting happens in expand_dimensions_shorthand!
160
+ def expand_border_shorthand! # :nodoc:
161
+ ['border', 'border-left', 'border-right', 'border-top', 'border-bottom'].each do |k|
162
+ next unless @declarations.has_key?(k)
163
+
164
+ value = @declarations[k][:value]
165
+
166
+ split_declaration(k, "#{k}-width", value.slice!(CssParser::RE_BORDER_UNITS))
167
+ split_declaration(k, "#{k}-color", value.slice!(CssParser::RE_COLOUR))
168
+ split_declaration(k, "#{k}-style", value.slice!(CssParser::RE_BORDER_STYLE))
169
+
170
+ @declarations.delete(k)
171
+ end
133
172
  end
134
173
 
135
174
  # Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)
136
- # into their constituent parts.
175
+ # into their constituent parts. Handles margin, padding, border-color, border-style and border-width.
137
176
  def expand_dimensions_shorthand! # :nodoc:
138
- ['margin', 'padding'].each do |property|
177
+ {'margin' => 'margin-%s',
178
+ 'padding' => 'padding-%s',
179
+ 'border-color' => 'border-%s-color',
180
+ 'border-style' => 'border-%s-style',
181
+ 'border-width' => 'border-%s-width'}.each do |property, expanded|
139
182
 
140
183
  next unless @declarations.has_key?(property)
141
184
 
142
185
  value = @declarations[property][:value]
143
- is_important = @declarations[property][:is_important]
144
- order = @declarations[property][:order]
145
- t, r, b, l = nil
146
186
 
147
- matches = value.scan(CssParser::BOX_MODEL_UNITS_RX)
187
+ # RGB and HSL values in borders are the only units that can have spaces (within params).
188
+ # We cheat a bit here by stripping spaces after commas in RGB and HSL values so that we
189
+ # can split easily on spaces.
190
+ #
191
+ # TODO: rgba, hsl, hsla
192
+ value.gsub!(RE_COLOUR) { |c| c.gsub(/[\s]+/, '') }
193
+
194
+ matches = value.strip.split(/[\s]+/)
195
+
196
+ t, r, b, l = nil
148
197
 
149
198
  case matches.length
150
199
  when 1
151
- t, r, b, l = matches[0][0], matches[0][0], matches[0][0], matches[0][0]
200
+ t, r, b, l = matches[0], matches[0], matches[0], matches[0]
152
201
  when 2
153
- t, b = matches[0][0], matches[0][0]
154
- r, l = matches[1][0], matches[1][0]
202
+ t, b = matches[0], matches[0]
203
+ r, l = matches[1], matches[1]
155
204
  when 3
156
- t = matches[0][0]
157
- r, l = matches[1][0], matches[1][0]
158
- b = matches[2][0]
205
+ t = matches[0]
206
+ r, l = matches[1], matches[1]
207
+ b = matches[2]
159
208
  when 4
160
- t = matches[0][0]
161
- r = matches[1][0]
162
- b = matches[2][0]
163
- l = matches[3][0]
209
+ t = matches[0]
210
+ r = matches[1]
211
+ b = matches[2]
212
+ l = matches[3]
164
213
  end
165
214
 
166
- values = { :is_important => is_important, :order => order }
167
- @declarations["#{property}-top"] = values.merge(:value => t.to_s)
168
- @declarations["#{property}-right"] = values.merge(:value => r.to_s)
169
- @declarations["#{property}-bottom"] = values.merge(:value => b.to_s)
170
- @declarations["#{property}-left"] = values.merge(:value => l.to_s)
215
+ split_declaration(property, expanded % 'top', t)
216
+ split_declaration(property, expanded % 'right', r)
217
+ split_declaration(property, expanded % 'bottom', b)
218
+ split_declaration(property, expanded % 'left', l)
219
+
171
220
  @declarations.delete(property)
172
221
  end
173
222
  end
@@ -227,92 +276,105 @@ module CssParser
227
276
  @declarations.delete('font')
228
277
  end
229
278
 
230
-
231
- # Convert shorthand background declarations (e.g. <tt>background: url("chess.png") gray 50% repeat fixed;</tt>)
279
+ # Convert shorthand list-style declarations (e.g. <tt>list-style: lower-alpha outside;</tt>)
232
280
  # into their constituent parts.
233
281
  #
234
- # See http://www.w3.org/TR/CSS21/colors.html#propdef-background
235
- def expand_background_shorthand! # :nodoc:
236
- return unless @declarations.has_key?('background')
237
-
238
- value = @declarations['background'][:value]
239
- is_important = @declarations['background'][:is_important]
240
- order = @declarations['background'][:order]
241
-
242
- bg_props = {}
282
+ # See http://www.w3.org/TR/CSS21/generate.html#lists
283
+ def expand_list_style_shorthand! # :nodoc:
284
+ return unless @declarations.has_key?('list-style')
243
285
 
286
+ value = @declarations['list-style'][:value]
244
287
 
245
- if m = value.match(Regexp.union(CssParser::URI_RX, /none/i)).to_s
246
- bg_props['background-image'] = m.strip unless m.empty?
247
- value.gsub!(Regexp.union(CssParser::URI_RX, /none/i), '')
248
- end
249
-
250
- if m = value.match(/([\s]*^)?(scroll|fixed)([\s]*$)?/i).to_s
251
- bg_props['background-attachment'] = m.strip unless m.empty?
288
+ if value =~ CssParser::RE_INHERIT
289
+ LIST_STYLE_PROPERTIES.each do |prop|
290
+ split_declaration('list-style', prop, 'inherit')
291
+ end
252
292
  end
253
293
 
254
- if m = value.match(/([\s]*^)?(repeat(\-x|\-y)*|no\-repeat)([\s]*$)?/i).to_s
255
- bg_props['background-repeat'] = m.strip unless m.empty?
256
- end
294
+ split_declaration('list-style', 'list-style-type', value.slice!(CssParser::RE_LIST_STYLE_TYPE))
295
+ split_declaration('list-style', 'list-style-position', value.slice!(CssParser::RE_INSIDE_OUTSIDE))
296
+ split_declaration('list-style', 'list-style-image', value.slice!(Regexp.union(CssParser::URI_RX, /none/i)))
257
297
 
258
- if m = value.match(CssParser::RE_COLOUR).to_s
259
- bg_props['background-color'] = m.strip unless m.empty?
260
- end
298
+ @declarations.delete('list-style')
299
+ end
261
300
 
262
- value.scan(CssParser::RE_BACKGROUND_POSITION).each do |m|
263
- if bg_props.has_key?('background-position')
264
- bg_props['background-position'] += ' ' + m[0].to_s.strip unless m.empty?
265
- else
266
- bg_props['background-position'] = m[0].to_s.strip unless m.empty?
267
- end
268
- end
301
+ # Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
302
+ def create_shorthand!
303
+ create_background_shorthand!
304
+ create_dimensions_shorthand!
305
+ # border must be shortened after dimensions
306
+ create_border_shorthand!
307
+ create_font_shorthand!
308
+ create_list_style_shorthand!
309
+ end
269
310
 
311
+ # Combine several properties into a shorthand one
312
+ def create_shorthand_properties! properties, shorthand_property # :nodoc:
313
+ values = []
314
+ properties.each do |property|
315
+ if @declarations.has_key?(property) and not @declarations[property][:is_important]
316
+ values << @declarations[property][:value]
317
+ @declarations.delete(property)
318
+ end
319
+ end
270
320
 
271
- if value =~ /([\s]*^)?inherit([\s]*$)?/i
272
- ['background-color', 'background-image', 'background-attachment', 'background-repeat', 'background-position'].each do |prop|
273
- bg_props["#{prop}"] = 'inherit' unless bg_props.has_key?(prop) and not bg_props[prop].empty?
274
- end
321
+ unless values.empty?
322
+ @declarations[shorthand_property] = {:value => values.join(' ')}
275
323
  end
276
-
277
- bg_props.each { |bg_prop, bg_val| @declarations[bg_prop] = {:value => bg_val, :is_important => is_important, :order => order} }
278
-
279
- @declarations.delete('background')
280
324
  end
281
325
 
282
-
283
- # Looks for long format CSS background properties (e.g. <tt>background-color</tt>) and
326
+ # Looks for long format CSS background properties (e.g. <tt>background-color</tt>) and
284
327
  # converts them into a shorthand CSS <tt>background</tt> property.
285
328
  #
286
329
  # Leaves properties declared !important alone.
287
330
  def create_background_shorthand! # :nodoc:
288
- new_value = ''
289
- ['background-color', 'background-image', 'background-repeat',
290
- 'background-position', 'background-attachment'].each do |property|
331
+ create_shorthand_properties! BACKGROUND_PROPERTIES, 'background'
332
+ end
333
+
334
+ # Combine border-color, border-style and border-width into border
335
+ # Should be run after create_dimensions_shorthand!
336
+ #
337
+ # TODO: this is extremely similar to create_background_shorthand! and should be combined
338
+ def create_border_shorthand! # :nodoc:
339
+ values = []
340
+
341
+ ['border-width', 'border-style', 'border-color'].each do |property|
291
342
  if @declarations.has_key?(property) and not @declarations[property][:is_important]
292
- new_value += @declarations[property][:value] + ' '
293
- @declarations.delete(property)
343
+ # can't merge if any value contains a space (i.e. has multiple values)
344
+ # we temporarily remove any spaces after commas for the check (inside rgba, etc...)
345
+ return if @declarations[property][:value].gsub(/\,[\s]/, ',').strip =~ /[\s]/
346
+ values << @declarations[property][:value]
294
347
  end
295
348
  end
296
349
 
297
- unless new_value.strip.empty?
298
- @declarations['background'] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
350
+ @declarations.delete('border-width')
351
+ @declarations.delete('border-style')
352
+ @declarations.delete('border-color')
353
+
354
+ unless values.empty?
355
+ @declarations['border'] = {:value => values.join(' ')}
299
356
  end
300
357
  end
301
-
302
- # Looks for long format CSS dimensional properties (i.e. <tt>margin</tt> and <tt>padding</tt>) and
303
- # converts them into shorthand CSS properties.
358
+
359
+ # Looks for long format CSS dimensional properties (margin, padding, border-color, border-style and border-width)
360
+ # and converts them into shorthand CSS properties.
304
361
  def create_dimensions_shorthand! # :nodoc:
305
- # geometric
306
362
  directions = ['top', 'right', 'bottom', 'left']
307
- ['margin', 'padding'].each do |property|
308
- values = {}
309
363
 
310
- foldable = @declarations.select { |dim, val| dim == "#{property}-top" or dim == "#{property}-right" or dim == "#{property}-bottom" or dim == "#{property}-left" }
364
+ {'margin' => 'margin-%s',
365
+ 'padding' => 'padding-%s',
366
+ 'border-color' => 'border-%s-color',
367
+ 'border-style' => 'border-%s-style',
368
+ 'border-width' => 'border-%s-width'}.each do |property, expanded|
369
+
370
+ foldable = @declarations.select do |dim, val|
371
+ dim == expanded % 'top' or dim == expanded % 'right' or dim == expanded % 'bottom' or dim == expanded % 'left'
372
+ end
311
373
  # All four dimensions must be present
312
374
  if foldable.length == 4
313
375
  values = {}
314
376
 
315
- directions.each { |d| values[d.to_sym] = @declarations["#{property}-#{d}"][:value].downcase.strip }
377
+ directions.each { |d| values[d.to_sym] = @declarations[expanded % d][:value].downcase.strip }
316
378
 
317
379
  if values[:left] == values[:right]
318
380
  if values[:top] == values[:bottom]
@@ -326,17 +388,15 @@ module CssParser
326
388
  end
327
389
  else # No sides are equal
328
390
  new_value = values[:top] + ' ' + values[:right] + ' ' + values[:bottom] + ' ' + values[:left]
329
- end # done creating 'new_value'
330
-
331
- # Save the new value
332
- unless new_value.strip.empty?
333
- @declarations[property] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
334
391
  end
335
392
 
336
- # Delete the shorthand values
337
- directions.each { |d| @declarations.delete("#{property}-#{d}") }
393
+ new_value.strip!
394
+ @declarations[property] = {:value => new_value.strip} unless new_value.empty?
395
+
396
+ # Delete the longhand values
397
+ directions.each { |d| @declarations.delete(expanded % d) }
338
398
  end
339
- end # done iterating through margin and padding
399
+ end
340
400
  end
341
401
 
342
402
 
@@ -373,7 +433,33 @@ module CssParser
373
433
 
374
434
  end
375
435
 
436
+ # Looks for long format CSS list-style properties (e.g. <tt>list-style-type</tt>) and
437
+ # converts them into a shorthand CSS <tt>list-style</tt> property.
438
+ #
439
+ # Leaves properties declared !important alone.
440
+ def create_list_style_shorthand! # :nodoc:
441
+ create_shorthand_properties! LIST_STYLE_PROPERTIES, 'list-style'
442
+ end
443
+
376
444
  private
445
+
446
+ # utility method for re-assign shorthand elements to longhand versions
447
+ def split_declaration(src, dest, v) # :nodoc:
448
+ return unless v and not v.empty?
449
+
450
+ if @declarations.has_key?(dest)
451
+ #puts "dest #{dest} already exists"
452
+
453
+ if @declarations[dest][:order] > @declarations[src][:order]
454
+ #puts "skipping #{dest}:#{v} due to order "
455
+ return
456
+ else
457
+ @declarations[dest] = {}
458
+ end
459
+ end
460
+ @declarations[dest] = @declarations[src].merge({:value => v.to_s.strip})
461
+ end
462
+
377
463
  def parse_declarations!(block) # :nodoc:
378
464
  @declarations = {}
379
465
 
@@ -52,7 +52,7 @@ class CssParserLoadingTests < Test::Unit::TestCase
52
52
  # http://github.com/alexdunae/css_parser/issues#issue/4
53
53
  def test_loading_a_remote_file_over_ssl
54
54
  # TODO: test SSL locally
55
- @cp.load_uri!("https://www.expresspardons.com/inc/screen.css")
55
+ @cp.load_uri!("https://dialect.ca/inc/screen.css")
56
56
  assert_match /margin\: 0\;/, @cp.find_by_selector('body').join(' ')
57
57
  end
58
58
 
@@ -83,6 +83,14 @@ class CssParserLoadingTests < Test::Unit::TestCase
83
83
  # from '/subdir/../simple.css'
84
84
  assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
85
85
  end
86
+
87
+ def test_following_badly_escaped_import_rules
88
+ css_block = '@import "http://example.com/css?family=Droid+Sans:regular,bold|Droid+Serif:regular,italic,bold,bolditalic&subset=latin";'
89
+
90
+ assert_nothing_raised do
91
+ @cp.add_block!(css_block, :base_uri => "#{@uri_base}/subdir/")
92
+ end
93
+ end
86
94
 
87
95
  def test_following_at_import_rules_from_add_block
88
96
  css_block = '@import "../simple.css";'
@@ -101,20 +109,19 @@ class CssParserLoadingTests < Test::Unit::TestCase
101
109
  assert_equal '', @cp.find_by_selector('p', :tty).join(' ')
102
110
  end
103
111
 
104
- def test_throwing_circular_reference_exception
112
+ def test_local_circular_reference_exception
105
113
  assert_raise CircularReferenceError do
106
- @cp.load_uri!("#{@uri_base}/import-circular-reference.css")
114
+ @cp.load_file!(File.dirname(__FILE__) + '/fixtures/import-circular-reference.css')
107
115
  end
116
+ end
108
117
 
109
- cp_without_exceptions = Parser.new(:io_exceptions => false)
110
-
111
- assert_nothing_raised CircularReferenceError do
112
- cp_without_exceptions.load_uri!("#{@uri_base}/import-circular-reference.css")
118
+ def test_remote_circular_reference_exception
119
+ assert_raise CircularReferenceError do
120
+ @cp.load_uri!("#{@uri_base}/import-circular-reference.css")
113
121
  end
114
-
115
122
  end
116
123
 
117
- def test_toggling_circular_reference_exception
124
+ def test_suppressing_circular_reference_exceptions
118
125
  cp_without_exceptions = Parser.new(:io_exceptions => false)
119
126
 
120
127
  assert_nothing_raised CircularReferenceError do
@@ -139,4 +139,26 @@ class CssParserTests < Test::Unit::TestCase
139
139
  assert_equal ".specs {font-family:Helvetica;font-weight:bold;font-style:italic;color:#008CA8;font-size:1.4em;list-style-image:url('http://www.example.org/directory/images/bullet.gif');}", converted_css
140
140
  end
141
141
 
142
+ def test_ruleset_with_braces
143
+ =begin
144
+ parser = Parser.new
145
+ parser.add_block!("div { background-color: black !important; }")
146
+ parser.add_block!("div { background-color: red; }")
147
+
148
+ rulesets = []
149
+
150
+ parser['div'].each do |declaration|
151
+ rulesets << RuleSet.new('div', declaration)
152
+ end
153
+
154
+ merged = CssParser.merge(rulesets)
155
+
156
+ result: # merged.to_s => "{ background-color: black !important; }"
157
+ =end
158
+
159
+ new_rule = RuleSet.new('div', "{ background-color: black !important; }")
160
+
161
+ assert_equal 'div { background-color: black !important; }', new_rule.to_s
162
+ end
163
+
142
164
  end
@@ -34,13 +34,13 @@ class RuleSetTests < Test::Unit::TestCase
34
34
  {:selector => "#content p", :declarations => "color: #fff;", :specificity => 101},
35
35
  {:selector => "a", :declarations => "color: #fff;", :specificity => 1}
36
36
  ]
37
-
37
+
38
38
  actual = []
39
39
  rs = RuleSet.new('#content p, a', 'color: #fff;')
40
40
  rs.each_selector do |sel, decs, spec|
41
41
  actual << {:selector => sel, :declarations => decs, :specificity => spec}
42
42
  end
43
-
43
+
44
44
  assert_equal(expected, actual)
45
45
  end
46
46
 
@@ -50,13 +50,13 @@ class RuleSetTests < Test::Unit::TestCase
50
50
  {:property => 'background', :value => 'white none no-repeat', :is_important => true},
51
51
  {:property => 'color', :value => '#fff', :is_important => false}
52
52
  ])
53
-
53
+
54
54
  actual = Set.new
55
55
  rs = RuleSet.new(nil, 'color: #fff; Background: white none no-repeat !important; margin: 1px -0.25em;')
56
56
  rs.each_declaration do |prop, val, imp|
57
57
  actual << {:property => prop, :value => val, :is_important => imp}
58
58
  end
59
-
59
+
60
60
  assert_equal(expected, actual)
61
61
  end
62
62
 
@@ -8,6 +8,35 @@ class RuleSetCreatingShorthandTests < Test::Unit::TestCase
8
8
  @cp = CssParser::Parser.new
9
9
  end
10
10
 
11
+ # ==== Border shorthand
12
+ def test_combining_borders_into_shorthand
13
+ properties = {'border-top-width' => 'auto', 'border-right-width' => 'thin', 'border-bottom-width' => 'auto', 'border-left-width' => '0px'}
14
+
15
+ combined = create_shorthand(properties)
16
+
17
+ assert_equal('', combined['border'])
18
+ assert_equal('auto thin auto 0px;', combined['border-width'])
19
+
20
+ # after creating shorthand, all long-hand properties should be deleted
21
+ assert_properties_are_deleted(combined, properties)
22
+
23
+ # should not combine if any properties are missing
24
+ properties.delete('border-top-width')
25
+
26
+ combined = create_shorthand(properties)
27
+
28
+ assert_equal '', combined['border-width']
29
+
30
+ properties = {'border-width' => '22%', 'border-color' => 'rgba(255, 0, 0)'}
31
+ combined = create_shorthand(properties)
32
+ assert_equal '22% rgba(255, 0, 0);', combined['border']
33
+ assert_equal '', combined['border-width']
34
+
35
+ properties = {'border-top-style' => 'none', 'border-right-style' => 'none', 'border-bottom-style' => 'none', 'border-left-style' => 'none'}
36
+ combined = create_shorthand(properties)
37
+ assert_equal 'none;', combined['border']
38
+ end
39
+
11
40
  # ==== Dimensions shorthand
12
41
  def test_combining_dimensions_into_shorthand
13
42
  properties = {'margin-right' => 'auto', 'margin-bottom' => '0px', 'margin-left' => 'auto', 'margin-top' => '0px',
@@ -64,6 +93,21 @@ class RuleSetCreatingShorthandTests < Test::Unit::TestCase
64
93
  assert_properties_are_deleted(combined, properties)
65
94
  end
66
95
 
96
+
97
+ # ==== List-style shorthand
98
+ def test_combining_list_style_into_shorthand
99
+ properties = {'list-style-image' => 'url(\'chess.png\')', 'list-style-type' => 'katakana',
100
+ 'list-style-position' => 'inside'}
101
+
102
+ combined = create_shorthand(properties)
103
+
104
+ assert_equal('katakana inside url(\'chess.png\');', combined['list-style'])
105
+
106
+ # after creating shorthand, all long-hand properties should be deleted
107
+ assert_properties_are_deleted(combined, properties)
108
+ end
109
+
110
+
67
111
  def test_property_values_in_url
68
112
  rs = RuleSet.new('#header', "background:url(http://example.com/1528/www/top-logo.jpg) no-repeat top right; padding: 79px 0 10px 0; text-align:left;")
69
113
  rs.expand_shorthand!
@@ -7,6 +7,26 @@ class RuleSetExpandingShorthandTests < Test::Unit::TestCase
7
7
  @cp = CssParser::Parser.new
8
8
  end
9
9
 
10
+ # ==== Dimensions shorthand
11
+ def test_expanding_border_shorthand
12
+ declarations = expand_declarations('border: none')
13
+ assert_equal 'none', declarations['border-right-style']
14
+
15
+ declarations = expand_declarations('border: 1px solid red')
16
+ assert_equal '1px', declarations['border-top-width']
17
+ assert_equal 'solid', declarations['border-bottom-style']
18
+
19
+ declarations = expand_declarations('border-color: red hsla(255, 0, 0, 5) rgb(2% ,2%,2%)')
20
+ assert_equal 'red', declarations['border-top-color']
21
+ assert_equal 'rgb(2%,2%,2%)', declarations['border-bottom-color']
22
+ assert_equal 'hsla(255,0,0,5)', declarations['border-left-color']
23
+
24
+ declarations = expand_declarations('border: thin dot-dot-dash')
25
+ assert_equal 'dot-dot-dash', declarations['border-left-style']
26
+ assert_equal 'thin', declarations['border-left-width']
27
+ assert_nil declarations['border-left-color']
28
+ end
29
+
10
30
  # ==== Dimensions shorthand
11
31
  def test_getting_dimensions_from_shorthand
12
32
  # test various shorthand forms
@@ -163,6 +183,31 @@ class RuleSetExpandingShorthandTests < Test::Unit::TestCase
163
183
  end
164
184
  end
165
185
 
186
+ # ==== List-style shorthand
187
+ def test_getting_list_style_properties_from_shorthand
188
+ expected = {'list-style-image' => 'url(\'chess.png\')', 'list-style-type' => 'katakana',
189
+ 'list-style-position' => 'inside'}
190
+
191
+ shorthand = "list-style: katakana inside url(\'chess.png\');"
192
+ declarations = expand_declarations(shorthand)
193
+ assert_equal expected, declarations
194
+ end
195
+
196
+ def test_getting_list_style_position_from_shorthand
197
+ ['inside', 'outside'].each do |position|
198
+ shorthand = "list-style: katakana #{position} url('chess.png');"
199
+ declarations = expand_declarations(shorthand)
200
+ assert_equal(position, declarations['list-style-position'])
201
+ end
202
+ end
203
+
204
+ def test_getting_list_style_type_from_shorthand
205
+ ['disc', 'circle', 'square', 'decimal', 'decimal-leading-zero', 'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha', 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana', 'katakana', 'hira-gana-iroha', 'katakana-iroha', 'none'].each do |type|
206
+ shorthand = "list-style: #{type} inside url('chess.png');"
207
+ declarations = expand_declarations(shorthand)
208
+ assert_equal(type, declarations['list-style-type'])
209
+ end
210
+ end
166
211
 
167
212
  protected
168
213
  def expand_declarations(declarations)
metadata CHANGED
@@ -1,32 +1,33 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: css_parser
3
- version: !ruby/object:Gem::Version
4
- prerelease: false
5
- segments:
6
- - 1
7
- - 1
8
- - 9
9
- version: 1.1.9
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.1
5
+ prerelease:
10
6
  platform: ruby
11
- authors:
7
+ authors:
12
8
  - Alex Dunae
13
9
  autorequire:
14
10
  bindir: bin
15
11
  cert_chain: []
16
-
17
- date: 2011-04-01 00:00:00 -07:00
18
- default_executable:
19
- dependencies: []
20
-
12
+ date: 2011-09-07 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: addressable
16
+ requirement: &2154706420 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2154706420
21
25
  description: A set of classes for parsing CSS in Ruby.
22
26
  email: code@dunae.ca
23
27
  executables: []
24
-
25
28
  extensions: []
26
-
27
29
  extra_rdoc_files: []
28
-
29
- files:
30
+ files:
30
31
  - lib/css_parser.rb
31
32
  - lib/css_parser/parser.rb
32
33
  - lib/css_parser/regexps.rb
@@ -46,43 +47,36 @@ files:
46
47
  - test/test_rule_set.rb
47
48
  - test/test_rule_set_creating_shorthand.rb
48
49
  - test/test_rule_set_expanding_shorthand.rb
49
- has_rdoc: true
50
- homepage: http://github.com/alexdunae/css_parser
50
+ homepage: https://github.com/alexdunae/css_parser
51
51
  licenses: []
52
-
53
52
  post_install_message:
54
- rdoc_options:
53
+ rdoc_options:
55
54
  - --all
56
55
  - --inline-source
57
56
  - --line-numbers
58
57
  - --charset
59
58
  - utf-8
60
- require_paths:
59
+ require_paths:
61
60
  - lib
62
- required_ruby_version: !ruby/object:Gem::Requirement
61
+ required_ruby_version: !ruby/object:Gem::Requirement
63
62
  none: false
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- segments:
68
- - 0
69
- version: "0"
70
- required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
68
  none: false
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- segments:
76
- - 0
77
- version: "0"
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
78
73
  requirements: []
79
-
80
74
  rubyforge_project:
81
- rubygems_version: 1.3.7
75
+ rubygems_version: 1.8.5
82
76
  signing_key:
83
77
  specification_version: 3
84
78
  summary: Ruby CSS parser.
85
- test_files:
79
+ test_files:
86
80
  - test/test_css_parser_basic.rb
87
81
  - test/test_css_parser_loading.rb
88
82
  - test/test_css_parser_media_types.rb