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