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.
- data/History.txt +17 -5
- data/Manifest.txt +33 -15
- data/Rakefile +22 -1
- data/Readme.rdoc +68 -32
- data/bin/juicer +1 -0
- data/lib/juicer.rb +26 -1
- data/lib/juicer/binary.rb +173 -0
- data/lib/juicer/cache_buster.rb +45 -0
- data/lib/juicer/chainable.rb +1 -0
- data/lib/juicer/cli.rb +13 -8
- data/lib/juicer/command/install.rb +59 -0
- data/lib/juicer/command/list.rb +50 -0
- data/lib/juicer/command/merge.rb +130 -31
- data/lib/juicer/command/util.rb +32 -0
- data/lib/juicer/command/verify.rb +60 -0
- data/lib/juicer/core.rb +61 -0
- data/lib/juicer/css_cache_buster.rb +106 -0
- data/lib/juicer/install/base.rb +186 -0
- data/lib/juicer/install/jslint_installer.rb +51 -0
- data/lib/juicer/install/rhino_installer.rb +52 -0
- data/lib/juicer/install/yui_compressor_installer.rb +66 -0
- data/lib/juicer/jslint.rb +90 -0
- data/lib/juicer/merger/base.rb +74 -72
- data/lib/juicer/merger/dependency_resolver.rb +34 -16
- data/lib/juicer/merger/stylesheet_merger.rb +71 -1
- data/lib/juicer/minifyer/yui_compressor.rb +20 -43
- data/tasks/test/setup.rake +35 -0
- data/test/juicer/command/test_install.rb +53 -0
- data/test/juicer/command/test_list.rb +69 -0
- data/test/juicer/command/test_merge.rb +160 -0
- data/test/juicer/command/test_util.rb +54 -0
- data/test/juicer/command/test_verify.rb +33 -0
- data/test/juicer/install/test_installer_base.rb +195 -0
- data/test/juicer/install/test_jslint_installer.rb +54 -0
- data/test/juicer/install/test_rhino_installer.rb +57 -0
- data/test/juicer/install/test_yui_compressor_installer.rb +56 -0
- data/test/juicer/merger/test_base.rb +2 -3
- data/test/juicer/merger/test_css_dependency_resolver.rb +8 -4
- data/test/juicer/merger/test_javascript_dependency_resolver.rb +6 -7
- data/test/juicer/merger/test_javascript_merger.rb +1 -2
- data/test/juicer/merger/test_stylesheet_merger.rb +118 -2
- data/test/juicer/minifyer/test_yui_compressor.rb +109 -29
- data/test/juicer/test_cache_buster.rb +58 -0
- data/test/juicer/test_chainable.rb +7 -0
- data/test/juicer/test_core.rb +47 -0
- data/test/juicer/test_css_cache_buster.rb +91 -0
- data/test/juicer/test_jslint.rb +33 -0
- data/test/test_helper.rb +65 -196
- metadata +77 -26
- data/.gitignore +0 -2
- data/juicer.gemspec +0 -38
- data/lib/juicer/minifyer/compressor.rb +0 -125
- 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
|
data/lib/juicer/merger/base.rb
CHANGED
@@ -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
|
-
@
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
if
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
#
|
39
|
-
#
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
@
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|