DanaDanger-css_parser 0.9.1

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