css_parser 1.5.0.pre2 → 1.8.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,65 +1,68 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CssParser
2
- def self.regex_possible_values *values
4
+ def self.regex_possible_values(*values)
3
5
  Regexp.new("([\s]*^)?(#{values.join('|')})([\s]*$)?", 'i')
4
6
  end
5
7
 
6
8
  # :stopdoc:
7
9
  # Base types
8
10
  RE_NL = Regexp.new('(\n|\r\n|\r|\f)')
9
- RE_NON_ASCII = Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE, 'n') #[^\0-\177]
11
+ RE_NON_ASCII = Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE, 'n') # [^\0-\177]
10
12
  RE_UNICODE = Regexp.new('(\\\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])*)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n')
11
13
  RE_ESCAPE = Regexp.union(RE_UNICODE, '|(\\\\[^\n\r\f0-9a-f])')
12
14
  RE_IDENT = Regexp.new("[\-]?([_a-z]|#{RE_NON_ASCII}|#{RE_ESCAPE})([_a-z0-9\-]|#{RE_NON_ASCII}|#{RE_ESCAPE})*", Regexp::IGNORECASE, 'n')
13
15
 
14
16
  # General strings
15
- RE_STRING1 = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')
16
- RE_STRING2 = Regexp.new('(\'(.[^\n\r\f\\\']*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\')')
17
+ RE_STRING1 = /("(.[^\n\r\f"]*|\\#{RE_NL}|#{RE_ESCAPE})*")/.freeze
18
+ RE_STRING2 = /('(.[^\n\r\f']*|\\#{RE_NL}|#{RE_ESCAPE})*')/.freeze
17
19
  RE_STRING = Regexp.union(RE_STRING1, RE_STRING2)
18
20
 
19
21
  RE_INHERIT = regex_possible_values 'inherit'
20
22
 
