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