css_parser 1.7.0 → 1.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,92 +1,302 @@
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
+ 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
13
18
 
14
19
  NUMBER_OF_DIMENSIONS = 4
15
20
 
16
21
  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
- ]
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
173
+
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
23
242
 
24
243
  # Array of selector strings.
25
- attr_reader :selectors
244
+ attr_reader :selectors
26
245
 
27
246
  # Integer with the specificity to use for this RuleSet.
28
- 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
29
278
 
30
- def initialize(selectors, block, specificity = nil)
31
279
  @selectors = []
32
280
  @specificity = specificity
33
- @declarations = {}
34
- @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
+
35
289
  parse_selectors!(selectors) if selectors
36
290
  parse_declarations!(block)
37
291
  end
38
292
 
39
293
  # Get the value of a property
40
294
  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
295
+ return '' unless (value = declarations[property])
72
296
 
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)
297
+ "#{value};"
89
298
  end
299
+ alias [] get_value
90
300
 
91
301
  # Iterate through selectors.
92
302
  #
@@ -98,42 +308,29 @@ module CssParser
98
308
  # ...
99
309
  # end
100
310
  def each_selector(options = {}) # :yields: selector, declarations, specificity
101
- declarations = declarations_to_s(options)
311
+ decs = declarations.to_s(options)
102
312
  if @specificity
103
- @selectors.each { |sel| yield sel.strip, declarations, @specificity }
313
+ @selectors.each { |sel| yield sel.strip, decs, @specificity }
104
314
  else
105
- @selectors.each { |sel| yield sel.strip, declarations, CssParser.calculate_specificity(sel) }
315
+ @selectors.each { |sel| yield sel.strip, decs, CssParser.calculate_specificity(sel) }
106
316
  end
107
317
  end
108
318
 
109
319
  # Iterate through declarations.
110
320
  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]
321
+ declarations.each do |property_name, value|
322
+ yield property_name, value.value, value.important
115
323
  end
116
324
  end
117
325
 
118
326
  # Return all declarations as a string.
119
- #--
120
- # TODO: Clean-up regexp doesn't seem to work
121
- #++
122
327
  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
328
+ declarations.to_s(options)
131
329
  end
132
330
 
133
331
  # Return the CSS rule set as a string.
134
332
  def to_s
135
- decs = declarations_to_s
136
- "#{@selectors.join(',')} { #{decs} }"
333
+ "#{@selectors.join(',')} { #{declarations} }"
137
334
  end
138
335
 
139
336
  # Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
@@ -151,45 +348,48 @@ module CssParser
151
348
  #
152
349
  # See http://www.w3.org/TR/CSS21/colors.html#propdef-background
153
350
  def expand_background_shorthand! # :nodoc:
154
- return unless @declarations.has_key?('background')
351
+ return unless (declaration = declarations['background'])
155
352
 
156
- value = @declarations['background'][:value]
353
+ value = declaration.value.dup
157
354
 
158
- if value =~ CssParser::RE_INHERIT
159
- BACKGROUND_PROPERTIES.each do |prop|
160
- 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
+ }
161
367
  end
162
- end
163
368
 
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')
369
+ declarations.replace_declaration!('background', replacement, preserve_importance: true)
172
370
  end
173
371
 
174
372
  def extract_background_size_from(value)
175
373
  size = value.slice!(CssParser::RE_BACKGROUND_SIZE)
176
374
 
177
- size.sub(/^\s*\/\s*/, '') if size
375
+ size.sub(%r{^\s*/\s*}, '') if size
178
376
  end
179
377
 
180
378
  # Split shorthand border declarations (e.g. <tt>border: 1px red;</tt>)
181
379
  # Additional splitting happens in expand_dimensions_shorthand!
182
380
  def expand_border_shorthand! # :nodoc:
183
381
  BORDER_PROPERTIES.each do |k|
184
- next unless @declarations.has_key?(k)
382
+ next unless (declaration = declarations[k])
185
383
 
186
- value = @declarations[k][:value]
384
+ value = declaration.value.dup
187
385
 
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))
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
+ }
191
391
 
192
- @declarations.delete(k)
392
+ declarations.replace_declaration!(k, replacement, preserve_importance: true)
193
393
  end
194
394
  end
195
395
 
@@ -197,17 +397,18 @@ module CssParser
197
397
  # into their constituent parts. Handles margin, padding, border-color, border-style and border-width.
