1137 lines
37 KiB
Ruby
1137 lines
37 KiB
Ruby
# -*- coding: utf-8 -*-
|
||
require 'erb'
|
||
require 'set'
|
||
require 'enumerator'
|
||
require 'stringio'
|
||
require 'rbconfig'
|
||
require 'uri'
|
||
require 'thread'
|
||
require 'pathname'
|
||
|
||
require 'sass/root'
|
||
require 'sass/util/subset_map'
|
||
|
||
module Sass
|
||
# A module containing various useful functions.
|
||
module 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"
|
||
|
||
# Returns the path of a file relative to the Sass root directory.
|
||
#
|
||
# @param file [String] The filename relative to the Sass root
|
||
# @return [String] The filename relative to the the working directory
|
||
def scope(file)
|
||
File.join(Sass::ROOT_DIR, file)
|
||
end
|
||
|
||
# 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
|
||
|
||
# Maps the values in a hash according to a block.
|
||
#
|
||
# @example
|
||
# map_values({:foo => "bar", :baz => "bang"}) {|v| v.to_sym}
|
||
# #=> {:foo => :bar, :baz => :bang}
|
||
# @param hash [Hash] The hash to map
|
||
# @yield [value] A block in which the values are transformed
|
||
# @yieldparam value [Object] The value that should be mapped
|
||
# @yieldreturn [Object] The new value for the value
|
||
# @return [Hash] The mapped hash
|
||
# @see #map_keys
|
||
# @see #map_hash
|
||
def map_vals(hash)
|
||
# We don't delegate to map_hash for performance here
|
||
# because map_hash does more than is necessary.
|
||
rv = hash.class.new
|
||
hash = hash.as_stored if hash.is_a?(NormalizedMap)
|
||
hash.each do |k, v|
|
||
rv[k] = yield(v)
|
||
end
|
||
rv
|
||
end
|
||
|
||
# Maps the key-value pairs of a hash according to a block.
|
||
#
|
||
# @example
|
||
# map_hash({:foo => "bar", :baz => "bang"}) {|k, v| [k.to_s, v.to_sym]}
|
||
# #=> {"foo" => :bar, "baz" => :bang}
|
||
# @param hash [Hash] The hash to map
|
||
# @yield [key, value] A block in which the key-value pairs are transformed
|
||
# @yieldparam [key] The hash key
|
||
# @yieldparam [value] The hash value
|
||
# @yieldreturn [(Object, Object)] The new value for the `[key, value]` pair
|
||
# @return [Hash] The mapped hash
|
||
# @see #map_keys
|
||
# @see #map_vals
|
||
def map_hash(hash)
|
||
# Copy and modify is more performant than mapping to an array and using
|
||
# to_hash on the result.
|
||
rv = hash.class.new
|
||
hash.each do |k, v|
|
||
new_key, new_value = yield(k, v)
|
||
new_key = hash.denormalize(new_key) if hash.is_a?(NormalizedMap) && new_key == k
|
||
rv[new_key] = new_value
|
||
end
|
||
rv
|
||
end
|
||
|
||
# Computes the powerset of the given array.
|
||
# This is the set of all subsets of the array.
|
||
#
|
||
# @example
|
||
# powerset([1, 2, 3]) #=>
|
||
# Set[Set[], Set[1], Set[2], Set[3], Set[1, 2], Set[2, 3], Set[1, 3], Set[1, 2, 3]]
|
||
# @param arr [Enumerable]
|
||
# @return [Set<Set>] The subsets of `arr`
|
||
def powerset(arr)
|
||
arr.inject([Set.new].to_set) do |powerset, el|
|
||
new_powerset = Set.new
|
||
powerset.each do |subset|
|
||
new_powerset << subset
|
||
new_powerset << subset + [el]
|
||
end
|
||
new_powerset
|
||
end
|
||
end
|
||
|
||
# Restricts a number to falling within a given range.
|
||
# Returns the number if it falls within the range,
|
||
# or the closest value in the range if it doesn't.
|
||
#
|
||
# @param value [Numeric]
|
||
# @param range [Range<Numeric>]
|
||
# @return [Numeric]
|
||
def restrict(value, range)
|
||
[[value, range.first].max, range.last].min
|
||
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 < 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
|
||
|
||
# Concatenates all strings that are adjacent in an array,
|
||
# while leaving other elements as they are.
|
||
#
|
||
# @example
|
||
# merge_adjacent_strings([1, "foo", "bar", 2, "baz"])
|
||
# #=> [1, "foobar", 2, "baz"]
|
||
# @param arr [Array]
|
||
# @return [Array] The enumerable with strings merged
|
||
def merge_adjacent_strings(arr)
|
||
# Optimize for the common case of one element
|
||
return arr if arr.size < 2
|
||
arr.inject([]) do |a, e|
|
||
if e.is_a?(String)
|
||
if a.last.is_a?(String)
|
||
a.last << e
|
||
else
|
||
a << e.dup
|
||
end
|
||
else
|
||
a << e
|
||
end
|
||
a
|
||
end
|
||
end
|
||
|
||
# Non-destructively replaces all occurrences of a subsequence in an array
|
||
# with another subsequence.
|
||
#
|
||
# @example
|
||
# replace_subseq([1, 2, 3, 4, 5], [2, 3], [:a, :b])
|
||
# #=> [1, :a, :b, 4, 5]
|
||
#
|
||
# @param arr [Array] The array whose subsequences will be replaced.
|
||
# @param subseq [Array] The subsequence to find and replace.
|
||
# @param replacement [Array] The sequence that `subseq` will be replaced with.
|
||
# @return [Array] `arr` with `subseq` replaced with `replacement`.
|
||
def replace_subseq(arr, subseq, replacement)
|
||
new = []
|
||
matched = []
|
||
i = 0
|
||
arr.each do |elem|
|
||
if elem != subseq[i]
|
||
new.push(*matched)
|
||
matched = []
|
||
i = 0
|
||
new << elem
|
||
next
|
||
end
|
||
|
||
if i == subseq.length - 1
|
||
matched = []
|
||
i = 0
|
||
new.push(*replacement)
|
||
else
|
||
matched << elem
|
||
i += 1
|
||
end
|
||
end
|
||
new.push(*matched)
|
||
new
|
||
end
|
||
|
||
# Intersperses a value in an enumerable, as would be done with `Array#join`
|
||
# but without concatenating the array together afterwards.
|
||
#
|
||
# @param enum [Enumerable]
|
||
# @param val
|
||
# @return [Array]
|
||
def intersperse(enum, val)
|
||
enum.inject([]) {|a, e| a << e << val}[0...-1]
|
||
end
|
||
|
||
def slice_by(enum)
|
||
results = []
|
||
enum.each do |value|
|
||
key = yield(value)
|
||
if !results.empty? && results.last.first == key
|
||
results.last.last << value
|
||
else
|
||
results << [key, [value]]
|
||
end
|
||
end
|
||
results
|
||
end
|
||
|
||
# Substitutes a sub-array of one array with another sub-array.
|
||
#
|
||
# @param ary [Array] The array in which to make the substitution
|
||
# @param from [Array] The sequence of elements to replace with `to`
|
||
# @param to [Array] The sequence of elements to replace `from` with
|
||
def substitute(ary, from, to)
|
||
res = ary.dup
|
||
i = 0
|
||
while i < res.size
|
||
if res[i...i + from.size] == from
|
||
res[i...i + from.size] = to
|
||
end
|
||
i += 1
|
||
end
|
||
res
|
||
end
|
||
|
||
# Destructively strips whitespace from the beginning and end of the first
|
||
# and last elements, respectively, in the array (if those elements are
|
||
# strings). Preserves CSS escapes at the end of the array.
|
||
#
|
||
# @param arr [Array]
|
||
# @return [Array] `arr`
|
||
def strip_string_array(arr)
|
||
arr.first.lstrip! if arr.first.is_a?(String)
|
||
arr[-1] = Sass::Util.rstrip_except_escapes(arr[-1]) if arr.last.is_a?(String)
|
||
arr
|
||
end
|
||
|
||
# Normalizes identifier escapes.
|
||
#
|
||
# See https://github.com/sass/language/blob/master/accepted/identifier-escapes.md.
|
||
#
|
||
# @param ident [String]
|
||
# @return [String]
|
||
def normalize_ident_escapes(ident, start: true)
|
||
ident.gsub(/(^)?(#{Sass::SCSS::RX::ESCAPE})/) do |s|
|
||
at_start = start && $1
|
||
char = escaped_char(s)
|
||
next char if char =~ (at_start ? Sass::SCSS::RX::NMSTART : Sass::SCSS::RX::NMCHAR)
|
||
if char =~ (at_start ? /[\x0-\x1F\x7F0-9]/ : /[\x0-\x1F\x7F]/)
|
||
"\\#{char.ord.to_s(16)} "
|
||
else
|
||
"\\#{char}"
|
||
end
|
||
end
|
||
end
|
||
|
||
# Returns the character encoded by the given escape sequence.
|
||
#
|
||
# @param escape [String]
|
||
# @return [String]
|
||
def escaped_char(escape)
|
||
if escape =~ /^\\([0-9a-fA-F]{1,6})[ \t\r\n\f]?/
|
||
$1.to_i(16).chr(Encoding::UTF_8)
|
||
else
|
||
escape[1]
|
||
end
|
||
end
|
||
|
||
# Like [String#strip], but preserves escaped whitespace at the end of the
|
||
# string.
|
||
#
|
||
# @param string [String]
|
||
# @return [String]
|
||
def strip_except_escapes(string)
|
||
rstrip_except_escapes(string.lstrip)
|
||
end
|
||
|
||
# Like [String#rstrip], but preserves escaped whitespace at the end of the
|
||
# string.
|
||
#
|
||
# @param string [String]
|
||
# @return [String]
|
||
def rstrip_except_escapes(string)
|
||
string.sub(/(?<!\\)\s+$/, '')
|
||
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
|
||
|
||
# Computes a single longest common subsequence for `x` and `y`.
|
||
# If there are more than one longest common subsequences,
|
||
# the one returned is that which starts first in `x`.
|
||
#
|
||
# @param x [Array]
|
||
# @param y [Array]
|
||
# @yield [a, b] An optional block to use in place of a check for equality
|
||
# between elements of `x` and `y`.
|
||
# @yieldreturn [Object, nil] If the two values register as equal,
|
||
# this will return the value to use in the LCS array.
|
||
# @return [Array] The LCS
|
||
def lcs(x, y, &block)
|
||
x = [nil, *x]
|
||
y = [nil, *y]
|
||
block ||= proc {|a, b| a == b && a}
|
||
lcs_backtrace(lcs_table(x, y, &block), x, y, x.size - 1, y.size - 1, &block)
|
||
end
|
||
|
||
# Like `String.upcase`, but only ever upcases ASCII letters.
|
||
def upcase(string)
|
||
return string.upcase unless ruby2_4?
|
||
string.upcase(:ascii)
|
||
end
|
||
|
||
# Like `String.downcase`, but only ever downcases ASCII letters.
|
||
def downcase(string)
|
||
return string.downcase unless ruby2_4?
|
||
string.downcase(:ascii)
|
||
end
|
||
|
||
# Returns a sub-array of `minuend` containing only elements that are also in
|
||
# `subtrahend`. Ensures that the return value has the same order as
|
||
# `minuend`, even on Rubinius where that's not guaranteed by `Array#-`.
|
||
#
|
||
# @param minuend [Array]
|
||
# @param subtrahend [Array]
|
||
# @return [Array]
|
||
def array_minus(minuend, subtrahend)
|
||
return minuend - subtrahend unless rbx?
|
||
set = Set.new(minuend) - subtrahend
|
||
minuend.select {|e| set.include?(e)}
|
||
end
|
||
|
||
# Returns the maximum of `val1` and `val2`. We use this over \{Array.max} to
|
||
# avoid unnecessary garbage collection.
|
||
def max(val1, val2)
|
||
val1 > val2 ? val1 : val2
|
||
end
|
||
|
||
# Returns the minimum of `val1` and `val2`. We use this over \{Array.min} to
|
||
# avoid unnecessary garbage collection.
|
||
def min(val1, val2)
|
||
val1 <= val2 ? val1 : val2
|
||
end
|
||
|
||
# Returns a string description of the character that caused an
|
||
# `Encoding::UndefinedConversionError`.
|
||
#
|
||
# @param e [Encoding::UndefinedConversionError]
|
||
# @return [String]
|
||
def undefined_conversion_error_char(e)
|
||
# Rubinius (as of 2.0.0.rc1) pre-quotes the error character.
|
||
return e.error_char if rbx?
|
||
# JRuby (as of 1.7.2) doesn't have an error_char field on
|
||
# Encoding::UndefinedConversionError.
|
||
return e.error_char.dump unless jruby?
|
||
e.message[/^"[^"]+"/] # "
|
||
end
|
||
|
||
# Asserts that `value` falls within `range` (inclusive), leaving
|
||
# room for slight floating-point errors.
|
||
#
|
||
# @param name [String] The name of the value. Used in the error message.
|
||
# @param range [Range] The allowed range of values.
|
||
# @param value [Numeric, Sass::Script::Value::Number] The value to check.
|
||
# @param unit [String] The unit of the value. Used in error reporting.
|
||
# @return [Numeric] `value` adjusted to fall within range, if it
|
||
# was outside by a floating-point margin.
|
||
def check_range(name, range, value, unit = '')
|
||
grace = (-0.00001..0.00001)
|
||
str = value.to_s
|
||
value = value.value if value.is_a?(Sass::Script::Value::Number)
|
||
return value if range.include?(value)
|
||
return range.first if grace.include?(value - range.first)
|
||
return range.last if grace.include?(value - range.last)
|
||
raise ArgumentError.new(
|
||
"#{name} #{str} must be between #{range.first}#{unit} and #{range.last}#{unit}")
|
||
end
|
||
|
||
# Returns whether or not `seq1` is a subsequence of `seq2`. That is, whether
|
||
# or not `seq2` contains every element in `seq1` in the same order (and
|
||
# possibly more elements besides).
|
||
#
|
||
# @param seq1 [Array]
|
||
# @param seq2 [Array]
|
||
# @return [Boolean]
|
||
def subsequence?(seq1, seq2)
|
||
i = j = 0
|
||
loop do
|
||
return true if i == seq1.size
|
||
return false if j == seq2.size
|
||
i += 1 if seq1[i] == seq2[j]
|
||
j += 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
|
||
|
||
# Returns whether one version string represents a more recent version than another.
|
||
#
|
||
# @param v1 [String] A version string.
|
||
# @param v2 [String] Another version string.
|
||
# @return [Boolean]
|
||
def version_gt(v1, v2)
|
||
# Construct an array to make sure the shorter version is padded with nil
|
||
Array.new([v1.length, v2.length].max).zip(v1.split("."), v2.split(".")) do |_, p1, p2|
|
||
p1 ||= "0"
|
||
p2 ||= "0"
|
||
release1 = p1 =~ /^[0-9]+$/
|
||
release2 = p2 =~ /^[0-9]+$/
|
||
if release1 && release2
|
||
# Integer comparison if both are full releases
|
||
p1, p2 = p1.to_i, p2.to_i
|
||
next if p1 == p2
|
||
return p1 > p2
|
||
elsif !release1 && !release2
|
||
# String comparison if both are prereleases
|
||
next if p1 == p2
|
||
return p1 > p2
|
||
else
|
||
# If only one is a release, that one is newer
|
||
return release1
|
||
end
|
||
end
|
||
end
|
||
|
||
# Returns whether one version string represents the same or a more
|
||
# recent version than another.
|
||
#
|
||
# @param v1 [String] A version string.
|
||
# @param v2 [String] Another version string.
|
||
# @return [Boolean]
|
||
def version_geq(v1, v2)
|
||
version_gt(v1, v2) || !version_gt(v2, v1)
|
||
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}"
|
||
Sass::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
|
||
Sass.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
|
||
|
||
# Returns whether this environment is using ActionPack
|
||
# version 3.0.0 or greater.
|
||
#
|
||
# @return [Boolean]
|
||
def ap_geq_3?
|
||
ap_geq?("3.0.0.beta1")
|
||
end
|
||
|
||
# Returns whether this environment is using ActionPack
|
||
# of a version greater than or equal to that specified.
|
||
#
|
||
# @param version [String] The string version number to check against.
|
||
# Should be greater than or equal to Rails 3,
|
||
# because otherwise ActionPack::VERSION isn't autoloaded
|
||
# @return [Boolean]
|
||
def ap_geq?(version)
|
||
# The ActionPack module is always loaded automatically in Rails >= 3
|
||
return false unless defined?(ActionPack) && defined?(ActionPack::VERSION) &&
|
||
defined?(ActionPack::VERSION::STRING)
|
||
|
||
version_geq(ActionPack::VERSION::STRING, version)
|
||
end
|
||
|
||
# Returns an ActionView::Template* class.
|
||
# In pre-3.0 versions of Rails, most of these classes
|
||
# were of the form `ActionView::TemplateFoo`,
|
||
# while afterwards they were of the form `ActionView;:Template::Foo`.
|
||
#
|
||
# @param name [#to_s] The name of the class to get.
|
||
# For example, `:Error` will return `ActionView::TemplateError`
|
||
# or `ActionView::Template::Error`.
|
||
def av_template_class(name)
|
||
return ActionView.const_get("Template#{name}") if ActionView.const_defined?("Template#{name}")
|
||
ActionView::Template.const_get(name.to_s)
|
||
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
|
||
|
||
# Like `Dir.glob`, but works with backslash-separated paths on Windows.
|
||
#
|
||
# @param path [String]
|
||
def glob(path)
|
||
path = path.tr('\\', '/') if windows?
|
||
if block_given?
|
||
Dir.glob(path) {|f| yield(f)}
|
||
else
|
||
Dir.glob(path)
|
||
end
|
||
end
|
||
|
||
# Like `Pathname.new`, but normalizes Windows paths to always use backslash
|
||
# separators.
|
||
#
|
||
# `Pathname#relative_path_from` can break if the two pathnames aren't
|
||
# consistent in their slash style.
|
||
#
|
||
# @param path [String]
|
||
# @return [Pathname]
|
||
def pathname(path)
|
||
path = path.tr("/", "\\") if windows?
|
||
Pathname.new(path)
|
||
end
|
||
|
||
# Like `Pathname#cleanpath`, but normalizes Windows paths to always use
|
||
# backslash separators. Normally, `Pathname#cleanpath` actually does the
|
||
# reverse -- it will convert backslashes to forward slashes, which can break
|
||
# `Pathname#relative_path_from`.
|
||
#
|
||
# @param path [String, Pathname]
|
||
# @return [Pathname]
|
||
def cleanpath(path)
|
||
path = Pathname.new(path) unless path.is_a?(Pathname)
|
||
pathname(path.cleanpath.to_s)
|
||
end
|
||
|
||
# Returns `path` with all symlinks resolved.
|
||
#
|
||
# @param path [String, Pathname]
|
||
# @return [Pathname]
|
||
def realpath(path)
|
||
path = Pathname.new(path) unless path.is_a?(Pathname)
|
||
|
||
# Explicitly DON'T run #pathname here. We don't want to convert
|
||
# to Windows directory separators because we're comparing these
|
||
# against the paths returned by Listen, which use forward
|
||
# slashes everywhere.
|
||
begin
|
||
path.realpath
|
||
rescue SystemCallError
|
||
# If [path] doesn't actually exist, don't bail, just
|
||
# return the original.
|
||
path
|
||
end
|
||
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
|
||
|
||
# Converts `path` to a "file:" URI. This handles Windows paths correctly.
|
||
#
|
||
# @param path [String, Pathname]
|
||
# @return [String]
|
||
def file_uri_from_path(path)
|
||
path = path.to_s if path.is_a?(Pathname)
|
||
path = path.tr('\\', '/') if windows?
|
||
path = URI::DEFAULT_PARSER.escape(path)
|
||
return path.start_with?('/') ? "file://" + path : path unless windows?
|
||
return "file:///" + path.tr("\\", "/") if path =~ %r{^[a-zA-Z]:[/\\]}
|
||
return "file:" + path.tr("\\", "/") if path =~ %r{\\\\[^\\]+\\[^\\/]+}
|
||
path.tr("\\", "/")
|
||
end
|
||
|
||
# Retries a filesystem operation if it fails on Windows. Windows
|
||
# has weird and flaky locking rules that can cause operations to fail.
|
||
#
|
||
# @yield [] The filesystem operation.
|
||
def retry_on_windows
|
||
return yield unless windows?
|
||
|
||
begin
|
||
yield
|
||
rescue SystemCallError
|
||
sleep 0.1
|
||
yield
|
||
end
|
||
end
|
||
|
||
# Prepare a value for a destructuring assignment (e.g. `a, b =
|
||
# val`). This works around a performance bug when using
|
||
# ActiveSupport, and only needs to be called when `val` is likely
|
||
# to be `nil` reasonably often.
|
||
#
|
||
# See [this bug report](http://redmine.ruby-lang.org/issues/4917).
|
||
#
|
||
# @param val [Object]
|
||
# @return [Object]
|
||
def destructure(val)
|
||
val || []
|
||
end
|
||
|
||
CHARSET_REGEXP = /\A@charset "([^"]+)"/
|
||
bom = "\uFEFF"
|
||
UTF_8_BOM = bom.encode("UTF-8").force_encoding('BINARY')
|
||
UTF_16BE_BOM = bom.encode("UTF-16BE").force_encoding('BINARY')
|
||
UTF_16LE_BOM = bom.encode("UTF-16LE").force_encoding('BINARY')
|
||
|
||
## Cross-Ruby-Version Compatibility
|
||
|
||
# Whether or not this is running under Ruby 2.4 or higher.
|
||
#
|
||
# @return [Boolean]
|
||
def ruby2_4?
|
||
return @ruby2_4 if defined?(@ruby2_4)
|
||
@ruby2_4 =
|
||
if RUBY_VERSION_COMPONENTS[0] == 2
|
||
RUBY_VERSION_COMPONENTS[1] >= 4
|
||
else
|
||
RUBY_VERSION_COMPONENTS[0] > 2
|
||
end
|
||
end
|
||
|
||
# Like {\#check\_encoding}, but also checks for a `@charset` declaration
|
||
# at the beginning of the file and uses that encoding if it exists.
|
||
#
|
||
# Sass follows CSS's decoding rules.
|
||
#
|
||
# @param str [String] The string of which to check the encoding
|
||
# @return [(String, Encoding)] The original string encoded as UTF-8,
|
||
# and the source encoding of the string
|
||
# @raise [Encoding::UndefinedConversionError] if the source encoding
|
||
# cannot be converted to UTF-8
|
||
# @raise [ArgumentError] if the document uses an unknown encoding with `@charset`
|
||
# @raise [Sass::SyntaxError] If the document declares an encoding that
|
||
# doesn't match its contents, or it doesn't declare an encoding and its
|
||
# contents are invalid in the native encoding.
|
||
def check_sass_encoding(str)
|
||
# Determine the fallback encoding following section 3.2 of CSS Syntax Level 3 and Encodings:
|
||
# http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#determine-the-fallback-encoding
|
||
# http://encoding.spec.whatwg.org/#decode
|
||
binary = str.dup.force_encoding("BINARY")
|
||
if binary.start_with?(UTF_8_BOM)
|
||
binary.slice! 0, UTF_8_BOM.length
|
||
str = binary.force_encoding('UTF-8')
|
||
elsif binary.start_with?(UTF_16BE_BOM)
|
||
binary.slice! 0, UTF_16BE_BOM.length
|
||
str = binary.force_encoding('UTF-16BE')
|
||
elsif binary.start_with?(UTF_16LE_BOM)
|
||
binary.slice! 0, UTF_16LE_BOM.length
|
||
str = binary.force_encoding('UTF-16LE')
|
||
elsif binary =~ CHARSET_REGEXP
|
||
charset = $1.force_encoding('US-ASCII')
|
||
encoding = Encoding.find(charset)
|
||
if encoding.name == 'UTF-16' || encoding.name == 'UTF-16BE'
|
||
encoding = Encoding.find('UTF-8')
|
||
end
|
||
str = binary.force_encoding(encoding)
|
||
elsif str.encoding.name == "ASCII-8BIT"
|
||
# Normally we want to fall back on believing the Ruby string
|
||
# encoding, but if that's just binary we want to make sure
|
||
# it's valid UTF-8.
|
||
str = str.force_encoding('utf-8')
|
||
end
|
||
|
||
find_encoding_error(str) unless str.valid_encoding?
|
||
|
||
begin
|
||
# If the string is valid, preprocess it according to section 3.3 of CSS Syntax Level 3.
|
||
return str.encode("UTF-8").gsub(/\r\n?|\f/, "\n").tr("\u0000", "<EFBFBD>"), str.encoding
|
||
rescue EncodingError
|
||
find_encoding_error(str)
|
||
end
|
||
end
|
||
|
||
# Destructively removes all elements from an array that match a block, and
|
||
# returns the removed elements.
|
||
#
|
||
# @param array [Array] The array from which to remove elements.
|
||
# @yield [el] Called for each element.
|
||
# @yieldparam el [*] The element to test.
|
||
# @yieldreturn [Boolean] Whether or not to extract the element.
|
||
# @return [Array] The extracted elements.
|
||
def extract!(array)
|
||
out = []
|
||
array.reject! do |e|
|
||
next false unless yield e
|
||
out << e
|
||
true
|
||
end
|
||
out
|
||
end
|
||
|
||
# Flattens the first level of nested arrays in `arrs`. Unlike
|
||
# `Array#flatten`, this orders the result by taking the first
|
||
# values from each array in order, then the second, and so on.
|
||
#
|
||
# @param arrs [Array] The array to flatten.
|
||
# @return [Array] The flattened array.
|
||
def flatten_vertically(arrs)
|
||
result = []
|
||
arrs = arrs.map {|sub| sub.is_a?(Array) ? sub.dup : Array(sub)}
|
||
until arrs.empty?
|
||
arrs.reject! do |arr|
|
||
result << arr.shift
|
||
arr.empty?
|
||
end
|
||
end
|
||
result
|
||
end
|
||
|
||
# Like `Object#inspect`, but preserves non-ASCII characters rather than
|
||
# escaping them under Ruby 1.9.2. This is necessary so that the
|
||
# precompiled Haml template can be `#encode`d into `@options[:encoding]`
|
||
# before being evaluated.
|
||
#
|
||
# @param obj {Object}
|
||
# @return {String}
|
||
def inspect_obj(obj)
|
||
return obj.inspect unless version_geq(RUBY_VERSION, "1.9.2")
|
||
return ':' + inspect_obj(obj.to_s) if obj.is_a?(Symbol)
|
||
return obj.inspect unless obj.is_a?(String)
|
||
'"' + obj.gsub(/[\x00-\x7F]+/) {|s| s.inspect[1...-1]} + '"'
|
||
end
|
||
|
||
# Extracts the non-string vlaues from an array containing both strings and non-strings.
|
||
# These values are replaced with escape sequences.
|
||
# This can be undone using \{#inject\_values}.
|
||
#
|
||
# This is useful e.g. when we want to do string manipulation
|
||
# on an interpolated string.
|
||
#
|
||
# The precise format of the resulting string is not guaranteed.
|
||
# However, it is guaranteed that newlines and whitespace won't be affected.
|
||
#
|
||
# @param arr [Array] The array from which values are extracted.
|
||
# @return [(String, Array)] The resulting string, and an array of extracted values.
|
||
def extract_values(arr)
|
||
values = []
|
||
mapped = arr.map do |e|
|
||
next e.gsub('{', '{{') if e.is_a?(String)
|
||
values << e
|
||
next "{#{values.count - 1}}"
|
||
end
|
||
return mapped.join, values
|
||
end
|
||
|
||
# Undoes \{#extract\_values} by transforming a string with escape sequences
|
||
# into an array of strings and non-string values.
|
||
#
|
||
# @param str [String] The string with escape sequences.
|
||
# @param values [Array] The array of values to inject.
|
||
# @return [Array] The array of strings and values.
|
||
def inject_values(str, values)
|
||
return [str.gsub('{{', '{')] if values.empty?
|
||
# Add an extra { so that we process the tail end of the string
|
||
result = (str + '{{').scan(/(.*?)(?:(\{\{)|\{(\d+)\})/m).map do |(pre, esc, n)|
|
||
[pre, esc ? '{' : '', n ? values[n.to_i] : '']
|
||
end.flatten(1)
|
||
result[-2] = '' # Get rid of the extra {
|
||
merge_adjacent_strings(result).reject {|s| s == ''}
|
||
end
|
||
|
||
# Allows modifications to be performed on the string form
|
||
# of an array containing both strings and non-strings.
|
||
#
|
||
# @param arr [Array] The array from which values are extracted.
|
||
# @yield [str] A block in which string manipulation can be done to the array.
|
||
# @yieldparam str [String] The string form of `arr`.
|
||
# @yieldreturn [String] The modified string.
|
||
# @return [Array] The modified, interpolated array.
|
||
def with_extracted_values(arr)
|
||
str, vals = extract_values(arr)
|
||
str = yield str
|
||
inject_values(str, vals)
|
||
end
|
||
|
||
# Builds a sourcemap file name given the generated CSS file name.
|
||
#
|
||
# @param css [String] The generated CSS file name.
|
||
# @return [String] The source map file name.
|
||
def sourcemap_name(css)
|
||
css + ".map"
|
||
end
|
||
|
||
# Escapes certain characters so that the result can be used
|
||
# as the JSON string value. Returns the original string if
|
||
# no escaping is necessary.
|
||
#
|
||
# @param s [String] The string to be escaped
|
||
# @return [String] The escaped string
|
||
def json_escape_string(s)
|
||
return s if s !~ /["\\\b\f\n\r\t]/
|
||
|
||
result = ""
|
||
s.split("").each do |c|
|
||
case c
|
||
when '"', "\\"
|
||
result << "\\" << c
|
||
when "\n" then result << "\\n"
|
||
when "\t" then result << "\\t"
|
||
when "\r" then result << "\\r"
|
||
when "\f" then result << "\\f"
|
||
when "\b" then result << "\\b"
|
||
else
|
||
result << c
|
||
end
|
||
end
|
||
result
|
||
end
|
||
|
||
# Converts the argument into a valid JSON value.
|
||
#
|
||
# @param v [Integer, String, Array, Boolean, nil]
|
||
# @return [String]
|
||
def json_value_of(v)
|
||
case v
|
||
when Integer
|
||
v.to_s
|
||
when String
|
||
"\"" + json_escape_string(v) + "\""
|
||
when Array
|
||
"[" + v.map {|x| json_value_of(x)}.join(",") + "]"
|
||
when NilClass
|
||
"null"
|
||
when TrueClass
|
||
"true"
|
||
when FalseClass
|
||
"false"
|
||
else
|
||
raise ArgumentError.new("Unknown type: #{v.class.name}")
|
||
end
|
||
end
|
||
|
||
VLQ_BASE_SHIFT = 5
|
||
VLQ_BASE = 1 << VLQ_BASE_SHIFT
|
||
VLQ_BASE_MASK = VLQ_BASE - 1
|
||
VLQ_CONTINUATION_BIT = VLQ_BASE
|
||
|
||
BASE64_DIGITS = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a + ['+', '/']
|
||
BASE64_DIGIT_MAP = begin
|
||
map = {}
|
||
BASE64_DIGITS.each_with_index.map do |digit, i|
|
||
map[digit] = i
|
||
end
|
||
map
|
||
end
|
||
|
||
# Encodes `value` as VLQ (http://en.wikipedia.org/wiki/VLQ).
|
||
#
|
||
# @param value [Integer]
|
||
# @return [String] The encoded value
|
||
def encode_vlq(value)
|
||
if value < 0
|
||
value = ((-value) << 1) | 1
|
||
else
|
||
value <<= 1
|
||
end
|
||
|
||
result = ''
|
||
begin
|
||
digit = value & VLQ_BASE_MASK
|
||
value >>= VLQ_BASE_SHIFT
|
||
if value > 0
|
||
digit |= VLQ_CONTINUATION_BIT
|
||
end
|
||
result << BASE64_DIGITS[digit]
|
||
end while value > 0
|
||
result
|
||
end
|
||
|
||
## Static Method Stuff
|
||
|
||
# The context in which the ERB for \{#def\_static\_method} will be run.
|
||
class StaticConditionalContext
|
||
# @param set [#include?] The set of variables that are defined for this context.
|
||
def initialize(set)
|
||
@set = set
|
||
end
|
||
|
||
# Checks whether or not a variable is defined for this context.
|
||
#
|
||
# @param name [Symbol] The name of the variable
|
||
# @return [Boolean]
|
||
def method_missing(name, *args)
|
||
super unless args.empty? && !block_given?
|
||
@set.include?(name)
|
||
end
|
||
end
|
||
|
||
# @private
|
||
ATOMIC_WRITE_MUTEX = Mutex.new
|
||
|
||
# This creates a temp file and yields it for writing. When the
|
||
# write is complete, the file is moved into the desired location.
|
||
# The atomicity of this operation is provided by the filesystem's
|
||
# rename operation.
|
||
#
|
||
# @param filename [String] The file to write to.
|
||
# @param perms [Integer] The permissions used for creating this file.
|
||
# Will be masked by the process umask. Defaults to readable/writeable
|
||
# by all users however the umask usually changes this to only be writable
|
||
# by the process's user.
|
||
# @yieldparam tmpfile [Tempfile] The temp file that can be written to.
|
||
# @return The value returned by the block.
|
||
def atomic_create_and_write_file(filename, perms = 0666)
|
||
require 'tempfile'
|
||
tmpfile = Tempfile.new(File.basename(filename), File.dirname(filename))
|
||
tmpfile.binmode if tmpfile.respond_to?(:binmode)
|
||
result = yield tmpfile
|
||
tmpfile.close
|
||
ATOMIC_WRITE_MUTEX.synchronize do
|
||
begin
|
||
File.chmod(perms & ~File.umask, tmpfile.path)
|
||
rescue Errno::EPERM
|
||
# If we don't have permissions to chmod the file, don't let that crash
|
||
# the compilation. See issue 1215.
|
||
end
|
||
File.rename tmpfile.path, filename
|
||
end
|
||
result
|
||
ensure
|
||
# close and remove the tempfile if it still exists,
|
||
# presumably due to an error during write
|
||
tmpfile.close if tmpfile
|
||
tmpfile.unlink if tmpfile
|
||
end
|
||
|
||
private
|
||
|
||
def find_encoding_error(str)
|
||
encoding = str.encoding
|
||
cr = Regexp.quote("\r".encode(encoding).force_encoding('BINARY'))
|
||
lf = Regexp.quote("\n".encode(encoding).force_encoding('BINARY'))
|
||
ff = Regexp.quote("\f".encode(encoding).force_encoding('BINARY'))
|
||
line_break = /#{cr}#{lf}?|#{ff}|#{lf}/
|
||
|
||
str.force_encoding("binary").split(line_break).each_with_index do |line, i|
|
||
begin
|
||
line.encode(encoding)
|
||
rescue Encoding::UndefinedConversionError => e
|
||
raise Sass::SyntaxError.new(
|
||
"Invalid #{encoding.name} character #{undefined_conversion_error_char(e)}",
|
||
:line => i + 1)
|
||
end
|
||
end
|
||
|
||
# We shouldn't get here, but it's possible some weird encoding stuff causes it.
|
||
return str, str.encoding
|
||
end
|
||
|
||
# Calculates the memoization table for the Least Common Subsequence algorithm.
|
||
# Algorithm from [Wikipedia](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem#Computing_the_length_of_the_LCS)
|
||
def lcs_table(x, y)
|
||
# This method does not take a block as an explicit parameter for performance reasons.
|
||
c = Array.new(x.size) {[]}
|
||
x.size.times {|i| c[i][0] = 0}
|
||
y.size.times {|j| c[0][j] = 0}
|
||
(1...x.size).each do |i|
|
||
(1...y.size).each do |j|
|
||
c[i][j] =
|
||
if yield x[i], y[j]
|
||
c[i - 1][j - 1] + 1
|
||
else
|
||
[c[i][j - 1], c[i - 1][j]].max
|
||
end
|
||
end
|
||
end
|
||
c
|
||
end
|
||
|
||
# Computes a single longest common subsequence for arrays x and y.
|
||
# Algorithm from [Wikipedia](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem#Reading_out_an_LCS)
|
||
def lcs_backtrace(c, x, y, i, j, &block)
|
||
return [] if i == 0 || j == 0
|
||
if (v = yield(x[i], y[j]))
|
||
return lcs_backtrace(c, x, y, i - 1, j - 1, &block) << v
|
||
end
|
||
|
||
return lcs_backtrace(c, x, y, i, j - 1, &block) if c[i][j - 1] > c[i - 1][j]
|
||
lcs_backtrace(c, x, y, i - 1, j, &block)
|
||
end
|
||
|
||
singleton_methods.each {|method| module_function method}
|
||
end
|
||
end
|
||
|
||
require 'sass/util/multibyte_string_scanner'
|
||
require 'sass/util/normalized_map'
|