css_parser 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,65 +1,68 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CssParser
2
- def self.regex_possible_values *values
4
+ def self.regex_possible_values(*values)
3
5
  Regexp.new("([\s]*^)?(#{values.join('|')})([\s]*$)?", 'i')
4
6
  end
5
7
 
6
8
  # :stopdoc:
7
9
  # Base types
8
10
  RE_NL = Regexp.new('(\n|\r\n|\r|\f)')
9
- RE_NON_ASCII = Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE, 'n') #[^\0-\177]
11
+ RE_NON_ASCII = Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE, 'n') # [^\0-\177]
10
12
  RE_UNICODE = Regexp.new('(\\\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])*)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n')
11
13
  RE_ESCAPE = Regexp.union(RE_UNICODE, '|(\\\\[^\n\r\f0-9a-f])')
12
14
  RE_IDENT = Regexp.new("[\-]?([_a-z]|#{RE_NON_ASCII}|#{RE_ESCAPE})([_a-z0-9\-]|#{RE_NON_ASCII}|#{RE_ESCAPE})*", Regexp::IGNORECASE, 'n')
13
15
 
14
16
  # General strings
15
- RE_STRING1 = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')
16
- RE_STRING2 = Regexp.new('(\'(.[^\n\r\f\\\']*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\')')
17
+ RE_STRING1 = /("(.[^\n\r\f"]*|\\#{RE_NL}|#{RE_ESCAPE})*")/.freeze
18
+ RE_STRING2 = /('(.[^\n\r\f']*|\\#{RE_NL}|#{RE_ESCAPE})*')/.freeze
17
19
  RE_STRING = Regexp.union(RE_STRING1, RE_STRING2)
18
20
 
19
21
  RE_INHERIT = regex_possible_values 'inherit'
20
22
 
21
- RE_URI = Regexp.new('(url\([\s]*([\s]*' + RE_STRING.to_s + '[\s]*)[\s]*\))|(url\([\s]*([!#$%&*\-~]|' + RE_NON_ASCII.to_s + '|' + RE_ESCAPE.to_s + ')*[\s]*)\)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n')
22
- URI_RX = /url\(("([^"]*)"|'([^']*)'|([^)]*))\)/im
23
- RE_GRADIENT = /[-a-z]*gradient\([-a-z0-9 .,#%()]*\)/im
23
+ RE_URI = /(url\(\s*(\s*#{RE_STRING}\s*)\s*\))|(url\(\s*([!#$%&*\-~]|#{RE_NON_ASCII}|#{RE_ESCAPE})*\s*)\)/ixm.freeze
24
+ URI_RX = /url\(("([^"]*)"|'([^']*)'|([^)]*))\)/im.freeze
25
+ RE_GRADIENT = /[-a-z]*gradient\([-a-z0-9 .,#%()]*\)/im.freeze
24
26
 
25
27
  # Initial parsing
26
- RE_AT_IMPORT_RULE = /\@import\s+(url\()?["']?(.[^'"\s]*)["']?\)?([\w\s\,^\])]*)\)?;?/
28
+ RE_AT_IMPORT_RULE = /@import\s+(url\()?["']?(.[^'"\s]*)["']?\)?([\w\s,^\])]*)\)?;?/.freeze
27
29
 
28
30
  #--
29
- #RE_AT_MEDIA_RULE = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')
31
+ # RE_AT_MEDIA_RULE = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')
30
32
 
31
- #RE_AT_IMPORT_RULE = Regexp.new('@import[\s]*(' + RE_STRING.to_s + ')([\w\s\,]*)[;]?', Regexp::IGNORECASE) -- should handle url() even though it is not allowed
33
+ # RE_AT_IMPORT_RULE = Regexp.new('@import[\s]*(' + RE_STRING.to_s + ')([\w\s\,]*)[;]?', Regexp::IGNORECASE) -- should handle url() even though it is not allowed
32
34
  #++
33
- IMPORTANT_IN_PROPERTY_RX = /[\s]*!important\b[\s]*/i
35
+ IMPORTANT_IN_PROPERTY_RX = /\s*!important\b\s*/i.freeze
34
36
 
35
37
  RE_INSIDE_OUTSIDE = regex_possible_values 'inside', 'outside'
36
38
  RE_SCROLL_FIXED = regex_possible_values 'scroll', 'fixed'
37
39
  RE_REPEAT = regex_possible_values 'repeat(\-x|\-y)*|no\-repeat'
38
- RE_LIST_STYLE_TYPE = regex_possible_values 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', 'lower-roman',
39
- 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha',
40
- 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana',
41
- 'hira-gana-iroha', 'katakana-iroha', 'katakana', 'none'
40
+ RE_LIST_STYLE_TYPE = regex_possible_values(
41
+ 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', 'lower-roman',
42
+ 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha',
43
+ 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana',
44
+ 'hira-gana-iroha', 'katakana-iroha', 'katakana', 'none'
45
+ )
42
46
 
43
- STRIP_CSS_COMMENTS_RX = /\/\*.*?\*\//m
44
- STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m
47
+ STRIP_CSS_COMMENTS_RX = %r{/\*.*?\*/}m.freeze
48
+ STRIP_HTML_COMMENTS_RX = /<!--|-->/m.freeze
45
49
 
46
50
  # Special units
47
- BOX_MODEL_UNITS_RX = /(auto|inherit|0|([\-]*([0-9]+|[0-9]*\.[0-9]+)(rem|vw|vh|vm|vmin|vmax|e[mx]+|px|[cm]+m|p[tc+]|in|\%)))([\s;]|\Z)/imx
51
+ BOX_MODEL_UNITS_RX = /(auto|inherit|0|(-*([0-9]+|[0-9]*\.[0-9]+)(rem|vw|vh|vm|vmin|vmax|e[mx]+|px|[cm]+m|p[tc+]|in|%)))([\s;]|\Z)/imx.freeze
48
52
  RE_LENGTH_OR_PERCENTAGE = Regexp.new('([\-]*(([0-9]*\.[0-9]+)|[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%))', Regexp::IGNORECASE)
49
53
  RE_BACKGROUND_POSITION = Regexp.new("((((#{RE_LENGTH_OR_PERCENTAGE})|left|center|right|top|bottom)[\s]*){1,2})", Regexp::IGNORECASE | Regexp::EXTENDED)
50
54
  RE_BACKGROUND_SIZE = Regexp.new("\\s*/\\s*((((#{RE_LENGTH_OR_PERCENTAGE})|auto|cover|contain|initial|inherit)[\\s]*){1,2})", Regexp::IGNORECASE | Regexp::EXTENDED)
51
- FONT_UNITS_RX = /(([x]+\-)*small|medium|large[r]*|auto|inherit|([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%)*)/i
52
- RE_BORDER_STYLE = /([\s]*^)?(none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset)([\s]*$)?/imx
55
+ FONT_UNITS_RX = /((x+-)*small|medium|larger*|auto|inherit|([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|%)*)/i.freeze
56
+ RE_BORDER_STYLE = /(\s*^)?(none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset)(\s*$)?/imx.freeze
53
57
  RE_BORDER_UNITS = Regexp.union(BOX_MODEL_UNITS_RX, /(thin|medium|thick)/i)
54
58
 
55
-
56
59
  # Patterns for specificity calculations
57
- NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX_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