cjohansen-juicer 0.2.0 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/History.txt +17 -5
  2. data/Manifest.txt +33 -15
  3. data/Rakefile +22 -1
  4. data/Readme.rdoc +68 -32
  5. data/bin/juicer +1 -0
  6. data/lib/juicer.rb +26 -1
  7. data/lib/juicer/binary.rb +173 -0
  8. data/lib/juicer/cache_buster.rb +45 -0
  9. data/lib/juicer/chainable.rb +1 -0
  10. data/lib/juicer/cli.rb +13 -8
  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 +130 -31
  14. data/lib/juicer/command/util.rb +32 -0
  15. data/lib/juicer/command/verify.rb +60 -0
  16. data/lib/juicer/core.rb +61 -0
  17. data/lib/juicer/css_cache_buster.rb +106 -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 -72
  24. data/lib/juicer/merger/dependency_resolver.rb +34 -16
  25. data/lib/juicer/merger/stylesheet_merger.rb +71 -1
  26. data/lib/juicer/minifyer/yui_compressor.rb +20 -43
  27. data/tasks/test/setup.rake +35 -0
  28. data/test/juicer/command/test_install.rb +53 -0
  29. data/test/juicer/command/test_list.rb +69 -0
  30. data/test/juicer/command/test_merge.rb +160 -0
  31. data/test/juicer/command/test_util.rb +54 -0
  32. data/test/juicer/command/test_verify.rb +33 -0
  33. data/test/juicer/install/test_installer_base.rb +195 -0
  34. data/test/juicer/install/test_jslint_installer.rb +54 -0
  35. data/test/juicer/install/test_rhino_installer.rb +57 -0
  36. data/test/juicer/install/test_yui_compressor_installer.rb +56 -0
  37. data/test/juicer/merger/test_base.rb +2 -3
  38. data/test/juicer/merger/test_css_dependency_resolver.rb +8 -4
  39. data/test/juicer/merger/test_javascript_dependency_resolver.rb +6 -7
  40. data/test/juicer/merger/test_javascript_merger.rb +1 -2
  41. data/test/juicer/merger/test_stylesheet_merger.rb +118 -2
  42. data/test/juicer/minifyer/test_yui_compressor.rb +109 -29
  43. data/test/juicer/test_cache_buster.rb +58 -0
  44. data/test/juicer/test_chainable.rb +7 -0
  45. data/test/juicer/test_core.rb +47 -0
  46. data/test/juicer/test_css_cache_buster.rb +91 -0
  47. data/test/juicer/test_jslint.rb +33 -0
  48. data/test/test_helper.rb +65 -196
  49. metadata +77 -26
  50. data/.gitignore +0 -2
  51. data/juicer.gemspec +0 -38
  52. data/lib/juicer/minifyer/compressor.rb +0 -125
  53. data/test/juicer/minifyer/test_compressor.rb +0 -36
@@ -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
@@ -1,72 +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
- @dependency_resolver ||= nil
14
- self.append files
15
- end
16
-
17
- #
18
- # Append contents to output. Resolves dependencies and adds
19
- # required files recursively
20
- # file = A file to add to merged content
21
- #
22
- def append(file)
23
- return file.each { |f| self << f } if file.class == Array
24
- return if @files.include?(file)
25
-
26
- if !@dependency_resolver.nil?
27
- path = File.expand_path(file)
28
- resolve_dependencies(path)
29
- elsif !@files.include?(file)
30
- @files << file
31
- end
32
- end
33
-
34
- alias_method :<<, :append
35
-
36
- #
37
- # Save the merged contents. If a filename is given the new file is
38
- # written. If a stream is provided, contents are written to it.
39
- #
40
- def save(file_or_stream)
41
- output = file_or_stream
42
- output = File.open(output, 'w') if output.is_a? String
43
-
44
- @files.each { |f| output.puts(merge(f)) }
45
- output.close if file_or_stream.is_a? String
46
- end
47
-
48
- chain_method :save
49
-
50
- private
51
- def resolve_dependencies(file)
52
- @dependency_resolver.resolve(file) do |f|
53
- if @files.include?(f)
54
- false
55
- else
56
- @files << f
57
- resolve_dependencies(f)
58
- true
59
- end
60
- end
61
-
62
- @files
63
- end
64
-
65
- # Fetch contents of a single file. May be overridden in subclasses to provide
66
- # custom content filtering
67
- def merge(file)
68
- IO.read(file) + "\n"
69
- end
70
- end
71
- end
72
- end
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
@@ -16,23 +16,9 @@ module Juicer
16
16
  # if the block is true for the given file. Without a block every found
