juicer 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/History.txt +10 -0
  2. data/Manifest.txt +58 -0
  3. data/Rakefile +44 -0
  4. data/Readme.rdoc +143 -0
  5. data/bin/juicer +8 -0
  6. data/lib/juicer.rb +70 -0
  7. data/lib/juicer/binary.rb +173 -0
  8. data/lib/juicer/cache_buster.rb +45 -0
  9. data/lib/juicer/chainable.rb +106 -0
  10. data/lib/juicer/cli.rb +56 -0
  11. data/lib/juicer/command/install.rb +59 -0
  12. data/lib/juicer/command/list.rb +50 -0
  13. data/lib/juicer/command/merge.rb +185 -0
  14. data/lib/juicer/command/util.rb +32 -0
  15. data/lib/juicer/command/verify.rb +60 -0
  16. data/lib/juicer/core.rb +59 -0
  17. data/lib/juicer/css_cache_buster.rb +99 -0
  18. data/lib/juicer/install/base.rb +186 -0
  19. data/lib/juicer/install/jslint_installer.rb +51 -0
  20. data/lib/juicer/install/rhino_installer.rb +52 -0
  21. data/lib/juicer/install/yui_compressor_installer.rb +66 -0
  22. data/lib/juicer/jslint.rb +90 -0
  23. data/lib/juicer/merger/base.rb +74 -0
  24. data/lib/juicer/merger/css_dependency_resolver.rb +25 -0
  25. data/lib/juicer/merger/dependency_resolver.rb +82 -0
  26. data/lib/juicer/merger/javascript_dependency_resolver.rb +21 -0
  27. data/lib/juicer/merger/javascript_merger.rb +30 -0
  28. data/lib/juicer/merger/stylesheet_merger.rb +112 -0
  29. data/lib/juicer/minifyer/yui_compressor.rb +129 -0
  30. data/tasks/ann.rake +80 -0
  31. data/tasks/bones.rake +20 -0
  32. data/tasks/gem.rake +201 -0
  33. data/tasks/git.rake +40 -0
  34. data/tasks/notes.rake +27 -0
  35. data/tasks/post_load.rake +34 -0
  36. data/tasks/rdoc.rake +50 -0
  37. data/tasks/rubyforge.rake +55 -0
  38. data/tasks/setup.rb +300 -0
  39. data/tasks/spec.rake +54 -0
  40. data/tasks/svn.rake +47 -0
  41. data/tasks/test.rake +40 -0
  42. data/tasks/test/setup.rake +35 -0
  43. data/test/bin/jslint.js +474 -0
  44. data/test/bin/rhino1_7R1.zip +0 -0
  45. data/test/bin/rhino1_7R2-RC1.zip +0 -0
  46. data/test/bin/yuicompressor +238 -0
  47. data/test/bin/yuicompressor-2.3.5.zip +0 -0
  48. data/test/bin/yuicompressor-2.4.2.zip +0 -0
  49. data/test/juicer/command/test_install.rb +53 -0
  50. data/test/juicer/command/test_list.rb +69 -0
  51. data/test/juicer/command/test_merge.rb +155 -0
  52. data/test/juicer/command/test_util.rb +54 -0
  53. data/test/juicer/command/test_verify.rb +33 -0
  54. data/test/juicer/install/test_installer_base.rb +195 -0
  55. data/test/juicer/install/test_jslint_installer.rb +54 -0
  56. data/test/juicer/install/test_rhino_installer.rb +57 -0
  57. data/test/juicer/install/test_yui_compressor_installer.rb +56 -0
  58. data/test/juicer/merger/test_base.rb +122 -0
  59. data/test/juicer/merger/test_css_dependency_resolver.rb +36 -0
  60. data/test/juicer/merger/test_javascript_dependency_resolver.rb +39 -0
  61. data/test/juicer/merger/test_javascript_merger.rb +74 -0
  62. data/test/juicer/merger/test_stylesheet_merger.rb +178 -0
  63. data/test/juicer/minifyer/test_yui_compressor.rb +159 -0
  64. data/test/juicer/test_cache_buster.rb +58 -0
  65. data/test/juicer/test_chainable.rb +94 -0
  66. data/test/juicer/test_core.rb +47 -0
  67. data/test/juicer/test_css_cache_buster.rb +72 -0
  68. data/test/juicer/test_jslint.rb +33 -0
  69. data/test/test_helper.rb +146 -0
  70. data/test/test_juicer.rb +4 -0
  71. metadata +194 -0
