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,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