123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 |
- require 'pathname'
- module Hike
- # `Index` is an internal cached variant of `Trail`. It assumes the
- # file system does not change between `find` calls. All `stat` and
- # `entries` calls are cached for the lifetime of the `Index` object.
- class Index
- # `Index#paths` is an immutable `Paths` collection.
- attr_reader :paths
- # `Index#extensions` is an immutable `Extensions` collection.
- attr_reader :extensions
- # `Index#aliases` is an immutable `Hash` mapping an extension to
- # an `Array` of aliases.
- attr_reader :aliases
- # `Index.new` is an internal method. Instead of constructing it
- # directly, create a `Trail` and call `Trail#index`.
- def initialize(root, paths, extensions, aliases)
- @root = root
- # Freeze is used here so an error is throw if a mutator method
- # is called on the array. Mutating `@paths`, `@extensions`, or
- # `@aliases` would have unpredictable results.
- @paths = paths.dup.freeze
- @extensions = extensions.dup.freeze
- @aliases = aliases.inject({}) { |h, (k, a)|
- h[k] = a.dup.freeze; h
- }.freeze
- @pathnames = paths.map { |path| Pathname.new(path) }
- @stats = {}
- @entries = {}
- @patterns = {}
- end
- # `Index#root` returns root path as a `String`. This attribute is immutable.
- def root
- @root.to_s
- end
- # `Index#index` returns `self` to be compatable with the `Trail` interface.
- def index
- self
- end
- # The real implementation of `find`. `Trail#find` generates a one
- # time index and delegates here.
- #
- # See `Trail#find` for usage.
- def find(*logical_paths, &block)
- if block_given?
- options = extract_options!(logical_paths)
- base_path = Pathname.new(options[:base_path] || @root)
- logical_paths.each do |logical_path|
- logical_path = Pathname.new(logical_path.sub(/^\//, ''))
- if relative?(logical_path)
- find_in_base_path(logical_path, base_path, &block)
- else
- find_in_paths(logical_path, &block)
- end
- end
- nil
- else
- find(*logical_paths) do |path|
- return path
- end
- end
- end
- # A cached version of `Dir.entries` that filters out `.` files and
- # `~` swap files. Returns an empty `Array` if the directory does
- # not exist.
- def entries(path)
- key = path.to_s
- @entries[key] ||= Pathname.new(path).entries.reject { |entry| entry.to_s =~ /^\.|~$|^\#.*\#$/ }.sort
- rescue Errno::ENOENT
- @entries[key] = []
- end
- # A cached version of `File.stat`. Returns nil if the file does
- # not exist.
- def stat(path)
- key = path.to_s
- if @stats.key?(key)
- @stats[key]
- else
- begin
- @stats[key] = File.stat(path)
- rescue Errno::ENOENT
- @stats[key] = nil
- end
- end
- end
- protected
- def extract_options!(arguments)
- arguments.last.is_a?(Hash) ? arguments.pop.dup : {}
- end
- def relative?(logical_path)
- logical_path.to_s =~ /^\.\.?\//
- end
- # Finds logical path across all `paths`
- def find_in_paths(logical_path, &block)
- dirname, basename = logical_path.split
- @pathnames.each do |base_path|
- match(base_path.join(dirname), basename, &block)
- end
- end
- # Finds relative logical path, `../test/test_trail`. Requires a
- # `base_path` for reference.
- def find_in_base_path(logical_path, base_path, &block)
- candidate = base_path.join(logical_path)
- dirname, basename = candidate.split
- match(dirname, basename, &block) if paths_contain?(dirname)
- end
- # Checks if the path is actually on the file system and performs
- # any syscalls if necessary.
- def match(dirname, basename)
- # Potential `entries` syscall
- matches = entries(dirname)
- pattern = pattern_for(basename)
- matches = matches.select { |m| m.to_s =~ pattern }
- sort_matches(matches, basename).each do |path|
- pathname = dirname.join(path)
- # Potential `stat` syscall
- stat = stat(pathname)
- # Exclude directories
- if stat && stat.file?
- yield pathname.to_s
- end
- end
- end
- # Returns true if `dirname` is a subdirectory of any of the `paths`
- def paths_contain?(dirname)
- paths.any? { |path| dirname.to_s[0, path.length] == path }
- end
- # Cache results of `build_pattern_for`
- def pattern_for(basename)
- @patterns[basename] ||= build_pattern_for(basename)
- end
- # Returns a `Regexp` that matches the allowed extensions.
- #
- # pattern_for("index.html") #=> /^index(.html|.htm)(.builder|.erb)*$/
- def build_pattern_for(basename)
- extname = basename.extname
- aliases = find_aliases_for(extname)
- if aliases.any?
- basename = basename.basename(extname)
- aliases = [extname] + aliases
- aliases_pattern = aliases.map { |e| Regexp.escape(e) }.join("|")
- basename_re = Regexp.escape(basename.to_s) + "(?:#{aliases_pattern})"
- else
- basename_re = Regexp.escape(basename.to_s)
- end
- extension_pattern = extensions.map { |e| Regexp.escape(e) }.join("|")
- /^#{basename_re}(?:#{extension_pattern})*$/
- end
- # Sorts candidate matches by their extension
- # priority. Extensions in the front of the `extensions` carry
- # more weight.
- def sort_matches(matches, basename)
- aliases = find_aliases_for(basename.extname)
- matches.sort_by do |match|
- extnames = match.sub(basename.to_s, '').to_s.scan(/\.[^.]+/)
- extnames.inject(0) do |sum, ext|
- if i = extensions.index(ext)
- sum + i + 1
- elsif i = aliases.index(ext)
- sum + i + 11
- else
- sum
- end
- end
- end
- end
- def find_aliases_for(extension)
- @aliases.inject([]) do |aliases, (key, value)|
- aliases.push(key) if value == extension
- aliases
- end
- end
- end
- end
|