232 lines
6.6 KiB
Ruby
232 lines
6.6 KiB
Ruby
|
# frozen_string_literal: true
|
||
|
|
||
|
require "erb"
|
||
|
require "set"
|
||
|
require "enumerator"
|
||
|
require "stringio"
|
||
|
require "rbconfig"
|
||
|
require "uri"
|
||
|
require "thread"
|
||
|
require "pathname"
|
||
|
|
||
|
# A module containing various useful functions.
|
||
|
|
||
|
module SassC::Util
|
||
|
|
||
|
extend self
|
||
|
|
||
|
# An array of ints representing the Ruby version number.
|
||
|
# @api public
|
||
|
RUBY_VERSION_COMPONENTS = RUBY_VERSION.split(".").map {|s| s.to_i}
|
||
|
|
||
|
# The Ruby engine we're running under. Defaults to `"ruby"`
|
||
|
# if the top-level constant is undefined.
|
||
|
# @api public
|
||
|
RUBY_ENGINE = defined?(::RUBY_ENGINE) ? ::RUBY_ENGINE : "ruby"
|
||
|
|
||
|
# Maps the keys in a hash according to a block.
|
||
|
# @example
|
||
|
# map_keys({:foo => "bar", :baz => "bang"}) {|k| k.to_s}
|
||
|
# #=> {"foo" => "bar", "baz" => "bang"}
|
||
|
# @param hash [Hash] The hash to map
|
||
|
# @yield [key] A block in which the keys are transformed
|
||
|
# @yieldparam key [Object] The key that should be mapped
|
||
|
# @yieldreturn [Object] The new value for the key
|
||
|
# @return [Hash] The mapped hash
|
||
|
# @see #map_vals
|
||
|
# @see #map_hash
|
||
|
def map_keys(hash)
|
||
|
map_hash(hash) {|k, v| [yield(k), v]}
|
||
|
end
|
||
|
|
||
|
# Restricts the numeric `value` to be within `min` and `max`, inclusive.
|
||
|
# If the value is lower than `min`
|
||
|
def clamp(value, min, max)
|
||
|
return min if value < min
|
||
|
return max if value > max
|
||
|
return value
|
||
|
end
|
||
|
|
||
|
# Like [Fixnum.round], but leaves rooms for slight floating-point
|
||
|
# differences.
|
||
|
#
|
||
|
# @param value [Numeric]
|
||
|
# @return [Numeric]
|
||
|
def round(value)
|
||
|
# If the number is within epsilon of X.5, round up (or down for negative
|
||
|
# numbers).
|
||
|
mod = value % 1
|
||
|
mod_is_half = (mod - 0.5).abs < SassC::Script::Value::Number.epsilon
|
||
|
if value > 0
|
||
|
!mod_is_half && mod < 0.5 ? value.floor : value.ceil
|
||
|
else
|
||
|
mod_is_half || mod < 0.5 ? value.floor : value.ceil
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# Return an array of all possible paths through the given arrays.
|
||
|
#
|
||
|
# @param arrs [Array<Array>]
|
||
|
# @return [Array<Arrays>]
|
||
|
#
|
||
|
# @example
|
||
|
# paths([[1, 2], [3, 4], [5]]) #=>
|
||
|
# # [[1, 3, 5],
|
||
|
# # [2, 3, 5],
|
||
|
# # [1, 4, 5],
|
||
|
# # [2, 4, 5]]
|
||
|
def paths(arrs)
|
||
|
arrs.inject([[]]) do |paths, arr|
|
||
|
arr.map {|e| paths.map {|path| path + [e]}}.flatten(1)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# Returns information about the caller of the previous method.
|
||
|
#
|
||
|
# @param entry [String] An entry in the `#caller` list, or a similarly formatted string
|
||
|
# @return [[String, Integer, (String, nil)]]
|
||
|
# An array containing the filename, line, and method name of the caller.
|
||
|
# The method name may be nil
|
||
|
def caller_info(entry = nil)
|
||
|
# JRuby evaluates `caller` incorrectly when it's in an actual default argument.
|
||
|
entry ||= caller[1]
|
||
|
info = entry.scan(/^((?:[A-Za-z]:)?.*?):(-?.*?)(?::.*`(.+)')?$/).first
|
||
|
info[1] = info[1].to_i
|
||
|
# This is added by Rubinius to designate a block, but we don't care about it.
|
||
|
info[2].sub!(/ \{\}\Z/, '') if info[2]
|
||
|
info
|
||
|
end
|
||
|
|
||
|
# Throws a NotImplementedError for an abstract method.
|
||
|
#
|
||
|
# @param obj [Object] `self`
|
||
|
# @raise [NotImplementedError]
|
||
|
def abstract(obj)
|
||
|
raise NotImplementedError.new("#{obj.class} must implement ##{caller_info[2]}")
|
||
|
end
|
||
|
|
||
|
# Prints a deprecation warning for the caller method.
|
||
|
#
|
||
|
# @param obj [Object] `self`
|
||
|
# @param message [String] A message describing what to do instead.
|
||
|
def deprecated(obj, message = nil)
|
||
|
obj_class = obj.is_a?(Class) ? "#{obj}." : "#{obj.class}#"
|
||
|
full_message = "DEPRECATION WARNING: #{obj_class}#{caller_info[2]} " +
|
||
|
"will be removed in a future version of Sass.#{("\n" + message) if message}"
|
||
|
SassC::Util.sass_warn full_message
|
||
|
end
|
||
|
|
||
|
# Silences all Sass warnings within a block.
|
||
|
#
|
||
|
# @yield A block in which no Sass warnings will be printed
|
||
|
def silence_sass_warnings
|
||
|
old_level, Sass.logger.log_level = Sass.logger.log_level, :error
|
||
|
yield
|
||
|
ensure
|
||
|
SassC.logger.log_level = old_level
|
||
|
end
|
||
|
|
||
|
# The same as `Kernel#warn`, but is silenced by \{#silence\_sass\_warnings}.
|
||
|
#
|
||
|
# @param msg [String]
|
||
|
def sass_warn(msg)
|
||
|
Sass.logger.warn("#{msg}\n")
|
||
|
end
|
||
|
|
||
|
## Cross Rails Version Compatibility
|
||
|
|
||
|
# Returns the root of the Rails application,
|
||
|
# if this is running in a Rails context.
|
||
|
# Returns `nil` if no such root is defined.
|
||
|
#
|
||
|
# @return [String, nil]
|
||
|
def rails_root
|
||
|
if defined?(::Rails.root)
|
||
|
return ::Rails.root.to_s if ::Rails.root
|
||
|
raise "ERROR: Rails.root is nil!"
|
||
|
end
|
||
|
return RAILS_ROOT.to_s if defined?(RAILS_ROOT)
|
||
|
nil
|
||
|
end
|
||
|
|
||
|
# Returns the environment of the Rails application,
|
||
|
# if this is running in a Rails context.
|
||
|
# Returns `nil` if no such environment is defined.
|
||
|
#
|
||
|
# @return [String, nil]
|
||
|
def rails_env
|
||
|
return ::Rails.env.to_s if defined?(::Rails.env)
|
||
|
return RAILS_ENV.to_s if defined?(RAILS_ENV)
|
||
|
nil
|
||
|
end
|
||
|
|
||
|
## Cross-OS Compatibility
|
||
|
#
|
||
|
# These methods are cached because some of them are called quite frequently
|
||
|
# and even basic checks like String#== are too costly to be called repeatedly.
|
||
|
|
||
|
# Whether or not this is running on Windows.
|
||
|
#
|
||
|
# @return [Boolean]
|
||
|
def windows?
|
||
|
return @windows if defined?(@windows)
|
||
|
@windows = (RbConfig::CONFIG['host_os'] =~ /mswin|windows|mingw/i)
|
||
|
end
|
||
|
|
||
|
# Whether or not this is running on IronRuby.
|
||
|
#
|
||
|
# @return [Boolean]
|
||
|
def ironruby?
|
||
|
return @ironruby if defined?(@ironruby)
|
||
|
@ironruby = RUBY_ENGINE == "ironruby"
|
||
|
end
|
||
|
|
||
|
# Whether or not this is running on Rubinius.
|
||
|
#
|
||
|
# @return [Boolean]
|
||
|
def rbx?
|
||
|
return @rbx if defined?(@rbx)
|
||
|
@rbx = RUBY_ENGINE == "rbx"
|
||
|
end
|
||
|
|
||
|
# Whether or not this is running on JRuby.
|
||
|
#
|
||
|
# @return [Boolean]
|
||
|
def jruby?
|
||
|
return @jruby if defined?(@jruby)
|
||
|
@jruby = RUBY_PLATFORM =~ /java/
|
||
|
end
|
||
|
|
||
|
# Returns an array of ints representing the JRuby version number.
|
||
|
#
|
||
|
# @return [Array<Integer>]
|
||
|
def jruby_version
|
||
|
@jruby_version ||= ::JRUBY_VERSION.split(".").map {|s| s.to_i}
|
||
|
end
|
||
|
|
||
|
# Returns `path` relative to `from`.
|
||
|
#
|
||
|
# This is like `Pathname#relative_path_from` except it accepts both strings
|
||
|
# and pathnames, it handles Windows path separators correctly, and it throws
|
||
|
# an error rather than crashing if the paths use different encodings
|
||
|
# (https://github.com/ruby/ruby/pull/713).
|
||
|
#
|
||
|
# @param path [String, Pathname]
|
||
|
# @param from [String, Pathname]
|
||
|
# @return [Pathname?]
|
||
|
def relative_path_from(path, from)
|
||
|
pathname(path.to_s).relative_path_from(pathname(from.to_s))
|
||
|
rescue NoMethodError => e
|
||
|
raise e unless e.name == :zero?
|
||
|
|
||
|
# Work around https://github.com/ruby/ruby/pull/713.
|
||
|
path = path.to_s
|
||
|
from = from.to_s
|
||
|
raise ArgumentError("Incompatible path encodings: #{path.inspect} is #{path.encoding}, " +
|
||
|
"#{from.inspect} is #{from.encoding}")
|
||
|
end
|
||
|
|
||
|
singleton_methods.each {|method| module_function method}
|
||
|
|
||
|
end
|