psyho_juicer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data/History.txt +58 -0
  2. data/Manifest.txt +58 -0
  3. data/Rakefile +96 -0
  4. data/Readme.rdoc +313 -0
  5. data/VERSION +1 -0
  6. data/bin/juicer +6 -0
  7. data/lib/juicer.rb +69 -0
  8. data/lib/juicer/asset/path.rb +275 -0
  9. data/lib/juicer/asset/path_resolver.rb +79 -0
  10. data/lib/juicer/binary.rb +171 -0
  11. data/lib/juicer/cache_buster.rb +131 -0
  12. data/lib/juicer/chainable.rb +106 -0
  13. data/lib/juicer/cli.rb +56 -0
  14. data/lib/juicer/command/install.rb +61 -0
  15. data/lib/juicer/command/list.rb +57 -0
  16. data/lib/juicer/command/merge.rb +205 -0
  17. data/lib/juicer/command/util.rb +32 -0
  18. data/lib/juicer/command/verify.rb +60 -0
  19. data/lib/juicer/css_cache_buster.rb +90 -0
  20. data/lib/juicer/datafy/datafy.rb +20 -0
  21. data/lib/juicer/dependency_resolver/css_dependency_resolver.rb +29 -0
  22. data/lib/juicer/dependency_resolver/dependency_resolver.rb +101 -0
  23. data/lib/juicer/dependency_resolver/javascript_dependency_resolver.rb +23 -0
  24. data/lib/juicer/ext/logger.rb +5 -0
  25. data/lib/juicer/ext/string.rb +47 -0
  26. data/lib/juicer/ext/symbol.rb +15 -0
  27. data/lib/juicer/image_embed.rb +129 -0
  28. data/lib/juicer/install/base.rb +186 -0
  29. data/lib/juicer/install/closure_compiler_installer.rb +69 -0
  30. data/lib/juicer/install/jslint_installer.rb +51 -0
  31. data/lib/juicer/install/rhino_installer.rb +53 -0
  32. data/lib/juicer/install/yui_compressor_installer.rb +67 -0
  33. data/lib/juicer/jslint.rb +90 -0
  34. data/lib/juicer/merger/base.rb +74 -0
  35. data/lib/juicer/merger/javascript_merger.rb +29 -0
  36. data/lib/juicer/merger/stylesheet_merger.rb +110 -0
  37. data/lib/juicer/minifyer/closure_compiler.rb +90 -0
  38. data/lib/juicer/minifyer/java_base.rb +77 -0
  39. data/lib/juicer/minifyer/yui_compressor.rb +96 -0
  40. data/test/bin/jslint-1.0.js +523 -0
  41. data/test/bin/jslint.js +523 -0
  42. data/test/bin/rhino1_7R1.zip +0 -0
  43. data/test/bin/rhino1_7R2-RC1.jar +0 -0
  44. data/test/bin/rhino1_7R2-RC1.zip +0 -0
  45. data/test/bin/yuicompressor +0 -0
  46. data/test/bin/yuicompressor-2.3.5.zip +0 -0
  47. data/test/bin/yuicompressor-2.4.2.jar +0 -0
  48. data/test/bin/yuicompressor-2.4.2.zip +0 -0
  49. data/test/fixtures/yui-download.html +425 -0
  50. data/test/test_helper.rb +175 -0
  51. data/test/unit/juicer/asset/path_resolver_test.rb +76 -0
  52. data/test/unit/juicer/asset/path_test.rb +370 -0
  53. data/test/unit/juicer/cache_buster_test.rb +104 -0
  54. data/test/unit/juicer/chainable_test.rb +94 -0
  55. data/test/unit/juicer/command/install_test.rb +58 -0
  56. data/test/unit/juicer/command/list_test.rb +81 -0
  57. data/test/unit/juicer/command/merge_test.rb +162 -0
  58. data/test/unit/juicer/command/util_test.rb +58 -0
  59. data/test/unit/juicer/command/verify_test.rb +48 -0
  60. data/test/unit/juicer/css_cache_buster_test.rb +71 -0
  61. data/test/unit/juicer/datafy_test.rb +37 -0
  62. data/test/unit/juicer/dependency_resolver/css_dependency_resolver_test.rb +36 -0
  63. data/test/unit/juicer/dependency_resolver/javascript_dependency_resolver_test.rb +50 -0
  64. data/test/unit/juicer/ext/string_test.rb +59 -0
  65. data/test/unit/juicer/ext/symbol_test.rb +27 -0
  66. data/test/unit/juicer/image_embed_test.rb +271 -0
  67. data/test/unit/juicer/install/installer_base_test.rb +214 -0
  68. data/test/unit/juicer/install/jslint_installer_test.rb +54 -0
  69. data/test/unit/juicer/install/rhino_installer_test.rb +57 -0
  70. data/test/unit/juicer/install/yui_compressor_test.rb +56 -0
  71. data/test/unit/juicer/jslint_test.rb +60 -0
  72. data/test/unit/juicer/merger/base_test.rb +122 -0
  73. data/test/unit/juicer/merger/javascript_merger_test.rb +74 -0
  74. data/test/unit/juicer/merger/stylesheet_merger_test.rb +180 -0
  75. data/test/unit/juicer/minifyer/closure_compressor_test.rb +107 -0
  76. data/test/unit/juicer/minifyer/yui_compressor_test.rb +116 -0
  77. data/test/unit/juicer_test.rb +1 -0
  78. metadata +278 -0
