css_parser 1.5.0 → 1.9.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,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