ktheory-juicer 1.0.0.ktheory1

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.
Files changed (98) hide show
  1. data/History.txt +30 -0
  2. data/Manifest.txt +58 -0
  3. data/Rakefile +96 -0
  4. data/Readme.rdoc +312 -0
  5. data/VERSION +1 -0
  6. data/bin/juicer +8 -0
  7. data/lib/juicer.rb +70 -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 +130 -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 +80 -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 +136 -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/data/Changelog.txt +10 -0
  50. data/test/data/a.css +3 -0
  51. data/test/data/a.js +5 -0
  52. data/test/data/a1.css +5 -0
  53. data/test/data/b.css +1 -0
  54. data/test/data/b.js +5 -0
  55. data/test/data/b1.css +5 -0
  56. data/test/data/c1.css +3 -0
  57. data/test/data/css/2.gif +1 -0
  58. data/test/data/css/test.css +11 -0
  59. data/test/data/css/test2.css +1 -0
  60. data/test/data/d1.css +3 -0
  61. data/test/data/images/1.png +1 -0
  62. data/test/data/my_app.js +2 -0
  63. data/test/data/not-ok.js +2 -0
  64. data/test/data/ok.js +3 -0
  65. data/test/data/path_test.css +5 -0
  66. data/test/data/path_test2.css +14 -0
  67. data/test/data/pkg/module/moda.js +2 -0
  68. data/test/data/pkg/module/modb.js +3 -0
  69. data/test/data/pkg/pkg.js +1 -0
  70. data/test/test_helper.rb +169 -0
  71. data/test/unit/juicer/asset/path_resolver_test.rb +76 -0
  72. data/test/unit/juicer/asset/path_test.rb +370 -0
  73. data/test/unit/juicer/cache_buster_test.rb +104 -0
  74. data/test/unit/juicer/chainable_test.rb +94 -0
  75. data/test/unit/juicer/command/install_test.rb +58 -0
  76. data/test/unit/juicer/command/list_test.rb +81 -0
  77. data/test/unit/juicer/command/merge_test.rb +162 -0
  78. data/test/unit/juicer/command/util_test.rb +58 -0
  79. data/test/unit/juicer/command/verify_test.rb +48 -0
  80. data/test/unit/juicer/css_cache_buster_test.rb +71 -0
  81. data/test/unit/juicer/datafy_test.rb +37 -0
  82. data/test/unit/juicer/dependency_resolver/css_dependency_resolver_test.rb +36 -0
  83. data/test/unit/juicer/dependency_resolver/javascript_dependency_resolver_test.rb +50 -0
  84. data/test/unit/juicer/ext/string_test.rb +59 -0
  85. data/test/unit/juicer/ext/symbol_test.rb +27 -0
  86. data/test/unit/juicer/image_embed_test.rb +271 -0
  87. data/test/unit/juicer/install/installer_base_test.rb +214 -0
  88. data/test/unit/juicer/install/jslint_installer_test.rb +54 -0
  89. data/test/unit/juicer/install/rhino_installer_test.rb +57 -0
  90. data/test/unit/juicer/install/yui_compressor_test.rb +56 -0
  91. data/test/unit/juicer/jslint_test.rb +60 -0
  92. data/test/unit/juicer/merger/base_test.rb +122 -0
  93. data/test/unit/juicer/merger/javascript_merger_test.rb +74 -0
  94. data/test/unit/juicer/merger/stylesheet_merger_test.rb +180 -0
  95. data/test/unit/juicer/minifyer/closure_compressor_test.rb +107 -0
  96. data/test/unit/juicer/minifyer/yui_compressor_test.rb +116 -0
  97. data/test/unit/juicer_test.rb +1 -0
  98. metadata +265 -0
@@ -0,0 +1,130 @@
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
+ file = self.clean(file, parameter)
83
+ filename = file.split("?").first
84
+ raise ArgumentError.new("#{file} could not be found") unless File.exists?(filename)
85
+ mtime = File.mtime(filename).to_i
86
+ type = [:soft, :hard].include?(type) ? type : :soft
87
+
88
+ if type == :soft
89
+ parameter = "#{parameter}=".sub(/^=$/, '')
90
+ return "#{file}#{file.index('?') ? '&' : '?'}#{parameter}#{mtime}"
91
+ end
92
+
93
+ file.sub(/(\.[^\.]+$)/, "-#{parameter}#{mtime}" + '\1')
94
+ end
95
+
96
+ #
97
+ # Add a hard cache buster to a filename. The parameter is an optional prefix
98
+ # that is added before the mtime timestamp. It results in filenames of the form:
99
+ # <tt>file-[parameter name][timestamp].suffix</tt>, ie
100
+ # <tt>images/logo-cb1234567890.png</tt> which is the case for the default
101
+ # parameter name "cb" (as in *c*ache *b*uster).
102
+ #
103
+ def self.hard(file, parameter = DEFAULT_PARAMETER)
104
+ self.path(file, :hard, parameter)
105
+ end
106
+
107
+ #
108
+ # Add a soft cache buster to a filename. The parameter is an optional name
109
+ # for the mtime timestamp value. It results in filenames of the form:
110
+ # <tt>file.suffix?[parameter name]=[timestamp]</tt>, ie
111
+ # <tt>images/logo.png?cb=1234567890</tt> which is the case for the default
112
+ # parameter name "cb" (as in *c*ache *b*uster).
113
+ #
114
+ def self.soft(file, parameter = DEFAULT_PARAMETER)
115
+ self.path(file, :soft, parameter)
116
+ end
117
+
118
+ #
119
+ # Remove cache buster from a URL for a given parameter name. Parameter name is
120
+ # "cb" by default.
121
+ #
122
+ def self.clean(file, parameter = DEFAULT_PARAMETER)
123
+ query_param = "#{parameter}".length == 0 ? "" : "#{parameter}="
124
+ new_file = file.sub(/#{query_param}\d+&?/, "").sub(/(\?|&)$/, "")
125
+ return new_file unless new_file == file
126
+
127
+ file.sub(/-#{parameter}\d+(\.\w+)($|\?)/, '\1\2')
128
+ end
129
+ end
130
+ 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
data/lib/juicer/cli.rb ADDED
@@ -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
+ @web_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 :web_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| @web_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
+ :web_root => @web_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(/:web_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(:web_root => @web_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