@@ -0,0 +1,32 @@
1
+ require "pathname"
2
+
3
+ module Juicer
4
+ module Command
5
+ # Utilities for Juicer command objects
6
+ #
7
+ module Util
8
+ # Returns an array of files from a variety of input. Input may be a single
9
+ # file, a single glob pattern or multiple files and/or patterns. It may
10
+ # even be an array of mixed input.
11
+ #
12
+ def files(*args)
13
+ args.flatten.collect { |file| Dir.glob(file) }.flatten
14
+ end
15
+
16
+ #
17
+ # Uses Pathname to calculate the shortest relative path from +path+ to
18
+ # +reference_path+ (default is +Dir.cwd+)
19
+ #
20
+ def relative(paths, reference_path = Dir.pwd)
21
+ paths = [paths].flatten.collect do |path|
22
+ path = Pathname.new(File.expand_path(path))
23
+ reference_path = Pathname.new(File.expand_path(reference_path))
24
+ path.relative_path_from(reference_path).to_s
25
+ end
26
+
27
+ paths.length == 1 ? paths.first : paths
28
+ end
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,60 @@
1
+ require "juicer/command/util"
2
+ require "rubygems"
3
+ require "cmdparse"
4
+ require "pathname"
5
+
6
+ module Juicer
7
+ module Command
8
+ # Verifies problem-free-ness of source code (JavaScript and CSS)
9
+ #
10
+ class Verify < CmdParse::Command
11
+ include Juicer::Command::Util
12
+
13
+ # Initializes command
14
+ #
15
+ def initialize(log = nil)
16
+ super('verify', false, true)
17
+ @log = log || Logger.new($STDIO)
18
+ self.short_desc = "Verifies that the given JavaScript/CSS file is problem free"
19
+ self.description = <<-EOF
20
+ Uses JsLint (http://www.jslint.com) to check that code adheres to good coding
21
+ practices to avoid potential bugs, and protect against introducing bugs by
22
+ minifying.
23
+ EOF
24
+ end
25
+
26
+ # Execute command
27
+ #
28
+ def execute(args)
29
+ # Need atleast one file
30
+ raise ArgumentError.new('Please provide atleast one input file/pattern') if args.length == 0
31
+ Juicer::Command::Verify.check_all(files(args), @log)
32
+ end
33
+
34
+ def self.check_all(files, log = nil)
35
+ log ||= Logger.new($stdio)
36
+ jslint = Juicer::JsLint.new(:bin_path => Juicer.home)
37
+ problems = false
38
+
39
+ # Check that JsLint is installed
40
+ raise FileNotFoundError.new("Missing 3rd party library JsLint, install with\njuicer install jslint") if jslint.locate_lib.nil?
41
+
42
+ # Verify all files
43
+ files.each do |file|
44
+ log.info "Verifying #{file} with JsLint"
45
+ report = jslint.check(file)
46
+
47
+ if report.ok?
48
+ log.info " OK!"
49
+ else
50
+ problems = true
51
+ log.warn " Problems detected"
52
+ log.warn " #{report.errors.join("\n").gsub(/\n/, "\n ")}\n"
53
+ end
54
+ end
55
+
56
+ !problems
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,90 @@
1
+ require "juicer/chainable"
2
+ require "juicer/cache_buster"
3
+ require "juicer/asset/path_resolver"
4
+
5
+ module Juicer
6
+ #
7
+ # The CssCacheBuster is a tool that can parse a CSS file and substitute all
8
+ # referenced URLs by a URL appended with a timestamp denoting it's last change.
9
+ # This causes the URLs to be unique every time they've been modified, thus
10
+ # facilitating using a far future expires header on your web server.
11
+ #
12
+ # See Juicer::CacheBuster for more information on how the cache buster URLs
13
+ # work.
14
+ #
15
+ # When dealing with CSS files that reference absolute URLs like /images/1.png
16
+ # you must specify the :document_root option that these URLs should be resolved
17
+ # against.
18
+ #
19
+ # When dealing with full URLs (ie including hosts) you can optionally specify
20
+ # an array of hosts to recognize as "local", meaning they serve assets from
21
+ # the :document_root directory. This way even asset host cycling can benefit from
22
+ # cache busters.
23
+ #
24
+ class CssCacheBuster
25
+ include Juicer::Chainable
26
+
27
+ def initialize(options = {})
28
+ @document_root = options[:document_root]
29
+ @document_root.sub!(%r{/?$}, "") if @document_root
30
+ @type = options[:type] || :soft
31
+ @hosts = (options[:hosts] || []).collect { |h| h.sub!(%r{/?$}, "") }
32
+ @contents = @base = nil
33
+ end
34
+
35
+ #
36
+ # Update file. If no +output+ is provided, the input file is overwritten
37
+ #
38
+ def save(file, output = nil)
39
+ @contents = File.read(file)
40
+ self.base = File.dirname(file)
41
+ used = []
42
+
43
+ urls(file).each do |asset|
44
+ begin
45
+ next if used.include?(asset.path)
46
+ @contents.gsub!(asset.path, asset.path(:cache_buster_type => @type))
47
+ rescue Errno::ENOENT
48
+ puts "Unable to locate file #{asset.path}, skipping cache buster"
49
+ rescue ArgumentError => e
50
+ if e.message =~ /No document root/
51
+ raise FileNotFoundError.new("Unable to resolve path #{asset.path} without :document_root option")
52
+ else
53
+ raise e
54
+ end
55
+ end
56
+ end
57
+
58
+ File.open(output || file, "w") { |f| f.puts @contents }
59
+ @contents = nil
60
+ end
61
+
62
+ chain_method :save
63
+
64
+ #
65
+ # Returns all referenced URLs in +file+. Returned paths are absolute (ie,
66
+ # they're resolved relative to the +file+ path.
67
+ #
68
+ def urls(file)
69
+ @contents = File.read(file) unless @contents
70
+
71
+ @contents.scan(/url\([\s"']*([^\)"'\s]*)[\s"']*\)/m).collect do |match|
72
+ path_resolver.resolve(match.first)
73
+ end
74
+ end
75
+
76
+ protected
77
+ def base=(base)
78
+ @prev_base = @base
79
+ @base = base
80
+ end
81
+
82
+ def path_resolver
83
+ return @path_resolver if @path_resolver && @base == @prev_base
84
+
85
+ @path_resolver = Juicer::Asset::PathResolver.new(:document_root => @document_root,
86
+ :hosts => @hosts,
87
+ :base => @base)
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ # Datafy code lifted from http://segment7.net/projects/ruby/datafy/
4
+
5
+ require 'base64'
6
+ require 'cgi'
7
+
8
+ module Datafy
9
+ def Datafy::make_data_uri(content, content_type)
10
+ outuri = 'data:' + content_type
11
+ unless content_type =~ /^text/i # base64 encode if not text
12
+ outuri += ';base64'
13
+ content = Base64.encode64(content).gsub("\n", '')
14
+ else
15
+ content = CGI::escape(content)
16
+ end
17
+ outuri += ",#{content}"
18
+ end
19
+
20
+ end
@@ -0,0 +1,29 @@
1
+ require "juicer/dependency_resolver/dependency_resolver"
2
+
3
+ module Juicer
4
+
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(?:\surl\(|\s)(['"]?)([^\?'"\)\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
+
24
+ def extension
25
+ ".css"
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,101 @@
1
+ module Juicer
2
+ class DependencyResolver
3
+ include Enumerable
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
+ # Yield files recursively. Resolve dependencies first, then call each, or
26
+ # any other enumerable methods.
27
+ #
28
+ def each(&block)
29
+ @files.each(&block)
30
+ end
31
+
32
+ #
33
+ # Resolves a path relative to another. If the path is absolute (ie it
34
+ # starts with a protocol or /) the <tt>:document_root</tt> options has to be
35
+ # set as well.
36
+ #
37
+ def resolve_path(path, reference)
38
+ # Absolute URL
39
+ if path =~ %r{^(/|[a-z]+:)}
40
+ if @options[:document_root].nil?
41
+ msg = "Cannot resolve absolute path '#{path}' without web root option"
42
+ raise ArgumentError.new(msg)
43
+ end
44
+
45
+ path.sub!(%r{^[a-z]+://[^/]+/}, '')
46
+ return File.expand_path(File.join(@options[:document_root], path))
47
+ end
48
+
49
+ File.expand_path(File.join(File.dirname(reference), path))
50
+ end
51
+
52
+ private
53
+ def parse(line)
54
+ raise NotImplementedError.new
55
+ end
56
+
57
+ def extension
58
+ raise NotImplementedError.new
59
+ end
60
+
61
+ #
62
+ # Carries out the actual work of resolve. resolve resets the internal
63
+ # file list and yields control to _resolve for rebuilding the file list.
64
+ #
65
+ def _resolve(file)
66
+ imported_path = nil
67
+
68
+ IO.foreach(file) do |line|
69
+ # Implementing subclasses may throw :done from the parse method when
70
+ # the file is exhausted for dependency declaration possibilities.
71
+ catch(:done) do
72
+ imported_path = parse(line, imported_path)
73
+
74
+ # If a dependency declaration was found
75
+ if imported_path
76
+ # Resolves a path relative to the file that imported it
77
+ imported_path = resolve_path(imported_path, file)
78
+
79
+ if File.directory?(imported_path)
80
+ imported_files = Dir.glob(File.join(imported_path, "**", "*#{extension}"))
81
+ else
82
+ imported_files = [imported_path]
83
+ end
84
+
85
+ imported_files.each do |imported_file|
86
+ # Only keep processing file if it's not already included.
87
+ # Yield to block to allow caller to ignore file
88
+ if !@files.include?(imported_file) && (!block_given? || yield(imported_file))
89
+ # Check this file for imports before adding it to get order right
90
+ _resolve(imported_file) { |f| f != File.expand_path(file) }
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ file = File.expand_path(file)
98
+ @files << file if !@files.include?(file) && (!block_given? || yield(file))
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,23 @@
1
+ require "juicer/dependency_resolver/dependency_resolver"
2
+
3
+ module Juicer
4
+ # Resolves @depends and @depend statements in comments in JavaScript files.
5
+ # Only the first comment in a JavaScript file is parsed
6
+ #
7
+ class JavaScriptDependencyResolver < DependencyResolver
8
+ @@depends_pattern = /\@depends?\s+([^\s\'\"\;]+)/
9
+
10
+ private
11
+ def parse(line, imported_file = nil)
12
+ return $1 if line =~ @@depends_pattern
13
+
14
+ # If we have already skimmed through some @depend/@depends or a
15
+ # closing comment we're done.
16
+ throw :done unless imported_file.nil? || !(line =~ /\*\//)
17
+ end
18
+
19
+ def extension
20
+ ".js"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ class Logger
2
+ def format_message(severity, datetime, progname, msg)
3
+ "#{msg}\n"
4
+ end
5
+ end
@@ -0,0 +1,47 @@
1
+ #
2
+ # Additions to core Ruby objects
3
+ #
4
+
5
+ class String
6
+
7
+ unless String.method_defined?(:camel_case)
8
+ #
9
+ # Turn an underscored string into camel case, ie this_becomes -> ThisBecomes
10
+ #
11
+ def camel_case
12
+ self.split("_").inject("") { |str, piece| str + piece.capitalize }
13
+ end
14
+ end
15
+
16
+ unless String.method_defined?(:to_class)
17
+ #
18
+ # Treat a string as a class name and return the class. Optionally provide a
19
+ # module to look up the class in.
20
+ #
21
+ def to_class(mod = nil)
22
+ res = "#{mod}::#{self}".sub(/^::/, "").split("::").inject(Object) do |mod, obj|
23
+ raise "No such class/module" unless mod.const_defined?(obj)
24
+ mod = mod.const_get(obj)
25
+ end
26
+ end
27
+ end
28
+
29
+ unless String.method_defined?(:classify)
30
+ #
31
+ # Turn a string in either underscore or camel case form into a class directly
32
+ #
33
+ def classify(mod = nil)
34
+ self.camel_case.to_class(mod)
35
+ end
36
+ end
37
+
38
+ unless String.method_defined?(:underscore)
39
+ #
40
+ # Turn a camelcase string into underscore string
41
+ #
42
+ def underscore
43
+ self.split(/([A-Z][^A-Z]*)/).find_all { |str| str != "" }.join("_").downcase
44
+ end
45
+ end
46
+ end
47
+
@@ -0,0 +1,15 @@
1
+ class Symbol
2
+ #
3
+ # Converts symbol to string and calls String#camel_case
4
+ #
5
+ def camel_case
6
+ self.to_s.camel_case
7
+ end
8
+
9
+ #
10
+ # Converts symbol to string and calls String#classify
11
+ #
12
+ def classify(mod = nil)
13
+ self.to_s.classify(mod)
14
+ end
15
+ end
@@ -0,0 +1,129 @@
1
+ require "juicer/chainable"
2
+ require "juicer/cache_buster"
3
+ require "juicer/asset/path_resolver"
4
+
5
+ module Juicer
6
+ #
7
+ # The ImageEmbed is a tool that can parse a CSS file and substitute all
8
+ # referenced URLs by a data uri
9
+ #
10
+ # - data uri (http://en.wikipedia.org/wiki/Data_URI_scheme)
11
+ #
12
+ # Only local resources will be processed this way, external resources referenced
13
+ # by absolute urls will be left alone
14
+ #
15
+ class ImageEmbed
16
+ include Juicer::Chainable
17
+
18
+ # The maximum supported limit for modern browsers, See the Readme.rdoc for details
19
+ SIZE_LIMIT = 32768
20
+
21
+ #
22
+ # Returns the size limit
23
+ #
24
+ def size_limit
25
+ SIZE_LIMIT
26
+ end
27
+
28
+ def initialize(options = {})
29
+ @document_root = options[:document_root]
30
+ @document_root.sub!(%r{/?$}, "") if @document_root # Remove trailing slash
31
+ @type = options[:type] || :none
32
+ @contents = nil
33
+ @hosts = options[:hosts]
34
+ @path_resolver = Juicer::Asset::PathResolver.new(:document_root => options[:document_root],
35
+ :hosts => options[:hosts])
36
+ end
37
+
38
+ #
39
+ # Update file. If no +output+ is provided, the input file is overwritten
40
+ #
41
+ def save(file, output = nil)
42
+ return unless @type == :data_uri
43
+
44
+ output_file = output || file
45
+ @contents = File.read(file)
46
+ used = []
47
+
48
+ @path_resolver = Juicer::Asset::PathResolver.new(:document_root => @document_root,
49
+ :hosts => @hosts,
50
+ :base => File.dirname(file))
51
+
52
+ assets = urls(file)
53
+
54
+ # TODO: Remove "?embed=true" from duplicate urls
55
+ duplicates = duplicate_urls(assets)
56
+
57
+ if duplicates.length > 0
58
+ Juicer::LOGGER.warn("Duplicate image urls detected, these images will not be embedded: #{duplicates.collect { |v| v.gsub('?embed=true', '') }.inspect}")
59
+ end
60
+
61
+ assets.each do |asset|
62
+ begin
63
+ next if used.include?(asset) || duplicates.include?(asset.path)
64
+ used << asset
65
+
66
+ # make sure we do not exceed SIZE_LIMIT
67
+ new_path = embed_data_uri(asset.filename)
68
+
69
+ if new_path.length < SIZE_LIMIT
70
+ # replace the url in the css file with the data uri
71
+ @contents.gsub!(asset.path, embed_data_uri(asset.path))
72
+ else
73
+ Juicer::LOGGER.warn("The final data uri for the image located at #{asset.path.gsub('?embed=true', '')} exceeds #{SIZE_LIMIT} and will not be embedded to maintain compatability.")
74
+ end
75
+ rescue Errno::ENOENT
76
+ puts "Unable to locate file #{asset.path}, skipping image embedding"
77
+ end
78
+ end
79
+
80
+ File.open(output || file, "w") { |f| f.puts @contents }
81
+ @contents = nil
82
+ end
83
+
84
+ chain_method :save
85
+
86
+ def embed_data_uri( path )
87
+ new_path = path
88
+
89
+ if path.match( /\?embed=true$/ )
90
+ supported_file_matches = path.match( /(?:\.)(png|gif|jpg|jpeg)(?:\?embed=true)$/i )
91
+ filetype = supported_file_matches[1] if supported_file_matches
92
+
93
+ if ( filetype )
94
+ filename = path.gsub('?embed=true','')
95
+
96
+ # check if file exists, throw an error if it doesn't exist
97
+ if File.exist?( filename )
98
+
99
+ # read contents of file into memory
100
+ content = File.read( filename )
101
+ content_type = "image/#{filetype}"
102
+
103
+ # encode the url
104
+ new_path = Datafy::make_data_uri( content, content_type )
105
+ else
106
+ puts "Unable to locate file #{filename} on local file system, skipping image embedding"
107
+ end
108
+ end
109
+ end
110
+ return new_path
111
+ end
112
+
113
+ #
114
+ # Returns all referenced URLs in +file+.
115
+ #
116
+ def urls(file)
117
+ @contents = File.read(file) unless @contents
118
+
119
+ @contents.scan(/url\([\s"']*([^\)"'\s]*)[\s"']*\)/m).collect do |match|
120
+ @path_resolver.resolve(match.first)
121
+ end
122
+ end
123
+
124
+ private
125
+ def duplicate_urls(urls)
126
+ urls.inject({}) { |h,v| h[v.path] = h[v.path].to_i+1; h }.reject{ |k,v| v == 1 }.keys
127
+ end
128
+ end
129
+ end