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