diff --git a/lib/sass/calculation_value.rb b/lib/sass/calculation_value.rb new file mode 100644 index 00000000..3541d14f --- /dev/null +++ b/lib/sass/calculation_value.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Sass + # The type of values that can be arguments to a SassCalculation. + # + # @see https://sass-lang.com/documentation/js-api/types/calculationvalue/ + module CalculationValue + # @return [CalculationValue] + # @raise [ScriptError] + def assert_calculation_value(_name = nil) + self + end + end +end + +require_relative 'calculation_value/calculation_interpolation' +require_relative 'calculation_value/calculation_operation' diff --git a/lib/sass/calculation_value/calculation_interpolation.rb b/lib/sass/calculation_value/calculation_interpolation.rb new file mode 100644 index 00000000..f524787b --- /dev/null +++ b/lib/sass/calculation_value/calculation_interpolation.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Sass + module CalculationValue + # A string injected into a SassCalculation using interpolation. + # + # @see https://sass-lang.com/documentation/js-api/classes/calculationinterpolation/ + class CalculationInterpolation + include CalculationValue + + def initialize(value) + @value = value + end + + attr_reader :value + + def ==(other) + other.is_a?(Sass::CalculationValue::CalculationInterpolation) && + other.value == value + end + + def hash + @hash ||= value.hash + end + end + end +end diff --git a/lib/sass/calculation_value/calculation_operation.rb b/lib/sass/calculation_value/calculation_operation.rb new file mode 100644 index 00000000..57e9300a --- /dev/null +++ b/lib/sass/calculation_value/calculation_operation.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Sass + module CalculationValue + # A binary operation that can appear in a SassCalculation. + # + # @see https://sass-lang.com/documentation/js-api/classes/calculationoperation/ + class CalculationOperation + include CalculationValue + + OPERATORS = ['+', '-', '*', '/'].freeze + + private_constant :OPERATORS + + def initialize(operator, left, right) + raise Sass::ScriptError, "Invalid operator: #{operator}" unless OPERATORS.include?(operator) + + left.assert_calculation_value + right.assert_calculation_value + + @operator = operator.freeze + @left = left.freeze + @right = right.freeze + end + + attr_reader :operator, :left, :right + + def ==(other) + other.is_a?(Sass::CalculationValue::CalculationOperation) && + other.operator == operator && + other.left == left && + other.right == right + end + + def hash + @hash ||= [operator, left, right].hash + end + end + end +end diff --git a/lib/sass/compile_result.rb b/lib/sass/compile_result.rb index c211b2e3..af48d9b7 100644 --- a/lib/sass/compile_result.rb +++ b/lib/sass/compile_result.rb @@ -3,7 +3,7 @@ module Sass # The result of compiling Sass to CSS. Returned by {Sass.compile} and {Sass.compile_string}. # - # @see https://sass-lang.com/documentation/js-api/interfaces/CompileResult + # @see https://sass-lang.com/documentation/js-api/interfaces/compileresult/ class CompileResult # @return [String] attr_reader :css diff --git a/lib/sass/embedded.rb b/lib/sass/embedded.rb index 096052ca..ff008083 100644 --- a/lib/sass/embedded.rb +++ b/lib/sass/embedded.rb @@ -95,7 +95,7 @@ def initialize # Compiles the Sass file at +path+ to CSS. # @param path [String] # @param load_paths [Array] Paths in which to look for stylesheets loaded by rules like - # {@use}[https://sass-lang.com/documentation/at-rules/use] and {@import}[https://sass-lang.com/documentation/at-rules/import]. + # {@use}[https://sass-lang.com/documentation/at-rules/use/] and {@import}[https://sass-lang.com/documentation/at-rules/import/]. # @param charset [Boolean] By default, if the CSS document contains non-ASCII characters, Sass adds a +@charset+ # declaration (in expanded output mode) or a byte-order mark (in compressed mode) to indicate its encoding to # browsers or other consumers. If +charset+ is +false+, these annotations are omitted. @@ -104,7 +104,7 @@ def initialize # @param style [String, Symbol] The OutputStyle of the compiled CSS. # @param functions [Hash] Additional built-in Sass functions that are available in all stylesheets. # @param importers [Array] Custom importers that control how Sass resolves loads from rules like - # {@use}[https://sass-lang.com/documentation/at-rules/use] and {@import}[https://sass-lang.com/documentation/at-rules/import]. + # {@use}[https://sass-lang.com/documentation/at-rules/use/] and {@import}[https://sass-lang.com/documentation/at-rules/import/]. # @param alert_ascii [Boolean] If this is +true+, the compiler will exclusively use ASCII characters in its error # and warning messages. Otherwise, it may use non-ASCII Unicode characters as well. # @param alert_color [Boolean] If this is +true+, the compiler will use ANSI color escape codes in its error and @@ -119,7 +119,7 @@ def initialize # deprecation warning it encounters. # @return [CompileResult] # @raise [CompileError] - # @see https://sass-lang.com/documentation/js-api/modules#compile + # @see https://sass-lang.com/documentation/js-api/functions/compile/ def compile(path, load_paths: [], @@ -163,7 +163,7 @@ def compile(path, # @param source [String] # @param importer [Object] The importer to use to handle loads that are relative to the entrypoint stylesheet. # @param load_paths [Array] Paths in which to look for stylesheets loaded by rules like - # {@use}[https://sass-lang.com/documentation/at-rules/use] and {@import}[https://sass-lang.com/documentation/at-rules/import]. + # {@use}[https://sass-lang.com/documentation/at-rules/use/] and {@import}[https://sass-lang.com/documentation/at-rules/import/]. # @param syntax [String, Symbol] The Syntax to use to parse the entrypoint stylesheet. # @param url [String] The canonical URL of the entrypoint stylesheet. If this is passed along with +importer+, it's # used to resolve relative loads in the entrypoint stylesheet. @@ -175,7 +175,7 @@ def compile(path, # @param style [String, Symbol] The OutputStyle of the compiled CSS. # @param functions [Hash] Additional built-in Sass functions that are available in all stylesheets. # @param importers [Array] Custom importers that control how Sass resolves loads from rules like - # {@use}[https://sass-lang.com/documentation/at-rules/use] and {@import}[https://sass-lang.com/documentation/at-rules/import]. + # {@use}[https://sass-lang.com/documentation/at-rules/use/] and {@import}[https://sass-lang.com/documentation/at-rules/import/]. # @param alert_ascii [Boolean] If this is +true+, the compiler will exclusively use ASCII characters in its error # and warning messages. Otherwise, it may use non-ASCII Unicode characters as well. # @param alert_color [Boolean] If this is +true+, the compiler will use ANSI color escape codes in its error and @@ -190,7 +190,7 @@ def compile(path, # deprecation warning it encounters. # @return [CompileResult] # @raise [CompileError] - # @see https://sass-lang.com/documentation/js-api/modules#compileString + # @see https://sass-lang.com/documentation/js-api/functions/compilestring/ def compile_string(source, importer: nil, load_paths: [], @@ -234,7 +234,7 @@ def compile_string(source, end # @return [String] Information about the Sass implementation. - # @see https://sass-lang.com/documentation/js-api/modules#info + # @see https://sass-lang.com/documentation/js-api/variables/info/ def info @info ||= Host.new(@dispatcher).version_request end diff --git a/lib/sass/embedded/host.rb b/lib/sass/embedded/host.rb index 8a4695c0..bbac4d35 100644 --- a/lib/sass/embedded/host.rb +++ b/lib/sass/embedded/host.rb @@ -86,7 +86,13 @@ def version_response(message) end def error(message) - @error = message + if message.is_a?(EmbeddedProtocol::ProtocolError) + return if message.id != id + + @error = Errno::EPROTO.new(message.message) + else + @error = message + end @queue.close end diff --git a/lib/sass/embedded/host/value_protofier.rb b/lib/sass/embedded/host/value_protofier.rb index 49042e22..60fc06c7 100644 --- a/lib/sass/embedded/host/value_protofier.rb +++ b/lib/sass/embedded/host/value_protofier.rb @@ -22,11 +22,7 @@ def to_proto(obj) ) when Sass::Value::Number EmbeddedProtocol::Value.new( - number: EmbeddedProtocol::Value::Number.new( - value: obj.value.to_f, - numerators: obj.numerator_units, - denominators: obj.denominator_units - ) + number: Number.to_proto(obj) ) when Sass::Value::Color if obj.instance_eval { !defined?(@hue) } @@ -100,6 +96,10 @@ def to_proto(obj) ) ) end + when Sass::Value::Calculation + EmbeddedProtocol::Value.new( + calculation: Calculation.to_proto(obj) + ) when Sass::Value::Boolean EmbeddedProtocol::Value.new( singleton: obj.value ? :TRUE : :FALSE @@ -123,12 +123,7 @@ def from_proto(proto) quoted: obj.quoted ) when :number - Sass::Value::Number.new( - obj.value, { - numerator_units: obj.numerators.to_a, - denominator_units: obj.denominators.to_a - } - ) + Number.from_proto(obj) when :rgb_color Sass::Value::Color.new( red: obj.red, @@ -181,6 +176,8 @@ def from_proto(proto) Sass::Value::Function.new(obj.id) when :host_function raise Sass::ScriptError, 'The compiler may not send Value.host_function to host' + when :calculation + Calculation.from_proto(obj) when :singleton case obj when :TRUE @@ -197,6 +194,178 @@ def from_proto(proto) end end + # The {Number} Protofier. + module Number + module_function + + def to_proto(obj) + EmbeddedProtocol::Value::Number.new( + value: obj.value.to_f, + numerators: obj.numerator_units, + denominators: obj.denominator_units + ) + end + + def from_proto(obj) + Sass::Value::Number.new( + obj.value, { + numerator_units: obj.numerators.to_a, + denominator_units: obj.denominators.to_a + } + ) + end + end + + private_constant :Number + + # The {Calculation} Protofier. + module Calculation + module_function + + def to_proto(obj) + EmbeddedProtocol::Value::Calculation.new( + name: obj.name, + arguments: obj.arguments.map { |argument| CalculationValue.to_proto(argument) } + ) + end + + def from_proto(obj) + case obj.name + when 'calc' + if obj.arguments.length != 1 + raise Sass::ScriptError, + 'Value.Calculation.arguments must have exactly one argument for calc().' + end + + Sass::Value::Calculation.calc(*obj.arguments.map { |argument| CalculationValue.from_proto(argument) }) + when 'clamp' + if obj.arguments.length != 3 + raise Sass::ScriptError, + 'Value.Calculation.arguments must have exactly 3 arguments for clamp().' + end + + Sass::Value::Calculation.clamp(*obj.arguments.map { |argument| CalculationValue.from_proto(argument) }) + when 'min' + if obj.arguments.empty? + raise Sass::ScriptError, + 'Value.Calculation.arguments must have at least 1 argument for min().' + end + + Sass::Value::Calculation.min(obj.arguments.map { |argument| CalculationValue.from_proto(argument) }) + when 'max' + if obj.arguments.empty? + raise Sass::ScriptError, + 'Value.Calculation.arguments must have at least 1 argument for max().' + end + + Sass::Value::Calculation.max(obj.arguments.map { |argument| CalculationValue.from_proto(argument) }) + else + raise Sass::ScriptError, + "Value.Calculation.name #{calculation.name.inspect} is not a recognized calculation type." + end + end + end + + private_constant :Calculation + + # The {CalculationValue} Protofier. + module CalculationValue + module_function + + def to_proto(value) + case value + when Sass::Value::Number + EmbeddedProtocol::Value::Calculation::CalculationValue.new( + number: Number.to_proto(value) + ) + when Sass::Value::Calculation + EmbeddedProtocol::Value::Calculation::CalculationValue.new( + calculation: Calculation.to_proto(value) + ) + when Sass::Value::String + EmbeddedProtocol::Value::Calculation::CalculationValue.new( + string: value.text + ) + when Sass::CalculationValue::CalculationOperation + EmbeddedProtocol::Value::Calculation::CalculationValue.new( + operation: EmbeddedProtocol::Value::Calculation::CalculationOperation.new( + operator: CalculationOperator.to_proto(value.operator), + left: to_proto(value.left), + right: to_proto(value.right) + ) + ) + when Sass::CalculationValue::CalculationInterpolation + EmbeddedProtocol::Value::Calculation::CalculationValue.new( + interpolation: value.value + ) + else + raise Sass::ScriptError, "Unknown CalculationValue #{value}" + end + end + + def from_proto(value) + oneof = value.value + obj = proto.public_send(oneof) + case oneof + when :number + Number.from_proto(obj) + when :calculation + Calculation.from_proto(obj) + when :string + Sass::Value::String.new(obj, quoted: false) + when :operation + Sass::CalculationValue::CalculationOperation.new( + CalculationOperator.from_proto(obj.operator), + from_proto(obj.left), + from_proto(obj.right) + ) + when :interpolation + Sass::CalculationValue::CalculationInterpolation.new(obj) + else + raise Sass::ScriptError, "Unknown CalculationValue #{value}" + end + end + end + + private_constant :CalculationValue + + # The {CalculationOperator} Protofier. + module CalculationOperator + module_function + + def to_proto(operator) + case operator + when '+' + :PLUS + when '-' + :MINUS + when '*' + :TIMES + when '/' + :DIVIDE + else + raise Sass::ScriptError, "Unknown CalculationOperator #{separator}" + end + end + + def from_proto(operator) + case operator + when :PLUS + '+' + when :MINUS + '-' + when :TIMES + '*' + when :DIVIDE + '/' + else + raise Sass::ScriptError, "Unknown CalculationOperator #{separator}" + end + end + end + + private_constant :CalculationOperator + # The {ListSeparator} Protofier. module ListSeparator module_function diff --git a/lib/sass/logger/silent.rb b/lib/sass/logger/silent.rb index 1014fd0f..ac06b756 100644 --- a/lib/sass/logger/silent.rb +++ b/lib/sass/logger/silent.rb @@ -3,11 +3,13 @@ module Sass # A namespace for built-in Loggers. # - # @see https://sass-lang.com/documentation/js-api/modules/Logger + # @see https://sass-lang.com/documentation/js-api/modules/logger/ module Logger module_function # A Logger that silently ignores all warnings and debug messages. + # + # @see https://sass-lang.com/documentation/js-api/variables/logger.silent/ def silent Silent end diff --git a/lib/sass/logger/source_location.rb b/lib/sass/logger/source_location.rb index c4949664..7181461f 100644 --- a/lib/sass/logger/source_location.rb +++ b/lib/sass/logger/source_location.rb @@ -6,7 +6,7 @@ module Logger # # This is always associated with a {SourceSpan} which indicates which file it refers to. # - # @see https://sass-lang.com/documentation/js-api/interfaces/SourceLocation + # @see https://sass-lang.com/documentation/js-api/interfaces/sourcelocation/ class SourceLocation # @return [Integer] attr_reader :offset, :line, :column diff --git a/lib/sass/logger/source_span.rb b/lib/sass/logger/source_span.rb index 090781a6..6e8ee08c 100644 --- a/lib/sass/logger/source_span.rb +++ b/lib/sass/logger/source_span.rb @@ -4,7 +4,7 @@ module Sass module Logger # A span of text within a source file. # - # @see https://sass-lang.com/documentation/js-api/interfaces/SourceSpan + # @see https://sass-lang.com/documentation/js-api/interfaces/sourcespan/ class SourceSpan # @return [SourceLocation] attr_reader :start, :end diff --git a/lib/sass/value.rb b/lib/sass/value.rb index 0507926f..24909366 100644 --- a/lib/sass/value.rb +++ b/lib/sass/value.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true +require_relative 'calculation_value' require_relative 'script_error' module Sass # The abstract base class of Sass's value types. # - # @see https://sass-lang.com/documentation/js-api/classes/Value + # @see https://sass-lang.com/documentation/js-api/classes/value/ module Value # @return [::String, nil] def separator @@ -60,11 +61,18 @@ def assert_boolean(name = nil) raise Sass::ScriptError.new("#{self} is not a boolean", name) end + # @return [Calculation] # @raise [ScriptError] def assert_calculation(name = nil) raise Sass::ScriptError.new("#{self} is not a calculation", name) end + # @return [CalculationValue] + # @raise [ScriptError] + def assert_calculation_value(name = nil) + raise Sass::ScriptError.new("#{self} is not a calculation value", name) + end + # @return [Color] # @raise [ScriptError] def assert_color(name = nil) @@ -119,6 +127,7 @@ def to_a_length require_relative 'value/list' require_relative 'value/argument_list' require_relative 'value/boolean' +require_relative 'value/calculation' require_relative 'value/color' require_relative 'value/function' require_relative 'value/fuzzy_math' diff --git a/lib/sass/value/argument_list.rb b/lib/sass/value/argument_list.rb index 61b63106..018fed38 100644 --- a/lib/sass/value/argument_list.rb +++ b/lib/sass/value/argument_list.rb @@ -7,7 +7,7 @@ module Value # An argument list comes from a rest argument. It's distinct from a normal {List} in that it may contain a keyword # map as well as the positional arguments. # - # @see https://sass-lang.com/documentation/js-api/classes/SassArgumentList + # @see https://sass-lang.com/documentation/js-api/classes/sassargumentlist/ class ArgumentList < Value::List # @param contents [Array] # @param keywords [Hash<::String, Value>] diff --git a/lib/sass/value/boolean.rb b/lib/sass/value/boolean.rb index bad569bb..ea4d418e 100644 --- a/lib/sass/value/boolean.rb +++ b/lib/sass/value/boolean.rb @@ -4,7 +4,7 @@ module Sass module Value # Sass's boolean type. # - # @see https://sass-lang.com/documentation/js-api/classes/SassBoolean + # @see https://sass-lang.com/documentation/js-api/classes/sassboolean/ class Boolean include Value diff --git a/lib/sass/value/calculation.rb b/lib/sass/value/calculation.rb new file mode 100644 index 00000000..9a0e328b --- /dev/null +++ b/lib/sass/value/calculation.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +module Sass + module Value + # Sass's calculation type. + # + # @see https://sass-lang.com/documentation/js-api/classes/sasscalculation/ + class Calculation + include Value + include CalculationValue + + def initialize(name, arguments) + @name = name.freeze + @arguments = arguments.freeze + end + + # @return [::String] + attr_reader :name + + # @return [Array] + attr_reader :arguments + + private_class_method :new + + class << self + # @return [Calculation] + def calc(argument) + argument.assert_calculation_value + new('calc', [argument]) + end + + # @return [Calculation] + def min(arguments) + arguments.each(&:assert_calculation_value) + new('min', arguments) + end + + # @return [Calculation] + def max(arguments) + arguments.each(&:assert_calculation_value) + new('max', arguments) + end + + # @return [Calculation] + def clamp(min, value = nil, max = nil) + if (value.nil? && !valid_clamp_arg?(min)) || + (max.nil? && [min, value].none? { |x| x && valid_clamp_arg?(x) }) + raise Sass::ScriptError, 'Argument must be an unquoted SassString or CalculationInterpolation.' + end + + arguments = [min] + arguments.push(value) unless value.nil? + arguments.push(max) unless max.nil? + arguments.each(&:assert_calculation_value) + new('clamp', arguments) + end + + private + + def valid_clamp_arg?(value) + value.is_a?(Sass::CalculationValue::CalculationInterpolation) || + (value.is_a?(Sass::Value::String) && !value.quoted?) + end + end + + # @return [Calculation] + def assert_calculation(_name = nil) + self + end + + # @return [::Boolean] + def ==(other) + other.is_a?(Sass::Value::Calculation) && + other.name == name && + other.arguments == arguments + end + + # @return [Integer] + def hash + @hash ||= [name, *arguments].hash + end + end + end +end diff --git a/lib/sass/value/color.rb b/lib/sass/value/color.rb index 04c233f7..6844148c 100644 --- a/lib/sass/value/color.rb +++ b/lib/sass/value/color.rb @@ -6,7 +6,7 @@ module Value # # No matter what representation was originally used to create this color, all of its channels are accessible. # - # @see https://sass-lang.com/documentation/js-api/classes/SassColor + # @see https://sass-lang.com/documentation/js-api/classes/sasscolor/ class Color include Value diff --git a/lib/sass/value/function.rb b/lib/sass/value/function.rb index dc7b91fc..3d412c92 100644 --- a/lib/sass/value/function.rb +++ b/lib/sass/value/function.rb @@ -4,7 +4,7 @@ module Sass module Value # Sass's function type. # - # @see https://sass-lang.com/documentation/js-api/classes/SassFunction + # @see https://sass-lang.com/documentation/js-api/classes/sassfunction/ class Function include Value diff --git a/lib/sass/value/list.rb b/lib/sass/value/list.rb index cb9802c9..c3269e39 100644 --- a/lib/sass/value/list.rb +++ b/lib/sass/value/list.rb @@ -4,7 +4,7 @@ module Sass module Value # Sass's list type. # - # @see https://sass-lang.com/documentation/js-api/classes/SassList + # @see https://sass-lang.com/documentation/js-api/classes/sasslist/ class List include Value diff --git a/lib/sass/value/map.rb b/lib/sass/value/map.rb index f42b3483..03c3b9f7 100644 --- a/lib/sass/value/map.rb +++ b/lib/sass/value/map.rb @@ -4,7 +4,7 @@ module Sass module Value # Sass's map type. # - # @see https://sass-lang.com/documentation/js-api/classes/SassMap + # @see https://sass-lang.com/documentation/js-api/classes/sassmap/ class Map include Value diff --git a/lib/sass/value/null.rb b/lib/sass/value/null.rb index 7c95a612..c680a300 100644 --- a/lib/sass/value/null.rb +++ b/lib/sass/value/null.rb @@ -4,7 +4,7 @@ module Sass module Value # Sass's null type. # - # @see https://sass-lang.com/documentation/js-api/modules#sassNull + # @see https://sass-lang.com/documentation/js-api/variables/sassnull/ class Null include Value diff --git a/lib/sass/value/number.rb b/lib/sass/value/number.rb index a0182299..bdca78ed 100644 --- a/lib/sass/value/number.rb +++ b/lib/sass/value/number.rb @@ -6,9 +6,10 @@ module Sass module Value # Sass's number type. # - # @see https://sass-lang.com/documentation/js-api/classes/SassNumber + # @see https://sass-lang.com/documentation/js-api/classes/sassnumber/ class Number include Value + include CalculationValue # @param value [Numeric] # @param unit [::String, Hash] diff --git a/lib/sass/value/string.rb b/lib/sass/value/string.rb index d1eb9907..f7176542 100644 --- a/lib/sass/value/string.rb +++ b/lib/sass/value/string.rb @@ -4,9 +4,10 @@ module Sass module Value # Sass's string type. # - # @see https://sass-lang.com/documentation/js-api/classes/SassString + # @see https://sass-lang.com/documentation/js-api/classes/sassstring/ class String include Value + include CalculationValue # @param text [::String] # @param quoted [::Boolean] @@ -38,6 +39,14 @@ def assert_string(_name = nil) self end + # @return [CalculationValue] + # @raise [ScriptError] + def assert_calculation_value(_name = nil) + raise Sass::ScriptError, "Expected #{self} to be an unquoted string." if quoted? + + self + end + # @param sass_index [Number] # @return [Integer] def sass_index_to_string_index(sass_index, name = nil) diff --git a/spec/sass/value/argument_list_spec.rb b/spec/sass/value/argument_list_spec.rb index 5b6dae72..46f031bc 100644 --- a/spec/sass/value/argument_list_spec.rb +++ b/spec/sass/value/argument_list_spec.rb @@ -96,6 +96,7 @@ it "isn't any other type" do expect { list.assert_boolean }.to raise_error(Sass::ScriptError) + expect { list.assert_calculation }.to raise_error(Sass::ScriptError) expect { list.assert_color }.to raise_error(Sass::ScriptError) expect { list.assert_function }.to raise_error(Sass::ScriptError) expect { list.assert_map }.to raise_error(Sass::ScriptError) diff --git a/spec/sass/value/boolean_spec.rb b/spec/sass/value/boolean_spec.rb index ab66326a..ce1a7ca5 100644 --- a/spec/sass/value/boolean_spec.rb +++ b/spec/sass/value/boolean_spec.rb @@ -23,6 +23,7 @@ end it "isn't any other type" do + expect { value.assert_calculation }.to raise_error(Sass::ScriptError) expect { value.assert_color }.to raise_error(Sass::ScriptError) expect { value.assert_function }.to raise_error(Sass::ScriptError) expect { value.assert_map }.to raise_error(Sass::ScriptError) @@ -52,6 +53,7 @@ end it "isn't any other type" do + expect { value.assert_calculation }.to raise_error(Sass::ScriptError) expect { value.assert_color }.to raise_error(Sass::ScriptError) expect { value.assert_function }.to raise_error(Sass::ScriptError) expect { value.assert_map }.to raise_error(Sass::ScriptError) diff --git a/spec/sass/value/calculation_spec.rb b/spec/sass/value/calculation_spec.rb new file mode 100644 index 00000000..717cac45 --- /dev/null +++ b/spec/sass/value/calculation_spec.rb @@ -0,0 +1,402 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Sass::Value::Calculation do + valid_calculation_values = [ + Sass::Value::Number.new(1), + Sass::Value::String.new('1', quoted: false), + described_class.calc(Sass::Value::Number.new(1)), + Sass::CalculationValue::CalculationOperation.new('+', Sass::Value::Number.new(1), Sass::Value::Number.new(1)), + Sass::CalculationValue::CalculationInterpolation.new('') + ] + invalid_calculation_values = [Sass::Value::String.new('1', quoted: true)] + + describe 'construction' do + calculation = nil + before do + calculation = described_class.calc(Sass::Value::Number.new(1)) + end + + it 'is a value' do + expect(calculation).to be_a(Sass::Value) + end + + it 'is a calculation' do + expect(calculation).to be_a(described_class) + expect(calculation.assert_calculation).to be(calculation) + end + + it "isn't any other type" do + expect { calculation.assert_boolean }.to raise_error(Sass::ScriptError) + expect { calculation.assert_color }.to raise_error(Sass::ScriptError) + expect { calculation.assert_function }.to raise_error(Sass::ScriptError) + expect { calculation.assert_map }.to raise_error(Sass::ScriptError) + expect(calculation.to_map).to be_nil + expect { calculation.assert_number }.to raise_error(Sass::ScriptError) + expect { calculation.assert_string }.to raise_error(Sass::ScriptError) + end + end + + describe 'calc' do + it 'correctly stores name and arguments' do + result = described_class.calc(Sass::Value::Number.new(1)) + expect(result.name).to be('calc') + expect(result.arguments).to eq([Sass::Value::Number.new(1)]) + end + + it 'rejects invalid arguments' do + invalid_calculation_values.each do |value| + expect { described_class.calc(value) }.to raise_error(Sass::ScriptError) + end + end + + it 'accepts valid arguments' do + valid_calculation_values.each do |value| + expect { described_class.calc(value) }.not_to raise_error + end + end + end + + describe 'min' do + it 'correctly stores name and arguments' do + result = described_class.min([ + Sass::Value::Number.new(1), + Sass::Value::Number.new(2) + ]) + expect(result.name).to be('min') + expect(result.arguments).to eq([ + Sass::Value::Number.new(1), + Sass::Value::Number.new(2) + ]) + end + + it 'rejects invalid arguments' do + invalid_calculation_values.each do |value| + expect { described_class.min([value, Sass::Value::Number.new(2)]) }.to raise_error(Sass::ScriptError) + expect { described_class.min([Sass::Value::Number.new(1), value]) }.to raise_error(Sass::ScriptError) + end + end + + it 'accepts valid arguments' do + valid_calculation_values.each do |value| + expect { described_class.min([value, Sass::Value::Number.new(2)]) }.not_to raise_error + expect { described_class.min([Sass::Value::Number.new(1), value]) }.not_to raise_error + end + end + end + + describe 'max' do + it 'correctly stores name and arguments' do + result = described_class.max([ + Sass::Value::Number.new(1), + Sass::Value::Number.new(2) + ]) + expect(result.name).to be('max') + expect(result.arguments).to eq([ + Sass::Value::Number.new(1), + Sass::Value::Number.new(2) + ]) + end + + it 'rejects invalid arguments' do + invalid_calculation_values.each do |value| + expect { described_class.max([value, Sass::Value::Number.new(2)]) }.to raise_error(Sass::ScriptError) + expect { described_class.max([Sass::Value::Number.new(1), value]) }.to raise_error(Sass::ScriptError) + end + end + + it 'accepts valid arguments' do + valid_calculation_values.each do |value| + expect { described_class.max([value, Sass::Value::Number.new(2)]) }.not_to raise_error + expect { described_class.max([Sass::Value::Number.new(1), value]) }.not_to raise_error + end + end + end + + describe 'clamp' do + it 'correctly stores name and arguments' do + result = described_class.clamp( + Sass::Value::Number.new(1), + Sass::Value::Number.new(2), + Sass::Value::Number.new(3) + ) + expect(result.name).to be('clamp') + expect(result.arguments).to eq([ + Sass::Value::Number.new(1), + Sass::Value::Number.new(2), + Sass::Value::Number.new(3) + ]) + end + + it 'rejects invalid arguments' do + invalid_calculation_values.each do |value| + expect do + described_class.clamp(value, Sass::Value::Number.new(2), + Sass::Value::Number.new(3)) + end.to raise_error(Sass::ScriptError) + expect do + described_class.clamp(Sass::Value::Number.new(1), value, + Sass::Value::Number.new(3)) + end.to raise_error(Sass::ScriptError) + expect do + described_class.clamp(Sass::Value::Number.new(1), Sass::Value::Number.new(2), + value) + end.to raise_error(Sass::ScriptError) + end + end + + it 'accepts valid arguments' do + valid_calculation_values.each do |value| + expect do + described_class.clamp(value, Sass::Value::Number.new(2), Sass::Value::Number.new(3)) + end.not_to raise_error + expect do + described_class.clamp(Sass::Value::Number.new(1), value, Sass::Value::Number.new(3)) + end.not_to raise_error + expect do + described_class.clamp(Sass::Value::Number.new(1), Sass::Value::Number.new(2), value) + end.not_to raise_error + end + end + + # When `clamp()` is called with less than three arguments, the list of + # accepted values is much narrower + valid_clamp_values = [ + Sass::Value::String.new('1', quoted: false), + Sass::CalculationValue::CalculationInterpolation.new('1') + ] + invalid_clamp_values = [ + Sass::Value::Number.new(1), + Sass::Value::String.new('1', quoted: true) + ] + + it 'rejects invalid values for one argument' do + invalid_clamp_values.each do |value| + expect { described_class.clamp(value) }.to raise_error(Sass::ScriptError) + end + end + + it 'accepts valid values for one argument' do + valid_clamp_values.each do |value| + expect { described_class.clamp(value) }.not_to raise_error + end + end + + it 'rejects invalid values for two arguments' do + invalid_clamp_values.each do |value| + expect { described_class.clamp(value, value) }.to raise_error(Sass::ScriptError) + end + end + + it 'accepts valid values for two arguments' do + valid_clamp_values.each do |value| + expect { described_class.clamp(value, value) }.not_to raise_error + end + end + end + + describe 'simplifies' do + it 'calc()' do + fn = lambda do |_args| + described_class.calc( + Sass::CalculationValue::CalculationOperation.new('+', Sass::Value::Number.new(1), Sass::Value::Number.new(2)) + ) + end + + expect( + Sass.compile_string('a {b: foo()}', + functions: { 'foo()': fn }).css + ).to eq("a {\n b: 3;\n}") + end + + it 'clamp()' do + fn = lambda do |_args| + described_class.clamp( + Sass::Value::Number.new(1), + Sass::Value::Number.new(2), + Sass::Value::Number.new(3) + ) + end + + expect( + Sass.compile_string('a {b: foo()}', + functions: { 'foo()': fn }).css + ).to eq("a {\n b: 2;\n}") + end + + it 'min()' do + fn = lambda do |_args| + described_class.min([Sass::Value::Number.new(1), Sass::Value::Number.new(2)]) + end + + expect( + Sass.compile_string('a {b: foo()}', + functions: { 'foo()': fn }).css + ).to eq("a {\n b: 1;\n}") + end + + it 'max()' do + fn = lambda do |_args| + described_class.max([Sass::Value::Number.new(1), Sass::Value::Number.new(2)]) + end + + expect( + Sass.compile_string('a {b: foo()}', + functions: { 'foo()': fn }).css + ).to eq("a {\n b: 2;\n}") + end + + it 'operations' do + fn = lambda do |_args| + described_class.calc( + Sass::CalculationValue::CalculationOperation.new( + '+', + described_class.min([Sass::Value::Number.new(3), Sass::Value::Number.new(4)]), + Sass::CalculationValue::CalculationOperation.new( + '*', + described_class.max([Sass::Value::Number.new(5), Sass::Value::Number.new(6)]), + Sass::CalculationValue::CalculationOperation.new( + '-', + Sass::Value::Number.new(3), + Sass::CalculationValue::CalculationOperation.new( + '/', + Sass::Value::Number.new(4), + Sass::Value::Number.new(5) + ) + ) + ) + ) + ) + end + + expect( + Sass.compile_string('a {b: foo()}', + functions: { 'foo()': fn }).css + ).to eq("a {\n b: 16.2;\n}") + end + end + + describe 'throws when simplifying' do + it 'calc() with more than one argument' do + fn = lambda do |_args| + described_class.calc(Sass::Value::Number.new(1), Sass::Value::Number.new(2)) + end + + expect do + Sass.compile_string('a {b: foo()}', + functions: { 'foo()': fn }).css + end.to raise_error(Sass::CompileError) + end + + it 'clamp() with the wrong number of arguments' do + fn = lambda do |_args| + described_class.clamp(Sass::CalculationValue::CalculationInterpolation.new('1')) + end + + expect do + Sass.compile_string('a {b: foo()}', + functions: { 'foo()': fn }).css + end.to raise_error do |error| + expect(error).to be_a(Sass::CompileError) + expect(error.full_message).to match(/exactly 3 arguments/) + end + end + + it 'an unknown calculation function' do + fn = lambda do |_args| + described_class.send(:new, 'foo', [Sass::Value::Number.new(1)]) + end + + expect do + Sass.compile_string('a {b: foo()}', + functions: { 'foo()': fn }).css + end.to raise_error do |error| + expect(error).to be_a(Sass::CompileError) + expect(error.full_message).to match(/"foo" is not a recognized calculation type/) + end + end + end + + describe 'CalculationOperation' do + valid_operators = ['+', '-', '*', '/'] + invalid_operators = ['||', '&&', 'plus', 'minus', ''] + + describe 'construction' do + it 'rejects invalid operators' do + invalid_operators.each do |operator| + expect do + Sass::CalculationValue::CalculationOperation.new( + operator, + Sass::Value::Number.new(1), + Sass::Value::Number.new(2) + ) + end.to raise_error(Sass::ScriptError) + end + end + + it 'accepts valid operators' do + valid_operators.each do |operator| + expect do + Sass::CalculationValue::CalculationOperation.new( + operator, + Sass::Value::Number.new(1), + Sass::Value::Number.new(2) + ) + end.not_to raise_error + end + end + end + + it 'rejects invalid operands' do + invalid_calculation_values.each do |operand| + expect do + Sass::CalculationValue::CalculationOperation.new('+', operand, Sass::Value::Number.new(1)) + end.to raise_error(Sass::ScriptError) + expect do + Sass::CalculationValue::CalculationOperation.new('+', Sass::Value::Number.new(1), operand) + end.to raise_error(Sass::ScriptError) + end + end + + it 'accepts valid operands' do + valid_calculation_values.each do |operand| + expect do + Sass::CalculationValue::CalculationOperation.new('+', operand, Sass::Value::Number.new(1)) + end.not_to raise_error + expect do + Sass::CalculationValue::CalculationOperation.new('+', Sass::Value::Number.new(1), operand) + end.not_to raise_error + end + end + + describe 'stores' do + operation = nil + before do + operation = Sass::CalculationValue::CalculationOperation.new( + '+', + Sass::Value::Number.new(1), + Sass::Value::Number.new(2) + ) + end + + it 'operator' do + expect(operation.operator).to eq('+') + end + + it 'left' do + expect(operation.left).to eq(Sass::Value::Number.new(1)) + end + + it 'right' do + expect(operation.right).to eq(Sass::Value::Number.new(2)) + end + end + end + + describe 'CalculationInterpolation' do + it 'stores value' do + expect(Sass::CalculationValue::CalculationInterpolation.new('1').value).to eq('1') + end + end +end diff --git a/spec/sass/value/color_spec.rb b/spec/sass/value/color_spec.rb index 04d25809..652503fd 100644 --- a/spec/sass/value/color_spec.rb +++ b/spec/sass/value/color_spec.rb @@ -33,6 +33,7 @@ def hwb(hue, whiteness, blackness, alpha = nil) it "isn't any other type" do expect { color.assert_boolean }.to raise_error(Sass::ScriptError) + expect { color.assert_calculation }.to raise_error(Sass::ScriptError) expect { color.assert_function }.to raise_error(Sass::ScriptError) expect { color.assert_map }.to raise_error(Sass::ScriptError) expect(color.to_map).to be_nil diff --git a/spec/sass/value/list_spec.rb b/spec/sass/value/list_spec.rb index ecbeacdf..485f3754 100644 --- a/spec/sass/value/list_spec.rb +++ b/spec/sass/value/list_spec.rb @@ -18,6 +18,7 @@ it "isn't any other type" do expect { list.assert_boolean }.to raise_error(Sass::ScriptError) + expect { list.assert_calculation }.to raise_error(Sass::ScriptError) expect { list.assert_color }.to raise_error(Sass::ScriptError) expect { list.assert_function }.to raise_error(Sass::ScriptError) expect { list.assert_map }.to raise_error(Sass::ScriptError) diff --git a/spec/sass/value/map_spec.rb b/spec/sass/value/map_spec.rb index 21e42994..3c30ecff 100644 --- a/spec/sass/value/map_spec.rb +++ b/spec/sass/value/map_spec.rb @@ -24,6 +24,7 @@ it "isn't any other type" do expect { map.assert_boolean }.to raise_error(Sass::ScriptError) + expect { map.assert_calculation }.to raise_error(Sass::ScriptError) expect { map.assert_color }.to raise_error(Sass::ScriptError) expect { map.assert_function }.to raise_error(Sass::ScriptError) expect { map.assert_number }.to raise_error(Sass::ScriptError) diff --git a/spec/sass/value/null_spec.rb b/spec/sass/value/null_spec.rb index ed89516a..12fd7643 100644 --- a/spec/sass/value/null_spec.rb +++ b/spec/sass/value/null_spec.rb @@ -23,6 +23,7 @@ it "isn't any type" do expect { value.assert_boolean }.to raise_error(Sass::ScriptError) + expect { value.assert_calculation }.to raise_error(Sass::ScriptError) expect { value.assert_color }.to raise_error(Sass::ScriptError) expect { value.assert_function }.to raise_error(Sass::ScriptError) expect { value.assert_map }.to raise_error(Sass::ScriptError) diff --git a/spec/sass/value/number_spec.rb b/spec/sass/value/number_spec.rb index 5861dcdb..5c606f16 100644 --- a/spec/sass/value/number_spec.rb +++ b/spec/sass/value/number_spec.rb @@ -33,6 +33,7 @@ it "isn't any other type" do expect { number.assert_boolean }.to raise_error(Sass::ScriptError) + expect { number.assert_calculation }.to raise_error(Sass::ScriptError) expect { number.assert_color }.to raise_error(Sass::ScriptError) expect { number.assert_function }.to raise_error(Sass::ScriptError) expect { number.assert_map }.to raise_error(Sass::ScriptError) diff --git a/spec/sass/value/string_spec.rb b/spec/sass/value/string_spec.rb index 9feee2e6..71259386 100644 --- a/spec/sass/value/string_spec.rb +++ b/spec/sass/value/string_spec.rb @@ -52,6 +52,7 @@ it "isn't any other type" do value = described_class.new('nb') expect { value.assert_boolean }.to raise_error(Sass::ScriptError) + expect { value.assert_calculation }.to raise_error(Sass::ScriptError) expect { value.assert_color }.to raise_error(Sass::ScriptError) expect { value.assert_function }.to raise_error(Sass::ScriptError) expect { value.assert_map }.to raise_error(Sass::ScriptError) diff --git a/spec/sass_function_spec.rb b/spec/sass_function_spec.rb index 559d73e2..e2cb3ad0 100644 --- a/spec/sass_function_spec.rb +++ b/spec/sass_function_spec.rb @@ -90,7 +90,7 @@ described_class.compile_string( 'a {b: foo()}', functions: { - 'foo()': -> { raise 'heck' } + 'foo()': ->(_args) { raise 'heck' } } ) end.to raise_error do |error| @@ -104,7 +104,7 @@ described_class.compile_string( 'a {b: foo()}', functions: { - 'foo()': -> {} + 'foo()': ->(_args) {} } ) end.to raise_error do |error| @@ -113,17 +113,33 @@ end end - it 'returning a non-Value' do - expect do - described_class.compile_string( - 'a {b: foo()}', - functions: { - 'foo()': -> { 'wrong' } - } - ) - end.to raise_error do |error| - expect(error).to be_a(Sass::CompileError) - expect(error.span.start.line).to eq(0) + describe 'returning a non-Value' do + it 'directly' do + expect do + described_class.compile_string( + 'a {b: foo()}', + functions: { + 'foo()': ->(_args) { 'wrong' } + } + ) + end.to raise_error do |error| + expect(error).to be_a(Sass::CompileError) + expect(error.span.start.line).to eq(0) + end + end + + it 'in a calculation' do + expect do + described_class.compile_string( + 'a {b: foo()}', + functions: { + 'foo()': ->(_args) { Sass::Value::Calculation.calc('wrong') } + } + ) + end.to raise_error do |error| + expect(error).to be_a(Sass::CompileError) + expect(error.span.start.line).to eq(0) + end end end end @@ -165,55 +181,55 @@ describe 'rejects a function signature that' do it 'is empty' do expect do - described_class.compile_string('', functions: { '': -> { Sass::Value::Null::NULL } }) + described_class.compile_string('', functions: { '': ->(_args) { Sass::Value::Null::NULL } }) end.to raise_error(Sass::CompileError) end it 'has no name' do expect do - described_class.compile_string('', functions: { '()': -> { Sass::Value::Null::NULL } }) + described_class.compile_string('', functions: { '()': ->(_args) { Sass::Value::Null::NULL } }) end.to raise_error(Sass::CompileError) end it 'has no arguments' do expect do - described_class.compile_string('', functions: { foo: -> { Sass::Value::Null::NULL } }) + described_class.compile_string('', functions: { foo: ->(_args) { Sass::Value::Null::NULL } }) end.to raise_error(Sass::CompileError) end it 'has invalid arguments' do expect do - described_class.compile_string('', functions: { 'foo(arg)': -> { Sass::Value::Null::NULL } }) + described_class.compile_string('', functions: { 'foo(arg)': ->(_args) { Sass::Value::Null::NULL } }) end.to raise_error(Sass::CompileError) end it 'has no closing parentheses' do expect do - described_class.compile_string('', functions: { 'foo(': -> { Sass::Value::Null::NULL } }) + described_class.compile_string('', functions: { 'foo(': ->(_args) { Sass::Value::Null::NULL } }) end.to raise_error(Sass::CompileError) end it 'has a non-identifier name' do expect do - described_class.compile_string('', functions: { '$foo()': -> { Sass::Value::Null::NULL } }) + described_class.compile_string('', functions: { '$foo()': ->(_args) { Sass::Value::Null::NULL } }) end.to raise_error(Sass::CompileError) end it 'has whitespace before the signature' do expect do - described_class.compile_string('', functions: { ' foo()': -> { Sass::Value::Null::NULL } }) + described_class.compile_string('', functions: { ' foo()': ->(_args) { Sass::Value::Null::NULL } }) end.to raise_error(Sass::CompileError) end it 'has whitespace after the signature' do expect do - described_class.compile_string('', functions: { 'foo() ': -> { Sass::Value::Null::NULL } }) + described_class.compile_string('', functions: { 'foo() ': ->(_args) { Sass::Value::Null::NULL } }) end.to raise_error(Sass::CompileError) end it 'has whitespace between the identifier and the arguments' do expect do - described_class.compile_string('', functions: { 'foo ()': -> { Sass::Value::Null::NULL } }) + described_class.compile_string('', functions: { 'foo ()': ->(_args) { Sass::Value::Null::NULL } }) end.to raise_error(Sass::CompileError) end end