blob: 754dad9c509e11ad8da84f6b51432ec98001dead [file] [log] [blame]
require 'set'
module Sass
module Importers
# The default importer, used for any strings found in the load path.
# Simply loads Sass files from the filesystem using the default logic.
class Filesystem < Base
attr_accessor :root
# Creates a new filesystem importer that imports files relative to a given path.
#
# @param root [String] The root path.
# This importer will import files relative to this path.
def initialize(root)
@root = File.expand_path(root)
@real_root = Sass::Util.realpath(@root).to_s
@same_name_warnings = Set.new
end
# @see Base#find_relative
def find_relative(name, base, options)
_find(File.dirname(base), name, options)
end
# @see Base#find
def find(name, options)
_find(@root, name, options)
end
# @see Base#mtime
def mtime(name, options)
file, _ = Sass::Util.destructure(find_real_file(@root, name, options))
File.mtime(file) if file
rescue Errno::ENOENT
nil
end
# @see Base#key
def key(name, options)
[self.class.name + ":" + File.dirname(File.expand_path(name)),
File.basename(name)]
end
# @see Base#to_s
def to_s
@root
end
def hash
@root.hash
end
def eql?(other)
!other.nil? && other.respond_to?(:root) && root.eql?(other.root)
end
# @see Base#directories_to_watch
def directories_to_watch
[root]
end
# @see Base#watched_file?
def watched_file?(filename)
# Check against the root with symlinks resolved, since Listen
# returns fully-resolved paths.
filename =~ /\.s[ac]ss$/ && filename.start_with?(@real_root + File::SEPARATOR)
end
def public_url(name, sourcemap_directory)
file_pathname = Sass::Util.cleanpath(File.absolute_path(name, @root))
return Sass::Util.file_uri_from_path(file_pathname) if sourcemap_directory.nil?
sourcemap_pathname = Sass::Util.cleanpath(sourcemap_directory)
begin
Sass::Util.file_uri_from_path(
Sass::Util.relative_path_from(file_pathname, sourcemap_pathname))
rescue ArgumentError # when a relative path cannot be constructed
Sass::Util.file_uri_from_path(file_pathname)
end
end
protected
# If a full uri is passed, this removes the root from it
# otherwise returns the name unchanged
def remove_root(name)
if name.index(@root + "/") == 0
name[(@root.length + 1)..-1]
else
name
end
end
# A hash from file extensions to the syntaxes for those extensions.
# The syntaxes must be `:sass` or `:scss`.
#
# This can be overridden by subclasses that want normal filesystem importing
# with unusual extensions.
#
# @return [{String => Symbol}]
def extensions
{'sass' => :sass, 'scss' => :scss}
end
# Given an `@import`ed path, returns an array of possible
# on-disk filenames and their corresponding syntaxes for that path.
#
# @param name [String] The filename.
# @return [Array(String, Symbol)] An array of pairs.
# The first element of each pair is a filename to look for;
# the second element is the syntax that file would be in (`:sass` or `:scss`).
def possible_files(name)
name = escape_glob_characters(name)
dirname, basename, extname = split(name)
sorted_exts = extensions.sort
syntax = extensions[extname]
if syntax
ret = [["#{dirname}/{_,}#{basename}.#{extensions.invert[syntax]}", syntax]]
else
ret = sorted_exts.map {|ext, syn| ["#{dirname}/{_,}#{basename}.#{ext}", syn]}
end
# JRuby chokes when trying to import files from JARs when the path starts with './'.
ret.map {|f, s| [f.sub(%r{^\./}, ''), s]}
end
def escape_glob_characters(name)
name.gsub(/[\*\[\]\{\}\?]/) do |char|
"\\#{char}"
end
end
REDUNDANT_DIRECTORY = /#{Regexp.escape(File::SEPARATOR)}\.#{Regexp.escape(File::SEPARATOR)}/
# Given a base directory and an `@import`ed name,
# finds an existent file that matches the name.
#
# @param dir [String] The directory relative to which to search.
# @param name [String] The filename to search for.
# @return [(String, Symbol)] A filename-syntax pair.
def find_real_file(dir, name, options)
# On windows 'dir' or 'name' can be in native File::ALT_SEPARATOR form.
dir = dir.gsub(File::ALT_SEPARATOR, File::SEPARATOR) unless File::ALT_SEPARATOR.nil?
name = name.gsub(File::ALT_SEPARATOR, File::SEPARATOR) unless File::ALT_SEPARATOR.nil?
found = possible_files(remove_root(name)).map do |f, s|
path = if dir == "." || Sass::Util.pathname(f).absolute?
f
else
"#{escape_glob_characters(dir)}/#{f}"
end
Dir[path].map do |full_path|
full_path.gsub!(REDUNDANT_DIRECTORY, File::SEPARATOR)
[Sass::Util.cleanpath(full_path).to_s, s]
end
end.flatten(1)
return if found.empty?
if found.size > 1 && !@same_name_warnings.include?(found.first.first)
found.each {|(f, _)| @same_name_warnings << f}
relative_to = Sass::Util.pathname(dir)
if options[:_from_import_node]
# If _line exists, we're here due to an actual import in an
# import_node and we want to print a warning for a user writing an
# ambiguous import.
candidates = found.map do |(f, _)|
" " + Sass::Util.pathname(f).relative_path_from(relative_to).to_s
end.join("\n")
raise Sass::SyntaxError.new(<<MESSAGE)
It's not clear which file to import for '@import "#{name}"'.
Candidates:
#{candidates}
Please delete or rename all but one of these files.
MESSAGE
else
# Otherwise, we're here via StalenessChecker, and we want to print a
# warning for a user running `sass --watch` with two ambiguous files.
candidates = found.map {|(f, _)| " " + File.basename(f)}.join("\n")
Sass::Util.sass_warn <<WARNING
WARNING: In #{File.dirname(name)}:
There are multiple files that match the name "#{File.basename(name)}":
#{candidates}
WARNING
end
end
found.first
end
# Splits a filename into three parts, a directory part, a basename, and an extension
# Only the known extensions returned from the extensions method will be recognized as such.
def split(name)
extension = nil
dirname, basename = File.dirname(name), File.basename(name)
if basename =~ /^(.*)\.(#{extensions.keys.map {|e| Regexp.escape(e)}.join('|')})$/
basename = $1
extension = $2
end
[dirname, basename, extension]
end
private
def _find(dir, name, options)
full_filename, syntax = Sass::Util.destructure(find_real_file(dir, name, options))
return unless full_filename && File.readable?(full_filename)
# TODO: this preserves historical behavior, but it's possible
# :filename should be either normalized to the native format
# or consistently URI-format.
full_filename = full_filename.tr("\\", "/") if Sass::Util.windows?
options[:syntax] = syntax
options[:filename] = full_filename
options[:importer] = self
Sass::Engine.new(File.read(full_filename), options)
end
end
end
end