198
398
  def expand_dimensions_shorthand! # :nodoc:
199
399
  DIMENSIONS.each do |property, (top, right, bottom, left)|
200
- next unless @declarations.has_key?(property)
201
- value = @declarations[property][:value]
400
+ next unless (declaration = declarations[property])
401
+
402
+ value = declaration.value.dup
202
403
 
203
404
  # RGB and HSL values in borders are the only units that can have spaces (within params).
204
405
  # We cheat a bit here by stripping spaces after commas in RGB and HSL values so that we
205
406
  # can split easily on spaces.
206
407
  #
207
408
  # TODO: rgba, hsl, hsla
208
- value.gsub!(RE_COLOUR) { |c| c.gsub(/(\s*\,\s*)/, ',') }
409
+ value.gsub!(RE_COLOUR) { |c| c.gsub(/(\s*,\s*)/, ',') }
209
410
 
210
- matches = value.strip.split(/\s+/)
411
+ matches = split_value_preserving_function_whitespace(value)
211
412
 
212
413
  case matches.length
213
414
  when 1
@@ -219,63 +420,59 @@ module CssParser
219
420
  values << matches[1] # left = right
220
421
  when 4
221
422
  values = matches.to_a
423
+ else
424
+ raise ArgumentError, "Cannot parse #{value}"
222
425
  end
223
426
 
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)
427
+ replacement = [top, right, bottom, left].zip(values).to_h
230
428
 
231
- @declarations.delete(property)
429
+ declarations.replace_declaration!(property, replacement, preserve_importance: true)
232
430
  end
233
431
  end
234
432
 
235
433
  # Convert shorthand font declarations (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)
236
434
  # into their constituent parts.
237
435
  def expand_font_shorthand! # :nodoc:
238
- return unless @declarations.has_key?('font')
239
-
240
- font_props = {}
436
+ return unless (declaration = declarations['font'])
241
437
 
242
438
  # 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
439
+ font_props = {
440
+ 'font-style' => 'normal',
441
+ 'font-variant' => 'normal',
442
+ 'font-weight' => 'normal',
443
+ 'font-size' => 'normal',
444
+ 'line-height' => 'normal'
445
+ }
247
446
 
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]
447
+ value = declaration.value.dup
448
+ value.gsub!(%r{/\s+}, '/') # handle spaces between font size and height shorthand (e.g. 14px/ 16px)
252
449
 
253
450
  in_fonts = false
254
451
 
255
- matches = value.scan(/("(.*[^"])"|'(.*[^'])'|(\w[^ ,]+))/)
256
- matches.each do |match|
257
- m = match[0].to_s.strip
258
- m.gsub!(/[;]$/, '')
452
+ matches = value.scan(/"(?:.*[^"])"|'(?:.*[^'])'|(?:\w[^ ,]+)/)
453
+ matches.each do |m|
454
+ m.strip!
455
+ m.gsub!(/;$/, '')
259
456
 
260
457
  if in_fonts
261
- if font_props.has_key?('font-family')
262
- font_props['font-family'] += ', ' + m
458
+ if font_props.key?('font-family')
459
+ font_props['font-family'] += ", #{m}"
263
460
  else
264
461
  font_props['font-family'] = m
265
462
  end
266
- elsif m =~ /normal|inherit/i
267
- ['font-style', 'font-weight', 'font-variant'].each do |font_prop|
268
- 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
269
466
  end
270
- elsif m =~ /italic|oblique/i
467
+ elsif /italic|oblique/i.match?(m)
271
468
  font_props['font-style'] = m
272
- elsif m =~ /small\-caps/i
469
+ elsif /small-caps/i.match?(m)
273
470
  font_props['font-variant'] = m
274
- elsif m =~ /[1-9]00$|bold|bolder|lighter/i
471
+ elsif /[1-9]00$|bold|bolder|lighter/i.match?(m)
275
472
  font_props['font-weight'] = m
276
- elsif m =~ CssParser::FONT_UNITS_RX
277
- if m =~ /\//
278
- 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)
279
476
  else
280
477
  font_props['font-size'] = m
281
478
  end
@@ -283,9 +480,7 @@ module CssParser
283
480
  end
284
481
  end
285
482
 
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')
483
+ declarations.replace_declaration!('font', font_props, preserve_importance: true)
289
484
  end
290
485
 
291
486
  # Convert shorthand list-style declarations (e.g. <tt>list-style: lower-alpha outside;</tt>)
