gems.sass-3.5.5.lib.sass.util.rb Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sass-maven-plugin Show documentation
Show all versions of sass-maven-plugin Show documentation
A Maven Plugin that compiles Sass files.
# -*- 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.
# @comment
# rubocop:disable ModuleLength
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] 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]
# @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).
#
# @param arr [Array]
# @return [Array] `arr`
def strip_string_array(arr)
arr.first.lstrip! if arr.first.is_a?(String)
arr.last.rstrip! if arr.last.is_a?(String)
arr
end
# Return an array of all possible paths through the given arrays.
#
# @param arrs [Array]
# @return [Array]
#
# @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]
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", "�"), 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
# rubocop:disable ParameterLists
# 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)
# rubocop:enable ParameterList
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'
© 2015 - 2025 Weber Informatics LLC | Privacy Policy