deadweight 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. data/README.rdoc +17 -5
  2. data/Rakefile +0 -1
  3. data/VERSION +1 -1
  4. data/deadweight.gemspec +29 -9
  5. data/lib/deadweight.rb +51 -15
  6. data/lib/deadweight/cli.rb +8 -2
  7. data/test/cli_test.rb +9 -6
  8. data/test/test_helper.rb +2 -2
  9. data/vendor/gems/css_parser-0.9.1/CHANGELOG +13 -0
  10. data/vendor/gems/css_parser-0.9.1/LICENSE +21 -0
  11. data/vendor/gems/css_parser-0.9.1/README +58 -0
  12. data/vendor/gems/css_parser-0.9.1/lib/css_parser.rb +149 -0
  13. data/vendor/gems/css_parser-0.9.1/lib/css_parser/parser.rb +345 -0
  14. data/vendor/gems/css_parser-0.9.1/lib/css_parser/regexps.rb +46 -0
  15. data/vendor/gems/css_parser-0.9.1/lib/css_parser/rule_set.rb +380 -0
  16. data/vendor/gems/css_parser-0.9.1/test/fixtures/import-circular-reference.css +4 -0
  17. data/vendor/gems/css_parser-0.9.1/test/fixtures/import-with-media-types.css +3 -0
  18. data/vendor/gems/css_parser-0.9.1/test/fixtures/import1.css +3 -0
  19. data/vendor/gems/css_parser-0.9.1/test/fixtures/simple.css +6 -0
  20. data/vendor/gems/css_parser-0.9.1/test/fixtures/subdir/import2.css +3 -0
  21. data/vendor/gems/css_parser-0.9.1/test/test_css_parser_basic.rb +56 -0
  22. data/vendor/gems/css_parser-0.9.1/test/test_css_parser_downloading.rb +81 -0
  23. data/vendor/gems/css_parser-0.9.1/test/test_css_parser_media_types.rb +71 -0
  24. data/vendor/gems/css_parser-0.9.1/test/test_css_parser_misc.rb +143 -0
  25. data/vendor/gems/css_parser-0.9.1/test/test_css_parser_regexps.rb +68 -0
  26. data/vendor/gems/css_parser-0.9.1/test/test_helper.rb +8 -0
  27. data/vendor/gems/css_parser-0.9.1/test/test_merging.rb +88 -0
  28. data/vendor/gems/css_parser-0.9.1/test/test_rule_set.rb +74 -0
  29. data/vendor/gems/css_parser-0.9.1/test/test_rule_set_creating_shorthand.rb +90 -0
  30. data/vendor/gems/css_parser-0.9.1/test/test_rule_set_expanding_shorthand.rb +178 -0
  31. metadata +25 -13
