deadweight 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/README.md +123 -0
  2. data/lib/deadweight.rb +37 -11
  3. data/lib/deadweight/hijack.rb +13 -0
  4. data/lib/deadweight/hijack/rails.rb +35 -0
  5. data/lib/deadweight/rack/capturing_middleware.rb +26 -0
  6. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/lib/css_parser.rb +25 -12
  7. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/lib/css_parser/parser.rb +120 -54
  8. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/lib/css_parser/regexps.rb +1 -1
  9. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/lib/css_parser/rule_set.rb +24 -13
  10. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/test/fixtures/import-circular-reference.css +0 -0
  11. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/test/fixtures/import-with-media-types.css +0 -0
  12. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/test/fixtures/import1.css +0 -0
  13. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/test/fixtures/simple.css +0 -0
  14. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/test/fixtures/subdir/import2.css +0 -0
  15. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/test/test_css_parser_basic.rb +4 -1
  16. data/vendor/gems/css_parser-1.1.5/test/test_css_parser_loading.rb +139 -0
  17. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/test/test_css_parser_media_types.rb +39 -4
  18. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/test/test_css_parser_misc.rb +1 -2
  19. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/test/test_css_parser_regexps.rb +1 -1
  20. data/vendor/gems/css_parser-1.1.5/test/test_helper.rb +6 -0
  21. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/test/test_merging.rb +16 -1
  22. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/test/test_rule_set.rb +16 -1
  23. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/test/test_rule_set_creating_shorthand.rb +1 -1
  24. data/vendor/gems/{css_parser-1.0.0 → css_parser-1.1.5}/test/test_rule_set_expanding_shorthand.rb +1 -1
  25. metadata +79 -54
  26. data/.document +0 -5
  27. data/.gitignore +0 -6
  28. data/README.rdoc +0 -85
  29. data/Rakefile +0 -58
  30. data/VERSION +0 -1
  31. data/deadweight.gemspec +0 -84
  32. data/test/cli_test.rb +0 -21
  33. data/test/deadweight_test.rb +0 -72
  34. data/test/fixtures/index.html +0 -16
  35. data/test/fixtures/index2.html +0 -21
  36. data/test/fixtures/style.css +0 -24
  37. data/test/rake_task_test.rb +0 -26
  38. data/test/test_helper.rb +0 -47
  39. data/vendor/gems/css_parser-1.0.0/test/test_css_parser_downloading.rb +0 -81
  40. data/vendor/gems/css_parser-1.0.0/test/test_helper.rb +0 -8
