├── .gitignore ├── .gitmodules ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── ext ├── depend └── extconf.rb ├── lib ├── sassc.rb └── sassc │ ├── dependency.rb │ ├── engine.rb │ ├── error.rb │ ├── functions_handler.rb │ ├── import_handler.rb │ ├── importer.rb │ ├── native.rb │ ├── native │ ├── native_context_api.rb │ ├── native_functions_api.rb │ ├── sass2scss_api.rb │ ├── sass_input_style.rb │ ├── sass_output_style.rb │ ├── sass_value.rb │ └── string_list.rb │ ├── sass_2_scss.rb │ ├── script.rb │ ├── script │ ├── functions.rb │ ├── value.rb │ ├── value │ │ ├── bool.rb │ │ ├── color.rb │ │ ├── list.rb │ │ ├── map.rb │ │ ├── number.rb │ │ └── string.rb │ ├── value_conversion.rb │ └── value_conversion │ │ ├── base.rb │ │ ├── bool.rb │ │ ├── color.rb │ │ ├── list.rb │ │ ├── map.rb │ │ ├── number.rb │ │ └── string.rb │ ├── util.rb │ ├── util │ └── normalized_map.rb │ └── version.rb ├── sassc.gemspec └── test ├── custom_importer_test.rb ├── engine_test.rb ├── error_test.rb ├── fixtures └── paths.scss ├── functions_test.rb ├── native_test.rb ├── output_style_test.rb ├── sass_2_scss_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /.rvmrc 4 | /Gemfile.lock 5 | /_yardoc/ 6 | /coverage/ 7 | /doc/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | *.bundle 12 | *.so 13 | *.o 14 | *.a 15 | *.gem 16 | mkmf.log 17 | vendor/bundle 18 | /ext/Makefile 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/libsass"] 2 | path = ext/libsass 3 | url = https://github.com/sass/libsass.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | bundler_args: "--binstubs --standalone --without documentation --path ../bundle" 4 | script: "bundle exec rake test" 5 | rvm: 6 | - 2.0.0 7 | - 2.1.10 8 | - 2.2.10 9 | - 2.3.8 10 | - 2.4.6 11 | - 2.5.5 12 | - 2.6.3 13 | - 2.7.0 14 | - truffleruby 15 | notifications: 16 | email: false 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | - **2.4.0** 2 | - [Update libsass to 3.6.4](https://github.com/sass/sassc-ruby/pull/199) 3 | - [Use FFI::MemoryPointer instead of libc's malloc()](https://github.com/sass/sassc-ruby/pull/205) 4 | - [Test against Ruby 2.7.0](https://github.com/sass/sassc-ruby/pull/193) 5 | 6 | - **2.3.0** 7 | - [Fix rake gem:native task](https://github.com/sass/sassc-ruby/pull/196) 8 | - [disable lto flag for darwin + nix](https://github.com/sass/sassc-ruby/pull/166) 9 | - [Sort input file list](https://github.com/sass/sassc-ruby/pull/178) 10 | - [Set appropriate encoding for source_map](https://github.com/sass/sassc-ruby/pull/152) 11 | - [allow passing functions directly](https://github.com/sass/sassc-ruby/pull/162) 12 | - [always dispose data context](https://github.com/sass/sassc-ruby/pull/161) 13 | - [Update libsass to 3.6.3](https://github.com/sass/sassc-ruby/pull/164) 14 | - [Restore strip symbols](https://github.com/sass/sassc-ruby/pull/184) 15 | - [Default --march-tune-native to false](https://github.com/sass/sassc-ruby/pull/158) 16 | - [Fix compile issue on Mac OS X](https://github.com/sass/sassc-ruby/pull/174) 17 | - [Test on TruffleRuby in TravisCI](https://github.com/sass/sassc-ruby/pull/171) 18 | - [Use RbConfig::MAKEFILE_CONFIG['DLEXT'] instead of hardcoding extensions](https://github.com/sass/sassc-ruby/pull/173) 19 | - **2.2.1** 20 | - [Fix LoadError on some non-rvm environments](https://github.com/sass/sassc-ruby/pull/156) 21 | - **2.2.0** 22 | - [Do not build precompiled gems for Linux](https://github.com/sass/sassc-ruby/pull/145) 23 | - **2.1.0** 24 | - Equivalent to 2.1.0.pre3 25 | - **2.1.0.pre3** 26 | - [extconf.rb: Always write VERSION if we have .git](https://github.com/sass/sassc-ruby/pull/131) 27 | - [Update libsass to 3.6.1](https://github.com/sass/sassc-ruby/pull/130) 28 | - **2.1.0.pre2** 29 | - [Reduce Ruby warnings](https://github.com/sass/sassc-ruby/pull/124) 30 | - [prefer equal? to determine object identity](https://github.com/sass/sassc-ruby/pull/122) 31 | - [Link C/C++ stdlib statically for binary gems](https://github.com/sass/sassc-ruby/pull/127) 32 | - **2.1.0.pre1** 33 | - [Update Libsass to 3.6.0](https://github.com/sass/sassc-ruby/pull/96/files) 34 | - [Support old Ruby versions](https://github.com/sass/sassc-ruby/pull/117/files) 35 | - **2.0.1** 36 | - [Relax FFI dependency](https://github.com/sass/sassc-ruby/pull/102) 37 | - **2.0.0** 38 | - [Remove dependency on Ruby Sass](https://github.com/sass/sassc-ruby/pull/85) 39 | - [frozen_string_literal all files](https://github.com/sass/sassc-ruby/pull/85) 40 | - **1.12.1** 41 | - [Downgrade to libsass 3.5.2 to fix css imports](https://github.com/sass/sassc-ruby/pull/81) 42 | - **1.12.0** 43 | - [Update Libsass to 3.5.4](https://github.com/sass/sassc-ruby/pull/78) 44 | - [bundler is a development dependency](https://github.com/sass/sassc-ruby/pull/51) 45 | - **1.11.4** 46 | - Fix `Value::List` related issue with sass 3.5.0 47 | - **1.11.3** 48 | - [Require Sass::Deprecation module](https://github.com/sass/sassc-ruby/pull/68) 49 | - **1.11.2** 50 | - [Update to libsass 3.4.3](https://github.com/sass/sassc-ruby/pull/65) 51 | - **1.11.1** 52 | - [Update to libsass 3.4.1](https://github.com/sass/sassc-ruby/pull/61) 53 | - **1.11.0** 54 | - [Add support for lists in functions](https://github.com/sass/sassc-ruby/pull/55) 55 | - [Update to libsass 3.4.0](https://github.com/sass/sassc-ruby/pull/57) 56 | - **1.10.1** 57 | - [Add sourcemap getter](https://github.com/sass/sassc-ruby/pull/48) 58 | - **1.10.0** 59 | - [Improved error messages](https://github.com/sass/sassc-ruby/pull/34) 60 | - Update to Libsass 3.3.6 61 | - **1.9.0** 62 | - Support boolean script support 63 | - **1.8.5** 64 | - Update to Libsass 3.3.4 65 | - **1.8.4** 66 | - Update to Libsass 3.3.3 67 | - **1.8.3** 68 | - [Passing empty string into engine does not raise error](https://github.com/sass/sassc-ruby/pull/31) 69 | - **1.8.2** 70 | - Update to Libsass 3.3.2 71 | - **1.8.1** 72 | - Update to Libsass 3.3.1 73 | - **1.8.0** 74 | - Update to Libsass 3.3.0 75 | - **1.8.0.pre2** 76 | - Fix bug with looking up gem_path 77 | - **1.8.0.pre1** 78 | - [Update to Libsass 3.3.0-beta3](https://github.com/sass/sassc-ruby/pull/20) 79 | - **1.7.1** 80 | - Some updates to `Engine` API. 81 | - **1.7.0** 82 | - [Support Precision](https://github.com/sass/sassc-ruby/pull/19) 83 | - **1.6.0** 84 | - [Support Sass Color types](https://github.com/bolandrm/sassc-ruby/pull/14) 85 | - [Support quoted strings](https://github.com/bolandrm/sassc-ruby/pull/13) 86 | - [Improve custom function error handling](https://github.com/bolandrm/sassc-ruby/pull/15) 87 | - **1.5.1** 88 | - 2nd attempt at fixing compilation bug (issue [#12](https://github.com/bolandrm/sassc-ruby/issues/12)) 89 | - **1.5.0** 90 | - Add support for inline source maps 91 | - Fix compilation bug (issue [#12](https://github.com/bolandrm/sassc-ruby/issues/12)) 92 | - **1.4.0** 93 | - Add support for line number comments 94 | - **1.3.0** 95 | - Support Sass color custom function arguments 96 | - Adds error handling for exceptions in custom functions 97 | - Custom functions may have optional/default arguments 98 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Sass is more than a technology; Sass is driven by the community of individuals 2 | that power its development and use every day. As a community, we want to embrace 3 | the very differences that have made our collaboration so powerful, and work 4 | together to provide the best environment for learning, growing, and sharing of 5 | ideas. It is imperative that we keep Sass a fun, welcoming, challenging, and 6 | fair place to play. 7 | 8 | [The full community guidelines can be found on the Sass website.][link] 9 | 10 | [link]: https://sass-lang.com/community-guidelines 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Ryan Boland & Contributors 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## SassC Has Reached End-of-Life 2 | 3 | The `sassc` gem should no longer be used, and will no longer be receiving any updates. 4 | 5 | The [`sass-embedded`](https://rubygems.org/gems/sass-embedded) gem is the recommended way to move away from `sassc`. It bundles the [Dart Sass](https://sass-lang.com/dart-sass/) command-line executable, and uses the [Embedded Sass Protocol](https://github.com/sass/sass/blob/HEAD/spec/embedded-protocol.md) to provide a [Modern Ruby API](https://rubydoc.info/gems/sass-embedded/Sass) for compiling Sass and defining custom importers and functions. 6 | 7 | If you find it difficult migrating to the Modern Ruby API, the [`sassc-embedded`](https://rubygems.org/gems/sassc-embedded) gem is a drop-in replacement for the `sassc` gem. It provides the same [Legacy Ruby API](https://github.com/sass-contrib/sassc-embedded-shim-ruby/blob/HEAD/README.md#usage), but internally runs `sass-embedded` instead of `libsass`. 8 | 9 | You can also use the [`dartsass-rails`](https://rubygems.org/gems/dartsass-rails) gem, a basic command-line integration with the Dart Sass executable from the `sass-embedded` gem; or [`dartsass-sprockets`](https://rubygems.org/gems/dartsass-sprockets) gem, an advanced sprockets integration with the Legacy Ruby API from the `sassc-embedded` gem, to plug smoothly into Ruby on Rails. 10 | 11 | Alternately, you can explore using a JS build system with Dart Sass as a [JavaScript library](https://sass-lang.com/dart-sass/#java-script-library). 12 | 13 | # SassC [![Build Status](https://travis-ci.org/sass/sassc-ruby.svg?branch=master)](https://travis-ci.org/sass/sassc-ruby) [![Gem Version](https://badge.fury.io/rb/sassc.svg)](http://badge.fury.io/rb/sassc) 14 | 15 | Use `libsass` with Ruby! 16 | 17 | This gem combines the speed of `libsass`, the [Sass C implementation](https://github.com/sass/libsass), with the ease of use of the original [Ruby Sass](https://github.com/sass/ruby-sass) library. 18 | 19 | ### libsass Version 20 | 21 | [3.6.1](https://github.com/sass/libsass/releases/3.6.1) 22 | 23 | ## Installation 24 | 25 | Add this line to your application's Gemfile: 26 | 27 | ```ruby 28 | gem 'sassc' 29 | ``` 30 | 31 | And then execute: 32 | 33 | ```bash 34 | bundle 35 | ``` 36 | 37 | Or install it yourself as: 38 | 39 | ```bash 40 | gem install sassc 41 | ``` 42 | 43 | ## Usage 44 | 45 | This library utilizes `libsass` to allow you to compile SCSS or SASS syntax 46 | to CSS. To compile, use a `SassC::Engine`, e.g.: 47 | 48 | ```ruby 49 | SassC::Engine.new(sass, style: :compressed).render 50 | ``` 51 | 52 | **Note**: If you want to use this library with Rails/Sprockets, check out 53 | [sassc-rails](https://github.com/bolandrm/sassc-rails). 54 | 55 | Additionally, you can use `SassC::Sass2Scss` to convert Sass syntax to Scss syntax. 56 | 57 | ## Credits 58 | 59 | This gem is maintained by [Ryan Boland](https://ryanboland.com) 60 | and [awesome contributors](https://github.com/bolandrm/sassc-ruby/graphs/contributors). 61 | 62 | ## Changelog 63 | 64 | See [CHANGELOG.md](CHANGELOG.md). 65 | 66 | ## Contributing 67 | 68 | ### Project Setup 69 | 70 | 1. Clone repo 71 | 1. Install dependencies - `bundle install` 72 | 1. Run the tests - `bundle exec rake test` 73 | 74 | ### Code Changes 75 | 76 | 1. Fork it ( https://github.com/sass/sassc-ruby/fork ) 77 | 1. Create your feature branch (`git checkout -b my-new-feature`) 78 | 1. Commit your changes (`git commit -am 'Add some feature'`) - try to include tests 79 | 1. Push to the branch (`git push origin my-new-feature`) 80 | 1. Create a new Pull Request 81 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | task default: :test 4 | 5 | require 'rake/extensiontask' 6 | gem_spec = Gem::Specification.load("sassc.gemspec") 7 | 8 | # HACK: Prevent rake-compiler from overriding required_ruby_version, 9 | # because the shared library here is Ruby-agnostic. 10 | # See https://github.com/rake-compiler/rake-compiler/issues/153 11 | module FixRequiredRubyVersion 12 | def required_ruby_version=(*); end 13 | end 14 | Gem::Specification.send(:prepend, FixRequiredRubyVersion) 15 | 16 | Rake::ExtensionTask.new('libsass', gem_spec) do |ext| 17 | ext.name = 'libsass' 18 | ext.ext_dir = 'ext' 19 | ext.lib_dir = 'lib/sassc' 20 | ext.cross_compile = true 21 | ext.cross_platform = %w[x86-mingw32 x64-mingw32] 22 | 23 | # Link C++ stdlib statically when building binary gems. 24 | ext.cross_config_options << '--enable-static-stdlib' 25 | 26 | ext.cross_config_options << '--disable-march-tune-native' 27 | 28 | ext.cross_compiling do |spec| 29 | spec.files.reject! { |path| File.fnmatch?('ext/*', path) } 30 | end 31 | end 32 | 33 | desc 'Compile all native gems via rake-compiler-dock (Docker)' 34 | task 'gem:native' do 35 | require 'rake_compiler_dock' 36 | 37 | # The RUBY_CC_VERSION here doesn't matter for the final package. 38 | # Only one version should be specified, as the shared library is Ruby-agnostic. 39 | RakeCompilerDock.sh "gem i rake bundler --no-document && bundle && "\ 40 | "rake clean && rake cross native gem MAKE='nice make -j`nproc`' "\ 41 | "RUBY_CC_VERSION=2.6.0 CLEAN=1" 42 | end 43 | 44 | CLEAN.include 'tmp', 'pkg', 'lib/sassc/libsass.{so,bundle}', 'ext/libsass/VERSION', 45 | 'ext/*.{o,so,bundle}', 'ext/Makefile' 46 | 47 | desc "Run all tests" 48 | task test: 'compile:libsass' do 49 | $LOAD_PATH.unshift('lib', 'test') 50 | Dir.glob('./test/**/*_test.rb') { |f| require f } 51 | end 52 | -------------------------------------------------------------------------------- /ext/depend: -------------------------------------------------------------------------------- 1 | # Replaces default mkmf dependencies. Default mkmf dependencies include all libruby headers. 2 | # We don't need libruby and some of these headers are missing on JRuby (breaking compilation there). 3 | $(OBJS): $(HDRS) 4 | 5 | -------------------------------------------------------------------------------- /ext/extconf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | gem_root = File.expand_path('..', __dir__) 4 | libsass_dir = File.join(gem_root, 'ext', 'libsass') 5 | 6 | if !File.directory?(libsass_dir) || 7 | # '.', '..', and possibly '.git' from a failed checkout: 8 | Dir.entries(libsass_dir).size <= 3 9 | Dir.chdir(gem_root) { system('git submodule update --init') } or 10 | fail 'Could not fetch libsass' 11 | end 12 | 13 | require 'mkmf' 14 | 15 | $CXXFLAGS << ' -std=c++11' 16 | 17 | # Set to true when building binary gems 18 | if enable_config('static-stdlib', false) 19 | $LDFLAGS << ' -static-libgcc -static-libstdc++' 20 | end 21 | 22 | if enable_config('march-tune-native', false) 23 | $CFLAGS << ' -march=native -mtune=native' 24 | $CXXFLAGS << ' -march=native -mtune=native' 25 | end 26 | 27 | # darwin nix clang doesn't support lto 28 | # disable -lto flag for darwin + nix 29 | # see: https://github.com/sass/sassc-ruby/issues/148 30 | enable_lto_by_default = (Gem::Platform.local.os == "darwin" && ENV['NIX_CC'].nil?) 31 | 32 | if enable_config('lto', enable_lto_by_default) 33 | $CFLAGS << ' -flto' 34 | $CXXFLAGS << ' -flto' 35 | $LDFLAGS << ' -flto' 36 | end 37 | 38 | # Disable noisy compilation warnings. 39 | $warnflags = '' 40 | $CFLAGS.gsub!(/[\s+](-ansi|-std=[^\s]+)/, '') 41 | 42 | dir_config 'libsass' 43 | 44 | libsass_version = Dir.chdir(libsass_dir) do 45 | if File.exist?('.git') 46 | ver = %x[git describe --abbrev=4 --dirty --always --tags].chomp 47 | File.write('VERSION', ver) 48 | ver 49 | end 50 | File.read('VERSION').chomp if File.exist?('VERSION') 51 | end 52 | 53 | if libsass_version 54 | libsass_version_def = %Q{ -DLIBSASS_VERSION='"#{libsass_version}"'} 55 | $CFLAGS << libsass_version_def 56 | $CXXFLAGS << libsass_version_def 57 | end 58 | 59 | $INCFLAGS << " -I$(srcdir)/libsass/include" 60 | $VPATH << "$(srcdir)/libsass/src" 61 | Dir.chdir(__dir__) do 62 | $VPATH += Dir['libsass/src/*/'].map { |p| "$(srcdir)/#{p}" } 63 | $srcs = Dir['libsass/src/**/*.{c,cpp}'].sort 64 | end 65 | 66 | # libsass.bundle malformed object (unknown load command 7) on Mac OS X 67 | # See https://github.com/sass/sassc-ruby/pull/174 68 | if enable_config('strip', RbConfig::CONFIG['host_os'].downcase !~ /darwin/) 69 | MakeMakefile::LINK_SO << "\nstrip -x $@" 70 | end 71 | 72 | # Don't link libruby. 73 | $LIBRUBYARG = nil 74 | 75 | # Disable .def file generation for mingw, as it defines an 76 | # `Init_libsass` export which we don't have. 77 | MakeMakefile.send(:remove_const, :EXPORT_PREFIX) 78 | MakeMakefile::EXPORT_PREFIX = nil 79 | 80 | if RUBY_ENGINE == 'jruby' && 81 | Gem::Version.new(RUBY_ENGINE_VERSION) < Gem::Version.new('9.2.8.0') 82 | # COUTFLAG is not set correctly on jruby<9.2.8.0 83 | # See https://github.com/jruby/jruby/issues/5749 84 | MakeMakefile.send(:remove_const, :COUTFLAG) 85 | MakeMakefile::COUTFLAG = '-o $(empty)' 86 | 87 | # CCDLFLAGS is not set correctly on jruby<9.2.8.0 88 | # See https://github.com/jruby/jruby/issues/5751 89 | $CXXFLAGS << ' -fPIC' 90 | end 91 | 92 | create_makefile 'sassc/libsass' 93 | -------------------------------------------------------------------------------- /lib/sassc.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | # The global load paths for Sass files. This is meant for plugins and 5 | # libraries to register the paths to their Sass stylesheets to that they may 6 | # be `@imported`. This load path is used by every instance of {Sass::Engine}. 7 | # They are lower-precedence than any load paths passed in via the 8 | # {file:SASS_REFERENCE.md#load_paths-option `:load_paths` option}. 9 | # 10 | # If the `SASS_PATH` environment variable is set, 11 | # the initial value of `load_paths` will be initialized based on that. 12 | # The variable should be a colon-separated list of path names 13 | # (semicolon-separated on Windows). 14 | # 15 | # Note that files on the global load path are never compiled to CSS 16 | # themselves, even if they aren't partials. They exist only to be imported. 17 | # 18 | # @example 19 | # SassC.load_paths << File.dirname(__FILE__ + '/sass') 20 | # @return [Array] 21 | def self.load_paths 22 | @load_paths ||= if ENV['SASS_PATH'] 23 | ENV['SASS_PATH'].split(SassC::Util.windows? ? ';' : ':') 24 | else 25 | [] 26 | end 27 | end 28 | end 29 | 30 | require_relative "sassc/version" 31 | require_relative "sassc/native" 32 | require_relative "sassc/import_handler" 33 | require_relative "sassc/importer" 34 | require_relative "sassc/util" 35 | require_relative "sassc/util/normalized_map" 36 | require_relative "sassc/script" 37 | require_relative "sassc/script/value" 38 | require_relative "sassc/script/value/bool" 39 | require_relative "sassc/script/value/number" 40 | require_relative "sassc/script/value/color" 41 | require_relative "sassc/script/value/string" 42 | require_relative "sassc/script/value/list" 43 | require_relative "sassc/script/value/map" 44 | require_relative "sassc/script/functions" 45 | require_relative "sassc/script/value_conversion" 46 | require_relative "sassc/script/value_conversion/base" 47 | require_relative "sassc/script/value_conversion/string" 48 | require_relative "sassc/script/value_conversion/number" 49 | require_relative "sassc/script/value_conversion/color" 50 | require_relative "sassc/script/value_conversion/map" 51 | require_relative "sassc/script/value_conversion/list" 52 | require_relative "sassc/script/value_conversion/bool" 53 | require_relative "sassc/functions_handler" 54 | require_relative "sassc/dependency" 55 | require_relative "sassc/error" 56 | require_relative "sassc/engine" 57 | require_relative "sassc/sass_2_scss" 58 | -------------------------------------------------------------------------------- /lib/sassc/dependency.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | class Dependency 5 | attr_reader :filename 6 | attr_reader :options 7 | 8 | def initialize(filename) 9 | @filename = filename 10 | @options = { filename: @filename } 11 | end 12 | 13 | def self.from_filenames(filenames) 14 | filenames.map { |f| new(f) } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/sassc/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "error" 4 | 5 | module SassC 6 | class Engine 7 | OUTPUT_STYLES = %i[ 8 | sass_style_nested 9 | sass_style_expanded 10 | sass_style_compact 11 | sass_style_compressed 12 | ] 13 | 14 | attr_reader :template, :options 15 | 16 | def initialize(template, options = {}) 17 | @template = template 18 | @options = options 19 | @functions = options.fetch(:functions, Script::Functions) 20 | end 21 | 22 | def render 23 | return @template.dup if @template.empty? 24 | 25 | data_context = Native.make_data_context(@template) 26 | context = Native.data_context_get_context(data_context) 27 | native_options = Native.context_get_options(context) 28 | 29 | Native.option_set_is_indented_syntax_src(native_options, true) if sass? 30 | Native.option_set_input_path(native_options, filename) if filename 31 | Native.option_set_precision(native_options, precision) if precision 32 | Native.option_set_include_path(native_options, load_paths) 33 | Native.option_set_output_style(native_options, output_style_enum) 34 | Native.option_set_source_comments(native_options, true) if line_comments? 35 | Native.option_set_source_map_file(native_options, source_map_file) if source_map_file 36 | Native.option_set_source_map_embed(native_options, true) if source_map_embed? 37 | Native.option_set_source_map_contents(native_options, true) if source_map_contents? 38 | Native.option_set_omit_source_map_url(native_options, true) if omit_source_map_url? 39 | 40 | import_handler.setup(native_options) 41 | functions_handler.setup(native_options, functions: @functions) 42 | 43 | status = Native.compile_data_context(data_context) 44 | 45 | if status != 0 46 | message = Native.context_get_error_message(context) 47 | filename = Native.context_get_error_file(context) 48 | line = Native.context_get_error_line(context) 49 | 50 | raise SyntaxError.new(message, filename: filename, line: line) 51 | end 52 | 53 | css = Native.context_get_output_string(context) 54 | 55 | @dependencies = Native.context_get_included_files(context) 56 | @source_map = Native.context_get_source_map_string(context) 57 | 58 | css.force_encoding(@template.encoding) 59 | @source_map.force_encoding(@template.encoding) if @source_map.is_a?(String) 60 | 61 | return css unless quiet? 62 | ensure 63 | Native.delete_data_context(data_context) if data_context 64 | end 65 | 66 | def dependencies 67 | raise NotRenderedError unless @dependencies 68 | Dependency.from_filenames(@dependencies) 69 | end 70 | 71 | def source_map 72 | raise NotRenderedError unless @source_map 73 | @source_map 74 | end 75 | 76 | def filename 77 | @options[:filename] 78 | end 79 | 80 | private 81 | 82 | def quiet? 83 | @options[:quiet] 84 | end 85 | 86 | def precision 87 | @options[:precision] 88 | end 89 | 90 | def sass? 91 | @options[:syntax] && @options[:syntax].to_sym == :sass 92 | end 93 | 94 | def line_comments? 95 | @options[:line_comments] 96 | end 97 | 98 | def source_map_embed? 99 | @options[:source_map_embed] 100 | end 101 | 102 | def source_map_contents? 103 | @options[:source_map_contents] 104 | end 105 | 106 | def omit_source_map_url? 107 | @options[:omit_source_map_url] 108 | end 109 | 110 | def source_map_file 111 | @options[:source_map_file] 112 | end 113 | 114 | def import_handler 115 | @import_handler ||= ImportHandler.new(@options) 116 | end 117 | 118 | def functions_handler 119 | @functions_handler = FunctionsHandler.new(@options) 120 | end 121 | 122 | def output_style_enum 123 | @output_style_enum ||= Native::SassOutputStyle[output_style] 124 | end 125 | 126 | def output_style 127 | @output_style ||= begin 128 | style = @options.fetch(:style, :sass_style_nested).to_s 129 | style = "sass_style_#{style}" unless style.include?("sass_style_") 130 | style = style.to_sym 131 | raise InvalidStyleError unless Native::SassOutputStyle.symbols.include?(style) 132 | style 133 | end 134 | end 135 | 136 | def load_paths 137 | paths = (@options[:load_paths] || []) + SassC.load_paths 138 | paths.join(File::PATH_SEPARATOR) unless paths.empty? 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /lib/sassc/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "pathname" 4 | 5 | module SassC 6 | 7 | class BaseError < StandardError; end 8 | class NotRenderedError < BaseError; end 9 | class InvalidStyleError < BaseError; end 10 | class UnsupportedValue < BaseError; end 11 | 12 | # When dealing with SyntaxErrors, 13 | # it's important to provide filename and line number information. 14 | # This will be used in various error reports to users, including backtraces. 15 | 16 | class SyntaxError < BaseError 17 | 18 | def initialize(message, filename: nil, line: nil) 19 | @filename = filename 20 | @line = line 21 | super(message) 22 | end 23 | 24 | def backtrace 25 | return nil if super.nil? 26 | sass_backtrace + super 27 | end 28 | 29 | # The backtrace of the error within Sass files. 30 | def sass_backtrace 31 | return [] unless @filename && @line 32 | ["#{@filename}:#{@line}"] 33 | end 34 | 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /lib/sassc/functions_handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | class FunctionsHandler 5 | def initialize(options) 6 | @options = options 7 | end 8 | 9 | def setup(native_options, functions: Script::Functions) 10 | @callbacks = {} 11 | @function_names = {} 12 | 13 | list = Native.make_function_list(Script.custom_functions(functions: functions).count) 14 | 15 | # use an anonymous class wrapper to avoid mutations in a threaded environment 16 | functions_wrapper = Class.new do 17 | attr_accessor :options 18 | include functions 19 | end.new 20 | functions_wrapper.options = @options 21 | 22 | Script.custom_functions(functions: functions).each_with_index do |custom_function, i| 23 | @callbacks[custom_function] = FFI::Function.new(:pointer, [:pointer, :pointer]) do |native_argument_list, cookie| 24 | begin 25 | function_arguments = arguments_from_native_list(native_argument_list) 26 | result = functions_wrapper.send(custom_function, *function_arguments) 27 | to_native_value(result) 28 | rescue StandardError => exception 29 | # This rescues any exceptions that occur either in value conversion 30 | # or during the execution of a custom function. 31 | error(exception.message) 32 | end 33 | end 34 | 35 | @function_names[custom_function] = Script.formatted_function_name(custom_function, functions: functions) 36 | 37 | callback = Native.make_function( 38 | @function_names[custom_function], 39 | @callbacks[custom_function], 40 | nil 41 | ) 42 | 43 | Native::function_set_list_entry(list, i, callback) 44 | end 45 | 46 | Native::option_set_c_functions(native_options, list) 47 | end 48 | 49 | private 50 | 51 | def arguments_from_native_list(native_argument_list) 52 | native_argument_list_length = Native.list_get_length(native_argument_list) 53 | 54 | (0...native_argument_list_length).map do |i| 55 | native_value = Native.list_get_value(native_argument_list, i) 56 | Script::ValueConversion.from_native(native_value, @options) 57 | end.compact 58 | end 59 | 60 | def to_native_value(sass_value) 61 | # if the custom function returns nil, we provide a "default" return 62 | # value of an empty string 63 | sass_value ||= SassC::Script::Value::String.new("") 64 | sass_value.options = @options 65 | Script::ValueConversion.to_native(sass_value) 66 | end 67 | 68 | def error(message) 69 | $stderr.puts "[SassC::FunctionsHandler] #{message}" 70 | Native.make_error(message) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/sassc/import_handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | class ImportHandler 5 | def initialize(options) 6 | @importer = if options[:importer] 7 | options[:importer].new(options) 8 | else 9 | nil 10 | end 11 | end 12 | 13 | def setup(native_options) 14 | return unless @importer 15 | 16 | importer_callback = Native.make_importer(import_function, nil) 17 | 18 | list = Native.make_function_list(1) 19 | Native::function_set_list_entry(list, 0, importer_callback) 20 | 21 | Native.option_set_c_importers(native_options, list) 22 | end 23 | 24 | private 25 | 26 | def import_function 27 | @import_function ||= FFI::Function.new(:pointer, [:string, :pointer, :pointer]) do |path, importer_entry, compiler| 28 | last_import = Native::compiler_get_last_import(compiler) 29 | parent_path = Native::import_get_abs_path(last_import) 30 | 31 | imports = [*@importer.imports(path, parent_path)] 32 | imports_to_native(imports) 33 | end 34 | end 35 | 36 | def imports_to_native(imports) 37 | import_list = Native.make_import_list(imports.size) 38 | 39 | imports.each_with_index do |import, i| 40 | source = import.source ? Native.native_string(import.source) : nil 41 | source_map_path = nil 42 | 43 | entry = Native.make_import_entry(import.path, source, source_map_path) 44 | Native.import_set_list_entry(import_list, i, entry) 45 | end 46 | 47 | import_list 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/sassc/importer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | class Importer 5 | attr_reader :options 6 | 7 | def initialize(options) 8 | @options = options 9 | end 10 | 11 | def imports(path, parent_path) 12 | # A custom importer must override this method. 13 | # Custom importer may return an Import, or an array of Imports. 14 | raise NotImplementedError 15 | end 16 | 17 | class Import 18 | attr_accessor :path, :source, :source_map_path 19 | 20 | def initialize(path, source: nil, source_map_path: nil) 21 | @path = path 22 | @source = source 23 | @source_map_path = source_map_path 24 | end 25 | 26 | def to_s 27 | "Import: #{path} #{source} #{source_map_path}" 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/sassc/native.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "ffi" 4 | 5 | module SassC 6 | module Native 7 | extend FFI::Library 8 | 9 | dl_ext = RbConfig::MAKEFILE_CONFIG['DLEXT'] 10 | begin 11 | ffi_lib File.expand_path("libsass.#{dl_ext}", __dir__) 12 | rescue LoadError # Some non-rvm environments don't copy a shared object over to lib/sassc 13 | ffi_lib File.expand_path("libsass.#{dl_ext}", "#{__dir__}/../../ext") 14 | end 15 | 16 | require_relative "native/sass_value" 17 | 18 | typedef :pointer, :sass_options_ptr 19 | typedef :pointer, :sass_context_ptr 20 | typedef :pointer, :sass_file_context_ptr 21 | typedef :pointer, :sass_data_context_ptr 22 | 23 | typedef :pointer, :sass_c_function_list_ptr 24 | typedef :pointer, :sass_c_function_callback_ptr 25 | typedef :pointer, :sass_value_ptr 26 | 27 | typedef :pointer, :sass_import_list_ptr 28 | typedef :pointer, :sass_importer 29 | typedef :pointer, :sass_import_ptr 30 | 31 | callback :sass_c_function, [:pointer, :pointer], :pointer 32 | callback :sass_c_import_function, [:pointer, :pointer, :pointer], :pointer 33 | 34 | require_relative "native/sass_input_style" 35 | require_relative "native/sass_output_style" 36 | require_relative "native/string_list" 37 | 38 | # Remove the redundant "sass_" from the beginning of every method name 39 | def self.attach_function(*args) 40 | return super if args.size != 3 41 | 42 | if args[0] =~ /^sass_/ 43 | args.unshift args[0].to_s.sub(/^sass_/, "") 44 | end 45 | 46 | super(*args) 47 | end 48 | 49 | # https://github.com/ffi/ffi/wiki/Examples#array-of-strings 50 | def self.return_string_array(ptr) 51 | ptr.null? ? [] : ptr.get_array_of_string(0).compact 52 | end 53 | 54 | def self.native_string(string) 55 | m = FFI::MemoryPointer.from_string(string) 56 | m.autorelease = false 57 | m 58 | end 59 | 60 | require_relative "native/native_context_api" 61 | require_relative "native/native_functions_api" 62 | require_relative "native/sass2scss_api" 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/sassc/native/native_context_api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Native 5 | attach_function :version, :libsass_version, [], :string 6 | 7 | # Create and initialize an option struct 8 | # ADDAPI struct Sass_Options* ADDCALL sass_make_options (void); 9 | attach_function :sass_make_options, [], :sass_options_ptr 10 | 11 | # Create and initialize a specific context 12 | # ADDAPI struct Sass_File_Context* ADDCALL sass_make_file_context (const char* input_path); 13 | # ADDAPI struct Sass_Data_Context* ADDCALL sass_make_data_context (char* source_string); 14 | attach_function :sass_make_file_context, [:string], :sass_file_context_ptr 15 | attach_function :_make_data_context, :sass_make_data_context, [:pointer], :sass_data_context_ptr 16 | 17 | def self.make_data_context(data) 18 | _make_data_context(Native.native_string(data)) 19 | end 20 | 21 | # Call the compilation step for the specific context 22 | # ADDAPI int ADDCALL sass_compile_file_context (struct Sass_File_Context* ctx); 23 | # ADDAPI int ADDCALL sass_compile_data_context (struct Sass_Data_Context* ctx); 24 | attach_function :sass_compile_file_context, [:sass_file_context_ptr], :int 25 | attach_function :sass_compile_data_context, [:sass_data_context_ptr], :int 26 | 27 | # Create a sass compiler instance for more control 28 | # ADDAPI struct Sass_Compiler* ADDCALL sass_make_file_compiler (struct Sass_File_Context* file_ctx); 29 | # ADDAPI struct Sass_Compiler* ADDCALL sass_make_data_compiler (struct Sass_Data_Context* data_ctx); 30 | 31 | # Execute the different compilation steps individually 32 | # Usefull if you only want to query the included files 33 | # ADDAPI int ADDCALL sass_compiler_parse(struct Sass_Compiler* compiler); 34 | # ADDAPI int ADDCALL sass_compiler_execute(struct Sass_Compiler* compiler); 35 | 36 | # Release all memory allocated with the compiler 37 | # This does _not_ include any contexts or options 38 | # ADDAPI void ADDCALL sass_delete_compiler(struct Sass_Compiler* compiler); 39 | 40 | # Release all memory allocated and also ourself 41 | # ADDAPI void ADDCALL sass_delete_file_context (struct Sass_File_Context* ctx); 42 | # ADDAPI void ADDCALL sass_delete_data_context (struct Sass_Data_Context* ctx); 43 | attach_function :sass_delete_file_context, [:sass_file_context_ptr], :void 44 | attach_function :sass_delete_data_context, [:sass_data_context_ptr], :void 45 | 46 | # Getters for context from specific implementation 47 | # ADDAPI struct Sass_Context* ADDCALL sass_file_context_get_context (struct Sass_File_Context* file_ctx); 48 | # ADDAPI struct Sass_Context* ADDCALL sass_data_context_get_context (struct Sass_Data_Context* data_ctx); 49 | attach_function :sass_file_context_get_context, [:sass_file_context_ptr], :sass_context_ptr 50 | attach_function :sass_data_context_get_context, [:sass_data_context_ptr], :sass_context_ptr 51 | 52 | # Getters for context options from Sass_Context 53 | # ADDAPI struct Sass_Options* ADDCALL sass_context_get_options (struct Sass_Context* ctx); 54 | # ADDAPI struct Sass_Options* ADDCALL sass_file_context_get_options (struct Sass_File_Context* file_ctx); 55 | # ADDAPI struct Sass_Options* ADDCALL sass_data_context_get_options (struct Sass_Data_Context* data_ctx); 56 | # ADDAPI void ADDCALL sass_file_context_set_options (struct Sass_File_Context* file_ctx, struct Sass_Options* opt); 57 | # ADDAPI void ADDCALL sass_data_context_set_options (struct Sass_Data_Context* data_ctx, struct Sass_Options* opt); 58 | attach_function :sass_context_get_options, [:sass_context_ptr], :sass_options_ptr 59 | attach_function :sass_file_context_get_options, [:sass_file_context_ptr], :sass_options_ptr 60 | attach_function :sass_data_context_get_options, [:sass_data_context_ptr], :sass_options_ptr 61 | attach_function :sass_file_context_set_options, [:sass_file_context_ptr, :sass_options_ptr], :void 62 | attach_function :sass_data_context_set_options, [:sass_data_context_ptr, :sass_options_ptr], :void 63 | 64 | # Getters for options 65 | # ADDAPI int ADDCALL sass_option_get_precision (struct Sass_Options* options); 66 | # ADDAPI enum Sass_Output_Style ADDCALL sass_option_get_output_style (struct Sass_Options* options); 67 | # ADDAPI bool ADDCALL sass_option_get_source_comments (struct Sass_Options* options); 68 | # ADDAPI bool ADDCALL sass_option_get_source_map_embed (struct Sass_Options* options); 69 | # ADDAPI bool ADDCALL sass_option_get_source_map_contents (struct Sass_Options* options); 70 | # ADDAPI bool ADDCALL sass_option_get_omit_source_map_url (struct Sass_Options* options); 71 | # ADDAPI bool ADDCALL sass_option_get_is_indented_syntax_src (struct Sass_Options* options); 72 | # ADDAPI const char* ADDCALL sass_option_get_input_path (struct Sass_Options* options); 73 | # ADDAPI const char* ADDCALL sass_option_get_output_path (struct Sass_Options* options); 74 | # ADDAPI const char* ADDCALL sass_option_get_include_path (struct Sass_Options* options); 75 | # ADDAPI const char* ADDCALL sass_option_get_source_map_file (struct Sass_Options* options); 76 | # ADDAPI Sass_C_Function_List ADDCALL sass_option_get_c_functions (struct Sass_Options* options); 77 | attach_function :sass_option_get_precision, [:sass_options_ptr], :int 78 | attach_function :sass_option_get_output_style, [:sass_options_ptr], SassOutputStyle 79 | attach_function :sass_option_get_source_comments, [:sass_options_ptr], :bool 80 | attach_function :sass_option_get_source_map_embed, [:sass_options_ptr], :bool 81 | attach_function :sass_option_get_source_map_contents, [:sass_options_ptr], :bool 82 | attach_function :sass_option_get_omit_source_map_url, [:sass_options_ptr], :bool 83 | attach_function :sass_option_get_is_indented_syntax_src, [:sass_options_ptr], :bool 84 | attach_function :sass_option_get_input_path, [:sass_options_ptr], :string 85 | attach_function :sass_option_get_output_path, [:sass_options_ptr], :string 86 | attach_function :sass_option_get_include_path, [:sass_options_ptr], :string 87 | attach_function :sass_option_get_source_map_file, [:sass_options_ptr], :string 88 | attach_function :sass_option_get_c_functions, [:sass_options_ptr], :sass_c_function_list_ptr 89 | # ADDAPI Sass_C_Import_Callback ADDCALL sass_option_get_importer (struct Sass_Options* options); 90 | 91 | # Setters for options 92 | # ADDAPI void ADDCALL sass_option_set_precision (struct Sass_Options* options, int precision); 93 | # ADDAPI void ADDCALL sass_option_set_output_style (struct Sass_Options* options, enum Sass_Output_Style output_style); 94 | # ADDAPI void ADDCALL sass_option_set_source_comments (struct Sass_Options* options, bool source_comments); 95 | # ADDAPI void ADDCALL sass_option_set_source_map_embed (struct Sass_Options* options, bool source_map_embed); 96 | # ADDAPI void ADDCALL sass_option_set_source_map_contents (struct Sass_Options* options, bool source_map_contents); 97 | # ADDAPI void ADDCALL sass_option_set_omit_source_map_url (struct Sass_Options* options, bool omit_source_map_url); 98 | # ADDAPI void ADDCALL sass_option_set_is_indented_syntax_src (struct Sass_Options* options, bool is_indented_syntax_src); 99 | # ADDAPI void ADDCALL sass_option_set_input_path (struct Sass_Options* options, const char* input_path); 100 | # ADDAPI void ADDCALL sass_option_set_output_path (struct Sass_Options* options, const char* output_path); 101 | # ADDAPI void ADDCALL sass_option_set_include_path (struct Sass_Options* options, const char* include_path); 102 | # ADDAPI void ADDCALL sass_option_set_source_map_file (struct Sass_Options* options, const char* source_map_file); 103 | # ADDAPI void ADDCALL sass_option_set_c_functions (struct Sass_Options* options, Sass_C_Function_List c_functions); 104 | # ADDAPI void ADDCALL sass_option_set_c_importers (struct Sass_Options* options, Sass_Importer_List c_importers); 105 | attach_function :sass_option_set_precision, [:sass_options_ptr, :int], :void 106 | attach_function :sass_option_set_output_style, [:sass_options_ptr, SassOutputStyle], :void 107 | attach_function :sass_option_set_source_comments, [:sass_options_ptr, :bool], :void 108 | attach_function :sass_option_set_source_map_embed, [:sass_options_ptr, :bool], :void 109 | attach_function :sass_option_set_source_map_contents, [:sass_options_ptr, :bool], :void 110 | attach_function :sass_option_set_omit_source_map_url, [:sass_options_ptr, :bool], :void 111 | attach_function :sass_option_set_is_indented_syntax_src, [:sass_options_ptr, :bool], :void 112 | attach_function :sass_option_set_input_path, [:sass_options_ptr, :string], :void 113 | attach_function :sass_option_set_output_path, [:sass_options_ptr, :string], :void 114 | attach_function :sass_option_set_include_path, [:sass_options_ptr, :string], :void 115 | attach_function :sass_option_set_source_map_file, [:sass_options_ptr, :string], :void 116 | attach_function :sass_option_set_c_functions, [:sass_options_ptr, :pointer], :void 117 | attach_function :sass_option_set_c_importers, [:sass_options_ptr, :pointer], :void 118 | #attach_function :sass_option_set_c_importers, [:sass_options_ptr, :sass_importer], :void 119 | 120 | # Getter for context 121 | # ADDAPI const char* ADDCALL sass_context_get_output_string (struct Sass_Context* ctx); 122 | # ADDAPI int ADDCALL sass_context_get_error_status (struct Sass_Context* ctx); 123 | # ADDAPI const char* ADDCALL sass_context_get_error_json (struct Sass_Context* ctx); 124 | # ADDAPI const char* ADDCALL sass_context_get_error_message (struct Sass_Context* ctx); 125 | # ADDAPI const char* ADDCALL sass_context_get_error_file (struct Sass_Context* ctx); 126 | # ADDAPI size_t ADDCALL sass_context_get_error_line (struct Sass_Context* ctx); 127 | # ADDAPI size_t ADDCALL sass_context_get_error_column (struct Sass_Context* ctx); 128 | # ADDAPI const char* ADDCALL sass_context_get_source_map_string (struct Sass_Context* ctx); 129 | # ADDAPI char** ADDCALL sass_context_get_included_files (struct Sass_Context* ctx); 130 | attach_function :sass_context_get_output_string, [:sass_context_ptr], :string 131 | attach_function :sass_context_get_error_status, [:sass_context_ptr], :int 132 | attach_function :sass_context_get_error_json, [:sass_context_ptr], :string 133 | attach_function :sass_context_get_error_message, [:sass_context_ptr], :string 134 | attach_function :sass_context_get_error_file, [:sass_context_ptr], :string 135 | attach_function :sass_context_get_error_line, [:sass_context_ptr], :size_t 136 | attach_function :sass_context_get_error_column, [:sass_context_ptr], :size_t 137 | attach_function :sass_context_get_source_map_string, [:sass_context_ptr], :string 138 | attach_function :_context_get_included_files, :sass_context_get_included_files, [:sass_context_ptr], :pointer 139 | 140 | def self.context_get_included_files(*args) 141 | return_string_array _context_get_included_files(*args) 142 | end 143 | 144 | # ADDAPI Sass_Import_Entry ADDCALL sass_compiler_get_last_import(struct Sass_Compiler* compiler); 145 | attach_function :sass_compiler_get_last_import, [:pointer], :pointer 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/sassc/native/native_functions_api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Native 5 | # Creators for sass function list and function descriptors 6 | # ADDAPI Sass_C_Function_List ADDCALL sass_make_function_list (size_t length); 7 | # ADDAPI Sass_C_Function_Callback ADDCALL sass_make_function (const char* signature, Sass_C_Function fn, void* cookie); 8 | attach_function :sass_make_function_list, [:size_t], :sass_c_function_list_ptr 9 | attach_function :sass_make_function, [:string, :sass_c_function, :pointer], :sass_c_function_callback_ptr 10 | 11 | # Setters and getters for callbacks on function lists 12 | # ADDAPI Sass_C_Function_Callback ADDCALL sass_function_get_list_entry(Sass_C_Function_List list, size_t pos); 13 | # ADDAPI void ADDCALL sass_function_set_list_entry(Sass_C_Function_List list, size_t pos, Sass_C_Function_Callback cb); 14 | attach_function :sass_function_get_list_entry, [:sass_c_function_list_ptr, :size_t], :sass_c_function_callback_ptr 15 | attach_function :sass_function_set_list_entry, [:sass_c_function_list_ptr, :size_t, :sass_c_function_callback_ptr], :void 16 | 17 | # ADDAPI union Sass_Value* ADDCALL sass_make_number (double val, const char* unit); 18 | attach_function :sass_make_number, [:double, :string], :sass_value_ptr 19 | 20 | # ADDAPI union Sass_Value* ADDCALL sass_make_string (const char* val); 21 | attach_function :sass_make_string, [:string], :sass_value_ptr 22 | 23 | # ADDAPI union Sass_Value* ADDCALL sass_make_qstring (const char* val); 24 | attach_function :sass_make_qstring, [:string], :sass_value_ptr 25 | 26 | # ADDAPI union Sass_Value* ADDCALL sass_make_color (double r, double g, double b, double a); 27 | attach_function :sass_make_color, [:double, :double, :double, :double], :sass_value_ptr 28 | 29 | # ADDAPI union Sass_Value* ADDCALL sass_make_map (size_t len); 30 | attach_function :sass_make_map, [:size_t], :sass_value_ptr 31 | 32 | # ADDAPI union Sass_Value* ADDCALL sass_make_list (size_t len, enum Sass_Separator sep) 33 | attach_function :sass_make_list, [:size_t, SassSeparator], :sass_value_ptr 34 | 35 | # ADDAPI union Sass_Value* ADDCALL sass_make_boolean (boolean val); 36 | attach_function :sass_make_boolean, [:bool], :sass_value_ptr 37 | 38 | # ADDAPI void ADDCALL sass_map_set_key (union Sass_Value* v, size_t i, union Sass_Value*); 39 | attach_function :sass_map_set_key, [:sass_value_ptr, :size_t, :sass_value_ptr], :void 40 | 41 | # ADDAPI union Sass_Value* ADDCALL sass_map_get_key (const union Sass_Value* v, size_t i); 42 | attach_function :sass_map_get_key, [:sass_value_ptr, :size_t], :sass_value_ptr 43 | 44 | # ADDAPI void ADDCALL sass_map_set_value (union Sass_Value* v, size_t i, union Sass_Value*); 45 | attach_function :sass_map_set_value, [:sass_value_ptr, :size_t, :sass_value_ptr], :void 46 | 47 | # ADDAPI union Sass_Value* ADDCALL sass_map_get_value (const union Sass_Value* v, size_t i); 48 | attach_function :sass_map_get_value, [:sass_value_ptr, :size_t], :sass_value_ptr 49 | 50 | # ADDAPI size_t ADDCALL sass_map_get_length (const union Sass_Value* v); 51 | attach_function :sass_map_get_length, [:sass_value_ptr], :size_t 52 | 53 | # ADDAPI union Sass_Value* ADDCALL sass_list_get_value (const union Sass_Value* v, size_t i); 54 | attach_function :sass_list_get_value, [:sass_value_ptr, :size_t], :sass_value_ptr 55 | 56 | # ADDAPI void ADDCALL sass_list_set_value (union Sass_Value* v, size_t i, union Sass_Value* value); 57 | attach_function :sass_list_set_value, [:sass_value_ptr, :size_t, :sass_value_ptr], :void 58 | 59 | # ADDAPI size_t ADDCALL sass_list_get_length (const union Sass_Value* v); 60 | attach_function :sass_list_get_length, [:sass_value_ptr], :size_t 61 | 62 | # ADDAPI union Sass_Value* ADDCALL sass_make_error (const char* msg); 63 | attach_function :sass_make_error, [:string], :sass_value_ptr 64 | 65 | # ADDAPI enum Sass_Tag ADDCALL sass_value_get_tag (const union Sass_Value* v); 66 | attach_function :sass_value_get_tag, [:sass_value_ptr], SassTag 67 | attach_function :sass_value_is_null, [:sass_value_ptr], :bool 68 | 69 | # ADDAPI const char* ADDCALL sass_string_get_value (const union Sass_Value* v); 70 | attach_function :sass_string_get_value, [:sass_value_ptr], :string 71 | 72 | # ADDAPI bool ADDCALL sass_string_is_quoted(const union Sass_Value* v); 73 | attach_function :sass_string_is_quoted, [:sass_value_ptr], :bool 74 | 75 | # ADDAPI const char* ADDCALL sass_number_get_value (const union Sass_Value* v); 76 | attach_function :sass_number_get_value, [:sass_value_ptr], :double 77 | 78 | # ADDAPI const char* ADDCALL sass_number_get_unit (const union Sass_Value* v); 79 | attach_function :sass_number_get_unit, [:sass_value_ptr], :string 80 | 81 | # ADDAPI const char* ADDCALL sass_boolean_get_value (const union Sass_Value* v); 82 | attach_function :sass_boolean_get_value, [:sass_value_ptr], :bool 83 | 84 | def self.string_get_type(native_value) 85 | string_is_quoted(native_value) ? :string : :identifier 86 | end 87 | 88 | # ADDAPI double ADDCALL sass_color_get_r (const union Sass_Value* v); 89 | # ADDAPI void ADDCALL sass_color_set_r (union Sass_Value* v, double r); 90 | # ADDAPI double ADDCALL sass_color_get_g (const union Sass_Value* v); 91 | # ADDAPI void ADDCALL sass_color_set_g (union Sass_Value* v, double g); 92 | # ADDAPI double ADDCALL sass_color_get_b (const union Sass_Value* v); 93 | # ADDAPI void ADDCALL sass_color_set_b (union Sass_Value* v, double b); 94 | # ADDAPI double ADDCALL sass_color_get_a (const union Sass_Value* v); 95 | # ADDAPI void ADDCALL sass_color_set_a (union Sass_Value* v, double a); 96 | ['r', 'g', 'b', 'a'].each do |color_channel| 97 | attach_function "sass_color_get_#{color_channel}".to_sym, [:sass_value_ptr], :double 98 | attach_function "sass_color_set_#{color_channel}".to_sym, [:sass_value_ptr, :double], :void 99 | end 100 | 101 | # ADDAPI char* ADDCALL sass_error_get_message (const union Sass_Value* v); 102 | # ADDAPI void ADDCALL sass_error_set_message (union Sass_Value* v, char* msg); 103 | attach_function :sass_error_get_message, [:sass_value_ptr], :string 104 | attach_function :sass_error_set_message, [:sass_value_ptr, :pointer], :void 105 | 106 | # Getters for custom function descriptors 107 | # ADDAPI const char* ADDCALL sass_function_get_signature (Sass_C_Function_Callback fn); 108 | # ADDAPI Sass_C_Function ADDCALL sass_function_get_function (Sass_C_Function_Callback fn); 109 | # ADDAPI void* ADDCALL sass_function_get_cookie (Sass_C_Function_Callback fn); 110 | attach_function :sass_function_get_signature, [:sass_c_function_callback_ptr], :string 111 | attach_function :sass_function_get_function, [:sass_c_function_callback_ptr], :sass_c_function 112 | attach_function :sass_function_get_cookie, [:sass_c_function_callback_ptr], :pointer 113 | 114 | # Creators for custom importer callback (with some additional pointer) 115 | # The pointer is mostly used to store the callback into the actual binding 116 | # ADDAPI Sass_C_Import_Callback ADDCALL sass_make_importer (Sass_C_Import_Fn, void* cookie); 117 | attach_function :sass_make_importer, [:sass_c_import_function, :pointer], :sass_importer 118 | 119 | # Getters for import function descriptors 120 | # ADDAPI Sass_C_Import_Fn ADDCALL sass_import_get_function (Sass_C_Import_Callback fn); 121 | # ADDAPI void* ADDCALL sass_import_get_cookie (Sass_C_Import_Callback fn); 122 | 123 | # Deallocator for associated memory 124 | # ADDAPI void ADDCALL sass_delete_importer (Sass_C_Import_Callback fn); 125 | 126 | # Creator for sass custom importer return argument list 127 | # ADDAPI struct Sass_Import** ADDCALL sass_make_import_list (size_t length); 128 | attach_function :sass_make_import_list, [:size_t], :sass_import_list_ptr 129 | 130 | # Creator for a single import entry returned by the custom importer inside the list 131 | # ADDAPI struct Sass_Import* ADDCALL sass_make_import_entry (const char* path, char* source, char* srcmap); 132 | # ADDAPI struct Sass_Import* ADDCALL sass_make_import (const char* path, const char* base, char* source, char* srcmap); 133 | attach_function :sass_make_import_entry, [:string, :pointer, :pointer], :sass_import_ptr 134 | 135 | # Setters to insert an entry into the import list (you may also use [] access directly) 136 | # Since we are dealing with pointers they should have a guaranteed and fixed size 137 | # ADDAPI void ADDCALL sass_import_set_list_entry (struct Sass_Import** list, size_t idx, struct Sass_Import* entry); 138 | attach_function :sass_import_set_list_entry, [:sass_import_list_ptr, :size_t, :sass_import_ptr], :void 139 | # ADDAPI struct Sass_Import* ADDCALL sass_import_get_list_entry (struct Sass_Import** list, size_t idx); 140 | 141 | # Getters for import entry 142 | # ADDAPI const char* ADDCALL sass_import_get_imp_path (struct Sass_Import*); 143 | attach_function :sass_import_get_imp_path, [:sass_import_ptr], :string 144 | # ADDAPI const char* ADDCALL sass_import_get_abs_path (struct Sass_Import*); 145 | attach_function :sass_import_get_abs_path, [:sass_import_ptr], :string 146 | # ADDAPI const char* ADDCALL sass_import_get_source (struct Sass_Import*); 147 | attach_function :sass_import_get_source, [:sass_import_ptr], :string 148 | # ADDAPI const char* ADDCALL sass_import_get_srcmap (struct Sass_Import*); 149 | # Explicit functions to take ownership of these items 150 | # The property on our struct will be reset to NULL 151 | # ADDAPI char* ADDCALL sass_import_take_source (struct Sass_Import*); 152 | # ADDAPI char* ADDCALL sass_import_take_srcmap (struct Sass_Import*); 153 | 154 | # Deallocator for associated memory (incl. entries) 155 | # ADDAPI void ADDCALL sass_delete_import_list (struct Sass_Import**); 156 | # Just in case we have some stray import structs 157 | # ADDAPI void ADDCALL sass_delete_import (struct Sass_Import*); 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /lib/sassc/native/sass2scss_api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Native 5 | # ADDAPI char* ADDCALL sass2scss (const char* sass, const int options); 6 | attach_function :sass2scss, [:string, :int], :string 7 | 8 | # ADDAPI const char* ADDCALL sass2scss_version(void); 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/sassc/native/sass_input_style.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Native 5 | SassInputStyle = enum( 6 | :sass_context_null, 7 | :sass_context_file, 8 | :sass_context_data, 9 | :sass_context_folder 10 | ) 11 | end 12 | end 13 | 14 | -------------------------------------------------------------------------------- /lib/sassc/native/sass_output_style.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Native 5 | SassOutputStyle = enum( 6 | :sass_style_nested, 7 | :sass_style_expanded, 8 | :sass_style_compact, 9 | :sass_style_compressed 10 | ) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/sassc/native/sass_value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Native 5 | class SassValue < FFI::Union; end 6 | 7 | SassTag = enum( 8 | :sass_boolean, 9 | :sass_number, 10 | :sass_color, 11 | :sass_string, 12 | :sass_list, 13 | :sass_map, 14 | :sass_null, 15 | :sass_error, 16 | :sass_warning 17 | ) 18 | 19 | SassSeparator = enum( 20 | :sass_comma, 21 | :sass_space 22 | ) 23 | 24 | class SassUnknown < FFI::Struct 25 | layout :tag, SassTag 26 | end 27 | 28 | class SassBoolean < FFI::Struct 29 | layout :tag, SassTag, 30 | :value, :bool 31 | end 32 | 33 | class SassNumber < FFI::Struct 34 | layout :tag, SassTag, 35 | :value, :double, 36 | :unit, :string 37 | end 38 | 39 | class SassColor < FFI::Struct 40 | layout :tag, SassTag, 41 | :r, :double, 42 | :g, :double, 43 | :b, :double, 44 | :a, :double 45 | end 46 | 47 | class SassString < FFI::Struct 48 | layout :tag, SassTag, 49 | :value, :string 50 | end 51 | 52 | class SassList < FFI::Struct 53 | layout :tag, SassTag, 54 | :separator, SassSeparator, 55 | :length, :size_t, 56 | :values, :pointer 57 | end 58 | 59 | class SassMapPair < FFI::Struct 60 | layout :key, SassValue.ptr, 61 | :value, SassValue.ptr 62 | end 63 | 64 | class SassMap < FFI::Struct 65 | layout :tag, SassTag, 66 | :length, :size_t, 67 | :pairs, SassMapPair.ptr 68 | end 69 | 70 | class SassNull < FFI::Struct 71 | layout :tag, SassTag 72 | end 73 | 74 | class SassError < FFI::Struct 75 | layout :tag, SassTag, 76 | :message, :string 77 | end 78 | 79 | class SassWarning < FFI::Struct 80 | layout :tag, SassTag, 81 | :message, :string 82 | end 83 | 84 | class SassValue # < FFI::Union 85 | layout :unknown, SassUnknown, 86 | :boolean, SassBoolean, 87 | :number, SassNumber, 88 | :color, SassColor, 89 | :string, SassString, 90 | :list, SassList, 91 | :map, SassMap, 92 | :null, SassNull, 93 | :error, SassError, 94 | :warning, SassWarning 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/sassc/native/string_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Native 5 | class StringList < FFI::Struct 6 | layout :string_list, StringList.ptr, 7 | :string, :string 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/sassc/sass_2_scss.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | class Sass2Scss 5 | def self.convert(sass) 6 | Native.sass2scss(sass, 0) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/sassc/script.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Script 5 | 6 | def self.custom_functions(functions: Functions) 7 | functions.public_instance_methods 8 | end 9 | 10 | def self.formatted_function_name(function_name, functions: Functions) 11 | params = functions.instance_method(function_name).parameters 12 | params = params.map { |param_type, name| "$#{name}#{': null' if param_type == :opt}" }.join(", ") 13 | return "#{function_name}(#{params})" 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/sassc/script/functions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Script 5 | module Functions 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/sassc/script/value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # The abstract superclass for SassScript objects. 4 | # Many of these methods, especially the ones that correspond to SassScript operations, 5 | # are designed to be overridden by subclasses which may change the semantics somewhat. 6 | # The operations listed here are just the defaults. 7 | 8 | class SassC::Script::Value 9 | 10 | # Returns the pure Ruby value of the value. 11 | # The type of this value varies based on the subclass. 12 | attr_reader :value 13 | 14 | # The source range in the document on which this node appeared. 15 | attr_accessor :source_range 16 | 17 | # Creates a new value. 18 | def initialize(value = nil) 19 | value.freeze unless value.nil? || value == true || value == false 20 | @value = value 21 | @options = nil 22 | end 23 | 24 | # Sets the options hash for this node, 25 | # as well as for all child nodes. 26 | # See the official Sass reference for options. 27 | attr_writer :options 28 | 29 | # Returns the options hash for this node. 30 | # Raises SassC::SyntaxError if the value was created 31 | # outside of the parser and \{#to\_s} was called on it 32 | def options 33 | return @options if @options 34 | 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.") 35 | end 36 | 37 | # Returns the hash code of this value. Two objects' hash codes should be 38 | # equal if the objects are equal. 39 | def hash 40 | value.hash 41 | end 42 | 43 | # True if this Value is the same as `other` 44 | def eql?(other) 45 | self == other 46 | end 47 | 48 | # Returns a system inspect value for this object 49 | def inspect 50 | value.inspect 51 | end 52 | 53 | # Returns `true` (all Values are truthy) 54 | def to_bool 55 | true 56 | end 57 | 58 | # Compares this object to `other` 59 | def ==(other) 60 | self.class == other.class && value == other.value 61 | end 62 | 63 | # Returns the integer value of this value. 64 | # Raises SassC::SyntaxError if this value doesn’t implment integer conversion. 65 | def to_i 66 | raise SassC::SyntaxError.new("#{inspect} is not an integer.") 67 | end 68 | 69 | # @raise [SassC::SyntaxError] if this value isn't an integer 70 | def assert_int!; to_i; end 71 | 72 | # Returns the separator for this value. For non-list-like values or the 73 | # empty list, this will be `nil`. For lists or maps, it will be `:space` or `:comma`. 74 | def separator 75 | nil 76 | end 77 | 78 | # Whether the value is surrounded by square brackets. For non-list values, 79 | # this will be `false`. 80 | def bracketed 81 | false 82 | end 83 | 84 | # Returns the value of this Value as an array. 85 | # Single Values are considered the same as single-element arrays. 86 | def to_a 87 | [self] 88 | end 89 | 90 | # Returns the value of this value as a hash. Most values don't have hash 91 | # representations, but [Map]s and empty [List]s do. 92 | # 93 | # @return [Hash] This value as a hash 94 | # @raise [SassC::SyntaxError] if this value doesn't have a hash representation 95 | def to_h 96 | raise SassC::SyntaxError.new("#{inspect} is not a map.") 97 | end 98 | 99 | # Returns the string representation of this value 100 | # as it would be output to the CSS document. 101 | # 102 | # @options opts :quote [String] 103 | # The preferred quote style for quoted strings. If `:none`, strings are 104 | # always emitted unquoted. 105 | # @return [String] 106 | def to_s(opts = {}) 107 | SassC::Util.abstract(self) 108 | end 109 | alias_method :to_sass, :to_s 110 | 111 | # Returns `false` (all Values are truthy) 112 | def null? 113 | false 114 | end 115 | 116 | # Creates a new list containing `contents` but with the same brackets and 117 | # separators as this object, when interpreted as a list. 118 | # 119 | # @param contents [Array] The contents of the new list. 120 | # @param separator [Symbol] The separator of the new list. Defaults to \{#separator}. 121 | # @param bracketed [Boolean] Whether the new list is bracketed. Defaults to \{#bracketed}. 122 | # @return [Sass::Script::Value::List] 123 | def with_contents(contents, separator: self.separator, bracketed: self.bracketed) 124 | SassC::Script::Value::List.new(contents, separator: separator, bracketed: bracketed) 125 | end 126 | 127 | protected 128 | 129 | # Evaluates the value. 130 | # 131 | # @param environment [Sass::Environment] The environment in which to evaluate the SassScript 132 | # @return [Value] This value 133 | def _perform(environment) 134 | self 135 | end 136 | 137 | end 138 | -------------------------------------------------------------------------------- /lib/sassc/script/value/bool.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # A SassScript object representing a boolean (true or false) value. 4 | 5 | class SassC::Script::Value::Bool < SassC::Script::Value 6 | 7 | # The true value in SassScript. 8 | # This is assigned before new is overridden below so that we use the default implementation. 9 | TRUE = new(true) 10 | 11 | # The false value in SassScript. 12 | # This is assigned before new is overridden below so that we use the default implementation. 13 | FALSE = new(false) 14 | 15 | # We override object creation so that users of the core API 16 | # will not need to know that booleans are specific constants. 17 | # Tests `value` for truthiness and returns the TRUE or FALSE constant. 18 | def self.new(value) 19 | value ? TRUE : FALSE 20 | end 21 | 22 | # The pure Ruby value of this Boolean 23 | attr_reader :value 24 | alias_method :to_bool, :value 25 | 26 | # Returns the string "true" or "false" for this value 27 | def to_s(opts = {}) 28 | @value.to_s 29 | end 30 | alias_method :to_sass, :to_s 31 | 32 | end 33 | -------------------------------------------------------------------------------- /lib/sassc/script/value/color.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # A SassScript object representing a CSS color. 4 | # This class provides a very bare-bones system for storing a RGB(A) or HSL(A) 5 | # color and converting it to a CSS color function. 6 | # 7 | # If your Sass method accepts a color you will need to perform any 8 | # needed color mathematics or transformations yourself. 9 | 10 | class SassC::Script::Value::Color < SassC::Script::Value 11 | 12 | attr_reader :red 13 | attr_reader :green 14 | attr_reader :blue 15 | attr_reader :hue 16 | attr_reader :saturation 17 | attr_reader :lightness 18 | attr_reader :alpha 19 | 20 | # Creates a new color with (`red`, `green`, `blue`) or (`hue`, `saturation`, `lightness` 21 | # values, plus an optional `alpha` transparency value. 22 | def initialize(red:nil, green:nil, blue:nil, hue:nil, saturation:nil, lightness:nil, alpha:1.0) 23 | if red && green && blue && alpha 24 | @mode = :rgba 25 | @red = SassC::Util.clamp(red.to_i, 0, 255) 26 | @green = SassC::Util.clamp(green.to_i, 0, 255) 27 | @blue = SassC::Util.clamp(blue.to_i, 0, 255) 28 | @alpha = SassC::Util.clamp(alpha.to_f, 0.0, 1.0) 29 | elsif hue && saturation && lightness && alpha 30 | @mode = :hsla 31 | @hue = SassC::Util.clamp(hue.to_i, 0, 360) 32 | @saturation = SassC::Util.clamp(saturation.to_i, 0, 100) 33 | @lightness = SassC::Util.clamp(lightness.to_i, 0, 100) 34 | @alpha = SassC::Util.clamp(alpha.to_f, 0.0, 1.0) 35 | else 36 | raise SassC::UnsupportedValue, "Unable to determine color configuration for " 37 | end 38 | end 39 | 40 | # Returns a CSS color declaration in the form 41 | # `rgb(…)`, `rgba(…)`, `hsl(…)`, or `hsla(…)`. 42 | def to_s 43 | if rgba? && @alpha == 1.0 44 | return "rgb(#{@red}, #{@green}, #{@blue})" 45 | elsif rgba? 46 | return "rgba(#{@red}, #{@green}, #{@blue}, #{alpha_string})" 47 | elsif hsla? && @alpha == 1.0 48 | return "hsl(#{@hue}, #{@saturation}%, #{@lightness}%)" 49 | else # hsla? 50 | return "hsla(#{@hue}, #{@saturation}%, #{@lightness}%, #{alpha_string})" 51 | end 52 | end 53 | 54 | # True if this color has RGBA values 55 | def rgba? 56 | @mode == :rgba 57 | end 58 | 59 | # True if this color has HSLA values 60 | def hlsa? 61 | @mode == :hlsa 62 | end 63 | 64 | # Returns the alpha value of this color as a string 65 | # and rounded to 8 decimal places. 66 | def alpha_string 67 | alpha.round(8).to_s 68 | end 69 | 70 | # Returns the values of this color in an array. 71 | # Provided for compatibility between different SassC::Script::Value classes 72 | def value 73 | return [ 74 | red, green, blue, 75 | hue, saturation, lightness, 76 | alpha, 77 | ].compact 78 | end 79 | 80 | # True if this Color is equal to `other_color` 81 | def eql?(other_color) 82 | unless other_color.is_a?(self.class) 83 | raise ArgumentError, "No implicit conversion of #{other_color.class} to #{self.class}" 84 | end 85 | self.value == other_color.value 86 | end 87 | alias_method :==, :eql? 88 | 89 | # Returns a numeric value for comparing two Color objects 90 | # This method is used internally by the Hash class and is not the same as `.to_h` 91 | def hash 92 | value.hash 93 | end 94 | 95 | end 96 | -------------------------------------------------------------------------------- /lib/sassc/script/value/list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # A SassScript object representing a CSS list. 4 | # This includes both comma-separated lists and space-separated lists. 5 | 6 | class SassC::Script::Value::List < SassC::Script::Value 7 | 8 | # The Ruby array containing the contents of the list. 9 | # 10 | # @return [Array] 11 | attr_reader :value 12 | alias_method :to_a, :value 13 | 14 | # The operator separating the values of the list. 15 | # Either `:comma` or `:space`. 16 | # 17 | # @return [Symbol] 18 | attr_reader :separator 19 | 20 | # Whether the list is surrounded by square brackets. 21 | # 22 | # @return [Boolean] 23 | attr_reader :bracketed 24 | 25 | # Creates a new list. 26 | # 27 | # @param value [Array] See \{#value} 28 | # @param separator [Symbol] See \{#separator} 29 | # @param bracketed [Boolean] See \{#bracketed} 30 | def initialize(value, separator: nil, bracketed: false) 31 | super(value) 32 | @separator = separator 33 | @bracketed = bracketed 34 | end 35 | 36 | # @see Value#options= 37 | def options=(options) 38 | super 39 | value.each {|v| v.options = options} 40 | end 41 | 42 | # @see Value#eq 43 | def eq(other) 44 | SassC::Script::Value::Bool.new( 45 | other.is_a?(List) && value == other.value && 46 | separator == other.separator && bracketed == other.bracketed 47 | ) 48 | end 49 | 50 | def hash 51 | @hash ||= [value, separator, bracketed].hash 52 | end 53 | 54 | # @see Value#to_s 55 | def to_s(opts = {}) 56 | if !bracketed && value.empty? 57 | raise SassC::SyntaxError.new("#{inspect} isn't a valid CSS value.") 58 | end 59 | 60 | members = value. 61 | reject {|e| e.is_a?(Null) || e.is_a?(List) && e.value.empty?}. 62 | map {|e| e.to_s(opts)} 63 | 64 | contents = members.join(sep_str) 65 | bracketed ? "[#{contents}]" : contents 66 | end 67 | 68 | # @see Value#to_sass 69 | def to_sass(opts = {}) 70 | return bracketed ? "[]" : "()" if value.empty? 71 | members = value.map do |v| 72 | if element_needs_parens?(v) 73 | "(#{v.to_sass(opts)})" 74 | else 75 | v.to_sass(opts) 76 | end 77 | end 78 | 79 | if separator == :comma && members.length == 1 80 | return "#{bracketed ? '[' : '('}#{members.first},#{bracketed ? ']' : ')'}" 81 | end 82 | 83 | contents = members.join(sep_str(nil)) 84 | bracketed ? "[#{contents}]" : contents 85 | end 86 | 87 | # @see Value#to_h 88 | def to_h 89 | return {} if value.empty? 90 | super 91 | end 92 | 93 | # @see Value#inspect 94 | def inspect 95 | (bracketed ? '[' : '(') + value.map {|e| e.inspect}.join(sep_str(nil)) + (bracketed ? ']' : ')') 96 | end 97 | 98 | # Asserts an index is within the list. 99 | # 100 | # @private 101 | # 102 | # @param list [Sass::Script::Value::List] The list for which the index should be checked. 103 | # @param n [Sass::Script::Value::Number] The index being checked. 104 | def self.assert_valid_index(list, n) 105 | if !n.int? || n.to_i == 0 106 | raise ArgumentError.new("List index #{n} must be a non-zero integer") 107 | elsif list.to_a.size == 0 108 | raise ArgumentError.new("List index is #{n} but list has no items") 109 | elsif n.to_i.abs > (size = list.to_a.size) 110 | raise ArgumentError.new( 111 | "List index is #{n} but list is only #{size} item#{'s' if size != 1} long") 112 | end 113 | end 114 | 115 | private 116 | 117 | def element_needs_parens?(element) 118 | if element.is_a?(List) 119 | return false if element.value.length < 2 120 | return false if element.bracketed 121 | precedence = Sass::Script::Parser.precedence_of(separator || :space) 122 | return Sass::Script::Parser.precedence_of(element.separator || :space) <= precedence 123 | end 124 | 125 | return false unless separator == :space 126 | return false unless element.is_a?(Sass::Script::Tree::UnaryOperation) 127 | element.operator == :minus || element.operator == :plus 128 | end 129 | 130 | def sep_str(opts = options) 131 | return ' ' if separator == :space 132 | return ',' if opts && opts[:style] == :compressed 133 | ', ' 134 | end 135 | 136 | end 137 | -------------------------------------------------------------------------------- /lib/sassc/script/value/map.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SassC::Script::Value::Map < SassC::Script::Value 4 | 5 | # The Ruby hash containing the contents of this map. 6 | # @return [Hash] 7 | attr_reader :value 8 | alias_method :to_h, :value 9 | 10 | # Creates a new map. 11 | # 12 | # @param hash [Hash] 13 | def initialize(hash) 14 | super(hash) 15 | end 16 | 17 | # @see Value#options= 18 | def options=(options) 19 | super 20 | value.each do |k, v| 21 | k.options = options 22 | v.options = options 23 | end 24 | end 25 | 26 | # @see Value#separator 27 | def separator 28 | :comma unless value.empty? 29 | end 30 | 31 | # @see Value#to_a 32 | def to_a 33 | value.map do |k, v| 34 | list = SassC::Script::Value::List.new([k, v], separator: :space) 35 | list.options = options 36 | list 37 | end 38 | end 39 | 40 | # @see Value#eq 41 | def eq(other) 42 | SassC::Script::Value::Bool.new(other.is_a?(Map) && value == other.value) 43 | end 44 | 45 | def hash 46 | @hash ||= value.hash 47 | end 48 | 49 | # @see Value#to_s 50 | def to_s(opts = {}) 51 | raise SassC::SyntaxError.new("#{inspect} isn't a valid CSS value.") 52 | end 53 | 54 | def to_sass(opts = {}) 55 | return "()" if value.empty? 56 | 57 | to_sass = lambda do |value| 58 | if value.is_a?(List) && value.separator == :comma 59 | "(#{value.to_sass(opts)})" 60 | else 61 | value.to_sass(opts) 62 | end 63 | end 64 | 65 | "(#{value.map {|(k, v)| "#{to_sass[k]}: #{to_sass[v]}"}.join(', ')})" 66 | end 67 | alias_method :inspect, :to_sass 68 | 69 | end 70 | -------------------------------------------------------------------------------- /lib/sassc/script/value/number.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # A SassScript object representing a number. 4 | # SassScript numbers can have decimal values, 5 | # and can also have units. 6 | # For example, `12`, `1px`, and `10.45em` 7 | # are all valid values. 8 | # 9 | # Numbers can also have more complex units, such as `1px*em/in`. 10 | # These cannot be inputted directly in Sass code at the moment. 11 | 12 | class SassC::Script::Value::Number < SassC::Script::Value 13 | 14 | # The Ruby value of the number. 15 | # 16 | # @return [Numeric] 17 | attr_reader :value 18 | 19 | # A list of units in the numerator of the number. 20 | # For example, `1px*em/in*cm` would return `["px", "em"]` 21 | # @return [Array] 22 | attr_reader :numerator_units 23 | 24 | # A list of units in the denominator of the number. 25 | # For example, `1px*em/in*cm` would return `["in", "cm"]` 26 | # @return [Array] 27 | attr_reader :denominator_units 28 | 29 | # The original representation of this number. 30 | # For example, although the result of `1px/2px` is `0.5`, 31 | # the value of `#original` is `"1px/2px"`. 32 | # 33 | # This is only non-nil when the original value should be used as the CSS value, 34 | # as in `font: 1px/2px`. 35 | # 36 | # @return [Boolean, nil] 37 | attr_accessor :original 38 | 39 | def self.precision 40 | Thread.current[:sass_numeric_precision] || Thread.main[:sass_numeric_precision] || 10 41 | end 42 | 43 | # Sets the number of digits of precision 44 | # For example, if this is `3`, 45 | # `3.1415926` will be printed as `3.142`. 46 | # The numeric precision is stored as a thread local for thread safety reasons. 47 | # To set for all threads, be sure to set the precision on the main thread. 48 | def self.precision=(digits) 49 | Thread.current[:sass_numeric_precision] = digits.round 50 | Thread.current[:sass_numeric_precision_factor] = nil 51 | Thread.current[:sass_numeric_epsilon] = nil 52 | end 53 | 54 | # the precision factor used in numeric output 55 | # it is derived from the `precision` method. 56 | def self.precision_factor 57 | Thread.current[:sass_numeric_precision_factor] ||= 10.0**precision 58 | end 59 | 60 | # Used in checking equality of floating point numbers. Any 61 | # numbers within an `epsilon` of each other are considered functionally equal. 62 | # The value for epsilon is one tenth of the current numeric precision. 63 | def self.epsilon 64 | Thread.current[:sass_numeric_epsilon] ||= 1 / (precision_factor * 10) 65 | end 66 | 67 | # Used so we don't allocate two new arrays for each new number. 68 | NO_UNITS = [] 69 | 70 | # @param value [Numeric] The value of the number 71 | # @param numerator_units [::String, Array<::String>] See \{#numerator\_units} 72 | # @param denominator_units [::String, Array<::String>] See \{#denominator\_units} 73 | def initialize(value, numerator_units = NO_UNITS, denominator_units = NO_UNITS) 74 | numerator_units = [numerator_units] if numerator_units.is_a?(::String) 75 | denominator_units = [denominator_units] if denominator_units.is_a?(::String) 76 | super(value) 77 | @numerator_units = numerator_units 78 | @denominator_units = denominator_units 79 | @options = nil 80 | normalize! 81 | end 82 | 83 | def hash 84 | [value, numerator_units, denominator_units].hash 85 | end 86 | 87 | # Hash-equality works differently than `==` equality for numbers. 88 | # Hash-equality must be transitive, so it just compares the exact value, 89 | # numerator units, and denominator units. 90 | def eql?(other) 91 | basically_equal?(value, other.value) && numerator_units == other.numerator_units && 92 | denominator_units == other.denominator_units 93 | end 94 | 95 | # @return [String] The CSS representation of this number 96 | # @raise [Sass::SyntaxError] if this number has units that can't be used in CSS 97 | # (e.g. `px*in`) 98 | def to_s(opts = {}) 99 | return original if original 100 | raise Sass::SyntaxError.new("#{inspect} isn't a valid CSS value.") unless legal_units? 101 | inspect 102 | end 103 | 104 | # Returns a readable representation of this number. 105 | # 106 | # This representation is valid CSS (and valid SassScript) 107 | # as long as there is only one unit. 108 | # 109 | # @return [String] The representation 110 | def inspect(opts = {}) 111 | return original if original 112 | 113 | value = self.class.round(self.value) 114 | str = value.to_s 115 | 116 | # Ruby will occasionally print in scientific notation if the number is 117 | # small enough. That's technically valid CSS, but it's not well-supported 118 | # and confusing. 119 | str = ("%0.#{self.class.precision}f" % value).gsub(/0*$/, '') if str.include?('e') 120 | 121 | # Sometimes numeric formatting will result in a decimal number with a trailing zero (x.0) 122 | if str =~ /(.*)\.0$/ 123 | str = $1 124 | end 125 | 126 | # We omit a leading zero before the decimal point in compressed mode. 127 | if @options && options[:style] == :compressed 128 | str.sub!(/^(-)?0\./, '\1.') 129 | end 130 | 131 | unitless? ? str : "#{str}#{unit_str}" 132 | end 133 | alias_method :to_sass, :inspect 134 | 135 | # @return [Integer] The integer value of the number 136 | # @raise [Sass::SyntaxError] if the number isn't an integer 137 | def to_i 138 | super unless int? 139 | value.to_i 140 | end 141 | 142 | # @return [Boolean] Whether or not this number is an integer. 143 | def int? 144 | basically_equal?(value % 1, 0.0) 145 | end 146 | 147 | # @return [Boolean] Whether or not this number has no units. 148 | def unitless? 149 | @numerator_units.empty? && @denominator_units.empty? 150 | end 151 | 152 | # Checks whether the number has the numerator unit specified. 153 | # 154 | # @example 155 | # number = Sass::Script::Value::Number.new(10, "px") 156 | # number.is_unit?("px") => true 157 | # number.is_unit?(nil) => false 158 | # 159 | # @param unit [::String, nil] The unit the number should have or nil if the number 160 | # should be unitless. 161 | # @see Number#unitless? The unitless? method may be more readable. 162 | def is_unit?(unit) 163 | if unit 164 | denominator_units.size == 0 && numerator_units.size == 1 && numerator_units.first == unit 165 | else 166 | unitless? 167 | end 168 | end 169 | 170 | # @return [Boolean] Whether or not this number has units that can be represented in CSS 171 | # (that is, zero or one \{#numerator\_units}). 172 | def legal_units? 173 | (@numerator_units.empty? || @numerator_units.size == 1) && @denominator_units.empty? 174 | end 175 | 176 | # Returns this number converted to other units. 177 | # The conversion takes into account the relationship between e.g. mm and cm, 178 | # as well as between e.g. in and cm. 179 | # 180 | # If this number has no units, it will simply return itself 181 | # with the given units. 182 | # 183 | # An incompatible coercion, e.g. between px and cm, will raise an error. 184 | # 185 | # @param num_units [Array] The numerator units to coerce this number into. 186 | # See {\#numerator\_units} 187 | # @param den_units [Array] The denominator units to coerce this number into. 188 | # See {\#denominator\_units} 189 | # @return [Number] The number with the new units 190 | # @raise [Sass::UnitConversionError] if the given units are incompatible with the number's 191 | # current units 192 | def coerce(num_units, den_units) 193 | Number.new(if unitless? 194 | value 195 | else 196 | value * coercion_factor(@numerator_units, num_units) / 197 | coercion_factor(@denominator_units, den_units) 198 | end, num_units, den_units) 199 | end 200 | 201 | # @param other [Number] A number to decide if it can be compared with this number. 202 | # @return [Boolean] Whether or not this number can be compared with the other. 203 | def comparable_to?(other) 204 | operate(other, :+) 205 | true 206 | rescue Sass::UnitConversionError 207 | false 208 | end 209 | 210 | # Returns a human readable representation of the units in this number. 211 | # For complex units this takes the form of: 212 | # numerator_unit1 * numerator_unit2 / denominator_unit1 * denominator_unit2 213 | # @return [String] a string that represents the units in this number 214 | def unit_str 215 | rv = @numerator_units.sort.join("*") 216 | if @denominator_units.any? 217 | rv << "/" 218 | rv << @denominator_units.sort.join("*") 219 | end 220 | rv 221 | end 222 | 223 | private 224 | 225 | # @private 226 | # @see Sass::Script::Number.basically_equal? 227 | def basically_equal?(num1, num2) 228 | self.class.basically_equal?(num1, num2) 229 | end 230 | 231 | # Checks whether two numbers are within an epsilon of each other. 232 | # @return [Boolean] 233 | def self.basically_equal?(num1, num2) 234 | (num1 - num2).abs < epsilon 235 | end 236 | 237 | # @private 238 | def self.round(num) 239 | if num.is_a?(Float) && (num.infinite? || num.nan?) 240 | num 241 | elsif basically_equal?(num % 1, 0.0) 242 | num.round 243 | else 244 | ((num * precision_factor).round / precision_factor).to_f 245 | end 246 | end 247 | 248 | OPERATIONS = [:+, :-, :<=, :<, :>, :>=, :%] 249 | 250 | def operate(other, operation) 251 | this = self 252 | if OPERATIONS.include?(operation) 253 | if unitless? 254 | this = this.coerce(other.numerator_units, other.denominator_units) 255 | else 256 | other = other.coerce(@numerator_units, @denominator_units) 257 | end 258 | end 259 | # avoid integer division 260 | value = :/ == operation ? this.value.to_f : this.value 261 | result = value.send(operation, other.value) 262 | 263 | if result.is_a?(Numeric) 264 | Number.new(result, *compute_units(this, other, operation)) 265 | else # Boolean op 266 | Bool.new(result) 267 | end 268 | end 269 | 270 | def coercion_factor(from_units, to_units) 271 | # get a list of unmatched units 272 | from_units, to_units = sans_common_units(from_units, to_units) 273 | 274 | if from_units.size != to_units.size || !convertable?(from_units | to_units) 275 | raise Sass::UnitConversionError.new( 276 | "Incompatible units: '#{from_units.join('*')}' and '#{to_units.join('*')}'.") 277 | end 278 | 279 | from_units.zip(to_units).inject(1) {|m, p| m * conversion_factor(p[0], p[1])} 280 | end 281 | 282 | def compute_units(this, other, operation) 283 | case operation 284 | when :* 285 | [this.numerator_units + other.numerator_units, 286 | this.denominator_units + other.denominator_units] 287 | when :/ 288 | [this.numerator_units + other.denominator_units, 289 | this.denominator_units + other.numerator_units] 290 | else 291 | [this.numerator_units, this.denominator_units] 292 | end 293 | end 294 | 295 | def normalize! 296 | return if unitless? 297 | @numerator_units, @denominator_units = 298 | sans_common_units(@numerator_units, @denominator_units) 299 | 300 | @denominator_units.each_with_index do |d, i| 301 | next unless convertable?(d) && (u = @numerator_units.find {|n| convertable?([n, d])}) 302 | @value /= conversion_factor(d, u) 303 | @denominator_units.delete_at(i) 304 | @numerator_units.delete_at(@numerator_units.index(u)) 305 | end 306 | end 307 | 308 | # This is the source data for all the unit logic. It's pre-processed to make 309 | # it efficient to figure out whether a set of units is mutually compatible 310 | # and what the conversion ratio is between two units. 311 | # 312 | # These come from http://www.w3.org/TR/2012/WD-css3-values-20120308/. 313 | relative_sizes = [ 314 | { 315 | "in" => Rational(1), 316 | "cm" => Rational(1, 2.54), 317 | "pc" => Rational(1, 6), 318 | "mm" => Rational(1, 25.4), 319 | "q" => Rational(1, 101.6), 320 | "pt" => Rational(1, 72), 321 | "px" => Rational(1, 96) 322 | }, 323 | { 324 | "deg" => Rational(1, 360), 325 | "grad" => Rational(1, 400), 326 | "rad" => Rational(1, 2 * Math::PI), 327 | "turn" => Rational(1) 328 | }, 329 | { 330 | "s" => Rational(1), 331 | "ms" => Rational(1, 1000) 332 | }, 333 | { 334 | "Hz" => Rational(1), 335 | "kHz" => Rational(1000) 336 | }, 337 | { 338 | "dpi" => Rational(1), 339 | "dpcm" => Rational(254, 100), 340 | "dppx" => Rational(96) 341 | } 342 | ] 343 | 344 | # A hash from each known unit to the set of units that it's mutually 345 | # convertible with. 346 | MUTUALLY_CONVERTIBLE = {} 347 | relative_sizes.map do |values| 348 | set = values.keys.to_set 349 | values.keys.each {|name| MUTUALLY_CONVERTIBLE[name] = set} 350 | end 351 | 352 | # A two-dimensional hash from two units to the conversion ratio between 353 | # them. Multiply `X` by `CONVERSION_TABLE[X][Y]` to convert it to `Y`. 354 | CONVERSION_TABLE = {} 355 | relative_sizes.each do |values| 356 | values.each do |(name1, value1)| 357 | CONVERSION_TABLE[name1] ||= {} 358 | values.each do |(name2, value2)| 359 | value = value1 / value2 360 | CONVERSION_TABLE[name1][name2] = value.denominator == 1 ? value.to_i : value.to_f 361 | end 362 | end 363 | end 364 | 365 | def conversion_factor(from_unit, to_unit) 366 | CONVERSION_TABLE[from_unit][to_unit] 367 | end 368 | 369 | def convertable?(units) 370 | units = Array(units).to_set 371 | return true if units.empty? 372 | return false unless (mutually_convertible = MUTUALLY_CONVERTIBLE[units.first]) 373 | units.subset?(mutually_convertible) 374 | end 375 | 376 | def sans_common_units(units1, units2) 377 | units2 = units2.dup 378 | # Can't just use -, because we want px*px to coerce properly to px*mm 379 | units1 = units1.map do |u| 380 | j = units2.index(u) 381 | next u unless j 382 | units2.delete_at(j) 383 | nil 384 | end 385 | units1.compact! 386 | return units1, units2 387 | end 388 | 389 | end 390 | -------------------------------------------------------------------------------- /lib/sassc/script/value/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SassC::Script::Value::String < SassC::Script::Value 4 | 5 | # The Ruby value of the string. 6 | attr_reader :value 7 | 8 | # Whether this is a CSS string or a CSS identifier. 9 | # The difference is that strings are written with double-quotes, 10 | # while identifiers aren't. 11 | # 12 | # @return [Symbol] `:string` or `:identifier` 13 | attr_reader :type 14 | 15 | # Returns the quoted string representation of `contents`. 16 | # 17 | # @options opts :quote [String] 18 | # The preferred quote style for quoted strings. If `:none`, strings are 19 | # always emitted unquoted. If `nil`, quoting is determined automatically. 20 | # @options opts :sass [String] 21 | # Whether to quote strings for Sass source, as opposed to CSS. Defaults to `false`. 22 | def self.quote(contents, opts = {}) 23 | quote = opts[:quote] 24 | 25 | # Short-circuit if there are no characters that need quoting. 26 | unless contents =~ /[\n\\"']|\#\{/ 27 | quote ||= '"' 28 | return "#{quote}#{contents}#{quote}" 29 | end 30 | 31 | if quote.nil? 32 | if contents.include?('"') 33 | if contents.include?("'") 34 | quote = '"' 35 | else 36 | quote = "'" 37 | end 38 | else 39 | quote = '"' 40 | end 41 | end 42 | 43 | # Replace single backslashes with multiples. 44 | contents = contents.gsub("\\", "\\\\\\\\") 45 | 46 | # Escape interpolation. 47 | contents = contents.gsub('#{', "\\\#{") if opts[:sass] 48 | 49 | if quote == '"' 50 | contents = contents.gsub('"', "\\\"") 51 | else 52 | contents = contents.gsub("'", "\\'") 53 | end 54 | 55 | contents = contents.gsub(/\n(?![a-fA-F0-9\s])/, "\\a").gsub("\n", "\\a ") 56 | "#{quote}#{contents}#{quote}" 57 | end 58 | 59 | # Creates a new string. 60 | # 61 | # @param value [String] See \{#value} 62 | # @param type [Symbol] See \{#type} 63 | # @param deprecated_interp_equivalent [String?] 64 | # If this was created via a potentially-deprecated string interpolation, 65 | # this is the replacement expression that should be suggested to the user. 66 | def initialize(value, type = :identifier) 67 | super(value) 68 | @type = type 69 | end 70 | 71 | # @see Value#plus 72 | def plus(other) 73 | if other.is_a?(SassC::Script::Value::String) 74 | other_value = other.value 75 | else 76 | other_value = other.to_s(:quote => :none) 77 | end 78 | SassC::Script::Value::String.new(value + other_value, type) 79 | end 80 | 81 | # @see Value#to_s 82 | def to_s(opts = {}) 83 | return @value.gsub(/\n\s*/, ' ') if opts[:quote] == :none || @type == :identifier 84 | self.class.quote(value, opts) 85 | end 86 | 87 | # @see Value#to_sass 88 | def to_sass(opts = {}) 89 | to_s(opts.merge(:sass => true)) 90 | end 91 | 92 | def inspect 93 | String.quote(value) 94 | end 95 | 96 | end 97 | -------------------------------------------------------------------------------- /lib/sassc/script/value_conversion.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC::Script::ValueConversion 4 | 5 | def self.from_native(native_value, options) 6 | case value_tag = SassC::Native.value_get_tag(native_value) 7 | when :sass_null 8 | # no-op 9 | when :sass_string 10 | value = SassC::Native.string_get_value(native_value) 11 | type = SassC::Native.string_get_type(native_value) 12 | argument = SassC::Script::Value::String.new(value, type) 13 | argument 14 | when :sass_boolean 15 | value = SassC::Native.boolean_get_value(native_value) 16 | argument = SassC::Script::Value::Bool.new(value) 17 | argument 18 | when :sass_number 19 | value = SassC::Native.number_get_value(native_value) 20 | unit = SassC::Native.number_get_unit(native_value) 21 | argument = SassC::Script::Value::Number.new(value, unit) 22 | argument 23 | when :sass_color 24 | 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) 25 | argument = SassC::Script::Value::Color.new(red:red, green:green, blue:blue, alpha:alpha) 26 | argument.options = options 27 | argument 28 | when :sass_map 29 | values = {} 30 | length = SassC::Native::map_get_length native_value 31 | (0..length-1).each do |index| 32 | key = SassC::Native::map_get_key(native_value, index) 33 | value = SassC::Native::map_get_value(native_value, index) 34 | values[from_native(key, options)] = from_native(value, options) 35 | end 36 | argument = SassC::Script::Value::Map.new values 37 | argument 38 | when :sass_list 39 | length = SassC::Native::list_get_length(native_value) 40 | items = (0...length).map do |index| 41 | native_item = SassC::Native::list_get_value(native_value, index) 42 | from_native(native_item, options) 43 | end 44 | SassC::Script::Value::List.new(items, separator: :space) 45 | else 46 | raise UnsupportedValue.new("Sass argument of type #{value_tag} unsupported") 47 | end 48 | end 49 | 50 | def self.to_native(value) 51 | case value_name = value.class.name.split("::").last 52 | when "String" 53 | SassC::Script::ValueConversion::String.new(value).to_native 54 | when "Color" 55 | SassC::Script::ValueConversion::Color.new(value).to_native 56 | when "Number" 57 | SassC::Script::ValueConversion::Number.new(value).to_native 58 | when "Map" 59 | SassC::Script::ValueConversion::Map.new(value).to_native 60 | when "List" 61 | SassC::Script::ValueConversion::List.new(value).to_native 62 | when "Bool" 63 | SassC::Script::ValueConversion::Bool.new(value).to_native 64 | else 65 | raise SassC::UnsupportedValue.new("Sass return type #{value_name} unsupported") 66 | end 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /lib/sassc/script/value_conversion/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Script 5 | module ValueConversion 6 | class Base 7 | def initialize(value) 8 | @value = value 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/sassc/script/value_conversion/bool.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Script 5 | module ValueConversion 6 | class Bool < Base 7 | def to_native 8 | Native::make_boolean(@value.value) 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/sassc/script/value_conversion/color.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Script 5 | module ValueConversion 6 | class Color < Base 7 | def to_native 8 | Native::make_color( 9 | @value.red, 10 | @value.green, 11 | @value.blue, 12 | @value.alpha 13 | ) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/sassc/script/value_conversion/list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Script 5 | module ValueConversion 6 | SEPARATORS = { 7 | space: :sass_space, 8 | comma: :sass_comma 9 | } 10 | 11 | class List < Base 12 | def to_native 13 | list = @value.to_a 14 | sep = SEPARATORS.fetch(@value.separator) 15 | native_list = Native::make_list(list.size, sep) 16 | list.each_with_index do |item, index| 17 | native_item = ValueConversion.to_native(item) 18 | Native::list_set_value(native_list, index, native_item) 19 | end 20 | native_list 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/sassc/script/value_conversion/map.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Script 5 | module ValueConversion 6 | class Map < Base 7 | def to_native 8 | hash = @value.to_h 9 | native_map = Native::make_map( hash.size ) 10 | hash.each_with_index do |(key, value), index| 11 | key = ValueConversion.to_native key 12 | value = ValueConversion.to_native value 13 | Native::map_set_key( native_map, index, key ) 14 | Native::map_set_value( native_map, index, value ) 15 | end 16 | return native_map 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/sassc/script/value_conversion/number.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Script 5 | module ValueConversion 6 | class Number < Base 7 | def to_native 8 | Native::make_number(@value.value, @value.numerator_units.first) 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/sassc/script/value_conversion/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Script 5 | module ValueConversion 6 | class String < Base 7 | def to_native(opts = {}) 8 | if opts[:quote] == :none || @value.type == :identifier 9 | Native::make_string(@value.to_s) 10 | else 11 | Native::make_qstring(@value.to_s) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/sassc/util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "erb" 4 | require "set" 5 | require "enumerator" 6 | require "stringio" 7 | require "rbconfig" 8 | require "uri" 9 | require "thread" 10 | require "pathname" 11 | 12 | # A module containing various useful functions. 13 | 14 | module SassC::Util 15 | 16 | extend self 17 | 18 | # An array of ints representing the Ruby version number. 19 | # @api public 20 | RUBY_VERSION_COMPONENTS = RUBY_VERSION.split(".").map {|s| s.to_i} 21 | 22 | # The Ruby engine we're running under. Defaults to `"ruby"` 23 | # if the top-level constant is undefined. 24 | # @api public 25 | RUBY_ENGINE = defined?(::RUBY_ENGINE) ? ::RUBY_ENGINE : "ruby" 26 | 27 | # Maps the keys in a hash according to a block. 28 | # @example 29 | # map_keys({:foo => "bar", :baz => "bang"}) {|k| k.to_s} 30 | # #=> {"foo" => "bar", "baz" => "bang"} 31 | # @param hash [Hash] The hash to map 32 | # @yield [key] A block in which the keys are transformed 33 | # @yieldparam key [Object] The key that should be mapped 34 | # @yieldreturn [Object] The new value for the key 35 | # @return [Hash] The mapped hash 36 | # @see #map_vals 37 | # @see #map_hash 38 | def map_keys(hash) 39 | map_hash(hash) {|k, v| [yield(k), v]} 40 | end 41 | 42 | # Restricts the numeric `value` to be within `min` and `max`, inclusive. 43 | # If the value is lower than `min` 44 | def clamp(value, min, max) 45 | return min if value < min 46 | return max if value > max 47 | return value 48 | end 49 | 50 | # Like [Fixnum.round], but leaves rooms for slight floating-point 51 | # differences. 52 | # 53 | # @param value [Numeric] 54 | # @return [Numeric] 55 | def round(value) 56 | # If the number is within epsilon of X.5, round up (or down for negative 57 | # numbers). 58 | mod = value % 1 59 | mod_is_half = (mod - 0.5).abs < SassC::Script::Value::Number.epsilon 60 | if value > 0 61 | !mod_is_half && mod < 0.5 ? value.floor : value.ceil 62 | else 63 | mod_is_half || mod < 0.5 ? value.floor : value.ceil 64 | end 65 | end 66 | 67 | # Return an array of all possible paths through the given arrays. 68 | # 69 | # @param arrs [Array] 70 | # @return [Array] 71 | # 72 | # @example 73 | # paths([[1, 2], [3, 4], [5]]) #=> 74 | # # [[1, 3, 5], 75 | # # [2, 3, 5], 76 | # # [1, 4, 5], 77 | # # [2, 4, 5]] 78 | def paths(arrs) 79 | arrs.inject([[]]) do |paths, arr| 80 | arr.map {|e| paths.map {|path| path + [e]}}.flatten(1) 81 | end 82 | end 83 | 84 | # Returns information about the caller of the previous method. 85 | # 86 | # @param entry [String] An entry in the `#caller` list, or a similarly formatted string 87 | # @return [[String, Integer, (String, nil)]] 88 | # An array containing the filename, line, and method name of the caller. 89 | # The method name may be nil 90 | def caller_info(entry = nil) 91 | # JRuby evaluates `caller` incorrectly when it's in an actual default argument. 92 | entry ||= caller[1] 93 | info = entry.scan(/^((?:[A-Za-z]:)?.*?):(-?.*?)(?::.*`(.+)')?$/).first 94 | info[1] = info[1].to_i 95 | # This is added by Rubinius to designate a block, but we don't care about it. 96 | info[2].sub!(/ \{\}\Z/, '') if info[2] 97 | info 98 | end 99 | 100 | # Throws a NotImplementedError for an abstract method. 101 | # 102 | # @param obj [Object] `self` 103 | # @raise [NotImplementedError] 104 | def abstract(obj) 105 | raise NotImplementedError.new("#{obj.class} must implement ##{caller_info[2]}") 106 | end 107 | 108 | # Prints a deprecation warning for the caller method. 109 | # 110 | # @param obj [Object] `self` 111 | # @param message [String] A message describing what to do instead. 112 | def deprecated(obj, message = nil) 113 | obj_class = obj.is_a?(Class) ? "#{obj}." : "#{obj.class}#" 114 | full_message = "DEPRECATION WARNING: #{obj_class}#{caller_info[2]} " + 115 | "will be removed in a future version of Sass.#{("\n" + message) if message}" 116 | SassC::Util.sass_warn full_message 117 | end 118 | 119 | # Silences all Sass warnings within a block. 120 | # 121 | # @yield A block in which no Sass warnings will be printed 122 | def silence_sass_warnings 123 | old_level, Sass.logger.log_level = Sass.logger.log_level, :error 124 | yield 125 | ensure 126 | SassC.logger.log_level = old_level 127 | end 128 | 129 | # The same as `Kernel#warn`, but is silenced by \{#silence\_sass\_warnings}. 130 | # 131 | # @param msg [String] 132 | def sass_warn(msg) 133 | Sass.logger.warn("#{msg}\n") 134 | end 135 | 136 | ## Cross Rails Version Compatibility 137 | 138 | # Returns the root of the Rails application, 139 | # if this is running in a Rails context. 140 | # Returns `nil` if no such root is defined. 141 | # 142 | # @return [String, nil] 143 | def rails_root 144 | if defined?(::Rails.root) 145 | return ::Rails.root.to_s if ::Rails.root 146 | raise "ERROR: Rails.root is nil!" 147 | end 148 | return RAILS_ROOT.to_s if defined?(RAILS_ROOT) 149 | nil 150 | end 151 | 152 | # Returns the environment of the Rails application, 153 | # if this is running in a Rails context. 154 | # Returns `nil` if no such environment is defined. 155 | # 156 | # @return [String, nil] 157 | def rails_env 158 | return ::Rails.env.to_s if defined?(::Rails.env) 159 | return RAILS_ENV.to_s if defined?(RAILS_ENV) 160 | nil 161 | end 162 | 163 | ## Cross-OS Compatibility 164 | # 165 | # These methods are cached because some of them are called quite frequently 166 | # and even basic checks like String#== are too costly to be called repeatedly. 167 | 168 | # Whether or not this is running on Windows. 169 | # 170 | # @return [Boolean] 171 | def windows? 172 | return @windows if defined?(@windows) 173 | @windows = (RbConfig::CONFIG['host_os'] =~ /mswin|windows|mingw/i) 174 | end 175 | 176 | # Whether or not this is running on IronRuby. 177 | # 178 | # @return [Boolean] 179 | def ironruby? 180 | return @ironruby if defined?(@ironruby) 181 | @ironruby = RUBY_ENGINE == "ironruby" 182 | end 183 | 184 | # Whether or not this is running on Rubinius. 185 | # 186 | # @return [Boolean] 187 | def rbx? 188 | return @rbx if defined?(@rbx) 189 | @rbx = RUBY_ENGINE == "rbx" 190 | end 191 | 192 | # Whether or not this is running on JRuby. 193 | # 194 | # @return [Boolean] 195 | def jruby? 196 | return @jruby if defined?(@jruby) 197 | @jruby = RUBY_PLATFORM =~ /java/ 198 | end 199 | 200 | # Returns an array of ints representing the JRuby version number. 201 | # 202 | # @return [Array] 203 | def jruby_version 204 | @jruby_version ||= ::JRUBY_VERSION.split(".").map {|s| s.to_i} 205 | end 206 | 207 | # Returns `path` relative to `from`. 208 | # 209 | # This is like `Pathname#relative_path_from` except it accepts both strings 210 | # and pathnames, it handles Windows path separators correctly, and it throws 211 | # an error rather than crashing if the paths use different encodings 212 | # (https://github.com/ruby/ruby/pull/713). 213 | # 214 | # @param path [String, Pathname] 215 | # @param from [String, Pathname] 216 | # @return [Pathname?] 217 | def relative_path_from(path, from) 218 | pathname(path.to_s).relative_path_from(pathname(from.to_s)) 219 | rescue NoMethodError => e 220 | raise e unless e.name == :zero? 221 | 222 | # Work around https://github.com/ruby/ruby/pull/713. 223 | path = path.to_s 224 | from = from.to_s 225 | raise ArgumentError("Incompatible path encodings: #{path.inspect} is #{path.encoding}, " + 226 | "#{from.inspect} is #{from.encoding}") 227 | end 228 | 229 | singleton_methods.each {|method| module_function method} 230 | 231 | end 232 | -------------------------------------------------------------------------------- /lib/sassc/util/normalized_map.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "delegate" 4 | 5 | # A hash that normalizes its string keys while still allowing you to get back 6 | # to the original keys that were stored. If several different values normalize 7 | # to the same value, whichever is stored last wins. 8 | 9 | class SassC::Util::NormalizedMap 10 | 11 | # Create a normalized map 12 | def initialize(map = nil) 13 | @key_strings = {} 14 | @map = {} 15 | map.each {|key, value| self[key] = value} if map 16 | end 17 | 18 | # Specifies how to transform the key. 19 | # This can be overridden to create other normalization behaviors. 20 | def normalize(key) 21 | key.tr("-", "_") 22 | end 23 | 24 | # Returns the version of `key` as it was stored before 25 | # normalization. If `key` isn't in the map, returns it as it was 26 | # passed in. 27 | # @return [String] 28 | def denormalize(key) 29 | @key_strings[normalize(key)] || key 30 | end 31 | 32 | # @private 33 | def []=(k, v) 34 | normalized = normalize(k) 35 | @map[normalized] = v 36 | @key_strings[normalized] = k 37 | v 38 | end 39 | 40 | # @private 41 | def [](k) 42 | @map[normalize(k)] 43 | end 44 | 45 | # @private 46 | def has_key?(k) 47 | @map.has_key?(normalize(k)) 48 | end 49 | 50 | # @private 51 | def delete(k) 52 | normalized = normalize(k) 53 | @key_strings.delete(normalized) 54 | @map.delete(normalized) 55 | end 56 | 57 | # @return [Hash] Hash with the keys as they were stored (before normalization). 58 | def as_stored 59 | SassC::Util.map_keys(@map) {|k| @key_strings[k]} 60 | end 61 | 62 | def empty? 63 | @map.empty? 64 | end 65 | 66 | def values 67 | @map.values 68 | end 69 | 70 | def keys 71 | @map.keys 72 | end 73 | 74 | def each 75 | @map.each {|k, v| yield(k, v)} 76 | end 77 | 78 | def size 79 | @map.size 80 | end 81 | 82 | def to_hash 83 | @map.dup 84 | end 85 | 86 | def to_a 87 | @map.to_a 88 | end 89 | 90 | def map 91 | @map.map {|k, v| yield(k, v)} 92 | end 93 | 94 | def dup 95 | d = super 96 | d.send(:instance_variable_set, "@map", @map.dup) 97 | d 98 | end 99 | 100 | def sort_by 101 | @map.sort_by {|k, v| yield k, v} 102 | end 103 | 104 | def update(map) 105 | map = map.as_stored if map.is_a?(NormalizedMap) 106 | map.each {|k, v| self[k] = v} 107 | end 108 | 109 | def method_missing(method, *args, &block) 110 | @map.send(method, *args, &block) 111 | end 112 | 113 | def respond_to_missing?(method, include_private = false) 114 | @map.respond_to?(method, include_private) 115 | end 116 | 117 | end 118 | -------------------------------------------------------------------------------- /lib/sassc/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | VERSION = "2.4.0" 5 | end 6 | -------------------------------------------------------------------------------- /sassc.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("../lib", __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "sassc/version" 6 | 7 | Gem::Specification.new do |spec| 8 | 9 | spec.name = "sassc" 10 | spec.version = SassC::VERSION 11 | spec.authors = ["Ryan Boland"] 12 | spec.email = ["ryan@tanookilabs.com"] 13 | spec.summary = "Use libsass with Ruby!" 14 | spec.description = "Use libsass with Ruby!" 15 | spec.homepage = "https://github.com/sass/sassc-ruby" 16 | spec.license = "MIT" 17 | 18 | spec.files = `git ls-files -z`.split("\x0") 19 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 20 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 21 | 22 | spec.required_ruby_version = ">= 2.0.0" 23 | 24 | spec.require_paths = ["lib"] 25 | 26 | spec.platform = Gem::Platform::RUBY 27 | spec.extensions = ["ext/extconf.rb"] 28 | 29 | spec.add_development_dependency "minitest", "~> 5.5.1" 30 | spec.add_development_dependency "minitest-around" 31 | spec.add_development_dependency "test_construct" 32 | spec.add_development_dependency "pry" 33 | spec.add_development_dependency "bundler" 34 | spec.add_development_dependency "rake" 35 | spec.add_development_dependency "rake-compiler" 36 | spec.add_development_dependency "rake-compiler-dock" 37 | 38 | spec.add_dependency "ffi", "~> 1.9" 39 | 40 | gem_dir = File.expand_path(File.dirname(__FILE__)) + "/" 41 | 42 | libsass_dir = File.join(gem_dir, 'ext', 'libsass') 43 | if !File.directory?(libsass_dir) || 44 | # '.', '..', and possibly '.git' from a failed checkout: 45 | Dir.entries(libsass_dir).size <= 3 46 | Dir.chdir(__dir__) { system('git submodule update --init') } or 47 | fail 'Could not fetch libsass' 48 | end 49 | 50 | # Write a VERSION file for non-binary gems (for `SassC::Native.version`). 51 | if File.exist?(File.join(libsass_dir, '.git')) 52 | libsass_version = Dir.chdir(libsass_dir) do 53 | %x[git describe --abbrev=4 --dirty --always --tags].chomp 54 | end 55 | File.write(File.join(libsass_dir, 'VERSION'), libsass_version) 56 | end 57 | 58 | Dir.chdir(libsass_dir) do 59 | submodule_relative_path = File.join('ext', 'libsass') 60 | skip_re = %r{(^("?test|docs|script)/)|\.md$|\.yml$} 61 | only_re = %r{\.[ch](pp)?$} 62 | `git ls-files`.split($\).each do |filename| 63 | next if filename =~ skip_re || filename !~ only_re 64 | spec.files << File.join(submodule_relative_path, filename) 65 | end 66 | spec.files << File.join(submodule_relative_path, 'VERSION') 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /test/custom_importer_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | module SassC 6 | class CustomImporterTest < MiniTest::Test 7 | include TempFileTest 8 | 9 | class CustomImporter < Importer 10 | def imports(path, parent_path) 11 | if path =~ /styles/ 12 | [ 13 | Import.new("#{path}1.scss", source: "$var1: #000;"), 14 | Import.new("#{path}2.scss") 15 | ] 16 | else 17 | Import.new(path) 18 | end 19 | end 20 | end 21 | 22 | class NoFilesImporter < Importer 23 | def imports(path, parent_path) 24 | [] 25 | end 26 | end 27 | 28 | class OptionsImporter < Importer 29 | def imports(path, parent_path) 30 | Import.new("name.scss", source: options[:custom_option_source]) 31 | end 32 | end 33 | 34 | class ParentImporter < Importer 35 | def imports(path, parent_path) 36 | Import.new("name.scss", source: ".#{parent_path} { color: red; }") 37 | end 38 | end 39 | 40 | def test_custom_importer_works 41 | temp_file("styles2.scss", ".hi { color: $var1; }") 42 | temp_file("fonts.scss", ".font { color: $var1; }") 43 | 44 | data = < err 24 | expected = "#{filename}:3" 25 | assert_equal expected, err.backtrace.first 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/fixtures/paths.scss: -------------------------------------------------------------------------------- 1 | div { 2 | url: url(asset-path("foo.svg")); 3 | url: url(image-path("foo.png")); 4 | url: url(video-path("foo.mov")); 5 | url: url(audio-path("foo.mp3")); 6 | url: url(font-path("foo.woff")); 7 | url: url(javascript-path('foo.js')); 8 | url: url(javascript-path("foo.js")); 9 | url: url(stylesheet-path("foo.css")); 10 | } 11 | -------------------------------------------------------------------------------- /test/functions_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | require "stringio" 5 | 6 | module SassC 7 | class FunctionsTest < MiniTest::Test 8 | include FixtureHelper 9 | 10 | def setup 11 | @real_stderr, $stderr = $stderr, StringIO.new 12 | end 13 | 14 | def teardown 15 | $stderr = @real_stderr 16 | end 17 | 18 | def test_functions_may_return_sass_string_type 19 | assert_sass <<-SCSS, <<-CSS 20 | div { url: url(sass_return_path("foo.svg")); } 21 | SCSS 22 | div { url: url("foo.svg"); } 23 | CSS 24 | end 25 | 26 | def test_functions_work_with_varying_quotes_and_string_types 27 | assert_sass <<-SCSS, <<-CSS 28 | div { 29 | url: url(asset-path("foo.svg")); 30 | url: url(image-path("foo.png")); 31 | url: url(video-path("foo.mov")); 32 | url: url(audio-path("foo.mp3")); 33 | url: url(font-path("foo.woff")); 34 | url: url(javascript-path('foo.js')); 35 | url: url(javascript-path("foo.js")); 36 | url: url(stylesheet-path("foo.css")); 37 | } 38 | SCSS 39 | div { 40 | url: url(asset-path("foo.svg")); 41 | url: url(image-path("foo.png")); 42 | url: url(video-path("foo.mov")); 43 | url: url(audio-path("foo.mp3")); 44 | url: url(font-path("foo.woff")); 45 | url: url("/js/foo.js"); 46 | url: url("/js/foo.js"); 47 | url: url(/css/foo.css); 48 | } 49 | CSS 50 | end 51 | 52 | def test_function_with_no_return_value 53 | assert_sass <<-SCSS, <<-CSS 54 | div {url: url(no-return-path('foo.svg'));} 55 | SCSS 56 | div { url: url(); } 57 | CSS 58 | end 59 | 60 | def test_function_that_returns_a_color 61 | assert_sass <<-SCSS, <<-CSS 62 | div { background: returns-a-color(); } 63 | SCSS 64 | div { background: black; } 65 | CSS 66 | end 67 | 68 | def test_function_that_returns_a_number 69 | assert_sass <<-SCSS, <<-CSS 70 | div { width: returns-a-number(); } 71 | SCSS 72 | div { width: -312rem; } 73 | CSS 74 | end 75 | 76 | def test_function_that_takes_a_number 77 | assert_sass <<-SCSS, <<-CSS 78 | div { display: inspect-number(42.1px); } 79 | SCSS 80 | div { display: 42.1px; } 81 | CSS 82 | end 83 | 84 | def test_function_that_returns_a_bool 85 | assert_sass <<-SCSS, <<-CSS 86 | div { width: returns-a-bool(); } 87 | SCSS 88 | div { width: true; } 89 | CSS 90 | end 91 | 92 | def test_function_that_takes_a_bool 93 | assert_sass <<-SCSS, <<-CSS 94 | div { display: inspect-bool(true)} 95 | SCSS 96 | div { display: true; } 97 | CSS 98 | end 99 | 100 | def test_function_with_optional_arguments 101 | assert_sass <<-SCSS, <<-EXPECTED_CSS 102 | div { 103 | url: optional_arguments('first'); 104 | url: optional_arguments('second', 'qux'); 105 | } 106 | SCSS 107 | div { 108 | url: "first/bar"; 109 | url: "second/qux"; 110 | } 111 | EXPECTED_CSS 112 | end 113 | 114 | def test_functions_may_accept_sass_color_type 115 | assert_sass <<-SCSS, <<-EXPECTED_CSS 116 | div { color: nice_color_argument(red); } 117 | SCSS 118 | div { color: rgb(255, 0, 0); } 119 | EXPECTED_CSS 120 | end 121 | 122 | def test_function_with_unsupported_tag 123 | skip('What are other unsupported tags?') 124 | engine = Engine.new("div {url: function_with_unsupported_tag(());}") 125 | 126 | exception = assert_raises(SassC::SyntaxError) do 127 | engine.render 128 | end 129 | 130 | assert_match /Sass argument of type sass_list unsupported/, exception.message 131 | assert_equal "[SassC::FunctionsHandler] Sass argument of type sass_list unsupported", stderr_output 132 | end 133 | 134 | def test_function_with_error 135 | engine = Engine.new("div {url: function_that_raises_errors();}") 136 | 137 | exception = assert_raises(SassC::SyntaxError) do 138 | engine.render 139 | end 140 | 141 | assert_match /Error: error in C function function_that_raises_errors/, exception.message 142 | assert_match /Intentional wrong thing happened somewhere inside the custom function/, exception.message 143 | assert_match /\[SassC::FunctionsHandler\] Intentional wrong thing happened somewhere inside the custom function/, stderr_output 144 | end 145 | 146 | def test_function_that_returns_a_sass_value 147 | assert_sass <<-SCSS, <<-CSS 148 | div { background: returns-sass-value(); } 149 | SCSS 150 | div { background: black; } 151 | CSS 152 | end 153 | 154 | def test_function_that_returns_a_sass_map 155 | assert_sass <<-SCSS, <<-CSS 156 | $my-map: returns-sass-map(); 157 | div { background: map-get( $my-map, color ); } 158 | SCSS 159 | div { background: black; } 160 | CSS 161 | end 162 | 163 | def test_function_that_takes_a_sass_map 164 | assert_sass <<-SCSS, <<-CSS 165 | div { background-color: map-get( inspect-map(( color: black, number: 1.23px, string: "abc", map: ( x: 'y' ))), color ); } 166 | SCSS 167 | div { background-color: black; } 168 | CSS 169 | end 170 | 171 | def test_function_that_returns_a_sass_list 172 | assert_sass <<-SCSS, <<-CSS 173 | $my-list: returns-sass-list(); 174 | div { width: nth( $my-list, 2 ); } 175 | SCSS 176 | div { width: 20; } 177 | CSS 178 | end 179 | 180 | def test_function_that_takes_a_sass_list 181 | assert_sass <<-SCSS, <<-CSS 182 | div { width: nth(inspect-list((10 20 30)), 2); } 183 | SCSS 184 | div { width: 20; } 185 | CSS 186 | end 187 | 188 | def test_concurrency 189 | 10.times do 190 | threads = [] 191 | 10.times do |i| 192 | threads << Thread.new(i) do |id| 193 | out = Engine.new("div { url: inspect_options(); }", {test_key1: 'test_value', test_key2: id}).render 194 | assert_match /test_key1/, out 195 | assert_match /test_key2/, out 196 | assert_match /test_value/, out 197 | assert_match /#{id}/, out 198 | end 199 | end 200 | threads.each(&:join) 201 | end 202 | end 203 | 204 | def test_pass_custom_functions_as_a_parameter 205 | out = Engine.new("div { url: test-function(); }", {functions: ExternalFunctions}).render 206 | assert_match /custom_function/, out 207 | end 208 | 209 | def test_pass_incompatible_type_to_custom_functions 210 | assert_raises(TypeError) do 211 | Engine.new("div { url: test-function(); }", {functions: Class.new}).render 212 | end 213 | end 214 | 215 | private 216 | 217 | def assert_sass(sass, expected_css) 218 | engine = Engine.new(sass) 219 | assert_equal expected_css.strip.gsub!(/\s+/, " "), # poor man's String#squish 220 | engine.render.strip.gsub!(/\s+/, " ") 221 | end 222 | 223 | def stderr_output 224 | $stderr.string.gsub("\u0000\n", '').chomp 225 | end 226 | 227 | module Script::Functions 228 | 229 | def javascript_path(path) 230 | SassC::Script::Value::String.new("/js/#{path.value}", :string) 231 | end 232 | 233 | def stylesheet_path(path) 234 | SassC::Script::Value::String.new("/css/#{path.value}", :identifier) 235 | end 236 | 237 | def no_return_path(path) 238 | nil 239 | end 240 | 241 | def sass_return_path(path) 242 | SassC::Script::Value::String.new("#{path.value}", :string) 243 | end 244 | 245 | def optional_arguments(path, optional = nil) 246 | optional ||= SassC::Script::Value::String.new("bar") 247 | SassC::Script::Value::String.new("#{path.value}/#{optional.value}", :string) 248 | end 249 | 250 | def function_that_raises_errors 251 | raise StandardError, "Intentional wrong thing happened somewhere inside the custom function" 252 | end 253 | 254 | def function_with_unsupported_tag(value) 255 | end 256 | 257 | def nice_color_argument(color) 258 | return SassC::Script::Value::String.new(color.to_s, :identifier) 259 | end 260 | 261 | def returns_a_color 262 | return SassC::Script::Value::Color.new(red: 0, green: 0, blue: 0) 263 | end 264 | 265 | def returns_a_number 266 | return SassC::Script::Value::Number.new(-312,'rem') 267 | end 268 | 269 | def returns_a_bool 270 | return SassC::Script::Value::Bool.new(true) 271 | end 272 | 273 | def inspect_bool ( argument ) 274 | raise StandardError.new "passed value is not a Sass::Script::Value::Bool" unless argument.is_a? SassC::Script::Value::Bool 275 | return argument 276 | end 277 | 278 | def inspect_number ( argument ) 279 | raise StandardError.new "passed value is not a Sass::Script::Value::Number" unless argument.is_a? SassC::Script::Value::Number 280 | return argument 281 | end 282 | 283 | def inspect_map ( argument ) 284 | argument.to_h.each_pair do |key, value| 285 | raise StandardError.new "key #{key.inspect} is not a string" unless key.is_a? SassC::Script::Value::String 286 | 287 | valueClass = case key.value 288 | when 'string' 289 | SassC::Script::Value::String 290 | when 'number' 291 | SassC::Script::Value::Number 292 | when 'color' 293 | SassC::Script::Value::Color 294 | when 'map' 295 | SassC::Script::Value::Map 296 | end 297 | 298 | raise StandardError.new "unknown key #{key.inspect}" unless valueClass 299 | raise StandardError.new "value for #{key.inspect} is not a #{valueClass}" unless value.is_a? valueClass 300 | end 301 | return argument 302 | end 303 | 304 | def inspect_list(argument) 305 | raise StandardError.new "passed value is not a Sass::Script::Value::List" unless argument.is_a? SassC::Script::Value::List 306 | return argument 307 | end 308 | 309 | def inspect_options 310 | SassC::Script::Value::String.new(self.options.inspect, :string) 311 | end 312 | 313 | def returns_sass_value 314 | return SassC::Script::Value::Color.new(red: 0, green: 0, blue: 0) 315 | end 316 | 317 | def returns_sass_map 318 | key = SassC::Script::Value::String.new("color", "string") 319 | value = SassC::Script::Value::Color.new(red: 0, green: 0, blue: 0) 320 | values = {} 321 | values[key] = value 322 | map = SassC::Script::Value::Map.new values 323 | return map 324 | end 325 | 326 | def returns_sass_list 327 | numbers = [10, 20, 30].map { |n| SassC::Script::Value::Number.new(n, '') } 328 | SassC::Script::Value::List.new(numbers, separator: :space) 329 | end 330 | 331 | end 332 | 333 | module ExternalFunctions 334 | def test_function 335 | SassC::Script::Value::String.new("custom_function", :string) 336 | end 337 | end 338 | 339 | end 340 | end 341 | -------------------------------------------------------------------------------- /test/native_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | module SassC 6 | module NativeTest 7 | SAMPLE_SASS_STRING = "$size: 30px; .hi { width: $size; }" 8 | SPECIAL_SASS_STRING = "$sißßßßßße: 30px; .hßß©i { width: $size; }" 9 | SAMPLE_CSS_OUTPUT = ".hi {\n width: 30px; }\n" 10 | BAD_SASS_STRING = "$size = 30px;" 11 | 12 | class General < MiniTest::Test 13 | def test_it_reports_the_libsass_version 14 | assert_equal "3.6.4", Native.version 15 | end 16 | end 17 | 18 | class DataContext < MiniTest::Test 19 | def teardown 20 | Native.delete_data_context(@data_context) if @data_context 21 | end 22 | 23 | def test_compile_status_is_zero_when_successful 24 | @data_context = Native.make_data_context(SAMPLE_SASS_STRING) 25 | context = Native.data_context_get_context(@data_context) 26 | 27 | status = Native.compile_data_context(@data_context) 28 | assert_equal 0, status 29 | 30 | status = Native.context_get_error_status(context) 31 | assert_equal 0, status 32 | end 33 | 34 | def test_compiled_css_is_correct 35 | @data_context = Native.make_data_context(SAMPLE_SASS_STRING) 36 | context = Native.data_context_get_context(@data_context) 37 | Native.compile_data_context(@data_context) 38 | 39 | css = Native.context_get_output_string(context) 40 | assert_equal SAMPLE_CSS_OUTPUT, css 41 | end 42 | 43 | def test_compile_status_is_one_if_failed 44 | @data_context = Native.make_data_context(BAD_SASS_STRING) 45 | context = Native.data_context_get_context(@data_context) 46 | 47 | status = Native.compile_data_context(@data_context) 48 | refute_equal 0, status 49 | 50 | status = Native.context_get_error_status(context) 51 | refute_equal 0, status 52 | end 53 | 54 | def test_multibyte_characters_work 55 | @data_context = Native.make_data_context(SPECIAL_SASS_STRING) 56 | context = Native.data_context_get_context(@data_context) 57 | 58 | status = Native.compile_data_context(@data_context) 59 | refute_equal 0, status 60 | end 61 | 62 | def test_custom_function 63 | data_context = Native.make_data_context("foo { margin: foo(); }") 64 | context = Native.data_context_get_context(data_context) 65 | options = Native.context_get_options(context) 66 | 67 | random_thing = FFI::MemoryPointer.from_string("hi") 68 | 69 | funct = FFI::Function.new(:pointer, [:pointer, :pointer]) do |s_args, cookie| 70 | Native.make_number(43, "px") 71 | end 72 | 73 | callback = Native.make_function( 74 | "foo()", 75 | funct, 76 | random_thing 77 | ) 78 | 79 | list = Native.make_function_list(1) 80 | Native::function_set_list_entry(list, 0, callback); 81 | Native::option_set_c_functions(options, list) 82 | 83 | assert_equal Native.option_get_c_functions(options), list 84 | 85 | first_list_entry = Native.function_get_list_entry(list, 0) 86 | assert_equal Native.function_get_function(first_list_entry), 87 | funct 88 | assert_equal Native.function_get_signature(first_list_entry), 89 | "foo()" 90 | assert_equal Native.function_get_cookie(first_list_entry), 91 | random_thing 92 | 93 | string = Native.make_string("hello") 94 | assert_equal :sass_string, Native.value_get_tag(string) 95 | assert_equal "hello", Native.string_get_value(string) 96 | 97 | number = Native.make_number(123.4, "rem") 98 | assert_equal 123.4, Native.number_get_value(number) 99 | assert_equal "rem", Native.number_get_unit(number) 100 | 101 | Native.compile_data_context(data_context) 102 | 103 | css = Native.context_get_output_string(context) 104 | assert_equal "foo {\n margin: 43px; }\n", css 105 | end 106 | end 107 | 108 | class FileContext < MiniTest::Test 109 | include TempFileTest 110 | 111 | def teardown 112 | Native.delete_file_context(@file_context) if @file_context 113 | end 114 | 115 | def test_compile_status_is_zero_when_successful 116 | temp_file("style.scss", SAMPLE_SASS_STRING) 117 | 118 | @file_context = Native.make_file_context("style.scss") 119 | context = Native.file_context_get_context(@file_context) 120 | 121 | status = Native.compile_file_context(@file_context) 122 | assert_equal 0, status 123 | 124 | status = Native.context_get_error_status(context) 125 | assert_equal 0, status 126 | end 127 | 128 | def test_compiled_css_is_correct 129 | temp_file("style.scss", SAMPLE_SASS_STRING) 130 | 131 | @file_context = Native.make_file_context("style.scss") 132 | context = Native.file_context_get_context(@file_context) 133 | Native.compile_file_context(@file_context) 134 | 135 | css = Native.context_get_output_string(context) 136 | assert_equal SAMPLE_CSS_OUTPUT, css 137 | end 138 | 139 | def test_invalid_file_name 140 | temp_file("style.scss", SAMPLE_SASS_STRING) 141 | 142 | @file_context = Native.make_file_context("style.jajaja") 143 | context = Native.file_context_get_context(@file_context) 144 | status = Native.compile_file_context(@file_context) 145 | 146 | refute_equal 0, status 147 | 148 | error = Native.context_get_error_message(context) 149 | 150 | assert_match "Error: File to read not found or unreadable: style.jajaja", 151 | error 152 | end 153 | 154 | def test_file_import 155 | temp_file("not_included.scss", "$size: 30px;") 156 | temp_file("import_parent.scss", "$size: 30px;") 157 | temp_file("import.scss", "@import 'import_parent'; $size: 30px;") 158 | temp_file("styles.scss", "@import 'import.scss'; .hi { width: $size; }") 159 | 160 | @file_context = Native.make_file_context("styles.scss") 161 | context = Native.file_context_get_context(@file_context) 162 | status = Native.compile_file_context(@file_context) 163 | 164 | assert_equal 0, status 165 | 166 | css = Native.context_get_output_string(context) 167 | assert_equal SAMPLE_CSS_OUTPUT, css 168 | 169 | included_files = Native.context_get_included_files(context) 170 | included_files.sort! 171 | 172 | assert_match /import.scss/, included_files[0] 173 | assert_match /import_parent.scss/, included_files[1] 174 | assert_match /styles.scss/, included_files[2] 175 | end 176 | 177 | def test_custom_importer 178 | temp_file("not_included.scss", "$size: $var + 25;") 179 | temp_file("styles.scss", "@import 'import.scss'; .hi { width: $size; }") 180 | 181 | @file_context = Native.make_file_context("styles.scss") 182 | context = Native.file_context_get_context(@file_context) 183 | options = Native.context_get_options(context) 184 | 185 | funct = FFI::Function.new(:pointer, [:pointer, :pointer, :pointer]) do |url, prev, cookie| 186 | list = Native.make_import_list(2) 187 | 188 | str = "$var: 5px;" 189 | data = FFI::MemoryPointer.from_string(str) 190 | data.autorelease = false 191 | 192 | entry0 = Native.make_import_entry("fake_includ.scss", data, nil) 193 | entry1 = Native.make_import_entry("not_included.scss", nil, nil) 194 | Native.import_set_list_entry(list, 0, entry0) 195 | Native.import_set_list_entry(list, 1, entry1) 196 | list 197 | end 198 | 199 | callback = Native.make_importer(funct, nil) 200 | list = Native.make_function_list(1) 201 | Native::function_set_list_entry(list, 0, callback) 202 | 203 | Native.option_set_c_importers(options, list) 204 | 205 | status = Native.compile_file_context(@file_context) 206 | assert_equal 0, status 207 | 208 | css = Native.context_get_output_string(context) 209 | assert_equal SAMPLE_CSS_OUTPUT, css 210 | end 211 | end 212 | end 213 | end 214 | -------------------------------------------------------------------------------- /test/output_style_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | module SassC 6 | class OutputStyleTest < MiniTest::Test 7 | def input_scss 8 | input_scss = <<-CSS 9 | $color: #fff; 10 | 11 | #main { 12 | color: $color; 13 | background-color: #000; 14 | p { 15 | width: 10em; 16 | } 17 | } 18 | 19 | .huge { 20 | font-size: 10em; 21 | font-weight: bold; 22 | text-decoration: underline; 23 | } 24 | CSS 25 | end 26 | 27 | def expected_nested_output 28 | <<-CSS 29 | #main { 30 | color: #fff; 31 | background-color: #000; } 32 | #main p { 33 | width: 10em; } 34 | 35 | .huge { 36 | font-size: 10em; 37 | font-weight: bold; 38 | text-decoration: underline; } 39 | CSS 40 | end 41 | 42 | def test_nested_output_is_default 43 | engine = Engine.new(input_scss) 44 | assert_equal expected_nested_output, engine.render 45 | end 46 | 47 | def test_output_style_accepts_strings 48 | engine = Engine.new(input_scss, style: 'sass_style_nested') 49 | assert_equal expected_nested_output, engine.render 50 | end 51 | 52 | def test_invalid_output_style 53 | engine = Engine.new(input_scss, style: 'totally_wrong') 54 | assert_raises(InvalidStyleError) { engine.render } 55 | end 56 | 57 | def test_nested_output 58 | engine = Engine.new(input_scss, style: :sass_style_nested) 59 | assert_equal expected_nested_output, engine.render 60 | end 61 | 62 | def test_expanded_output 63 | engine = Engine.new(input_scss, style: :sass_style_expanded) 64 | assert_equal <<-CSS, engine.render 65 | #main { 66 | color: #fff; 67 | background-color: #000; 68 | } 69 | 70 | #main p { 71 | width: 10em; 72 | } 73 | 74 | .huge { 75 | font-size: 10em; 76 | font-weight: bold; 77 | text-decoration: underline; 78 | } 79 | CSS 80 | end 81 | 82 | def test_compact_output 83 | engine = Engine.new(input_scss, style: :sass_style_compact) 84 | assert_equal <<-CSS, engine.render 85 | #main { color: #fff; background-color: #000; } 86 | 87 | #main p { width: 10em; } 88 | 89 | .huge { font-size: 10em; font-weight: bold; text-decoration: underline; } 90 | CSS 91 | end 92 | 93 | def test_compressed_output 94 | engine = Engine.new(input_scss, style: :sass_style_compressed) 95 | assert_equal <<-CSS, engine.render 96 | #main{color:#fff;background-color:#000}#main p{width:10em}.huge{font-size:10em;font-weight:bold;text-decoration:underline} 97 | CSS 98 | end 99 | 100 | def test_short_output_style_names 101 | engine = Engine.new(input_scss, style: :compressed) 102 | assert_equal <<-CSS, engine.render 103 | #main{color:#fff;background-color:#000}#main p{width:10em}.huge{font-size:10em;font-weight:bold;text-decoration:underline} 104 | CSS 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /test/sass_2_scss_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | module SassC 6 | class Sass2ScssTest < MiniTest::Test 7 | def test_compact_output 8 | assert_equal ".blat { color: red; }", Sass2Scss.convert(<