css_parser_1.1.0 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,46 @@
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\()?["']+(.[^'"]*)["']\)?([\w\s\,]*);?/i
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)", 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
+
36
+ # Patterns for specificity calculations
37
+ ELEMENTS_AND_PSEUDO_ELEMENTS_RX = /((^|[\s\+\>]+)[\w]+|\:(first\-line|first\-letter|before|after))/i
38
+ NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = /(\.[\w]+)|(\[[\w]+)|(\:(link|first\-child|lang))/i
39
+
40
+ # Colours
41
+ RE_COLOUR_RGB = Regexp.new('(rgb[\s]*\([\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*\))', Regexp::IGNORECASE)
42
+ RE_COLOUR_HEX = /(#([0-9a-f]{6}|[0-9a-f]{3})([\s;]|$))/i
43
+ RE_COLOUR_NAMED = /([\s]*^)?(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|transparent)([\s]*$)?/i
44
+ RE_COLOUR = Regexp.union(RE_COLOUR_RGB, RE_COLOUR_HEX, RE_COLOUR_NAMED)
45
+ # :startdoc:
46
+ end
@@ -0,0 +1,390 @@
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
+ # Iterate through selectors.
68
+ #
69
+ # Options
70
+ # - +force_important+ -- boolean
71
+ #
72
+ # ==== Example
73
+ # ruleset.each_selector do |sel, dec, spec|
74
+ # ...
75
+ # end
76
+ def each_selector(options = {}) # :yields: selector, declarations, specificity
77
+ declarations = declarations_to_s(options)
78
+ if @specificity
79
+ @selectors.each { |sel| yield sel.strip, declarations, @specificity }
80
+ else
81
+ @selectors.each { |sel| yield sel.strip, declarations, CssParser.calculate_specificity(sel) }
82
+ end
83
+ end
84
+
85
+ # Iterate through declarations.
86
+ def each_declaration # :yields: property, value, is_important
87
+ decs = @declarations.sort { |a,b| a[1][:order].nil? || b[1][:order].nil? ? 0 : a[1][:order] <=> b[1][:order] }
88
+ decs.each do |property, data|
89
+ value = data[:value]
90
+ yield property.downcase.strip, value.strip, data[:is_important]
91
+ end
92
+ end
93
+
94
+ # Return all declarations as a string.
95
+ #--
96
+ # TODO: Clean-up regexp doesn't seem to work
97
+ #++
98
+ def declarations_to_s(options = {})
99
+ options = {:force_important => false}.merge(options)
100
+ str = ''
101
+ importance = options[:force_important] ? ' !important' : ''
102
+ each_declaration { |prop, val| str += "#{prop}: #{val}#{importance}; " }
103
+ str.gsub(/^[\s]+|[\n\r\f\t]*|[\s]+$/mx, '').strip
104
+ end
105
+
106
+ # Return the CSS rule set as a string.
107
+ def to_s
108
+ decs = declarations_to_s
109
+ "#{@selectors} { #{decs} }"
110
+ end
111
+
112
+ # Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
113
+ def expand_shorthand!
114
+ expand_dimensions_shorthand!
115
+ expand_font_shorthand!
116
+ expand_background_shorthand!
117
+ end
118
+
119
+ # Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
120
+ def create_shorthand!
121
+ create_background_shorthand!
122
+ create_dimensions_shorthand!
123
+ create_font_shorthand!
124
+ end
125
+
126
+ private
127
+ def parse_declarations!(block) # :nodoc:
128
+ @declarations = {}
129
+
130
+ return unless block
131
+
132
+ block.gsub!(/(^[\s]*)|([\s]*$)/, '')
133
+
134
+ block.split(/[\;$]+/m).each do |decs|
135
+ if matches = decs.match(/(.[^:]*)\:(.[^;]*)(;|\Z)/i)
136
+ property, value, end_of_declaration = matches.captures
137
+
138
+ add_declaration!(property, value)
139
+ end
140
+ end
141
+ end
142
+
143
+ #--
144
+ # TODO: way too simplistic
145
+ #++
146
+ def parse_selectors!(selectors) # :nodoc:
147
+ @selectors = selectors.split(',')
148
+ end
149
+
150
+ public
151
+ # Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)
152
+ # into their constituent parts.
153
+ def expand_dimensions_shorthand! # :nodoc:
154
+ ['margin', 'padding'].each do |property|
155
+
156
+ next unless @declarations.has_key?(property)
157
+
158
+ value = @declarations[property][:value]
159
+ is_important = @declarations[property][:is_important]
160
+ order = @declarations[property][:order]
161
+ t, r, b, l = nil
162
+
163
+ matches = value.scan(CssParser::BOX_MODEL_UNITS_RX)
164
+
165
+ case matches.length
166
+ when 1
167
+ t, r, b, l = matches[0][0], matches[0][0], matches[0][0], matches[0][0]
168
+ when 2
169
+ t, b = matches[0][0], matches[0][0]
170
+ r, l = matches[1][0], matches[1][0]
171
+ when 3
172
+ t = matches[0][0]
173
+ r, l = matches[1][0], matches[1][0]
174
+ b = matches[2][0]
175
+ when 4
176
+ t = matches[0][0]
177
+ r = matches[1][0]
178
+ b = matches[2][0]
179
+ l = matches[3][0]
180
+ end
181
+
182
+ values = { :is_important => is_important, :order => order }
183
+ @declarations["#{property}-top"] = values.merge(:value => t.to_s)
184
+ @declarations["#{property}-right"] = values.merge(:value => r.to_s)
185
+ @declarations["#{property}-bottom"] = values.merge(:value => b.to_s)
186
+ @declarations["#{property}-left"] = values.merge(:value => l.to_s)
187
+ @declarations.delete(property)
188
+ end
189
+ end
190
+
191
+ # Convert shorthand font declarations (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)
192
+ # into their constituent parts.
193
+ def expand_font_shorthand! # :nodoc:
194
+ return unless @declarations.has_key?('font')
195
+
196
+ font_props = {}
197
+
198
+ # reset properties to 'normal' per http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
199
+ ['font-style', 'font-variant', 'font-weight', 'font-size',
200
+ 'line-height'].each do |prop|
201
+ font_props[prop] = 'normal'
202
+ end
203
+
204
+ value = @declarations['font'][:value]
205
+ is_important = @declarations['font'][:is_important]
206
+ order = @declarations['font'][:order]
207
+
208
+ in_fonts = false
209
+
210
+ matches = value.scan(/("(.*[^"])"|'(.*[^'])'|(\w[^ ,]+))/)
211
+ matches.each do |match|
212
+ m = match[0].to_s.strip
213
+ m.gsub!(/[;]$/, '')
214
+
215
+ if in_fonts
216
+ if font_props.has_key?('font-family')
217
+ font_props['font-family'] += ', ' + m
218
+ else
219
+ font_props['font-family'] = m
220
+ end
221
+ elsif m =~ /normal|inherit/i
222
+ ['font-style', 'font-weight', 'font-variant'].each do |font_prop|
223
+ font_props[font_prop] = m unless font_props.has_key?(font_prop)
224
+ end
225
+ elsif m =~ /italic|oblique/i
226
+ font_props['font-style'] = m
227
+ elsif m =~ /small\-caps/i
228
+ font_props['font-variant'] = m
229
+ elsif m =~ /[1-9]00$|bold|bolder|lighter/i
230
+ font_props['font-weight'] = m
231
+ elsif m =~ CssParser::FONT_UNITS_RX
232
+ if m =~ /\//
233
+ font_props['font-size'], font_props['line-height'] = m.split('/')
234
+ else
235
+ font_props['font-size'] = m
236
+ end
237
+ in_fonts = true
238
+ end
239
+ end
240
+
241
+ font_props.each { |font_prop, font_val| @declarations[font_prop] = {:value => font_val, :is_important => is_important, :order => order} }
242
+
243
+ @declarations.delete('font')
244
+ end
245
+
246
+
247
+ # Convert shorthand background declarations (e.g. <tt>background: url("chess.png") gray 50% repeat fixed;</tt>)
248
+ # into their constituent parts.
249
+ #
250
+ # See http://www.w3.org/TR/CSS21/colors.html#propdef-background
251
+ def expand_background_shorthand! # :nodoc:
252
+ return unless @declarations.has_key?('background')
253
+
254
+ value = @declarations['background'][:value]
255
+ is_important = @declarations['background'][:is_important]
256
+ order = @declarations['background'][:order]
257
+
258
+ bg_props = {}
259
+
260
+
261
+ if m = value.match(Regexp.union(CssParser::URI_RX, /none/i)).to_s
262
+ bg_props['background-image'] = m.strip unless m.empty?
263
+ value.gsub!(Regexp.union(CssParser::URI_RX, /none/i), '')
264
+ end
265
+
266
+ if m = value.match(/([\s]*^)?(scroll|fixed)([\s]*$)?/i).to_s
267
+ bg_props['background-attachment'] = m.strip unless m.empty?
268
+ end
269
+
270
+ if m = value.match(/([\s]*^)?(repeat(\-x|\-y)*|no\-repeat)([\s]*$)?/i).to_s
271
+ bg_props['background-repeat'] = m.strip unless m.empty?
272
+ end
273
+
274
+ if m = value.match(CssParser::RE_COLOUR).to_s
275
+ bg_props['background-color'] = m.strip unless m.empty?
276
+ end
277
+
278
+ value.scan(CssParser::RE_BACKGROUND_POSITION).each do |m|
279
+ if bg_props.has_key?('background-position')
280
+ bg_props['background-position'] += ' ' + m[0].to_s.strip unless m.empty?
281
+ else
282
+ bg_props['background-position'] = m[0].to_s.strip unless m.empty?
283
+ end
284
+ end
285
+
286
+
287
+ if value =~ /([\s]*^)?inherit([\s]*$)?/i
288
+ ['background-color', 'background-image', 'background-attachment', 'background-repeat', 'background-position'].each do |prop|
289
+ bg_props["#{prop}"] = 'inherit' unless bg_props.has_key?(prop) and not bg_props[prop].empty?
290
+ end
291
+ end
292
+
293
+ bg_props.each { |bg_prop, bg_val| @declarations[bg_prop] = {:value => bg_val, :is_important => is_important, :order => order} }
294
+
295
+ @declarations.delete('background')
296
+ end
297
+
298
+
299
+ # Looks for long format CSS background properties (e.g. <tt>background-color</tt>) and
300
+ # converts them into a shorthand CSS <tt>background</tt> property.
301
+ def create_background_shorthand! # :nodoc:
302
+ new_value = ''
303
+ ['background-color', 'background-image', 'background-repeat',
304
+ 'background-position', 'background-attachment'].each do |property|
305
+ if @declarations.has_key?(property)
306
+ new_value += @declarations[property][:value] + ' '
307
+ @declarations.delete(property)
308
+ end
309
+ end
310
+
311
+ unless new_value.strip.empty?
312
+ @declarations['background'] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
313
+ end
314
+ end
315
+
316
+ # Looks for long format CSS dimensional properties (i.e. <tt>margin</tt> and <tt>padding</tt>) and
317
+ # converts them into shorthand CSS properties.
318
+ def create_dimensions_shorthand! # :nodoc:
319
+ # geometric
320
+ directions = ['top', 'right', 'bottom', 'left']
321
+ ['margin', 'padding'].each do |property|
322
+ values = {}
323
+
324
+ foldable = @declarations.select { |dim, val| dim == "#{property}-top" or dim == "#{property}-right" or dim == "#{property}-bottom" or dim == "#{property}-left" }
325
+ # All four dimensions must be present
326
+ if foldable.length == 4
327
+ values = {}
328
+
329
+ directions.each { |d| values[d.to_sym] = @declarations["#{property}-#{d}"][:value].downcase.strip }
330
+
331
+ if values[:left] == values[:right]
332
+ if values[:top] == values[:bottom]
333
+ if values[:top] == values[:left] # All four sides are equal
334
+ new_value = values[:top]
335
+ else # Top and bottom are equal, left and right are equal
336
+ new_value = values[:top] + ' ' + values[:left]
337
+ end
338
+ else # Only left and right are equal
339
+ new_value = values[:top] + ' ' + values[:left] + ' ' + values[:bottom]
340
+ end
341
+ else # No sides are equal
342
+ new_value = values[:top] + ' ' + values[:right] + ' ' + values[:bottom] + ' ' + values[:left]
343
+ end # done creating 'new_value'
344
+
345
+ # Save the new value
346
+ unless new_value.strip.empty?
347
+ @declarations[property] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
348
+ end
349
+
350
+ # Delete the shorthand values
351
+ directions.each { |d| @declarations.delete("#{property}-#{d}") }
352
+ end
353
+ end # done iterating through margin and padding
354
+ end
355
+
356
+
357
+ # Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and
358
+ # tries to convert them into a shorthand CSS <tt>font</tt> property. All
359
+ # font properties must be present in order to create a shorthand declaration.
360
+ def create_font_shorthand! # :nodoc:
361
+ ['font-style', 'font-variant', 'font-weight', 'font-size',
362
+ 'line-height', 'font-family'].each do |prop|
363
+ return unless @declarations.has_key?(prop)
364
+ end
365
+
366
+ new_value = ''
367
+ ['font-style', 'font-variant', 'font-weight'].each do |property|
368
+ unless @declarations[property][:value] == 'normal'
369
+ new_value += @declarations[property][:value] + ' '
370
+ end
371
+ end
372
+
373
+ new_value += @declarations['font-size'][:value]
374
+
375
+ unless @declarations['line-height'][:value] == 'normal'
376
+ new_value += '/' + @declarations['line-height'][:value]
377
+ end
378
+
379
+ new_value += ' ' + @declarations['font-family'][:value]
380
+
381
+ @declarations['font'] = {:value => new_value.gsub(/[\s]+/, ' ').strip}
382
+
383
+ ['font-style', 'font-variant', 'font-weight', 'font-size',
384
+ 'line-height', 'font-family'].each do |prop|
385
+ @declarations.delete(prop)
386
+ end
387
+
388
+ end
389
+ end
390
+ end
@@ -0,0 +1,4 @@
1
+ @import "import-circular-reference.css";
2
+
3
+ body { color: black; background: white; }
4
+ p { margin: 0px; }
@@ -0,0 +1,3 @@
1
+ @import "simple.css" print, tv, screen;
2
+
3
+ div { color: lime; }
@@ -0,0 +1,3 @@
1
+ @import 'subdir/import2.css';
2
+
3
+ div { color: lime; }