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