css_parser 3.3.0 → 3.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1b890ee0d537d338ae0a218c68424271242859ba814a52c27fcd05ffefe1acd
4
- data.tar.gz: 147e41dae4da802d96124fc07b488b122967a44c0ff79bfe623291c1795e0d9b
3
+ metadata.gz: 3ff7ead55436d394e644feb148e017f631ea4ede0783746107a72cc790a70ee5
4
+ data.tar.gz: 8bdd909c65c7f7fe4139d164898e6cd7906b1b649b6a68429dee620cc5798cd0
5
5
  SHA512:
6
- metadata.gz: 76728b4b816f4b620f28198b6fd5f92f005733bec3afba0df5707d8daf99ab650fd0a0143a6222fc53c5453c32c3a13a76eb4877f3c06f5dc903a559a18d200f
7
- data.tar.gz: a5b5a901312932dd9d3c50fa9a6cd4ea549180f57a7e4d85d700d5c3ec58f458927be66c2eb7169165333e153d58061dbdfa98c75c0e6ffb6a81181bc78fdd6f
6
+ metadata.gz: ecf5d9f9ef55e3f69269c511b51f5363bdc6e317fa1446ae04be7850256c30039dd9719c74705df29dbb9011847d3cbe3602bbd11f8b273cd7b6c92b9e2268cb
7
+ data.tar.gz: 7ddbd0f9bab2c8327f081a48cce1e7fb20cc133eb5af8530eb1dcc6ef1acc2836f9a2274e75e74dec6b9866461754ceecc759efbbd7aec7dbe6b06814bd2cbc9
@@ -35,6 +35,9 @@ module CssParser
35
35
  #++
36
36
  IMPORTANT_IN_PROPERTY_RX = /\s*!important\b\s*/i.freeze
37
37
 
38
+ # What sanitize_media_query would change: blank, padded, or not single-space separated
39
+ UNSANITIZED_MEDIA_QUERY_RX = /\A\z|\A[\s\0]|[\s\0]\z|\s\s|[^\S ]/.freeze
40
+
38
41
  RE_INSIDE_OUTSIDE = regex_possible_values 'inside', 'outside'
39
42
  RE_SCROLL_FIXED = regex_possible_values 'scroll', 'fixed'
40
43
  RE_REPEAT = regex_possible_values 'repeat(\-x|\-y)*|no\-repeat'
@@ -18,6 +18,11 @@ module CssParser
18
18
 
19
19
  NUMBER_OF_DIMENSIONS = 4
20
20
 
