juicer 0.2.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.
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