diff --git a/.travis.yml b/.travis.yml index 5534161d..11461c17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ bundler_args: "--binstubs --standalone --without documentation --path ../bundle" script: "bundle exec rake test" rvm: - 2.3.3 + - 2.3.7 - 2.4.4 - 2.5.1 notifications: diff --git a/Gemfile b/Gemfile index 8f059dd9..3be9c3cd 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,2 @@ -source 'https://rubygems.org' - -# Specify your gem's dependencies in sassc.gemspec +source "https://rubygems.org" gemspec diff --git a/LICENSE.txt b/LICENSE.txt index 6cf46396..0a58909a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2015 Ryan Boland +Copyright (c) Ryan Boland & Contributors MIT License diff --git a/lib/sassc.rb b/lib/sassc.rb index 8fe7f11d..6d5bd74b 100644 --- a/lib/sassc.rb +++ b/lib/sassc.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC end @@ -5,9 +7,26 @@ module SassC require_relative "sassc/native" require_relative "sassc/import_handler" require_relative "sassc/importer" +require_relative "sassc/util" +require_relative "sassc/util/normalized_map" require_relative "sassc/script" +require_relative "sassc/script/value" +require_relative "sassc/script/value/bool" +require_relative "sassc/script/value/number" +require_relative "sassc/script/value/color" +require_relative "sassc/script/value/string" +require_relative "sassc/script/value/list" +require_relative "sassc/script/value/map" +require_relative "sassc/script/functions" +require_relative "sassc/script/value_conversion" +require_relative "sassc/script/value_conversion/base" +require_relative "sassc/script/value_conversion/string" +require_relative "sassc/script/value_conversion/number" +require_relative "sassc/script/value_conversion/color" +require_relative "sassc/script/value_conversion/map" +require_relative "sassc/script/value_conversion/list" +require_relative "sassc/script/value_conversion/bool" require_relative "sassc/functions_handler" -require_relative "sassc/cache_stores" require_relative "sassc/dependency" require_relative "sassc/error" require_relative "sassc/engine" diff --git a/lib/sassc/cache_stores.rb b/lib/sassc/cache_stores.rb deleted file mode 100644 index d18c3ac4..00000000 --- a/lib/sassc/cache_stores.rb +++ /dev/null @@ -1,6 +0,0 @@ -module SassC - module CacheStores - end -end - -require_relative "cache_stores/base" diff --git a/lib/sassc/cache_stores/base.rb b/lib/sassc/cache_stores/base.rb deleted file mode 100644 index f1d60d56..00000000 --- a/lib/sassc/cache_stores/base.rb +++ /dev/null @@ -1,8 +0,0 @@ -module SassC - module CacheStores - class Base - # we don't actually cache anything yet, - # this is just for API compatibility - end - end -end diff --git a/lib/sassc/dependency.rb b/lib/sassc/dependency.rb index a4f137ff..e5097109 100644 --- a/lib/sassc/dependency.rb +++ b/lib/sassc/dependency.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC class Dependency attr_reader :filename diff --git a/lib/sassc/engine.rb b/lib/sassc/engine.rb index 5fd93143..ea01477e 100644 --- a/lib/sassc/engine.rb +++ b/lib/sassc/engine.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "error" module SassC diff --git a/lib/sassc/error.rb b/lib/sassc/error.rb index b6a10b9f..10f53553 100644 --- a/lib/sassc/error.rb +++ b/lib/sassc/error.rb @@ -1,7 +1,9 @@ -require 'pathname' -require 'sass/error' +# frozen_string_literal: true + +require "pathname" module SassC + class BaseError < StandardError; end class NotRenderedError < BaseError; end class InvalidStyleError < BaseError; end @@ -9,8 +11,10 @@ class UnsupportedValue < BaseError; end # When dealing with SyntaxErrors, # it's important to provide filename and line number information. - # This will be used in various error reports to users, including backtraces; + # This will be used in various error reports to users, including backtraces. + class SyntaxError < BaseError + def initialize(message, filename: nil, line: nil) @filename = filename @line = line @@ -27,5 +31,7 @@ def sass_backtrace return [] unless @filename && @line ["#{@filename}:#{@line}"] end + end + end diff --git a/lib/sassc/functions_handler.rb b/lib/sassc/functions_handler.rb index 819349ae..c2237794 100644 --- a/lib/sassc/functions_handler.rb +++ b/lib/sassc/functions_handler.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC class FunctionsHandler def initialize(options) @@ -54,8 +56,7 @@ def arguments_from_native_list(native_argument_list) def to_native_value(sass_value) # if the custom function returns nil, we provide a "default" return # value of an empty string - sass_value ||= Script::String.new("") - + sass_value ||= SassC::Script::Value::String.new("") sass_value.options = @options Script::ValueConversion.to_native(sass_value) end diff --git a/lib/sassc/import_handler.rb b/lib/sassc/import_handler.rb index 7842a250..498df7c2 100644 --- a/lib/sassc/import_handler.rb +++ b/lib/sassc/import_handler.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC class ImportHandler def initialize(options) diff --git a/lib/sassc/importer.rb b/lib/sassc/importer.rb index 57efd024..6f7c1b84 100644 --- a/lib/sassc/importer.rb +++ b/lib/sassc/importer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC class Importer attr_reader :options diff --git a/lib/sassc/native.rb b/lib/sassc/native.rb index b52b0c2e..443dcbe1 100644 --- a/lib/sassc/native.rb +++ b/lib/sassc/native.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "ffi" module SassC @@ -48,8 +50,7 @@ def self.return_string_array(ptr) end def self.native_string(string) - string = string.to_s - string << "\0" + string = "#{string}\0" data = Native::LibC.malloc(string.bytesize) data.write_string(string) data diff --git a/lib/sassc/native/lib_c.rb b/lib/sassc/native/lib_c.rb index 295f1f04..8e9b08ee 100644 --- a/lib/sassc/native/lib_c.rb +++ b/lib/sassc/native/lib_c.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Native module LibC diff --git a/lib/sassc/native/native_context_api.rb b/lib/sassc/native/native_context_api.rb index f66edc81..a692ca98 100644 --- a/lib/sassc/native/native_context_api.rb +++ b/lib/sassc/native/native_context_api.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Native attach_function :version, :libsass_version, [], :string diff --git a/lib/sassc/native/native_functions_api.rb b/lib/sassc/native/native_functions_api.rb index 920f7f5b..e7a4fd0e 100644 --- a/lib/sassc/native/native_functions_api.rb +++ b/lib/sassc/native/native_functions_api.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Native # Creators for sass function list and function descriptors diff --git a/lib/sassc/native/sass2scss_api.rb b/lib/sassc/native/sass2scss_api.rb index 579627e3..1520cd74 100644 --- a/lib/sassc/native/sass2scss_api.rb +++ b/lib/sassc/native/sass2scss_api.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Native # ADDAPI char* ADDCALL sass2scss (const char* sass, const int options); diff --git a/lib/sassc/native/sass_input_style.rb b/lib/sassc/native/sass_input_style.rb index ce33729a..593f382a 100644 --- a/lib/sassc/native/sass_input_style.rb +++ b/lib/sassc/native/sass_input_style.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Native SassInputStyle = enum( diff --git a/lib/sassc/native/sass_output_style.rb b/lib/sassc/native/sass_output_style.rb index b05291e9..266d4f11 100644 --- a/lib/sassc/native/sass_output_style.rb +++ b/lib/sassc/native/sass_output_style.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Native SassOutputStyle = enum( diff --git a/lib/sassc/native/sass_value.rb b/lib/sassc/native/sass_value.rb index 6da1003d..86588edc 100644 --- a/lib/sassc/native/sass_value.rb +++ b/lib/sassc/native/sass_value.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Native class SassValue < FFI::Union; end diff --git a/lib/sassc/native/string_list.rb b/lib/sassc/native/string_list.rb index 36bea094..4b593b32 100644 --- a/lib/sassc/native/string_list.rb +++ b/lib/sassc/native/string_list.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Native class StringList < FFI::Struct diff --git a/lib/sassc/sass_2_scss.rb b/lib/sassc/sass_2_scss.rb index 02c248c9..59e5b09c 100644 --- a/lib/sassc/sass_2_scss.rb +++ b/lib/sassc/sass_2_scss.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC class Sass2Scss def self.convert(sass) diff --git a/lib/sassc/script.rb b/lib/sassc/script.rb index 28076c04..5f5586c4 100644 --- a/lib/sassc/script.rb +++ b/lib/sassc/script.rb @@ -1,5 +1,8 @@ +# frozen_string_literal: true + module SassC module Script + def self.custom_functions Functions.instance_methods.select do |function| Functions.public_method_defined?(function) @@ -8,42 +11,9 @@ def self.custom_functions def self.formatted_function_name(function_name) params = Functions.instance_method(function_name).parameters - params = params.map { |param_type, name| "$#{name}#{': null' if param_type == :opt}" } - .join(", ") - - "#{function_name}(#{params})" - end - - module Value + params = params.map { |param_type, name| "$#{name}#{': null' if param_type == :opt}" }.join(", ") + return "#{function_name}(#{params})" end - end -end - -require_relative "script/functions" -require_relative "script/value_conversion" -module Sass - module Script end end - -require 'sass/util' - -begin - require 'sass/deprecation' -rescue LoadError -end - -require 'sass/script/value/base' -require 'sass/script/value/string' -require 'sass/script/value/color' -require 'sass/script/value/bool' - -SassC::Script::String = Sass::Script::Value::String -SassC::Script::Value::String = Sass::Script::Value::String - -SassC::Script::Color = Sass::Script::Value::Color -SassC::Script::Value::Color = Sass::Script::Value::Color - -SassC::Script::Bool = Sass::Script::Value::Bool -SassC::Script::Value::Bool = Sass::Script::Value::Bool diff --git a/lib/sassc/script/functions.rb b/lib/sassc/script/functions.rb index aba75ac5..d44cd14e 100644 --- a/lib/sassc/script/functions.rb +++ b/lib/sassc/script/functions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Script module Functions diff --git a/lib/sassc/script/value.rb b/lib/sassc/script/value.rb new file mode 100644 index 00000000..b1d05dbd --- /dev/null +++ b/lib/sassc/script/value.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +# The abstract superclass for SassScript objects. +# Many of these methods, especially the ones that correspond to SassScript operations, +# are designed to be overridden by subclasses which may change the semantics somewhat. +# The operations listed here are just the defaults. + +class SassC::Script::Value + + # Returns the pure Ruby value of the value. + # The type of this value varies based on the subclass. + attr_reader :value + + # The source range in the document on which this node appeared. + attr_accessor :source_range + + # Creates a new value. + def initialize(value = nil) + value.freeze unless value.nil? || value == true || value == false + @value = value + @options = nil + end + + # Sets the options hash for this node, + # as well as for all child nodes. + # See the official Sass reference for options. + attr_writer :options + + # Returns the options hash for this node. + # Raises SassC::SyntaxError if the value was created + # outside of the parser and \{#to\_s} was called on it + def options + return @options if @options + raise SassC::SyntaxError.new("The #options attribute is not set on this #{self.class}. This error is probably occurring because #to_s was called on this value within a custom Sass function without first setting the #options attribute.") + end + + # Returns the hash code of this value. Two objects' hash codes should be + # equal if the objects are equal. + def hash + value.hash + end + + # True if this Value is the same as `other` + def eql?(other) + self == other + end + + # Returns a system inspect value for this object + def inspect + value.inspect + end + + # Returns `true` (all Values are truthy) + def to_bool + true + end + + # Compares this object to `other` + def ==(other) + self.class == other.class && value == other.value + end + + # Returns the integer value of this value. + # Raises SassC::SyntaxError if this value doesn’t implment integer conversion. + def to_i + raise SassC::SyntaxError.new("#{inspect} is not an integer.") + end + + # @raise [SassC::SyntaxError] if this value isn't an integer + def assert_int!; to_i; end + + # Returns the separator for this value. For non-list-like values or the + # empty list, this will be `nil`. For lists or maps, it will be `:space` or `:comma`. + def separator + nil + end + + # Whether the value is surrounded by square brackets. For non-list values, + # this will be `false`. + def bracketed + false + end + + # Returns the value of this Value as an array. + # Single Values are considered the same as single-element arrays. + def to_a + [self] + end + + # Returns the value of this value as a hash. Most values don't have hash + # representations, but [Map]s and empty [List]s do. + # + # @return [Hash] This value as a hash + # @raise [SassC::SyntaxError] if this value doesn't have a hash representation + def to_h + raise SassC::SyntaxError.new("#{inspect} is not a map.") + end + + # Returns the string representation of this value + # as it would be output to the CSS document. + # + # @options opts :quote [String] + # The preferred quote style for quoted strings. If `:none`, strings are + # always emitted unquoted. + # @return [String] + def to_s(opts = {}) + SassC::Util.abstract(self) + end + alias_method :to_sass, :to_s + + # Returns `false` (all Values are truthy) + def null? + false + end + + # Creates a new list containing `contents` but with the same brackets and + # separators as this object, when interpreted as a list. + # + # @param contents [Array] The contents of the new list. + # @param separator [Symbol] The separator of the new list. Defaults to \{#separator}. + # @param bracketed [Boolean] Whether the new list is bracketed. Defaults to \{#bracketed}. + # @return [Sass::Script::Value::List] + def with_contents(contents, separator: self.separator, bracketed: self.bracketed) + SassC::Script::Value::List.new(contents, separator: separator, bracketed: bracketed) + end + + protected + + # Evaluates the value. + # + # @param environment [Sass::Environment] The environment in which to evaluate the SassScript + # @return [Value] This value + def _perform(environment) + self + end + +end diff --git a/lib/sassc/script/value/bool.rb b/lib/sassc/script/value/bool.rb new file mode 100644 index 00000000..9b4c2e8f --- /dev/null +++ b/lib/sassc/script/value/bool.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# A SassScript object representing a boolean (true or false) value. + +class SassC::Script::Value::Bool < SassC::Script::Value + + # The true value in SassScript. + # This is assigned before new is overridden below so that we use the default implementation. + TRUE = new(true) + + # The false value in SassScript. + # This is assigned before new is overridden below so that we use the default implementation. + FALSE = new(false) + + # We override object creation so that users of the core API + # will not need to know that booleans are specific constants. + # Tests `value` for truthiness and returns the TRUE or FALSE constant. + def self.new(value) + value ? TRUE : FALSE + end + + # The pure Ruby value of this Boolean + attr_reader :value + alias_method :to_bool, :value + + # Returns the string "true" or "false" for this value + def to_s(opts = {}) + @value.to_s + end + alias_method :to_sass, :to_s + +end diff --git a/lib/sassc/script/value/color.rb b/lib/sassc/script/value/color.rb new file mode 100644 index 00000000..1827e459 --- /dev/null +++ b/lib/sassc/script/value/color.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +# A SassScript object representing a CSS color. +# This class provides a very bare-bones system for storing a RGB(A) or HSL(A) +# color and converting it to a CSS color function. +# +# If your Sass method accepts a color you will need to perform any +# needed color mathematics or transformations yourself. + +class SassC::Script::Value::Color < SassC::Script::Value + + attr_reader :red + attr_reader :green + attr_reader :blue + attr_reader :hue + attr_reader :saturation + attr_reader :lightness + attr_reader :alpha + + # Creates a new color with (`red`, `green`, `blue`) or (`hue`, `saturation`, `lightness` + # values, plus an optional `alpha` transparency value. + def initialize(red:nil, green:nil, blue:nil, hue:nil, saturation:nil, lightness:nil, alpha:1.0) + if red && green && blue && alpha + @mode = :rgba + @red = SassC::Util.clamp(red.to_i, 0, 255) + @green = SassC::Util.clamp(green.to_i, 0, 255) + @blue = SassC::Util.clamp(blue.to_i, 0, 255) + @alpha = SassC::Util.clamp(alpha.to_f, 0.0, 1.0) + elsif hue && saturation && lightness && alpha + @mode = :hsla + @hue = SassC::Util.clamp(hue.to_i, 0, 360) + @saturation = SassC::Util.clamp(saturation.to_i, 0, 100) + @lightness = SassC::Util.clamp(lightness.to_i, 0, 100) + @alpha = SassC::Util.clamp(alpha.to_f, 0.0, 1.0) + else + raise SassC::UnsupportedValue, "Unable to determine color configuration for " + end + end + + # Returns a CSS color declaration in the form + # `rgb(…)`, `rgba(…)`, `hsl(…)`, or `hsla(…)`. + def to_s + if rgba? && @alpha == 1.0 + return "rgb(#{@red}, #{@green}, #{@blue})" + elsif rgba? + return "rgba(#{@red}, #{@green}, #{@blue}, #{alpha_string})" + elsif hsla? && @alpha == 1.0 + return "hsl(#{@hue}, #{@saturation}%, #{@lightness}%)" + else # hsla? + return "hsla(#{@hue}, #{@saturation}%, #{@lightness}%, #{alpha_string})" + end + end + + # True if this color has RGBA values + def rgba? + @mode == :rgba + end + + # True if this color has HSLA values + def hlsa? + @mode == :hlsa + end + + # Returns the alpha value of this color as a string + # and rounded to 8 decimal places. + def alpha_string + alpha.round(8).to_s + end + + # Returns the values of this color in an array. + # Provided for compatibility between different SassC::Script::Value classes + def value + return [ + red, green, blue, + hue, saturation, lightness, + alpha, + ].compact + end + + # True if this Color is equal to `other_color` + def eql?(other_color) + unless other_color.is_a?(self.class) + raise ArgumentError, "No implicit conversion of #{other_color.class} to #{self.class}" + end + self.value == other_color.value + end + alias_method :==, :eql? + + # Returns a numeric value for comparing two Color objects + # This method is used internally by the Hash class and is not the same as `.to_h` + def hash + value.hash + end + +end diff --git a/lib/sassc/script/value/list.rb b/lib/sassc/script/value/list.rb new file mode 100644 index 00000000..7fdd0c48 --- /dev/null +++ b/lib/sassc/script/value/list.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +# A SassScript object representing a CSS list. +# This includes both comma-separated lists and space-separated lists. + +class SassC::Script::Value::List < SassC::Script::Value + + # The Ruby array containing the contents of the list. + # + # @return [Array] + attr_reader :value + alias_method :to_a, :value + + # The operator separating the values of the list. + # Either `:comma` or `:space`. + # + # @return [Symbol] + attr_reader :separator + + # Whether the list is surrounded by square brackets. + # + # @return [Boolean] + attr_reader :bracketed + + # Creates a new list. + # + # @param value [Array] See \{#value} + # @param separator [Symbol] See \{#separator} + # @param bracketed [Boolean] See \{#bracketed} + def initialize(value, separator: nil, bracketed: false) + super(value) + @separator = separator + @bracketed = bracketed + end + + # @see Value#options= + def options=(options) + super + value.each {|v| v.options = options} + end + + # @see Value#eq + def eq(other) + SassC::Script::Value::Bool.new( + other.is_a?(List) && value == other.value && + separator == other.separator && bracketed == other.bracketed + ) + end + + def hash + @hash ||= [value, separator, bracketed].hash + end + + # @see Value#to_s + def to_s(opts = {}) + if !bracketed && value.empty? + raise SassC::SyntaxError.new("#{inspect} isn't a valid CSS value.") + end + + members = value. + reject {|e| e.is_a?(Null) || e.is_a?(List) && e.value.empty?}. + map {|e| e.to_s(opts)} + + contents = members.join(sep_str) + bracketed ? "[#{contents}]" : contents + end + + # @see Value#to_sass + def to_sass(opts = {}) + return bracketed ? "[]" : "()" if value.empty? + members = value.map do |v| + if element_needs_parens?(v) + "(#{v.to_sass(opts)})" + else + v.to_sass(opts) + end + end + + if separator == :comma && members.length == 1 + return "#{bracketed ? '[' : '('}#{members.first},#{bracketed ? ']' : ')'}" + end + + contents = members.join(sep_str(nil)) + bracketed ? "[#{contents}]" : contents + end + + # @see Value#to_h + def to_h + return {} if value.empty? + super + end + + # @see Value#inspect + def inspect + (bracketed ? '[' : '(') + value.map {|e| e.inspect}.join(sep_str(nil)) + (bracketed ? ']' : ')') + end + + # Asserts an index is within the list. + # + # @private + # + # @param list [Sass::Script::Value::List] The list for which the index should be checked. + # @param n [Sass::Script::Value::Number] The index being checked. + def self.assert_valid_index(list, n) + if !n.int? || n.to_i == 0 + raise ArgumentError.new("List index #{n} must be a non-zero integer") + elsif list.to_a.size == 0 + raise ArgumentError.new("List index is #{n} but list has no items") + elsif n.to_i.abs > (size = list.to_a.size) + raise ArgumentError.new( + "List index is #{n} but list is only #{size} item#{'s' if size != 1} long") + end + end + + private + + def element_needs_parens?(element) + if element.is_a?(List) + return false if element.value.length < 2 + return false if element.bracketed + precedence = Sass::Script::Parser.precedence_of(separator || :space) + return Sass::Script::Parser.precedence_of(element.separator || :space) <= precedence + end + + return false unless separator == :space + return false unless element.is_a?(Sass::Script::Tree::UnaryOperation) + element.operator == :minus || element.operator == :plus + end + + def sep_str(opts = options) + return ' ' if separator == :space + return ',' if opts && opts[:style] == :compressed + ', ' + end + +end diff --git a/lib/sassc/script/value/map.rb b/lib/sassc/script/value/map.rb new file mode 100644 index 00000000..27ae9670 --- /dev/null +++ b/lib/sassc/script/value/map.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +class SassC::Script::Value::Map < SassC::Script::Value + + # The Ruby hash containing the contents of this map. + # @return [Hash] + attr_reader :value + alias_method :to_h, :value + + # Creates a new map. + # + # @param hash [Hash] + def initialize(hash) + super(hash) + end + + # @see Value#options= + def options=(options) + super + value.each do |k, v| + k.options = options + v.options = options + end + end + + # @see Value#separator + def separator + :comma unless value.empty? + end + + # @see Value#to_a + def to_a + value.map do |k, v| + list = SassC::Script::Value::List.new([k, v], separator: :space) + list.options = options + list + end + end + + # @see Value#eq + def eq(other) + SassC::Script::Value::Bool.new(other.is_a?(Map) && value == other.value) + end + + def hash + @hash ||= value.hash + end + + # @see Value#to_s + def to_s(opts = {}) + raise SassC::SyntaxError.new("#{inspect} isn't a valid CSS value.") + end + + def to_sass(opts = {}) + return "()" if value.empty? + + to_sass = lambda do |value| + if value.is_a?(List) && value.separator == :comma + "(#{value.to_sass(opts)})" + else + value.to_sass(opts) + end + end + + "(#{value.map {|(k, v)| "#{to_sass[k]}: #{to_sass[v]}"}.join(', ')})" + end + alias_method :inspect, :to_sass + +end diff --git a/lib/sassc/script/value/number.rb b/lib/sassc/script/value/number.rb new file mode 100644 index 00000000..fe75441d --- /dev/null +++ b/lib/sassc/script/value/number.rb @@ -0,0 +1,389 @@ +# frozen_string_literal: true + +# A SassScript object representing a number. +# SassScript numbers can have decimal values, +# and can also have units. +# For example, `12`, `1px`, and `10.45em` +# are all valid values. +# +# Numbers can also have more complex units, such as `1px*em/in`. +# These cannot be inputted directly in Sass code at the moment. + +class SassC::Script::Value::Number < SassC::Script::Value + + # The Ruby value of the number. + # + # @return [Numeric] + attr_reader :value + + # A list of units in the numerator of the number. + # For example, `1px*em/in*cm` would return `["px", "em"]` + # @return [Array] + attr_reader :numerator_units + + # A list of units in the denominator of the number. + # For example, `1px*em/in*cm` would return `["in", "cm"]` + # @return [Array] + attr_reader :denominator_units + + # The original representation of this number. + # For example, although the result of `1px/2px` is `0.5`, + # the value of `#original` is `"1px/2px"`. + # + # This is only non-nil when the original value should be used as the CSS value, + # as in `font: 1px/2px`. + # + # @return [Boolean, nil] + attr_accessor :original + + def self.precision + Thread.current[:sass_numeric_precision] || Thread.main[:sass_numeric_precision] || 10 + end + + # Sets the number of digits of precision + # For example, if this is `3`, + # `3.1415926` will be printed as `3.142`. + # The numeric precision is stored as a thread local for thread safety reasons. + # To set for all threads, be sure to set the precision on the main thread. + def self.precision=(digits) + Thread.current[:sass_numeric_precision] = digits.round + Thread.current[:sass_numeric_precision_factor] = nil + Thread.current[:sass_numeric_epsilon] = nil + end + + # the precision factor used in numeric output + # it is derived from the `precision` method. + def self.precision_factor + Thread.current[:sass_numeric_precision_factor] ||= 10.0**precision + end + + # Used in checking equality of floating point numbers. Any + # numbers within an `epsilon` of each other are considered functionally equal. + # The value for epsilon is one tenth of the current numeric precision. + def self.epsilon + Thread.current[:sass_numeric_epsilon] ||= 1 / (precision_factor * 10) + end + + # Used so we don't allocate two new arrays for each new number. + NO_UNITS = [] + + # @param value [Numeric] The value of the number + # @param numerator_units [::String, Array<::String>] See \{#numerator\_units} + # @param denominator_units [::String, Array<::String>] See \{#denominator\_units} + def initialize(value, numerator_units = NO_UNITS, denominator_units = NO_UNITS) + numerator_units = [numerator_units] if numerator_units.is_a?(::String) + denominator_units = [denominator_units] if denominator_units.is_a?(::String) + super(value) + @numerator_units = numerator_units + @denominator_units = denominator_units + @options = nil + normalize! + end + + def hash + [value, numerator_units, denominator_units].hash + end + + # Hash-equality works differently than `==` equality for numbers. + # Hash-equality must be transitive, so it just compares the exact value, + # numerator units, and denominator units. + def eql?(other) + basically_equal?(value, other.value) && numerator_units == other.numerator_units && + denominator_units == other.denominator_units + end + + # @return [String] The CSS representation of this number + # @raise [Sass::SyntaxError] if this number has units that can't be used in CSS + # (e.g. `px*in`) + def to_s(opts = {}) + return original if original + raise Sass::SyntaxError.new("#{inspect} isn't a valid CSS value.") unless legal_units? + inspect + end + + # Returns a readable representation of this number. + # + # This representation is valid CSS (and valid SassScript) + # as long as there is only one unit. + # + # @return [String] The representation + def inspect(opts = {}) + return original if original + + value = self.class.round(self.value) + str = value.to_s + + # Ruby will occasionally print in scientific notation if the number is + # small enough. That's technically valid CSS, but it's not well-supported + # and confusing. + str = ("%0.#{self.class.precision}f" % value).gsub(/0*$/, '') if str.include?('e') + + # Sometimes numeric formatting will result in a decimal number with a trailing zero (x.0) + if str =~ /(.*)\.0$/ + str = $1 + end + + # We omit a leading zero before the decimal point in compressed mode. + if @options && options[:style] == :compressed + str.sub!(/^(-)?0\./, '\1.') + end + + unitless? ? str : "#{str}#{unit_str}" + end + alias_method :to_sass, :inspect + + # @return [Integer] The integer value of the number + # @raise [Sass::SyntaxError] if the number isn't an integer + def to_i + super unless int? + value.to_i + end + + # @return [Boolean] Whether or not this number is an integer. + def int? + basically_equal?(value % 1, 0.0) + end + + # @return [Boolean] Whether or not this number has no units. + def unitless? + @numerator_units.empty? && @denominator_units.empty? + end + + # Checks whether the number has the numerator unit specified. + # + # @example + # number = Sass::Script::Value::Number.new(10, "px") + # number.is_unit?("px") => true + # number.is_unit?(nil) => false + # + # @param unit [::String, nil] The unit the number should have or nil if the number + # should be unitless. + # @see Number#unitless? The unitless? method may be more readable. + def is_unit?(unit) + if unit + denominator_units.size == 0 && numerator_units.size == 1 && numerator_units.first == unit + else + unitless? + end + end + + # @return [Boolean] Whether or not this number has units that can be represented in CSS + # (that is, zero or one \{#numerator\_units}). + def legal_units? + (@numerator_units.empty? || @numerator_units.size == 1) && @denominator_units.empty? + end + + # Returns this number converted to other units. + # The conversion takes into account the relationship between e.g. mm and cm, + # as well as between e.g. in and cm. + # + # If this number has no units, it will simply return itself + # with the given units. + # + # An incompatible coercion, e.g. between px and cm, will raise an error. + # + # @param num_units [Array] The numerator units to coerce this number into. + # See {\#numerator\_units} + # @param den_units [Array] The denominator units to coerce this number into. + # See {\#denominator\_units} + # @return [Number] The number with the new units + # @raise [Sass::UnitConversionError] if the given units are incompatible with the number's + # current units + def coerce(num_units, den_units) + Number.new(if unitless? + value + else + value * coercion_factor(@numerator_units, num_units) / + coercion_factor(@denominator_units, den_units) + end, num_units, den_units) + end + + # @param other [Number] A number to decide if it can be compared with this number. + # @return [Boolean] Whether or not this number can be compared with the other. + def comparable_to?(other) + operate(other, :+) + true + rescue Sass::UnitConversionError + false + end + + # Returns a human readable representation of the units in this number. + # For complex units this takes the form of: + # numerator_unit1 * numerator_unit2 / denominator_unit1 * denominator_unit2 + # @return [String] a string that represents the units in this number + def unit_str + rv = @numerator_units.sort.join("*") + if @denominator_units.any? + rv << "/" + rv << @denominator_units.sort.join("*") + end + rv + end + + private + + # @private + # @see Sass::Script::Number.basically_equal? + def basically_equal?(num1, num2) + self.class.basically_equal?(num1, num2) + end + + # Checks whether two numbers are within an epsilon of each other. + # @return [Boolean] + def self.basically_equal?(num1, num2) + (num1 - num2).abs < epsilon + end + + # @private + def self.round(num) + if num.is_a?(Float) && (num.infinite? || num.nan?) + num + elsif basically_equal?(num % 1, 0.0) + num.round + else + ((num * precision_factor).round / precision_factor).to_f + end + end + + OPERATIONS = [:+, :-, :<=, :<, :>, :>=, :%] + + def operate(other, operation) + this = self + if OPERATIONS.include?(operation) + if unitless? + this = this.coerce(other.numerator_units, other.denominator_units) + else + other = other.coerce(@numerator_units, @denominator_units) + end + end + # avoid integer division + value = :/ == operation ? this.value.to_f : this.value + result = value.send(operation, other.value) + + if result.is_a?(Numeric) + Number.new(result, *compute_units(this, other, operation)) + else # Boolean op + Bool.new(result) + end + end + + def coercion_factor(from_units, to_units) + # get a list of unmatched units + from_units, to_units = sans_common_units(from_units, to_units) + + if from_units.size != to_units.size || !convertable?(from_units | to_units) + raise Sass::UnitConversionError.new( + "Incompatible units: '#{from_units.join('*')}' and '#{to_units.join('*')}'.") + end + + from_units.zip(to_units).inject(1) {|m, p| m * conversion_factor(p[0], p[1])} + end + + def compute_units(this, other, operation) + case operation + when :* + [this.numerator_units + other.numerator_units, + this.denominator_units + other.denominator_units] + when :/ + [this.numerator_units + other.denominator_units, + this.denominator_units + other.numerator_units] + else + [this.numerator_units, this.denominator_units] + end + end + + def normalize! + return if unitless? + @numerator_units, @denominator_units = + sans_common_units(@numerator_units, @denominator_units) + + @denominator_units.each_with_index do |d, i| + next unless convertable?(d) && (u = @numerator_units.find {|n| convertable?([n, d])}) + @value /= conversion_factor(d, u) + @denominator_units.delete_at(i) + @numerator_units.delete_at(@numerator_units.index(u)) + end + end + + # This is the source data for all the unit logic. It's pre-processed to make + # it efficient to figure out whether a set of units is mutually compatible + # and what the conversion ratio is between two units. + # + # These come from http://www.w3.org/TR/2012/WD-css3-values-20120308/. + relative_sizes = [ + { + "in" => Rational(1), + "cm" => Rational(1, 2.54), + "pc" => Rational(1, 6), + "mm" => Rational(1, 25.4), + "q" => Rational(1, 101.6), + "pt" => Rational(1, 72), + "px" => Rational(1, 96) + }, + { + "deg" => Rational(1, 360), + "grad" => Rational(1, 400), + "rad" => Rational(1, 2 * Math::PI), + "turn" => Rational(1) + }, + { + "s" => Rational(1), + "ms" => Rational(1, 1000) + }, + { + "Hz" => Rational(1), + "kHz" => Rational(1000) + }, + { + "dpi" => Rational(1), + "dpcm" => Rational(254, 100), + "dppx" => Rational(96) + } + ] + + # A hash from each known unit to the set of units that it's mutually + # convertible with. + MUTUALLY_CONVERTIBLE = {} + relative_sizes.map do |values| + set = values.keys.to_set + values.keys.each {|name| MUTUALLY_CONVERTIBLE[name] = set} + end + + # A two-dimensional hash from two units to the conversion ratio between + # them. Multiply `X` by `CONVERSION_TABLE[X][Y]` to convert it to `Y`. + CONVERSION_TABLE = {} + relative_sizes.each do |values| + values.each do |(name1, value1)| + CONVERSION_TABLE[name1] ||= {} + values.each do |(name2, value2)| + value = value1 / value2 + CONVERSION_TABLE[name1][name2] = value.denominator == 1 ? value.to_i : value.to_f + end + end + end + + def conversion_factor(from_unit, to_unit) + CONVERSION_TABLE[from_unit][to_unit] + end + + def convertable?(units) + units = Array(units).to_set + return true if units.empty? + return false unless (mutually_convertible = MUTUALLY_CONVERTIBLE[units.first]) + units.subset?(mutually_convertible) + end + + def sans_common_units(units1, units2) + units2 = units2.dup + # Can't just use -, because we want px*px to coerce properly to px*mm + units1 = units1.map do |u| + j = units2.index(u) + next u unless j + units2.delete_at(j) + nil + end + units1.compact! + return units1, units2 + end + +end diff --git a/lib/sassc/script/value/string.rb b/lib/sassc/script/value/string.rb new file mode 100644 index 00000000..b3dfa2fe --- /dev/null +++ b/lib/sassc/script/value/string.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +class SassC::Script::Value::String < SassC::Script::Value + + # The Ruby value of the string. + attr_reader :value + + # Whether this is a CSS string or a CSS identifier. + # The difference is that strings are written with double-quotes, + # while identifiers aren't. + # + # @return [Symbol] `:string` or `:identifier` + attr_reader :type + + # Returns the quoted string representation of `contents`. + # + # @options opts :quote [String] + # The preferred quote style for quoted strings. If `:none`, strings are + # always emitted unquoted. If `nil`, quoting is determined automatically. + # @options opts :sass [String] + # Whether to quote strings for Sass source, as opposed to CSS. Defaults to `false`. + def self.quote(contents, opts = {}) + quote = opts[:quote] + + # Short-circuit if there are no characters that need quoting. + unless contents =~ /[\n\\"']|\#\{/ + quote ||= '"' + return "#{quote}#{contents}#{quote}" + end + + if quote.nil? + if contents.include?('"') + if contents.include?("'") + quote = '"' + else + quote = "'" + end + else + quote = '"' + end + end + + # Replace single backslashes with multiples. + contents = contents.gsub("\\", "\\\\\\\\") + + # Escape interpolation. + contents = contents.gsub('#{', "\\\#{") if opts[:sass] + + if quote == '"' + contents = contents.gsub('"', "\\\"") + else + contents = contents.gsub("'", "\\'") + end + + contents = contents.gsub(/\n(?![a-fA-F0-9\s])/, "\\a").gsub("\n", "\\a ") + "#{quote}#{contents}#{quote}" + end + + # Creates a new string. + # + # @param value [String] See \{#value} + # @param type [Symbol] See \{#type} + # @param deprecated_interp_equivalent [String?] + # If this was created via a potentially-deprecated string interpolation, + # this is the replacement expression that should be suggested to the user. + def initialize(value, type = :identifier) + super(value) + @type = type + end + + # @see Value#plus + def plus(other) + if other.is_a?(SassC::Script::Value::String) + other_value = other.value + else + other_value = other.to_s(:quote => :none) + end + SassC::Script::Value::String.new(value + other_value, type) + end + + # @see Value#to_s + def to_s(opts = {}) + return @value.gsub(/\n\s*/, ' ') if opts[:quote] == :none || @type == :identifier + self.class.quote(value, opts) + end + + # @see Value#to_sass + def to_sass(opts = {}) + to_s(opts.merge(:sass => true)) + end + + def inspect + String.quote(value) + end + +end diff --git a/lib/sassc/script/value_conversion.rb b/lib/sassc/script/value_conversion.rb index 06d864cc..df580f1d 100644 --- a/lib/sassc/script/value_conversion.rb +++ b/lib/sassc/script/value_conversion.rb @@ -1,89 +1,69 @@ -module SassC - module Script - module ValueConversion - def self.from_native(native_value, options) - case value_tag = Native.value_get_tag(native_value) - when :sass_null - # no-op - when :sass_string - value = Native.string_get_value(native_value) - type = Native.string_get_type(native_value) - argument = Script::String.new(value, type) +# frozen_string_literal: true - argument - when :sass_boolean - value = Native.boolean_get_value(native_value) - argument = Script::Bool.new(value) - - argument - when :sass_number - value = Native.number_get_value(native_value) - unit = Native.number_get_unit(native_value) - argument = Sass::Script::Value::Number.new(value, unit) +module SassC::Script::ValueConversion - argument - when :sass_color - red, green, blue, alpha = Native.color_get_r(native_value), Native.color_get_g(native_value), Native.color_get_b(native_value), Native.color_get_a(native_value) - - argument = Script::Color.new([red, green, blue, alpha]) - argument.options = options - - argument - when :sass_map - values = {} - length = Native::map_get_length native_value - - (0..length-1).each do |index| - key = Native::map_get_key(native_value, index) - value = Native::map_get_value(native_value, index) - values[from_native(key, options)] = from_native(value, options) - end - - argument = Sass::Script::Value::Map.new values - argument - when :sass_list - length = Native::list_get_length(native_value) - items = (0...length).map do |index| - native_item = Native::list_get_value(native_value, index) - from_native(native_item, options) - end - - if Gem.loaded_specs['sass'].version < Gem::Version.create('3.5') - Sass::Script::Value::List.new(items, :space) - else - Sass::Script::Value::List.new(items, separator: :space) - end - else - raise UnsupportedValue.new("Sass argument of type #{value_tag} unsupported") - end + def self.from_native(native_value, options) + case value_tag = SassC::Native.value_get_tag(native_value) + when :sass_null + # no-op + when :sass_string + value = SassC::Native.string_get_value(native_value) + type = SassC::Native.string_get_type(native_value) + argument = SassC::Script::Value::String.new(value, type) + argument + when :sass_boolean + value = SassC::Native.boolean_get_value(native_value) + argument = SassC::Script::Value::Bool.new(value) + argument + when :sass_number + value = SassC::Native.number_get_value(native_value) + unit = SassC::Native.number_get_unit(native_value) + argument = SassC::Script::Value::Number.new(value, unit) + argument + when :sass_color + red, green, blue, alpha = SassC::Native.color_get_r(native_value), SassC::Native.color_get_g(native_value), SassC::Native.color_get_b(native_value), SassC::Native.color_get_a(native_value) + argument = SassC::Script::Value::Color.new(red:red, green:green, blue:blue, alpha:alpha) + argument.options = options + argument + when :sass_map + values = {} + length = SassC::Native::map_get_length native_value + (0..length-1).each do |index| + key = SassC::Native::map_get_key(native_value, index) + value = SassC::Native::map_get_value(native_value, index) + values[from_native(key, options)] = from_native(value, options) end - - def self.to_native(value) - case value_name = value.class.name.split("::").last - when "String" - String.new(value).to_native - when "Color" - Color.new(value).to_native - when "Number" - Number.new(value).to_native - when "Map" - Map.new(value).to_native - when "List" - List.new(value).to_native - when "Bool" - Bool.new(value).to_native - else - raise UnsupportedValue.new("Sass return type #{value_name} unsupported") - end + argument = SassC::Script::Value::Map.new values + argument + when :sass_list + length = SassC::Native::list_get_length(native_value) + items = (0...length).map do |index| + native_item = SassC::Native::list_get_value(native_value, index) + from_native(native_item, options) end + SassC::Script::Value::List.new(items, separator: :space) + else + raise UnsupportedValue.new("Sass argument of type #{value_tag} unsupported") end end -end -require_relative "value_conversion/base" -require_relative "value_conversion/string" -require_relative "value_conversion/number" -require_relative "value_conversion/color" -require_relative "value_conversion/map" -require_relative "value_conversion/list" -require_relative "value_conversion/bool" + def self.to_native(value) + case value_name = value.class.name.split("::").last + when "String" + SassC::Script::ValueConversion::String.new(value).to_native + when "Color" + SassC::Script::ValueConversion::Color.new(value).to_native + when "Number" + SassC::Script::ValueConversion::Number.new(value).to_native + when "Map" + SassC::Script::ValueConversion::Map.new(value).to_native + when "List" + SassC::Script::ValueConversion::List.new(value).to_native + when "Bool" + SassC::Script::ValueConversion::Bool.new(value).to_native + else + raise SassC::UnsupportedValue.new("Sass return type #{value_name} unsupported") + end + end + +end diff --git a/lib/sassc/script/value_conversion/base.rb b/lib/sassc/script/value_conversion/base.rb index ec29dd2e..a9ed1fa8 100644 --- a/lib/sassc/script/value_conversion/base.rb +++ b/lib/sassc/script/value_conversion/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Script module ValueConversion diff --git a/lib/sassc/script/value_conversion/bool.rb b/lib/sassc/script/value_conversion/bool.rb index 1cb721e3..d1c475c9 100644 --- a/lib/sassc/script/value_conversion/bool.rb +++ b/lib/sassc/script/value_conversion/bool.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Script module ValueConversion diff --git a/lib/sassc/script/value_conversion/color.rb b/lib/sassc/script/value_conversion/color.rb index ca8161a0..7ec35358 100644 --- a/lib/sassc/script/value_conversion/color.rb +++ b/lib/sassc/script/value_conversion/color.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Script module ValueConversion diff --git a/lib/sassc/script/value_conversion/list.rb b/lib/sassc/script/value_conversion/list.rb index 7bbcc0c2..0f60e15c 100644 --- a/lib/sassc/script/value_conversion/list.rb +++ b/lib/sassc/script/value_conversion/list.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Script module ValueConversion diff --git a/lib/sassc/script/value_conversion/map.rb b/lib/sassc/script/value_conversion/map.rb index 07505006..8243a21b 100644 --- a/lib/sassc/script/value_conversion/map.rb +++ b/lib/sassc/script/value_conversion/map.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Script module ValueConversion diff --git a/lib/sassc/script/value_conversion/number.rb b/lib/sassc/script/value_conversion/number.rb index e50e99cd..2351a2e5 100644 --- a/lib/sassc/script/value_conversion/number.rb +++ b/lib/sassc/script/value_conversion/number.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Script module ValueConversion diff --git a/lib/sassc/script/value_conversion/string.rb b/lib/sassc/script/value_conversion/string.rb index 76e44139..c62ef2e8 100644 --- a/lib/sassc/script/value_conversion/string.rb +++ b/lib/sassc/script/value_conversion/string.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC module Script module ValueConversion diff --git a/lib/sassc/util.rb b/lib/sassc/util.rb new file mode 100644 index 00000000..546a4989 --- /dev/null +++ b/lib/sassc/util.rb @@ -0,0 +1,231 @@ +# frozen_string_literal: true + +require "erb" +require "set" +require "enumerator" +require "stringio" +require "rbconfig" +require "uri" +require "thread" +require "pathname" + +# A module containing various useful functions. + +module SassC::Util + + extend self + + # An array of ints representing the Ruby version number. + # @api public + RUBY_VERSION_COMPONENTS = RUBY_VERSION.split(".").map {|s| s.to_i} + + # The Ruby engine we're running under. Defaults to `"ruby"` + # if the top-level constant is undefined. + # @api public + RUBY_ENGINE = defined?(::RUBY_ENGINE) ? ::RUBY_ENGINE : "ruby" + + # Maps the keys in a hash according to a block. + # @example + # map_keys({:foo => "bar", :baz => "bang"}) {|k| k.to_s} + # #=> {"foo" => "bar", "baz" => "bang"} + # @param hash [Hash] The hash to map + # @yield [key] A block in which the keys are transformed + # @yieldparam key [Object] The key that should be mapped + # @yieldreturn [Object] The new value for the key + # @return [Hash] The mapped hash + # @see #map_vals + # @see #map_hash + def map_keys(hash) + map_hash(hash) {|k, v| [yield(k), v]} + end + + # Restricts the numeric `value` to be within `min` and `max`, inclusive. + # If the value is lower than `min` + def clamp(value, min, max) + return min if value < min + return max if value > max + return value + end + + # Like [Fixnum.round], but leaves rooms for slight floating-point + # differences. + # + # @param value [Numeric] + # @return [Numeric] + def round(value) + # If the number is within epsilon of X.5, round up (or down for negative + # numbers). + mod = value % 1 + mod_is_half = (mod - 0.5).abs < SassC::Script::Value::Number.epsilon + if value > 0 + !mod_is_half && mod < 0.5 ? value.floor : value.ceil + else + mod_is_half || mod < 0.5 ? value.floor : value.ceil + end + end + + # Return an array of all possible paths through the given arrays. + # + # @param arrs [Array] + # @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 + + # Returns information about the caller of the previous method. + # + # @param entry [String] An entry in the `#caller` list, or a similarly formatted string + # @return [[String, Integer, (String, nil)]] + # An array containing the filename, line, and method name of the caller. + # The method name may be nil + def caller_info(entry = nil) + # JRuby evaluates `caller` incorrectly when it's in an actual default argument. + entry ||= caller[1] + info = entry.scan(/^((?:[A-Za-z]:)?.*?):(-?.*?)(?::.*`(.+)')?$/).first + info[1] = info[1].to_i + # This is added by Rubinius to designate a block, but we don't care about it. + info[2].sub!(/ \{\}\Z/, '') if info[2] + info + end + + # Throws a NotImplementedError for an abstract method. + # + # @param obj [Object] `self` + # @raise [NotImplementedError] + def abstract(obj) + raise NotImplementedError.new("#{obj.class} must implement ##{caller_info[2]}") + end + + # Prints a deprecation warning for the caller method. + # + # @param obj [Object] `self` + # @param message [String] A message describing what to do instead. + def deprecated(obj, message = nil) + obj_class = obj.is_a?(Class) ? "#{obj}." : "#{obj.class}#" + full_message = "DEPRECATION WARNING: #{obj_class}#{caller_info[2]} " + + "will be removed in a future version of Sass.#{("\n" + message) if message}" + SassC::Util.sass_warn full_message + end + + # Silences all Sass warnings within a block. + # + # @yield A block in which no Sass warnings will be printed + def silence_sass_warnings + old_level, Sass.logger.log_level = Sass.logger.log_level, :error + yield + ensure + SassC.logger.log_level = old_level + end + + # The same as `Kernel#warn`, but is silenced by \{#silence\_sass\_warnings}. + # + # @param msg [String] + def sass_warn(msg) + Sass.logger.warn("#{msg}\n") + end + + ## Cross Rails Version Compatibility + + # Returns the root of the Rails application, + # if this is running in a Rails context. + # Returns `nil` if no such root is defined. + # + # @return [String, nil] + def rails_root + if defined?(::Rails.root) + return ::Rails.root.to_s if ::Rails.root + raise "ERROR: Rails.root is nil!" + end + return RAILS_ROOT.to_s if defined?(RAILS_ROOT) + nil + end + + # Returns the environment of the Rails application, + # if this is running in a Rails context. + # Returns `nil` if no such environment is defined. + # + # @return [String, nil] + def rails_env + return ::Rails.env.to_s if defined?(::Rails.env) + return RAILS_ENV.to_s if defined?(RAILS_ENV) + nil + end + + ## Cross-OS Compatibility + # + # These methods are cached because some of them are called quite frequently + # and even basic checks like String#== are too costly to be called repeatedly. + + # Whether or not this is running on Windows. + # + # @return [Boolean] + def windows? + return @windows if defined?(@windows) + @windows = (RbConfig::CONFIG['host_os'] =~ /mswin|windows|mingw/i) + end + + # Whether or not this is running on IronRuby. + # + # @return [Boolean] + def ironruby? + return @ironruby if defined?(@ironruby) + @ironruby = RUBY_ENGINE == "ironruby" + end + + # Whether or not this is running on Rubinius. + # + # @return [Boolean] + def rbx? + return @rbx if defined?(@rbx) + @rbx = RUBY_ENGINE == "rbx" + end + + # Whether or not this is running on JRuby. + # + # @return [Boolean] + def jruby? + return @jruby if defined?(@jruby) + @jruby = RUBY_PLATFORM =~ /java/ + end + + # Returns an array of ints representing the JRuby version number. + # + # @return [Array] + def jruby_version + @jruby_version ||= ::JRUBY_VERSION.split(".").map {|s| s.to_i} + end + + # Returns `path` relative to `from`. + # + # This is like `Pathname#relative_path_from` except it accepts both strings + # and pathnames, it handles Windows path separators correctly, and it throws + # an error rather than crashing if the paths use different encodings + # (https://github.com/ruby/ruby/pull/713). + # + # @param path [String, Pathname] + # @param from [String, Pathname] + # @return [Pathname?] + def relative_path_from(path, from) + pathname(path.to_s).relative_path_from(pathname(from.to_s)) + rescue NoMethodError => e + raise e unless e.name == :zero? + + # Work around https://github.com/ruby/ruby/pull/713. + path = path.to_s + from = from.to_s + raise ArgumentError("Incompatible path encodings: #{path.inspect} is #{path.encoding}, " + + "#{from.inspect} is #{from.encoding}") + end + + singleton_methods.each {|method| module_function method} + +end diff --git a/lib/sassc/util/normalized_map.rb b/lib/sassc/util/normalized_map.rb new file mode 100644 index 00000000..e04a0eca --- /dev/null +++ b/lib/sassc/util/normalized_map.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require "delegate" + +# A hash that normalizes its string keys while still allowing you to get back +# to the original keys that were stored. If several different values normalize +# to the same value, whichever is stored last wins. + +class SassC::Util::NormalizedMap + + # Create a normalized map + def initialize(map = nil) + @key_strings = {} + @map = {} + map.each {|key, value| self[key] = value} if map + end + + # Specifies how to transform the key. + # This can be overridden to create other normalization behaviors. + def normalize(key) + key.tr("-", "_") + end + + # Returns the version of `key` as it was stored before + # normalization. If `key` isn't in the map, returns it as it was + # passed in. + # @return [String] + def denormalize(key) + @key_strings[normalize(key)] || key + end + + # @private + def []=(k, v) + normalized = normalize(k) + @map[normalized] = v + @key_strings[normalized] = k + v + end + + # @private + def [](k) + @map[normalize(k)] + end + + # @private + def has_key?(k) + @map.has_key?(normalize(k)) + end + + # @private + def delete(k) + normalized = normalize(k) + @key_strings.delete(normalized) + @map.delete(normalized) + end + + # @return [Hash] Hash with the keys as they were stored (before normalization). + def as_stored + SassC::Util.map_keys(@map) {|k| @key_strings[k]} + end + + def empty? + @map.empty? + end + + def values + @map.values + end + + def keys + @map.keys + end + + def each + @map.each {|k, v| yield(k, v)} + end + + def size + @map.size + end + + def to_hash + @map.dup + end + + def to_a + @map.to_a + end + + def map + @map.map {|k, v| yield(k, v)} + end + + def dup + d = super + d.send(:instance_variable_set, "@map", @map.dup) + d + end + + def sort_by + @map.sort_by {|k, v| yield k, v} + end + + def update(map) + map = map.as_stored if map.is_a?(NormalizedMap) + map.each {|k, v| self[k] = v} + end + + def method_missing(method, *args, &block) + @map.send(method, *args, &block) + end + + def respond_to_missing?(method, include_private = false) + @map.respond_to?(method, include_private) + end + +end diff --git a/lib/sassc/version.rb b/lib/sassc/version.rb index 05c405ac..2ffc63a0 100644 --- a/lib/sassc/version.rb +++ b/lib/sassc/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SassC - VERSION = "1.12.1" + VERSION = "2.0.0" end diff --git a/lib/tasks/libsass.rb b/lib/tasks/libsass.rb index ed920b43..57ed47bd 100644 --- a/lib/tasks/libsass.rb +++ b/lib/tasks/libsass.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + namespace :libsass do desc "Compile libsass" task :compile do diff --git a/sassc.gemspec b/sassc.gemspec index b11c1ca8..140f9099 100644 --- a/sassc.gemspec +++ b/sassc.gemspec @@ -1,9 +1,11 @@ -# coding: utf-8 -lib = File.expand_path('../lib', __FILE__) +# frozen_string_literal: true + +lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'sassc/version' +require "sassc/version" Gem::Specification.new do |spec| + spec.name = "sassc" spec.version = SassC::VERSION spec.authors = ["Ryan Boland"] @@ -17,6 +19,8 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.required_ruby_version = ">= 2.3.3" + spec.require_paths = ["lib"] spec.extensions = ["ext/Rakefile"] @@ -29,7 +33,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency "bundler" spec.add_dependency "ffi", "~> 1.9.6" - spec.add_dependency "sass", ">= 3.3.0" gem_dir = File.expand_path(File.dirname(__FILE__)) + "/" `git submodule --quiet foreach pwd`.split($\).each do |submodule_path| @@ -42,4 +45,5 @@ Gem::Specification.new do |spec| end end end + end diff --git a/test/custom_importer_test.rb b/test/custom_importer_test.rb index d1429328..b803b139 100644 --- a/test/custom_importer_test.rb +++ b/test/custom_importer_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "test_helper" module SassC diff --git a/test/engine_test.rb b/test/engine_test.rb index e8afdf64..ba923411 100644 --- a/test/engine_test.rb +++ b/test/engine_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "test_helper" module SassC @@ -224,7 +226,7 @@ def test_sass_variation end def test_encoding_matches_input - input = "$size: 30px;" + input = String.new("$size: 30px;") input.force_encoding("UTF-8") output = Engine.new(input).render assert_equal input.encoding, output.encoding @@ -257,13 +259,13 @@ def test_empty_template end def test_empty_template_returns_a_new_object - input = '' + input = String.new output = Engine.new(input).render assert input.object_id != output.object_id, 'empty template must return a new object' end def test_empty_template_encoding_matches_input - input = ''.force_encoding("ISO-8859-1") + input = String.new.force_encoding("ISO-8859-1") output = Engine.new(input).render assert_equal input.encoding, output.encoding end diff --git a/test/error_test.rb b/test/error_test.rb index 4f4a06e8..1caa316c 100644 --- a/test/error_test.rb +++ b/test/error_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "test_helper" module SassC diff --git a/test/functions_test.rb b/test/functions_test.rb index dadc8aea..71fc0fa8 100644 --- a/test/functions_test.rb +++ b/test/functions_test.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true + require_relative "test_helper" require "stringio" -require "sass/script" module SassC class FunctionsTest < MiniTest::Test @@ -114,7 +115,7 @@ def test_functions_may_accept_sass_color_type assert_sass <<-SCSS, <<-EXPECTED_CSS div { color: nice_color_argument(red); } SCSS - div { color: "red"; } + div { color: rgb(255, 0, 0); } EXPECTED_CSS end @@ -197,8 +198,13 @@ def stderr_output end module Script::Functions + def javascript_path(path) - Script::String.new("/js/#{path.value}", :string) + SassC::Script::Value::String.new("/js/#{path.value}", :string) + end + + def stylesheet_path(path) + SassC::Script::Value::String.new("/css/#{path.value}", :identifier) end def no_return_path(path) @@ -206,12 +212,12 @@ def no_return_path(path) end def sass_return_path(path) - Script::String.new("#{path.value}", :string) + SassC::Script::Value::String.new("#{path.value}", :string) end def optional_arguments(path, optional = nil) - optional ||= Script::String.new("bar") - Script::String.new("#{path.value}/#{optional.value}", :string) + optional ||= SassC::Script::Value::String.new("bar") + SassC::Script::Value::String.new("#{path.value}/#{optional.value}", :string) end def function_that_raises_errors @@ -222,44 +228,44 @@ def function_with_unsupported_tag(value) end def nice_color_argument(color) - return Script::String.new(color.to_s, :string) + return SassC::Script::Value::String.new(color.to_s, :identifier) end def returns_a_color - return Script::Color.new(red: 0, green: 0, blue: 0) + return SassC::Script::Value::Color.new(red: 0, green: 0, blue: 0) end def returns_a_number - return Sass::Script::Value::Number.new(-312,'rem') + return SassC::Script::Value::Number.new(-312,'rem') end def returns_a_bool - return Sass::Script::Value::Bool.new(true) + return SassC::Script::Value::Bool.new(true) end def inspect_bool ( argument ) - raise StandardError.new "passed value is not a Sass::Script::Value::Bool" unless argument.is_a? Sass::Script::Value::Bool + raise StandardError.new "passed value is not a Sass::Script::Value::Bool" unless argument.is_a? SassC::Script::Value::Bool return argument end def inspect_number ( argument ) - raise StandardError.new "passed value is not a Sass::Script::Value::Number" unless argument.is_a? Sass::Script::Value::Number + raise StandardError.new "passed value is not a Sass::Script::Value::Number" unless argument.is_a? SassC::Script::Value::Number return argument end def inspect_map ( argument ) argument.to_h.each_pair do |key, value| - raise StandardError.new "key #{key.inspect} is not a string" unless key.is_a? Sass::Script::Value::String + raise StandardError.new "key #{key.inspect} is not a string" unless key.is_a? SassC::Script::Value::String valueClass = case key.value when 'string' - Sass::Script::Value::String + SassC::Script::Value::String when 'number' - Sass::Script::Value::Number + SassC::Script::Value::Number when 'color' - Sass::Script::Value::Color + SassC::Script::Value::Color when 'map' - Sass::Script::Value::Map + SassC::Script::Value::Map end raise StandardError.new "unknown key #{key.inspect}" unless valueClass @@ -269,41 +275,29 @@ def inspect_map ( argument ) end def inspect_list(argument) - raise StandardError.new "passed value is not a Sass::Script::Value::List" unless argument.is_a? Sass::Script::Value::List + raise StandardError.new "passed value is not a Sass::Script::Value::List" unless argument.is_a? SassC::Script::Value::List return argument end def returns_sass_value - return Sass::Script::Value::Color.new(red: 0, green: 0, blue: 0) + return SassC::Script::Value::Color.new(red: 0, green: 0, blue: 0) end def returns_sass_map - key = Script::String.new("color", "string") - value = Sass::Script::Value::Color.new(red: 0, green: 0, blue: 0) + key = SassC::Script::Value::String.new("color", "string") + value = SassC::Script::Value::Color.new(red: 0, green: 0, blue: 0) values = {} values[key] = value - map = Sass::Script::Value::Map.new values + map = SassC::Script::Value::Map.new values return map end def returns_sass_list - numbers = [10, 20, 30].map do |n| - Sass::Script::Value::Number.new(n, '') - end - - if Gem.loaded_specs['sass'].version < Gem::Version.create('3.5') - Sass::Script::Value::List.new(numbers, :space) - else - Sass::Script::Value::List.new(numbers, separator: :space) - end + numbers = [10, 20, 30].map { |n| SassC::Script::Value::Number.new(n, '') } + SassC::Script::Value::List.new(numbers, separator: :space) end - module Compass - def stylesheet_path(path) - Script::String.new("/css/#{path.value}", :identifier) - end - end - include Compass end + end end diff --git a/test/native_test.rb b/test/native_test.rb index ad0528ad..639e0e7c 100644 --- a/test/native_test.rb +++ b/test/native_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "test_helper" module SassC @@ -57,10 +59,6 @@ def test_multibyte_characters_work refute_equal 0, status end - def test_failed_compile_gives_error_message - skip - end - def test_custom_function data_context = Native.make_data_context("foo { margin: foo(); }") context = Native.data_context_get_context(data_context) diff --git a/test/output_style_test.rb b/test/output_style_test.rb index fee731ff..f9a90597 100644 --- a/test/output_style_test.rb +++ b/test/output_style_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "test_helper" module SassC diff --git a/test/sass_2_scss_test.rb b/test/sass_2_scss_test.rb index 24a4011c..85282f31 100644 --- a/test/sass_2_scss_test.rb +++ b/test/sass_2_scss_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "test_helper" module SassC diff --git a/test/test_helper.rb b/test/test_helper.rb index 2551f6b7..f1ca3307 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "minitest/autorun" require "minitest/pride" require "minitest/around/unit"