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