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