@@ -0,0 +1,123 @@
1
+ Deadweight
2
+ ==========
3
+
4
+ Deadweight is a CSS coverage tool. Given a set of stylesheets and a set of URLs, it determines which selectors are actually used and reports which can be "safely" deleted.
5
+
6
+ Screencast!
7
+ -----------
8
+
9
+ Ryan Bates has worked his magic once again. [Head over here for an excellent introduction to Deadweight](http://railscasts.com/episodes/180-finding-unused-css).
10
+
11
+ How to Use It
12
+ -------------
13
+
14
+ There are multiple ways to use Deadweight. It's designed to be completely agnostic of whatever framework (or, indeed language) your website is built on, but optimised for Ruby and Rails.
15
+
16
+ I'm going to tell you about the coolest way to use it first.
17
+
18
+ ### The Coolest Way to Use It ###
19
+
20
+ Deadweight can hijack your Rails integration tests (both the spartan Test::Unit type and the refreshing Cucumber variety), capturing every page that is returned by your app during testing and saving you the trouble of manually specifying a ton of URLs and login processses. THIS IS PRETTY COOL YOU SHOULD TRY IT. (It's also somewhat new and untested, but you're the kind of person who just _loves_ it out here on the edge, I can tell.)
21
+
22
+ Here's how it works. First, put this in your `Gemfile`:
23
+
24
+ group :test do
25
+ gem 'colored' # optional, in the same way that dressing respectably when you leave the house is 'optional'
26
+ gem 'deadweight', :require => 'deadweight/hijack/rails'
27
+ end
28
+
29
+ Then, run your integration tests with the environment variable `DEADWEIGHT` set to `true`:
30
+
31
+ rake test DEADWEIGHT=true
32
+ rake cucumber DEADWEIGHT=true
33
+
34
+ Let me know how it goes. It's not terribly customisable at the moment (you can't specify what exact stylesheets to look at, or what selectors to ignore). I'm looking for your feedback on how you'd like to be able to do that.
35
+
36
+ ### Or Make a Rake Task ###
37
+
38
+ # lib/tasks/deadweight.rake
39
+
40
+ require 'deadweight'
41
+
42
+ Deadweight::RakeTask.new do |dw|
43
+ dw.stylesheets = %w( /stylesheets/style.css )
44
+ dw.pages = %w( / /page/1 /about )
45
+ end
46
+
47
+ Running `rake deadweight` will output all unused rules, one per line. Note that it looks at `http://localhost:3000` by default, so you'll need to have `script/server` (or whatever your server command looks like) running.
48
+
49
+ ### Or Run it From the Command Line ###
50
+
51
+ $ deadweight -s styles.css -s ie.css index.html about.html
52
+ $ deadweight -s http://www.tigerbloodwins.com/index.css http://www.tigerbloodwins.com/
53
+ $ deadweight --root http://kottke.org/ -s '/templates/2009/css.php?p=mac' / /everfresh /about
54
+
55
+ You can pipe in CSS rules from STDIN:
56
+
57
+ $ cat styles.css | deadweight index.html
58
+
59
+ And you can use it as an HTTP proxy:
60
+
61
+ $ deadweight -l deadweight.log -s styles.css -w http://github.com/ -P
62
+
63
+ ### Or Call it Directly ###
64
+
65
+ require 'deadweight'
66
+
67
+ dw = Deadweight.new
68
+ dw.stylesheets = %w( /stylesheets/style.css )
69
+ dw.pages = %w( / /page/1 /about )
70
+ puts dw.run
71
+
72
+ Setting the Root URL
73
+ --------------------
74
+
75
+ By default, Deadweight uses `http://localhost:3000` as the base URL for all paths. To change it, set `root`:
76
+
77
+ dw.root = "http://staging.example.com" # staging server
78
+ dw.root = "http://example.com/staging-area" # urls can have paths in
79
+ dw.root = "/path/to/some/html" # local paths work too
80
+
81
+ What About Stuff Added by Javascript?
82
+ -------------------------------------
83
+
84
+ Deadweight is completely dumb about any classes, IDs or tags that are only added by your Javascript layer, but you can filter them out by setting `ignore_selectors`:
85
+
86
+ dw.ignore_selectors = /hover|lightbox|superimposed_kittens/
87
+
88
+ The command-line tool also has basic support for [Lyndon](http://github.com/defunkt/lyndon) with the `-L` flag, which simply pipes all HTML through the `lyndon` executable.
89
+
90
+ You Can Use Mechanize for Complex Stuff
91
+ ---------------------------------------
92
+
93
+ Set `mechanize` to `true` and add a Proc to `pages` (rather than a String), and Deadweight will execute it using [Mechanize](http://mechanize.rubyforge.org/mechanize):
94
+
95
+ dw.mechanize = true
96
+
97
+ # go through the login form to get to a protected URL
98
+ dw.pages << proc {
99
+ fetch('/login')
100
+ form = agent.page.forms.first
101
+ form.username = 'username'
102
+ form.password = 'password'
103
+ agent.submit(form)
104
+ fetch('/secret-page')
105
+ }
106
+
107
+ # use HTTP basic auth
108
+ dw.pages << proc {
109
+ agent.auth('username', 'password')
110
+ fetch('/other-secret-page')
111
+ }
112
+
113
+ The `agent` method returns the Mechanize instance. The `fetch` method is a wrapper around `agent.get` that will abort in the event of an HTTP error status.
114
+
115
+ If You Install `colored`, It'll Look Nicer
116
+ -------------------------------------------------
117
+
118
+ gem install colored
119
+
120
+ Copyright
121
+ ---------
122
+
123
+ Copyright (c) 2009 Aanand Prasad. See LICENSE for details.
@@ -1,7 +1,11 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup
4
+
1
5
  $LOAD_PATH.concat Dir.glob(File.expand_path('../../vendor/gems/*/lib', __FILE__))
2
6
 
3
7
  require 'css_parser'
4
- require 'hpricot'
8
+ require 'nokogiri'
5
9
  require 'open-uri'
6
10
 
7
11
  begin
@@ -30,12 +34,16 @@ class Deadweight
30
34
  end
31
35
 
32
36
  def analyze(html)
33
- doc = Hpricot(html)
37
+ doc = Nokogiri::HTML(html)
34
38
 
35
39
  @unused_selectors.collect do |selector, declarations|
36
40
  # We test against the selector stripped of any pseudo classes,
37
41
  # but we report on the selector with its pseudo classes.
38
- unless doc.search(strip(selector)).empty?
42
+ stripped_selector = strip(selector)
43
+
44
+ next if stripped_selector.empty?
45
+
46
+ if doc.search(stripped_selector).any?
39
47
  log.puts(" #{selector.green}")
40
48
  selector
41
49
  end
@@ -62,8 +70,7 @@ class Deadweight
62
70
  selector_count
63
71
  end
64
72
 
65
- # Find all unused CSS selectors and return them as an array.
66
- def run
73
+ def reset!
67
74
  @parsed_rules = {}
68
75
  @unused_selectors = []
69
76
 
@@ -78,7 +85,18 @@ class Deadweight
78
85
  log.puts("Added #{new_selector_count} extra selectors".yellow)
79
86
  end
80
87
 
81
- total_selectors = @unused_selectors.size
88
+ @total_selectors = @unused_selectors.size
89
+ end
90
+
91
+ def report
92
+ log.puts
93
+ log.puts "found #{@unused_selectors.size} unused selectors out of #{@total_selectors} total".yellow
94
+ log.puts
95
+ end
96
+
97
+ # Find all unused CSS selectors and return them as an array.
98
+ def run
99
+ reset!
82
100
 
83
101
  pages.each do |page|
84
102
  log.puts
@@ -106,9 +124,7 @@ class Deadweight
106
124
  process!(html)
107
125
  end
108
126
 
109
- log.puts
110
- log.puts "found #{@unused_selectors.size} unused selectors out of #{total_selectors} total".yellow
111
- log.puts
127
+ report
112
128
 
113
129
  @unused_selectors
114
130
  end
@@ -139,7 +155,7 @@ class Deadweight
139
155
 
140
156
  begin
141
157
  page = agent.get(loc)
142
- rescue WWW::Mechanize::ResponseCodeError => e
158
+ rescue Mechanize::ResponseCodeError => e
143
159
  raise FetchError.new("#{loc} returned a response code of #{e.response_code}")
144
160
  end
145
161
 
@@ -178,7 +194,17 @@ private
178
194
  def initialize_agent
179
195
  begin
180
196
  require 'mechanize'
181
- return WWW::Mechanize.new
197
+
198
+ unless defined?(Mechanize::VERSION) and Mechanize::VERSION >= "1.0.0"
199
+ log.puts %{
200
+ =================================================================
201
+ A mechanize version of 1.0.0 or above is required.
202
+ Install it like so: gem install mechanize
203
+ =================================================================
204
+ }
205
+ end
206
+
207
+ return Mechanize.new
182
208
  rescue LoadError
183
209
  log.puts %{
184
210
  =================================================================
@@ -0,0 +1,13 @@
1
+ class Deadweight
2
+ module Hijack
3
+ def self.redirect_output(log_file_prefix)
4
+ original_stdout, original_stderr = STDOUT.clone, STDERR.clone
5
+
6
+ STDOUT.reopen(File.open("#{log_file_prefix}stdout.log", 'w'))
7
+ STDERR.reopen(File.open("#{log_file_prefix}stderr.log", 'w'))
8
+
9
+ [original_stdout, original_stderr]
10
+ end
11
+ end
12
+ end
13
+
@@ -0,0 +1,35 @@
1
+ if ENV['DEADWEIGHT'] == 'true'
2
+ require 'deadweight'
3
+ require 'deadweight/hijack'
4
+ require 'deadweight/rack/capturing_middleware'
5
+
6
+ class Deadweight
7
+ module Hijack
8
+ module Rails
9
+ class Railtie < ::Rails::Railtie
10
+ initializer "deadweight.hijack" do |app|
11
+ root = ::Rails.root
12
+
13
+ original_stdout, original_stderr = Deadweight::Hijack.redirect_output(root + 'log/test_')
14
+
15
+ dw = Deadweight.new
16
+
17
+ dw.root = root + 'public'
18
+ dw.stylesheets = Dir.chdir(dw.root) { Dir.glob("stylesheets/*.css") }
19
+ dw.log_file = original_stderr
20
+
21
+ dw.reset!
22
+
23
+ at_exit do
24
+ dw.report
25
+ dw.dump(original_stdout)
26
+ end
27
+
28
+ app.middleware.insert(0, Deadweight::Rack::CapturingMiddleware, dw)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,26 @@
1
+ class Deadweight
2
+ module Rack
3
+ class CapturingMiddleware
4
+ def initialize(app, dw)
5
+ @app = app
6
+ @dw = dw
7
+ end
8
+
9
+ def call(env)
10
+ response = @app.call(env)
11
+ process(response)
12
+ response
13
+ end
14
+
15
+ def process(rack_response)
16
+ status, headers, response = rack_response
17
+
18
+ if response.respond_to?(:body)
19
+ html = response.body
20
+ @dw.process!(html)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
@@ -1,13 +1,14 @@
1
- $:.unshift File.dirname(__FILE__)
2
1
  require 'uri'
2
+ require 'net/https'
3
+ require 'open-uri'
3
4
  require 'digest/md5'
4
5
  require 'zlib'
6
+ require 'stringio'
5
7
  require 'iconv'
6
- require 'css_parser/rule_set'
7
- require 'css_parser/regexps'
8
- require 'css_parser/parser'
9
8
 
10
9
  module CssParser
10
+ VERSION = '1.1.5'
11
+
11
12
  # Merge multiple CSS RuleSets by cascading according to the CSS 2.1 cascading rules
12
13
  # (http://www.w3.org/TR/REC-CSS2/cascade.html#cascading-order).
13
14
  #
@@ -74,20 +75,28 @@ module CssParser
74
75
 
75
76
  rule_set.each_declaration do |property, value, is_important|
76
77
  # 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}
78
+ if not properties.has_key?(property)
79
+ properties[property] = {:value => value, :specificity => specificity, :is_important => is_important}
80
+ elsif properties[property][:specificity] < specificity or properties[property][:specificity] == specificity
81
+ unless properties[property][:is_important]
82
+ properties[property] = {:value => value, :specificity => specificity, :is_important => is_important}
83
+ end
82
84
  end
85
+
86
+ if is_important
87
+ properties[property] = {:value => value, :specificity => specificity, :is_important => is_important}
88
+ end
83
89
  end
84
90
  end
85
91
 
86
92
  merged = RuleSet.new(nil, nil)
87
93
 
88
- # TODO: what about important
89
94
  properties.each do |property, details|
90
- merged[property.strip] = details[:value].strip
95
+ if details[:is_important]
96
+ merged[property.strip] = details[:value].strip.gsub(/\;\Z/, '') + '!important'
97
+ else
98
+ merged[property.strip] = details[:value].strip
99
+ end
91
100
  end
92
101
 
93
102
  merged.create_shorthand!
@@ -146,4 +155,8 @@ module CssParser
146
155
  end
147
156
  out
148
157
  end
149
- end
158
+ end
159
+
160
+ require File.dirname(__FILE__) + '/css_parser/rule_set'
161
+ require File.dirname(__FILE__) + '/css_parser/regexps'
162
+ require File.dirname(__FILE__) + '/css_parser/parser'
@@ -15,19 +15,15 @@ module CssParser
15
15
  # [<tt>import</tt>] Follow <tt>@import</tt> rules. Boolean, default is <tt>true</tt>.
16
16
  # [<tt>io_exceptions</tt>] Throw an exception if a link can not be found. Boolean, default is <tt>true</tt>.
17
17
  class Parser
18
- USER_AGENT = "Ruby CSS Parser/#{RUBY_VERSION} (http://code.dunae.ca/css_parser/)"
18
+ USER_AGENT = "Ruby CSS Parser/#{CssParser::VERSION} (http://github.com/alexdunae/css_parser)"
19
19
 
20
20
  STRIP_CSS_COMMENTS_RX = /\/\*.*?\*\//m
21
21
  STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m
22
22
 
23
23
  # Initial parsing
24
- RE_AT_IMPORT_RULE = /\@import[\s]+(url\()?["']+(.[^'"]*)["']\)?([\w\s\,]*);?/i
24
+ RE_AT_IMPORT_RULE = /\@import\s*(?:url\s*)?(?:\()?(?:\s*)["']?([^'"\s\)]*)["']?\)?([\w\s\,^\])]*)\)?[;\n]?/
25
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.
26
+ # Array of CSS files that have been loaded.
31
27
  attr_reader :loaded_uris
32
28
 
33
29
  #attr_reader :rules
@@ -82,6 +78,13 @@ module CssParser
82
78
 
83
79
  # Add a raw block of CSS.
84
80
  #
81
+ # In order to follow +@import+ rules you must supply either a
82
+ # +:base_dir+ or +:base_uri+ option.
83
+ #
84
+ # Use the +:media_types+ option to set the media type(s) for this block. Takes an array of symbols.
85
+ #
86
+ # Use the +:only_media_types+ option to selectively follow +@import+ rules. Takes an array of symbols.
87
+ #
85
88
  # ==== Example
86
89
  # css = <<-EOT
87
90
  # body { font-size: 10pt }
@@ -92,21 +95,43 @@ module CssParser
92
95
  # EOT
93
96
  #
94
97
  # parser = CssParser::Parser.new
95
- # parser.load_css!(css)
96
- #--
97
- # TODO: add media_type
98
- #++
98
+ # parser.add_block!(css)
99
99
  def add_block!(block, options = {})
100
- options = {:base_uri => nil, :charset => nil, :media_types => :all}.merge(options)
101
-
100
+ options = {:base_uri => nil, :base_dir => nil, :charset => nil, :media_types => :all, :only_media_types => :all}.merge(options)
101
+ options[:media_types] = [options[:media_types]].flatten
102
+ options[:only_media_types] = [options[:only_media_types]].flatten
103
+
102
104
  block = cleanup_block(block)
103
105
 
104
106
  if options[:base_uri] and @options[:absolute_paths]
105
107
  block = CssParser.convert_uris(block, options[:base_uri])
106
108
  end
109
+
110
+ # Load @imported CSS
111
+ block.scan(RE_AT_IMPORT_RULE).each do |import_rule|
112
+ media_types = []
113
+ if media_string = import_rule[-1]
114
+ media_string.split(/\s|\,/).each do |t|
115
+ media_types << t.to_sym unless t.empty?
116
+ end
117
+ end
118
+
119
+ next unless options[:only_media_types].include?(:all) or media_types.length < 1 or (media_types & options[:only_media_types]).length > 0
120
+
121
+ import_path = import_rule[0].to_s.gsub(/['"]*/, '').strip
122
+
123
+ if options[:base_uri]
124
+ import_uri = URI.parse(options[:base_uri].to_s).merge(import_path)
125
+ load_uri!(import_uri, options[:base_uri], media_types)
126
+ elsif options[:base_dir]
127
+ load_file!(import_path, options[:base_dir], media_types)
128
+ end
129
+ end
130
+
131
+ # Remove @import declarations
132
+ block.gsub!(RE_AT_IMPORT_RULE, '')
107
133
 
108
134
  parse_block_into_rule_sets!(block, options)
109
-
110
135
  end
111
136
 
112
137
  # Add a CSS rule by setting the +selectors+, +declarations+ and +media_types+.
@@ -175,7 +200,7 @@ module CssParser
175
200
  options = {:media_types => :all}.merge(options)
176
201
  media_types = options[:media_types]
177
202
 
178
- in_declarations = false
203
+ in_declarations = 0
179
204
 
180
205
  block_depth = 0
181
206
 
@@ -195,13 +220,25 @@ module CssParser
195
220
  in_string = !in_string
196
221
  end
197
222
 
198
- if in_declarations
223
+ if in_declarations > 0
224
+
225
+ # too deep, malformed declaration block
226
+ if in_declarations > 1
227
+ in_declarations -= 1 if token =~ /\}/
228
+ next
229
+ end
230
+
231
+ if token =~ /\{/
232
+ in_declarations += 1
233
+ next
234
+ end
235
+
199
236
  current_declarations += token
200
237
 
201
238
  if token =~ /\}/ and not in_string
202
239
  current_declarations.gsub!(/\}[\s]*$/, '')
203
240
 
204
- in_declarations = false
241
+ in_declarations -= 1
205
242
 
206
243
  unless current_declarations.strip.empty?
207
244
  #puts "SAVING #{current_selectors} -> #{current_declarations}"
@@ -233,7 +270,7 @@ module CssParser
233
270
  if token =~ /\{/ and not in_string
234
271
  current_selectors.gsub!(/^[\s]*/, '')
235
272
  current_selectors.gsub!(/[\s]*$/, '')
236
- in_declarations = true
273
+ in_declarations += 1
237
274
  else
238
275
  current_selectors += token
239
276
  end
@@ -243,35 +280,35 @@ module CssParser
243
280
  end
244
281
 
245
282
  # Load a remote CSS file.
283
+ #
284
+ # You can also pass in file://test.css
246
285
  def load_uri!(uri, base_uri = nil, media_types = :all)
286
+ uri = URI.parse(uri) unless uri.respond_to? :scheme
287
+ if uri.scheme == 'file' or uri.scheme.nil?
288
+ uri.path = File.expand_path(uri.path)
289
+ uri.scheme = 'file'
290
+ end
247
291
  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
292
 
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
293
+ src, charset = read_remote_file(uri)
262
294
 
263
- # Recurse
264
- load_uri!(import_uri, nil, media_types)
295
+ if src
296
+ add_block!(src, {:media_types => media_types, :base_uri => base_uri})
265
297
  end
298
+ end
299
+
300
+ # Load a local CSS file.
301
+ def load_file!(file_name, base_dir = nil, media_types = :all)
302
+ file_name = File.expand_path(file_name, base_dir)
303
+ return unless File.readable?(file_name)
266
304
 
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]
305
+ src = IO.read(file_name)
306
+ base_dir = File.dirname(file_name)
272
307
 
273
- add_block!(src, {:media_types => media_types})
308
+ add_block!(src, {:media_types => media_types, :base_dir => base_dir})
274
309
  end
310
+
311
+
275
312
 
276
313
  protected
277
314
  # Strip comments and clean up blank lines from a block of CSS.
@@ -298,30 +335,59 @@ module CssParser
298
335
  # TODO: add option to fail silently or throw and exception on a 404
299
336
  #++
300
337
  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)
338
+ if @loaded_uris.include?(uri.to_s)
339
+ raise CircularReferenceError, "can't load #{uri.to_s} more than once" if @options[:io_exceptions]
340
+ return '', nil
341
+ end
342
+
302
343
  @loaded_uris << uri.to_s
303
344
 
304
- begin
305
- #fh = open(uri, 'rb')
306
- fh = open(uri, 'rb', 'User-Agent' => USER_AGENT, 'Accept-Encoding' => 'gzip')
345
+ src = '', charset = nil
307
346
 
308
- if fh.content_encoding.include?('gzip')
309
- remote_src = Zlib::GzipReader.new(fh).read
347
+ begin
348
+ uri = URI.parse(uri.to_s)
349
+ http = Net::HTTP.new(uri.host, uri.port)
350
+
351
+ if uri.scheme == 'file'
352
+ # local file
353
+ fh = open(uri.path, 'rb')
354
+ src = fh.read
355
+ fh.close
310
356
  else
311
- remote_src = fh.read
312
- end
357
+ # remote file
358
+ if uri.scheme == 'https'
359
+ http.use_ssl = true
360
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
361
+ end
313
362
 
314
- #puts "reading #{uri} (#{fh.charset})"
363
+ res, src = http.get(uri.path, {'User-Agent' => USER_AGENT, 'Accept-Encoding' => 'gzip'})
364
+ charset = fh.respond_to?(:charset) ? fh.charset : 'utf-8'
315
365
 
316
- ic = Iconv.new('UTF-8//IGNORE', fh.charset)
317
- src = ic.iconv(remote_src)
366
+ if res.code.to_i >= 400
367
+ raise RemoteFileError if @options[:io_exceptions]
368
+ return '', nil
369
+ end
370
+
371
+ case res['content-encoding']
372
+ when 'gzip'
373
+ io = Zlib::GzipReader.new(StringIO.new(res.body))
374
+ src = io.read
375
+ when 'deflate'
376
+ io = Zlib::Inflate.new
377
+ src = io.inflate(res.body)
378
+ end
379
+ end
318
380
 
319
- fh.close
320
- return src, fh.charset
381
+ if charset
382
+ ic = Iconv.new('UTF-8//IGNORE', charset)
383
+ src = ic.iconv(src)
384
+ end
321
385
  rescue
322
386
  raise RemoteFileError if @options[:io_exceptions]
323
- return '', nil
387
+ return nil, nil
324
388
  end
389
+
390
+ return src, charset
325
391
  end
326
392
 
327
393
  private
@@ -342,4 +408,4 @@ module CssParser
342
408
  @css_warnings = []
343
409
  end
344
410
  end
345
- end
411
+ end