css_parser 1.5.0 → 1.9.0

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