css_parser 1.5.0 → 1.12.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.
@@ -1,77 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
1
5
  module CssParser
2
6
  class RuleSet
3
7
  # 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
8
+ RE_ELEMENTS_AND_PSEUDO_ELEMENTS = /((^|[\s+>]+)\w+|:(first-line|first-letter|before|after))/i.freeze
9
+ RE_NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES = /(\.\w+)|(\[\w+)|(:(link|first-child|lang))/i.freeze
10
+
11
+ BACKGROUND_PROPERTIES = ['background-color', 'background-image', 'background-repeat', 'background-position', 'background-size', 'background-attachment'].freeze
12
+ LIST_STYLE_PROPERTIES = ['list-style-type', 'list-style-position', 'list-style-image'].freeze
13
+ FONT_STYLE_PROPERTIES = ['font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family'].freeze
14
+ BORDER_STYLE_PROPERTIES = ['border-width', 'border-style', 'border-color'].freeze
15
+ BORDER_PROPERTIES = ['border', 'border-left', 'border-right', 'border-top', 'border-bottom'].freeze
16
+
17
+ NUMBER_OF_DIMENSIONS = 4
18
+
19
+ DIMENSIONS = [
20
+ ['margin', %w[margin-top margin-right margin-bottom margin-left]],
21
+ ['padding', %w[padding-top padding-right padding-bottom padding-left]],
22
+ ['border-color', %w[border-top-color border-right-color border-bottom-color border-left-color]],
23
+ ['border-style', %w[border-top-style border-right-style border-bottom-style border-left-style]],
24
+ ['border-width', %w[border-top-width border-right-width border-bottom-width border-left-width]]
25
+ ].freeze
26
+
27
+ WHITESPACE_REPLACEMENT = '___SPACE___'
28
+
29
+ class Declarations
30
+ class Value
31
+ attr_reader :value
32
+ attr_accessor :important
33
+
34
+ def initialize(value, important: nil)
35
+ self.value = value
36
+ @important = important unless important.nil?
37
+ end
38
+
39
+ def value=(value)
40
+ value = value.to_s.sub(/\s*;\s*\Z/, '')
41
+ self.important = !value.slice!(CssParser::IMPORTANT_IN_PROPERTY_RX).nil?
42
+ value.strip!
43
+ raise ArgumentError, 'value is empty' if value.empty?
44
+
45
+ @value = value.freeze
46
+ end
47
+
48
+ def to_s
49
+ important ? "#{value} !important" : value
50
+ end
51
+
52
+ def ==(other)
53
+ return false unless other.is_a?(self.class)
54
+
55
+ value == other.value && important == other.important
56
+ end
57
+ end
58
+
59
+ extend Forwardable
60
+
61
+ def_delegators :declarations, :each
62
+
63
+ def initialize(declarations = {})
64
+ self.declarations = {}
65
+ declarations.each { |property, value| add_declaration!(property, value) }
66
+ end
67
+
68
+ # Add a CSS declaration
69
+ # @param [#to_s] property that should be added
70
+ # @param [Value, #to_s] value of the property
71
+ #
72
+ # @example
73
+ # declarations['color'] = 'blue'
74
+ #
75
+ # puts declarations['color']
76
+ # => #<CssParser::RuleSet::Declarations::Value:0x000000000305c730 @important=false, @order=1, @value="blue">
77
+ #
78
+ # @example
79
+ # declarations['margin'] = '0px auto !important'
80
+ #
81
+ # puts declarations['margin']
82
+ # => #<CssParser::RuleSet::Declarations::Value:0x00000000030c1838 @important=true, @order=2, @value="0px auto">
83
+ #
84
+ # If the property already exists its value will be over-written.
85
+ # If the value is empty - property will be deleted
86
+ def []=(property, value)
87
+ property = normalize_property(property)
88
+
89
+ if value.is_a?(Value)
90
+ declarations[property] = value
91
+ elsif value.to_s.strip.empty?
92
+ delete property
93
+ else
94
+ declarations[property] = Value.new(value)
95
+ end
96
+ rescue ArgumentError => e
97
+ raise e.exception, "#{property} #{e.message}"
98
+ end
99
+ alias add_declaration! []=
100
+
101
+ def [](property)
102
+ declarations[normalize_property(property)]
103
+ end
104
+ alias get_value []
105
+
106
+ def key?(property)
107
+ declarations.key?(normalize_property(property))
108
+ end
109
+
110
+ def size
111
+ declarations.size
112
+ end
113
+
114
+ # Remove CSS declaration
115
+ # @param [#to_s] property property to be removed
116
+ #
117
+ # @example
118
+ # declarations.delete('color')
119
+ def delete(property)
120
+ declarations.delete(normalize_property(property))
121
+ end
122
+ alias remove_declaration! delete
123
+
124
+ # Replace CSS property with multiple declarations
125
+ # @param [#to_s] property property name to be replaces
126
+ # @param [Hash<String => [String, Value]>] replacements hash with properties to replace with
127
+ #
128
+ # @example
129
+ # declarations = Declarations.new('line-height' => '0.25px', 'font' => 'small-caps', 'font-size' => '12em')
130
+ # declarations.replace_declaration!('font', {'line-height' => '1px', 'font-variant' => 'small-caps', 'font-size' => '24px'})
131
+ # declarations
132
+ # => #<CssParser::RuleSet::Declarations:0x00000000029c3018
133
+ # @declarations=
134
+ # {"line-height"=>#<CssParser::RuleSet::Declarations::Value:0x00000000038ac458 @important=false, @value="1px">,
135
+ # "font-variant"=>#<CssParser::RuleSet::Declarations::Value:0x00000000039b3ec8 @important=false, @value="small-caps">,
136
+ # "font-size"=>#<CssParser::RuleSet::Declarations::Value:0x00000000029c2c80 @important=false, @value="12em">}>
137
+ def replace_declaration!(property, replacements, preserve_importance: false)
138
+ property = normalize_property(property)
139
+ raise ArgumentError, "property #{property} does not exist" unless key?(property)
140
+
141
+ replacement_declarations = self.class.new(replacements)
142
+
143
+ if preserve_importance
144
+ importance = get_value(property).important
145
+ replacement_declarations.each { |_key, value| value.important = importance }
146
+ end
147
+
148
+ replacement_keys = declarations.keys
149
+ replacement_values = declarations.values
150
+ property_index = replacement_keys.index(property)
6
151
 
7
- BACKGROUND_PROPERTIES = ['background-color', 'background-image', 'background-repeat', 'background-position', 'background-size', 'background-attachment']
8
- LIST_STYLE_PROPERTIES = ['list-style-type', 'list-style-position', 'list-style-image']
152
+ # We should preserve subsequent declarations of the same properties
153
+ # and prior important ones if replacement one is not important
154
+ replacements = replacement_declarations.each.with_object({}) do |(key, replacement), result|
155
+ existing = declarations[key]
156
+
157
+ # No existing -> set
158
+ unless existing
159
+ result[key] = replacement
160
+ next
161
+ end
162
+
163
+ # Replacement more important than existing -> replace
164
+ if replacement.important && !existing.important
165
+ result[key] = replacement
166
+ replaced_index = replacement_keys.index(key)
167
+ replacement_keys.delete_at(replaced_index)
168
+ replacement_values.delete_at(replaced_index)
169
+ property_index -= 1 if replaced_index < property_index
170
+ next
171
+ end
172
+
173
+ # Existing is more important than replacement -> keep
174
+ next if !replacement.important && existing.important
175
+
176
+ # Existing and replacement importance are the same,
177
+ # value which is declared later wins
178
+ result[key] = replacement if property_index > replacement_keys.index(key)
179
+ end
180
+
181
+ return if replacements.empty?
182
+
183
+ replacement_keys.delete_at(property_index)
184
+ replacement_keys.insert(property_index, *replacements.keys)
185
+
186
+ replacement_values.delete_at(property_index)
187
+ replacement_values.insert(property_index, *replacements.values)
188
+
189
+ self.declarations = replacement_keys.zip(replacement_values).to_h
190
+ end
191
+
192
+ def to_s(options = {})
193
+ str = declarations.reduce(String.new) do |memo, (prop, value)|
194
+ importance = options[:force_important] || value.important ? ' !important' : ''
195
+ memo << "#{prop}: #{value.value}#{importance}; "
196
+ end
197
+ # TODO: Clean-up regexp doesn't seem to work
198
+ str.gsub!(/^[\s^({)]+|[\n\r\f\t]*|\s+$/mx, '')
199
+ str.strip!
200
+ str
201
+ end
202
+
203
+ def ==(other)
204
+ return false unless other.is_a?(self.class)
205
+
206
+ declarations == other.declarations && declarations.keys == other.declarations.keys
207
+ end
208
+
209
+ protected
210
+
211
+ attr_reader :declarations
212
+
213
+ private
214
+
215
+ attr_writer :declarations
216
+
217
+ def normalize_property(property)
218
+ property = property.to_s.downcase
219
+ property.strip!
220
+ property
221
+ end
222
+ end
223
+
224
+ extend Forwardable
9
225
 
10
226
  # Array of selector strings.
11
- attr_reader :selectors
227
+ attr_reader :selectors
12
228
 
13
229
  # Integer with the specificity to use for this RuleSet.
14
- attr_accessor :specificity
230
+ attr_accessor :specificity
231
+
232
+ # @!method add_declaration!
233
+ # @see CssParser::RuleSet::Declarations#add_declaration!
234
+ # @!method delete
235
+ # @see CssParser::RuleSet::Declarations#delete
236
+ def_delegators :declarations, :add_declaration!, :delete
237
+ alias []= add_declaration!
238
+ alias remove_declaration! delete
15
239
 
16
240
  def initialize(selectors, block, specificity = nil)
17
241
  @selectors = []
18
242
  @specificity = specificity
19
- @declarations = {}
20
- @order = 0
21
243
  parse_selectors!(selectors) if selectors
22
244
  parse_declarations!(block)
23
245
  end
24
246
 
25
247
  # Get the value of a property
26
248
  def get_value(property)
27
- return '' unless property and not property.empty?
28
-
29
- property = property.downcase.strip
30
- properties = @declarations.inject('') do |val, (key, data)|
31
- #puts "COMPARING #{key} #{key.inspect} against #{property} #{property.inspect}"
32
- importance = data[:is_important] ? ' !important' : ''
33
- val << "#{data[:value]}#{importance}; " if key.downcase.strip == property
34
- val
35
- end
36
- return properties ? properties.strip : ''
37
- end
38
- alias_method :[], :get_value
249
+ return '' unless (value = declarations[property])
39
250
 
40
- # Add a CSS declaration to the current RuleSet.
41
- #
42
- # rule_set.add_declaration!('color', 'blue')
43
- #
44
- # puts rule_set['color']
45
- # => 'blue;'
46
- #
47
- # rule_set.add_declaration!('margin', '0px auto !important')
48
- #
49
- # puts rule_set['margin']
50
- # => '0px auto !important;'
51
- #
52
- # If the property already exists its value will be over-written.
53
- def add_declaration!(property, value)
54
- if value.nil? or value.empty?
55
- @declarations.delete(property)
56
- return
57
- end
58
-
59
- value.gsub!(/;\Z/, '')
60
- is_important = !value.gsub!(CssParser::IMPORTANT_IN_PROPERTY_RX, '').nil?
61
- property = property.downcase.strip
62
- #puts "SAVING #{property} #{value} #{is_important.inspect}"
63
- @declarations[property] = {
64
- :value => value, :is_important => is_important, :order => @order += 1
65
- }
66
- end
67
- alias_method :[]=, :add_declaration!
68
-
69
- # Remove CSS declaration from the current RuleSet.
70
- #
71
- # rule_set.remove_declaration!('color')
72
- def remove_declaration!(property)
73
- @declarations.delete(property)
251
+ "#{value};"
74
252
  end
253
+ alias [] get_value
75
254
 
76
255
  # Iterate through selectors.
77
256
  #
@@ -83,41 +262,29 @@ module CssParser
83
262
  # ...
84
263
  # end
85
264
  def each_selector(options = {}) # :yields: selector, declarations, specificity
86
- declarations = declarations_to_s(options)
265
+ decs = declarations.to_s(options)
87
266
  if @specificity
88
- @selectors.each { |sel| yield sel.strip, declarations, @specificity }
267
+ @selectors.each { |sel| yield sel.strip, decs, @specificity }
89
268
  else
90
- @selectors.each { |sel| yield sel.strip, declarations, CssParser.calculate_specificity(sel) }
269
+ @selectors.each { |sel| yield sel.strip, decs, CssParser.calculate_specificity(sel) }
91
270
  end
92
271
  end
93
272
 
94
273
  # Iterate through declarations.
95
274
  def each_declaration # :yields: property, value, is_important
96
- decs = @declarations.sort { |a,b| a[1][:order].nil? || b[1][:order].nil? ? 0 : a[1][:order] <=> b[1][:order] }
97
- decs.each do |property, data|
98
- value = data[:value]
99
- yield property.downcase.strip, value.strip, data[:is_important]
275
+ declarations.each do |property_name, value|
276
+ yield property_name, value.value, value.important
100
277
  end
101
278
  end
102
279
 
103
280
  # Return all declarations as a string.
104
- #--
105
- # TODO: Clean-up regexp doesn't seem to work
106
- #++
107
281
  def declarations_to_s(options = {})
108
- options = {:force_important => false}.merge(options)
109
- str = ''
110
- each_declaration do |prop, val, is_important|
111
- importance = (options[:force_important] || is_important) ? ' !important' : ''
112
- str += "#{prop}: #{val}#{importance}; "
113
- end
114
- str.gsub(/^[\s^(\{)]+|[\n\r\f\t]*|[\s]+$/mx, '').strip
282
+ declarations.to_s(options)
115
283
  end
116
284
 
117
285
  # Return the CSS rule set as a string.
118
286
  def to_s
119
- decs = declarations_to_s
120
- "#{@selectors.join(',')} { #{decs} }"
287
+ "#{@selectors.join(',')} { #{declarations} }"
121
288
  end
122
289
 
123
290
  # Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
@@ -135,141 +302,131 @@ module CssParser
135
302
  #
136
303
  # See http://www.w3.org/TR/CSS21/colors.html#propdef-background
137
304
  def expand_background_shorthand! # :nodoc:
138
- return unless @declarations.has_key?('background')
305
+ return unless (declaration = declarations['background'])
139
306
 
140
- value = @declarations['background'][:value]
307
+ value = declaration.value.dup
141
308
 
142
- if value =~ CssParser::RE_INHERIT
143
- BACKGROUND_PROPERTIES.each do |prop|
144
- split_declaration('background', prop, 'inherit')
309
+ replacement =
310
+ if value.match(CssParser::RE_INHERIT)
311
+ BACKGROUND_PROPERTIES.map { |key| [key, 'inherit'] }.to_h
312
+ else
313
+ {
314
+ 'background-image' => value.slice!(CssParser::RE_IMAGE),
315
+ 'background-attachment' => value.slice!(CssParser::RE_SCROLL_FIXED),
316
+ 'background-repeat' => value.slice!(CssParser::RE_REPEAT),
317
+ 'background-color' => value.slice!(CssParser::RE_COLOUR),
318
+ 'background-size' => extract_background_size_from(value),
319
+ 'background-position' => value.slice!(CssParser::RE_BACKGROUND_POSITION)
320
+ }
145
321
  end
146
- end
147
322
 
148
- split_declaration('background', 'background-image', value.slice!(Regexp.union(CssParser::URI_RX, CssParser::RE_GRADIENT, /none/i)))
149
- split_declaration('background', 'background-attachment', value.slice!(CssParser::RE_SCROLL_FIXED))
150
- split_declaration('background', 'background-repeat', value.slice!(CssParser::RE_REPEAT))
151
- split_declaration('background', 'background-color', value.slice!(CssParser::RE_COLOUR))
152
- split_declaration('background', 'background-size', extract_background_size_from(value))
153
- split_declaration('background', 'background-position', value.slice(CssParser::RE_BACKGROUND_POSITION))
154
-
155
- @declarations.delete('background')
323
+ declarations.replace_declaration!('background', replacement, preserve_importance: true)
156
324
  end
157
325
 
158
326
  def extract_background_size_from(value)
159
327
  size = value.slice!(CssParser::RE_BACKGROUND_SIZE)
160
328
 
161
- size.sub(/^\s*\/\s*/, '') if size
329
+ size.sub(%r{^\s*/\s*}, '') if size
162
330
  end
163
331
 
164
332
  # Split shorthand border declarations (e.g. <tt>border: 1px red;</tt>)
165
333
  # Additional splitting happens in expand_dimensions_shorthand!
166
334
  def expand_border_shorthand! # :nodoc:
167
- ['border', 'border-left', 'border-right', 'border-top', 'border-bottom'].each do |k|
168
- next unless @declarations.has_key?(k)
335
+ BORDER_PROPERTIES.each do |k|
336
+ next unless (declaration = declarations[k])
169
337
 
170
- value = @declarations[k][:value]
338
+ value = declaration.value.dup
171
339
 
172
- split_declaration(k, "#{k}-width", value.slice!(CssParser::RE_BORDER_UNITS))
173
- split_declaration(k, "#{k}-color", value.slice!(CssParser::RE_COLOUR))
174
- split_declaration(k, "#{k}-style", value.slice!(CssParser::RE_BORDER_STYLE))
340
+ replacement = {
341
+ "#{k}-width" => value.slice!(CssParser::RE_BORDER_UNITS),
342
+ "#{k}-color" => value.slice!(CssParser::RE_COLOUR),
343
+ "#{k}-style" => value.slice!(CssParser::RE_BORDER_STYLE)
344
+ }
175
345
 
176
- @declarations.delete(k)
346
+ declarations.replace_declaration!(k, replacement, preserve_importance: true)
177
347
  end
178
348
  end
179
349
 
180
350
  # Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)
181
351
  # into their constituent parts. Handles margin, padding, border-color, border-style and border-width.
182
352
  def expand_dimensions_shorthand! # :nodoc:
183
- {'margin' => 'margin-%s',
184
- 'padding' => 'padding-%s',
185
- 'border-color' => 'border-%s-color',
186
- 'border-style' => 'border-%s-style',
187
- 'border-width' => 'border-%s-width'}.each do |property, expanded|
188
-
189
- next unless @declarations.has_key?(property)
353
+ DIMENSIONS.each do |property, (top, right, bottom, left)|
354
+ next unless (declaration = declarations[property])
190
355
 
191
- value = @declarations[property][:value]
356
+ value = declaration.value.dup
192
357
 
193
358
  # RGB and HSL values in borders are the only units that can have spaces (within params).
194
359
  # We cheat a bit here by stripping spaces after commas in RGB and HSL values so that we
195
360
  # can split easily on spaces.
196
361
  #
197
362
  # TODO: rgba, hsl, hsla
198
- value.gsub!(RE_COLOUR) { |c| c.gsub(/(\s*\,\s*)/, ',') }
363
+ value.gsub!(RE_COLOUR) { |c| c.gsub(/(\s*,\s*)/, ',') }
199
364
 
200
- matches = value.strip.split(/\s+/)
201
-
202
- t, r, b, l = nil
365
+ matches = split_value_preserving_function_whitespace(value)
203
366
 
204
367
  case matches.length
205
- when 1
206
- t, r, b, l = matches[0], matches[0], matches[0], matches[0]
207
- when 2
208
- t, b = matches[0], matches[0]
209
- r, l = matches[1], matches[1]
210
- when 3
211
- t = matches[0]
212
- r, l = matches[1], matches[1]
213
- b = matches[2]
214
- when 4
215
- t = matches[0]
216
- r = matches[1]
217
- b = matches[2]
218
- l = matches[3]
368
+ when 1
369
+ values = matches.to_a * 4
370
+ when 2
371
+ values = matches.to_a * 2
372
+ when 3
373
+ values = matches.to_a
374
+ values << matches[1] # left = right
375
+ when 4
376
+ values = matches.to_a
377
+ else
378
+ raise ArgumentError, "Cannot parse #{value}"
219
379
  end
220
380
 
221
- split_declaration(property, expanded % 'top', t)
222
- split_declaration(property, expanded % 'right', r)
223
- split_declaration(property, expanded % 'bottom', b)
224
- split_declaration(property, expanded % 'left', l)
381
+ replacement = [top, right, bottom, left].zip(values).to_h
225
382
 
226
- @declarations.delete(property)
383
+ declarations.replace_declaration!(property, replacement, preserve_importance: true)
227
384
  end
228
385
  end
229
386
 
230
387
  # Convert shorthand font declarations (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)
231
388
  # into their constituent parts.
232
389
  def expand_font_shorthand! # :nodoc:
233
- return unless @declarations.has_key?('font')
234
-
235
- font_props = {}
390
+ return unless (declaration = declarations['font'])
236
391
 
237
392
  # reset properties to 'normal' per http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
238
- ['font-style', 'font-variant', 'font-weight', 'font-size',
239
- 'line-height'].each do |prop|
240
- font_props[prop] = 'normal'
241
- end
393
+ font_props = {
394
+ 'font-style' => 'normal',
395
+ 'font-variant' => 'normal',
396
+ 'font-weight' => 'normal',
397
+ 'font-size' => 'normal',
398
+ 'line-height' => 'normal'
399
+ }
242
400
 
243
- value = @declarations['font'][:value]
244
- is_important = @declarations['font'][:is_important]
245
- order = @declarations['font'][:order]
401
+ value = declaration.value.dup
402
+ value.gsub!(%r{/\s+}, '/') # handle spaces between font size and height shorthand (e.g. 14px/ 16px)
246
403
 
247
404
  in_fonts = false
248
405
 
249
- matches = value.scan(/("(.*[^"])"|'(.*[^'])'|(\w[^ ,]+))/)
250
- matches.each do |match|
251
- m = match[0].to_s.strip
252
- m.gsub!(/[;]$/, '')
406
+ matches = value.scan(/"(?:.*[^"])"|'(?:.*[^'])'|(?:\w[^ ,]+)/)
407
+ matches.each do |m|
408
+ m.strip!
409
+ m.gsub!(/;$/, '')
253
410
 
254
411
  if in_fonts
255
- if font_props.has_key?('font-family')
256
- font_props['font-family'] += ', ' + m
412
+ if font_props.key?('font-family')
413
+ font_props['font-family'] += ", #{m}"
257
414
  else
258
415
  font_props['font-family'] = m
259
416
  end
260
417
  elsif m =~ /normal|inherit/i
261
418
  ['font-style', 'font-weight', 'font-variant'].each do |font_prop|
262
- font_props[font_prop] = m unless font_props.has_key?(font_prop)
419
+ font_props[font_prop] ||= m
263
420
  end
264
421
  elsif m =~ /italic|oblique/i
265
422
  font_props['font-style'] = m
266
- elsif m =~ /small\-caps/i
423
+ elsif m =~ /small-caps/i
267
424
  font_props['font-variant'] = m
268
425
  elsif m =~ /[1-9]00$|bold|bolder|lighter/i
269
426
  font_props['font-weight'] = m
270
427
  elsif m =~ CssParser::FONT_UNITS_RX
271
- if m =~ /\//
272
- font_props['font-size'], font_props['line-height'] = m.split('/')
428
+ if m.include?('/')
429
+ font_props['font-size'], font_props['line-height'] = m.split('/', 2)
273
430
  else
274
431
  font_props['font-size'] = m
275
432
  end
@@ -277,9 +434,7 @@ module CssParser
277
434
  end
278
435
  end
279
436
 
280
- font_props.each { |font_prop, font_val| @declarations[font_prop] = {:value => font_val, :is_important => is_important, :order => order} }
281
-
282
- @declarations.delete('font')
437
+ declarations.replace_declaration!('font', font_props, preserve_importance: true)
283
438
  end
284
439
 
285
440
  # Convert shorthand list-style declarations (e.g. <tt>list-style: lower-alpha outside;</tt>)
@@ -287,21 +442,22 @@ module CssParser
287
442
  #
288
443
  # See http://www.w3.org/TR/CSS21/generate.html#lists
289
444
  def expand_list_style_shorthand! # :nodoc:
290
- return unless @declarations.has_key?('list-style')
445
+ return unless (declaration = declarations['list-style'])
291
446
 
292
- value = @declarations['list-style'][:value]
447
+ value = declaration.value.dup
293
448
 
294
- if value =~ CssParser::RE_INHERIT
295
- LIST_STYLE_PROPERTIES.each do |prop|
296
- split_declaration('list-style', prop, 'inherit')
449
+ replacement =
450
+ if value =~ CssParser::RE_INHERIT
451
+ LIST_STYLE_PROPERTIES.map { |key| [key, 'inherit'] }.to_h
452
+ else
453
+ {
454
+ 'list-style-type' => value.slice!(CssParser::RE_LIST_STYLE_TYPE),
455
+ 'list-style-position' => value.slice!(CssParser::RE_INSIDE_OUTSIDE),
456
+ 'list-style-image' => value.slice!(CssParser::URI_RX_OR_NONE)
457
+ }
297
458
  end
298
- end
299
459
 
300
- split_declaration('list-style', 'list-style-type', value.slice!(CssParser::RE_LIST_STYLE_TYPE))
301
- split_declaration('list-style', 'list-style-position', value.slice!(CssParser::RE_INSIDE_OUTSIDE))
302
- split_declaration('list-style', 'list-style-image', value.slice!(Regexp.union(CssParser::URI_RX, /none/i)))
303
-
304
- @declarations.delete('list-style')
460
+ declarations.replace_declaration!('list-style', replacement, preserve_importance: true)
305
461
  end
306
462
 
307
463
  # Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
@@ -315,23 +471,24 @@ module CssParser
315
471
  end
316
472
 
317
473
  # Combine several properties into a shorthand one
318
- def create_shorthand_properties! properties, shorthand_property # :nodoc:
474
+ def create_shorthand_properties!(properties, shorthand_property) # :nodoc:
319
475
  values = []
320
476
  properties_to_delete = []
321
477
  properties.each do |property|
322
- if @declarations.has_key?(property) and not @declarations[property][:is_important]
323
- values << @declarations[property][:value]
324
- properties_to_delete << property
325
- end
478
+ next unless (declaration = declarations[property])
479
+ next if declaration.important
480
+
481
+ values << declaration.value
482
+ properties_to_delete << property
326
483
  end
327
484
 
328
- if values.length > 1
329
- properties_to_delete.each do |property|
330
- @declarations.delete(property)
331
- end
485
+ return if values.length <= 1
332
486
 
333
- @declarations[shorthand_property] = {:value => values.join(' ')}
487
+ properties_to_delete.each do |property|
488
+ declarations.delete(property)
334
489
  end
490
+
491
+ declarations[shorthand_property] = values.join(' ')
335
492
  end
336
493
 
337
494
  # Looks for long format CSS background properties (e.g. <tt>background-color</tt>) and
@@ -340,15 +497,12 @@ module CssParser
340
497
  # Leaves properties declared !important alone.
341
498
  def create_background_shorthand! # :nodoc:
342
499
  # When we have a background-size property we must separate it and distinguish it from
343
- # background-position by preceeding it with a backslash. In this case we also need to
500
+ # background-position by preceding it with a backslash. In this case we also need to
344
501
  # have a background-position property, so we set it if it's missing.
345
502
  # http://www.w3schools.com/cssref/css3_pr_background.asp
346
- if @declarations.has_key?('background-size') and not @declarations['background-size'][:is_important]
347
- unless @declarations.has_key?('background-position')
348
- @declarations['background-position'] = {:value => '0% 0%'}
349
- end
350
-
351
- @declarations['background-size'][:value] = "/ #{@declarations['background-size'][:value]}"
503
+ if (declaration = declarations['background-size']) && !declaration.important
504
+ declarations['background-position'] ||= '0% 0%'
505
+ declaration.value = "/ #{declaration.value}"
352
506
  end
353
507
 
354
508
  create_shorthand_properties! BACKGROUND_PROPERTIES, 'background'
@@ -359,102 +513,72 @@ module CssParser
359
513
  #
360
514
  # TODO: this is extremely similar to create_background_shorthand! and should be combined
361
515
  def create_border_shorthand! # :nodoc:
362
- values = []
516
+ values = BORDER_STYLE_PROPERTIES.map do |property|
517
+ next unless (declaration = declarations[property])
518
+ next if declaration.important
519
+ # can't merge if any value contains a space (i.e. has multiple values)
520
+ # we temporarily remove any spaces after commas for the check (inside rgba, etc...)
521
+ next if declaration.value.gsub(/,\s/, ',').strip =~ /\s/
363
522
 
364
- ['border-width', 'border-style', 'border-color'].each do |property|
365
- if @declarations.has_key?(property) and not @declarations[property][:is_important]
366
- # can't merge if any value contains a space (i.e. has multiple values)
367
- # we temporarily remove any spaces after commas for the check (inside rgba, etc...)
368
- return if @declarations[property][:value].gsub(/\,[\s]/, ',').strip =~ /[\s]/
369
- values << @declarations[property][:value]
370
- end
371
- end
523
+ declaration.value
524
+ end.compact
372
525
 
373
- @declarations.delete('border-width')
374
- @declarations.delete('border-style')
375
- @declarations.delete('border-color')
526
+ return if values.size != BORDER_STYLE_PROPERTIES.size
376
527
 
377
- unless values.empty?
378
- @declarations['border'] = {:value => values.join(' ')}
528
+ BORDER_STYLE_PROPERTIES.each do |property|
529
+ declarations.delete(property)
379
530
  end
531
+
532
+ declarations['border'] = values.join(' ')
380
533
  end
381
534
 
382
535
  # Looks for long format CSS dimensional properties (margin, padding, border-color, border-style and border-width)
383
536
  # and converts them into shorthand CSS properties.
384
537
  def create_dimensions_shorthand! # :nodoc:
385
- directions = ['top', 'right', 'bottom', 'left']
538
+ return if declarations.size < NUMBER_OF_DIMENSIONS
386
539
 
387
- {'margin' => 'margin-%s',
388
- 'padding' => 'padding-%s',
389
- 'border-color' => 'border-%s-color',
390
- 'border-style' => 'border-%s-style',
391
- 'border-width' => 'border-%s-width'}.each do |property, expanded|
540
+ DIMENSIONS.each do |property, dimensions|
541
+ values = [:top, :right, :bottom, :left].each_with_index.with_object({}) do |(side, index), result|
542
+ next unless (declaration = declarations[dimensions[index]])
392
543
 
393
- top, right, bottom, left = ['top', 'right', 'bottom', 'left'].map { |side| expanded % side }
394
- foldable = @declarations.select do |dim, val|
395
- dim == top or dim == right or dim == bottom or dim == left
544
+ result[side] = declaration.value
396
545
  end
546
+
397
547
  # All four dimensions must be present
398
- if foldable.length == 4
399
- values = {}
400
-
401
- directions.each { |d| values[d.to_sym] = @declarations[expanded % d][:value].downcase.strip }
402
-
403
- if values[:left] == values[:right]
404
- if values[:top] == values[:bottom]
405
- if values[:top] == values[:left] # All four sides are equal
406
- new_value = values[:top]
407
- else # Top and bottom are equal, left and right are equal
408
- new_value = values[:top] + ' ' + values[:left]
409
- end
410
- else # Only left and right are equal
411
- new_value = values[:top] + ' ' + values[:left] + ' ' + values[:bottom]
412
- end
413
- else # No sides are equal
414
- new_value = values[:top] + ' ' + values[:right] + ' ' + values[:bottom] + ' ' + values[:left]
415
- end
548
+ next if values.size != dimensions.size
416
549
 
417
- new_value.strip!
418
- @declarations[property] = {:value => new_value.strip} unless new_value.empty?
550
+ new_value = values.values_at(*compute_dimensions_shorthand(values)).join(' ').strip
551
+ declarations[property] = new_value unless new_value.empty?
419
552
 
420
- # Delete the longhand values
421
- directions.each { |d| @declarations.delete(expanded % d) }
422
- end
553
+ # Delete the longhand values
554
+ dimensions.each { |d| declarations.delete(d) }
423
555
  end
424
556
  end
425
557
 
426
-
427
558
  # Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and
428
559
  # tries to convert them into a shorthand CSS <tt>font</tt> property. All
429
560
  # font properties must be present in order to create a shorthand declaration.
430
561
  def create_font_shorthand! # :nodoc:
431
- ['font-style', 'font-variant', 'font-weight', 'font-size',
432
- 'line-height', 'font-family'].each do |prop|
433
- return unless @declarations.has_key?(prop)
434
- end
562
+ return unless FONT_STYLE_PROPERTIES.all? { |prop| declarations.key?(prop) }
435
563
 
436
- new_value = ''
564
+ new_value = String.new
437
565
  ['font-style', 'font-variant', 'font-weight'].each do |property|
438
- unless @declarations[property][:value] == 'normal'
439
- new_value += @declarations[property][:value] + ' '
566
+ unless declarations[property].value == 'normal'
567
+ new_value << declarations[property].value << ' '
440
568
  end
441
569
  end
442
570
 
443
- new_value += @declarations['font-size'][:value]
571
+ new_value << declarations['font-size'].value
444
572
 
445
- unless @declarations['line-height'][:value] == 'normal'
446
- new_value += '/' + @declarations['line-height'][:value]
573
+ unless declarations['line-height'].value == 'normal'
574
+ new_value << '/' << declarations['line-height'].value
447
575
  end
448
576
 
449
- new_value += ' ' + @declarations['font-family'][:value]
577
+ new_value << ' ' << declarations['font-family'].value
450
578
 
451
- @declarations['font'] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
452
-
453
- ['font-style', 'font-variant', 'font-weight', 'font-size',
454
- 'line-height', 'font-family'].each do |prop|
455
- @declarations.delete(prop)
456
- end
579
+ declarations['font'] = new_value.gsub(/\s+/, ' ')
457
580
 
581
+ FONT_STYLE_PROPERTIES.each { |prop| declarations.delete(prop) }
458
582
  end
459
583
 
460
584
  # Looks for long format CSS list-style properties (e.g. <tt>list-style-type</tt>) and
@@ -467,41 +591,37 @@ module CssParser
467
591
 
468
592
  private
469
593
 
470
- # utility method for re-assign shorthand elements to longhand versions
471
- def split_declaration(src, dest, v) # :nodoc:
472
- return unless v and not v.empty?
594
+ attr_accessor :declarations
473
595
 
474
- if @declarations.has_key?(dest)
475
- #puts "dest #{dest} already exists"
596
+ def compute_dimensions_shorthand(values)
597
+ # All four sides are equal, returning single value
598
+ return [:top] if values.values.uniq.count == 1
476
599
 
477
- if @declarations[src][:order].nil? || (!@declarations[dest][:order].nil? && @declarations[dest][:order] > @declarations[src][:order])
478
- #puts "skipping #{dest}:#{v} due to order "
479
- return
480
- else
481
- @declarations[dest] = {}
482
- end
483
- end
484
- @declarations[dest] = @declarations[src].merge({:value => v.to_s.strip})
600
+ # `/* top | right | bottom | left */`
601
+ return [:top, :right, :bottom, :left] if values[:left] != values[:right]
602
+
603
+ # Vertical are the same & horizontal are the same, `/* vertical | horizontal */`
604
+ return [:top, :left] if values[:top] == values[:bottom]
605
+
606
+ [:top, :left, :bottom]
485
607
  end
486
608
 
487
609
  def parse_declarations!(block) # :nodoc:
488
- @declarations = {}
610
+ self.declarations = Declarations.new
489
611
 
490
612
  return unless block
491
613
 
492
- block.gsub!(/(^[\s]*)|([\s]*$)/, '')
493
-
494
- continuation = ''
495
- block.split(/[\;$]+/m).each do |decs|
496
- decs = continuation + decs
614
+ continuation = nil
615
+ block.split(/[;$]+/m).each do |decs|
616
+ decs = (continuation ? continuation + decs : decs)
497
617
  if decs =~ /\([^)]*\Z/ # if it has an unmatched parenthesis
498
- continuation = decs + ';'
499
-
500
- elsif matches = decs.match(/(.[^:]*)\s*:\s*(.+)(;?\s*\Z)/i)
501
- property, value, = matches.captures # skip end_of_declaration
502
-
618
+ continuation = "#{decs};"
619
+ elsif (matches = decs.match(/\s*(.[^:]*)\s*:\s*(.+?)(?:;?\s*\Z)/i))
620
+ # skip end_of_declaration
621
+ property = matches[1]
622
+ value = matches[2]
503
623
  add_declaration!(property, value)
504
- continuation = ''
624
+ continuation = nil
505
625
  end
506
626
  end
507
627
  end
@@ -510,12 +630,28 @@ module CssParser
510
630
  # TODO: way too simplistic
511
631
  #++
512
632
  def parse_selectors!(selectors) # :nodoc:
513
- @selectors = selectors.split(',').map { |s| s.gsub(/\s+/, ' ').strip }
633
+ @selectors = selectors.split(',').map do |s|
634
+ s.gsub!(/\s+/, ' ')
635
+ s.strip!
636
+ s
637
+ end
638
+ end
639
+
640
+ def split_value_preserving_function_whitespace(value)
641
+ split_value = value.gsub(RE_FUNCTIONS) do |c|
642
+ c.gsub!(/\s+/, WHITESPACE_REPLACEMENT)
643
+ c
644
+ end
645
+
646
+ matches = split_value.strip.split(/\s+/)
647
+
648
+ matches.each do |c|
649
+ c.gsub!(WHITESPACE_REPLACEMENT, ' ')
650
+ end
514
651
  end
515
652
  end
516
653
 
517
654
  class OffsetAwareRuleSet < RuleSet
518
-
519
655
  # File offset range
520
656
  attr_reader :offset
521
657