diff --git a/Rakefile b/Rakefile index 6a6b7700..13e97e8a 100644 --- a/Rakefile +++ b/Rakefile @@ -4,22 +4,12 @@ rescue LoadError puts 'Cannot load bundler/gem_tasks' end -task default: :prepare +require 'tasks/libsass' -task prepare: "ext/lib/libsass.so" +task default: :test -file "ext/lib/libsass.so" do - gem_dir = File.expand_path(File.dirname(__FILE__)) + "/" - cd "ext/libsass" - sh 'make lib/libsass.so LDFLAGS="-Wall -O2"' - cd gem_dir -end - -task test: :prepare do +desc "Run all tests" +task test: 'libsass:compile' do $LOAD_PATH.unshift('lib', 'test') Dir.glob('./test/**/*_test.rb') { |f| require f } end - -task :submodule do - sh "git submodule update --init" -end diff --git a/ext/Rakefile b/ext/Rakefile new file mode 100644 index 00000000..a8ed3958 --- /dev/null +++ b/ext/Rakefile @@ -0,0 +1,3 @@ +require_relative '../lib/tasks/libsass' + +task default: 'libsass:compile' diff --git a/lib/sassc/functions_handler.rb b/lib/sassc/functions_handler.rb index 3dc49b0c..b6c3c7d5 100644 --- a/lib/sassc/functions_handler.rb +++ b/lib/sassc/functions_handler.rb @@ -10,25 +10,45 @@ def setup(native_options) list = Native.make_function_list(Script.custom_functions.count) - functs = FunctionWrapper.extend(Script::Functions) - functs.options = @options + functions = FunctionWrapper.extend(Script::Functions) + functions.options = @options Script.custom_functions.each_with_index do |custom_function, i| - @callbacks[custom_function] = FFI::Function.new(:pointer, [:pointer, :pointer]) do |s_args, cookie| - length = Native.list_get_length(s_args) + @callbacks[custom_function] = FFI::Function.new(:pointer, [:pointer, :pointer]) do |native_argument_list, cookie| + native_argument_list_length = Native.list_get_length(native_argument_list) + custom_function_arguments = [] + error_tag = nil - v = Native.list_get_value(s_args, 0) - v = Native.string_get_value(v).dup + (0...native_argument_list_length).each do |i| + native_value = Native.list_get_value(native_argument_list, i) - s = Script::String.new(Script::String.unquote(v), Script::String.type(v)) + case value_tag = Native.value_get_tag(native_value) + when :sass_null + # no-op + when :sass_string + native_string = Native.string_get_value(native_value) + argument = Script::String.new(Script::String.unquote(native_string), Script::String.type(native_string)) - value = functs.send(custom_function, s) + custom_function_arguments << argument + else + error_tag = error("Sass argument of type #{value_tag} unsupported") + break + end + end + + next error_tag if error_tag - if value - value = Script::String.new(Script::String.unquote(value.to_s), value.type) - value.to_native - else - Script::String.new("").to_native + begin + value = functions.send(custom_function, *custom_function_arguments) + + if value + value = Script::String.new(Script::String.unquote(value.to_s), value.type) + value.to_native + else + Script::String.new("").to_native + end + rescue StandardError => exception + error(exception.message) end end @@ -48,6 +68,21 @@ def setup(native_options) private + def error(message) + value = Native::SassValue.new + value[:unknown] = Native::SassUnknown.new + + error = Native::SassError.new + error[:tag] = :sass_error + + Native.error_set_message(error, Native.native_string(message)) + $stderr.puts "[SassC::FunctionsHandler] #{message}" + + value[:unknown][:tag] = :sass_error + value[:error] = error + value.pointer + end + class FunctionWrapper class << self attr_accessor :options diff --git a/lib/sassc/native.rb b/lib/sassc/native.rb index 469d1d99..dde78a86 100644 --- a/lib/sassc/native.rb +++ b/lib/sassc/native.rb @@ -48,7 +48,8 @@ def self.return_string_array(ptr) end def self.native_string(string) - string += "\0" + string = string.to_s + string << "\0" data = Native::LibC.malloc(string.bytesize) data.write_string(string) data diff --git a/lib/sassc/native/native_functions_api.rb b/lib/sassc/native/native_functions_api.rb index 8a7f5096..83678516 100644 --- a/lib/sassc/native/native_functions_api.rb +++ b/lib/sassc/native/native_functions_api.rb @@ -19,6 +19,7 @@ module Native # ADDAPI enum Sass_Tag ADDCALL sass_value_get_tag (const union Sass_Value* v); attach_function :sass_value_get_tag, [:sass_value_ptr], SassTag + attach_function :sass_value_is_null, [:sass_value_ptr], :bool # ADDAPI const char* ADDCALL sass_string_get_value (const union Sass_Value* v); attach_function :sass_string_get_value, [:sass_value_ptr], :string @@ -28,6 +29,11 @@ module Native attach_function :sass_list_get_length, [:sass_value_ptr], :size_t attach_function :sass_list_get_value, [:sass_value_ptr, :size_t], :sass_value_ptr + # ADDAPI char* ADDCALL sass_error_get_message (const union Sass_Value* v); + # ADDAPI void ADDCALL sass_error_set_message (union Sass_Value* v, char* msg); + attach_function :sass_error_get_message, [:sass_value_ptr], :string + attach_function :sass_error_set_message, [:sass_value_ptr, :pointer], :void + # Getters for custom function descriptors # ADDAPI const char* ADDCALL sass_function_get_signature (Sass_C_Function_Callback fn); # ADDAPI Sass_C_Function ADDCALL sass_function_get_function (Sass_C_Function_Callback fn); diff --git a/lib/sassc/native/sass_value.rb b/lib/sassc/native/sass_value.rb index f8950482..6da1003d 100644 --- a/lib/sassc/native/sass_value.rb +++ b/lib/sassc/native/sass_value.rb @@ -71,12 +71,12 @@ class SassNull < FFI::Struct class SassError < FFI::Struct layout :tag, SassTag, - :messsage, :string + :message, :string end class SassWarning < FFI::Struct layout :tag, SassTag, - :messsage, :string + :message, :string end class SassValue # < FFI::Union diff --git a/lib/sassc/script.rb b/lib/sassc/script.rb index f9b4f7d2..9704407f 100644 --- a/lib/sassc/script.rb +++ b/lib/sassc/script.rb @@ -8,10 +8,9 @@ def self.custom_functions def self.formatted_function_name(function_name) params = Functions.instance_method(function_name).parameters - params = params.select { |param| param[0] == :req } - .map(&:first) - .map { |p| "$#{p}" } + params = params.map { |param_type, name| "$#{name}#{': null' if param_type == :opt}" } .join(", ") + "#{function_name}(#{params})" end end diff --git a/lib/sassc/script/string.rb b/lib/sassc/script/string.rb index b41aa168..35050941 100644 --- a/lib/sassc/script/string.rb +++ b/lib/sassc/script/string.rb @@ -66,12 +66,12 @@ def self.type(contents) def self.unquote(contents) s = contents.dup - case contents[0,1] + case contents[0, 1] when "'", '"', '`' s[0] = '' end - case contents[-1,1] + case contents[-1, 1] when "'", '"', '`' s[-1] = '' end diff --git a/lib/tasks/libsass.rb b/lib/tasks/libsass.rb new file mode 100644 index 00000000..a659cd07 --- /dev/null +++ b/lib/tasks/libsass.rb @@ -0,0 +1,21 @@ +namespace :libsass do + desc "Compile libsass" + task compile: "ext/libsass/lib/libsass.so" + + file "ext/libsass/.git" do + sh "git submodule update --init" + end + + file "ext/libsass/lib/libsass.so" => "ext/libsass/.git" do + libsass_path = "" + if Dir.pwd.end_with?('/ext') + libsass_path = "libsass" + else + libsass_path = "ext/libsass" + end + + cd libsass_path do + sh 'make lib/libsass.so LDFLAGS="-Wall -O2"' + end + end +end diff --git a/sassc.gemspec b/sassc.gemspec index c7242bf0..0c31ea5f 100644 --- a/sassc.gemspec +++ b/sassc.gemspec @@ -16,9 +16,10 @@ Gem::Specification.new do |spec| spec.files = `git ls-files -z`.split("\x0") spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) - spec.require_paths = ["lib", "ext"] - spec.extensions = ["Rakefile"] + spec.require_paths = ["lib"] + + spec.extensions = ["ext/Rakefile"] spec.add_development_dependency "rake" spec.add_development_dependency "minitest", "~> 5.5.1" diff --git a/test/functions_test.rb b/test/functions_test.rb index 2ebd0b99..529239d5 100644 --- a/test/functions_test.rb +++ b/test/functions_test.rb @@ -1,34 +1,16 @@ require_relative "test_helper" +require "stringio" module SassC class FunctionsTest < MiniTest::Test include FixtureHelper - SassString = Struct.new(:value, :type) do - def to_s - value - end + def setup + @real_stderr, $stderr = $stderr, StringIO.new end - module Script::Functions - def javascript_path(path) - Script::String.new("/js/#{path.value}", :string) - end - - def no_return_path(path) - nil - end - - def sass_return_path(path) - return SassString.new("'#{path.value}'", :string) - end - - module Compass - def stylesheet_path(path) - Script::String.new("/css/#{path.value}", :identifier) - end - end - include Compass + def teardown + $stderr = @real_stderr end def test_functions_may_return_sass_string_type @@ -70,5 +52,89 @@ def test_function_with_no_return_value url: url(); } EOS end + + def test_function_with_optional_arguments + engine = Engine.new("div {url: optional_arguments('first'); url: optional_arguments('second', 'qux')}") + assert_equal <<-EOS, engine.render +div { + url: "first/bar"; + url: "second/qux"; } + EOS + end + + def test_function_with_unsupported_tag + engine = Engine.new("div {url: function_with_unsupported_tag(red);}") + + exception = assert_raises(SassC::SyntaxError) do + engine.render + end + + assert_equal "Error: error in C function function_with_unsupported_tag: Sass argument of type sass_color unsupported\n\n Backtrace:\n \tstdin:1, in function `function_with_unsupported_tag`\n \tstdin:1\n on line 1 of stdin\n>> div {url: function_with_unsupported_tag(red);}\n ----------^\n", exception.message + + assert_equal "[SassC::FunctionsHandler] Sass argument of type sass_color unsupported", stderr_output + end + + def test_function_with_error + engine = Engine.new("div {url: function_that_raises_errors();}") + exception = assert_raises(SassC::SyntaxError) do + engine.render + end + + assert_equal "Error: error in C function function_that_raises_errors: Intentional wrong thing happened somewhere inside the custom function + + Backtrace: + \tstdin:1, in function `function_that_raises_errors` + \tstdin:1 + on line 1 of stdin +>> div {url: function_that_raises_errors();} + ----------^ +", exception.message + + assert_equal "[SassC::FunctionsHandler] Intentional wrong thing happened somewhere inside the custom function", stderr_output + end + + private + + SassString = Struct.new(:value, :type) do + def to_s + value + end + end + + module Script::Functions + def javascript_path(path) + Script::String.new("/js/#{path.value}", :string) + end + + def no_return_path(path) + nil + end + + def sass_return_path(path) + return SassString.new("'#{path.value}'", :string) + end + + def optional_arguments(path, optional = "bar") + return SassString.new("#{path}/#{optional}", :string) + end + + def function_that_raises_errors() + raise StandardError, "Intentional wrong thing happened somewhere inside the custom function" + end + + def function_with_unsupported_tag(color) + end + + module Compass + def stylesheet_path(path) + Script::String.new("/css/#{path.value}", :identifier) + end + end + include Compass + end + + def stderr_output + $stderr.string.gsub("\u0000\n", '') + end end end