cjohansen-juicer 0.2.0 → 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +17 -5
- data/Manifest.txt +33 -15
- data/Rakefile +22 -1
- data/Readme.rdoc +68 -32
- data/bin/juicer +1 -0
- data/lib/juicer.rb +26 -1
- data/lib/juicer/binary.rb +173 -0
- data/lib/juicer/cache_buster.rb +45 -0
- data/lib/juicer/chainable.rb +1 -0
- data/lib/juicer/cli.rb +13 -8
- data/lib/juicer/command/install.rb +59 -0
- data/lib/juicer/command/list.rb +50 -0
- data/lib/juicer/command/merge.rb +130 -31
- data/lib/juicer/command/util.rb +32 -0
- data/lib/juicer/command/verify.rb +60 -0
- data/lib/juicer/core.rb +61 -0
- data/lib/juicer/css_cache_buster.rb +106 -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 -72
- data/lib/juicer/merger/dependency_resolver.rb +34 -16
- data/lib/juicer/merger/stylesheet_merger.rb +71 -1
- data/lib/juicer/minifyer/yui_compressor.rb +20 -43
- data/tasks/test/setup.rake +35 -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 +160 -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 +2 -3
- data/test/juicer/merger/test_css_dependency_resolver.rb +8 -4
- data/test/juicer/merger/test_javascript_dependency_resolver.rb +6 -7
- data/test/juicer/merger/test_javascript_merger.rb +1 -2
- data/test/juicer/merger/test_stylesheet_merger.rb +118 -2
- data/test/juicer/minifyer/test_yui_compressor.rb +109 -29
- data/test/juicer/test_cache_buster.rb +58 -0
- data/test/juicer/test_chainable.rb +7 -0
- data/test/juicer/test_core.rb +47 -0
- data/test/juicer/test_css_cache_buster.rb +91 -0
- data/test/juicer/test_jslint.rb +33 -0
- data/test/test_helper.rb +65 -196
- metadata +77 -26
- data/.gitignore +0 -2
- data/juicer.gemspec +0 -38
- data/lib/juicer/minifyer/compressor.rb +0 -125
- data/test/juicer/minifyer/test_compressor.rb +0 -36
@@ -0,0 +1,45 @@
|
|
1
|
+
module Juicer
|
2
|
+
#
|
3
|
+
# Tool that assists in creating filenames that update everytime the file
|
4
|
+
# contents change. There's two ways of generating filenames, soft and hard.
|
5
|
+
# The point of all this is to facilitate configuring web servers to send
|
6
|
+
# static assets with a far future expires header - improving end user
|
7
|
+
# performance through caching.
|
8
|
+
#
|
9
|
+
# Soft cache busters require no web server configuration, but will not work
|
10
|
+
# as intended with older default configurations for popular proxy server
|
11
|
+
# Squid. The soft busters use query parameters to create unique file names,
|
12
|
+
# and these may not force an update in some cases. The soft cache busters
|
13
|
+
# transforms /images/logo.png to /images/logo.png?cb=1232923789
|
14
|
+
#
|
15
|
+
# Hard cache busters change the file name itself, and thus requires either
|
16
|
+
# the web server to (internally) rewrite requests for these files to the
|
17
|
+
# original ones, or the file names to actually change. Hard cache busters
|
18
|
+
# transforms /images/logo.png to /images/logo-1232923789.png
|
19
|
+
#
|
20
|
+
module CacheBuster
|
21
|
+
#
|
22
|
+
# Creates a unique file name for every revision to the files contents.
|
23
|
+
# Default parameter name for soft cache busters is cb (ie ?cb=<timestamp>)
|
24
|
+
# while default parameter names for hard cache busters is none (ie
|
25
|
+
# file-<timestamp>.png).
|
26
|
+
#
|
27
|
+
def self.path(file, type = :soft, param = :undef)
|
28
|
+
param = (type == :soft ? "jcb" : nil) if param == :undef
|
29
|
+
f = File.new(file.split("?").first)
|
30
|
+
mtime = f.mtime.to_i
|
31
|
+
f.close
|
32
|
+
|
33
|
+
if type == :soft
|
34
|
+
param = "#{param}".length == 0 ? "" : "#{param}="
|
35
|
+
file = file.sub(/#{param}\d+/, "").sub(/(\?|\&)$/, "")
|
36
|
+
"#{file}#{file.index('?') ? '&' : '?'}#{param}#{mtime}"
|
37
|
+
else
|
38
|
+
parts = file.split(".")
|
39
|
+
suffix = parts.pop
|
40
|
+
file = parts.join.sub(/-#{param}\d+/, "")
|
41
|
+
"#{parts.join('.')}-#{param}#{mtime}.#{suffix}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/juicer/chainable.rb
CHANGED
data/lib/juicer/cli.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
-
require
|
2
|
-
require 'cmdparse'
|
1
|
+
require "cmdparse"
|
3
2
|
|
4
3
|
# Command line interpreter for Juicer
|
5
4
|
#
|
6
5
|
module Juicer
|
7
6
|
class Cli
|
7
|
+
|
8
8
|
def initialize
|
9
|
-
|
9
|
+
@log = Juicer::LOGGER
|
10
|
+
@log.level = Logger::INFO
|
10
11
|
end
|
11
12
|
|
12
13
|
# Set up command parser and parse arguments
|
@@ -16,13 +17,17 @@ module Juicer
|
|
16
17
|
@cmd.program_name = "juicer"
|
17
18
|
@cmd.program_version = Juicer.version.split(".")
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
23
25
|
|
24
26
|
add_commands
|
25
27
|
@cmd.parse(arguments)
|
28
|
+
@log.close
|
29
|
+
rescue SystemExit
|
30
|
+
exit
|
26
31
|
end
|
27
32
|
|
28
33
|
# Run CLI
|
@@ -43,7 +48,7 @@ module Juicer
|
|
43
48
|
if Juicer.const_defined?("Command")
|
44
49
|
Juicer::Command.constants.each do |const|
|
45
50
|
const = Juicer::Command.const_get(const)
|
46
|
-
@cmd.add_command(const.new) if const.kind_of?(Class)
|
51
|
+
@cmd.add_command(const.new(@log)) if const.kind_of?(Class)
|
47
52
|
end
|
48
53
|
end
|
49
54
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "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
|
+
if args.length == 0
|
34
|
+
raise ArgumentError.new('Please provide a library to install')
|
35
|
+
end
|
36
|
+
|
37
|
+
args.each do |lib|
|
38
|
+
installer = Juicer::Install.get(lib).new(@path)
|
39
|
+
path = File.join(installer.install_dir, installer.path)
|
40
|
+
version = version(installer)
|
41
|
+
|
42
|
+
if installer.installed?(version)
|
43
|
+
@io.info "#{installer.name} #{version} is already installed in #{path}"
|
44
|
+
break
|
45
|
+
end
|
46
|
+
|
47
|
+
installer.install(version)
|
48
|
+
@io.info "Successfully installed #{lib.camel_case} #{version} in #{path}" if installer.installed?(version)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns which version to install
|
53
|
+
#
|
54
|
+
def version(installer)
|
55
|
+
@version ||= installer.latest
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "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(io = STDOUT)
|
16
|
+
super('list', false, true)
|
17
|
+
@io = io
|
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::Merger::JavaScriptDependencyResolver.new,
|
38
|
+
:css => Juicer::Merger::CssDependencyResolver.new }
|
39
|
+
|
40
|
+
files(args).each do |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
|
+
@io.puts "Dependency chain for #{relative file}:"
|
45
|
+
@io.puts " #{relative(types[type].resolve(file)).join("\n ")}\n\n"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/juicer/command/merge.rb
CHANGED
@@ -1,22 +1,38 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require File.join(File.dirname(__FILE__), "util")
|
2
|
+
require File.join(File.dirname(__FILE__), "verify")
|
3
|
+
require "cmdparse"
|
4
|
+
require "pathname"
|
4
5
|
|
5
6
|
module Juicer
|
6
7
|
module Command
|
7
8
|
# The compress command combines and minifies CSS and JavaScript files
|
8
9
|
#
|
9
10
|
class Merge < CmdParse::Command
|
11
|
+
include Juicer::Command::Util
|
12
|
+
|
10
13
|
# Initializes compress command
|
11
14
|
#
|
12
|
-
def initialize
|
15
|
+
def initialize(log = nil)
|
13
16
|
super('merge', false, true)
|
14
17
|
@types = { :js => Juicer::Merger::JavaScriptMerger,
|
15
18
|
:css => Juicer::Merger::StylesheetMerger }
|
16
|
-
@output = nil
|
17
|
-
@force = false
|
18
|
-
@
|
19
|
-
@
|
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
|
+
|
34
|
+
@log = log || Logger.new(STDOUT)
|
35
|
+
|
20
36
|
self.short_desc = "Combines and minifies CSS and JavaScript files"
|
21
37
|
self.description = <<-EOF
|
22
38
|
Each file provided as input will be checked for dependencies to other files,
|
@@ -36,35 +52,75 @@ the YUI Compressor the path should be the path to where the jar file is found.
|
|
36
52
|
EOF
|
37
53
|
|
38
54
|
self.options = CmdParse::OptionParserWrapper.new do |opt|
|
39
|
-
opt.on(
|
40
|
-
opt.on(
|
41
|
-
opt.on(
|
42
|
-
opt.on(
|
55
|
+
opt.on("-o", "--output file", "Output filename") { |filename| @output = filename }
|
56
|
+
opt.on("-p", "--path path", "Path to compressor binary") { |path| @opts[:bin_path] = path }
|
57
|
+
opt.on("-m", "--minifyer name", "Which minifer to use. Currently only supports yui_compressor") { |name| @minifyer = name }
|
58
|
+
opt.on("-f", "--force", "Force overwrite of target file") { @force = true }
|
59
|
+
opt.on("-a", "--arguments arguments", "Arguments to minifyer, escape with quotes") { |arguments| @arguments = arguments }
|
60
|
+
opt.on("-i", "--ignore-problems", "Merge and minify even if verifyer finds problems") { @ignore = true }
|
61
|
+
opt.on("-s", "--skip-verification", "Skip JsLint verification (js files only). Not recomended!") { @verify = false }
|
62
|
+
opt.on("-t", "--type type", "Juicer can only guess type when files have .css or .js extensions. Specify js or\n" +
|
63
|
+
(" " * 37) + "css with this option in cases where files have other extensions.") { |type| @type = type.to_sym }
|
64
|
+
opt.on("-h", "--hosts hosts", "Cycle asset hosts for referenced urls. Comma separated") { |hosts| @hosts = hosts.split(",") }
|
65
|
+
opt.on("-l", "--local-hosts hosts", "Host names that are served from --document-root (can be given cache busters). Comma separated") do |hosts|
|
66
|
+
@local_hosts = hosts.split(",")
|
67
|
+
end
|
68
|
+
opt.on("", "--all-hosts-local", "Treat all hosts as local (ie served from --document-root") { |t| @local_hosts = @hosts }
|
69
|
+
opt.on("-r", "--relative-urls", "Convert all referenced URLs to relative URLs. Requires --document-root if\n" +
|
70
|
+
(" " * 37) + "absolute URLs are used. Only valid for CSS files") { |t| @relative_urls = true }
|
71
|
+
opt.on("-b", "--absolute-urls", "Convert all referenced URLs to absolute URLs. Requires --document-root.\n" +
|
72
|
+
(" " * 37) + "Works with cycled asset hosts. Only valid for CSS files") { |t| @absolute_urls = true }
|
73
|
+
opt.on("-d", "--document-root dir", "Path to resolve absolute URLs relative to") { |path| @web_root = path }
|
74
|
+
opt.on("-c", "--cache-buster type", "none, soft or hard. Default is soft, which adds timestamps to referenced\n" +
|
75
|
+
(" " * 37) + "URLs as query parameters. None leaves URLs untouched and hard alters file names") do |type|
|
76
|
+
@cache_buster = [:soft, :hard].include?(type.to_sym) ? type.to_sym : nil
|
77
|
+
end
|
43
78
|
end
|
44
79
|
end
|
45
80
|
|
46
81
|
# Execute command
|
47
82
|
#
|
48
83
|
def execute(args)
|
49
|
-
if args.length == 0
|
50
|
-
|
84
|
+
if (files = files(args)).length == 0
|
85
|
+
@log.fatal "Please provide atleast one input file"
|
86
|
+
raise SystemExit.new("Please provide atleast one input file")
|
87
|
+
end
|
88
|
+
|
89
|
+
# Figure out which file to output to
|
90
|
+
output = output(files.first)
|
91
|
+
|
92
|
+
# Warn if file already exists
|
93
|
+
if File.exists?(output) && !@force
|
94
|
+
msg = "Unable to continue, #{output} exists. Run again with --force to overwrite"
|
95
|
+
@log.fatal msg
|
96
|
+
raise SystemExit.new(msg)
|
51
97
|
end
|
52
98
|
|
53
|
-
#
|
54
|
-
#
|
55
|
-
|
99
|
+
# Set up merger to resolve imports and so on. Do not touch URLs now, if
|
100
|
+
# asset host cycling is added at this point, the cache buster WILL be
|
101
|
+
# confused
|
102
|
+
merger = merger(output).new(files, :relative_urls => @relative_urls,
|
103
|
+
:absolute_urls => @absolute_urls,
|
104
|
+
:web_root => @web_root,
|
105
|
+
:hosts => @hosts)
|
56
106
|
|
57
|
-
if
|
58
|
-
|
59
|
-
|
107
|
+
# Fail if syntax trouble (js only)
|
108
|
+
if @verify && !Juicer::Command::Verify.check_all(merger.files.reject { |f| f =~ /\.css$/ }, @log)
|
109
|
+
@log.error "Problems were detected during verification"
|
110
|
+
raise SystemExit.new("Input files contain problems") unless @ignore
|
111
|
+
@log.warn "Ignoring detected problems"
|
60
112
|
end
|
61
113
|
|
62
|
-
|
63
|
-
merger.set_next(minifyer)
|
64
|
-
merger.save(
|
114
|
+
# Set command chain and execute
|
115
|
+
merger.set_next(cache_buster(output)).set_next(minifyer)
|
116
|
+
merger.save(output)
|
65
117
|
|
66
118
|
# Print report
|
67
|
-
|
119
|
+
@log.info "Produced #{relative output} from"
|
120
|
+
merger.files.each { |file| @log.info " #{relative file}" }
|
121
|
+
rescue FileNotFoundError => err
|
122
|
+
# Handle missing document-root option
|
123
|
+
puts err.message.sub(/:web_root/, "--document-root")
|
68
124
|
end
|
69
125
|
|
70
126
|
private
|
@@ -72,18 +128,61 @@ the YUI Compressor the path should be the path to where the jar file is found.
|
|
72
128
|
# Resolve and load minifyer
|
73
129
|
#
|
74
130
|
def minifyer
|
131
|
+
return nil if @minifyer.nil? || @minifyer == "" || @minifyer.downcase == "none"
|
132
|
+
|
75
133
|
begin
|
76
|
-
|
77
|
-
compressor = Juicer::Minifyer
|
134
|
+
@opts[:bin_path] = File.join(Juicer.home, "lib", @minifyer, "bin") unless @opts[:bin_path]
|
135
|
+
compressor = @minifyer.classify(Juicer::Minifyer).new(@opts)
|
136
|
+
compressor.set_opts(@arguments) if @arguments
|
137
|
+
@log.debug "Using #{@minifyer.camel_case} for minification"
|
138
|
+
|
139
|
+
return compressor
|
78
140
|
rescue NameError
|
79
|
-
|
80
|
-
|
141
|
+
@log.fatal "No such minifyer '#{@minifyer}', aborting"
|
142
|
+
raise SystemExit.new("No such minifyer '#{@minifyer}', aborting")
|
143
|
+
rescue FileNotFoundError => e
|
144
|
+
@log.fatal e.message
|
145
|
+
@log.fatal "Try installing with; juicer install #{@minifyer.underscore}"
|
146
|
+
raise SystemExit.new(e.message)
|
81
147
|
rescue Exception => e
|
82
|
-
|
83
|
-
|
148
|
+
@log.fatal e.message
|
149
|
+
raise SystemExit.new(e.message)
|
84
150
|
end
|
85
151
|
|
86
|
-
|
152
|
+
nil
|
153
|
+
end
|
154
|
+
|
155
|
+
#
|
156
|
+
# Resolve and load merger
|
157
|
+
#
|
158
|
+
def merger(output = "")
|
159
|
+
@type ||= output.split(/\.([^\.]*)$/)[1]
|
160
|
+
type = @type.to_sym if @type
|
161
|
+
|
162
|
+
if !@types.include?(type)
|
163
|
+
@log.warn "Unknown type '#{type}', defaulting to 'js'"
|
164
|
+
type = :js
|
165
|
+
end
|
166
|
+
|
167
|
+
@types[type]
|
168
|
+
end
|
169
|
+
|
170
|
+
#
|
171
|
+
# Load cache buster, only available for CSS files
|
172
|
+
#
|
173
|
+
def cache_buster(file)
|
174
|
+
return nil if !file || file !~ /\.css$/ || @cache_buster.nil?
|
175
|
+
Juicer::CssCacheBuster.new(:web_root => @web_root, :type => @cache_buster, :hosts => @local_hosts)
|
176
|
+
end
|
177
|
+
|
178
|
+
#
|
179
|
+
# Generate output file name. Optional argument is a filename to base the new
|
180
|
+
# name on. It will prepend the original suffix with ".min"
|
181
|
+
#
|
182
|
+
def output(file = "#{Time.now.to_i}.tmp")
|
183
|
+
@output = File.dirname(file) if @output.nil?
|
184
|
+
@output = File.join(@output, File.basename(file).sub(/\.([^\.]+)$/, '.min.\1')) if File.directory?(@output)
|
185
|
+
@output = File.expand_path(@output)
|
87
186
|
end
|
88
187
|
end
|
89
188
|
end
|
@@ -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
|
+
|