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