@@ -0,0 +1,32 @@
1
+ require "pathname"
2
+
3
+ module Juicer
4
+ module Command
5
+ # Utilities for Juicer command objects
6
+ #
7
+ module Util
8
+ # Returns an array of files from a variety of input. Input may be a single
9
+ # file, a single glob pattern or multiple files and/or patterns. It may
10
+ # even be an array of mixed input.
11
+ #
12
+ def files(*args)
13
+ args.flatten.collect { |file| Dir.glob(file) }.flatten
14
+ end
15
+
16
+ #
17
+ # Uses Pathname to calculate the shortest relative path from +path+ to
18
+ # +reference_path+ (default is +Dir.cwd+)
19
+ #
20
+ def relative(paths, reference_path = Dir.pwd)
21
+ paths = [paths].flatten.collect do |path|
22
+ path = Pathname.new(File.expand_path(path))
23
+ reference_path = Pathname.new(File.expand_path(reference_path))
24
+ path.relative_path_from(reference_path).to_s
25
+ end
26
+
27
+ paths.length == 1 ? paths.first : paths
28
+ end
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,60 @@
1
+ require File.join(File.dirname(__FILE__), "util")
2
+ require "rubygems"
3
+ require "cmdparse"
4
+ require "pathname"
5
+
6
+ module Juicer
7
+ module Command
8
+ # Verifies problem-free-ness of source code (JavaScript and CSS)
9
+ #
10
+ class Verify < CmdParse::Command
11
+ include Juicer::Command::Util
12
+
13
+ # Initializes command
14
+ #
15
+ def initialize(log = nil)
16
+ super('verify', false, true)
17
+ @log = log || Logger.new($STDIO)
18
+ self.short_desc = "Verifies that the given JavaScript/CSS file is problem free"
19
+ self.description = <<-EOF
20
+ Uses JsLint (http://www.jslint.com) to check that code adheres to good coding
21
+ practices to avoid potential bugs, and protect against introducing bugs by
22
+ minifying.
23
+ EOF
24
+ end
25
+
26
+ # Execute command
27
+ #
28
+ def execute(args)
29
+ # Need atleast one file
30
+ raise ArgumentError.new('Please provide atleast one input file/pattern') if args.length == 0
31
+ Juicer::Command::Verify.check_all(files(args), @log)
32
+ end
33
+
34
+ def self.check_all(files, log = nil)
35
+ log ||= Logger.new($stdio)
36
+ jslint = Juicer::JsLint.new(:bin_path => Juicer.home)
37
+ problems = false
38
+
39
+ # Check that JsLint is installed
40
+ raise FileNotFoundError.new("Missing 3rd party library JsLint, install with\njuicer install jslint") if jslint.locate_lib.nil?
41
+
42
+ # Verify all files
43
+ files.each do |file|
44
+ log.info "Verifying #{file} with JsLint"
45
+ report = jslint.check(file)
46
+
47
+ if report.ok?
48
+ log.info " OK!"
49
+ else
50
+ problems = true
51
+ log.warn " Problems detected"
52
+ log.warn " #{report.errors.join("\n").gsub(/\n/, "\n ")}\n"
53
+ end
54
+ end
55
+
56
+ !problems
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,59 @@
1
+ #
2
+ # Additions to core Ruby objects
3
+ #
4
+
5
+ class String
6
+ #
7
+ # Turn an underscored string into camel case, ie this_becomes -> ThisBecomes
8
+ #
9
+ def camel_case
10
+ self.split("_").inject("") { |str, piece| str + piece.capitalize }
11
+ end
12
+
13
+ #
14
+ # Treat a string as a class name and return the class. Optionally provide a
15
+ # module to look up the class in.
16
+ #
17
+ def to_class(mod = nil)
18
+ res = "#{mod}::#{self}".sub(/^::/, "").split("::").inject(Object) do |mod, obj|
19
+ raise "No such class/module" unless mod.const_defined?(obj)
20
+ mod = mod.const_get(obj)
21
+ end
22
+ end
23
+
24
+ #
25
+ # Turn a string in either underscore or camel case form into a class directly
26
+ #
27
+ def classify(mod = nil)
28
+ self.camel_case.to_class(mod)
29
+ end
30
+
31
+ #
32
+ # Turn a camelcase string into underscore string
33
+ #
34
+ def underscore
35
+ self.split(/([A-Z][^A-Z]*)/).find_all { |str| str != "" }.join("_").downcase
36
+ end
37
+ end
38
+
39
+ class Symbol
40
+ #
41
+ # Converts symbol to string and calls String#camel_case
42
+ #
43
+ def camel_case
44
+ self.to_s.camel_case
45
+ end
46
+
47
+ #
48
+ # Converts symbol to string and calls String#classify
49
+ #
50
+ def classify(mod = nil)
51
+ self.to_s.classify(mod)
52
+ end
53
+ end
54
+
55
+ class Logger
56
+ def format_message(severity, datetime, progname, msg)
57
+ "#{msg}\n"
58
+ end
59
+ end
@@ -0,0 +1,99 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "chainable"))
2
+ require File.expand_path(File.join(File.dirname(__FILE__), "cache_buster"))
3
+
4
+ module Juicer
5
+ #
6
+ # The CssCacheBuster is a tool that can parse a CSS file and substitute all
7
+ # referenced URLs by a URL appended with a timestamp denoting it's last change.
8
+ # This causes the URLs to be unique every time they've been modified, thus
9
+ # facilitating using a far future expires header on your web server.
10
+ #
11
+ # See Juicer::CacheBuster for more information on how the cache buster URLs
12
+ # work.
13
+ #
14
+ # When dealing with CSS files that reference absolute URLs like /images/1.png
15
+ # you must specify the :web_root option that these URLs should be resolved
16
+ # against.
17
+ #
18
+ # When dealing with full URLs (ie including hosts) you can optionally specify
19
+ # an array of hosts to recognize as "local", meaning they serve assets from
20
+ # the :web_root directory. This way even asset host cycling can benefit from
21
+ # cache busters.
22
+ #
23
+ class CssCacheBuster
24
+ include Juicer::Chainable
25
+
26
+ def initialize(options = {})
27
+ @web_root = options[:web_root]
28
+ @web_root.sub!(%r{/?$}, "") if @web_root # Remove trailing slash
29
+ @type = options[:type] || :soft
30
+ @hosts = (options[:hosts] || []).collect { |h| h.sub!(%r{/?$}, "") } # Remove trailing slashes
31
+ @contents = nil
32
+ end
33
+
34
+ #
35
+ # Update file. If no +output+ is provided, the input file is overwritten
36
+ #
37
+ def save(file, output = nil)
38
+ @contents = File.read(file)
39
+
40
+ urls(file).each do |url|
41
+ path = resolve(url, file)
42
+
43
+ if path != url
44
+ basename = File.basename(Juicer::CacheBuster.path(path))
45
+ @contents.sub!(url, File.join(File.dirname(url), basename))
46
+ end
47
+ end
48
+
49
+ File.open(output || file, "w") { |f| f.puts @contents }
50
+ @contents = nil
51
+ end
52
+
53
+ chain_method :save
54
+
55
+ #
56
+ # Returns all referenced URLs in +file+. Returned paths are absolute (ie,
57
+ # they're resolved relative to the +file+ path.
58
+ #
59
+ def urls(file)
60
+ @contents = File.read(file) unless @contents
61
+
62
+ @contents.scan(/url\(([^\)]*)\)/m).collect do |match|
63
+ match.first
64
+ end
65
+ end
66
+
67
+ #
68
+ # Resolve full path from URL
69
+ #
70
+ def resolve(target, from)
71
+ # If URL is external, check known hosts to see if URL can be treated
72
+ # like a local one (ie so we can add cache buster)
73
+ catch(:continue) do
74
+ if target =~ %r{^[a-z]+\://}
75
+ # This could've been a one-liner, but I prefer to be
76
+ # able to read my own code ;)
77
+ @hosts.each do |host|
78
+ if target =~ /^#{host}/
79
+ target.sub!(/^#{host}/, "")
80
+ throw :continue
81
+ end
82
+ end
83
+
84
+ # No known hosts matched, return
85
+ return target
86
+ end
87
+ end
88
+
89
+ # Simply add web root to absolute URLs
90
+ if target =~ %r{^/}
91
+ raise FileNotFoundError.new("Unable to resolve absolute path #{target} without :web_root option") unless @web_root
92
+ return File.expand_path(File.join(@web_root, target))
93
+ end
94
+
95
+ # Resolve relative URLs to full paths
96
+ File.expand_path(File.join(File.dirname(File.expand_path(from)), target))
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,186 @@
1
+ require 'hpricot'
2
+ require 'open-uri'
3
+ require 'fileutils'
4
+ require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. juicer])) unless defined?(Juicer)
5
+
6
+ module Juicer
7
+ module Install
8
+ #
9
+ # Installer skeleton. Provides basic functionality like figuring out where
10
+ # to install, create base directories, remove unneeded directories and more
11
+ # housekeeping.
12
+ #
13
+ class Base
14
+ attr_reader :install_dir
15
+
16
+ #
17
+ # Create new installer
18
+ #
19
+ def initialize(install_dir = Juicer.home)
20
+ @install_dir = install_dir
21
+ @path = nil
22
+ @bin_path = nil
23
+ @name = nil
24
+ @dependencies = {}
25
+ end
26
+
27
+ #
28
+ # Returns the latest available version number. Must be implemented in
29
+ # subclasses. Raises an exception when called directly.
30
+ #
31
+ def latest
32
+ raise NotImplementedError.new("Implement in subclasses")
33
+ end
34
+
35
+ # Returns the path relative to installation path this installer will
36
+ # install to
37
+ def path
38
+ return @path if @path
39
+ @path = "lib/" + self.class.to_s.split("::").pop.sub(/Installer$/, "").underscore
40
+ end
41
+
42
+ # Returns the path to search for binaries from
43
+ #
44
+ def bin_path
45
+ return @bin_path if @bin_path
46
+ @bin_path = File.join(path, "bin")
47
+ end
48
+
49
+ #
50
+ # Returns name of component. Default implementation returns class name
51
+ # with "Installer" removed
52
+ #
53
+ def name
54
+ return @name if @name
55
+ @name = File.basename(path).split("_").inject("") { |str, word| (str + " #{word.capitalize}").strip }
56
+ end
57
+
58
+ #
59
+ # Checks if the component is currently installed.
60
+ #
61
+ # If no version is provided the most recent version is assumed.
62
+ #
63
+ def installed?(version = nil)
64
+ installed = File.exists?(File.join(@install_dir, path, "#{version || latest}"))
65
+ deps = @dependencies.length == 0 || dependencies.all? { |d, v| d.installed?(v) }
66
+ installed && deps
67
+ end
68
+
69
+ #
70
+ # Install the component. Creates basic directory structure.
71
+ #
72
+ def install(version = nil)
73
+ raise "#{name} #{version} is already installed in #{File.join(@install_dir, path)}" if installed?(version)
74
+ version ||= latest
75
+ log "Installing #{name} #{version} in #{File.join(@install_dir, path)}"
76
+
77
+ if @dependencies.length > 0
78
+ log "Installing dependencies"
79
+ dependencies { |dependency, ver| dependency.install(ver) unless dependency.installed?(ver) }
80
+ end
81
+
82
+ # Create directories
83
+ FileUtils.mkdir_p(File.join(@install_dir, path, "bin"))
84
+ FileUtils.mkdir_p(File.join(@install_dir, path, version))
85
+
86
+ # Return resolved version for subclass to use
87
+ version
88
+ end
89
+
90
+ #
91
+ # Uninstalls the given version of the component.
92
+ #
93
+ # If no version is provided the most recent version is assumed.
94
+ #
95
+ # If there are no more files left in INSTALLATION_PATH/<path>, the
96
+ # whole directory is removed.
97
+ #
98
+ # This method takes a block and can be used from subclasses like so:
99
+ #
100
+ # def self.uninstall(install_dir = nil, version = nil)
101
+ # super do |home_dir, version|
102
+ # # Custom uninstall logic
103
+ # end
104
+ # end
105
+ #
106
+ #
107
+ def uninstall(version = nil)
108
+ version ||= self.latest
109
+ install_dir = File.join(@install_dir, path, version)
110
+ raise "#{name} #{version} is not installed" if !File.exists?(install_dir)
111
+
112
+ FileUtils.rm_rf(install_dir)
113
+
114
+ yield(File.join(@install_dir, path), version) if block_given?
115
+
116
+ files = Dir.glob(File.join(@install_dir, path, "**", "*")).find_all { |f| File.file?(f) }
117
+ FileUtils.rm_rf(File.join(@install_dir, path)) if files.length == 0
118
+ end
119
+
120
+ #
121
+ # Download a file to Juicer temporary directory. The file will be kept
122
+ # until #purge is called to wipe it. If the installer receives a request
123
+ # to download the same file again, the disk cache will be used unless the
124
+ # force argument is true (default false)
125
+ #
126
+ def download(url, force = false)
127
+ filename = File.join(@install_dir, "download", path.sub("lib/", ""), File.basename(url))
128
+ return filename if File.exists?(filename) && !force
129
+ FileUtils.mkdir_p(File.dirname(filename))
130
+ File.delete(filename) if File.exists?(filename) && force
131
+
132
+ log "Downloading #{url}"
133
+ File.open(filename, "wb") do |file|
134
+ webpage = open(url)
135
+ file.write(webpage.read)
136
+ webpage.close
137
+ end
138
+
139
+ filename
140
+ end
141
+
142
+ #
143
+ # Display a message to the user through Juicer::LOGGER
144
+ #
145
+ def log(str)
146
+ Juicer::LOGGER.info str
147
+ end
148
+
149
+ #
150
+ # Add a dependency. Dependency should be a Juicer::Install::Base installer
151
+ # class (not instance) OR a symbol/string like :rhino/"rhino" (which will
152
+ # be expanded unto Juicer::Install::RhinoInstaller). Version is optional
153
+ # and defaults to latest and greatest.
154
+ #
155
+ def dependency(dependency, version = nil)
156
+ dependency = Juicer::Install.get(dependency) if [String, Symbol].include?(dependency.class)
157
+
158
+ @dependencies[dependency.to_s + (version || "")] = [dependency, version]
159
+ end
160
+
161
+ #
162
+ # Yields depencies one at a time: class and version and returns an array
163
+ # of arrays: [dependency, version] where dependency is an instance and
164
+ # version a string.
165
+ #
166
+ def dependencies(&block)
167
+ @dependencies.collect do |name, dependency|
168
+ version = dependency[1]
169
+ dependency = dependency[0].new(@install_dir)
170
+ block.call(dependency, version) if block
171
+ [dependency, version]
172
+ end
173
+ end
174
+ end
175
+
176
+ #
177
+ # Returns the installer. Accepts installer classes (which are returned
178
+ # directly), strings or symbols. Strings and symbols may be on the form
179
+ # :my_module which is expanded to Juicer::Install::MyModuleInstaller
180
+ #
181
+ def self.get(nameOrClass)
182
+ return nameOrClass if nameOrClass.is_a? Class
183
+ (nameOrClass.to_s + "_installer").classify(Juicer::Install)
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,51 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. juicer])) unless defined?(Juicer)
2
+ require File.expand_path(File.join(File.dirname(__FILE__), "base"))
3
+ require "zip/zip"
4
+
5
+ module Juicer
6
+ module Install
7
+ #
8
+ # Install and uninstall routines for the JSLint library by Douglas Crockford.
9
+ # Installation downloads the jslintfull.js and rhino.js files and stores
10
+ # them in the Juicer installation directory.
11
+ #
12
+ class JSLintInstaller < Base
13
+ attr_reader :latest
14
+
15
+ def initialize(install_dir = Juicer.home)
16
+ super(install_dir)
17
+ @latest = "1.0"
18
+ @website = "http://www.jslint.com/"
19
+ @path = "lib/jslint"
20
+ @name = "JsLint"
21
+ dependency :rhino
22
+ end
23
+
24
+ #
25
+ # Install JSLint. Downloads the two js files and stores them in the
26
+ # installation directory.
27
+ #
28
+ def install(version = nil)
29
+ version = super(version)
30
+ filename = download(File.join(@website, "rhino/jslint.js"))
31
+ File.copy(filename, File.join(@install_dir, path, "bin", "jslint-#{version}.js"))
32
+ end
33
+
34
+ #
35
+ # Uninstalls JSLint
36
+ #
37
+ def uninstall(version = nil)
38
+ super(version) do |dir, version|
39
+ File.delete(File.join(dir, "bin", "jslint-#{version}.js"))
40
+ end
41
+ end
42
+ end
43
+
44
+ #
45
+ # This class makes it possible to do Juicer.install("jslint") instead of
46
+ # Juicer.install("j_s_lint"). Sugar, sugar...
47
+ #
48
+ class JslintInstaller < JSLintInstaller
49
+ end
50
+ end
51
+ end