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,131 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Juicer
4
+ #
5
+ # Assists in creating filenames that reflect the last change to the file. These
6
+ # kinds of filenames are useful when serving static content through a web server.
7
+ # If the filename changes everytime the file is modified, you can safely configure
8
+ # the web server to cache files indefinately, and know that the updated filename
9
+ # will cause the file to be downloaded again - only once - when it has changed.
10
+ #
11
+ # = Types of cache busters
12
+ #
13
+ # == Query string / "soft" cache busters
14
+ # Soft cache busters require no web server configuration. However, it is not
15
+ # guaranteed to work in all settings. For example, older default
16
+ # configurations for popular proxy server Squid does not consider a known URL
17
+ # with a new query string a new URL, and thus will not download the file over.
18
+ #
19
+ # The soft cache busters transforms
20
+ # <tt>/images/logo.png</tt> to <tt>/images/logo.png?cb=1232923789</tt>
21
+ #
22
+ # == Filename change / "hard" cache busters
23
+ # Hard cache busters change the file name itself, and thus requires either
24
+ # the web server to (internally) rewrite requests for these files to the
25
+ # original ones, or the file names to actually change. Hard cache busters
26
+ # transforms <tt>/images/logo.png</tt> to <tt>/images/logo-1232923789.png</tt>
27
+ #
28
+ # Hard cache busters are guaranteed to work, and is the recommended variant.
29
+ # An example configuration for the Apache web server that does not require
30
+ # you to actually change the filenames can be seen below.
31
+ #
32
+ # <VirtualHost *>
33
+ # # Application/website configuration
34
+ #
35
+ # # Cache static resources for a year
36
+ # <FilesMatch "\.(ico|pdf|flv|jpg|jpeg|png|gif|js|css|swf)$">
37
+ # ExpiresActive On
38
+ # ExpiresDefault "access plus 1 year"
39
+ # </FilesMatch>
40
+ #
41
+ # # Rewrite URLs like /images/logo-cb1234567890.png to /images/logo.png
42
+ # RewriteEngine On
43
+ # RewriteRule (.*)-cb\d+\.(.*)$ $1.$2 [L]
44
+ # </VirtualHost>])
45
+ #
46
+ # = Consecutive calls
47
+ #
48
+ # Consecutive calls to add a cache buster to a path will replace the existing
49
+ # cache buster *as long as the parameter name is the same*. Consider this:
50
+ #
51
+ # file = Juicer::CacheBuster.hard("/home/file.png") #=> "/home/file-cb1234567890.png"
52
+ # Juicer::CacheBuster.hard(file) #=> "/home/file-cb1234567891.png"
53
+ #
54
+ # # Changing the parameter name breaks this
55
+ # Juicer::CacheBuster.hard(file, :juicer) #=> "/home/file-cb1234567891-juicer1234567892.png"
56
+ #
57
+ # Avoid this type of trouble simply be cleaning the URL with the old name first:
58
+ #
59
+ # Juicer::CacheBuster.clean(file) #=> "/home/file.png"
60
+ # file = Juicer::CacheBuster.hard(file, :juicer) #=> "/home/file-juicer1234567892.png"
61
+ # Juicer::CacheBuster.clean(file, :juicer) #=> "/home/file.png"
62
+ #
63
+ # Author:: Christian Johansen (christian@cjohansen.no)
64
+ # Copyright:: Copyright (c) 2009 Christian Johansen
65
+ # License:: BSD
66
+ #
67
+ module CacheBuster
68
+ DEFAULT_PARAMETER = "jcb"
69
+
70
+ #
71
+ # Creates a unique file name for every revision to the files contents.
72
+ # Raises an <tt>ArgumentError</tt> if the file can not be found.
73
+ #
74
+ # The type indicates which type of cache buster you want, <tt>:soft</tt>
75
+ # or <tt>:hard</tt>. Default is <tt>:soft</tt>. If an unsupported value
76
+ # is specified, <tt>:soft</tt> will be used.
77
+ #
78
+ # See <tt>#hard</tt> and <tt>#soft</tt> for explanation of the parameter
79
+ # argument.
80
+ #
81
+ def self.path(file, type = :soft, parameter = DEFAULT_PARAMETER)
82
+ return file if file =~ /data:.*;base64/
83
+ file = self.clean(file, parameter)
84
+ filename = file.split("?").first
85
+ raise ArgumentError.new("#{file} could not be found") unless File.exists?(filename)
86
+ mtime = File.mtime(filename).to_i
87
+ type = [:soft, :hard].include?(type) ? type : :soft
88
+
89
+ if type == :soft
90
+ parameter = "#{parameter}=".sub(/^=$/, '')
91
+ return "#{file}#{file.index('?') ? '&' : '?'}#{parameter}#{mtime}"
92
+ end
93
+
94
+ file.sub(/(\.[^\.]+$)/, "-#{parameter}#{mtime}" + '\1')
95
+ end
96
+
97
+ #
98
+ # Add a hard cache buster to a filename. The parameter is an optional prefix
99
+ # that is added before the mtime timestamp. It results in filenames of the form:
100
+ # <tt>file-[parameter name][timestamp].suffix</tt>, ie
101
+ # <tt>images/logo-cb1234567890.png</tt> which is the case for the default
102
+ # parameter name "cb" (as in *c*ache *b*uster).
103
+ #
104
+ def self.hard(file, parameter = DEFAULT_PARAMETER)
105
+ self.path(file, :hard, parameter)
106
+ end
107
+
108
+ #
109
+ # Add a soft cache buster to a filename. The parameter is an optional name
110
+ # for the mtime timestamp value. It results in filenames of the form:
111
+ # <tt>file.suffix?[parameter name]=[timestamp]</tt>, ie
112
+ # <tt>images/logo.png?cb=1234567890</tt> which is the case for the default
113
+ # parameter name "cb" (as in *c*ache *b*uster).
114
+ #
115
+ def self.soft(file, parameter = DEFAULT_PARAMETER)
116
+ self.path(file, :soft, parameter)
117
+ end
118
+
119
+ #
120
+ # Remove cache buster from a URL for a given parameter name. Parameter name is
121
+ # "cb" by default.
122
+ #
123
+ def self.clean(file, parameter = DEFAULT_PARAMETER)
124
+ query_param = "#{parameter}".length == 0 ? "" : "#{parameter}="
125
+ new_file = file.sub(/#{query_param}\d+&?/, "").sub(/(\?|&)$/, "")
126
+ return new_file unless new_file == file
127
+
128
+ file.sub(/-#{parameter}\d+(\.\w+)($|\?)/, '\1\2')
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,106 @@
1
+ module Juicer
2
+ #
3
+ # Facilitates the chain of responsibility pattern. Wraps given methods and
4
+ # calls them in a chain.
5
+ #
6
+ # To make an object chainable, simply include the module and call the class
7
+ # method chain_method for each method that should be chained.
8
+ #
9
+ # Example is a simplified version of the Wikipedia one
10
+ # (http://en.wikipedia.org/wiki/Chain-of-responsibility_pattern)
11
+ #
12
+ # class Logger
13
+ # include Juicer::Chainable
14
+ #
15
+ # ERR = 3
16
+ # NOTICE = 5
17
+ # DEBUG = 7
18
+ #
19
+ # def initialize(level)
20
+ # @level = level
21
+ # end
22
+ #
23
+ # def log(str, level)
24
+ # if level <= @level
25
+ # write str
26
+ # else
27
+ # abort_chain
28
+ # end
29
+ # end
30
+ #
31
+ # def write(str)
32
+ # puts str
33
+ # end
34
+ #
35
+ # chain_method :message
36
+ # end
37
+ #
38
+ # class EmailLogger < Logger
39
+ # def write(str)
40
+ # p "Logging by email"
41
+ # # ...
42
+ # end
43
+ # end
44
+ #
45
+ # logger = Logger.new(Logger::NOTICE)
46
+ # logger.next_in_chain = EmailLogger.new(Logger::ERR)
47
+ #
48
+ # logger.log("Some message", Logger::DEBUG) # Ignored
49
+ # logger.log("A warning", Logger::NOTICE) # Logged to console
50
+ # logger.log("An error", Logger::ERR) # Logged to console and email
51
+ #
52
+ module Chainable
53
+
54
+ #
55
+ # Add the chain_method to classes that includes the module
56
+ #
57
+ def self.included(base)
58
+ base.extend(ClassMethods)
59
+ end
60
+
61
+ #
62
+ # Sets the next command in the chain
63
+ #
64
+ def next_in_chain=(next_obj)
65
+ @_next_in_chain = next_obj
66
+ next_obj || self
67
+ end
68
+
69
+ alias_method :set_next, :next_in_chain=
70
+
71
+ #
72
+ # Get next command in chain
73
+ #
74
+ def next_in_chain
75
+ @_next_in_chain ||= nil
76
+ @_next_in_chain
77
+ end
78
+
79
+ private
80
+ #
81
+ # Abort the chain for the current message
82
+ #
83
+ def abort_chain
84
+ @_abort_chain = true
85
+ end
86
+
87
+ module ClassMethods
88
+ #
89
+ # Sets up a method for chaining
90
+ #
91
+ def chain_method(method)
92
+ original_method = "execute_#{method}".to_sym
93
+ alias_method original_method, method
94
+
95
+ self.class_eval <<-RUBY
96
+ def #{method}(*args, &block)
97
+ @_abort_chain = false
98
+ #{original_method}(*args, &block)
99
+ next_in_chain.#{method}(*args, &block) if !@_abort_chain && next_in_chain
100
+ @_abort_chain = false
101
+ end
102
+ RUBY
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,56 @@
1
+ require "cmdparse"
2
+
3
+ # Command line interpreter for Juicer
4
+ #
5
+ module Juicer
6
+ class Cli
7
+
8
+ def initialize
9
+ @log = Juicer::LOGGER
10
+ @log.level = Logger::INFO
11
+ end
12
+
13
+ # Set up command parser and parse arguments
14
+ #
15
+ def parse(arguments = ARGV)
16
+ @cmd = CmdParse::CommandParser.new(true, true)
17
+ @cmd.program_name = "juicer"
18
+ @cmd.program_version = Juicer.version.split(".")
19
+
20
+ @cmd.options = CmdParse::OptionParserWrapper.new do |opt|
21
+ opt.separator "Global options:"
22
+ opt.on("-v", "--verbose", "Be verbose when outputting info") { |t| @log.level = Logger::DEBUG }
23
+ opt.on("-q", "--quiet", "Only log warnings and errors") { |t| @log.level = Logger::WARN }
24
+ end
25
+
26
+ add_commands
27
+ @cmd.parse(arguments)
28
+ @log.close
29
+ rescue SystemExit
30
+ exit
31
+ end
32
+
33
+ # Run CLI
34
+ #
35
+ def self.run(arguments = ARGV)
36
+ juicer = self.new
37
+ juicer.parse(arguments)
38
+ end
39
+
40
+ private
41
+ # Adds commands supported by juicer. Instantiates all classes in the
42
+ # Juicer::Command namespace.
43
+ #
44
+ def add_commands
45
+ @cmd.add_command(CmdParse::HelpCommand.new)
46
+ @cmd.add_command(CmdParse::VersionCommand.new)
47
+
48
+ if Juicer.const_defined?("Command")
49
+ Juicer::Command.constants.each do |const|
50
+ const = Juicer::Command.const_get(const)
51
+ @cmd.add_command(const.new(@log)) if const.kind_of?(Class)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,61 @@
1
+ require "juicer/command/util"
2
+ require "cmdparse"
3
+ require "pathname"
4
+
5
+ module Juicer
6
+ module Command
7
+ # Installs a third party library so Juicer can use it.
8
+ #
9
+ class Install < CmdParse::Command
10
+ include Juicer::Command::Util
11
+
12
+ # Initializes command
13
+ #
14
+ def initialize(io = nil)
15
+ super('install', false, true)
16
+ @io = io || Logger.new(STDOUT)
17
+ @version = nil
18
+ @path = Juicer.home
19
+ self.short_desc = "Install a third party library"
20
+ self.description = <<-EOF
21
+ Installs a third party used by Juicer. Downloads necessary binaries and licenses
22
+ into Juicer installation directory, usually ~/.juicer
23
+ EOF
24
+
25
+ self.options = CmdParse::OptionParserWrapper.new do |opt|
26
+ opt.on('-v', '--version [VERSION]', 'Specify version of library to install') { |version| @version = version }
27
+ end
28
+ end
29
+
30
+ # Execute command
31
+ #
32
+ def execute(*args)
33
+ args.flatten!
34
+
35
+ if args.length == 0
36
+ raise ArgumentError.new('Please provide a library to install')
37
+ end
38
+
39
+ args.each do |lib|
40
+ installer = Juicer::Install.get(lib).new(@path)
41
+ path = File.join(installer.install_dir, installer.path)
42
+ version = version(installer)
43
+
44
+ if installer.installed?(version)
45
+ @io.info "#{installer.name} #{version} is already installed in #{path}"
46
+ break
47
+ end
48
+
49
+ installer.install(version)
50
+ @io.info "Successfully installed #{lib.camel_case} #{version} in #{path}" if installer.installed?(version)
51
+ end
52
+ end
53
+
54
+ # Returns which version to install
55
+ #
56
+ def version(installer)
57
+ @version ||= installer.latest
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,57 @@
1
+ require "juicer/command/util"
2
+ require "cmdparse"
3
+ require "pathname"
4
+
5
+ module Juicer
6
+ module Command
7
+ # Displays a list of files that make up the dependency chain for the input
8
+ # files/patterns.
9
+ #
10
+ class List < CmdParse::Command
11
+ include Juicer::Command::Util
12
+
13
+ # Initializes command
14
+ #
15
+ def initialize(log = nil)
16
+ super('list', false, true)
17
+ @log = log
18
+ self.short_desc = "Lists all dependencies for all input files/patterns"
19
+ self.description = <<-EOF
20
+ Dependencies are looked up recursively. The dependency chain reveals which files
21
+ will be joined by juicer merge.
22
+
23
+ Input parameters may be:
24
+ * Single file, ie $ juicer list myfile.css
25
+ * Single glob pattern, ie $ juicer list **/*.css
26
+ * Multiple mixed arguments, ie $ juicer list **/*.js **/*.css
27
+ EOF
28
+ end
29
+
30
+ # Execute command
31
+ #
32
+ def execute(args)
33
+ if args.length == 0
34
+ raise ArgumentError.new('Please provide atleast one input file/pattern')
35
+ end
36
+
37
+ types = { :js => Juicer::JavaScriptDependencyResolver.new,
38
+ :css => Juicer::CssDependencyResolver.new }
39
+
40
+ result = files(args).map { |file|
41
+ type = file.split(".").pop.to_sym
42
+ raise FileNotFoundError.new("Unable to guess type (CSS/JavaScript) of file #{relative(file)}") unless types[type]
43
+
44
+ deps = relative types[type].resolve(file)
45
+ # there may only be one dependency, which resolve() returns as a string
46
+ deps = deps.join("\n ") if deps.is_a? Array
47
+
48
+ "Dependency chain for #{relative file}:\n #{deps}"
49
+ }.join("\n\n") + "\n"
50
+
51
+ @log.info result
52
+
53
+ result
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,205 @@
1
+ require "juicer/command/util"
2
+ require "juicer/command/verify"
3
+ require "cmdparse"
4
+ require "pathname"
5
+
6
+ module Juicer
7
+ module Command
8
+ # The compress command combines and minifies CSS and JavaScript files
9
+ #
10
+ class Merge < CmdParse::Command
11
+ include Juicer::Command::Util
12
+
13
+ # Initializes compress command
14
+ #
15
+ def initialize(log = nil)
16
+ super('merge', false, true)
17
+ @types = { :js => Juicer::Merger::JavaScriptMerger,
18
+ :css => Juicer::Merger::StylesheetMerger }
19
+ @output = nil # File to write to
20
+ @force = false # Overwrite existing file if true
21
+ @type = nil # "css" or "js" - for minifyer
22
+ @minifyer = "yui_compressor" # Which minifyer to use
23
+ @opts = {} # Path to minifyer binary
24
+ @arguments = nil # Minifyer arguments
25
+ @ignore = false # Ignore syntax problems if true
26
+ @cache_buster = :soft # What kind of cache buster to use, :soft or :hard
27
+ @hosts = nil # Hosts to use when replacing URLs in stylesheets
28
+ @document_root = nil # Used to understand absolute paths
29
+ @relative_urls = false # Make the merger use relative URLs
30
+ @absolute_urls = false # Make the merger use absolute URLs
31
+ @local_hosts = [] # Host names that are served from :document_root
32
+ @verify = true # Verify js files with JsLint
33
+ @image_embed_type = :none # Embed images in css files, options are :none, :data_uri
34
+
35
+ @log = log || Logger.new(STDOUT)
36
+
37
+ self.short_desc = "Combines and minifies CSS and JavaScript files"
38
+ self.description = <<-EOF
39
+ Each file provided as input will be checked for dependencies to other files,
40
+ and those files will be added to the final output
41
+
42
+ For CSS files the dependency checking is done through regular @import
43
+ statements.
44
+
45
+ For JavaScript files you can tell Juicer about dependencies through special
46
+ comment switches. These should appear inside a multi-line comment, specifically
47
+ inside the first multi-line comment. The switch is @depend or @depends, your
48
+ choice.
49
+
50
+ The -m --minifyer switch can be used to select which minifyer to use. Currently
51
+ only YUI Compressor and Google Closure Compiler is supported, ie -m yui_compressor (default) or -m closure_compiler. When using
52
+ the compressor the path should be the path to where the jar file is found.
53
+ EOF
54
+
55
+ self.options = CmdParse::OptionParserWrapper.new do |opt|
56
+ opt.on("-o", "--output file", "Output filename") { |filename| @output = filename }
57
+ opt.on("-p", "--path path", "Path to compressor binary") { |path| @opts[:bin_path] = path }
58
+ opt.on("-m", "--minifyer name", "Which minifer to use. Currently only supports yui_compressor and closure compiler") { |name| @minifyer = name }
59
+ opt.on("-f", "--force", "Force overwrite of target file") { @force = true }
60
+ opt.on("-a", "--arguments arguments", "Arguments to minifyer, escape with quotes") { |arguments|
61
+ @arguments = arguments.to_s.gsub(/(^['"]|["']$)/, "")
62
+ }
63
+ opt.on("-i", "--ignore-problems", "Merge and minify even if verifyer finds problems") { @ignore = true }
64
+ opt.on("-s", "--skip-verification", "Skip JsLint verification (js files only). Not recomended!") { @verify = false }
65
+ opt.on("-t", "--type type", "Juicer can only guess type when files have .css or .js extensions. Specify js or\n" +
66
+ (" " * 37) + "css with this option in cases where files have other extensions.") { |type| @type = type.to_sym }
67
+ opt.on("-h", "--hosts hosts", "Cycle asset hosts for referenced urls. Comma separated") { |hosts| @hosts = hosts.split(",") }
68
+ opt.on("-l", "--local-hosts hosts", "Host names that are served from --document-root (can be given cache busters). Comma separated") do |hosts|
69
+ @local_hosts = hosts.split(",")
70
+ end
71
+ opt.on("", "--all-hosts-local", "Treat all hosts as local (ie served from --document-root") { |t| @local_hosts = @hosts }
72
+ opt.on("-r", "--relative-urls", "Convert all referenced URLs to relative URLs. Requires --document-root if\n" +
73
+ (" " * 37) + "absolute URLs are used. Only valid for CSS files") { |t| @relative_urls = true }
74
+ opt.on("-b", "--absolute-urls", "Convert all referenced URLs to absolute URLs. Requires --document-root.\n" +
75
+ (" " * 37) + "Works with cycled asset hosts. Only valid for CSS files") { |t| @absolute_urls = true }
76
+ opt.on("-d", "--document-root dir", "Path to resolve absolute URLs relative to") { |path| @document_root = path }
77
+ opt.on("-c", "--cache-buster type", "none, soft or hard. Default is soft, which adds timestamps to referenced\n" +
78
+ (" " * 37) + "URLs as query parameters. None leaves URLs untouched and hard alters file names") do |type|
79
+ @cache_buster = [:soft, :hard].include?(type.to_sym) ? type.to_sym : nil
80
+ end
81
+ opt.on("-e", "--embed-images type", "none or data_uri. Default is none. Data_uri embeds images using Base64 encoding\n" +
82
+ (" " * 37) + "None leaves URLs untouched. Candiate images must be flagged with '?embed=true to be considered") do |embed|
83
+ @image_embed_type = [:none, :data_uri].include?(embed.to_sym) ? embed.to_sym : nil
84
+ end
85
+ end
86
+ end
87
+
88
+ # Execute command
89
+ #
90
+ def execute(args)
91
+ if (files = files(args)).length == 0
92
+ @log.fatal "Please provide atleast one input file"
93
+ raise SystemExit.new("Please provide atleast one input file")
94
+ end
95
+
96
+ # Figure out which file to output to
97
+ output = output(files.first)
98
+
99
+ # Warn if file already exists
100
+ if File.exists?(output) && !@force
101
+ msg = "Unable to continue, #{output} exists. Run again with --force to overwrite"
102
+ @log.fatal msg
103
+ raise SystemExit.new(msg)
104
+ end
105
+
106
+ # Set up merger to resolve imports and so on. Do not touch URLs now, if
107
+ # asset host cycling is added at this point, the cache buster WILL be
108
+ # confused
109
+ merger = merger(output).new(files, :relative_urls => @relative_urls,
110
+ :absolute_urls => @absolute_urls,
111
+ :document_root => @document_root,
112
+ :hosts => @hosts)
113
+
114
+ # Fail if syntax trouble (js only)
115
+ if @verify && !Juicer::Command::Verify.check_all(merger.files.reject { |f| f =~ /\.css$/ }, @log)
116
+ @log.error "Problems were detected during verification"
117
+ raise SystemExit.new("Input files contain problems") unless @ignore
118
+ @log.warn "Ignoring detected problems"
119
+ end
120
+
121
+ # Set command chain and execute
122
+ merger.set_next(image_embed(output)).set_next(cache_buster(output)).set_next(minifyer)
123
+ merger.save(output)
124
+
125
+ # Print report
126
+ @log.info "Produced #{relative output} from"
127
+ merger.files.each { |file| @log.info " #{relative file}" }
128
+ rescue FileNotFoundError => err
129
+ # Handle missing document-root option
130
+ puts err.message.sub(/:document_root/, "--document-root")
131
+ end
132
+
133
+ private
134
+ #
135
+ # Resolve and load minifyer
136
+ #
137
+ def minifyer
138
+ return nil if @minifyer.nil? || @minifyer == "" || @minifyer.downcase == "none"
139
+
140
+ begin
141
+ @opts[:bin_path] = File.join(Juicer.home, "lib", @minifyer, "bin") unless @opts[:bin_path]
142
+ compressor = @minifyer.classify(Juicer::Minifyer).new(@opts)
143
+ compressor.set_opts(@arguments) if @arguments
144
+ @log.debug "Using #{@minifyer.camel_case} for minification"
145
+
146
+ return compressor
147
+ rescue NameError => e
148
+ @log.fatal e.message
149
+ @log.fatal "No such minifyer '#{@minifyer}', aborting"
150
+ raise SystemExit.new("No such minifyer '#{@minifyer}', aborting")
151
+ rescue FileNotFoundError => e
152
+ @log.fatal e.message
153
+ @log.fatal "Try installing with; juicer install #{@minifyer.underscore}"
154
+ raise SystemExit.new(e.message)
155
+ rescue Exception => e
156
+ @log.fatal e.message
157
+ raise SystemExit.new(e.message)
158
+ end
159
+
160
+ nil
161
+ end
162
+
163
+ #
164
+ # Resolve and load merger
165
+ #
166
+ def merger(output = "")
167
+ @type ||= output.split(/\.([^\.]*)$/)[1]
168
+ type = @type.to_sym if @type
169
+
170
+ if !@types.include?(type)
171
+ @log.warn "Unknown type '#{type}', defaulting to 'js'"
172
+ type = :js
173
+ end
174
+
175
+ @types[type]
176
+ end
177
+
178
+ #
179
+ # Load cache buster, only available for CSS files
180
+ #
181
+ def cache_buster(file)
182
+ return nil if !file || file !~ /\.css$/ || @cache_buster.nil?
183
+ Juicer::CssCacheBuster.new(:document_root => @document_root, :type => @cache_buster, :hosts => @local_hosts)
184
+ end
185
+
186
+ #
187
+ # Load image embed, only available for CSS files
188
+ #
189
+ def image_embed(file)
190
+ return nil if !file || file !~ /\.css$/ || @image_embed_type.nil?
191
+ Juicer::ImageEmbed.new( :type => @image_embed_type )
192
+ end
193
+
194
+ #
195
+ # Generate output file name. Optional argument is a filename to base the new
196
+ # name on. It will prepend the original suffix with ".min"
197
+ #
198
+ def output(file = "#{Time.now.to_i}.tmp")
199
+ @output = File.dirname(file) if @output.nil?
200
+ @output = File.join(@output, File.basename(file).sub(/\.([^\.]+)$/, '.min.\1')) if File.directory?(@output)
201
+ @output = File.expand_path(@output)
202
+ end
203
+ end
204
+ end
205
+ end