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