21
- RE_URI = Regexp.new('(url\([\s]*([\s]*' + RE_STRING.to_s + '[\s]*)[\s]*\))|(url\([\s]*([!#$%&*\-~]|' + RE_NON_ASCII.to_s + '|' + RE_ESCAPE.to_s + ')*[\s]*)\)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n')
22
- URI_RX = /url\(("([^"]*)"|'([^']*)'|([^)]*))\)/im
23
- RE_GRADIENT = /[-a-z]*gradient\([-a-z0-9 .,#%()]*\)/im
23
+ RE_URI = /(url\(\s*(\s*#{RE_STRING}\s*)\s*\))|(url\(\s*([!#$%&*\-~]|#{RE_NON_ASCII}|#{RE_ESCAPE})*\s*)\)/ixm.freeze
24
+ URI_RX = /url\(("([^"]*)"|'([^']*)'|([^)]*))\)/im.freeze
25
+ RE_GRADIENT = /[-a-z]*gradient\([-a-z0-9 .,#%()]*\)/im.freeze
24
26
 
25
27
  # Initial parsing
26
- RE_AT_IMPORT_RULE = /\@import\s+(url\()?["']?(.[^'"\s]*)["']?\)?([\w\s\,^\])]*)\)?;?/
28
+ RE_AT_IMPORT_RULE = /@import\s+(url\()?["']?(.[^'"\s]*)["']?\)?([\w\s,^\])]*)\)?;?/.freeze
27
29
 
28
30
  #--
29
- #RE_AT_MEDIA_RULE = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')
31
+ # RE_AT_MEDIA_RULE = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')
30
32
 
31
- #RE_AT_IMPORT_RULE = Regexp.new('@import[\s]*(' + RE_STRING.to_s + ')([\w\s\,]*)[;]?', Regexp::IGNORECASE) -- should handle url() even though it is not allowed
33
+ # RE_AT_IMPORT_RULE = Regexp.new('@import[\s]*(' + RE_STRING.to_s + ')([\w\s\,]*)[;]?', Regexp::IGNORECASE) -- should handle url() even though it is not allowed
32
34
  #++
33
- IMPORTANT_IN_PROPERTY_RX = /[\s]*!important\b[\s]*/i
35
+ IMPORTANT_IN_PROPERTY_RX = /\s*!important\b\s*/i.freeze
34
36
 
35
37
  RE_INSIDE_OUTSIDE = regex_possible_values 'inside', 'outside'
36
38
  RE_SCROLL_FIXED = regex_possible_values 'scroll', 'fixed'
37
39
  RE_REPEAT = regex_possible_values 'repeat(\-x|\-y)*|no\-repeat'
38
- RE_LIST_STYLE_TYPE = regex_possible_values 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', 'lower-roman',
39
- 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha',
40
- 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana',
41
- 'hira-gana-iroha', 'katakana-iroha', 'katakana', 'none'
40
+ RE_LIST_STYLE_TYPE = regex_possible_values(
41
+ 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', 'lower-roman',
42
+ 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha',
43
+ 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana',
44
+ 'hira-gana-iroha', 'katakana-iroha', 'katakana', 'none'
45
+ )
42
46
 
43
- STRIP_CSS_COMMENTS_RX = /\/\*.*?\*\//m
44
- STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m
47
+ STRIP_CSS_COMMENTS_RX = %r{/\*.*?\*/}m.freeze
48
+ STRIP_HTML_COMMENTS_RX = /<!--|-->/m.freeze
45
49
 
46
50
  # Special units
47
- BOX_MODEL_UNITS_RX = /(auto|inherit|0|([\-]*([0-9]+|[0-9]*\.[0-9]+)(rem|vw|vh|vm|vmin|vmax|e[mx]+|px|[cm]+m|p[tc+]|in|\%)))([\s;]|\Z)/imx
51
+ BOX_MODEL_UNITS_RX = /(auto|inherit|0|(-*([0-9]+|[0-9]*\.[0-9]+)(rem|vw|vh|vm|vmin|vmax|e[mx]+|px|[cm]+m|p[tc+]|in|%)))([\s;]|\Z)/imx.freeze
48
52
  RE_LENGTH_OR_PERCENTAGE = Regexp.new('([\-]*(([0-9]*\.[0-9]+)|[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%))', Regexp::IGNORECASE)
49
53
  RE_BACKGROUND_POSITION = Regexp.new("((((#{RE_LENGTH_OR_PERCENTAGE})|left|center|right|top|bottom)[\s]*){1,2})", Regexp::IGNORECASE | Regexp::EXTENDED)
50
54
  RE_BACKGROUND_SIZE = Regexp.new("\\s*/\\s*((((#{RE_LENGTH_OR_PERCENTAGE})|auto|cover|contain|initial|inherit)[\\s]*){1,2})", Regexp::IGNORECASE | Regexp::EXTENDED)
51
- FONT_UNITS_RX = /(([x]+\-)*small|medium|large[r]*|auto|inherit|([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%)*)/i
52
- RE_BORDER_STYLE = /([\s]*^)?(none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset)([\s]*$)?/imx
55
+ FONT_UNITS_RX = /((x+-)*small|medium|larger*|auto|inherit|([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|%)*)/i.freeze
56
+ RE_BORDER_STYLE = /(\s*^)?(none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset)(\s*$)?/imx.freeze
53
57
  RE_BORDER_UNITS = Regexp.union(BOX_MODEL_UNITS_RX, /(thin|medium|thick)/i)
54
58
 
55
-
56
59
  # Patterns for specificity calculations
57
- NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX= /
58
- (\.[\w]+) # classes
60
+ NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX_NC = /
61
+ (?:\.\w+) # classes
59
62
  |
60
- \[(\w+) # attributes
63
+ \[(?:\w+) # attributes
61
64
  |
62
- (\:( # pseudo classes
65
+ (?::(?: # pseudo classes
63
66
  link|visited|active
64
67
  |hover|focus
65
68
  |lang
@@ -71,16 +74,16 @@ module CssParser
71
74
  |only-child|only-of-type
72
75
  |empty|contains
73
76
  ))
74
- /ix
75
- ELEMENTS_AND_PSEUDO_ELEMENTS_RX = /
76
- ((^|[\s\+\>\~]+)[\w]+ # elements
77
+ /ix.freeze
78
+ ELEMENTS_AND_PSEUDO_ELEMENTS_RX_NC = /
79
+ (?:(?:^|[\s+>~]+)\w+ # elements
77
80
  |
78
- \:{1,2}( # pseudo-elements
81
+ :{1,2}(?: # pseudo-elements
79
82
  after|before
80
83
  |first-letter|first-line
81
84
  |selection
82
85
  )
83
- )/ix
86
+ )/ix.freeze
84
87
 
85
88
  # Colours
86
89
  NAMED_COLOURS = %w[
@@ -235,11 +238,11 @@ module CssParser
235
238
  transparent
236
239
  inherit
237
240
  currentColor
238
- ]
239
- RE_COLOUR_NUMERIC = /\b(hsl|rgb)\s*\(-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?\)/i
240
- RE_COLOUR_NUMERIC_ALPHA = /\b(hsla|rgba)\s*\(-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?\)/i
241
- RE_COLOUR_HEX = /\s*#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/
242
- RE_COLOUR_NAMED = /\s*\b(#{NAMED_COLOURS.join('|')})\b/i
241
+ ].freeze
242
+ RE_COLOUR_NUMERIC = /\b(hsl|rgb)\s*\(-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?\)/i.freeze
243
+ RE_COLOUR_NUMERIC_ALPHA = /\b(hsla|rgba)\s*\(-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?\)/i.freeze
244
+ RE_COLOUR_HEX = /\s*#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/.freeze
245
+ RE_COLOUR_NAMED = /\s*\b(#{NAMED_COLOURS.join('|')})\b/i.freeze
243
246
  RE_COLOUR = Regexp.union(RE_COLOUR_NUMERIC, RE_COLOUR_NUMERIC_ALPHA, RE_COLOUR_HEX, RE_COLOUR_NAMED)
244
247
  # :startdoc:
245
248
  end
@@ -1,77 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
1
5
  module CssParser
2
6
  class RuleSet
3
7
  # Patterns for specificity calculations
4
- RE_ELEMENTS_AND_PSEUDO_ELEMENTS = /((^|[\s\+\>]+)[\w]+|\:(first\-line|first\-letter|before|after))/i
5
- RE_NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES = /(\.[\w]+)|(\[[\w]+)|(\:(link|first\-child|lang))/i
8
+ RE_ELEMENTS_AND_PSEUDO_ELEMENTS = /((^|[\s+>]+)\w+|:(first-line|first-letter|before|after))/i.freeze
9
+ RE_NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES = /(\.\w+)|(\[\w+)|(:(link|first-child|lang))/i.freeze
10
+
11
+ BACKGROUND_PROPERTIES = ['background-color', 'background-image', 'background-repeat', 'background-position', 'background-size', 'background-attachment'].freeze
12
+ LIST_STYLE_PROPERTIES = ['list-style-type', 'list-style-position', 'list-style-image'].freeze
13
+ FONT_STYLE_PROPERTIES = ['font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family'].freeze
14
+ BORDER_STYLE_PROPERTIES = ['border-width', 'border-style', 'border-color'].freeze
15
+ BORDER_PROPERTIES = ['border', 'border-left', 'border-right', 'border-top', 'border-bottom'].freeze
16
+
17
+ NUMBER_OF_DIMENSIONS = 4
18
+
19
+ DIMENSIONS = [
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
+ class Declarations
28
+ class Value
29
+ attr_reader :value
30
+ attr_accessor :important
31
+
32
+ def initialize(value, important: nil)
33
+ self.value = value
34
+ @important = important unless important.nil?
35
+ end
36
+
37
+ def value=(value)
38
+ value = value.to_s.sub(/\s*;\s*\Z/, '')
39
+ self.important = !value.slice!(CssParser::IMPORTANT_IN_PROPERTY_RX).nil?
40
+ value.strip!
41
+ raise ArgumentError, 'value is empty' if value.empty?
42
+
43
+ @value = value.freeze
44
+ end
45
+
46
+ def to_s
47
+ return value unless important
48
+
49
+ "#{value} !important"
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
6
60
 
7
- BACKGROUND_PROPERTIES = ['background-color', 'background-image', 'background-repeat', 'background-position', 'background-size', 'background-attachment']
8
- LIST_STYLE_PROPERTIES = ['list-style-type', 'list-style-position', 'list-style-image']
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
+ return
92
+ end
93
+
94
+ if value.to_s.strip.empty?
95
+ delete(property)
96
+ return
97
+ end
98
+
99
+ declarations[property] = Value.new(value)
100
+ end
101
+ alias add_declaration! []=
102
+
103
+ def [](property)
104
+ declarations[normalize_property(property)]
105
+ end
106
+ alias get_value []
107
+
108
+ def key?(property)
109
+ declarations.key?(normalize_property(property))
110
+ end
111
+
112
+ def size
113
+ declarations.size
114
+ end
115
+
116
+ # Remove CSS declaration
117
+ # @param [#to_s] property property to be removed
118
+ #
119
+ # @example
120
+ # declarations.delete('color')
121
+ def delete(property)
122
+ declarations.delete(normalize_property(property))
123
+ end
124
+ alias remove_declaration! delete
125
+
126
+ # Replace CSS property with multiple declarations
127
+ # @param [#to_s] property property name to be replaces
128
+ # @param [Hash<String => [String, Value]>] replacements hash with properties to replace with
129
+ #
130
+ # @example
131
+ # declarations = Declarations.new('line-height' => '0.25px', 'font' => 'small-caps', 'font-size' => '12em')
132
+ # declarations.replace_declaration!('font', {'line-height' => '1px', 'font-variant' => 'small-caps', 'font-size' => '24px'})
133
+ # declarations
134
+ # => #<CssParser::RuleSet::Declarations:0x00000000029c3018
135
+ # @declarations=
136
+ # {"line-height"=>#<CssParser::RuleSet::Declarations::Value:0x00000000038ac458 @important=false, @value="1px">,
137
+ # "font-variant"=>#<CssParser::RuleSet::Declarations::Value:0x00000000039b3ec8 @important=false, @value="small-caps">,
138
+ # "font-size"=>#<CssParser::RuleSet::Declarations::Value:0x00000000029c2c80 @important=false, @value="12em">}>
139
+ def replace_declaration!(property, replacements, preserve_importance: false)
140
+ property = normalize_property(property)
141
+ raise ArgumentError, "property #{property} does not exist" unless key?(property)
142
+
143
+ replacement_declarations = self.class.new(replacements)
144
+
145
+ if preserve_importance
146
+ importance = get_value(property).important
147
+ replacement_declarations.each { |_key, value| value.important = importance }
148
+ end
149
+
150
+ replacement_keys = declarations.keys
151
+ replacement_values = declarations.values
152
+ property_index = replacement_keys.index(property)
153
+
154
+ # We should preserve subsequent declarations of the same properties
155
+ # and prior important ones if replacement one is not important
156
+ replacements = replacement_declarations.each.with_object({}) do |(key, value), result|
157
+ # Replacement property doesn't exist, adding
158
+ next result[key] = value unless declarations.key?(key)
159
+
160
+ # Replacement property is important while existing one is not,
161
+ # replacing unconditionally
162
+ if value.important && !declarations[key].important
163
+ result[key] = value
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 value is important while replacing is not, existing one
172
+ # takes precedence
173
+ next if !value.important && declarations[key].important
174
+
175
+ # Importance of existing and replacement values are the same,
176
+ # value which is declared later wins
177
+ result[key] = value if property_index > replacement_keys.index(key)
178
+ end
179
+
180
+ return if replacements.empty?
181
+
182
+ replacement_keys.delete_at(property_index)
183
+ replacement_keys.insert(property_index, *replacements.keys)
184
+
185
+ replacement_values.delete_at(property_index)
186
+ replacement_values.insert(property_index, *replacements.values)
187
+
188
+ self.declarations = replacement_keys.zip(replacement_values).to_h
189
+ end
190
+
191
+ def to_s(options = {})
192
+ str = declarations.reduce(String.new) do |memo, (prop, value)|
193
+ importance = options[:force_important] || value.important ? ' !important' : ''
194
+ memo << "#{prop}: #{value.value}#{importance}; "
195
+ end
196
+ # TODO: Clean-up regexp doesn't seem to work
197
+ str.gsub!(/^[\s^({)]+|[\n\r\f\t]*|\s+$/mx, '')
198
+ str.strip!
199
+ str
200
+ end
201
+
202
+ def ==(other)
203
+ return false unless other.is_a?(self.class)
204
+
205
+ declarations == other.declarations && declarations.keys == other.declarations.keys
206
+ end
207
+
208
+ protected
209
+
210
+ attr_reader :declarations
211
+
212
+ private
213
+
214
+ attr_writer :declarations
215
+
216
+ def normalize_property(property)
217
+ property = property.to_s.downcase
218
+ property.strip!
219
+ property
220
+ end
221
+ end
222
+
223
+ extend Forwardable
9
224
 
10
225
  # Array of selector strings.
11
- attr_reader :selectors
226
+ attr_reader :selectors
12
227
 
13
228
  # Integer with the specificity to use for this RuleSet.
14
- attr_accessor :specificity
229
+ attr_accessor :specificity
230
+
231
+ # @!method add_declaration!
232
+ # @see CssParser::RuleSet::Declarations#add_declaration!
233
+ # @!method delete
234
+ # @see CssParser::RuleSet::Declarations#delete
235
+ def_delegators :declarations, :add_declaration!, :delete
236
+ alias []= add_declaration!
237
+ alias remove_declaration! delete
15
238
 
16
239
  def initialize(selectors, block, specificity = nil)
17
240
  @selectors = []
18
241
  @specificity = specificity
19
- @declarations = {}
20
- @order = 0
21
242
  parse_selectors!(selectors) if selectors
22
243
  parse_declarations!(block)
23
244
  end
24
245
 
25
246
  # Get the value of a property
26
247
  def get_value(property)
27
- return '' unless property and not property.empty?
28
-
29
- property = property.downcase.strip
30
- properties = @declarations.inject('') do |val, (key, data)|
31
- #puts "COMPARING #{key} #{key.inspect} against #{property} #{property.inspect}"
32
- importance = data[:is_important] ? ' !important' : ''
33
- val << "#{data[:value]}#{importance}; " if key.downcase.strip == property
34
- val
35
- end
36
- return properties ? properties.strip : ''
37
- end
38
- alias_method :[], :get_value
39
-
40
- # Add a CSS declaration to the current RuleSet.
41
- #
42
- # rule_set.add_declaration!('color', 'blue')
43
- #
44
- # puts rule_set['color']
45
- # => 'blue;'
46
- #
47
- # rule_set.add_declaration!('margin', '0px auto !important')
48
- #
49
- # puts rule_set['margin']
50
- # => '0px auto !important;'
51
- #
52
- # If the property already exists its value will be over-written.
53
- def add_declaration!(property, value)
54
- if value.nil? or value.empty?
55
- @declarations.delete(property)
56
- return
57
- end
248
+ return '' unless declarations.key?(property)
58
249
 
59
- value.gsub!(/;\Z/, '')
60
- is_important = !value.gsub!(CssParser::IMPORTANT_IN_PROPERTY_RX, '').nil?
61
- property = property.downcase.strip
62
- #puts "SAVING #{property} #{value} #{is_important.inspect}"
63
- @declarations[property] = {
64
- :value => value, :is_important => is_important, :order => @order += 1
65
- }
66
- end
67
- alias_method :[]=, :add_declaration!
68
-
69
- # Remove CSS declaration from the current RuleSet.
70
- #
71
- # rule_set.remove_declaration!('color')
72
- def remove_declaration!(property)
73
- @declarations.delete(property)
250
+ "#{declarations[property]};"
74
251
  end
252
+ alias [] get_value
75
253
 
76
254
  # Iterate through selectors.
77
255
  #
@@ -83,41 +261,29 @@ module CssParser
83
261
  # ...
84
262
  # end
85
263
  def each_selector(options = {}) # :yields: selector, declarations, specificity
86
- declarations = declarations_to_s(options)
264
+ decs = declarations.to_s(options)
87
265
  if @specificity
88
- @selectors.each { |sel| yield sel.strip, declarations, @specificity }
266
+ @selectors.each { |sel| yield sel.strip, decs, @specificity }
89
267
  else
90
- @selectors.each { |sel| yield sel.strip, declarations, CssParser.calculate_specificity(sel) }
268
+ @selectors.each { |sel| yield sel.strip, decs, CssParser.calculate_specificity(sel) }
91
269
  end
92
270
  end
93
271
 
94
272
  # Iterate through declarations.
95
273
  def each_declaration # :yields: property, value, is_important
96
- decs = @declarations.sort { |a,b| a[1][:order].nil? || b[1][:order].nil? ? 0 : a[1][:order] <=> b[1][:order] }
97
- decs.each do |property, data|
98
- value = data[:value]
99
- yield property.downcase.strip, value.strip, data[:is_important]
274
+ declarations.each do |property_name, value|
275
+ yield property_name, value.value, value.important
100
276
  end
101
277
  end
102
278
 
103
279
  # Return all declarations as a string.
104
- #--
105
- # TODO: Clean-up regexp doesn't seem to work
106
- #++
107
280
  def declarations_to_s(options = {})
108
- options = {:force_important => false}.merge(options)
109
- str = ''
110
- each_declaration do |prop, val, is_important|
111
- importance = (options[:force_important] || is_important) ? ' !important' : ''
112
- str += "#{prop}: #{val}#{importance}; "
113
- end
114
- str.gsub(/^[\s^(\{)]+|[\n\r\f\t]*|[\s]+$/mx, '').strip
281
+ declarations.to_s(options)
115
282
  end
116
283
 
117
284
  # Return the CSS rule set as a string.
118
285
  def to_s
119
- decs = declarations_to_s
120
- "#{@selectors.join(',')} { #{decs} }"
286
+ "#{@selectors.join(',')} { #{declarations} }"
121
287
  end
122
288
 
123
289
  # Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
@@ -135,140 +301,126 @@ module CssParser
135
301
  #
136
302
  # See http://www.w3.org/TR/CSS21/colors.html#propdef-background
137
303
  def expand_background_shorthand! # :nodoc:
138
- return unless @declarations.has_key?('background')
139
-
140
- value = @declarations['background'][:value]
141
-
142
- if value =~ CssParser::RE_INHERIT
143
- BACKGROUND_PROPERTIES.each do |prop|
144
- split_declaration('background', prop, 'inherit')
145
- end
146
- end
147
-
148
- split_declaration('background', 'background-image', value.slice!(Regexp.union(CssParser::URI_RX, CssParser::RE_GRADIENT, /none/i)))
149
- split_declaration('background', 'background-attachment', value.slice!(CssParser::RE_SCROLL_FIXED))
150
- split_declaration('background', 'background-repeat', value.slice!(CssParser::RE_REPEAT))
151
- split_declaration('background', 'background-color', value.slice!(CssParser::RE_COLOUR))
152
- split_declaration('background', 'background-size', extract_background_size_from(value))
153
- split_declaration('background', 'background-position', value.slice(CssParser::RE_BACKGROUND_POSITION))
304
+ return unless declarations.key?('background')
305
+
306
+ value = declarations['background'].value.dup
307
+
308
+ replacement = BACKGROUND_PROPERTIES.map { |key| [key, 'inherit'] }.to_h if value.match(CssParser::RE_INHERIT)
309
+ replacement ||= {
310
+ 'background-image' => value.slice!(Regexp.union(CssParser::URI_RX, CssParser::RE_GRADIENT, /none/i)),
311
+ 'background-attachment' => value.slice!(CssParser::RE_SCROLL_FIXED),
312
+ 'background-repeat' => value.slice!(CssParser::RE_REPEAT),
313
+ 'background-color' => value.slice!(CssParser::RE_COLOUR),
314
+ 'background-size' => extract_background_size_from(value),
315
+ 'background-position' => value.slice!(CssParser::RE_BACKGROUND_POSITION)
316
+ }
154
317
 
155
- @declarations.delete('background')
318
+ declarations.replace_declaration!('background', replacement, preserve_importance: true)
156
319
  end
157
320
 
158
321
  def extract_background_size_from(value)
159
322
  size = value.slice!(CssParser::RE_BACKGROUND_SIZE)
160
323
 
161
- size.sub(/^\s*\/\s*/, '') if size
324
+ size.sub(%r{^\s*/\s*}, '') if size
162
325
  end
163
326
 
164
327
  # Split shorthand border declarations (e.g. <tt>border: 1px red;</tt>)
165
328
  # Additional splitting happens in expand_dimensions_shorthand!
166
329
  def expand_border_shorthand! # :nodoc:
167
- ['border', 'border-left', 'border-right', 'border-top', 'border-bottom'].each do |k|
168
- next unless @declarations.has_key?(k)
330
+ BORDER_PROPERTIES.each do |k|
331
+ next unless declarations.key?(k)
169
332
 
170
- value = @declarations[k][:value]
333
+ value = declarations[k].value.dup
171
334
 
172
- split_declaration(k, "#{k}-width", value.slice!(CssParser::RE_BORDER_UNITS))
173
- split_declaration(k, "#{k}-color", value.slice!(CssParser::RE_COLOUR))
174
- split_declaration(k, "#{k}-style", value.slice!(CssParser::RE_BORDER_STYLE))
335
+ replacement = {
336
+ "#{k}-width" => value.slice!(CssParser::RE_BORDER_UNITS),
337
+ "#{k}-color" => value.slice!(CssParser::RE_COLOUR),
338
+ "#{k}-style" => value.slice!(CssParser::RE_BORDER_STYLE)
339
+ }
175
340
 
176
- @declarations.delete(k)
341
+ declarations.replace_declaration!(k, replacement, preserve_importance: true)
177
342
  end
178
343
  end
179
344
 
180
345
  # Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)
181
346
  # into their constituent parts. Handles margin, padding, border-color, border-style and border-width.
182
347
  def expand_dimensions_shorthand! # :nodoc:
183
- {'margin' => 'margin-%s',
184
- 'padding' => 'padding-%s',
185
- 'border-color' => 'border-%s-color',
186
- 'border-style' => 'border-%s-style',
187
- 'border-width' => 'border-%s-width'}.each do |property, expanded|
348
+ DIMENSIONS.each do |property, (top, right, bottom, left)|
349
+ next unless declarations.key?(property)
188
350
 
189
- next unless @declarations.has_key?(property)
190
-
191
- value = @declarations[property][:value]
351
+ value = declarations[property].value.dup
192
352
 
193
353
  # RGB and HSL values in borders are the only units that can have spaces (within params).
194
354
  # We cheat a bit here by stripping spaces after commas in RGB and HSL values so that we
195
355
  # can split easily on spaces.
196
356
  #
197
357
  # TODO: rgba, hsl, hsla
198
- value.gsub!(RE_COLOUR) { |c| c.gsub(/(\s*\,\s*)/, ',') }
358
+ value.gsub!(RE_COLOUR) { |c| c.gsub(/(\s*,\s*)/, ',') }
199
359
 
200
360
  matches = value.strip.split(/\s+/)
201
361
 
202
- t, r, b, l = nil
203
-
204
362
  case matches.length
205
- when 1
206
- t, r, b, l = matches[0], matches[0], matches[0], matches[0]
207
- when 2
208
- t, b = matches[0], matches[0]
209
- r, l = matches[1], matches[1]
210
- when 3
211
- t = matches[0]
212
- r, l = matches[1], matches[1]
213
- b = matches[2]
214
- when 4
215
- t = matches[0]
216
- r = matches[1]
217
- b = matches[2]
218
- l = matches[3]
363
+ when 1
364
+ values = matches.to_a * 4
365
+ when 2
366
+ values = matches.to_a * 2
367
+ when 3
368
+ values = matches.to_a
369
+ values << matches[1] # left = right
370
+ when 4
371
+ values = matches.to_a
219
372
  end
220
373
 
221
- split_declaration(property, expanded % 'top', t)
222
- split_declaration(property, expanded % 'right', r)
223
- split_declaration(property, expanded % 'bottom', b)
224
- split_declaration(property, expanded % 'left', l)
374
+ t, r, b, l = values
225
375
 
226
- @declarations.delete(property)
376
+ declarations.replace_declaration!(
377
+ property,
378
+ {top => t, right => r, bottom => b, left => l},
379
+ preserve_importance: true
380
+ )
227
381
  end
228
382
  end
229
383
 
230
384
  # Convert shorthand font declarations (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)
231
385
  # into their constituent parts.
232
386
  def expand_font_shorthand! # :nodoc:
233
- return unless @declarations.has_key?('font')
387
+ return unless declarations.key?('font')
234
388
 
235
389
  font_props = {}
236
390
 
237
391
  # reset properties to 'normal' per http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
238
- ['font-style', 'font-variant', 'font-weight', 'font-size',
239
- 'line-height'].each do |prop|
392
+ ['font-style', 'font-variant', 'font-weight', 'font-size', 'line-height'].each do |prop|
240
393
  font_props[prop] = 'normal'
241
- end
394
+ end
242
395
 
243
- value = @declarations['font'][:value]
244
- is_important = @declarations['font'][:is_important]
245
- order = @declarations['font'][:order]
396
+ value = declarations['font'].value.dup
397
+ value.gsub!(%r{/\s+}, '/') # handle spaces between font size and height shorthand (e.g. 14px/ 16px)
246
398
 
247
399
  in_fonts = false
248
400
 
249
401
  matches = value.scan(/("(.*[^"])"|'(.*[^'])'|(\w[^ ,]+))/)
250
402
  matches.each do |match|
251
403
  m = match[0].to_s.strip
252
- m.gsub!(/[;]$/, '')
404
+ m.gsub!(/;$/, '')
253
405
 
254
406
  if in_fonts
255
- if font_props.has_key?('font-family')
256
- font_props['font-family'] += ', ' + m
407
+ if font_props.key?('font-family')
408
+ font_props['font-family'] += ", #{m}"
257
409
  else
258
410
  font_props['font-family'] = m
259
411
  end
260
412
  elsif m =~ /normal|inherit/i
261
413
  ['font-style', 'font-weight', 'font-variant'].each do |font_prop|
262
- font_props[font_prop] = m unless font_props.has_key?(font_prop)
414
+ font_props[font_prop] = m unless font_props.key?(font_prop)
263
415
  end
264
416
  elsif m =~ /italic|oblique/i
265
417
  font_props['font-style'] = m
266
- elsif m =~ /small\-caps/i
418
+ elsif m =~ /small-caps/i
267
419
  font_props['font-variant'] = m
268
420
  elsif m =~ /[1-9]00$|bold|bolder|lighter/i
269
421
  font_props['font-weight'] = m
270
422
  elsif m =~ CssParser::FONT_UNITS_RX
271
- if m =~ /\//
423
+ if m =~ %r{/}
272
424
  font_props['font-size'], font_props['line-height'] = m.split('/')
273
425
  else
274
426
  font_props['font-size'] = m
@@ -277,9 +429,7 @@ module CssParser
277
429
  end
278
430
  end
279
431
 
280
- font_props.each { |font_prop, font_val| @declarations[font_prop] = {:value => font_val, :is_important => is_important, :order => order} }
281
-
282
- @declarations.delete('font')
432
+ declarations.replace_declaration!('font', font_props, preserve_importance: true)
283
433
  end
284
434
 
285
435
  # Convert shorthand list-style declarations (e.g. <tt>list-style: lower-alpha outside;</tt>)
@@ -287,21 +437,18 @@ module CssParser
287
437
  #
288
438
  # See http://www.w3.org/TR/CSS21/generate.html#lists
289
439
  def expand_list_style_shorthand! # :nodoc:
290
- return unless @declarations.has_key?('list-style')
291
-
292
- value = @declarations['list-style'][:value]
440
+ return unless declarations.key?('list-style')
293
441
 
294
- if value =~ CssParser::RE_INHERIT
295
- LIST_STYLE_PROPERTIES.each do |prop|
296
- split_declaration('list-style', prop, 'inherit')
297
- end
298
- end
442
+ value = declarations['list-style'].value.dup
299
443
 
300
- split_declaration('list-style', 'list-style-type', value.slice!(CssParser::RE_LIST_STYLE_TYPE))
301
- split_declaration('list-style', 'list-style-position', value.slice!(CssParser::RE_INSIDE_OUTSIDE))
302
- split_declaration('list-style', 'list-style-image', value.slice!(Regexp.union(CssParser::URI_RX, /none/i)))
444
+ replacement = LIST_STYLE_PROPERTIES.map { |key| [key, 'inherit'] }.to_h if value =~ CssParser::RE_INHERIT
445
+ replacement ||= {
446
+ 'list-style-type' => value.slice!(CssParser::RE_LIST_STYLE_TYPE),
447
+ 'list-style-position' => value.slice!(CssParser::RE_INSIDE_OUTSIDE),
448
+ 'list-style-image' => value.slice!(Regexp.union(CssParser::URI_RX, /none/i))
449
+ }
303
450
 
304
- @declarations.delete('list-style')
451
+ declarations.replace_declaration!('list-style', replacement, preserve_importance: true)
305
452
  end
306
453
 
307
454
  # Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
@@ -315,23 +462,23 @@ module CssParser
315
462
  end
316
463
 
317
464
  # Combine several properties into a shorthand one
318
- def create_shorthand_properties! properties, shorthand_property # :nodoc:
465
+ def create_shorthand_properties!(properties, shorthand_property) # :nodoc:
319
466
  values = []
320
467
  properties_to_delete = []
321
468
  properties.each do |property|
322
- if @declarations.has_key?(property) and not @declarations[property][:is_important]
323
- values << @declarations[property][:value]
469
+ if declarations.key?(property) and not declarations[property].important
470
+ values << declarations[property].value
324
471
  properties_to_delete << property
325
472
  end
326
473
  end
327
474
 
328
- if values.length > 1
329
- properties_to_delete.each do |property|
330
- @declarations.delete(property)
331
- end
475
+ return if values.length <= 1
332
476
 
333
- @declarations[shorthand_property] = {:value => values.join(' ')}
477
+ properties_to_delete.each do |property|
478
+ declarations.delete(property)
334
479
  end
480
+
481
+ declarations[shorthand_property] = values.join(' ')
335
482
  end
336
483
 
337
484
  # Looks for long format CSS background properties (e.g. <tt>background-color</tt>) and
@@ -340,15 +487,15 @@ module CssParser
340
487
  # Leaves properties declared !important alone.
341
488
  def create_background_shorthand! # :nodoc:
342
489
  # When we have a background-size property we must separate it and distinguish it from
343
- # background-position by preceeding it with a backslash. In this case we also need to
490
+ # background-position by preceding it with a backslash. In this case we also need to
344
491
  # have a background-position property, so we set it if it's missing.
345
492
  # http://www.w3schools.com/cssref/css3_pr_background.asp
346
- if @declarations.has_key?('background-size') and not @declarations['background-size'][:is_important]
347
- unless @declarations.has_key?('background-position')
348
- @declarations['background-position'] = {:value => '0% 0%'}
493
+ if declarations.key?('background-size') and not declarations['background-size'].important
494
+ unless declarations.key?('background-position')
495
+ declarations['background-position'] = '0% 0%'
349
496
  end
350
497
 
351
- @declarations['background-size'][:value] = "/ #{@declarations['background-size'][:value]}"
498
+ declarations['background-size'].value = "/ #{declarations['background-size'].value}"
352
499
  end
353
500
 
354
501
  create_shorthand_properties! BACKGROUND_PROPERTIES, 'background'
@@ -361,100 +508,68 @@ module CssParser
361
508
  def create_border_shorthand! # :nodoc:
362
509
  values = []
363
510
 
364
- ['border-width', 'border-style', 'border-color'].each do |property|
365
- if @declarations.has_key?(property) and not @declarations[property][:is_important]
366
- # can't merge if any value contains a space (i.e. has multiple values)
367
- # we temporarily remove any spaces after commas for the check (inside rgba, etc...)
368
- return if @declarations[property][:value].gsub(/\,[\s]/, ',').strip =~ /[\s]/
369
- values << @declarations[property][:value]
370
- end
511
+ BORDER_STYLE_PROPERTIES.each do |property|
512
+ next unless declarations.key?(property) and not declarations[property].important
513
+ # can't merge if any value contains a space (i.e. has multiple values)
514
+ # we temporarily remove any spaces after commas for the check (inside rgba, etc...)
515
+ return nil if declarations[property].value.gsub(/,\s/, ',').strip =~ /\s/
516
+
517
+ values << declarations[property].value
518
+ declarations.delete(property)
371
519
  end
372
520
 
373
- @declarations.delete('border-width')
374
- @declarations.delete('border-style')
375
- @declarations.delete('border-color')
521
+ return if values.empty?
376
522
 
377
- unless values.empty?
378
- @declarations['border'] = {:value => values.join(' ')}
379
- end
523
+ declarations['border'] = values.join(' ')
380
524
  end
381
525
 
382
526
  # Looks for long format CSS dimensional properties (margin, padding, border-color, border-style and border-width)
383
527
  # and converts them into shorthand CSS properties.
384
528
  def create_dimensions_shorthand! # :nodoc:
385
- directions = ['top', 'right', 'bottom', 'left']
529
+ return if declarations.size < NUMBER_OF_DIMENSIONS
386
530
 
387
- {'margin' => 'margin-%s',
388
- 'padding' => 'padding-%s',
389
- 'border-color' => 'border-%s-color',
390
- 'border-style' => 'border-%s-style',
391
- 'border-width' => 'border-%s-width'}.each do |property, expanded|
531
+ DIMENSIONS.each do |property, dimensions|
532
+ values = %i[top right bottom left].each_with_index.with_object({}) do |(side, index), result|
533
+ next unless declarations.key?(dimensions[index])
392
534
 
393
- top, right, bottom, left = ['top', 'right', 'bottom', 'left'].map { |side| expanded % side }
394
- foldable = @declarations.select do |dim, val|
395
- dim == top or dim == right or dim == bottom or dim == left
535
+ result[side] = declarations[dimensions[index]].value
396
536
  end
537
+
397
538
  # All four dimensions must be present
398
- if foldable.length == 4
399
- values = {}
400
-
401
- directions.each { |d| values[d.to_sym] = @declarations[expanded % d][:value].downcase.strip }
402
-
403
- if values[:left] == values[:right]
404
- if values[:top] == values[:bottom]
405
- if values[:top] == values[:left] # All four sides are equal
406
- new_value = values[:top]
407
- else # Top and bottom are equal, left and right are equal
408
- new_value = values[:top] + ' ' + values[:left]
409
- end
410
- else # Only left and right are equal
411
- new_value = values[:top] + ' ' + values[:left] + ' ' + values[:bottom]
412
- end
413
- else # No sides are equal
414
- new_value = values[:top] + ' ' + values[:right] + ' ' + values[:bottom] + ' ' + values[:left]
415
- end
539
+ next if values.size != dimensions.size
416
540
 
417
- new_value.strip!
418
- @declarations[property] = {:value => new_value.strip} unless new_value.empty?
541
+ new_value = values.values_at(*compute_dimensions_shorthand(values)).join(' ').strip
542
+ declarations[property] = new_value unless new_value.empty?
419
543
 
420
- # Delete the longhand values
421
- directions.each { |d| @declarations.delete(expanded % d) }
422
- end
544
+ # Delete the longhand values
545
+ dimensions.each { |d| declarations.delete(d) }
423
546
  end
424
547
  end
425
548
 
426
-
427
549
  # Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and
428
550
  # tries to convert them into a shorthand CSS <tt>font</tt> property. All
429
551
  # font properties must be present in order to create a shorthand declaration.
430
552
  def create_font_shorthand! # :nodoc:
431
- ['font-style', 'font-variant', 'font-weight', 'font-size',
432
- 'line-height', 'font-family'].each do |prop|
433
- return unless @declarations.has_key?(prop)
434
- end
553
+ return unless FONT_STYLE_PROPERTIES.all? { |prop| declarations.key?(prop) }
435
554
 
436
- new_value = ''
555
+ new_value = String.new
437
556
  ['font-style', 'font-variant', 'font-weight'].each do |property|
438
- unless @declarations[property][:value] == 'normal'
439
- new_value += @declarations[property][:value] + ' '
557
+ unless declarations[property].value == 'normal'
558
+ new_value << declarations[property].value << ' '
440
559
  end
441
560
  end
442
561
 
443
- new_value += @declarations['font-size'][:value]
562
+ new_value << declarations['font-size'].value
444
563
 
445
- unless @declarations['line-height'][:value] == 'normal'
446
- new_value += '/' + @declarations['line-height'][:value]
564
+ unless declarations['line-height'].value == 'normal'
565
+ new_value << '/' << declarations['line-height'].value
447
566
  end
448
567
 
449
- new_value += ' ' + @declarations['font-family'][:value]
450
-
451
- @declarations['font'] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
568
+ new_value << ' ' << declarations['font-family'].value
452
569
 
453
- ['font-style', 'font-variant', 'font-weight', 'font-size',
454
- 'line-height', 'font-family'].each do |prop|
455
- @declarations.delete(prop)
456
- end
570
+ declarations['font'] = new_value.gsub(/\s+/, ' ')
457
571
 
572
+ FONT_STYLE_PROPERTIES.each { |prop| declarations.delete(prop) }
458
573
  end
459
574
 
460
575
  # Looks for long format CSS list-style properties (e.g. <tt>list-style-type</tt>) and
@@ -467,41 +582,37 @@ module CssParser
467
582
 
468
583
  private
469
584
 
470
- # utility method for re-assign shorthand elements to longhand versions
471
- def split_declaration(src, dest, v) # :nodoc:
472
- return unless v and not v.empty?
585
+ attr_accessor :declarations
473
586
 
474
- if @declarations.has_key?(dest)
475
- #puts "dest #{dest} already exists"
587
+ def compute_dimensions_shorthand(values)
588
+ # All four sides are equal, returning single value
589
+ return %i[top] if values.values.uniq.count == 1
476
590
 
477
- if @declarations[src][:order].nil? || (!@declarations[dest][:order].nil? && @declarations[dest][:order] > @declarations[src][:order])
478
- #puts "skipping #{dest}:#{v} due to order "
479
- return
480
- else
481
- @declarations[dest] = {}
482
- end
483
- end
484
- @declarations[dest] = @declarations[src].merge({:value => v.to_s.strip})
591
+ # `/* top | right | bottom | left */`
592
+ return %i[top right bottom left] if values[:left] != values[:right]
593
+
594
+ # Vertical are the same & horizontal are the same, `/* vertical | horizontal */`
595
+ return %i[top left] if values[:top] == values[:bottom]
596
+
597
+ %i[top left bottom]
485
598
  end
486
599
 
487
600
  def parse_declarations!(block) # :nodoc:
488
- @declarations = {}
601
+ self.declarations = Declarations.new
489
602
 
490
603
  return unless block
491
604
 
492
- block.gsub!(/(^[\s]*)|([\s]*$)/, '')
493
-
494
- continuation = ''
495
- block.split(/[\;$]+/m).each do |decs|
496
- decs = continuation + decs
605
+ continuation = nil
606
+ block.split(/[;$]+/m).each do |decs|
607
+ decs = continuation ? continuation + decs : decs
497
608
  if decs =~ /\([^)]*\Z/ # if it has an unmatched parenthesis
498
- continuation = decs + ';'
499
-
500
- elsif matches = decs.match(/(.[^:]*)\s*:\s*(.+)(;?\s*\Z)/i)
501
- property, value, = matches.captures # skip end_of_declaration
502
-
609
+ continuation = "#{decs};"
610
+ elsif (matches = decs.match(/\s*(.[^:]*)\s*:\s*(.+?)(;?\s*\Z)/i))
611
+ # skip end_of_declaration
612
+ property = matches[1]
613
+ value = matches[2]
503
614
  add_declaration!(property, value)
504
- continuation = ''
615
+ continuation = nil
505
616
  end
506
617
  end
507
618
  end
@@ -510,12 +621,15 @@ module CssParser
510
621
  # TODO: way too simplistic
511
622
  #++
512
623
  def parse_selectors!(selectors) # :nodoc:
513
- @selectors = selectors.split(',').map { |s| s.gsub(/\s+/, ' ').strip }
624
+ @selectors = selectors.split(',').map do |s|
625
+ s.gsub!(/\s+/, ' ')
626
+ s.strip!
627
+ s
628
+ end
514
629
  end
515
630
  end
516
631
 
517
632
  class OffsetAwareRuleSet < RuleSet
518
-
519
633
  # File offset range
520
634
  attr_reader :offset
521
635