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,52 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. juicer])) unless defined?(Juicer)
2
+ require "zip/zip"
3
+
4
+ module Juicer
5
+ module Install
6
+ #
7
+ # Install and uninstall routines for the Mozilla Rhino jar.
8
+ #
9
+ class RhinoInstaller < Base
10
+ attr_reader :latest
11
+
12
+ def initialize(install_dir = Juicer.home)
13
+ super(install_dir)
14
+ @latest = "1_7R2-RC1"
15
+ @website = "ftp://ftp.mozilla.org/pub/mozilla.org/js/"
16
+ end
17
+
18
+ #
19
+ # Install Rhino. Downloads the jar file and stores it in the installation
20
+ # directory along with the License text.
21
+ #
22
+ def install(version = nil)
23
+ version = super((version || latest).gsub(/\./, "_"))
24
+ base = "rhino#{version}"
25
+ filename = download(File.join(@website, "#{base}.zip"))
26
+ target = File.join(@install_dir, path)
27
+
28
+ Zip::ZipFile.open(filename) do |file|
29
+ FileUtils.mkdir_p(File.join(target, version))
30
+
31
+ begin
32
+ file.extract("#{base.sub(/-RC\d/, "")}/LICENSE.txt", File.join(target, version, "LICENSE.txt"))
33
+ rescue Exception
34
+ # Fail silently, some releases don't carry the license
35
+ end
36
+
37
+ file.extract("#{base.sub(/-RC\d/, "")}/js.jar", File.join(target, "bin", "#{base}.jar"))
38
+ end
39
+ end
40
+
41
+ #
42
+ # Uninstalls Rhino
43
+ #
44
+ def uninstall(version = nil)
45
+ super((version || latest).gsub(/\./, "_")) do |dir, version|
46
+ base = "rhino#{version}"
47
+ File.delete(File.join(dir, "bin/", "#{base}.jar"))
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,66 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. juicer])) unless defined?(Juicer)
2
+ require "zip/zip"
3
+
4
+ module Juicer
5
+ module Install
6
+ #
7
+ # Install and uninstall routines for the YUI Compressor.
8
+ # Installation downloads the YUI Compressor distribution, unzips it and
9
+ # storesthe jar file on disk along with the license.
10
+ #
11
+ class YuiCompressorInstaller < Base
12
+ def initialize(install_dir = Juicer.home)
13
+ super(install_dir)
14
+ @latest = nil
15
+ @website = "http://www.julienlecomte.net/yuicompressor/"
16
+ end
17
+
18
+ #
19
+ # Install the Yui Compressor. Downloads the distribution and keeps the jar
20
+ # file inside PATH/yui_compressor/bin and the README and CHANGELOG in
21
+ # PATH/yui_compressor/x.y.z/ where x.y.z is the version, most recent if
22
+ # not specified otherwise.
23
+ #
24
+ # Path defaults to environment variable $JUICER_HOME or default Juicer
25
+ # home
26
+ #
27
+ def install(version = nil)
28
+ version = super(version)
29
+ base = "yuicompressor-#{version}"
30
+ filename = download(File.join(@website, "#{base}.zip"))
31
+ target = File.join(@install_dir, path)
32
+
33
+ Zip::ZipFile.open(filename) do |file|
34
+ file.extract("#{base}/doc/README", File.join(target, version, "README"))
35
+ file.extract("#{base}/doc/CHANGELOG", File.join(target, version, "CHANGELOG"))
36
+ file.extract("#{base}/build/#{base}.jar", File.join(target, "bin", "#{base}.jar"))
37
+ end
38
+ end
39
+
40
+ #
41
+ # Uninstalls the given version of YUI Compressor. If no location is
42
+ # provided the environment variable $JUICER_HOME or Juicers default home
43
+ # directory is used.
44
+ #
45
+ # If no version is provided the most recent version is assumed.
46
+ #
47
+ # If there are no more files left in INSTALLATION_PATH/yui_compressor, the
48
+ # whole directory is removed.
49
+ #
50
+ def uninstall(version = nil)
51
+ super(version) do |dir, version|
52
+ File.delete(File.join(dir, "bin/yuicompressor-#{version}.jar"))
53
+ end
54
+ end
55
+
56
+ #
57
+ # Check which version is the most recent
58
+ #
59
+ def latest
60
+ return @latest if @latest
61
+ webpage = Hpricot(open(@website))
62
+ @latest = (webpage / "#downloadbutton a")[0].get_attribute("href").match(/(\d\.\d\.\d)/)[1]
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,90 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "binary"))
2
+
3
+ module Juicer
4
+ #
5
+ # A Ruby API to Douglas Crockfords genious JsLint program
6
+ # http://www.jslint.com/
7
+ #
8
+ # JsLint parses JavaScript code and identifies (potential) problems.
9
+ # Effectively, JsLint defines a subset of JavaScript which is safe to use, and
10
+ # among other things make code minification a substantially less dangerous
11
+ # task.
12
+ #
13
+ class JsLint
14
+ include Juicer::Binary
15
+
16
+ def initialize(options = {})
17
+ super(options[:java] || "java")
18
+ path << options[:bin_path] if options[:bin_path]
19
+ end
20
+
21
+ #
22
+ # Checks if a files has problems. Also includes experimental support for CSS
23
+ # files. CSS files should begin with the line @charset "UTF-8";
24
+ #
25
+ # Returns a Juicer::JsLint::Report object
26
+ #
27
+ def check(file)
28
+ rhino_jar = rhino
29
+ js_file = locate_lib
30
+
31
+ raise FileNotFoundError.new("Unable to locate Rhino jar '#{rhino_jar}'") if !rhino_jar || !File.exists?(rhino_jar)
32
+ raise FileNotFoundError.new("Unable to locate JsLint '#{js_file}'") if !js_file || !File.exists?(js_file)
33
+ raise FileNotFoundError.new("Unable to locate input file '#{file}'") unless File.exists?(file)
34
+
35
+ lines = execute(%Q{-jar #{rhino} #{locate_lib} "#{file}"}).split("\n")
36
+ return Report.new if lines.length == 1 && lines[0] =~ /jslint: No problems/
37
+
38
+ report = Report.new
39
+ lines = lines.reject { |line| !line || "#{line}".strip == "" }
40
+ report.add_error(lines.shift, lines.shift) while lines.length > 0
41
+
42
+ return report
43
+ end
44
+
45
+ def rhino
46
+ files = locate("**/rhino*.jar", "RHINO_HOME")
47
+ !files || files.empty? ? nil : files.sort.last
48
+ end
49
+
50
+ def locate_lib
51
+ files = locate("**/jslint-*.js", "JSLINT_HOME")
52
+ !files || files.empty? ? nil : files.sort.last
53
+ end
54
+
55
+ #
56
+ # Represents the results of a JsLint run
57
+ #
58
+ class Report
59
+ attr_accessor :errors
60
+
61
+ def initialize(errors = [])
62
+ @errors = errors
63
+ end
64
+
65
+ def add_error(message, code)
66
+ @errors << JsLint::Error.new(message, code)
67
+ end
68
+
69
+ def ok?
70
+ @errors.nil? || @errors.length == 0
71
+ end
72
+ end
73
+
74
+ #
75
+ # A JsLint error
76
+ #
77
+ class Error
78
+ attr_accessor :message, :code
79
+
80
+ def initialize(message, code)
81
+ @message = message
82
+ @code = code
83
+ end
84
+
85
+ def to_s
86
+ "#@message\n#@code"
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,74 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "chainable"))
2
+
3
+ # Merge several files into one single output file
4
+ module Juicer
5
+ module Merger
6
+ class Base
7
+ include Chainable
8
+ attr_accessor :dependency_resolver
9
+ attr_reader :files
10
+
11
+ def initialize(files = [], options = {})
12
+ @files = []
13
+ @root = nil
14
+ @options = options
15
+ @dependency_resolver ||= nil
16
+ self.append files
17
+ end
18
+
19
+ #
20
+ # Append contents to output. Resolves dependencies and adds
21
+ # required files recursively
22
+ # file = A file to add to merged content
23
+ #
24
+ def append(file)
25
+ return file.each { |f| self << f } if file.class == Array
26
+ return if @files.include?(file)
27
+
28
+ if !@dependency_resolver.nil?
29
+ path = File.expand_path(file)
30
+ resolve_dependencies(path)
31
+ elsif !@files.include?(file)
32
+ @files << file
33
+ end
34
+ end
35
+
36
+ alias_method :<<, :append
37
+
38
+ #
39
+ # Save the merged contents. If a filename is given the new file is
40
+ # written. If a stream is provided, contents are written to it.
41
+ #
42
+ def save(file_or_stream)
43
+ output = file_or_stream
44
+
45
+ if output.is_a? String
46
+ @root = Pathname.new(File.dirname(File.expand_path(output)))
47
+ output = File.open(output, 'w')
48
+ else
49
+ @root = Pathname.new(File.expand_path("."))
50
+ end
51
+
52
+ @files.each do |f|
53
+ output.puts(merge(f))
54
+ end
55
+
56
+ output.close if file_or_stream.is_a? String
57
+ end
58
+
59
+ chain_method :save
60
+
61
+ private
62
+ def resolve_dependencies(file)
63
+ @files.concat @dependency_resolver.resolve(file)
64
+ @files.uniq!
65
+ end
66
+
67
+ # Fetch contents of a single file. May be overridden in subclasses to provide
68
+ # custom content filtering
69
+ def merge(file)
70
+ IO.read(file) + "\n"
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,25 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "dependency_resolver"))
2
+
3
+ module Juicer
4
+ module Merger
5
+ # Resolves @import statements in CSS files and builds a list of all
6
+ # files, in order.
7
+ #
8
+ class CssDependencyResolver < DependencyResolver
9
+ # Regexp borrowed from similar project:
10
+ # http://github.com/cgriego/front-end-blender/tree/master/lib/front_end_architect/blender.rb
11
+ @@import_pattern = /^\s*@import(?: url\(| )(['"]?)([^\?'"\)\s]+)(\?(?:[^'"\)]+)?)?\1\)?(?:[^?;]+)?;?/im
12
+
13
+ private
14
+ def parse(line, imported_file = nil)
15
+ return $2 if line =~ @@import_pattern
16
+
17
+ # At first sight of actual CSS rules we abort (TODO: This does not take
18
+ # into account the fact that rules may be commented out and that more
19
+ # imports may follow)
20
+ throw :done if imported_file && line =~ %r{/*}
21
+ throw :done if line =~ /^[\.\#a-zA-Z\:]/
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,82 @@
1
+ module Juicer
2
+ module Merger
3
+ class DependencyResolver
4
+ attr_reader :files
5
+
6
+ # Constructor
7
+ def initialize(options = {})
8
+ @files = []
9
+ @options = options
10
+ end
11
+
12
+ #
13
+ # Resolve dependencies.
14
+ # This method accepts an optional block. The block will receive each
15
+ # file in succession. The file is included in the returned collection
16
+ # if the block is true for the given file. Without a block every found
17
+ # file is returned.
18
+ #
19
+ def resolve(file, &block)
20
+ @files = []
21
+ _resolve(file, &block)
22
+ end
23
+
24
+ #
25
+ # Resolves a path relative to another. If the path is absolute (ie it
26
+ # starts with a protocol or /) the <tt>:web_root</tt> options has to be
27
+ # set as well.
28
+ #
29
+ def resolve_path(path, reference)
30
+ # Absolute URL
31
+ if path =~ %r{^(/|[a-z]+:)}
32
+ if @options[:web_root].nil?
33
+ msg = "Cannot resolve absolute path '#{path}' without web root option"
34
+ raise ArgumentError.new(msg)
35
+ end
36
+
37
+ path.sub!(%r{^[a-z]+://[^/]+/}, '')
38
+ return File.expand_path(File.join(@options[:web_root], path))
39
+ end
40
+
41
+ File.expand_path(File.join(File.dirname(reference), path))
42
+ end
43
+
44
+ private
45
+ def parse(line)
46
+ raise NotImplementedError.new
47
+ end
48
+
49
+ #
50
+ # Carries out the actual work of resolve. resolve resets the internal
51
+ # file list and yields control to _resolve for rebuilding the file list.
52
+ #
53
+ def _resolve(file)
54
+ imported_file = nil
55
+
56
+ IO.foreach(file) do |line|
57
+ # Implementing subclasses may throw :done from the parse method when
58
+ # the file is exhausted for dependency declaration possibilities.
59
+ catch(:done) do
60
+ imported_file = parse(line, imported_file)
61
+
62
+ # If a dependency declaration was found
63
+ if imported_file
64
+ # Resolves a path relative to the file that imported it
65
+ imported_file = resolve_path(imported_file, file)
66
+
67
+ # Only keep processing file if it's not already included.
68
+ # Yield to block to allow caller to ignore file
69
+ if !@files.include?(imported_file) && (!block_given? || yield(imported_file))
70
+ # Check this file for imports before adding it to get order right
71
+ _resolve(imported_file) { |f| f != File.expand_path(file) }
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ file = File.expand_path(file)
78
+ @files << file if !@files.include?(file) && (!block_given? || yield(file))
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,21 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "dependency_resolver"))
2
+
3
+ module Juicer
4
+ module Merger
5
+ # Resolves @depends and @depend statements in comments in JavaScript files.
6
+ # Only the first comment in a JavaScript file is parsed
7
+ #
8
+ class JavaScriptDependencyResolver < DependencyResolver
9
+ @@depends_pattern = /\@depends?\s+([^\s\'\"\;]+)/
10
+
11
+ private
12
+ def parse(line, imported_file = nil)
13
+ return $1 if line =~ @@depends_pattern
14
+
15
+ # If we have already skimmed through some @depend/@depends or a
16
+ # closing comment we're done.
17
+ throw :done unless imported_file.nil? || !(line =~ /\*\//)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ ['base', 'javascript_dependency_resolver'].each do |lib|
3
+ require File.expand_path(File.join(File.dirname(__FILE__), lib))
4
+ end
5
+
6
+ module Juicer
7
+ module Merger
8
+ # Merge several files into one single output file. Resolves and adds in files from @depend comments
9
+ class JavaScriptMerger < Base
10
+
11
+ # Constructor
12
+ def initialize(files = [], options = {})
13
+ @dependency_resolver = JavaScriptDependencyResolver.new
14
+ super(files, options)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ # Run file from command line
21
+ # TODO: Refactor to testable Juicer::Merger::JavaScript::FileMerger.cli method
22
+ # or similar.
23
+ #
24
+ if $0 == __FILE__
25
+ return puts("Usage: javascript_merger.rb file[...] output") if $*.length < 2
26
+
27
+ fm = JavaScriptMerger.new()
28
+ fm << $*[0..-2]
29
+ fm.save($*[-1])
30
+ end