require 'pathname' require 'hike/extensions' require 'hike/index' require 'hike/paths' module Hike # `Trail` is the public container class for holding paths and extensions. class Trail # `Trail#paths` is a mutable `Paths` collection. # # trail = Hike::Trail.new # trail.paths.push "~/Projects/hike/lib", "~/Projects/hike/test" # # The order of the paths is significant. Paths in the beginning of # the collection will be checked first. In the example above, # `~/Projects/hike/lib/hike.rb` would shadow the existent of # `~/Projects/hike/test/hike.rb`. attr_reader :paths # `Trail#extensions` is a mutable `Extensions` collection. # # trail = Hike::Trail.new # trail.paths.push "~/Projects/hike/lib" # trail.extensions.push ".rb" # # Extensions allow you to find files by just their name omitting # their extension. Is similar to Ruby's require mechanism that # allows you to require files with specifiying `foo.rb`. attr_reader :extensions # `Index#aliases` is a mutable `Hash` mapping an extension to # an `Array` of aliases. # # trail = Hike::Trail.new # trail.paths.push "~/Projects/hike/site" # trail.aliases['.htm'] = 'html' # trail.aliases['.xhtml'] = 'html' # trail.aliases['.php'] = 'html' # # Aliases provide a fallback when the primary extension is not # matched. In the example above, a lookup for "foo.html" will # check for the existence of "foo.htm", "foo.xhtml", or "foo.php". attr_reader :aliases # A Trail accepts an optional root path that defaults to your # current working directory. Any relative paths added to # `Trail#paths` will expanded relative to the root. def initialize(root = ".") @root = Pathname.new(root).expand_path @paths = Paths.new(@root) @extensions = Extensions.new @aliases = Hash.new { |h, k| h[k] = Extensions.new } end # `Trail#root` returns root path as a `String`. This attribute is immutable. def root @root.to_s end # Prepend `path` to `Paths` collection def prepend_paths(*paths) self.paths.unshift(*paths) end alias_method :prepend_path, :prepend_paths # Append `path` to `Paths` collection def append_paths(*paths) self.paths.push(*paths) end alias_method :append_path, :append_paths # Remove `path` from `Paths` collection def remove_path(path) self.paths.delete(path) end # Prepend `extension` to `Extensions` collection def prepend_extensions(*extensions) self.extensions.unshift(*extensions) end alias_method :prepend_extension, :prepend_extensions # Append `extension` to `Extensions` collection def append_extensions(*extensions) self.extensions.push(*extensions) end alias_method :append_extension, :append_extensions # Remove `extension` from `Extensions` collection def remove_extension(extension) self.extensions.delete(extension) end # Alias `new_extension` to `old_extension` def alias_extension(new_extension, old_extension) aliases[normalize_extension(new_extension)] = normalize_extension(old_extension) end # Remove the alias for `extension` def unalias_extension(extension) aliases.delete(normalize_extension(extension)) end # `Trail#find` returns a the expand path for a logical path in the # path collection. # # trail = Hike::Trail.new "~/Projects/hike" # trail.extensions.push ".rb" # trail.paths.push "lib", "test" # # trail.find "hike/trail" # # => "~/Projects/hike/lib/hike/trail.rb" # # trail.find "test_trail" # # => "~/Projects/hike/test/test_trail.rb" # # `find` accepts multiple fallback logical paths that returns the # first match. # # trail.find "hike", "hike/index" # # is equivalent to # # trail.find("hike") || trail.find("hike/index") # # Though `find` always returns the first match, it is possible # to iterate over all shadowed matches and fallbacks by supplying # a block. # # trail.find("hike", "hike/index") { |path| warn path } # # This allows you to filter your matches by any condition. # # trail.find("application") do |path| # return path if mime_type_for(path) == "text/css" # end # def find(*args, &block) index.find(*args, &block) end # `Trail#index` returns an `Index` object that has the same # interface as `Trail`. An `Index` is a cached `Trail` object that # does not update when the file system changes. If you are # confident that you are not making changes the paths you are # searching, `index` will avoid excess system calls. # # index = trail.index # index.find "hike/trail" # index.find "test_trail" # def index Index.new(root, paths, extensions, aliases) end # `Trail#entries` is equivalent to `Dir#entries`. It is not # recommend to use this method for general purposes. It exists for # parity with `Index#entries`. def entries(*args) index.entries(*args) end # `Trail#stat` is equivalent to `File#stat`. It is not # recommend to use this method for general purposes. It exists for # parity with `Index#stat`. def stat(*args) index.stat(*args) end private def normalize_extension(extension) if extension[/^\./] extension else ".#{extension}" end end end end