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