marcosinger-css_parser 1.3.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.
- data/lib/css_parser.rb +162 -0
- data/lib/css_parser/parser.rb +454 -0
- data/lib/css_parser/regexps.rb +75 -0
- data/lib/css_parser/rule_set.rb +451 -0
- data/test/fixtures/import-circular-reference.css +4 -0
- data/test/fixtures/import-with-media-types.css +3 -0
- data/test/fixtures/import1.css +3 -0
- data/test/fixtures/simple.css +6 -0
- data/test/fixtures/subdir/import2.css +3 -0
- data/test/test_css_parser_basic.rb +72 -0
- data/test/test_css_parser_loading.rb +139 -0
- data/test/test_css_parser_media_types.rb +106 -0
- data/test/test_css_parser_misc.rb +164 -0
- data/test/test_css_parser_regexps.rb +69 -0
- data/test/test_helper.rb +6 -0
- data/test/test_merging.rb +103 -0
- data/test/test_rule_set.rb +90 -0
- data/test/test_rule_set_creating_shorthand.rb +119 -0
- data/test/test_rule_set_expanding_shorthand.rb +198 -0
- metadata +101 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
module CssParser
|
2
|
+
# :stopdoc:
|
3
|
+
# Base types
|
4
|
+
RE_NL = Regexp.new('(\n|\r\n|\r|\f)')
|
5
|
+
RE_NON_ASCII = Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE, 'n') #[^\0-\177]
|
6
|
+
RE_UNICODE = Regexp.new('(\\\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])*)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n')
|
7
|
+
RE_ESCAPE = Regexp.union(RE_UNICODE, '|(\\\\[^\n\r\f0-9a-f])')
|
8
|
+
RE_IDENT = Regexp.new("[\-]?([_a-z]|#{RE_NON_ASCII}|#{RE_ESCAPE})([_a-z0-9\-]|#{RE_NON_ASCII}|#{RE_ESCAPE})*", Regexp::IGNORECASE, 'n')
|
9
|
+
|
10
|
+
# General strings
|
11
|
+
RE_STRING1 = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')
|
12
|
+
RE_STRING2 = Regexp.new('(\'(.[^\n\r\f\\\']*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\')')
|
13
|
+
RE_STRING = Regexp.union(RE_STRING1, RE_STRING2)
|
14
|
+
|
15
|
+
RE_URI = Regexp.new('(url\([\s]*([\s]*' + RE_STRING.to_s + '[\s]*)[\s]*\))|(url\([\s]*([!#$%&*\-~]|' + RE_NON_ASCII.to_s + '|' + RE_ESCAPE.to_s + ')*[\s]*)\)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n')
|
16
|
+
URI_RX = /url\(("([^"]*)"|'([^']*)'|([^)]*))\)/im
|
17
|
+
|
18
|
+
# Initial parsing
|
19
|
+
RE_AT_IMPORT_RULE = /\@import[\s]+(url\()?["''"]?(.[^'"\s"']*)["''"]?\)?([\w\s\,^\])]*)\)?;?/
|
20
|
+
|
21
|
+
#--
|
22
|
+
#RE_AT_MEDIA_RULE = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')
|
23
|
+
|
24
|
+
#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
|
25
|
+
#++
|
26
|
+
IMPORTANT_IN_PROPERTY_RX = /[\s]*\!important[\s]*/i
|
27
|
+
STRIP_CSS_COMMENTS_RX = /\/\*.*?\*\//m
|
28
|
+
STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m
|
29
|
+
|
30
|
+
# Special units
|
31
|
+
BOX_MODEL_UNITS_RX = /(auto|inherit|0|([\-]*([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%)))([\s;]|\Z)/imx
|
32
|
+
RE_LENGTH_OR_PERCENTAGE = Regexp.new('([\-]*(([0-9]*\.[0-9]+)|[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%))', Regexp::IGNORECASE)
|
33
|
+
RE_BACKGROUND_POSITION = Regexp.new("((((#{RE_LENGTH_OR_PERCENTAGE})|left|center|right|top|bottom)[\s]*){1,2})", Regexp::IGNORECASE | Regexp::EXTENDED)
|
34
|
+
FONT_UNITS_RX = /(([x]+\-)*small|medium|large[r]*|auto|inherit|([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%)*)/i
|
35
|
+
RE_BORDER_STYLE = /([\s]*^)?(none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset)([\s]*$)?/imx
|
36
|
+
RE_BORDER_UNITS = Regexp.union(BOX_MODEL_UNITS_RX, /(thin|medium|thick)/i)
|
37
|
+
|
38
|
+
|
39
|
+
# Patterns for specificity calculations
|
40
|
+
NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX= /
|
41
|
+
(\.[\w]+) # classes
|
42
|
+
|
|
43
|
+
\[(\w+) # attributes
|
44
|
+
|
|
45
|
+
(\:( # pseudo classes
|
46
|
+
link|visited|active
|
47
|
+
|hover|focus
|
48
|
+
|lang
|
49
|
+
|target
|
50
|
+
|enabled|disabled|checked|indeterminate
|
51
|
+
|root
|
52
|
+
|nth-child|nth-last-child|nth-of-type|nth-last-of-type
|
53
|
+
|first-child|last-child|first-of-type|last-of-type
|
54
|
+
|only-child|only-of-type
|
55
|
+
|empty|contains
|
56
|
+
))
|
57
|
+
/ix
|
58
|
+
ELEMENTS_AND_PSEUDO_ELEMENTS_RX = /
|
59
|
+
((^|[\s\+\>\~]+)[\w]+ # elements
|
60
|
+
|
|
61
|
+
\:{1,2}( # pseudo-elements
|
62
|
+
after|before
|
63
|
+
|first-letter|first-line
|
64
|
+
|selection
|
65
|
+
)
|
66
|
+
)/ix
|
67
|
+
|
68
|
+
# Colours
|
69
|
+
RE_COLOUR_NUMERIC = Regexp.new('((hsl|rgb)[\s]*\([\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*\))', Regexp::IGNORECASE)
|
70
|
+
RE_COLOUR_NUMERIC_ALPHA = Regexp.new('((hsla|rgba)[\s]*\([\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*\))', Regexp::IGNORECASE)
|
71
|
+
RE_COLOUR_HEX = /(#([0-9a-f]{6}|[0-9a-f]{3})([\s;]|$))/i
|
72
|
+
RE_COLOUR_NAMED = /([\s]*^)?(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|transparent)([\s]*$)?/i
|
73
|
+
RE_COLOUR = Regexp.union(RE_COLOUR_NUMERIC, RE_COLOUR_NUMERIC_ALPHA, RE_COLOUR_HEX, RE_COLOUR_NAMED)
|
74
|
+
# :startdoc:
|
75
|
+
end
|
@@ -0,0 +1,451 @@
|
|
1
|
+
module CssParser
|
2
|
+
class RuleSet
|
3
|
+
# Patterns for specificity calculations
|
4
|
+
RE_ELEMENTS_AND_PSEUDO_ELEMENTS = /((^|[\s\+\>]+)[\w]+|\:(first\-line|first\-letter|before|after))/i
|
5
|
+
RE_NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES = /(\.[\w]+)|(\[[\w]+)|(\:(link|first\-child|lang))/i
|
6
|
+
|
7
|
+
# Array of selector strings.
|
8
|
+
attr_reader :selectors
|
9
|
+
|
10
|
+
# Integer with the specificity to use for this RuleSet.
|
11
|
+
attr_accessor :specificity
|
12
|
+
|
13
|
+
def initialize(selectors, block, specificity = nil)
|
14
|
+
@selectors = []
|
15
|
+
@specificity = specificity
|
16
|
+
@declarations = {}
|
17
|
+
@order = 0
|
18
|
+
parse_selectors!(selectors) if selectors
|
19
|
+
parse_declarations!(block)
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# Get the value of a property
|
24
|
+
def get_value(property)
|
25
|
+
return '' unless property and not property.empty?
|
26
|
+
|
27
|
+
property = property.downcase.strip
|
28
|
+
properties = @declarations.inject('') do |val, (key, data)|
|
29
|
+
#puts "COMPARING #{key} #{key.inspect} against #{property} #{property.inspect}"
|
30
|
+
importance = data[:is_important] ? ' !important' : ''
|
31
|
+
val << "#{data[:value]}#{importance}; " if key.downcase.strip == property
|
32
|
+
val
|
33
|
+
end
|
34
|
+
return properties ? properties.strip : ''
|
35
|
+
end
|
36
|
+
alias_method :[], :get_value
|
37
|
+
|
38
|
+
# Add a CSS declaration to the current RuleSet.
|
39
|
+
#
|
40
|
+
# rule_set.add_declaration!('color', 'blue')
|
41
|
+
#
|
42
|
+
# puts rule_set['color']
|
43
|
+
# => 'blue;'
|
44
|
+
#
|
45
|
+
# rule_set.add_declaration!('margin', '0px auto !important')
|
46
|
+
#
|
47
|
+
# puts rule_set['margin']
|
48
|
+
# => '0px auto !important;'
|
49
|
+
#
|
50
|
+
# If the property already exists its value will be over-written.
|
51
|
+
def add_declaration!(property, value)
|
52
|
+
if value.nil? or value.empty?
|
53
|
+
@declarations.delete(property)
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
57
|
+
value.gsub!(/;\Z/, '')
|
58
|
+
is_important = !value.gsub!(CssParser::IMPORTANT_IN_PROPERTY_RX, '').nil?
|
59
|
+
property = property.downcase.strip
|
60
|
+
#puts "SAVING #{property} #{value} #{is_important.inspect}"
|
61
|
+
@declarations[property] = {
|
62
|
+
:value => value, :is_important => is_important, :order => @order += 1
|
63
|
+
}
|
64
|
+
end
|
65
|
+
alias_method :[]=, :add_declaration!
|
66
|
+
|
67
|
+
# Remove CSS declaration from the current RuleSet.
|
68
|
+
#
|
69
|
+
# rule_set.remove_declaration!('color')
|
70
|
+
def remove_declaration!(property)
|
71
|
+
@declarations.delete(property)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Iterate through selectors.
|
75
|
+
#
|
76
|
+
# Options
|
77
|
+
# - +force_important+ -- boolean
|
78
|
+
#
|
79
|
+
# ==== Example
|
80
|
+
# ruleset.each_selector do |sel, dec, spec|
|
81
|
+
# ...
|
82
|
+
# end
|
83
|
+
def each_selector(options = {}) # :yields: selector, declarations, specificity
|
84
|
+
declarations = declarations_to_s(options)
|
85
|
+
if @specificity
|
86
|
+
@selectors.each { |sel| yield sel.strip, declarations, @specificity }
|
87
|
+
else
|
88
|
+
@selectors.each { |sel| yield sel.strip, declarations, CssParser.calculate_specificity(sel) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Iterate through declarations.
|
93
|
+
def each_declaration # :yields: property, value, is_important
|
94
|
+
decs = @declarations.sort { |a,b| a[1][:order].nil? || b[1][:order].nil? ? 0 : a[1][:order] <=> b[1][:order] }
|
95
|
+
decs.each do |property, data|
|
96
|
+
value = data[:value]
|
97
|
+
yield property.downcase.strip, value.strip, data[:is_important]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Return all declarations as a string.
|
102
|
+
#--
|
103
|
+
# TODO: Clean-up regexp doesn't seem to work
|
104
|
+
#++
|
105
|
+
def declarations_to_s(options = {})
|
106
|
+
options = {:force_important => false}.merge(options)
|
107
|
+
str = ''
|
108
|
+
each_declaration do |prop, val, is_important|
|
109
|
+
importance = (options[:force_important] || is_important) ? ' !important' : ''
|
110
|
+
str += "#{prop}: #{val}#{importance}; "
|
111
|
+
end
|
112
|
+
str.gsub(/^[\s^(\{)]+|[\n\r\f\t]*|[\s]+$/mx, '').strip
|
113
|
+
end
|
114
|
+
|
115
|
+
# Return the CSS rule set as a string.
|
116
|
+
def to_s
|
117
|
+
decs = declarations_to_s
|
118
|
+
"#{@selectors.join} { #{decs} }"
|
119
|
+
end
|
120
|
+
|
121
|
+
# Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
|
122
|
+
def expand_shorthand!
|
123
|
+
# border must be expanded before dimensions
|
124
|
+
expand_border_shorthand!
|
125
|
+
expand_dimensions_shorthand!
|
126
|
+
expand_font_shorthand!
|
127
|
+
expand_background_shorthand!
|
128
|
+
end
|
129
|
+
|
130
|
+
# Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
|
131
|
+
def create_shorthand!
|
132
|
+
create_background_shorthand!
|
133
|
+
create_dimensions_shorthand!
|
134
|
+
# border must be shortened after dimensions
|
135
|
+
create_border_shorthand!
|
136
|
+
create_font_shorthand!
|
137
|
+
end
|
138
|
+
|
139
|
+
# Split shorthand border declarations (e.g. <tt>border: 1px red;</tt>)
|
140
|
+
# Additional splitting happens in expand_dimensions_shorthand!
|
141
|
+
def expand_border_shorthand! # :nodoc:
|
142
|
+
['border', 'border-left', 'border-right', 'border-top', 'border-bottom'].each do |k|
|
143
|
+
next unless @declarations.has_key?(k)
|
144
|
+
|
145
|
+
value = @declarations[k][:value]
|
146
|
+
|
147
|
+
split_declaration(k, "#{k}-width", value.slice!(CssParser::RE_BORDER_UNITS))
|
148
|
+
split_declaration(k, "#{k}-color", value.slice!(CssParser::RE_COLOUR))
|
149
|
+
split_declaration(k, "#{k}-style", value.slice!(CssParser::RE_BORDER_STYLE))
|
150
|
+
|
151
|
+
@declarations.delete(k)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)
|
156
|
+
# into their constituent parts. Handles margin, padding, border-color, border-style and border-width.
|
157
|
+
def expand_dimensions_shorthand! # :nodoc:
|
158
|
+
{'margin' => 'margin-%s',
|
159
|
+
'padding' => 'padding-%s',
|
160
|
+
'border-color' => 'border-%s-color',
|
161
|
+
'border-style' => 'border-%s-style',
|
162
|
+
'border-width' => 'border-%s-width'}.each do |property, expanded|
|
163
|
+
|
164
|
+
next unless @declarations.has_key?(property)
|
165
|
+
|
166
|
+
value = @declarations[property][:value]
|
167
|
+
|
168
|
+
# RGB and HSL values in borders are the only units that can have spaces (within params).
|
169
|
+
# We cheat a bit here by stripping spaces after commas in RGB and HSL values so that we
|
170
|
+
# can split easily on spaces.
|
171
|
+
#
|
172
|
+
# TODO: rgba, hsl, hsla
|
173
|
+
value.gsub!(RE_COLOUR) { |c| c.gsub(/[\s]+/, '') }
|
174
|
+
|
175
|
+
matches = value.strip.split(/[\s]+/)
|
176
|
+
|
177
|
+
t, r, b, l = nil
|
178
|
+
|
179
|
+
case matches.length
|
180
|
+
when 1
|
181
|
+
t, r, b, l = matches[0], matches[0], matches[0], matches[0]
|
182
|
+
when 2
|
183
|
+
t, b = matches[0], matches[0]
|
184
|
+
r, l = matches[1], matches[1]
|
185
|
+
when 3
|
186
|
+
t = matches[0]
|
187
|
+
r, l = matches[1], matches[1]
|
188
|
+
b = matches[2]
|
189
|
+
when 4
|
190
|
+
t = matches[0]
|
191
|
+
r = matches[1]
|
192
|
+
b = matches[2]
|
193
|
+
l = matches[3]
|
194
|
+
end
|
195
|
+
|
196
|
+
values = @declarations[property]
|
197
|
+
|
198
|
+
split_declaration(property, expanded % 'top', t)
|
199
|
+
split_declaration(property, expanded % 'right', r)
|
200
|
+
split_declaration(property, expanded % 'bottom', b)
|
201
|
+
split_declaration(property, expanded % 'left', l)
|
202
|
+
|
203
|
+
@declarations.delete(property)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Convert shorthand font declarations (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)
|
208
|
+
# into their constituent parts.
|
209
|
+
def expand_font_shorthand! # :nodoc:
|
210
|
+
return unless @declarations.has_key?('font')
|
211
|
+
|
212
|
+
font_props = {}
|
213
|
+
|
214
|
+
# reset properties to 'normal' per http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
|
215
|
+
['font-style', 'font-variant', 'font-weight', 'font-size',
|
216
|
+
'line-height'].each do |prop|
|
217
|
+
font_props[prop] = 'normal'
|
218
|
+
end
|
219
|
+
|
220
|
+
value = @declarations['font'][:value]
|
221
|
+
is_important = @declarations['font'][:is_important]
|
222
|
+
order = @declarations['font'][:order]
|
223
|
+
|
224
|
+
in_fonts = false
|
225
|
+
|
226
|
+
matches = value.scan(/("(.*[^"])"|'(.*[^'])'|(\w[^ ,]+))/)
|
227
|
+
matches.each do |match|
|
228
|
+
m = match[0].to_s.strip
|
229
|
+
m.gsub!(/[;]$/, '')
|
230
|
+
|
231
|
+
if in_fonts
|
232
|
+
if font_props.has_key?('font-family')
|
233
|
+
font_props['font-family'] += ', ' + m
|
234
|
+
else
|
235
|
+
font_props['font-family'] = m
|
236
|
+
end
|
237
|
+
elsif m =~ /normal|inherit/i
|
238
|
+
['font-style', 'font-weight', 'font-variant'].each do |font_prop|
|
239
|
+
font_props[font_prop] = m unless font_props.has_key?(font_prop)
|
240
|
+
end
|
241
|
+
elsif m =~ /italic|oblique/i
|
242
|
+
font_props['font-style'] = m
|
243
|
+
elsif m =~ /small\-caps/i
|
244
|
+
font_props['font-variant'] = m
|
245
|
+
elsif m =~ /[1-9]00$|bold|bolder|lighter/i
|
246
|
+
font_props['font-weight'] = m
|
247
|
+
elsif m =~ CssParser::FONT_UNITS_RX
|
248
|
+
if m =~ /\//
|
249
|
+
font_props['font-size'], font_props['line-height'] = m.split('/')
|
250
|
+
else
|
251
|
+
font_props['font-size'] = m
|
252
|
+
end
|
253
|
+
in_fonts = true
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
font_props.each { |font_prop, font_val| @declarations[font_prop] = {:value => font_val, :is_important => is_important, :order => order} }
|
258
|
+
|
259
|
+
@declarations.delete('font')
|
260
|
+
end
|
261
|
+
|
262
|
+
# Convert shorthand background declarations (e.g. <tt>background: url("chess.png") gray 50% repeat fixed;</tt>)
|
263
|
+
# into their constituent parts.
|
264
|
+
#
|
265
|
+
# See http://www.w3.org/TR/CSS21/colors.html#propdef-background
|
266
|
+
def expand_background_shorthand! # :nodoc:
|
267
|
+
return unless @declarations.has_key?('background')
|
268
|
+
|
269
|
+
value = @declarations['background'][:value]
|
270
|
+
|
271
|
+
if value =~ /([\s]*^)?inherit([\s]*$)?/i
|
272
|
+
['background-color', 'background-image', 'background-attachment', 'background-repeat', 'background-position'].each do |prop|
|
273
|
+
split_declaration('background', prop, 'inherit')
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
split_declaration('background', 'background-image', value.slice!(Regexp.union(CssParser::URI_RX, /none/i)))
|
278
|
+
split_declaration('background', 'background-attachment', value.slice!(/([\s]*^)?(scroll|fixed)([\s]*$)?/i))
|
279
|
+
split_declaration('background', 'background-repeat', value.slice!(/([\s]*^)?(repeat(\-x|\-y)*|no\-repeat)([\s]*$)?/i))
|
280
|
+
split_declaration('background', 'background-color', value.slice!(CssParser::RE_COLOUR))
|
281
|
+
split_declaration('background', 'background-position', value.slice(CssParser::RE_BACKGROUND_POSITION))
|
282
|
+
|
283
|
+
@declarations.delete('background')
|
284
|
+
end
|
285
|
+
|
286
|
+
# Looks for long format CSS background properties (e.g. <tt>background-color</tt>) and
|
287
|
+
# converts them into a shorthand CSS <tt>background</tt> property.
|
288
|
+
#
|
289
|
+
# Leaves properties declared !important alone.
|
290
|
+
def create_background_shorthand! # :nodoc:
|
291
|
+
new_value = ''
|
292
|
+
['background-color', 'background-image', 'background-repeat',
|
293
|
+
'background-position', 'background-attachment'].each do |property|
|
294
|
+
if @declarations.has_key?(property) and not @declarations[property][:is_important]
|
295
|
+
new_value += @declarations[property][:value] + ' '
|
296
|
+
@declarations.delete(property)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
unless new_value.strip.empty?
|
301
|
+
@declarations['background'] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# Combine border-color, border-style and border-width into border
|
306
|
+
# Should be run after create_dimensions_shorthand!
|
307
|
+
#
|
308
|
+
# TODO: this is extremely similar to create_background_shorthand! and should be combined
|
309
|
+
def create_border_shorthand! # :nodoc:
|
310
|
+
new_value = ''
|
311
|
+
|
312
|
+
['border-width', 'border-style', 'border-color'].each do |property|
|
313
|
+
if @declarations.has_key?(property) and not @declarations[property][:is_important]
|
314
|
+
|
315
|
+
# can't merge if any value contains a space (i.e. has multiple values)
|
316
|
+
return if @declarations[property][:value].strip =~ /[\s]/
|
317
|
+
new_value += @declarations[property][:value] + ' '
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
@declarations.delete('border-width')
|
322
|
+
@declarations.delete('border-style')
|
323
|
+
@declarations.delete('border-color')
|
324
|
+
|
325
|
+
unless new_value.strip.empty?
|
326
|
+
@declarations['border'] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# Looks for long format CSS dimensional properties (margin, padding, border-color, border-style and border-width)
|
331
|
+
# and converts them into shorthand CSS properties.
|
332
|
+
def create_dimensions_shorthand! # :nodoc:
|
333
|
+
directions = ['top', 'right', 'bottom', 'left']
|
334
|
+
|
335
|
+
{'margin' => 'margin-%s',
|
336
|
+
'padding' => 'padding-%s',
|
337
|
+
'border-color' => 'border-%s-color',
|
338
|
+
'border-style' => 'border-%s-style',
|
339
|
+
'border-width' => 'border-%s-width'}.each do |property, expanded|
|
340
|
+
|
341
|
+
foldable = @declarations.select do |dim, val|
|
342
|
+
dim == expanded % 'top' or dim == expanded % 'right' or dim == expanded % 'bottom' or dim == expanded % 'left'
|
343
|
+
end
|
344
|
+
# All four dimensions must be present
|
345
|
+
if foldable.length == 4
|
346
|
+
values = {}
|
347
|
+
|
348
|
+
directions.each { |d| values[d.to_sym] = @declarations[expanded % d][:value].downcase.strip }
|
349
|
+
|
350
|
+
if values[:left] == values[:right]
|
351
|
+
if values[:top] == values[:bottom]
|
352
|
+
if values[:top] == values[:left] # All four sides are equal
|
353
|
+
new_value = values[:top]
|
354
|
+
else # Top and bottom are equal, left and right are equal
|
355
|
+
new_value = values[:top] + ' ' + values[:left]
|
356
|
+
end
|
357
|
+
else # Only left and right are equal
|
358
|
+
new_value = values[:top] + ' ' + values[:left] + ' ' + values[:bottom]
|
359
|
+
end
|
360
|
+
else # No sides are equal
|
361
|
+
new_value = values[:top] + ' ' + values[:right] + ' ' + values[:bottom] + ' ' + values[:left]
|
362
|
+
end
|
363
|
+
|
364
|
+
new_value.strip!
|
365
|
+
@declarations[property] = {:value => new_value.strip} unless new_value.empty?
|
366
|
+
|
367
|
+
# Delete the longhand values
|
368
|
+
directions.each { |d| @declarations.delete(expanded % d) }
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
|
374
|
+
# Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and
|
375
|
+
# tries to convert them into a shorthand CSS <tt>font</tt> property. All
|
376
|
+
# font properties must be present in order to create a shorthand declaration.
|
377
|
+
def create_font_shorthand! # :nodoc:
|
378
|
+
['font-style', 'font-variant', 'font-weight', 'font-size',
|
379
|
+
'line-height', 'font-family'].each do |prop|
|
380
|
+
return unless @declarations.has_key?(prop)
|
381
|
+
end
|
382
|
+
|
383
|
+
new_value = ''
|
384
|
+
['font-style', 'font-variant', 'font-weight'].each do |property|
|
385
|
+
unless @declarations[property][:value] == 'normal'
|
386
|
+
new_value += @declarations[property][:value] + ' '
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
new_value += @declarations['font-size'][:value]
|
391
|
+
|
392
|
+
unless @declarations['line-height'][:value] == 'normal'
|
393
|
+
new_value += '/' + @declarations['line-height'][:value]
|
394
|
+
end
|
395
|
+
|
396
|
+
new_value += ' ' + @declarations['font-family'][:value]
|
397
|
+
|
398
|
+
@declarations['font'] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
|
399
|
+
|
400
|
+
['font-style', 'font-variant', 'font-weight', 'font-size',
|
401
|
+
'line-height', 'font-family'].each do |prop|
|
402
|
+
@declarations.delete(prop)
|
403
|
+
end
|
404
|
+
|
405
|
+
end
|
406
|
+
|
407
|
+
private
|
408
|
+
|
409
|
+
# utility method for re-assign shorthand elements to longhand versions
|
410
|
+
def split_declaration(src, dest, v) # :nodoc:
|
411
|
+
return unless v and not v.empty?
|
412
|
+
|
413
|
+
atts = @declarations[src]
|
414
|
+
|
415
|
+
if @declarations.has_key?(dest)
|
416
|
+
#puts "dest #{dest} already exists"
|
417
|
+
|
418
|
+
if @declarations[dest][:order] > @declarations[src][:order]
|
419
|
+
#puts "skipping #{dest}:#{v} due to order "
|
420
|
+
return
|
421
|
+
else
|
422
|
+
@declarations[dest] = {}
|
423
|
+
end
|
424
|
+
end
|
425
|
+
@declarations[dest] = @declarations[src].merge({:value => v.to_s.strip})
|
426
|
+
end
|
427
|
+
|
428
|
+
def parse_declarations!(block) # :nodoc:
|
429
|
+
@declarations = {}
|
430
|
+
|
431
|
+
return unless block
|
432
|
+
|
433
|
+
block.gsub!(/(^[\s]*)|([\s]*$)/, '')
|
434
|
+
|
435
|
+
block.split(/[\;$]+/m).each do |decs|
|
436
|
+
if matches = decs.match(/(.[^:]*)\:(.[^;]*)(;|\Z)/i)
|
437
|
+
property, value, end_of_declaration = matches.captures
|
438
|
+
|
439
|
+
add_declaration!(property, value)
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
#--
|
445
|
+
# TODO: way too simplistic
|
446
|
+
#++
|
447
|
+
def parse_selectors!(selectors) # :nodoc:
|
448
|
+
@selectors = selectors.split(',')
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|