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