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