21
+ NORMALIZED_PROPERTY_RX = /\A[-a-z0-9_]+\z/.freeze
22
+ BLANK_VALUE_RX = /\A[\s\0]*\z/.freeze
23
+ SURROUNDING_WHITESPACE_RX = /\A[\s\0]|[\s\0]\z/.freeze
24
+ TRAILING_SEMICOLON_RX = /\s*;\s*\Z/.freeze
25
+
21
26
  DIMENSIONS = [
22
27
  ['margin', %w[margin-top margin-right margin-bottom margin-left]],
23
28
  ['padding', %w[padding-top padding-right padding-bottom padding-left]],
@@ -35,6 +40,8 @@ module CssParser
35
40
  RPAREN = ')'.freeze
36
41
  IMPORTANT = '!important'.freeze
37
42
  class Declarations
43
+ EMPTY_DECLARATIONS = {}.freeze
44
+
38
45
  class Value
39
46
  attr_reader :value
40
47
  attr_accessor :important
@@ -45,12 +52,17 @@ module CssParser
45
52
  end
46
53
 
47
54
  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!
55
+ # sub and strip always return a copy, and the caller's string must not be mutated:
56
+ # only copy when there is something to remove, so a clean value allocates nothing
57
+ value = value.to_s
58
+ value = value.sub(TRAILING_SEMICOLON_RX, '') if value.match?(TRAILING_SEMICOLON_RX)
59
+ self.important = value.match?(CssParser::IMPORTANT_IN_PROPERTY_RX)
60
+ value = value.sub(CssParser::IMPORTANT_IN_PROPERTY_RX, '') if important
61
+ value = value.strip if value.match?(SURROUNDING_WHITESPACE_RX)
51
62
  raise ArgumentError, 'value is empty' if value.empty?
52
63
 
53
- @value = value.freeze
64
+ # value may still be the caller's string: -value freezes a copy instead of it
65
+ @value = -value
54
66
  end
55
67
 
56
68
  def to_s
@@ -68,7 +80,7 @@ module CssParser
68
80
 
69
81
  def_delegators :declarations, :each, :each_value
70
82
 
71
- def initialize(declarations = {})
83
+ def initialize(declarations = EMPTY_DECLARATIONS)
72
84
  self.declarations = {}
73
85
  declarations.each { |property, value| add_declaration!(property, value) }
74
86
  end
@@ -98,7 +110,7 @@ module CssParser
98
110
 
99
111
  if value.is_a?(Value) && (!currently_important || value.important)
100
112
  declarations[property] = value
101
- elsif value.to_s.strip.empty?
113
+ elsif value.to_s.match?(BLANK_VALUE_RX)
102
114
  delete property
103
115
  else
104
116
  value = Value.new(value)
@@ -162,18 +174,19 @@ module CssParser
162
174
 
163
175
  # We should preserve subsequent declarations of the same properties
164
176
  # and prior important ones if replacement one is not important
165
- replacements = replacement_declarations.each.with_object({}) do |(key, replacement), result|
177
+ replacements = {}
178
+ replacement_declarations.declarations.each do |key, replacement|
166
179
  existing = declarations[key]
167
180
 
168
181
  # No existing -> set
169
182
  unless existing
170
- result[key] = replacement
183
+ replacements[key] = replacement
171
184
  next
172
185
  end
173
186
 
174
187
  # Replacement more important than existing -> replace
175
188
  if replacement.important && !existing.important
176
- result[key] = replacement
189
+ replacements[key] = replacement
177
190
  replaced_index = replacement_keys.index(key)
178
191
  replacement_keys.delete_at(replaced_index)
179
192
  replacement_values.delete_at(replaced_index)
@@ -186,7 +199,7 @@ module CssParser
186
199
 
187
200
  # Existing and replacement importance are the same,
188
201
  # value which is declared later wins
189
- result[key] = replacement if property_index > replacement_keys.index(key)
202
+ replacements[key] = replacement if property_index > replacement_keys.index(key)
190
203
  end
191
204
 
192
205
  return if replacements.empty?
@@ -197,16 +210,17 @@ module CssParser
197
210
  replacement_values.delete_at(property_index)
198
211
  replacement_values.insert(property_index, *replacements.values)
199
212
 
200
- self.declarations = replacement_keys.zip(replacement_values).to_h
213
+ self.declarations = {}
214
+ replacement_keys.each_with_index { |key, index| declarations[key] = replacement_values[index] }
201
215
  end
202
216
 
203
217
  def to_s(options = {})
204
- str = declarations.reduce(+'') do |memo, (prop, value)|
218
+ str = +''
219
+ declarations.each do |prop, value|
205
220
  importance = options[:force_important] || value.important ? ' !important' : ''
206
- memo << "#{prop}: #{value.value}#{importance}; "
221
+ str << prop << ': ' << value.value << importance << '; '
207
222
  end
208
- # TODO: Clean-up regexp doesn't seem to work
209
- str.gsub!(/^[\s^({)]+|[\n\r\f\t]*|\s+$/mx, '')
223
+ str.gsub!(/^[\s^({)]+|[\n\r\f\t]+/, '')
210
224
  str.strip!
211
225
  str
212
226
  end
@@ -226,7 +240,10 @@ module CssParser
226
240
  attr_writer :declarations
227
241
 
228
242
  def normalize_property(property)
229
- property = property.to_s.downcase
243
+ property = property.to_s
244
+ return property if property.match?(NORMALIZED_PROPERTY_RX)
245
+
246
+ property = property.downcase
230
247
  property.strip!
231
248
  property
232
249
  end
@@ -399,14 +416,14 @@ module CssParser
399
416
  DIMENSIONS.each do |property, (top, right, bottom, left)|
400
417
  next unless (declaration = declarations[property])
401
418
 
402
- value = declaration.value.dup
419
+ value = declaration.value
403
420
 
404
421
  # RGB and HSL values in borders are the only units that can have spaces (within params).
405
422
  # We cheat a bit here by stripping spaces after commas in RGB and HSL values so that we
406
423
  # can split easily on spaces.
407
424
  #
408
425
  # TODO: rgba, hsl, hsla
409
- value.gsub!(RE_COLOUR) { |c| c.gsub(/(\s*,\s*)/, ',') }
426
+ value = value.gsub(RE_COLOUR) { |c| c.gsub(/(\s*,\s*)/, ',') } if value.include?(',')
410
427
 
411
428
  matches = split_value_preserving_function_whitespace(value)
412
429
 
@@ -424,7 +441,7 @@ module CssParser
424
441
  raise ArgumentError, "Cannot parse #{value}"
425
442
  end
426
443
 
427
- replacement = [top, right, bottom, left].zip(values).to_h
444
+ replacement = {top => values[0], right => values[1], bottom => values[2], left => values[3]}
428
445
 
429
446
  declarations.replace_declaration!(property, replacement, preserve_importance: true)
430
447
  end
@@ -518,17 +535,13 @@ module CssParser
518
535
 
519
536
  # Combine several properties into a shorthand one
520
537
  def create_shorthand_properties!(properties, shorthand_property) # :nodoc:
521
- values = []
522
- properties_to_delete = []
523
- properties.each do |property|
524
- next unless (declaration = declarations[property])
525
- next if declaration.important
526
-
527
- values << declaration.value
528
- properties_to_delete << property
538
+ properties_to_delete = properties.select do |property|
539
+ (declaration = declarations[property]) && !declaration.important
529
540
  end
530
541
 
531
- return if values.length <= 1
542
+ return if properties_to_delete.length <= 1
543
+
544
+ values = properties_to_delete.map { |property| declarations[property].value }
532
545
 
533
546
  properties_to_delete.each do |property|
534
547
  declarations.delete(property)
@@ -584,14 +597,11 @@ module CssParser
584
597
  return if declarations.size < NUMBER_OF_DIMENSIONS
585
598
 
586
599
  DIMENSIONS.each do |property, dimensions|
587
- values = DIMENSION_DIRECTIONS.each_with_index.with_object({}) do |(side, index), result|
588
- next unless (declaration = declarations[dimensions[index]])
589
-
590
- result[side] = declaration.value
591
- end
592
-
593
600
  # All four dimensions must be present
594
- next if values.size != dimensions.size
601
+ next unless dimensions.all? { |dimension| declarations.key?(dimension) }
602
+
603
+ top, right, bottom, left = dimensions.map { |dimension| declarations[dimension].value }
604
+ values = {top: top, right: right, bottom: bottom, left: left}
595
605
 
596
606
  new_value = values.values_at(*compute_dimensions_shorthand(values)).join(' ').strip
597
607
  declarations[property] = new_value unless new_value.empty?
@@ -641,7 +651,7 @@ module CssParser
641
651
 
642
652
  def compute_dimensions_shorthand(values)
643
653
  # All four sides are equal, returning single value
644
- return [:top] if values.values.uniq.count == 1
654
+ return [:top] if values[:top] == values[:right] && values[:right] == values[:bottom] && values[:bottom] == values[:left]
645
655
 
646
656
  # `/* top | right | bottom | left */`
647
657
  return DIMENSION_DIRECTIONS if values[:left] != values[:right]
@@ -670,12 +680,13 @@ module CssParser
670
680
  next unless (colon = decs.index(COLON))
671
681
 
672
682
  property = decs[0, colon]
673
- value = decs[(colon + 1)..]
683
+ value = decs[colon + 1, decs.length]
674
684
  property.strip!
675
685
  value.strip!
676
- next if property.empty? || value.empty? || value.casecmp?(IMPORTANT)
686
+ # casecmp? case-folds both strings, allocating two of them per declaration
687
+ next if property.empty? || value.empty? || value.casecmp(IMPORTANT).zero?
677
688
 
678
- add_declaration!(property, value)
689
+ add_declaration!(property, value.freeze)
679
690
  continuation = nil
680
691
  end
681
692
  end
@@ -688,7 +699,7 @@ module CssParser
688
699
  # TODO: way too simplistic
689
700
  #++
690
701
  def parse_selectors!(selectors) # :nodoc:
691
- @selectors = selectors.split(',').map do |s|
702
+ @selectors = selectors.split(',').map! do |s|
692
703
  s.gsub!(/\s+/, ' ')
693
704
  s.strip!
694
705
  s
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CssParser
4
- VERSION = '3.3.0'.freeze
4
+ VERSION = '3.4.0'.freeze
5
5
  end
data/lib/css_parser.rb CHANGED
@@ -85,9 +85,10 @@ module CssParser
85
85
  end
86
86
  end
87
87
 
88
- merged = properties.each_with_object(RuleSet.new(nil, nil)) do |(property, details), rule_set|
89
- value = details[:value].strip
90
- rule_set[property.strip] = details[:is_important] ? "#{value.gsub(/;\Z/, '')}!important" : value
88
+ merged = RuleSet.new(nil, nil)
89
+ properties.each do |property, details|
90
+ value = details[:value]
91
+ merged[property] = details[:is_important] ? "#{value.gsub(/;\Z/, '')}!important" : value
91
92
  end
92
93
 
93
94
  merged.create_shorthand!
@@ -107,7 +108,7 @@ module CssParser
107
108
  #++
108
109
  def self.calculate_specificity(selector)
109
110
  a = 0
110
- b = selector.scan('#').length
111
+ b = selector.count('#')
111
112
  c = selector.scan(NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX_NC).length
112
113
  d = selector.scan(ELEMENTS_AND_PSEUDO_ELEMENTS_RX_NC).length
113
114
 
@@ -147,6 +148,8 @@ module CssParser
147
148
  end
148
149
 
149
150
  def self.sanitize_media_query(raw)
151
+ return raw if raw.is_a?(Symbol) && !raw.match?(UNSANITIZED_MEDIA_QUERY_RX)
152
+
150
153
  mq = raw.to_s.gsub(/\s+/, ' ')
151
154
  mq.strip!
152
155
  mq = 'all' if mq.empty?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: css_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Dunae