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