juicer 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +10 -0
- data/Manifest.txt +58 -0
- data/Rakefile +44 -0
- data/Readme.rdoc +143 -0
- data/bin/juicer +8 -0
- data/lib/juicer.rb +70 -0
- data/lib/juicer/binary.rb +173 -0
- data/lib/juicer/cache_buster.rb +45 -0
- data/lib/juicer/chainable.rb +106 -0
- data/lib/juicer/cli.rb +56 -0
- data/lib/juicer/command/install.rb +59 -0
- data/lib/juicer/command/list.rb +50 -0
- data/lib/juicer/command/merge.rb +185 -0
- data/lib/juicer/command/util.rb +32 -0
- data/lib/juicer/command/verify.rb +60 -0
- data/lib/juicer/core.rb +59 -0
- data/lib/juicer/css_cache_buster.rb +99 -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 -0
- data/lib/juicer/merger/css_dependency_resolver.rb +25 -0
- data/lib/juicer/merger/dependency_resolver.rb +82 -0
- data/lib/juicer/merger/javascript_dependency_resolver.rb +21 -0
- data/lib/juicer/merger/javascript_merger.rb +30 -0
- data/lib/juicer/merger/stylesheet_merger.rb +112 -0
- data/lib/juicer/minifyer/yui_compressor.rb +129 -0
- data/tasks/ann.rake +80 -0
- data/tasks/bones.rake +20 -0
- data/tasks/gem.rake +201 -0
- data/tasks/git.rake +40 -0
- data/tasks/notes.rake +27 -0
- data/tasks/post_load.rake +34 -0
- data/tasks/rdoc.rake +50 -0
- data/tasks/rubyforge.rake +55 -0
- data/tasks/setup.rb +300 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- data/tasks/test.rake +40 -0
- data/tasks/test/setup.rake +35 -0
- data/test/bin/jslint.js +474 -0
- data/test/bin/rhino1_7R1.zip +0 -0
- data/test/bin/rhino1_7R2-RC1.zip +0 -0
- data/test/bin/yuicompressor +238 -0
- data/test/bin/yuicompressor-2.3.5.zip +0 -0
- data/test/bin/yuicompressor-2.4.2.zip +0 -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 +155 -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 +122 -0
- data/test/juicer/merger/test_css_dependency_resolver.rb +36 -0
- data/test/juicer/merger/test_javascript_dependency_resolver.rb +39 -0
- data/test/juicer/merger/test_javascript_merger.rb +74 -0
- data/test/juicer/merger/test_stylesheet_merger.rb +178 -0
- data/test/juicer/minifyer/test_yui_compressor.rb +159 -0
- data/test/juicer/test_cache_buster.rb +58 -0
- data/test/juicer/test_chainable.rb +94 -0
- data/test/juicer/test_core.rb +47 -0
- data/test/juicer/test_css_cache_buster.rb +72 -0
- data/test/juicer/test_jslint.rb +33 -0
- data/test/test_helper.rb +146 -0
- data/test/test_juicer.rb +4 -0
- 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
|