17
17
  # file is returned.
18
18
  #
19
- def resolve(file)
20
- imported_file = nil
19
+ def resolve(file, &block)
21
20
  @files = []
22
-
23
- catch(:done) do
24
- IO.foreach(file) do |line|
25
- imported_file = parse(line, imported_file)
26
-
27
- if imported_file
28
- imported_file = resolve_path(imported_file, file)
29
- @files << imported_file if !block_given? || yield(imported_file)
30
- end
31
- end
32
- end
33
-
34
- file = File.expand_path(file)
35
- @files << file if !block_given? || yield(file)
21
+ _resolve(file, &block)
36
22
  end
37
23
 
38
24
  #
@@ -59,6 +45,38 @@ module Juicer
59
45
  def parse(line)
60
46
  raise NotImplementedError.new
61
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
62
80
  end
63
81
  end
64
82
  end
@@ -20,12 +20,82 @@ module Juicer
20
20
  #
21
21
  def initialize(files = [], options = {})
22
22
  @dependency_resolver = CssDependencyResolver.new(options)
23
- super(files, options)
23
+ super(files || [], options)
24
+ @hosts = options[:hosts] || []
25
+ @host_num = 0
26
+ @use_absolute = options.key?(:absolute_urls) ? options[:absolute_urls] : false
27
+ @use_relative = options.key?(:relative_urls) ? options[:relative_urls] : false
28
+ @web_root = options[:web_root]
29
+ @web_root = File.expand_path(@web_root).sub(/\/?$/, "") if @web_root # Make sure path doesn't end in a /
24
30
  end
25
31
 
26
32
  private
33
+ #
34
+ # Takes care of removing any import statements. This avoids importing the
35
+ # file that was just merged into the current file.
36
+ #
37
+ # +merge+ also recalculates any referenced URLs. Relative URLs are adjusted
38
+ # to be relative to the resulting merged file. Absolute URLs are left alone
39
+ # by default. If the :hosts option is set, the absolute URLs will cycle
40
+ # through these. This may help in concurrent downloads.
41
+ #
42
+ # The options hash decides how Juicer recalculates referenced URLs:
43
+ #
44
+ # options[:absolute_urls] When true, all paths are converted to absolute
45
+ # URLs. Requires options[:web_root] to define
46
+ # root directory to resolve absolute URLs from.
47
+ # options[:relative_urls] When true, all paths are converted to relative
48
+ # paths. Requires options[:web_root] to define
49
+ # root directory to resolve absolute URLs from.
50
+ #
51
+ # If none if these are set then relative URLs are recalculated to match
52
+ # location of merged target while absolute URLs are left absolute.
53
+ #
54
+ # If options[:hosts] is set to an array of hosts, then they will be cycled
55
+ # for all absolute URLs regardless of absolute/relative URL strategy.
56
+ #
27
57
  def merge(file)
28
58
  content = super.gsub(/^\s*\@import\s("|')(.*)("|')\;?/, '')
59
+ dir = File.expand_path(File.dirname(file))
60
+
61
+ content.scan(/url\(([^\)]*)\)/).uniq.collect do |url|
62
+ url = url.first
63
+ path = resolve_path(url, dir)
64
+ content.gsub!(/\(#{url}\)/m, "(#{path})") unless path == url
65
+ end
66
+
67
+ content
68
+ end
69
+
70
+ #
71
+ # Resolves a path relative to a directory
72
+ #
73
+ def resolve_path(url, dir)
74
+ path = url
75
+
76
+ # Absolute URLs
77
+ if url =~ %r{^/} && @use_relative
78
+ raise ArgumentError.new("Unable to handle absolute URLs without :web_root option") if !@web_root
79
+ path = Pathname.new(File.join(@web_root, url)).relative_path_from(@root).to_s
80
+ end
81
+
82
+ # All URLs that don't start with a protocol
83
+ if url !~ %r{^/} && url !~ %r{^[a-z]+://}
84
+ if @use_absolute
85
+ raise ArgumentError.new("Unable to handle absolute URLs without :web_root option") if !@web_root
86
+ path = File.expand_path(File.join(dir, url)).sub(@web_root, "") # Make absolute
87
+ else
88
+ path = Pathname.new(File.join(dir, url)).relative_path_from(@root).to_s # ...or redefine relative ref
89
+ end
90
+ end
91
+
92
+ # Cycle hosts, if any
93
+ if url =~ %r{^/} && @hosts.length > 0
94
+ path = File.join(@hosts[@host_num % @hosts.length], url)
95
+ @host_num += 1
96
+ end
97
+
98
+ path
29
99
  end
30
100
  end
31
101
  end