css_parser 1.7.1 → 1.9.0

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