css_parser 1.1.9 → 1.2.1

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