├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── .rubocop.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── sassc-embedded.rb └── sassc │ ├── embedded.rb │ └── embedded │ └── version.rb ├── sassc-embedded.gemspec └── test ├── custom_importer_test.rb ├── engine_test.rb ├── error_test.rb ├── fixtures └── paths.scss ├── functions_test.rb ├── output_style_test.rb ├── patches ├── bootstrap-rubygem.diff ├── sassc-rails.diff └── sprockets.diff └── test_helper.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "bundler" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "gitsubmodule" 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | with: 14 | submodules: true 15 | 16 | - name: Setup ruby 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: ruby 20 | bundler-cache: true 21 | 22 | - name: Lint 23 | run: bundle exec rake rubocop 24 | 25 | test: 26 | 27 | runs-on: ${{ matrix.os }} 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | os: [macos-latest, ubuntu-latest, windows-latest] 33 | ruby-version: ['3.1', '3.2', '3.3', '3.4', 'jruby'] 34 | exclude: 35 | - os: windows-latest 36 | ruby-version: jruby 37 | 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v4 41 | with: 42 | submodules: true 43 | 44 | - name: Setup ruby 45 | uses: ruby/setup-ruby@v1 46 | with: 47 | ruby-version: ${{ matrix.ruby-version }} 48 | bundler-cache: true 49 | 50 | - name: Test 51 | run: bundle exec rake test 52 | 53 | test-vendor: 54 | 55 | runs-on: ${{ matrix.os }} 56 | 57 | strategy: 58 | fail-fast: false 59 | matrix: 60 | os: [macos-latest, ubuntu-latest, windows-latest] 61 | ruby-version: ['3.1', '3.2', '3.3', '3.4', 'jruby'] 62 | submodule: 63 | - vendor/github.com/rails/sprockets 64 | - vendor/github.com/sass/sassc-rails 65 | - vendor/github.com/twbs/bootstrap-rubygem 66 | exclude: 67 | - os: windows-latest 68 | ruby-version: jruby 69 | - os: windows-latest 70 | submodule: vendor/github.com/rails/sprockets 71 | - os: ubuntu-latest 72 | submodule: vendor/github.com/twbs/bootstrap-rubygem 73 | - os: windows-latest 74 | submodule: vendor/github.com/twbs/bootstrap-rubygem 75 | - ruby-version: jruby 76 | submodule: vendor/github.com/twbs/bootstrap-rubygem 77 | 78 | steps: 79 | - name: Checkout 80 | uses: actions/checkout@v4 81 | with: 82 | submodules: true 83 | 84 | - name: Setup ruby 85 | uses: ruby/setup-ruby@v1 86 | with: 87 | ruby-version: ${{ matrix.ruby-version }} 88 | bundler-cache: true 89 | 90 | - name: Test 91 | run: bundle exec rake git:submodule:test[${{matrix.submodule}}] 92 | 93 | release: 94 | 95 | if: github.event.repository.fork == false && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) 96 | 97 | needs: [lint, test, test-vendor] 98 | 99 | runs-on: ubuntu-latest 100 | 101 | steps: 102 | - name: Checkout 103 | uses: actions/checkout@v4 104 | with: 105 | fetch-depth: 0 106 | submodules: true 107 | ssh-key: ${{ secrets.DEPLOY_KEY }} 108 | 109 | - name: Setup ruby 110 | uses: ruby/setup-ruby@v1 111 | with: 112 | ruby-version: ruby 113 | 114 | - name: Release 115 | run: | 116 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 117 | git config user.name github-actions[bot] 118 | rake -f -r bundler/gem_tasks release gem_push=no 119 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | 11 | if: github.event.repository.fork == false 12 | 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | id-token: write 17 | attestations: write 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | submodules: true 24 | 25 | - name: Setup ruby 26 | uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: ruby 29 | rubygems: latest 30 | 31 | - name: Configure trusted publishing credentials 32 | uses: rubygems/configure-rubygems-credentials@v1.0.0 33 | 34 | - name: Release 35 | run: rake -f -r bundler/gem_tasks release 36 | 37 | - name: Generate artifact attestation 38 | uses: actions/attest-build-provenance@v2 39 | with: 40 | subject-path: pkg/*.gem 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | # Ignore Byebug command history file. 17 | .byebug_history 18 | 19 | ## Specific to RubyMotion: 20 | .dat* 21 | .repl_history 22 | build/ 23 | *.bridgesupport 24 | build-iPhoneOS/ 25 | build-iPhoneSimulator/ 26 | 27 | ## Specific to RubyMotion (use of CocoaPods): 28 | # 29 | # We recommend against adding the Pods directory to your .gitignore. However 30 | # you should judge for yourself, the pros and cons are mentioned at: 31 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 32 | # 33 | # vendor/Pods/ 34 | 35 | ## Documentation cache and generated files: 36 | /.yardoc/ 37 | /_yardoc/ 38 | /doc/ 39 | /rdoc/ 40 | 41 | ## Environment normalization: 42 | /.bundle/ 43 | /vendor/bundle 44 | /lib/bundler/man/ 45 | 46 | # for a library or gem, you might want to ignore these files since the code is 47 | # intended to run in multiple environments; otherwise, check them in: 48 | Gemfile.lock 49 | .ruby-version 50 | .ruby-gemset 51 | 52 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 53 | .rvmrc 54 | 55 | # Used by RuboCop. Remote config files pulled in from inherit_from directive. 56 | # .rubocop-https?--* 57 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/github.com/rails/sprockets"] 2 | path = vendor/github.com/rails/sprockets 3 | url = https://github.com/rails/sprockets.git 4 | [submodule "vendor/github.com/sass/sassc-rails"] 5 | path = vendor/github.com/sass/sassc-rails 6 | url = https://github.com/sass/sassc-rails.git 7 | [submodule "vendor/github.com/sass/sassc-ruby"] 8 | path = vendor/github.com/sass/sassc-ruby 9 | url = https://github.com/sass/sassc-ruby.git 10 | [submodule "vendor/github.com/twbs/bootstrap-rubygem"] 11 | path = vendor/github.com/twbs/bootstrap-rubygem 12 | url = https://github.com/twbs/bootstrap-rubygem.git 13 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - rubocop-minitest 3 | - rubocop-performance 4 | - rubocop-rake 5 | 6 | AllCops: 7 | Exclude: 8 | - 'vendor/**/*' 9 | 10 | TargetRubyVersion: 3.1 11 | 12 | NewCops: enable 13 | 14 | Metrics: 15 | Enabled: false 16 | 17 | Minitest/MultipleAssertions: 18 | Enabled: false 19 | 20 | Style/Documentation: 21 | Enabled: false 22 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | group :development do 8 | gem 'minitest', '~> 5.25.0' 9 | gem 'minitest-around', '~> 0.5.0' 10 | gem 'rake', '>= 10.0.0' 11 | gem 'rubocop', '~> 1.76.0' 12 | gem 'rubocop-minitest', '~> 0.38.0' 13 | gem 'rubocop-performance', '~> 1.25.0' 14 | gem 'rubocop-rake', '~> 0.7.1' 15 | end 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 なつき 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Embedded Sass Shim for SassC Ruby 2 | 3 | [![build](https://github.com/sass-contrib/sassc-embedded-shim-ruby/actions/workflows/build.yml/badge.svg)](https://github.com/sass-contrib/sassc-embedded-shim-ruby/actions/workflows/build.yml) 4 | [![gem](https://badge.fury.io/rb/sassc-embedded.svg)](https://rubygems.org/gems/sassc-embedded) 5 | 6 | Use `sass-embedded` with SassC Ruby! 7 | 8 | This library shims [`sassc`](https://github.com/sass/sassc-ruby) with the [`sass-embedded`](https://github.com/sass-contrib/sass-embedded-host-ruby) implementation. 9 | 10 | ## Which Sass implementation should I use for my Ruby project? 11 | 12 | - [`sass-embedded`](https://github.com/sass-contrib/sass-embedded-host-ruby) is recommended for all projects. It is compatible with: 13 | - [`dartsass-rails >=0.5.0`](https://github.com/rails/dartsass-rails) 14 | - [`haml >=6.0.9`](https://github.com/haml/haml) 15 | - [`silm >=5.2.0`](https://github.com/slim-template/slim) 16 | - [`sinatra >=3.1.0`](https://github.com/sinatra/sinatra) 17 | - [`tilt >=2.0.11`](https://github.com/jeremyevans/tilt) 18 | - [`sassc-embedded`](https://github.com/sass-contrib/sassc-embedded-shim-ruby) is recommended for existing projects still using `sassc` or `sprockets`. It is compatible with: 19 | - [`dartsass-sprockets`](https://github.com/tablecheck/dartsass-sprockets) 20 | - [`sassc`](https://github.com/sass/sassc-ruby) 21 | - [`sassc-rails`](https://github.com/sass/sassc-rails) 22 | - [`sprockets`](https://github.com/rails/sprockets) 23 | - [`sprockets-rails`](https://github.com/rails/sprockets-rails) 24 | - [`sassc`](https://github.com/sass/sassc-ruby) [has reached end-of-life](https://github.com/sass/sassc-ruby/blob/HEAD/README.md#sassc-has-reached-end-of-life). 25 | - [`sass`](https://github.com/sass/ruby-sass) [has reached end-of-life](https://sass-lang.com/blog/ruby-sass-is-unsupported/). 26 | 27 | ## Install 28 | 29 | Add this line to your application's Gemfile: 30 | 31 | ``` ruby 32 | gem 'sassc-embedded' 33 | ``` 34 | 35 | And then execute: 36 | 37 | ``` sh 38 | bundle 39 | ``` 40 | 41 | Or install it yourself as: 42 | 43 | ``` sh 44 | gem install sassc-embedded 45 | ``` 46 | 47 | ## Usage 48 | 49 | This shim utilizes `sass-embedded` to allow you to compile SCSS or SASS syntax to CSS. To compile, use a `SassC::Engine`, e.g.: 50 | 51 | ``` ruby 52 | require 'sassc-embedded' 53 | 54 | SassC::Engine.new(sass, style: :compressed).render 55 | ``` 56 | 57 | Most of the original `sassc` options are supported with no behavior difference unless noted otherwise: 58 | 59 | - `:filename` 60 | - `:quiet` 61 | - ~~`:precision`~~ - ignored 62 | - ~~`:line_comments`~~ - ignored 63 | - `:syntax` 64 | - `:source_map_embed` 65 | - `:source_map_contents` 66 | - `:omit_source_map_url` 67 | - `:source_map_file` 68 | - `:importer` 69 | - `:functions` 70 | - `:style` - ~~`:nested`~~ and ~~`:compact`~~ behave as `:expanded` 71 | - `:load_paths` 72 | 73 | See [`sassc-ruby` source code](https://github.com/sass/sassc-ruby/blob/master/lib/sassc/engine.rb) and [`libsass` documentation](https://github.com/sass/libsass/blob/master/docs/api-context.md) for details. 74 | 75 | Additional `sass-embedded` options are supported: 76 | 77 | - `:charset` 78 | - `:importers` 79 | - `:alert_ascii` 80 | - `:alert_color` 81 | - `:fatal_deprecations` 82 | - `:future_deprecations` 83 | - `:logger` 84 | - `:quiet_deps` 85 | - `:silence_deprecations` 86 | - `:verbose` 87 | 88 | See [`sass-embedded` documentation](https://rubydoc.info/gems/sass-embedded/Sass#compile_string-class_method) for details. 89 | 90 | ## Troubleshooting 91 | 92 | ### The original `sassc` gem is still being used instead of `sassc-embedded` 93 | 94 | When launching an application via `bundle exec`, it puts `sassc-embedded` at higher priority than `sassc` in `$LOAD_PATH`. You can verify the order of `$LOAD_PATH` with the following command: 95 | 96 | ``` ruby 97 | bundle exec ruby -e 'puts $LOAD_PATH' 98 | ``` 99 | 100 | If you see `sassc` has higher priority than `sassc-embedded`, try remove `sassc`: 101 | 102 | ``` 103 | bundle remove sassc 104 | ``` 105 | 106 | If your application has a transitive dependency on `sassc` that cannot be removed, you can use one of the following workarounds. 107 | 108 | #### Workaround One 109 | 110 | Add this line to your application's Gemfile: 111 | 112 | ``` ruby 113 | gem 'sassc', github: 'sass/sassc-ruby', ref: 'refs/pull/233/head' 114 | ``` 115 | 116 | And then execute: 117 | 118 | ``` sh 119 | bundle 120 | ``` 121 | 122 | The fork of `sassc` at https://github.com/sass/sassc-ruby/pull/233 will load the shim whenever `require 'sassc'` is invoked, meaning no other code changes needed in your application. 123 | 124 | #### Workaround Two 125 | 126 | Add this line to your application's code: 127 | 128 | ``` ruby 129 | require 'sassc-embedded' 130 | ``` 131 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rake/testtask' 5 | require 'rubocop/rake_task' 6 | 7 | task default: %i[rubocop test git:submodule:test] 8 | 9 | Rake::TestTask.new do |t| 10 | t.libs << 'test' 11 | t.test_files = FileList['test/**/*_test.rb'] 12 | end 13 | 14 | RuboCop::RakeTask.new do |task| 15 | task.formatters = ['progress'] 16 | task.formatters << 'github' if ENV.key?('GITHUB_ACTIONS') 17 | end 18 | 19 | namespace :git do 20 | namespace :submodule do 21 | desc 'Init git submodule' 22 | task :init do |_, args| 23 | sh(*%w[git submodule init], *args.extras) 24 | end 25 | 26 | desc 'Update git submodule' 27 | task update: :init do |_, args| 28 | sh(*%w[git submodule update --force], *args.extras) 29 | end 30 | 31 | desc 'Deinit git submodule' 32 | task :deinit do |_, args| 33 | sh(*%w[git submodule deinit --force], *args.extras) 34 | end 35 | 36 | desc 'Test git submodule' 37 | task :test do |_, args| 38 | submodules = if args.extras.empty? 39 | %w[ 40 | vendor/github.com/rails/sprockets 41 | vendor/github.com/sass/sassc-rails 42 | vendor/github.com/twbs/bootstrap-rubygem 43 | ] 44 | else 45 | args.extras 46 | end 47 | Rake::Task['git:submodule:update'].invoke(*submodules) 48 | submodules.each do |submodule| 49 | patch = File.absolute_path("test/patches/#{File.basename(submodule)}.diff", __dir__) 50 | sh(*%w[git apply], patch, chdir: submodule) if File.exist?(patch) 51 | case submodule 52 | when 'vendor/github.com/rails/sprockets' 53 | Bundler.with_original_env do 54 | sh(*%w[bundle install], chdir: submodule) 55 | sh(*%w[bundle exec rake test TEST=test/test_sassc.rb], chdir: submodule) 56 | end 57 | when 'vendor/github.com/sass/sassc-rails' 58 | Bundler.with_original_env do 59 | gemfiles = %w[ 60 | Gemfile 61 | gemfiles/rails_6_0.gemfile 62 | gemfiles/sprockets_4_0.gemfile 63 | gemfiles/sprockets-rails_3_0.gemfile 64 | ] 65 | gemfiles.each do |gemfile| 66 | env = { 'BUNDLE_GEMFILE' => gemfile, 'MT_COMPAT' => 'true', 'RUBYOPT' => '-rlogger' } 67 | sh(env, *%w[bundle install], chdir: submodule) 68 | sh(env, *%w[bundle exec rake test], chdir: submodule) 69 | end 70 | end 71 | when 'vendor/github.com/twbs/bootstrap-rubygem' 72 | Bundler.with_original_env do 73 | gemfiles = %w[ 74 | test/gemfiles/rails_6_0.gemfile 75 | test/gemfiles/rails_6_1.gemfile 76 | test/gemfiles/rails_7_0_sassc.gemfile 77 | test/gemfiles/rails_7_0_dartsass.gemfile 78 | ] 79 | gemfiles.each do |gemfile| 80 | env = { 'BUNDLE_GEMFILE' => gemfile, 'RUBYOPT' => '-rlogger' } 81 | sh(env, *%w[bundle install], chdir: submodule) 82 | sh(env, *%w[bundle exec rake], chdir: submodule) 83 | end 84 | end 85 | end 86 | end 87 | Rake::Task['git:submodule:deinit'].invoke(*submodules) 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/sassc-embedded.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require_relative 'sassc/embedded' 5 | -------------------------------------------------------------------------------- /lib/sassc/embedded.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'sassc' 4 | require 'sass-embedded' 5 | 6 | require 'json' 7 | require 'uri' 8 | 9 | require_relative 'embedded/version' 10 | 11 | module SassC 12 | class Engine 13 | remove_method(:render) if public_method_defined?(:render, false) 14 | 15 | def render 16 | result = ::Sass.compile_string( 17 | @template, 18 | importer: (NoopImporter unless @options[:importer].nil?), 19 | load_paths:, 20 | syntax:, 21 | url: file_url, 22 | 23 | charset: @options.fetch(:charset, true), 24 | source_map: source_map_embed? || !source_map_file.nil?, 25 | source_map_include_sources: source_map_contents?, 26 | style: output_style, 27 | 28 | functions: functions_handler.setup(nil, functions: @functions), 29 | importers: import_handler.setup(nil).concat(@options.fetch(:importers, [])), 30 | 31 | alert_ascii: @options.fetch(:alert_ascii, false), 32 | alert_color: @options.fetch(:alert_color, nil), 33 | fatal_deprecations: @options.fetch(:fatal_deprecations, []), 34 | future_deprecations: @options.fetch(:future_deprecations, []), 35 | logger: quiet? ? ::Sass::Logger.silent : @options.fetch(:logger, nil), 36 | quiet_deps: @options.fetch(:quiet_deps, false), 37 | silence_deprecations: @options.fetch(:silence_deprecations, []), 38 | verbose: @options.fetch(:verbose, false) 39 | ) 40 | 41 | @loaded_urls = result.loaded_urls 42 | @source_map = result.source_map 43 | 44 | css = result.css.encode(@template.encoding) 45 | css << "\n" unless css.empty? 46 | unless @source_map.nil? || omit_source_map_url? 47 | source_mapping_url = if source_map_embed? 48 | "data:application/json;base64,#{[@source_map].pack('m0')}" 49 | else 50 | Uri.file_urls_to_relative_url(source_map_file_url, file_url) 51 | end 52 | css << "\n/*# sourceMappingURL=#{source_mapping_url} */" 53 | end 54 | css 55 | rescue ::Sass::CompileError => e 56 | @loaded_urls = e.loaded_urls 57 | 58 | line = e.span&.start&.line 59 | line += 1 unless line.nil? 60 | url = e.span&.url 61 | path = (Uri.file_urls_to_relative_path(url, Uri.path_to_file_url("#{Dir.pwd}/")) if url&.start_with?('file:')) 62 | raise SyntaxError.new(e.full_message, filename: path, line:) 63 | end 64 | 65 | remove_method(:dependencies) if public_method_defined?(:dependencies, false) 66 | 67 | def dependencies 68 | raise NotRenderedError unless @loaded_urls 69 | 70 | Dependency.from_filenames(@loaded_urls.filter_map do |url| 71 | Uri.file_url_to_path(url) if url.start_with?('file:') && !url.include?('?') && url != file_url 72 | end) 73 | end 74 | 75 | remove_method(:source_map) if public_method_defined?(:source_map, false) 76 | 77 | def source_map 78 | raise NotRenderedError unless @source_map 79 | 80 | url = Uri.parse(source_map_file_url || file_url) 81 | data = JSON.parse(@source_map) 82 | data['sources'].map! do |source| 83 | if source.start_with?('file:') 84 | Uri.file_urls_to_relative_url(source, url) 85 | else 86 | source 87 | end 88 | end 89 | 90 | JSON.fast_generate(data).encode!(@template.encoding) 91 | end 92 | 93 | private 94 | 95 | def file_url 96 | @file_url ||= Uri.path_to_file_url(File.absolute_path(filename || 'stdin')) 97 | end 98 | 99 | def source_map_file_url 100 | @source_map_file_url ||= if source_map_file 101 | Uri.path_to_file_url(File.absolute_path(source_map_file)) 102 | .gsub('%3F', '?') # https://github.com/sass-contrib/sassc-embedded-shim-ruby/pull/69 103 | end 104 | end 105 | 106 | remove_method(:output_style) if private_method_defined?(:output_style, false) 107 | 108 | def output_style 109 | @output_style ||= case @options.fetch(:style, :nested).to_sym 110 | when :nested, :expanded, :compact, :sass_style_nested, :sass_style_expanded, :sass_style_compact 111 | :expanded 112 | when :compressed, :sass_style_compressed 113 | :compressed 114 | else 115 | raise InvalidStyleError 116 | end 117 | end 118 | 119 | def syntax 120 | syntax = @options.fetch(:syntax, :scss).to_sym 121 | syntax = :indented if syntax == :sass 122 | syntax 123 | end 124 | 125 | remove_method(:load_paths) if private_method_defined?(:load_paths, false) 126 | 127 | def load_paths 128 | @load_paths ||= (@options[:load_paths] || []) + SassC.load_paths 129 | end 130 | end 131 | 132 | class FunctionsHandler 133 | remove_method(:setup) if public_method_defined?(:setup, false) 134 | 135 | def setup(_native_options, functions: Script::Functions) 136 | functions_wrapper = Class.new do 137 | attr_accessor :options 138 | 139 | include functions 140 | end.new 141 | functions_wrapper.options = @options 142 | 143 | Script.custom_functions(functions:).each_with_object({}) do |custom_function, callbacks| 144 | callback = lambda do |native_argument_list| 145 | function_arguments = arguments_from_native_list(native_argument_list) 146 | begin 147 | result = functions_wrapper.send(custom_function, *function_arguments) 148 | rescue StandardError 149 | raise ::Sass::ScriptError, "Error: error in C function #{custom_function}" 150 | end 151 | to_native_value(result) 152 | rescue StandardError => e 153 | warn "[SassC::FunctionsHandler] #{e.cause.message}" 154 | raise e 155 | end 156 | 157 | callbacks[Script.formatted_function_name(custom_function, functions:)] = callback 158 | end 159 | end 160 | 161 | private 162 | 163 | remove_method(:arguments_from_native_list) if private_method_defined?(:arguments_from_native_list, false) 164 | 165 | def arguments_from_native_list(native_argument_list) 166 | native_argument_list.filter_map do |native_value| 167 | Script::ValueConversion.from_native(native_value, @options) 168 | end 169 | end 170 | 171 | remove_method(:to_native_value) if private_method_defined?(:to_native_value, false) 172 | 173 | def to_native_value(sass_value) 174 | Script::ValueConversion.to_native(sass_value) 175 | end 176 | end 177 | 178 | module NoopImporter 179 | module_function 180 | 181 | def canonicalize(...); end 182 | 183 | def load(...); end 184 | end 185 | 186 | private_constant :NoopImporter 187 | 188 | class ImportHandler 189 | remove_method(:setup) if public_method_defined?(:setup, false) 190 | 191 | def setup(_native_options) 192 | if @importer 193 | import_cache = ImportCache.new(@importer) 194 | [Importer.new(import_cache), FileImporter.new(import_cache)] 195 | else 196 | [] 197 | end 198 | end 199 | 200 | class Importer 201 | def initialize(import_cache) 202 | @import_cache = import_cache 203 | end 204 | 205 | def canonicalize(...) 206 | @import_cache.canonicalize(...) 207 | end 208 | 209 | def load(...) 210 | @import_cache.load(...) 211 | end 212 | end 213 | 214 | private_constant :Importer 215 | 216 | class FileImporter 217 | def initialize(import_cache) 218 | @import_cache = import_cache 219 | end 220 | 221 | def find_file_url(...) 222 | @import_cache.find_file_url(...) 223 | end 224 | end 225 | 226 | private_constant :FileImporter 227 | 228 | module FileSystemImporter 229 | class << self 230 | def resolve_path(path, from_import) 231 | ext = File.extname(path) 232 | if ['.sass', '.scss', '.css'].include?(ext) 233 | if from_import 234 | result = exactly_one(try_path("#{without_ext(path)}.import#{ext}")) 235 | return result unless result.nil? 236 | end 237 | return exactly_one(try_path(path)) 238 | end 239 | 240 | if from_import 241 | result = exactly_one(try_path_with_ext("#{path}.import")) 242 | return result unless result.nil? 243 | end 244 | 245 | result = exactly_one(try_path_with_ext(path)) 246 | return result unless result.nil? 247 | 248 | try_path_as_dir(path, from_import) 249 | end 250 | 251 | private 252 | 253 | def try_path_with_ext(path) 254 | result = try_path("#{path}.sass") + try_path("#{path}.scss") 255 | result.empty? ? try_path("#{path}.css") : result 256 | end 257 | 258 | def try_path(path) 259 | partial = File.join(File.dirname(path), "_#{File.basename(path)}") 260 | result = [] 261 | result.push(partial) if file_exist?(partial) 262 | result.push(path) if file_exist?(path) 263 | result 264 | end 265 | 266 | def try_path_as_dir(path, from_import) 267 | return unless dir_exist?(path) 268 | 269 | if from_import 270 | result = exactly_one(try_path_with_ext(File.join(path, 'index.import'))) 271 | return result unless result.nil? 272 | end 273 | 274 | exactly_one(try_path_with_ext(File.join(path, 'index'))) 275 | end 276 | 277 | def exactly_one(paths) 278 | return if paths.empty? 279 | return paths.first if paths.one? 280 | 281 | raise "It's not clear which file to import. Found:\n#{paths.map { |path| " #{path}" }.join("\n")}" 282 | end 283 | 284 | def file_exist?(path) 285 | File.exist?(path) && File.file?(path) 286 | end 287 | 288 | def dir_exist?(path) 289 | File.exist?(path) && File.directory?(path) 290 | end 291 | 292 | def without_ext(path) 293 | ext = File.extname(path) 294 | path.delete_suffix(ext) 295 | end 296 | end 297 | end 298 | 299 | private_constant :FileSystemImporter 300 | 301 | class ImportCache 302 | def initialize(importer) 303 | @importer = importer 304 | @importer_results = {} 305 | @importer_result = nil 306 | @file_url = nil 307 | end 308 | 309 | def canonicalize(url, context) 310 | return unless context.containing_url&.start_with?('file:') 311 | 312 | containing_url = context.containing_url 313 | 314 | path = Uri.decode_uri_component(url) 315 | parent_path = Uri.file_url_to_path(containing_url) 316 | parent_dir = File.dirname(parent_path) 317 | 318 | if containing_url.include?('?') 319 | canonical_url = Uri.path_to_file_url(File.absolute_path(path, parent_dir)) 320 | unless @importer_results.key?(canonical_url) 321 | @file_url = resolve_file_url(path, parent_dir, context.from_import) 322 | return 323 | end 324 | else 325 | imports = [*@importer.imports(path, parent_path)] 326 | canonical_url = imports_to_native(imports, parent_dir, context.from_import, url, containing_url) 327 | unless @importer_results.key?(canonical_url) 328 | @file_url = canonical_url 329 | return 330 | end 331 | end 332 | 333 | @importer_result = @importer_results.delete(canonical_url) 334 | canonical_url 335 | end 336 | 337 | def load(_canonical_url) 338 | importer_result = @importer_result 339 | @importer_result = nil 340 | importer_result 341 | end 342 | 343 | def find_file_url(_url, context) 344 | return if context.containing_url.nil? || @file_url.nil? 345 | 346 | canonical_url = @file_url 347 | @file_url = nil 348 | canonical_url 349 | end 350 | 351 | private 352 | 353 | def resolve_file_url(path, parent_dir, from_import) 354 | resolved = FileSystemImporter.resolve_path(File.absolute_path(path, parent_dir), from_import) 355 | Uri.path_to_file_url(resolved) unless resolved.nil? 356 | end 357 | 358 | def syntax(path) 359 | case File.extname(path) 360 | when '.sass' 361 | :indented 362 | when '.css' 363 | :css 364 | else 365 | :scss 366 | end 367 | end 368 | 369 | def import_to_native(import, parent_dir, from_import, canonicalize) 370 | if import.source 371 | canonical_url = Uri.path_to_file_url(File.absolute_path(import.path, parent_dir)) 372 | @importer_results[canonical_url] = if import.source.is_a?(Hash) 373 | { 374 | contents: import.source[:contents], 375 | syntax: import.source[:syntax], 376 | source_map_url: canonical_url 377 | } 378 | else 379 | { 380 | contents: import.source, 381 | syntax: syntax(import.path), 382 | source_map_url: canonical_url 383 | } 384 | end 385 | return canonical_url if canonicalize 386 | elsif canonicalize 387 | return resolve_file_url(import.path, parent_dir, from_import) 388 | end 389 | 390 | Uri.encode_uri_path_component(import.path) 391 | end 392 | 393 | def imports_to_native(imports, parent_dir, from_import, url, containing_url) 394 | return import_to_native(imports.first, parent_dir, from_import, true) if imports.one? 395 | 396 | canonical_url = "#{containing_url}?url=#{Uri.encode_uri_query_component(url)}&from_import=#{from_import}" 397 | @importer_results[canonical_url] = { 398 | contents: imports.flat_map do |import| 399 | at_rule = from_import ? '@import' : '@forward' 400 | url = import_to_native(import, parent_dir, from_import, false) 401 | "#{at_rule} #{Script::Value::String.quote(url)};" 402 | end.join("\n"), 403 | syntax: :scss 404 | } 405 | 406 | canonical_url 407 | end 408 | end 409 | 410 | private_constant :ImportCache 411 | end 412 | 413 | class Sass2Scss 414 | class << self 415 | remove_method(:convert) if public_method_defined?(:convert, false) 416 | end 417 | 418 | def self.convert(sass) 419 | { 420 | contents: sass, 421 | syntax: :indented 422 | } 423 | end 424 | end 425 | 426 | module Script 427 | class Value 428 | class String 429 | class << self 430 | remove_method(:quote) if public_method_defined?(:quote, false) 431 | end 432 | 433 | # Returns the quoted string representation of `contents`. 434 | # 435 | # @options opts :quote [String] 436 | # The preferred quote style for quoted strings. If `:none`, strings are 437 | # always emitted unquoted. If `nil`, quoting is determined automatically. 438 | # @options opts :sass [String] 439 | # Whether to quote strings for Sass source, as opposed to CSS. Defaults to `false`. 440 | def self.quote(contents, opts = {}) 441 | contents = ::Sass::Value::String.new(contents, quoted: opts[:quote] != :none).to_s 442 | opts[:sass] ? contents.gsub('#', '\#') : contents 443 | end 444 | 445 | remove_method(:to_s) if public_method_defined?(:to_s, false) 446 | 447 | def to_s(opts = {}) 448 | opts = { quote: :none }.merge!(opts) if @type == :identifier 449 | self.class.quote(@value, opts) 450 | end 451 | end 452 | end 453 | 454 | module ValueConversion 455 | class << self 456 | remove_method(:from_native) if public_method_defined?(:from_native, false) 457 | end 458 | 459 | def self.from_native(value, options) 460 | case value 461 | when ::Sass::Value::Null::NULL 462 | nil 463 | when ::Sass::Value::Boolean 464 | ::SassC::Script::Value::Bool.new(value.to_bool) 465 | when ::Sass::Value::Color 466 | case value.space 467 | when 'hsl', 'hwb' 468 | value = value.to_space('hsl') 469 | ::SassC::Script::Value::Color.new( 470 | hue: value.channel('hue'), 471 | saturation: value.channel('saturation'), 472 | lightness: value.channel('lightness'), 473 | alpha: value.alpha 474 | ) 475 | else 476 | value = value.to_space('rgb') 477 | ::SassC::Script::Value::Color.new( 478 | red: value.channel('red'), 479 | green: value.channel('green'), 480 | blue: value.channel('blue'), 481 | alpha: value.alpha 482 | ) 483 | end 484 | when ::Sass::Value::List 485 | ::SassC::Script::Value::List.new( 486 | value.to_a.map { |element| from_native(element, options) }, 487 | separator: case value.separator 488 | when ',' 489 | :comma 490 | when ' ' 491 | :space 492 | else 493 | raise UnsupportedValue, "Sass list separator #{value.separator} unsupported" 494 | end, 495 | bracketed: value.bracketed? 496 | ) 497 | when ::Sass::Value::Map 498 | ::SassC::Script::Value::Map.new( 499 | value.contents.each_with_object({}) { |(k, v), h| h[from_native(k, options)] = from_native(v, options) } 500 | ) 501 | when ::Sass::Value::Number 502 | ::SassC::Script::Value::Number.new( 503 | value.value, 504 | value.numerator_units, 505 | value.denominator_units 506 | ) 507 | when ::Sass::Value::String 508 | ::SassC::Script::Value::String.new( 509 | value.text, 510 | value.quoted? ? :string : :identifier 511 | ) 512 | else 513 | raise UnsupportedValue, "Sass argument of type #{value.class.name.split('::').last} unsupported" 514 | end 515 | end 516 | 517 | class << self 518 | remove_method(:to_native) if public_method_defined?(:to_native, false) 519 | end 520 | 521 | def self.to_native(value) 522 | case value 523 | when nil 524 | ::Sass::Value::Null::NULL 525 | when ::SassC::Script::Value::Bool 526 | ::Sass::Value::Boolean.new(value.to_bool) 527 | when ::SassC::Script::Value::Color 528 | if value.rgba? 529 | ::Sass::Value::Color.new( 530 | red: value.red, 531 | green: value.green, 532 | blue: value.blue, 533 | alpha: value.alpha, 534 | space: 'rgb' 535 | ) 536 | elsif value.hlsa? 537 | ::Sass::Value::Color.new( 538 | hue: value.hue, 539 | saturation: value.saturation, 540 | lightness: value.lightness, 541 | alpha: value.alpha, 542 | space: 'hsl' 543 | ) 544 | else 545 | raise UnsupportedValue, "Sass color mode #{value.instance_eval { @mode }} unsupported" 546 | end 547 | when ::SassC::Script::Value::List 548 | ::Sass::Value::List.new( 549 | value.to_a.map { |element| to_native(element) }, 550 | separator: case value.separator 551 | when :comma 552 | ',' 553 | when :space 554 | ' ' 555 | else 556 | raise UnsupportedValue, "Sass list separator #{value.separator} unsupported" 557 | end, 558 | bracketed: value.bracketed 559 | ) 560 | when ::SassC::Script::Value::Map 561 | ::Sass::Value::Map.new( 562 | value.value.each_with_object({}) { |(k, v), h| h[to_native(k)] = to_native(v) } 563 | ) 564 | when ::SassC::Script::Value::Number 565 | ::Sass::Value::Number.new( 566 | value.value, { 567 | numerator_units: value.numerator_units, 568 | denominator_units: value.denominator_units 569 | } 570 | ) 571 | when ::SassC::Script::Value::String 572 | ::Sass::Value::String.new( 573 | value.value, 574 | quoted: value.type != :identifier 575 | ) 576 | else 577 | raise UnsupportedValue, "Sass return type #{value.class.name.split('::').last} unsupported" 578 | end 579 | end 580 | end 581 | end 582 | 583 | module Uri 584 | module_function 585 | 586 | def parse(...) 587 | ::URI::RFC3986_PARSER.parse(...) 588 | end 589 | 590 | encode_uri_hash = {} 591 | decode_uri_hash = {} 592 | 256.times do |i| 593 | c = -[i].pack('C') 594 | h = c.unpack1('H') 595 | l = c.unpack1('h') 596 | pdd = -"%#{h}#{l}" 597 | pdu = -"%#{h}#{l.upcase}" 598 | pud = -"%#{h.upcase}#{l}" 599 | puu = -pdd.upcase 600 | encode_uri_hash[c] = puu 601 | decode_uri_hash[pdd] = c 602 | decode_uri_hash[pdu] = c 603 | decode_uri_hash[pud] = c 604 | decode_uri_hash[puu] = c 605 | end.freeze 606 | encode_uri_hash.freeze 607 | decode_uri_hash.freeze 608 | 609 | { 610 | uri_path_component: "!$&'()*+,;=:/@", 611 | uri_query_component: "!$&'()*+,;=:/?@", 612 | uri_component: nil, 613 | uri: "!$&'()*+,;=:/?#[]@" 614 | } 615 | .each do |symbol, unescaped| 616 | encode_regexp = Regexp.new("[^0-9A-Za-z#{Regexp.escape("-._~#{unescaped}")}]", Regexp::NOENCODING) 617 | 618 | define_method(:"encode_#{symbol}") do |str| 619 | str.b.gsub(encode_regexp, encode_uri_hash).force_encoding(str.encoding) 620 | end 621 | 622 | next if symbol.match?(/_.+_/o) 623 | 624 | decode_regexp = /%[0-9A-Fa-f]{2}/o 625 | decode_uri_hash_with_preserve_escaped = if unescaped.nil? || unescaped.empty? 626 | decode_uri_hash 627 | else 628 | decode_uri_hash.each_with_object({}) do |(key, value), hash| 629 | hash[key] = unescaped.include?(value) ? key : value 630 | end.freeze 631 | end 632 | 633 | define_method(:"decode_#{symbol}") do |str| 634 | str.gsub(decode_regexp, decode_uri_hash_with_preserve_escaped).force_encoding(str.encoding) 635 | end 636 | end 637 | 638 | def file_urls_to_relative_url(url, from_url) 639 | parse(url).route_from(from_url).to_s 640 | end 641 | 642 | def file_urls_to_relative_path(url, from_url) 643 | decode_uri_component(file_urls_to_relative_url(url, from_url)) 644 | end 645 | 646 | def file_url_to_path(url) 647 | path = decode_uri_component(parse(url).path) 648 | if path.start_with?('/') 649 | windows_path = path[1..] 650 | path = windows_path if File.absolute_path?(windows_path) 651 | end 652 | path 653 | end 654 | 655 | def path_to_file_url(path) 656 | path = "/#{path}" unless path.start_with?('/') 657 | 658 | "file://#{encode_uri_path_component(path)}" 659 | end 660 | end 661 | 662 | private_constant :Uri 663 | end 664 | -------------------------------------------------------------------------------- /lib/sassc/embedded/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SassC 4 | module Embedded 5 | VERSION = '1.80.4' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /sassc-embedded.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/sassc/embedded/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'sassc-embedded' 7 | spec.version = SassC::Embedded::VERSION 8 | spec.authors = ['なつき'] 9 | spec.email = ['i@ntk.me'] 10 | spec.summary = 'Use dart-sass with SassC!' 11 | spec.description = 'An embedded sass shim for SassC.' 12 | spec.homepage = 'https://github.com/sass-contrib/sassc-embedded-shim-ruby' 13 | spec.license = 'MIT' 14 | spec.metadata = { 15 | 'bug_tracker_uri' => "#{spec.homepage}/issues", 16 | 'documentation_uri' => 'https://rubydoc.info/gems/sassc', 17 | 'source_code_uri' => "#{spec.homepage}/tree/v#{spec.version}", 18 | 'funding_uri' => 'https://github.com/sponsors/ntkme', 19 | 'rubygems_mfa_required' => 'true' 20 | } 21 | 22 | spec.files = Dir['lib/**/*.rb', 'vendor/github.com/sass/sassc-ruby/lib/**/*.rb'] + [ 23 | 'LICENSE', 24 | 'README.md', 25 | 'vendor/github.com/sass/sassc-ruby/LICENSE.txt' 26 | ] 27 | 28 | spec.require_paths = ['lib', 'vendor/github.com/sass/sassc-ruby/lib'] 29 | 30 | spec.required_ruby_version = '>= 3.1' 31 | 32 | spec.add_dependency 'sass-embedded', '~> 1.80' 33 | end 34 | -------------------------------------------------------------------------------- /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.include?('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: ".#{File.basename(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 | temp_file('スタイル.scss', '.test { color: $var1; }') 44 | 45 | data = <<~SCSS 46 | @import "styles"; 47 | @import "fonts"; 48 | @import "スタイル"; 49 | SCSS 50 | 51 | engine = Engine.new(data, { 52 | importer: CustomImporter 53 | }) 54 | 55 | assert_equal <<~CSS, engine.render 56 | .hi { 57 | color: #000; 58 | } 59 | 60 | .font { 61 | color: #000; 62 | } 63 | 64 | .test { 65 | color: #000; 66 | } 67 | CSS 68 | end 69 | 70 | def test_custom_importer_works_for_file_in_parent_dir 71 | temp_dir('sub') 72 | temp_file('a.scss', 'a {b: c}') 73 | temp_file('sub/b.scss', '@import "../a"') 74 | 75 | data = <<~SCSS 76 | @import "sub/b.scss"; 77 | SCSS 78 | 79 | engine = Engine.new(data, { 80 | importer: CustomImporter 81 | }) 82 | 83 | assert_equal <<~CSS, engine.render 84 | a { 85 | b: c; 86 | } 87 | CSS 88 | end 89 | 90 | def test_dependency_list 91 | base = Dir.pwd 92 | 93 | temp_dir('fonts') 94 | temp_dir('fonts/sub') 95 | temp_file('fonts/sub/sub_fonts.scss', '$font: arial;') 96 | temp_file('styles2.scss', '.hi { color: $var1; }') 97 | temp_file 'fonts/fonts.scss', <<~SCSS 98 | @import "sub/sub_fonts"; 99 | .font { font-familiy: $font; color: $var1; } 100 | SCSS 101 | 102 | data = <<~SCSS 103 | @import "styles"; 104 | @import "fonts"; 105 | SCSS 106 | 107 | engine = Engine.new(data, { 108 | importer: CustomImporter, 109 | load_paths: ['fonts'] 110 | }) 111 | engine.render 112 | 113 | dependencies = engine.dependencies.map(&:filename).map { |f| f.gsub(base, '') } 114 | 115 | assert_equal [ 116 | '/styles1.scss', 117 | '/styles2.scss', 118 | '/fonts/fonts.scss', 119 | '/fonts/sub/sub_fonts.scss' 120 | ], dependencies 121 | end 122 | 123 | def test_custom_importer_works_with_no_files 124 | engine = Engine.new("@import 'fake.scss';", { 125 | importer: NoFilesImporter 126 | }) 127 | 128 | assert_equal '', engine.render 129 | end 130 | 131 | def test_custom_importer_can_access_sassc_options 132 | engine = Engine.new("@import 'fake.scss';", { 133 | importer: OptionsImporter, 134 | custom_option_source: '.test { width: 30px; }' 135 | }) 136 | 137 | assert_equal <<~CSS, engine.render 138 | .test { 139 | width: 30px; 140 | } 141 | CSS 142 | end 143 | 144 | def test_parent_path_is_accessible 145 | engine = Engine.new("@import 'parent.scss';", { 146 | importer: ParentImporter, 147 | filename: 'import-parent-filename.scss' 148 | }) 149 | 150 | assert_equal <<~CSS, engine.render 151 | .import-parent-filename.scss { 152 | color: red; 153 | } 154 | CSS 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /test/engine_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'test_helper' 4 | 5 | module SassC 6 | class EngineTest < Minitest::Test 7 | include TempFileTest 8 | 9 | def render(data) 10 | Engine.new(data).render 11 | end 12 | 13 | def test_line_comments 14 | template = <<~SCSS 15 | .foo { 16 | baz: bang; } 17 | SCSS 18 | expected_output = <<~CSS 19 | .foo { 20 | baz: bang; 21 | } 22 | CSS 23 | output = Engine.new(template, line_comments: true).render 24 | 25 | assert_equal expected_output, output 26 | end 27 | 28 | def test_one_line_comments 29 | assert_equal <<~CSS, render(<<~SCSS) 30 | .foo { 31 | baz: bang; 32 | } 33 | CSS 34 | .foo {// bar: baz;} 35 | baz: bang; //} 36 | } 37 | SCSS 38 | assert_equal <<~CSS, render(<<~SCSS) 39 | .foo bar[val="//"] { 40 | baz: bang; 41 | } 42 | CSS 43 | .foo bar[val="//"] { 44 | baz: bang; //} 45 | } 46 | SCSS 47 | end 48 | 49 | def test_variables 50 | assert_equal <<~CSS, render(<<~SCSS) 51 | blat { 52 | a: foo; 53 | } 54 | CSS 55 | $var: foo; 56 | 57 | blat {a: $var} 58 | SCSS 59 | 60 | assert_equal <<~CSS, render(<<~SCSS) 61 | foo { 62 | a: 2; 63 | b: 6; 64 | } 65 | CSS 66 | foo { 67 | $var: 2; 68 | $another-var: 4; 69 | a: $var; 70 | b: $var + $another-var;} 71 | SCSS 72 | end 73 | 74 | def test_precision 75 | template = <<~SCSS 76 | @use 'sass:math'; 77 | $var: 1; 78 | .foo { 79 | baz: math.div($var, 3); } 80 | SCSS 81 | expected_output = <<~CSS 82 | .foo { 83 | baz: 0.3333333333; 84 | } 85 | CSS 86 | output = Engine.new(template, precision: 8).render 87 | 88 | assert_equal expected_output, output 89 | end 90 | 91 | def test_precision_not_specified 92 | template = <<~SCSS 93 | @use 'sass:math'; 94 | $var: 1; 95 | .foo { 96 | baz: math.div($var, 3); } 97 | SCSS 98 | expected_output = <<~CSS 99 | .foo { 100 | baz: 0.3333333333; 101 | } 102 | CSS 103 | output = Engine.new(template).render 104 | 105 | assert_equal expected_output, output 106 | end 107 | 108 | def test_dependency_filenames_are_reported 109 | base = Dir.pwd 110 | 111 | temp_file('not_included.scss', '$size: 30px;') 112 | temp_file('import_parent.scss', '$size: 30px;') 113 | temp_file('import.scss', "@import 'import_parent'; $size: 30px;") 114 | temp_file('styles.scss', "@import 'import.scss'; .hi { width: $size; }") 115 | 116 | engine = Engine.new(File.read('styles.scss')) 117 | engine.render 118 | deps = engine.dependencies 119 | 120 | expected = ['/import.scss', '/import_parent.scss'] 121 | 122 | assert_equal expected, deps.map { |dep| dep.options[:filename].gsub(base, '') }.sort 123 | assert_equal expected, deps.map { |dep| dep.filename.gsub(base, '') }.sort 124 | end 125 | 126 | def test_no_dependencies 127 | engine = Engine.new('$size: 30px;') 128 | engine.render 129 | deps = engine.dependencies 130 | 131 | assert_empty deps 132 | end 133 | 134 | def test_not_rendered_error 135 | engine = Engine.new('$size: 30px;') 136 | assert_raises(NotRenderedError) { engine.dependencies } 137 | end 138 | 139 | def test_source_map 140 | temp_dir('admin') 141 | 142 | temp_file('admin/text-color.scss', <<~SCSS) 143 | p { 144 | color: red; 145 | } 146 | SCSS 147 | temp_file('style.scss', <<~SCSS) 148 | @import 'admin/text-color'; 149 | 150 | p { 151 | padding: 20px; 152 | } 153 | SCSS 154 | engine = Engine.new(File.read('style.scss'), { 155 | source_map_file: 'style.scss.map', 156 | source_map_contents: true 157 | }) 158 | output = engine.render 159 | 160 | assert_includes(output, '/*# sourceMappingURL=style.scss.map */') 161 | assert_match(/"version":3/, engine.source_map) 162 | end 163 | 164 | def test_source_map_with_query 165 | temp_dir('admin') 166 | 167 | temp_file('admin/text-color.scss', <<~SCSS) 168 | p { 169 | color: red; 170 | } 171 | SCSS 172 | temp_file('style.scss', <<~SCSS) 173 | @import 'admin/text-color'; 174 | 175 | p { 176 | padding: 20px; 177 | } 178 | SCSS 179 | engine = Engine.new(File.read('style.scss'), { 180 | source_map_file: 'style.scss.map?__ws=hostname', 181 | source_map_contents: true 182 | }) 183 | 184 | output = engine.render 185 | 186 | assert_includes(output, '/*# sourceMappingURL=style.scss.map?__ws=hostname */') 187 | assert_match(/"version":3/, engine.source_map) 188 | end 189 | 190 | def test_no_source_map 191 | engine = Engine.new('$size: 30px;') 192 | engine.render 193 | assert_raises(NotRenderedError) { engine.source_map } 194 | end 195 | 196 | def test_omit_source_map_url 197 | temp_file('style.scss', <<~SCSS) 198 | p { 199 | padding: 20px; 200 | } 201 | SCSS 202 | engine = Engine.new(File.read('style.scss'), { 203 | source_map_file: 'style.scss.map', 204 | source_map_contents: true, 205 | omit_source_map_url: true 206 | }) 207 | output = engine.render 208 | 209 | refute_match(/sourceMappingURL/, output) 210 | end 211 | 212 | def test_load_paths 213 | temp_dir('included_1') 214 | temp_dir('included_2') 215 | 216 | temp_file('included_1/import_parent.scss', '$s: 30px;') 217 | temp_file('included_2/import.scss', "@import 'import_parent'; $size: $s;") 218 | temp_file('styles.scss', "@import 'import.scss'; .hi { width: $size; }") 219 | 220 | assert_equal ".hi {\n width: 30px;\n}\n", Engine.new( 221 | File.read('styles.scss'), 222 | load_paths: %w[included_1 included_2] 223 | ).render 224 | end 225 | 226 | def test_global_load_paths 227 | temp_dir('included_1') 228 | temp_dir('included_2') 229 | 230 | temp_file('included_1/import_parent.scss', '$s: 30px;') 231 | temp_file('included_2/import.scss', "@import 'import_parent'; $size: $s;") 232 | temp_file('styles.scss', "@import 'import.scss'; .hi { width: $size; }") 233 | 234 | ::SassC.load_paths << 'included_1' 235 | ::SassC.load_paths << 'included_2' 236 | 237 | assert_equal ".hi {\n width: 30px;\n}\n", Engine.new( 238 | File.read('styles.scss') 239 | ).render 240 | ::SassC.load_paths.clear 241 | end 242 | 243 | def test_env_load_paths 244 | expected_load_paths = %w[included_1 included_2] 245 | ::SassC.instance_eval { @load_paths = nil } 246 | ENV['SASS_PATH'] = expected_load_paths.join(File::PATH_SEPARATOR) 247 | 248 | assert_equal expected_load_paths, ::SassC.load_paths 249 | ::SassC.load_paths.clear 250 | ENV['SASS_PATH'] = nil 251 | end 252 | 253 | def test_load_paths_not_configured 254 | temp_file('included_1/import_parent.scss', '$s: 30px;') 255 | temp_file('included_2/import.scss', "@import 'import_parent'; $size: $s;") 256 | temp_file('styles.scss', "@import 'import.scss'; .hi { width: $size; }") 257 | 258 | assert_raises(SyntaxError) do 259 | Engine.new(File.read('styles.scss')).render 260 | end 261 | end 262 | 263 | def test_sass_variation 264 | sass = <<~SASS 265 | $size: 30px 266 | .foo 267 | width: $size 268 | SASS 269 | 270 | css = <<~CSS 271 | .foo { 272 | width: 30px; 273 | } 274 | CSS 275 | 276 | assert_equal css, Engine.new(sass, syntax: :sass).render 277 | assert_equal css, Engine.new(sass, syntax: 'sass').render 278 | assert_raises(SyntaxError) { Engine.new(sass).render } 279 | end 280 | 281 | def test_encoding_matches_input 282 | input = +'$size: 30px;' 283 | input.force_encoding('UTF-8') 284 | output = Engine.new(input).render 285 | 286 | assert_equal input.encoding, output.encoding 287 | end 288 | 289 | def test_inline_source_maps 290 | template = <<~SCSS 291 | .foo { 292 | baz: bang; } 293 | SCSS 294 | 295 | output = Engine.new(template, { 296 | source_map_file: '.', 297 | source_map_embed: true, 298 | source_map_contents: true 299 | }).render 300 | 301 | assert_match(/sourceMappingURL/, output) 302 | assert_match(/.foo/, output) 303 | end 304 | 305 | def test_empty_template 306 | output = Engine.new('').render 307 | 308 | assert_equal '', output 309 | end 310 | 311 | def test_empty_template_returns_a_new_object 312 | input = +'' 313 | output = Engine.new(input).render 314 | 315 | refute_same input, output, 'empty template must return a new object' 316 | end 317 | 318 | def test_empty_template_encoding_matches_input 319 | input = (+'').force_encoding('ISO-8859-1') 320 | output = Engine.new(input).render 321 | 322 | assert_equal input.encoding, output.encoding 323 | end 324 | 325 | def test_handling_of_frozen_strings 326 | output = Engine.new('body { background-color: red; }').render 327 | 328 | assert_equal("body {\n background-color: red;\n}\n", output) 329 | end 330 | 331 | def test_import_plain_css 332 | temp_file('test.css', '.something{color: red}') 333 | expected_output = <<~CSS 334 | .something { 335 | color: red; 336 | } 337 | CSS 338 | 339 | output = Engine.new("@import 'test'").render 340 | 341 | assert_equal expected_output, output 342 | end 343 | end 344 | end 345 | -------------------------------------------------------------------------------- /test/error_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'test_helper' 4 | 5 | module SassC 6 | class ErrorTest < Minitest::Test 7 | def render(data, opts = {}) 8 | Engine.new(data, opts).render 9 | end 10 | 11 | def test_first_backtrace_is_sass 12 | filename = 'app/assets/stylesheets/application.scss' 13 | 14 | begin 15 | template = <<~SCSS 16 | .foo { 17 | baz: bang; 18 | padding top: 10px; 19 | } 20 | SCSS 21 | 22 | render(template, filename:) 23 | rescue SassC::SyntaxError => e 24 | expected = "#{filename}:3" 25 | 26 | assert_equal expected, e.backtrace.first 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /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 12 | $stderr = StringIO.new 13 | end 14 | 15 | def teardown 16 | $stderr = @real_stderr 17 | end 18 | 19 | def test_functions_may_return_sass_string_type 20 | assert_sass <<-SCSS, <<-CSS 21 | div { url: url(sass_return_path("foo.svg")); } 22 | SCSS 23 | div { url: url("foo.svg"); } 24 | CSS 25 | end 26 | 27 | def test_functions_work_with_varying_quotes_and_string_types 28 | assert_sass <<-SCSS, <<-CSS 29 | div { 30 | url: url(asset-path("foo.svg")); 31 | url: url(image-path("foo.png")); 32 | url: url(video-path("foo.mov")); 33 | url: url(audio-path("foo.mp3")); 34 | url: url(font-path("foo.woff")); 35 | url: url(javascript-path('foo.js')); 36 | url: url(javascript-path("foo.js")); 37 | url: url(stylesheet-path("foo.css")); 38 | } 39 | SCSS 40 | div { 41 | url: url(asset-path("foo.svg")); 42 | url: url(image-path("foo.png")); 43 | url: url(video-path("foo.mov")); 44 | url: url(audio-path("foo.mp3")); 45 | url: url(font-path("foo.woff")); 46 | url: url("/js/foo.js"); 47 | url: url("/js/foo.js"); 48 | url: url(/css/foo.css); 49 | } 50 | CSS 51 | end 52 | 53 | def test_function_with_no_return_value 54 | assert_sass <<-SCSS, <<-CSS 55 | div {url: url(no-return-path('foo.svg'));} 56 | SCSS 57 | div { url: url(); } 58 | CSS 59 | end 60 | 61 | def test_function_that_returns_a_color 62 | assert_sass <<-SCSS, <<-CSS 63 | div { background: returns-a-color(); } 64 | SCSS 65 | div { background: black; } 66 | CSS 67 | end 68 | 69 | def test_function_that_returns_a_number 70 | assert_sass <<-SCSS, <<-CSS 71 | div { width: returns-a-number(); } 72 | SCSS 73 | div { width: -312rem; } 74 | CSS 75 | end 76 | 77 | def test_function_that_takes_a_number 78 | assert_sass <<-SCSS, <<-CSS 79 | div { display: inspect-number(42.1px); } 80 | SCSS 81 | div { display: 42.1px; } 82 | CSS 83 | end 84 | 85 | def test_function_that_returns_a_bool 86 | assert_sass <<-SCSS, <<-CSS 87 | div { width: returns-a-bool(); } 88 | SCSS 89 | div { width: true; } 90 | CSS 91 | end 92 | 93 | def test_function_that_takes_a_bool 94 | assert_sass <<-SCSS, <<-CSS 95 | div { display: inspect-bool(true)} 96 | SCSS 97 | div { display: true; } 98 | CSS 99 | end 100 | 101 | def test_function_with_optional_arguments 102 | assert_sass <<-SCSS, <<-EXPECTED_CSS 103 | div { 104 | url: optional_arguments('first'); 105 | url: optional_arguments('second', 'qux'); 106 | } 107 | SCSS 108 | div { 109 | url: "first/bar"; 110 | url: "second/qux"; 111 | } 112 | EXPECTED_CSS 113 | end 114 | 115 | def test_functions_may_accept_sass_color_type 116 | assert_sass <<-SCSS, <<-EXPECTED_CSS 117 | div { color: nice_color_argument(red); } 118 | SCSS 119 | div { color: rgb(255, 0, 0); } 120 | EXPECTED_CSS 121 | end 122 | 123 | def test_function_with_error 124 | engine = Engine.new('div {url: function_that_raises_errors();}') 125 | 126 | exception = assert_raises(SassC::SyntaxError) do 127 | engine.render 128 | end 129 | 130 | assert_match(/Error: error in C function function_that_raises_errors/, exception.message) 131 | assert_match(/Intentional wrong thing happened somewhere inside the custom function/, exception.message) 132 | assert_match(/\[SassC::FunctionsHandler\] Intentional wrong thing happened somewhere inside the custom function/, 133 | stderr_output) 134 | end 135 | 136 | def test_function_that_returns_a_sass_value 137 | assert_sass <<-SCSS, <<-CSS 138 | div { background: returns-sass-value(); } 139 | SCSS 140 | div { background: black; } 141 | CSS 142 | end 143 | 144 | def test_function_that_returns_a_sass_map 145 | assert_sass <<-SCSS, <<-CSS 146 | $my-map: returns-sass-map(); 147 | div { background: map-get( $my-map, color ); } 148 | SCSS 149 | div { background: black; } 150 | CSS 151 | end 152 | 153 | def test_function_that_takes_a_sass_map 154 | assert_sass <<-SCSS, <<-CSS 155 | div { background-color: map-get( inspect-map(( color: black, number: 1.23px, string: "abc", map: ( x: 'y' ))), color ); } 156 | SCSS 157 | div { background-color: black; } 158 | CSS 159 | end 160 | 161 | def test_function_that_returns_a_sass_list 162 | assert_sass <<-SCSS, <<-CSS 163 | $my-list: returns-sass-list(); 164 | div { width: nth( $my-list, 2 ); } 165 | SCSS 166 | div { width: 20; } 167 | CSS 168 | end 169 | 170 | def test_function_that_takes_a_sass_list 171 | assert_sass <<-SCSS, <<-CSS 172 | div { width: nth(inspect-list((10 20 30)), 2); } 173 | SCSS 174 | div { width: 20; } 175 | CSS 176 | end 177 | 178 | def test_concurrency 179 | 10.times do 180 | threads = [] 181 | 10.times do |i| 182 | threads << Thread.new(i) do |id| 183 | out = Engine.new('div { url: inspect_options(); }', { test_key1: 'test_value', test_key2: id }).render 184 | 185 | assert_match(/test_key1/, out) 186 | assert_match(/test_key2/, out) 187 | assert_match(/test_value/, out) 188 | assert_match(/#{id}/, out) 189 | end 190 | end 191 | threads.each(&:join) 192 | end 193 | end 194 | 195 | def test_pass_custom_functions_as_a_parameter 196 | out = Engine.new('div { url: test-function(); }', { functions: ExternalFunctions }).render 197 | 198 | assert_match(/custom_function/, out) 199 | end 200 | 201 | def test_pass_incompatible_type_to_custom_functions 202 | assert_raises(TypeError) do 203 | Engine.new('div { url: test-function(); }', { functions: Class.new }).render 204 | end 205 | end 206 | 207 | private 208 | 209 | def assert_sass(sass, expected_css) 210 | engine = Engine.new(sass) 211 | 212 | assert_equal expected_css.strip.gsub!(/\s+/, ' '), # poor man's String#squish 213 | engine.render.strip.gsub!(/\s+/, ' ') 214 | end 215 | 216 | def stderr_output 217 | $stderr.string.gsub("\u0000\n", '').chomp 218 | end 219 | 220 | module Script::Functions # rubocop:disable Style/ClassAndModuleChildren 221 | def javascript_path(path) 222 | SassC::Script::Value::String.new("/js/#{path.value}", :string) 223 | end 224 | 225 | def stylesheet_path(path) 226 | SassC::Script::Value::String.new("/css/#{path.value}", :identifier) 227 | end 228 | 229 | def no_return_path(_path) 230 | nil 231 | end 232 | 233 | def sass_return_path(path) 234 | SassC::Script::Value::String.new(path.value.to_s, :string) 235 | end 236 | 237 | def optional_arguments(path, optional = nil) 238 | optional ||= SassC::Script::Value::String.new('bar') 239 | SassC::Script::Value::String.new("#{path.value}/#{optional.value}", :string) 240 | end 241 | 242 | def function_that_raises_errors 243 | raise StandardError, 'Intentional wrong thing happened somewhere inside the custom function' 244 | end 245 | 246 | def nice_color_argument(color) 247 | SassC::Script::Value::String.new(color.to_s, :identifier) 248 | end 249 | 250 | def returns_a_color 251 | SassC::Script::Value::Color.new(red: 0, green: 0, blue: 0) 252 | end 253 | 254 | def returns_a_number 255 | SassC::Script::Value::Number.new(-312, 'rem') 256 | end 257 | 258 | def returns_a_bool 259 | SassC::Script::Value::Bool.new(true) 260 | end 261 | 262 | def inspect_bool(argument) 263 | unless argument.is_a? SassC::Script::Value::Bool 264 | raise StandardError, 'passed value is not a Sass::Script::Value::Bool' 265 | end 266 | 267 | argument 268 | end 269 | 270 | def inspect_number(argument) 271 | unless argument.is_a? SassC::Script::Value::Number 272 | raise StandardError, 'passed value is not a Sass::Script::Value::Number' 273 | end 274 | 275 | argument 276 | end 277 | 278 | def inspect_map(argument) 279 | argument.to_h.each_pair do |key, value| 280 | raise StandardError, "key #{key.inspect} is not a string" unless key.is_a? SassC::Script::Value::String 281 | 282 | value_class = case key.value 283 | when 'string' 284 | SassC::Script::Value::String 285 | when 'number' 286 | SassC::Script::Value::Number 287 | when 'color' 288 | SassC::Script::Value::Color 289 | when 'map' 290 | SassC::Script::Value::Map 291 | end 292 | 293 | raise StandardError, "unknown key #{key.inspect}" unless value_class 294 | raise StandardError, "value for #{key.inspect} is not a #{value_class}" unless value.is_a? value_class 295 | end 296 | argument 297 | end 298 | 299 | def inspect_list(argument) 300 | unless argument.is_a? SassC::Script::Value::List 301 | raise StandardError, 'passed value is not a Sass::Script::Value::List' 302 | end 303 | 304 | argument 305 | end 306 | 307 | def inspect_options 308 | SassC::Script::Value::String.new(options.inspect, :string) 309 | end 310 | 311 | def returns_sass_value 312 | SassC::Script::Value::Color.new(red: 0, green: 0, blue: 0) 313 | end 314 | 315 | def returns_sass_map 316 | key = SassC::Script::Value::String.new('color', 'string') 317 | value = SassC::Script::Value::Color.new(red: 0, green: 0, blue: 0) 318 | values = {} 319 | values[key] = value 320 | SassC::Script::Value::Map.new values 321 | end 322 | 323 | def returns_sass_list 324 | numbers = [10, 20, 30].map { |n| SassC::Script::Value::Number.new(n, '') } 325 | SassC::Script::Value::List.new(numbers, separator: :space) 326 | end 327 | end 328 | 329 | module ExternalFunctions 330 | def test_function 331 | SassC::Script::Value::String.new('custom_function', :string) 332 | end 333 | end 334 | end 335 | end 336 | -------------------------------------------------------------------------------- /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 | <<~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 | } 33 | #main p { 34 | width: 10em; 35 | } 36 | 37 | .huge { 38 | font-size: 10em; 39 | font-weight: bold; 40 | text-decoration: underline; 41 | } 42 | CSS 43 | end 44 | 45 | def test_nested_output_is_default 46 | engine = Engine.new(input_scss) 47 | 48 | assert_equal expected_nested_output, engine.render 49 | end 50 | 51 | def test_output_style_accepts_strings 52 | engine = Engine.new(input_scss, style: 'sass_style_nested') 53 | 54 | assert_equal expected_nested_output, engine.render 55 | end 56 | 57 | def test_invalid_output_style 58 | engine = Engine.new(input_scss, style: 'totally_wrong') 59 | assert_raises(InvalidStyleError) { engine.render } 60 | end 61 | 62 | def test_nested_output 63 | engine = Engine.new(input_scss, style: :sass_style_nested) 64 | 65 | assert_equal expected_nested_output, engine.render 66 | end 67 | 68 | def test_expanded_output 69 | engine = Engine.new(input_scss, style: :sass_style_expanded) 70 | 71 | assert_equal <<~CSS, engine.render 72 | #main { 73 | color: #fff; 74 | background-color: #000; 75 | } 76 | #main p { 77 | width: 10em; 78 | } 79 | 80 | .huge { 81 | font-size: 10em; 82 | font-weight: bold; 83 | text-decoration: underline; 84 | } 85 | CSS 86 | end 87 | 88 | def test_compact_output 89 | engine = Engine.new(input_scss, style: :sass_style_compact) 90 | 91 | assert_equal <<~CSS, engine.render 92 | #main { 93 | color: #fff; 94 | background-color: #000; 95 | } 96 | #main p { 97 | width: 10em; 98 | } 99 | 100 | .huge { 101 | font-size: 10em; 102 | font-weight: bold; 103 | text-decoration: underline; 104 | } 105 | CSS 106 | end 107 | 108 | def test_compressed_output 109 | engine = Engine.new(input_scss, style: :sass_style_compressed) 110 | 111 | assert_equal <<~CSS, engine.render 112 | #main{color:#fff;background-color:#000}#main p{width:10em}.huge{font-size:10em;font-weight:bold;text-decoration:underline} 113 | CSS 114 | end 115 | 116 | def test_short_output_style_names 117 | engine = Engine.new(input_scss, style: :compressed) 118 | 119 | assert_equal <<~CSS, engine.render 120 | #main{color:#fff;background-color:#000}#main p{width:10em}.huge{font-size:10em;font-weight:bold;text-decoration:underline} 121 | CSS 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /test/patches/bootstrap-rubygem.diff: -------------------------------------------------------------------------------- 1 | diff --git a/test/gemfiles/rails_6_0.gemfile b/test/gemfiles/rails_6_0.gemfile 2 | index daeffa5..44e38be 100644 3 | --- a/test/gemfiles/rails_6_0.gemfile 4 | +++ b/test/gemfiles/rails_6_0.gemfile 5 | @@ -5,3 +5,9 @@ gem 'activesupport', '~> 6.0.3' 6 | gem 'sassc-rails', '~> 2.0' 7 | 8 | gemspec path: '../../' 9 | + 10 | +gem 'base64' 11 | +gem 'drb' 12 | +gem 'mutex_m' 13 | +gem 'sassc', github: 'sass/sassc-ruby', ref: 'refs/pull/233/head' 14 | +gem 'sassc-embedded', path: '../../../../../..' 15 | diff --git a/test/gemfiles/rails_6_1.gemfile b/test/gemfiles/rails_6_1.gemfile 16 | index 04342fb..e59f402 100644 17 | --- a/test/gemfiles/rails_6_1.gemfile 18 | +++ b/test/gemfiles/rails_6_1.gemfile 19 | @@ -5,3 +5,9 @@ gem 'activesupport', '~> 6.1.3' 20 | gem 'sassc-rails', '~> 2.0' 21 | 22 | gemspec path: '../../' 23 | + 24 | +gem 'base64' 25 | +gem 'drb' 26 | +gem 'mutex_m' 27 | +gem 'sassc', github: 'sass/sassc-ruby', ref: 'refs/pull/233/head' 28 | +gem 'sassc-embedded', path: '../../../../../..' 29 | diff --git a/test/gemfiles/rails_7_0_dartsass.gemfile b/test/gemfiles/rails_7_0_dartsass.gemfile 30 | index 6df9142..759ab8d 100644 31 | --- a/test/gemfiles/rails_7_0_dartsass.gemfile 32 | +++ b/test/gemfiles/rails_7_0_dartsass.gemfile 33 | @@ -5,3 +5,8 @@ gem 'activesupport', '~> 7.0.4' 34 | gem 'dartsass-sprockets', '~> 3.0' 35 | 36 | gemspec path: '../../' 37 | + 38 | +gem 'base64' 39 | +gem 'drb' 40 | +gem 'mutex_m' 41 | +gem 'sassc-embedded', path: '../../../../../..' 42 | diff --git a/test/gemfiles/rails_7_0_sassc.gemfile b/test/gemfiles/rails_7_0_sassc.gemfile 43 | index 58fc039..aa8c54b 100644 44 | --- a/test/gemfiles/rails_7_0_sassc.gemfile 45 | +++ b/test/gemfiles/rails_7_0_sassc.gemfile 46 | @@ -5,3 +5,9 @@ gem 'activesupport', '~> 7.0.4' 47 | gem 'sassc-rails', '~> 2.0' 48 | 49 | gemspec path: '../../' 50 | + 51 | +gem 'base64' 52 | +gem 'drb' 53 | +gem 'mutex_m' 54 | +gem 'sassc', github: 'sass/sassc-ruby', ref: 'refs/pull/233/head' 55 | +gem 'sassc-embedded', path: '../../../../../..' 56 | diff --git a/test/test_helper.rb b/test/test_helper.rb 57 | index da89690..dd535dc 100644 58 | --- a/test/test_helper.rb 59 | +++ b/test/test_helper.rb 60 | @@ -34,9 +34,9 @@ end 61 | 62 | Capybara.configure do |config| 63 | config.server = :webrick 64 | - config.app_host = 'http://localhost:7000' 65 | + config.app_host = 'http://localhost:8000' 66 | config.default_driver = :cuprite 67 | config.javascript_driver = :cuprite 68 | - config.server_port = 7000 69 | + config.server_port = 8000 70 | config.default_max_wait_time = 10 71 | end 72 | -------------------------------------------------------------------------------- /test/patches/sassc-rails.diff: -------------------------------------------------------------------------------- 1 | diff --git a/Gemfile b/Gemfile 2 | index 09189d3..aee822f 100644 3 | --- a/Gemfile 4 | +++ b/Gemfile 5 | @@ -5,3 +5,10 @@ source 'https://rubygems.org' 6 | 7 | # Specify your gem's dependencies in sassc-rails.gemspec 8 | gemspec 9 | + 10 | +gem 'base64' 11 | +gem 'mutex_m' 12 | +gem 'ruby2_keywords' 13 | +gem 'tzinfo-data' if Gem.win_platform? 14 | +gem 'sassc', github: 'sass/sassc-ruby', ref: 'refs/pull/233/head' 15 | +gem 'sassc-embedded', path: '../../../..' 16 | diff --git a/gemfiles/rails_4_2.gemfile b/gemfiles/rails_4_2.gemfile 17 | index c27b143..d1fe529 100644 18 | --- a/gemfiles/rails_4_2.gemfile 19 | +++ b/gemfiles/rails_4_2.gemfile 20 | @@ -4,3 +4,10 @@ gem "rails", "~> 4.2.0" 21 | 22 | # Specify your gem's dependencies in sassc-rails.gemspec 23 | gemspec path: "../" 24 | + 25 | +gem 'base64' 26 | +gem 'mutex_m' 27 | +gem 'ruby2_keywords' 28 | +gem 'tzinfo-data' if Gem.win_platform? 29 | +gem 'sassc', github: 'sass/sassc-ruby', ref: 'refs/pull/233/head' 30 | +gem 'sassc-embedded', path: '../../../../..' 31 | diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile 32 | index 3682fc7..391033b 100644 33 | --- a/gemfiles/rails_5_2.gemfile 34 | +++ b/gemfiles/rails_5_2.gemfile 35 | @@ -4,3 +4,10 @@ gem "rails", "~> 5.2.1" 36 | 37 | # Specify your gem's dependencies in sassc-rails.gemspec 38 | gemspec path: "../" 39 | + 40 | +gem 'base64' 41 | +gem 'mutex_m' 42 | +gem 'ruby2_keywords' 43 | +gem 'tzinfo-data' if Gem.win_platform? 44 | +gem 'sassc', github: 'sass/sassc-ruby', ref: 'refs/pull/233/head' 45 | +gem 'sassc-embedded', path: '../../../../..' 46 | diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile 47 | index 8f7e3da..272395e 100644 48 | --- a/gemfiles/rails_6_0.gemfile 49 | +++ b/gemfiles/rails_6_0.gemfile 50 | @@ -4,3 +4,10 @@ gem "rails", "~> 6.0.a" 51 | 52 | # Specify your gem's dependencies in sassc-rails.gemspec 53 | gemspec path: "../" 54 | + 55 | +gem 'base64' 56 | +gem 'mutex_m' 57 | +gem 'ruby2_keywords' 58 | +gem 'tzinfo-data' if Gem.win_platform? 59 | +gem 'sassc', github: 'sass/sassc-ruby', ref: 'refs/pull/233/head' 60 | +gem 'sassc-embedded', path: '../../../../..' 61 | diff --git a/gemfiles/sprockets-rails_2_3.gemfile b/gemfiles/sprockets-rails_2_3.gemfile 62 | index e378718..f05a608 100644 63 | --- a/gemfiles/sprockets-rails_2_3.gemfile 64 | +++ b/gemfiles/sprockets-rails_2_3.gemfile 65 | @@ -4,3 +4,10 @@ gem "sprockets-rails", "~> 2.3.3" 66 | 67 | # Specify your gem's dependencies in sassc-rails.gemspec 68 | gemspec path: "../" 69 | + 70 | +gem 'base64' 71 | +gem 'mutex_m' 72 | +gem 'ruby2_keywords' 73 | +gem 'tzinfo-data' if Gem.win_platform? 74 | +gem 'sassc', github: 'sass/sassc-ruby', ref: 'refs/pull/233/head' 75 | +gem 'sassc-embedded', path: '../../../../..' 76 | diff --git a/gemfiles/sprockets-rails_3_0.gemfile b/gemfiles/sprockets-rails_3_0.gemfile 77 | index 3426de8..7fcb12a 100644 78 | --- a/gemfiles/sprockets-rails_3_0.gemfile 79 | +++ b/gemfiles/sprockets-rails_3_0.gemfile 80 | @@ -4,3 +4,10 @@ gem "sprockets-rails", "~> 3.2" 81 | 82 | # Specify your gem's dependencies in sassc-rails.gemspec 83 | gemspec path: "../" 84 | + 85 | +gem 'base64' 86 | +gem 'mutex_m' 87 | +gem 'ruby2_keywords' 88 | +gem 'tzinfo-data' if Gem.win_platform? 89 | +gem 'sassc', github: 'sass/sassc-ruby', ref: 'refs/pull/233/head' 90 | +gem 'sassc-embedded', path: '../../../../..' 91 | diff --git a/gemfiles/sprockets_3_0.gemfile b/gemfiles/sprockets_3_0.gemfile 92 | index 98bf2fd..a2b7807 100644 93 | --- a/gemfiles/sprockets_3_0.gemfile 94 | +++ b/gemfiles/sprockets_3_0.gemfile 95 | @@ -4,3 +4,10 @@ gem "sprockets", "~> 3.7" 96 | 97 | # Specify your gem's dependencies in sassc-rails.gemspec 98 | gemspec path: "../" 99 | + 100 | +gem 'base64' 101 | +gem 'mutex_m' 102 | +gem 'ruby2_keywords' 103 | +gem 'tzinfo-data' if Gem.win_platform? 104 | +gem 'sassc', github: 'sass/sassc-ruby', ref: 'refs/pull/233/head' 105 | +gem 'sassc-embedded', path: '../../../../..' 106 | diff --git a/gemfiles/sprockets_4_0.gemfile b/gemfiles/sprockets_4_0.gemfile 107 | index bf7d65c..2193329 100644 108 | --- a/gemfiles/sprockets_4_0.gemfile 109 | +++ b/gemfiles/sprockets_4_0.gemfile 110 | @@ -4,3 +4,10 @@ gem "sprockets", "~> 4.0.x" 111 | 112 | # Specify your gem's dependencies in sassc-rails.gemspec 113 | gemspec path: "../" 114 | + 115 | +gem 'base64' 116 | +gem 'mutex_m' 117 | +gem 'ruby2_keywords' 118 | +gem 'tzinfo-data' if Gem.win_platform? 119 | +gem 'sassc', github: 'sass/sassc-ruby', ref: 'refs/pull/233/head' 120 | +gem 'sassc-embedded', path: '../../../../..' 121 | diff --git a/sassc-rails.gemspec b/sassc-rails.gemspec 122 | index 38349f7..34a4eb9 100644 123 | --- a/sassc-rails.gemspec 124 | +++ b/sassc-rails.gemspec 125 | @@ -20,13 +20,13 @@ Gem::Specification.new do |spec| 126 | 127 | spec.add_development_dependency 'pry' 128 | spec.add_development_dependency "bundler" 129 | - spec.add_development_dependency "rake", "~> 10.0" 130 | + spec.add_development_dependency "rake" 131 | spec.add_development_dependency 'mocha' 132 | 133 | - spec.add_dependency "sassc", ">= 2.0" 134 | + spec.add_dependency "sassc", "~> 2.4" 135 | spec.add_dependency "tilt" 136 | 137 | - spec.add_dependency 'railties', '>= 4.0.0' 138 | - spec.add_dependency 'sprockets', '> 3.0' 139 | - spec.add_dependency 'sprockets-rails' 140 | + spec.add_dependency 'railties', '~> 6.1' 141 | + spec.add_dependency 'sprockets', '~> 4.2' 142 | + spec.add_dependency 'sprockets-rails', '~> 3.4' 143 | end 144 | diff --git a/test/dummy/app/assets/stylesheets/partials/_explicit_extension_import.foo b/test/dummy/app/assets/stylesheets/partials/_explicit_extension_import.foo.scss 145 | similarity index 100% 146 | rename from test/dummy/app/assets/stylesheets/partials/_explicit_extension_import.foo 147 | rename to test/dummy/app/assets/stylesheets/partials/_explicit_extension_import.foo.scss 148 | diff --git a/test/sassc_rails_test.rb b/test/sassc_rails_test.rb 149 | index a15110d..452a251 100644 150 | --- a/test/sassc_rails_test.rb 151 | +++ b/test/sassc_rails_test.rb 152 | @@ -164,6 +164,8 @@ class SassRailsTest < MiniTest::Test 153 | end 154 | 155 | def test_line_comments_active_in_dev 156 | + skip 157 | + 158 | @app.config.sass.line_comments = true 159 | initialize_dev! 160 | 161 | @@ -211,7 +213,7 @@ class SassRailsTest < MiniTest::Test 162 | 163 | asset = render_asset("application.css") 164 | assert_equal <<-CSS, asset 165 | -.hello{color:#FFF} 166 | +.hello{color:#fff} 167 | CSS 168 | end 169 | 170 | @@ -220,7 +222,7 @@ class SassRailsTest < MiniTest::Test 171 | 172 | asset = render_asset("application.css") 173 | assert_equal <<-CSS, asset 174 | -.hello{color:#FFF} 175 | +.hello{color:#fff} 176 | CSS 177 | end 178 | 179 | -------------------------------------------------------------------------------- /test/patches/sprockets.diff: -------------------------------------------------------------------------------- 1 | diff --git a/Gemfile b/Gemfile 2 | index 3be9c3cd..df08f6a7 100644 3 | --- a/Gemfile 4 | +++ b/Gemfile 5 | @@ -1,2 +1,5 @@ 6 | source "https://rubygems.org" 7 | gemspec 8 | + 9 | +gem 'sassc', github: 'sass/sassc-ruby', ref: 'refs/pull/233/head' 10 | +gem 'sassc-embedded', path: '../../../..' 11 | diff --git a/test/fixtures/octicons/octicons.scss b/test/fixtures/octicons/octicons.scss 12 | index f326ce05..38018c5b 100644 13 | --- a/test/fixtures/octicons/octicons.scss 14 | +++ b/test/fixtures/octicons/octicons.scss 15 | @@ -1,5 +1,5 @@ 16 | @font-face { 17 | - font-family: 'octicons'; 18 | + font-family: "octicons"; 19 | src: font-url('octicons.eot?#iefix') format('embedded-opentype'), 20 | font-url('octicons.woff2') format('woff2'), 21 | font-url('octicons.woff') format('woff'), 22 | diff --git a/test/fixtures/sass/variables.sass b/test/fixtures/sass/variables.sass 23 | index 7182195d..2b65656a 100644 24 | --- a/test/fixtures/sass/variables.sass 25 | +++ b/test/fixtures/sass/variables.sass 26 | @@ -1,3 +1,5 @@ 27 | +@use "sass:math" 28 | + 29 | $blue: #3bbfce 30 | $margin: 16px 31 | 32 | @@ -6,6 +8,6 @@ $margin: 16px 33 | color: darken($blue, 9%) 34 | 35 | .border 36 | - padding: $margin / 2 37 | - margin: $margin / 2 38 | + padding: math.div($margin, 2) 39 | + margin: math.div($margin, 2) 40 | border-color: $blue 41 | diff --git a/test/shared_sass_tests.rb b/test/shared_sass_tests.rb 42 | index 1cfb8413..97e2db79 100644 43 | --- a/test/shared_sass_tests.rb 44 | +++ b/test/shared_sass_tests.rb 45 | @@ -26,7 +26,8 @@ div { 46 | url: url(font-path("foo.woff2")); 47 | url: url(font-path("foo.woff")); 48 | url: url("/js/foo.js"); 49 | - url: url("/css/foo.css"); } 50 | + url: url("/css/foo.css"); 51 | +} 52 | EOS 53 | end 54 | end 55 | @@ -39,26 +40,31 @@ module SharedSassTestSprockets 56 | assert_equal <<-EOS, render('sass/variables.sass') 57 | .content-navigation { 58 | border-color: #3bbfce; 59 | - color: #2ca2af; } 60 | + color: rgb(43.82, 161.8657142857, 175.28); 61 | +} 62 | 63 | .border { 64 | padding: 8px; 65 | margin: 8px; 66 | - border-color: #3bbfce; } 67 | + border-color: #3bbfce; 68 | +} 69 | EOS 70 | end 71 | 72 | test "process nesting" do 73 | assert_equal <<-EOS, render('sass/nesting.scss') 74 | table.hl { 75 | - margin: 2em 0; } 76 | - table.hl td.ln { 77 | - text-align: right; } 78 | + margin: 2em 0; 79 | +} 80 | +table.hl td.ln { 81 | + text-align: right; 82 | +} 83 | 84 | li { 85 | font-family: serif; 86 | font-weight: bold; 87 | - font-size: 1.2em; } 88 | + font-size: 1.2em; 89 | +} 90 | EOS 91 | end 92 | 93 | @@ -67,17 +73,20 @@ li { 94 | #navbar li { 95 | border-top-radius: 10px; 96 | -moz-border-radius-top: 10px; 97 | - -webkit-border-top-radius: 10px; } 98 | + -webkit-border-top-radius: 10px; 99 | +} 100 | 101 | #footer { 102 | border-top-radius: 5px; 103 | -moz-border-radius-top: 5px; 104 | - -webkit-border-top-radius: 5px; } 105 | + -webkit-border-top-radius: 5px; 106 | +} 107 | 108 | #sidebar { 109 | border-left-radius: 8px; 110 | -moz-border-radius-left: 8px; 111 | - -webkit-border-left-radius: 8px; } 112 | + -webkit-border-left-radius: 8px; 113 | +} 114 | EOS 115 | end 116 | 117 | @@ -86,17 +95,20 @@ li { 118 | #navbar li { 119 | border-top-radius: 10px; 120 | -moz-border-radius-top: 10px; 121 | - -webkit-border-top-radius: 10px; } 122 | + -webkit-border-top-radius: 10px; 123 | +} 124 | 125 | #footer { 126 | border-top-radius: 5px; 127 | -moz-border-radius-top: 5px; 128 | - -webkit-border-top-radius: 5px; } 129 | + -webkit-border-top-radius: 5px; 130 | +} 131 | 132 | #sidebar { 133 | border-left-radius: 8px; 134 | -moz-border-radius-left: 8px; 135 | - -webkit-border-left-radius: 8px; } 136 | + -webkit-border-left-radius: 8px; 137 | +} 138 | EOS 139 | end 140 | 141 | @@ -104,12 +116,14 @@ li { 142 | assert_equal <<-EOS, render('sass/import_nonpartial.scss') 143 | .content-navigation { 144 | border-color: #3bbfce; 145 | - color: #2ca2af; } 146 | + color: rgb(43.82, 161.8657142857, 175.28); 147 | +} 148 | 149 | .border { 150 | padding: 8px; 151 | margin: 8px; 152 | - border-color: #3bbfce; } 153 | + border-color: #3bbfce; 154 | +} 155 | EOS 156 | end 157 | 158 | @@ -133,24 +147,28 @@ footer, header, hgroup, menu, nav, section { 159 | #navbar li { 160 | border-top-radius: 10px; 161 | -moz-border-radius-top: 10px; 162 | - -webkit-border-top-radius: 10px; } 163 | + -webkit-border-top-radius: 10px; 164 | +} 165 | 166 | #footer { 167 | border-top-radius: 5px; 168 | -moz-border-radius-top: 5px; 169 | - -webkit-border-top-radius: 5px; } 170 | + -webkit-border-top-radius: 5px; 171 | +} 172 | 173 | #sidebar { 174 | border-left-radius: 8px; 175 | -moz-border-radius-left: 8px; 176 | - -webkit-border-left-radius: 8px; } 177 | + -webkit-border-left-radius: 8px; 178 | +} 179 | EOS 180 | end 181 | 182 | test "@import relative nested file" do 183 | assert_equal <<-EOS, render('sass/relative.scss') 184 | body { 185 | - background: #666666; } 186 | + background: #666666; 187 | +} 188 | EOS 189 | end 190 | 191 | @@ -159,13 +177,13 @@ body { 192 | 193 | sandbox filename do 194 | File.open(filename, 'w') { |f| f.write "body { background: red; };" } 195 | - assert_equal "body {\n background: red; }\n", render(filename) 196 | + assert_equal "body {\n background: red;\n}\n", render(filename) 197 | 198 | File.open(filename, 'w') { |f| f.write "body { background: blue; };" } 199 | mtime = Time.now + 1 200 | File.utime(mtime, mtime, filename) 201 | 202 | - assert_equal "body {\n background: blue; }\n", render(filename) 203 | + assert_equal "body {\n background: blue;\n}\n", render(filename) 204 | end 205 | end 206 | 207 | @@ -175,27 +193,29 @@ body { 208 | sandbox filename, partial do 209 | File.open(filename, 'w') { |f| f.write "@import 'partial';" } 210 | File.open(partial, 'w') { |f| f.write "body { background: red; };" } 211 | - assert_equal "body {\n background: red; }\n", render(filename) 212 | + assert_equal "body {\n background: red;\n}\n", render(filename) 213 | 214 | File.open(partial, 'w') { |f| f.write "body { background: blue; };" } 215 | mtime = Time.now + 1 216 | File.utime(mtime, mtime, partial) 217 | 218 | - assert_equal "body {\n background: blue; }\n", render(filename) 219 | + assert_equal "body {\n background: blue;\n}\n", render(filename) 220 | end 221 | end 222 | 223 | test "reference @import'd variable" do 224 | assert_equal <<-EOS, render('sass/links.scss') 225 | a:link { 226 | - color: "red"; } 227 | + color: "red"; 228 | +} 229 | EOS 230 | end 231 | 232 | test "@import reference variable" do 233 | assert_equal <<-EOS, render('sass/main.scss') 234 | #header { 235 | - color: "blue"; } 236 | + color: "blue"; 237 | +} 238 | EOS 239 | end 240 | end 241 | @@ -238,7 +258,8 @@ div { 242 | url: url("/foo.woff2"); 243 | url: url("/foo.woff"); 244 | url: url("/foo.js"); 245 | - url: url("/foo.css"); } 246 | + url: url("/foo.css"); 247 | +} 248 | EOS 249 | end 250 | 251 | @@ -252,17 +273,19 @@ div { 252 | url: url(/foo.woff2); 253 | url: url(/foo.woff); 254 | url: url(/foo.js); 255 | - url: url(/foo.css); } 256 | + url: url(/foo.css); 257 | +} 258 | EOS 259 | end 260 | 261 | test "url functions with query and hash parameters" do 262 | assert_equal <<-EOS, render('octicons/octicons.scss') 263 | @font-face { 264 | - font-family: 'octicons'; 265 | + font-family: "octicons"; 266 | src: url(/octicons.eot?#iefix) format("embedded-opentype"), url(/octicons.woff2) format("woff2"), url(/octicons.woff) format("woff"), url(/octicons.ttf) format("truetype"), url(/octicons.svg#octicons) format("svg"); 267 | font-weight: normal; 268 | - font-style: normal; } 269 | + font-style: normal; 270 | +} 271 | EOS 272 | end 273 | 274 | @@ -287,7 +310,8 @@ div { 275 | test "data-url function" do 276 | assert_equal <<-EOS, render('sass/data_url.scss') 277 | div { 278 | - url: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAMAAAAoyzS7AAAABlBMVEUFO2sAAADPfNHpAAAACklEQVQIW2NgAAAAAgABYkBPaAAAAABJRU5ErkJggg%3D%3D); } 279 | + url: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAMAAAAoyzS7AAAABlBMVEUFO2sAAADPfNHpAAAACklEQVQIW2NgAAAAAgABYkBPaAAAAABJRU5ErkJggg%3D%3D); 280 | +} 281 | EOS 282 | end 283 | end 284 | diff --git a/test/test_sassc.rb b/test/test_sassc.rb 285 | index 0d8f2f32..03ad99e3 100644 286 | --- a/test/test_sassc.rb 287 | +++ b/test/test_sassc.rb 288 | @@ -88,8 +88,7 @@ class TestSprocketsSassc < TestBaseSassc 289 | rescue SassC::SyntaxError => error 290 | # this is not exactly consistent with ruby sass 291 | assert error.message.include?("invalid") 292 | - assert error.message.include?("error.sass") 293 | - assert error.message.include?("line 5") 294 | + assert error.message.include?("error.sass 5") 295 | end 296 | end 297 | 298 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'sassc-embedded' 4 | 5 | require 'fileutils' 6 | require 'minitest/autorun' 7 | require 'minitest/around/unit' 8 | 9 | module FixtureHelper 10 | FIXTURE_ROOT = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures')) 11 | 12 | def fixture(path) 13 | File.read(fixture_path(path)) 14 | end 15 | 16 | def fixture_path(path) 17 | if path.match(FIXTURE_ROOT) 18 | path 19 | else 20 | File.join(FIXTURE_ROOT, path) 21 | end 22 | end 23 | end 24 | 25 | module TempFileTest 26 | def around 27 | pwd = Dir.pwd 28 | tmpdir = Dir.mktmpdir 29 | Dir.chdir tmpdir 30 | yield 31 | ensure 32 | Dir.chdir pwd 33 | FileUtils.rm_rf(tmpdir) 34 | end 35 | 36 | def temp_file(filename, contents) 37 | FileUtils.mkdir_p(File.dirname(filename)) 38 | File.write(filename, contents) 39 | end 40 | 41 | def temp_dir(directory) 42 | FileUtils.mkdir_p(directory) 43 | end 44 | end 45 | --------------------------------------------------------------------------------