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