@@ -293,21 +488,22 @@ module CssParser
293
488
  #
294
489
  # See http://www.w3.org/TR/CSS21/generate.html#lists
295
490
  def expand_list_style_shorthand! # :nodoc:
296
- return unless @declarations.has_key?('list-style')
491
+ return unless (declaration = declarations['list-style'])
297
492
 
298
- value = @declarations['list-style'][:value]
493
+ value = declaration.value.dup
299
494
 
300
- if value =~ CssParser::RE_INHERIT
301
- LIST_STYLE_PROPERTIES.each do |prop|
302
- 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
+ }
303
504
  end
304
- end
305
505
 
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')
506
+ declarations.replace_declaration!('list-style', replacement, preserve_importance: true)
311
507
  end
312
508
 
313
509
  # Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
@@ -321,23 +517,24 @@ module CssParser
321
517
  end
322
518
 
323
519
  # Combine several properties into a shorthand one
324
- def create_shorthand_properties! properties, shorthand_property # :nodoc:
520
+ def create_shorthand_properties!(properties, shorthand_property) # :nodoc:
325
521
  values = []
326
522
  properties_to_delete = []
327
523
  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
524
+ next unless (declaration = declarations[property])
525
+ next if declaration.important
526
+
527
+ values << declaration.value
528
+ properties_to_delete << property
332
529
  end
333
530
 
334
- if values.length > 1
335
- properties_to_delete.each do |property|
336
- @declarations.delete(property)
337
- end
531
+ return if values.length <= 1
338
532
 
339
- @declarations[shorthand_property] = {:value => values.join(' ')}
533
+ properties_to_delete.each do |property|
534
+ declarations.delete(property)
340
535
  end
536
+
537
+ declarations[shorthand_property] = values.join(' ')
341
538
  end
342
539
 
343
540
  # Looks for long format CSS background properties (e.g. <tt>background-color</tt>) and
@@ -349,12 +546,9 @@ module CssParser
349
546
  # background-position by preceding it with a backslash. In this case we also need to
350
547
  # have a background-position property, so we set it if it's missing.
351
548
  # 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]}"
549
+ if (declaration = declarations['background-size']) && !declaration.important
550
+ declarations['background-position'] ||= '0% 0%'
551
+ declaration.value = "/ #{declaration.value}"
358
552
  end
359
553
 
360
554
  create_shorthand_properties! BACKGROUND_PROPERTIES, 'background'
@@ -365,92 +559,72 @@ module CssParser
365
559
  #
366
560
  # TODO: this is extremely similar to create_background_shorthand! and should be combined
367
561
  def create_border_shorthand! # :nodoc:
368
- values = []
369
-
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
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
377
570
  end
378
571
 
379
- BORDER_STYLE_PROPERTIES.each { |prop| @declarations.delete(prop)}
572
+ return if values.size != BORDER_STYLE_PROPERTIES.size
380
573
 
381
- unless values.empty?
382
- @declarations['border'] = {:value => values.join(' ')}
574
+ BORDER_STYLE_PROPERTIES.each do |property|
575
+ declarations.delete(property)
383
576
  end
577
+
578
+ declarations['border'] = values.join(' ')
384
579
  end
385
580
 
386
581
  # Looks for long format CSS dimensional properties (margin, padding, border-color, border-style and border-width)
387
582
  # and converts them into shorthand CSS properties.
388
583
  def create_dimensions_shorthand! # :nodoc:
389
- return if @declarations.size < NUMBER_OF_DIMENSIONS
584
+ return if declarations.size < NUMBER_OF_DIMENSIONS
390
585
 
391
586
  DIMENSIONS.each do |property, dimensions|
392
- (top, right, bottom, left) = dimensions
587
+ values = DIMENSION_DIRECTIONS.each_with_index.with_object({}) do |(side, index), result|
588
+ next unless (declaration = declarations[dimensions[index]])
589
+
590
+ result[side] = declaration.value
591
+ end
592
+
393
593
  # 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
594
+ next if values.size != dimensions.size
417
595
 
418
- new_value.strip!
419
- @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?
420
598
 
421
- # Delete the longhand values
422
- [top, right, bottom, left].each { |d| @declarations.delete(d) }
423
- end
599
+ # Delete the longhand values
600
+ dimensions.each { |d| declarations.delete(d) }
424
601
  end
425
602
  end
426
603
 
427
-
428
604
  # Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and
