All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gems.sass-3.5.5.lib.sass.util.rb Maven / Gradle / Ivy

There is a newer version: 3.7.2
Show newest version
# -*- 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