@@ -0,0 +1,149 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+ require 'uri'
3
+ require 'md5'
4
+ require 'zlib'
5
+ require 'iconv'
6
+ require 'css_parser/rule_set'
7
+ require 'css_parser/regexps'
8
+ require 'css_parser/parser'
9
+
10
+ module CssParser
11
+ # Merge multiple CSS RuleSets by cascading according to the CSS 2.1 cascading rules
12
+ # (http://www.w3.org/TR/REC-CSS2/cascade.html#cascading-order).
13
+ #
14
+ # Takes one or more RuleSet objects.
15
+ #
16
+ # Returns a RuleSet.
17
+ #
18
+ # ==== Cascading
19
+ # If a RuleSet object has its +specificity+ defined, that specificity is
20
+ # used in the cascade calculations.
21
+ #
22
+ # If no specificity is explicitly set and the RuleSet has *one* selector,
23
+ # the specificity is calculated using that selector.
24
+ #
25
+ # If no selectors or multiple selectors are present, the specificity is
26
+ # treated as 0.
27
+ #
28
+ # ==== Example #1
29
+ # rs1 = RuleSet.new(nil, 'color: black;')
30
+ # rs2 = RuleSet.new(nil, 'margin: 0px;')
31
+ #
32
+ # merged = CssParser.merge(rs1, rs2)
33
+ #
34
+ # puts merged
35
+ # => "{ margin: 0px; color: black; }"
36
+ #
37
+ # ==== Example #2
38
+ # rs1 = RuleSet.new(nil, 'background-color: black;')
39
+ # rs2 = RuleSet.new(nil, 'background-image: none;')
40
+ #
41
+ # merged = CssParser.merge(rs1, rs2)
42
+ #
43
+ # puts merged
44
+ # => "{ background: none black; }"
45
+ #--
46
+ # TODO: declaration_hashes should be able to contain a RuleSet
47
+ # this should be a Class method
48
+ def CssParser.merge(*rule_sets)
49
+ @folded_declaration_cache = {}
50
+
51
+ # in case called like CssParser.merge([rule_set, rule_set])
52
+ rule_sets.flatten! if rule_sets[0].kind_of?(Array)
53
+
54
+ unless rule_sets.all? {|rs| rs.kind_of?(CssParser::RuleSet)}
55
+ raise ArgumentError, "all parameters must be CssParser::RuleSets."
56
+ end
57
+
58
+ return rule_sets[0] if rule_sets.length == 1
59
+
60
+ # Internal storage of CSS properties that we will keep
61
+ properties = {}
62
+
63
+ rule_sets.each do |rule_set|
64
+ rule_set.expand_shorthand!
65
+
66
+ specificity = rule_set.specificity
67
+ unless specificity
68
+ if rule_set.selectors.length == 1
69
+ specificity = calculate_specificity(rule_set.selectors[0])
70
+ else
71
+ specificity = 0
72
+ end
73
+ end
74
+
75
+ rule_set.each_declaration do |property, value, is_important|
76
+ # Add the property to the list to be folded per http://www.w3.org/TR/CSS21/cascade.html#cascading-order
77
+ if not properties.has_key?(property) or
78
+ is_important or # step 2
79
+ properties[property][:specificity] < specificity or # step 3
80
+ properties[property][:specificity] == specificity # step 4
81
+ properties[property] = {:value => value, :specificity => specificity, :is_important => is_important}
82
+ end
83
+ end
84
+ end
85
+
86
+ merged = RuleSet.new(nil, nil)
87
+
88
+ # TODO: what about important
89
+ properties.each do |property, details|
90
+ merged[property.strip] = details[:value].strip
91
+ end
92
+
93
+ merged.create_shorthand!
94
+ merged
95
+ end
96
+
97
+ # Calculates the specificity of a CSS selector
98
+ # per http://www.w3.org/TR/CSS21/cascade.html#specificity
99
+ #
100
+ # Returns an integer.
101
+ #
102
+ # ==== Example
103
+ # CssParser.calculate_specificity('#content div p:first-line a:link')
104
+ # => 114
105
+ #--
106
+ # Thanks to Rafael Salazar and Nick Fitzsimons on the css-discuss list for their help.
107
+ #++
108
+ def CssParser.calculate_specificity(selector)
109
+ a = 0
110
+ b = selector.scan(/\#/).length
111
+ c = selector.scan(NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX).length
112
+ d = selector.scan(ELEMENTS_AND_PSEUDO_ELEMENTS_RX).length
113
+
114
+ (a.to_s + b.to_s + c.to_s + d.to_s).to_i
115
+ rescue
116
+ return 0
117
+ end
118
+
119
+ # Make <tt>url()</tt> links absolute.
120
+ #
121
+ # Takes a block of CSS and returns it with all relative URIs converted to absolute URIs.
122
+ #
123
+ # "For CSS style sheets, the base URI is that of the style sheet, not that of the source document."
124
+ # per http://www.w3.org/TR/CSS21/syndata.html#uri
125
+ #
126
+ # Returns a string.
127
+ #
128
+ # ==== Example
129
+ # CssParser.convert_uris("body { background: url('../style/yellow.png?abc=123') };",
130
+ # "http://example.org/style/basic.css").inspect
131
+ # => "body { background: url('http://example.org/style/yellow.png?abc=123') };"
132
+ def self.convert_uris(css, base_uri)
133
+ out = ''
134
+ base_uri = URI.parse(base_uri) unless base_uri.kind_of?(URI)
135
+
136
+ out = css.gsub(URI_RX) do |s|
137
+ uri = $1.to_s
138
+ uri.gsub!(/["']+/, '')
139
+ # Don't process URLs that are already absolute
140
+ unless uri =~ /^[a-z]+\:\/\//i
141
+ begin
142
+ uri = base_uri.merge(uri)
143
+ rescue; end
144
+ end
145
+ "url('" + uri.to_s + "')"
146
+ end
147
+ out
148
+ end
149
+ end
@@ -0,0 +1,345 @@
1
+ module CssParser
2
+ # Exception class used for any errors encountered while downloading remote files.
3
+ class RemoteFileError < IOError; end
4
+
5
+ # Exception class used if a request is made to load a CSS file more than once.
6
+ class CircularReferenceError < StandardError; end
7
+
8
+
9
+ # == Parser class
10
+ #
11
+ # All CSS is converted to UTF-8.
12
+ #
13
+ # When calling Parser#new there are some configuaration options:
14
+ # [<tt>absolute_paths</tt>] Convert relative paths to absolute paths (<tt>href</tt>, <tt>src</tt> and <tt>url('')</tt>. Boolean, default is <tt>false</tt>.
15
+ # [<tt>import</tt>] Follow <tt>@import</tt> rules. Boolean, default is <tt>true</tt>.
16
+ # [<tt>io_exceptions</tt>] Throw an exception if a link can not be found. Boolean, default is <tt>true</tt>.
17
+ class Parser
18
+ USER_AGENT = "Ruby CSS Parser/#{VERSION} (http://code.dunae.ca/css_parser/)"
19
+
20
+ STRIP_CSS_COMMENTS_RX = /\/\*.*?\*\//m
21
+ STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m
22
+
23
+ # Initial parsing
24
+ RE_AT_IMPORT_RULE = /\@import[\s]+(url\()?["']+(.[^'"]*)["']\)?([\w\s\,]*);?/i
25
+
26
+ #--
27
+ # 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
28
+ #++
29
+
30
+ # Array of CSS files that have been loaded.
31
+ attr_reader :loaded_uris
32
+
33
+ #attr_reader :rules
34
+
35
+ #--
36
+ # Class variable? see http://www.oreillynet.com/ruby/blog/2007/01/nubygems_dont_use_class_variab_1.html
37
+ #++
38
+ @folded_declaration_cache = {}
39
+ class << self; attr_reader :folded_declaration_cache; end
40
+
41
+ def initialize(options = {})
42
+ @options = {:absolute_paths => false,
43
+ :import => true,
44
+ :io_exceptions => true}.merge(options)
45
+
46
+ # array of RuleSets
47
+ @rules = []
48
+
49
+
50
+ @loaded_uris = []
51
+
52
+ # unprocessed blocks of CSS
53
+ @blocks = []
54
+ reset!
55
+ end
56
+
57
+ # Get declarations by selector.
58
+ #
59
+ # +media_types+ are optional, and can be a symbol or an array of symbols.
60
+ # The default value is <tt>:all</tt>.
61
+ #
62
+ # ==== Examples
63
+ # find_by_selector('#content')
64
+ # => 'font-size: 13px; line-height: 1.2;'
65
+ #
66
+ # find_by_selector('#content', [:screen, :handheld])
67
+ # => 'font-size: 13px; line-height: 1.2;'
68
+ #
69
+ # find_by_selector('#content', :print)
70
+ # => 'font-size: 11pt; line-height: 1.2;'
71
+ #
72
+ # Returns an array of declarations.
73
+ def find_by_selector(selector, media_types = :all)
74
+ out = []
75
+ each_selector(media_types) do |sel, dec, spec|
76
+ out << dec if sel.strip == selector.strip
77
+ end
78
+ out
79
+ end
80
+ alias_method :[], :find_by_selector
81
+
82
+
83
+ # Add a raw block of CSS.
84
+ #
85
+ # ==== Example
86
+ # css = <<-EOT
87
+ # body { font-size: 10pt }
88
+ # p { margin: 0px; }
89
+ # @media screen, print {
90
+ # body { line-height: 1.2 }
91
+ # }
92
+ # EOT
93
+ #
94
+ # parser = CssParser::Parser.new
95
+ # parser.load_css!(css)
96
+ #--
97
+ # TODO: add media_type
98
+ #++
99
+ def add_block!(block, options = {})
100
+ options = {:base_uri => nil, :charset => nil, :media_types => :all}.merge(options)
101
+
102
+ block = cleanup_block(block)
103
+
104
+ if options[:base_uri] and @options[:absolute_paths]
105
+ block = CssParser.convert_uris(block, options[:base_uri])
106
+ end
107
+
108
+ parse_block_into_rule_sets!(block, options)
109
+
110
+ end
111
+
112
+ # Add a CSS rule by setting the +selectors+, +declarations+ and +media_types+.
113
+ #
114
+ # +media_types+ can be a symbol or an array of symbols.
115
+ def add_rule!(selectors, declarations, media_types = :all)
116
+ rule_set = RuleSet.new(selectors, declarations)
117
+ add_rule_set!(rule_set, media_types)
118
+ end
119
+
120
+ # Add a CssParser RuleSet object.
121
+ #
122
+ # +media_types+ can be a symbol or an array of symbols.
123
+ def add_rule_set!(ruleset, media_types = :all)
124
+ raise ArgumentError unless ruleset.kind_of?(CssParser::RuleSet)
125
+
126
+ media_types = [media_types] if media_types.kind_of?(Symbol)
127
+
128
+ @rules << {:media_types => media_types, :rules => ruleset}
129
+ end
130
+
131
+ # Iterate through RuleSet objects.
132
+ #
133
+ # +media_types+ can be a symbol or an array of symbols.
134
+ def each_rule_set(media_types = :all) # :yields: rule_set
135
+ media_types = [:all] if media_types.nil?
136
+ media_types = [media_types] if media_types.kind_of?(Symbol)
137
+
138
+ @rules.each do |block|
139
+ if media_types.include?(:all) or block[:media_types].any? { |mt| media_types.include?(mt) }
140
+ yield block[:rules]
141
+ end
142
+ end
143
+ end
144
+
145
+ # Iterate through CSS selectors.
146
+ #
147
+ # +media_types+ can be a symbol or an array of symbols.
148
+ # See RuleSet#each_selector for +options+.
149
+ def each_selector(media_types = :all, options = {}) # :yields: selectors, declarations, specificity
150
+ each_rule_set(media_types) do |rule_set|
151
+ #puts rule_set
152
+ rule_set.each_selector(options) do |selectors, declarations, specificity|
153
+ yield selectors, declarations, specificity
154
+ end
155
+ end
156
+ end
157
+
158
+ # Output all CSS rules as a single stylesheet.
159
+ def to_s(media_types = :all)
160
+ out = ''
161
+ each_selector(media_types) do |selectors, declarations, specificity|
162
+ out << "#{selectors} {\n#{declarations}\n}\n"
163
+ end
164
+ out
165
+ end
166
+
167
+ # Merge declarations with the same selector.
168
+ def compact! # :nodoc:
169
+ compacted = []
170
+
171
+ compacted
172
+ end
173
+
174
+ def parse_block_into_rule_sets!(block, options = {}) # :nodoc:
175
+ options = {:media_types => :all}.merge(options)
176
+ media_types = options[:media_types]
177
+
178
+ in_declarations = false
179
+
180
+ block_depth = 0
181
+
182
+ # @charset is ignored for now
183
+ in_charset = false
184
+ in_string = false
185
+ in_at_media_rule = false
186
+
187
+ current_selectors = ''
188
+ current_declarations = ''
189
+
190
+ block.scan(/([\\]?[{}\s"]|(.[^\s"{}\\]*))/).each do |matches|
191
+ #block.scan(/((.[^{}"\n\r\f\s]*)[\s]|(.[^{}"\n\r\f]*)\{|(.[^{}"\n\r\f]*)\}|(.[^{}"\n\r\f]*)\"|(.*)[\s]+)/).each do |matches|
192
+ token = matches[0]
193
+ #puts "TOKEN: #{token}" unless token =~ /^[\s]*$/
194
+ if token =~ /\A"/ # found un-escaped double quote
195
+ in_string = !in_string
196
+ end
197
+
198
+ if in_declarations
199
+ current_declarations += token
200
+
201
+ if token =~ /\}/ and not in_string
202
+ current_declarations.gsub!(/\}[\s]*$/, '')
203
+
204
+ in_declarations = false
205
+
206
+ unless current_declarations.strip.empty?
207
+ #puts "SAVING #{current_selectors} -> #{current_declarations}"
208
+ add_rule!(current_selectors, current_declarations, media_types)
209
+ end
210
+
211
+ current_selectors = ''
212
+ current_declarations = ''
213
+ end
214
+ elsif token =~ /@media/i
215
+ # found '@media', reset current media_types
216
+ in_at_media_rule = true
217
+ media_types = []
218
+ elsif in_at_media_rule
219
+ if token =~ /\{/
220
+ block_depth = block_depth + 1
221
+ in_at_media_rule = false
222
+ else
223
+ token.gsub!(/[,\s]*/, '')
224
+ media_types << token.strip.downcase.to_sym unless token.empty?
225
+ end
226
+ elsif in_charset or token =~ /@charset/i
227
+ # iterate until we are out of the charset declaration
228
+ in_charset = (token =~ /;/ ? false : true)
229
+ else
230
+ if token =~ /\}/ and not in_string
231
+ block_depth = block_depth - 1
232
+ else
233
+ if token =~ /\{/ and not in_string
234
+ current_selectors.gsub!(/^[\s]*/, '')
235
+ current_selectors.gsub!(/[\s]*$/, '')
236
+ in_declarations = true
237
+ else
238
+ current_selectors += token
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ # Load a remote CSS file.
246
+ def load_uri!(uri, base_uri = nil, media_types = :all)
247
+ base_uri = uri if base_uri.nil?
248
+ src, charset = read_remote_file(uri)
249
+
250
+ # Load @imported CSS
251
+ src.scan(RE_AT_IMPORT_RULE).each do |import_rule|
252
+ import_path = import_rule[1].to_s.gsub(/['"]*/, '').strip
253
+ import_uri = URI.parse(base_uri.to_s).merge(import_path)
254
+ #puts import_uri.to_s
255
+
256
+ media_types = []
257
+ if media_string = import_rule[import_rule.length-1]
258
+ media_string.split(/\s|\,/).each do |t|
259
+ media_types << t.to_sym unless t.empty?
260
+ end
261
+ end
262
+
263
+ # Recurse
264
+ load_uri!(import_uri, nil, media_types)
265
+ end
266
+
267
+ # Remove @import declarations
268
+ src.gsub!(RE_AT_IMPORT_RULE, '')
269
+
270
+ # Relative paths need to be converted here
271
+ src = CssParser.convert_uris(src, base_uri) if base_uri and @options[:absolute_paths]
272
+
273
+ add_block!(src, {:media_types => media_types})
274
+ end
275
+
276
+ protected
277
+ # Strip comments and clean up blank lines from a block of CSS.
278
+ #
279
+ # Returns a string.
280
+ def cleanup_block(block) # :nodoc:
281
+ # Strip CSS comments
282
+ block.gsub!(STRIP_CSS_COMMENTS_RX, '')
283
+
284
+ # Strip HTML comments - they shouldn't really be in here but
285
+ # some people are just crazy...
286
+ block.gsub!(STRIP_HTML_COMMENTS_RX, '')
287
+
288
+ # Strip lines containing just whitespace
289
+ block.gsub!(/^\s+$/, "")
290
+
291
+ block
292
+ end
293
+
294
+ # Download a file into a string.
295
+ #
296
+ # Returns the file's data and character set in an array.
297
+ #--
298
+ # TODO: add option to fail silently or throw and exception on a 404
299
+ #++
300
+ def read_remote_file(uri) # :nodoc:
301
+ raise CircularReferenceError, "can't load #{uri.to_s} more than once" if @loaded_uris.include?(uri.to_s)
302
+ @loaded_uris << uri.to_s
303
+
304
+ begin
305
+ #fh = open(uri, 'rb')
306
+ fh = open(uri, 'rb', 'User-Agent' => USER_AGENT, 'Accept-Encoding' => 'gzip')
307
+
308
+ if fh.content_encoding.include?('gzip')
309
+ remote_src = Zlib::GzipReader.new(fh).read
310
+ else
311
+ remote_src = fh.read
312
+ end
313
+
314
+ #puts "reading #{uri} (#{fh.charset})"
315
+
316
+ ic = Iconv.new('UTF-8//IGNORE', fh.charset)
317
+ src = ic.iconv(remote_src)
318
+
319
+ fh.close
320
+ return src, fh.charset
321
+ rescue
322
+ raise RemoteFileError if @options[:io_exceptions]
323
+ return '', nil
324
+ end
325
+ end
326
+
327
+ private
328
+ # Save a folded declaration block to the internal cache.
329
+ def save_folded_declaration(block_hash, folded_declaration) # :nodoc:
330
+ @folded_declaration_cache[block_hash] = folded_declaration
331
+ end
332
+
333
+ # Retrieve a folded declaration block from the internal cache.
334
+ def get_folded_declaration(block_hash) # :nodoc:
335
+ return @folded_declaration_cache[block_hash] ||= nil
336
+ end
337
+
338
+ def reset! # :nodoc:
339
+ @folded_declaration_cache = {}
340
+ @css_source = ''
341
+ @css_rules = []
342
+ @css_warnings = []
343
+ end
344
+ end
345
+ end