429
605
  # tries to convert them into a shorthand CSS <tt>font</tt> property. All
430
606
  # font properties must be present in order to create a shorthand declaration.
431
607
  def create_font_shorthand! # :nodoc:
432
- FONT_STYLE_PROPERTIES.each do |prop|
433
- return unless @declarations.has_key?(prop)
434
- end
608
+ return unless FONT_STYLE_PROPERTIES.all? { |prop| declarations.key?(prop) }
435
609
 
436
- new_value = String.new
610
+ new_value = +''
437
611
  ['font-style', 'font-variant', 'font-weight'].each do |property|
438
- unless @declarations[property][:value] == 'normal'
439
- new_value << @declarations[property][:value] << ' '
612
+ unless declarations[property].value == 'normal'
613
+ new_value << declarations[property].value << ' '
440
614
  end
441
615
  end
442
616
 
443
- new_value << @declarations['font-size'][:value]
617
+ new_value << declarations['font-size'].value
444
618
 
445
- unless @declarations['line-height'][:value] == 'normal'
446
- new_value << '/' << @declarations['line-height'][:value]
619
+ unless declarations['line-height'].value == 'normal'
620
+ new_value << '/' << declarations['line-height'].value
447
621
  end
448
622
 
449
- new_value << ' ' << @declarations['font-family'][:value]
623
+ new_value << ' ' << declarations['font-family'].value
450
624
 
451
- @declarations['font'] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
625
+ declarations['font'] = new_value.gsub(/\s+/, ' ')
452
626
 
453
- FONT_STYLE_PROPERTIES.each { |prop| @declarations.delete(prop) }
627
+ FONT_STYLE_PROPERTIES.each { |prop| declarations.delete(prop) }
454
628
  end
455
629
 
456
630
  # Looks for long format CSS list-style properties (e.g. <tt>list-style-type</tt>) and
@@ -463,46 +637,53 @@ module CssParser
463
637
 
464
638
  private
465
639
 
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?
640
+ attr_accessor :declarations
469
641
 
470
- if @declarations.has_key?(dest)
471
- #puts "dest #{dest} already exists"
642
+ def compute_dimensions_shorthand(values)
643
+ # All four sides are equal, returning single value
644
+ return [:top] if values.values.uniq.count == 1
472
645
 
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
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]
480
651
 
481
- @declarations[dest] = @declarations[src].dup
482
- @declarations[dest][:value] = v.to_s.strip
652
+ [:top, :left, :bottom]
483
653
  end
484
654
 
485
655
  def parse_declarations!(block) # :nodoc:
486
- @declarations = {}
656
+ self.declarations = Declarations.new
487
657
 
488
658
  return unless block
489
659
 
490
660
  continuation = nil
491
- block.split(/[\;$]+/m).each do |decs|
492
- decs = continuation ? continuation + decs : decs
493
- if decs =~ /\([^)]*\Z/ # if it has an unmatched parenthesis
494
- continuation = decs + ';'
495
-
496
- elsif matches = decs.match(/\s*(.[^:]*)\s*:\s*(.+?)(;?\s*\Z)/i)
497
- # skip end_of_declaration
498
- property = matches[1]
499
- value = matches[2]
500
- add_declaration!(property, value)
501
- 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
502
668
  end
669
+
670
+ next unless (colon = decs.index(COLON))
671
+
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
503
680
  end
504
681
  end
505
682
 
683
+ def unmatched_open_parenthesis?(declarations)
684
+ (lparen_index = declarations.index(LPAREN)) && !declarations.index(RPAREN, lparen_index)
685
+ end
686
+
506
687
  #--
507
688
  # TODO: way too simplistic
508
689
  #++
@@ -513,20 +694,18 @@ module CssParser
513
694
  s
514
695
  end
515
696
  end
516
- end
517
-
518
- class OffsetAwareRuleSet < RuleSet
519
697
 
520
- # File offset range
521
- attr_reader :offset
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
522
703
 
523
- # the local or remote location
524
- attr_accessor :filename
704
+ matches = split_value.strip.split(/\s+/)
525
705
 
526
- def initialize(filename, offset, selectors, block, specificity = nil)
527
- super(selectors, block, specificity)
528
- @offset = offset
529
- @filename = filename
706
+ matches.each do |c|
707
+ c.gsub!(WHITESPACE_REPLACEMENT, ' ')
708
+ end
530
709
  end
531
710
  end
532
711
  end