123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807 |
- require 'erb'
- require 'set'
- require 'enumerator'
- require 'stringio'
- require 'rbconfig'
- 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 = ::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
- # Converts an array of `[key, value]` pairs to a hash.
- #
- # @example
- # to_hash([[:foo, "bar"], [:baz, "bang"]])
- # #=> {:foo => "bar", :baz => "bang"}
- # @param arr [Array<(Object, Object)>] An array of pairs
- # @return [Hash] A hash
- def to_hash(arr)
- Hash[arr.compact]
- 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)
- to_hash(hash.map {|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)
- to_hash(hash.map {|k, v| [k, yield(v)]})
- 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)
- # Using &block here completely hoses performance on 1.8.
- to_hash(hash.map {|k, v| yield k, v})
- 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
- # 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
- # 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
- # 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<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|
- flatten(arr.map {|e| paths.map {|path| path + [e]}}, 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
- # Converts a Hash to an Array. This is usually identical to `Hash#to_a`,
- # with the following exceptions:
- #
- # * In Ruby 1.8, `Hash#to_a` is not deterministically ordered, but this is.
- # * In Ruby 1.9 when running tests, this is ordered in the same way it would
- # be under Ruby 1.8 (sorted key order rather than insertion order).
- #
- # @param hash [Hash]
- # @return [Array]
- def hash_to_a(hash)
- return hash.to_a unless ruby1_8? || defined?(Test::Unit)
- return hash.sort_by {|k, v| k}
- 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::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::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, Fixnum, (String, nil)]] An array containing the filename, line, and method name of the caller.
- # The method name may be nil
- def caller_info(entry = caller[1])
- info = entry.scan(/^(.*?):(-?.*?)(?::.*`(.+)')?$/).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
- # Silence all output to STDERR within a block.
- #
- # @yield A block in which no output will be printed to STDERR
- def silence_warnings
- the_real_stderr, $stderr = $stderr, StringIO.new
- yield
- ensure
- $stderr = the_real_stderr
- end
- @@silence_warnings = false
- # 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)
- 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)
- return 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)
- return 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}")
- return ActionView::Template.const_get(name.to_s)
- end
- ## Cross-OS Compatibility
- # Whether or not this is running on Windows.
- #
- # @return [Boolean]
- def windows?
- RbConfig::CONFIG['host_os'] =~ /mswin|windows|mingw/i
- end
- # Whether or not this is running on IronRuby.
- #
- # @return [Boolean]
- def ironruby?
- RUBY_ENGINE == "ironruby"
- end
- # Like `Dir.glob`, but works with backslash-separated paths on Windows.
- #
- # @param path [String]
- def glob(path, &block)
- path = path.gsub('\\', '/') if windows?
- Dir.glob(path, &block)
- end
- ## Cross-Ruby-Version Compatibility
- # Whether or not this is running under Ruby 1.8 or lower.
- #
- # Note that IronRuby counts as Ruby 1.8,
- # because it doesn't support the Ruby 1.9 encoding API.
- #
- # @return [Boolean]
- def ruby1_8?
- # IronRuby says its version is 1.9, but doesn't support any of the encoding APIs.
- # We have to fall back to 1.8 behavior.
- ironruby? || (Sass::Util::RUBY_VERSION[0] == 1 && Sass::Util::RUBY_VERSION[1] < 9)
- end
- # Whether or not this is running under Ruby 1.8.6 or lower.
- # Note that lower versions are not officially supported.
- #
- # @return [Boolean]
- def ruby1_8_6?
- ruby1_8? && Sass::Util::RUBY_VERSION[2] < 7
- end
- # Whether or not this is running under MacRuby.
- #
- # @return [Boolean]
- def macruby?
- RUBY_ENGINE == 'macruby'
- end
- # Checks that the encoding of a string is valid in Ruby 1.9
- # and cleans up potential encoding gotchas like the UTF-8 BOM.
- # If it's not, yields an error string describing the invalid character
- # and the line on which it occurrs.
- #
- # @param str [String] The string of which to check the encoding
- # @yield [msg] A block in which an encoding error can be raised.
- # Only yields if there is an encoding error
- # @yieldparam msg [String] The error message to be raised
- # @return [String] `str`, potentially with encoding gotchas like BOMs removed
- def check_encoding(str)
- if ruby1_8?
- return str.gsub(/\A\xEF\xBB\xBF/, '') # Get rid of the UTF-8 BOM
- elsif str.valid_encoding?
- # Get rid of the Unicode BOM if possible
- if str.encoding.name =~ /^UTF-(8|16|32)(BE|LE)?$/
- return str.gsub(Regexp.new("\\A\uFEFF".encode(str.encoding.name)), '')
- else
- return str
- end
- end
- encoding = str.encoding
- newlines = Regexp.new("\r\n|\r|\n".encode(encoding).force_encoding("binary"))
- str.force_encoding("binary").split(newlines).each_with_index do |line, i|
- begin
- line.encode(encoding)
- rescue Encoding::UndefinedConversionError => e
- yield <<MSG.rstrip, i + 1
- Invalid #{encoding.name} character #{e.error_char.dump}
- MSG
- end
- end
- return str
- end
- # Like {\#check\_encoding}, but also checks for a `@charset` declaration
- # at the beginning of the file and uses that encoding if it exists.
- #
- # The Sass encoding rules are simple.
- # If a `@charset` declaration exists,
- # we assume that that's the original encoding of the document.
- # Otherwise, we use whatever encoding Ruby has.
- # Then we convert that to UTF-8 to process internally.
- # The UTF-8 end result is what's returned by this method.
- #
- # @param str [String] The string of which to check the encoding
- # @yield [msg] A block in which an encoding error can be raised.
- # Only yields if there is an encoding error
- # @yieldparam msg [String] The error message to be raised
- # @return [(String, Encoding)] The original string encoded as UTF-8,
- # and the source encoding of the string (or `nil` under Ruby 1.8)
- # @raise [Encoding::UndefinedConversionError] if the source encoding
- # cannot be converted to UTF-8
- # @raise [ArgumentError] if the document uses an unknown encoding with `@charset`
- def check_sass_encoding(str, &block)
- return check_encoding(str, &block), nil if ruby1_8?
- # We allow any printable ASCII characters but double quotes in the charset decl
- bin = str.dup.force_encoding("BINARY")
- encoding = Sass::Util::ENCODINGS_TO_CHECK.find do |enc|
- re = Sass::Util::CHARSET_REGEXPS[enc]
- re && bin =~ re
- end
- charset, bom = $1, $2
- if charset
- charset = charset.force_encoding(encoding).encode("UTF-8")
- if endianness = encoding[/[BL]E$/]
- begin
- Encoding.find(charset + endianness)
- charset << endianness
- rescue ArgumentError # Encoding charset + endianness doesn't exist
- end
- end
- str.force_encoding(charset)
- elsif bom
- str.force_encoding(encoding)
- end
- str = check_encoding(str, &block)
- return str.encode("UTF-8"), str.encoding
- end
- unless ruby1_8?
- # @private
- def _enc(string, encoding)
- string.encode(encoding).force_encoding("BINARY")
- end
- # We could automatically add in any non-ASCII-compatible encodings here,
- # but there's not really a good way to do that
- # without manually checking that each encoding
- # encodes all ASCII characters properly,
- # which takes long enough to affect the startup time of the CLI.
- ENCODINGS_TO_CHECK = %w[UTF-8 UTF-16BE UTF-16LE UTF-32BE UTF-32LE]
- CHARSET_REGEXPS = Hash.new do |h, e|
- h[e] =
- begin
- # /\A(?:\uFEFF)?@charset "(.*?)"|\A(\uFEFF)/
- Regexp.new(/\A(?:#{_enc("\uFEFF", e)})?#{
- _enc('@charset "', e)}(.*?)#{_enc('"', e)}|\A(#{
- _enc("\uFEFF", e)})/)
- rescue Encoding::ConverterNotFoundError => _
- nil # JRuby on Java 5 doesn't support UTF-32
- rescue
- # /\A@charset "(.*?)"/
- Regexp.new(/\A#{_enc('@charset "', e)}(.*?)#{_enc('"', e)}/)
- end
- end
- end
- # Checks to see if a class has a given method.
- # For example:
- #
- # Sass::Util.has?(:public_instance_method, String, :gsub) #=> true
- #
- # Method collections like `Class#instance_methods`
- # return strings in Ruby 1.8 and symbols in Ruby 1.9 and on,
- # so this handles checking for them in a compatible way.
- #
- # @param attr [#to_s] The (singular) name of the method-collection method
- # (e.g. `:instance_methods`, `:private_methods`)
- # @param klass [Module] The class to check the methods of which to check
- # @param method [String, Symbol] The name of the method do check for
- # @return [Boolean] Whether or not the given collection has the given method
- def has?(attr, klass, method)
- klass.send("#{attr}s").include?(ruby1_8? ? method.to_s : method.to_sym)
- end
- # A version of `Enumerable#enum_with_index` that works in Ruby 1.8 and 1.9.
- #
- # @param enum [Enumerable] The enumerable to get the enumerator for
- # @return [Enumerator] The with-index enumerator
- def enum_with_index(enum)
- ruby1_8? ? enum.enum_with_index : enum.each_with_index
- end
- # A version of `Enumerable#enum_cons` that works in Ruby 1.8 and 1.9.
- #
- # @param enum [Enumerable] The enumerable to get the enumerator for
- # @param n [Fixnum] The size of each cons
- # @return [Enumerator] The consed enumerator
- def enum_cons(enum, n)
- ruby1_8? ? enum.enum_cons(n) : enum.each_cons(n)
- end
- # A version of `Enumerable#enum_slice` that works in Ruby 1.8 and 1.9.
- #
- # @param enum [Enumerable] The enumerable to get the enumerator for
- # @param n [Fixnum] The size of each slice
- # @return [Enumerator] The consed enumerator
- def enum_slice(enum, n)
- ruby1_8? ? enum.enum_slice(n) : enum.each_slice(n)
- 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
- # Returns the ASCII code of the given character.
- #
- # @param c [String] All characters but the first are ignored.
- # @return [Fixnum] The ASCII code of `c`.
- def ord(c)
- ruby1_8? ? c[0] : c.ord
- end
- # Flattens the first `n` nested arrays in a cross-version manner.
- #
- # @param arr [Array] The array to flatten
- # @param n [Fixnum] The number of levels to flatten
- # @return [Array] The flattened array
- def flatten(arr, n)
- return arr.flatten(n) unless ruby1_8_6?
- return arr if n == 0
- arr.inject([]) {|res, e| e.is_a?(Array) ? res.concat(flatten(e, n - 1)) : res << e}
- end
- # Returns the hash code for a set in a cross-version manner.
- # Aggravatingly, this is order-dependent in Ruby 1.8.6.
- #
- # @param set [Set]
- # @return [Fixnum] The order-independent hashcode of `set`
- def set_hash(set)
- return set.hash unless ruby1_8_6?
- set.map {|e| e.hash}.uniq.sort.hash
- end
- # Tests the hash-equality of two sets in a cross-version manner.
- # Aggravatingly, this is order-dependent in Ruby 1.8.6.
- #
- # @param set1 [Set]
- # @param set2 [Set]
- # @return [Boolean] Whether or not the sets are hashcode equal
- def set_eql?(set1, set2)
- return set1.eql?(set2) unless ruby1_8_6?
- set1.to_a.uniq.sort_by {|e| e.hash}.eql?(set2.to_a.uniq.sort_by {|e| e.hash})
- 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 = []
- return arr.map do |e|
- next e.gsub('{', '{{') if e.is_a?(String)
- values << e
- next "{#{values.count - 1}}"
- end.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
- ## 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, &block)
- super unless args.empty? && block.nil?
- @set.include?(name)
- end
- end
- private
- # 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)
- 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
- return 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]
- return lcs_backtrace(c, x, y, i-1, j, &block)
- end
- end
- end
- require 'sass/util/multibyte_string_scanner'
|