DanaDanger-css_parser 0.9.1

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.
@@ -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; }