css_parser 1.7.1 → 1.8.0

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