├── script ├── spec ├── cibuild ├── bootstrap ├── test ├── release └── fmt ├── .rspec ├── spec ├── source │ ├── _sass │ │ ├── _grid.scss │ │ └── _color.scss │ ├── css │ │ ├── main.scss │ │ └── app.scss │ └── _config.yml ├── [alpha]beta │ ├── _sass │ │ ├── _grid.scss │ │ └── _color.scss │ ├── css │ │ └── main.scss │ └── _config.yml ├── nested_source │ └── src │ │ ├── _sass │ │ ├── _grid.scss │ │ └── _color.scss │ │ ├── css │ │ └── main.scss │ │ └── _config.yml ├── pages-collection │ ├── _sass │ │ ├── _grid.scss │ │ └── _color.scss │ ├── css │ │ └── main.scss │ └── _pages │ │ └── test.md ├── other_sass_library │ ├── _sass │ │ └── _color.scss │ └── css │ │ └── main.scss ├── spec_helper.rb ├── sass_converter_spec.rb └── scss_converter_spec.rb ├── Rakefile ├── docs ├── _config.yml ├── assets │ └── css │ │ └── main.scss ├── _layouts │ └── default.html ├── _sass │ └── _typography.scss ├── README.md └── index.md ├── lib ├── jekyll-sass-converter │ └── version.rb ├── jekyll-sass-converter.rb └── jekyll │ ├── converters │ ├── sass.rb │ └── scss.rb │ └── source_map_page.rb ├── .gitignore ├── Gemfile ├── jekyll-sass-converter.gemspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── LICENSE.txt ├── History.markdown └── README.md /script/spec: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format doc 3 | -------------------------------------------------------------------------------- /spec/source/_sass/_grid.scss: -------------------------------------------------------------------------------- 1 | .half { width: 50%; } -------------------------------------------------------------------------------- /spec/[alpha]beta/_sass/_grid.scss: -------------------------------------------------------------------------------- 1 | .half { width: 50% } 2 | -------------------------------------------------------------------------------- /spec/source/css/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "grid"; -------------------------------------------------------------------------------- /spec/nested_source/src/_sass/_grid.scss: -------------------------------------------------------------------------------- 1 | .half { width: 50%; } 2 | -------------------------------------------------------------------------------- /spec/pages-collection/_sass/_grid.scss: -------------------------------------------------------------------------------- 1 | .half { width: 50%; } 2 | -------------------------------------------------------------------------------- /spec/source/css/app.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "color"; 5 | -------------------------------------------------------------------------------- /spec/[alpha]beta/css/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "grid"; 5 | -------------------------------------------------------------------------------- /spec/[alpha]beta/_sass/_color.scss: -------------------------------------------------------------------------------- 1 | $black: #999; 2 | a { color: $black } 3 | -------------------------------------------------------------------------------- /spec/nested_source/src/css/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "grid"; 5 | -------------------------------------------------------------------------------- /spec/other_sass_library/_sass/_color.scss: -------------------------------------------------------------------------------- 1 | a { 2 | color: #999999; 3 | } 4 | -------------------------------------------------------------------------------- /spec/pages-collection/css/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "grid"; 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | script/test 6 | script/fmt 7 | -------------------------------------------------------------------------------- /spec/nested_source/src/_sass/_color.scss: -------------------------------------------------------------------------------- 1 | $black: #999; 2 | a { color: $black; } 3 | -------------------------------------------------------------------------------- /spec/other_sass_library/css/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "color"; 5 | -------------------------------------------------------------------------------- /spec/pages-collection/_sass/_color.scss: -------------------------------------------------------------------------------- 1 | $black: #999; 2 | a { color: $black; } 3 | -------------------------------------------------------------------------------- /spec/source/_config.yml: -------------------------------------------------------------------------------- 1 | sass: 2 | style: :compressed 3 | highlighter: rouge 4 | -------------------------------------------------------------------------------- /spec/[alpha]beta/_config.yml: -------------------------------------------------------------------------------- 1 | sass: 2 | style: :compressed 3 | highlighter: rouge 4 | -------------------------------------------------------------------------------- /spec/nested_source/src/_config.yml: -------------------------------------------------------------------------------- 1 | sass: 2 | style: :compressed 3 | highlighter: rouge 4 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | bundle install -j8 || bundle install 6 | -------------------------------------------------------------------------------- /spec/pages-collection/_pages/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Test Document 3 | --- 4 | 5 | Hello World 6 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | sass: 2 | style: compact # possible values: nested expanded compact compressed 3 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "Running rspec with sass-embedded" 6 | bundle exec rspec $@ 7 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Tag and push a release. 3 | 4 | set -e 5 | script/cibuild 6 | bundle exec rake release 7 | -------------------------------------------------------------------------------- /lib/jekyll-sass-converter/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JekyllSassConverter 4 | VERSION = "3.1.0" 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .jekyll-cache 2 | .sass-cache 3 | *.gem 4 | docs/_site 5 | Gemfile.lock 6 | pkg 7 | rdoc 8 | spec/dest 9 | vendor/bundle 10 | -------------------------------------------------------------------------------- /spec/source/_sass/_color.scss: -------------------------------------------------------------------------------- 1 | // {% debug %}This is a comment.{% enddebug %} 2 | // Import using {{ site.mytheme.skin }}. 3 | 4 | $black: #999; 5 | a { color: $black; } 6 | -------------------------------------------------------------------------------- /lib/jekyll-sass-converter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "jekyll-sass-converter/version" 4 | require "jekyll/converters/scss" 5 | require "jekyll/converters/sass" 6 | 7 | module JekyllSassConverter 8 | end 9 | -------------------------------------------------------------------------------- /script/fmt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "RuboCop $(bundle exec rubocop --version)" 3 | bundle exec rubocop -D $@ 4 | success=$? 5 | if ((success != 0)); then 6 | echo -e "\nTry running \`script/fmt --safe-auto-correct\` to automatically fix errors" 7 | fi 8 | exit $success 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | gemspec 5 | 6 | gem "jekyll", ENV["JEKYLL_VERSION"] ? "~> #{ENV["JEKYLL_VERSION"]}" : ">= 4.0" 7 | gem "minima" 8 | 9 | gem "rake" 10 | gem "rspec", "~> 3.0" 11 | gem "rubocop-jekyll", "~> 0.14.0" 12 | -------------------------------------------------------------------------------- /docs/assets/css/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | # this ensures Jekyll reads the file to be transformed into CSS later 3 | # only Main files contain this front matter, not partials. 4 | --- 5 | 6 | @import "typography"; 7 | 8 | .content { 9 | width: 45rem; 10 | margin: 0 auto; 11 | } 12 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sass Example Site 6 | 7 | 8 | 9 |
10 | {{ content }} 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/jekyll/converters/sass.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "jekyll/converters/scss" 4 | 5 | module Jekyll 6 | module Converters 7 | class Sass < Scss 8 | EXTENSION_PATTERN = %r!^\.sass$!i 9 | 10 | safe true 11 | priority :low 12 | 13 | def syntax 14 | :indented 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /docs/_sass/_typography.scss: -------------------------------------------------------------------------------- 1 | // This is a partial. 2 | // It lies in /_sass, just waiting to be imported. 3 | // It does not contain the YAML front matter and has no corresponding output file in the built site. 4 | 5 | body { 6 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; 7 | font-size: 18px; 8 | line-height: 1.5; 9 | color: #24292e; 10 | background-color: #fff; 11 | } 12 | 13 | a { 14 | color: #0366d6; 15 | } 16 | 17 | code, 18 | pre { 19 | font-family: Menlo, Consolas, "Consolas for Powerline", "Courier New", Courier, monospace; 20 | background-color: #2b2b2b; 21 | color: #fff; 22 | padding: 0.25em 23 | } 24 | -------------------------------------------------------------------------------- /jekyll-sass-converter.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/jekyll-sass-converter/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "jekyll-sass-converter" 7 | spec.version = JekyllSassConverter::VERSION 8 | spec.authors = ["Parker Moore"] 9 | spec.email = ["parkrmoore@gmail.com"] 10 | spec.summary = "A basic Sass converter for Jekyll." 11 | spec.homepage = "https://github.com/jekyll/jekyll-sass-converter" 12 | spec.license = "MIT" 13 | 14 | spec.files = `git ls-files -z`.split("\x0").grep(%r!^lib/!) 15 | spec.require_paths = ["lib"] 16 | 17 | spec.required_ruby_version = ">= 3.1.0" 18 | 19 | spec.add_dependency "sass-embedded", "~> 1.75" 20 | end 21 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | require: rubocop-jekyll 4 | inherit_gem: 5 | rubocop-jekyll: .rubocop.yml 6 | 7 | AllCops: 8 | TargetRubyVersion: 3.1 9 | SuggestExtensions: false 10 | Exclude: 11 | - vendor/**/* 12 | 13 | Layout/LineEndStringConcatenationIndentation: 14 | Enabled: true 15 | 16 | Lint/EmptyInPattern: 17 | Enabled: false 18 | 19 | Naming/InclusiveLanguage: 20 | Enabled: false 21 | 22 | Performance/MapCompact: 23 | Enabled: true 24 | Performance/RedundantEqualityComparisonBlock: 25 | Enabled: true 26 | Performance/RedundantSplitRegexpArgument: 27 | Enabled: true 28 | 29 | Style/InPatternThen: 30 | Enabled: false 31 | Style/MultilineInPatternThen: 32 | Enabled: false 33 | Style/QuotedSymbols: 34 | Enabled: true 35 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config --auto-gen-only-exclude` 3 | # on 2024-10-28 15:58:10 UTC using RuboCop version 1.57.2. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 3 10 | # Configuration parameters: AllowedMethods. 11 | # AllowedMethods: enums 12 | Lint/ConstantDefinitionInBlock: 13 | Exclude: 14 | - 'spec/spec_helper.rb' 15 | 16 | # Offense count: 1 17 | # Configuration parameters: AllowedMethods, AllowedPatterns, Max. 18 | Metrics/PerceivedComplexity: 19 | Exclude: 20 | - 'lib/jekyll/converters/scss.rb' 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Gem 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | paths: ["lib/**/version.rb"] 7 | 8 | jobs: 9 | release: 10 | if: "github.repository_owner == 'jekyll'" 11 | name: "Release Gem (Ruby ${{ matrix.ruby_version }})" 12 | runs-on: "ubuntu-latest" 13 | strategy: 14 | fail-fast: true 15 | matrix: 16 | ruby_version: ["3.1"] 17 | steps: 18 | - name: Checkout Repository 19 | uses: actions/checkout@v4 20 | - name: "Set up Ruby ${{ matrix.ruby_version }}" 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: ${{ matrix.ruby_version }} 24 | bundler-cache: true 25 | - name: Build and Publish Gem 26 | uses: ashmaroli/release-gem@dist 27 | with: 28 | gemspec_name: "jekyll-sass-converter" 29 | env: 30 | GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_GEM_PUSH_API_KEY }} 31 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | # This is an example site for Sass integration in Jekyll 6 | 7 | You have two kinds of Sass files: 8 | 9 | 1. Main files, which you wish to be output as CSS files 10 | 2. Partials, which are used by main files in `@import` statements 11 | 12 | Main files are like pages – they go where you want them to be output, and they contain the YAML front matter (`---` lines) at the top. Partials are like hidden Jekyll data, so they go in an underscored directory, which defaults to `_sass`. You site might look like this: 13 | 14 | . 15 | | - _sass 16 | | - _typography.scss 17 | | - _layout.scss 18 | | - _colors.scss 19 | | - stylesheets 20 | | - screen.scss 21 | | - print.scss 22 | 23 | And so on. 24 | 25 | The output, in your `_site` directory, would look like this: 26 | 27 | . 28 | | - stylesheets 29 | | - screen.css 30 | | - print.css 31 | 32 | Boom! Now you have just your SCSS/Sass converted over to CSS with all the proper inputs. 33 | -------------------------------------------------------------------------------- /lib/jekyll/source_map_page.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | # A Jekyll::Page subclass to manage the source map file associated with 5 | # a given scss / sass page. 6 | class SourceMapPage < Page 7 | # Initialize a new SourceMapPage. 8 | # 9 | # @param [Jekyll::Page] css_page The Page object that manages the css file. 10 | def initialize(css_page) 11 | @site = css_page.site 12 | @dir = css_page.dir 13 | @data = css_page.data 14 | @name = "#{css_page.basename}.css.map" 15 | 16 | process(@name) 17 | Jekyll::Hooks.trigger :pages, :post_init, self 18 | end 19 | 20 | def source_map(map) 21 | self.content = map 22 | end 23 | 24 | def ext 25 | ".map" 26 | end 27 | 28 | def asset_file? 29 | true 30 | end 31 | 32 | def render_with_liquid? 33 | false 34 | end 35 | 36 | # @return[String] the object as a debug String. 37 | def inspect 38 | "#<#{self.class} @name=#{name.inspect}>" 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | # This is an example site for Sass integration in Jekyll 6 | 7 | You have two kinds of Sass files: 8 | 9 | 1. Main files, which you wish to be output as CSS files 10 | 2. Partials, which are used by main files in `@import` statements 11 | 12 | Main files are like pages – they go where you want them to be output, and they contain the YAML front matter (`---` lines) at the top. Partials are like hidden Jekyll data, so they go in an underscored directory, which defaults to `_sass`. 13 | 14 | You site might look like this: 15 | 16 | | - _sass 17 | | - _typography.scss 18 | | - _layout.scss 19 | | - _colors.scss 20 | | - assets/css 21 | | - main.scss 22 | | - print.scss 23 | 24 | And so on. 25 | 26 | The output, in your `_site` directory, would look like this: 27 | 28 | | - assets/css 29 | | - main.css 30 | | - print.css 31 | 32 | Boom! Now you have just your SCSS/Sass converted over to CSS with all the proper inputs. See also [assets section in Jekyll's documentation](https://jekyllrb.com/docs/assets/). 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-present Parker Moore and the jekyll-sass-converter 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 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | ci: 13 | name: "Ruby ${{ matrix.ruby_version }} (${{ matrix.os }})" 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: 19 | - "ubuntu-latest" 20 | - "windows-latest" 21 | ruby_version: 22 | - "3.1" 23 | - "3.3" 24 | - "3.4" 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 5 29 | - name: "Set up Ruby ${{ matrix.ruby_version }}" 30 | uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: ${{ matrix.ruby_version }} 33 | bundler-cache: true 34 | - name: Execute Unit Tests 35 | run: bash script/test --force-color 36 | 37 | style_check: 38 | name: "Style Check (Ruby ${{ matrix.ruby_version }})" 39 | runs-on: "ubuntu-latest" 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | ruby_version: 44 | - "3.1" 45 | steps: 46 | - uses: actions/checkout@v4 47 | with: 48 | fetch-depth: 5 49 | - name: "Set up Ruby ${{ matrix.ruby_version }}" 50 | uses: ruby/setup-ruby@v1 51 | with: 52 | ruby-version: ${{ matrix.ruby_version }} 53 | bundler-cache: true 54 | - name: Run RuboCop 55 | run: bash script/fmt 56 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "fileutils" 4 | require "jekyll" 5 | 6 | lib = File.expand_path("lib", __dir__) 7 | 8 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 9 | require "jekyll-sass-converter" 10 | 11 | Jekyll.logger.log_level = :error 12 | 13 | RSpec.configure do |config| 14 | config.run_all_when_everything_filtered = true 15 | config.filter_run :focus 16 | config.order = "random" 17 | 18 | SOURCE_DIR = File.expand_path("source", __dir__) 19 | DEST_DIR = File.expand_path("dest", __dir__) 20 | SASS_LIB_DIR = File.expand_path("other_sass_library", __dir__) 21 | FileUtils.rm_rf(DEST_DIR) 22 | FileUtils.mkdir_p(DEST_DIR) 23 | 24 | def source_dir(*files) 25 | File.join(SOURCE_DIR, *files) 26 | end 27 | 28 | def dest_dir(*files) 29 | File.join(DEST_DIR, *files) 30 | end 31 | 32 | def sass_lib(*files) 33 | File.join(SASS_LIB_DIR, *files) 34 | end 35 | 36 | def site_configuration(overrides = {}) 37 | Jekyll.configuration( 38 | overrides.merge( 39 | "source" => source_dir, 40 | "destination" => dest_dir 41 | ) 42 | ) 43 | end 44 | 45 | # rubocop:disable Style/StringConcatenation 46 | def compressed(content) 47 | content.gsub(%r!\s+!, "").gsub(%r!;}!, "}") + "\n" 48 | end 49 | # rubocop:enable Style/StringConcatenation 50 | 51 | def make_site(config) 52 | Jekyll::Site.new(site_configuration.merge(config)) 53 | end 54 | 55 | def scss_converter_instance(site) 56 | site.find_converter_instance(Jekyll::Converters::Scss) 57 | end 58 | 59 | def sass_converter_instance(site) 60 | site.find_converter_instance(Jekyll::Converters::Sass) 61 | end 62 | 63 | def create_directory(location) 64 | FileUtils.mkdir_p(location) unless File.directory?(location) 65 | end 66 | 67 | def remove_directory(location) 68 | FileUtils.rmdir(location) if File.directory?(location) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/sass_converter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe(Jekyll::Converters::Sass) do 6 | let(:site) do 7 | Jekyll::Site.new(site_configuration) 8 | end 9 | 10 | let(:sass_converter) do 11 | sass_converter_instance(site) 12 | end 13 | 14 | let(:content) do 15 | <<~SASS 16 | // tl;dr some sass 17 | $font-stack: Helvetica, sans-serif 18 | body 19 | font-family: $font-stack 20 | font-color: fuschia 21 | SASS 22 | end 23 | 24 | let(:expanded_css_output) do 25 | <<~CSS.chomp 26 | body { 27 | font-family: Helvetica, sans-serif; 28 | font-color: fuschia; 29 | } 30 | CSS 31 | end 32 | 33 | let(:invalid_content) do 34 | <<~SASS 35 | font-family: $font-stack; 36 | SASS 37 | end 38 | 39 | def converter(overrides = {}) 40 | sass_converter_instance(site).dup.tap do |obj| 41 | obj.instance_variable_get(:@config)["sass"] = overrides 42 | end 43 | end 44 | 45 | context "matching file extensions" do 46 | it "does not match .scss files" do 47 | expect(converter.matches(".scss")).to be_falsey 48 | end 49 | 50 | it "matches .sass files" do 51 | expect(converter.matches(".sass")).to be_truthy 52 | end 53 | end 54 | 55 | context "converting sass" do 56 | it "produces CSS" do 57 | expect(converter.convert(content)).to eql(expanded_css_output) 58 | end 59 | 60 | it "includes the syntax error line in the syntax error message" do 61 | expected = %r!Expected newline!i 62 | expect do 63 | converter.convert(invalid_content) 64 | end.to raise_error(Jekyll::Converters::Scss::SyntaxError, expected) 65 | end 66 | 67 | it "does not include the charset without an associated page" do 68 | overrides = { "style" => :expanded } 69 | result = converter(overrides).convert(%(a\n content: "あ")) 70 | expect(result).to eql(%(a {\n content: "あ";\n})) 71 | end 72 | 73 | it "does not include the BOM without an associated page" do 74 | overrides = { "style" => :compressed } 75 | result = converter(overrides).convert(%(a\n content: "あ")) 76 | expect(result).to eql(%(a{content:"あ"})) 77 | expect(result.bytes.to_a[0..2]).not_to eql([0xEF, 0xBB, 0xBF]) 78 | end 79 | end 80 | 81 | context "in a site with a collection labelled 'pages'" do 82 | let(:site) do 83 | make_site( 84 | "source" => File.expand_path("pages-collection", __dir__), 85 | "sass" => { 86 | "style" => :expanded, 87 | }, 88 | "collections" => { 89 | "pages" => { 90 | "output" => true, 91 | }, 92 | } 93 | ) 94 | end 95 | 96 | it "produces CSS without raising errors" do 97 | expect { site.process }.not_to raise_error 98 | expect(sass_converter.convert(content)).to eql(expanded_css_output) 99 | end 100 | end 101 | 102 | context "in a site nested inside directory with square brackets" do 103 | let(:site) do 104 | make_site( 105 | "source" => File.expand_path("[alpha]beta", __dir__), 106 | "sass" => { 107 | "style" => :expanded, 108 | } 109 | ) 110 | end 111 | 112 | it "produces CSS without raising errors" do 113 | expect { site.process }.not_to raise_error 114 | expect(sass_converter.convert(content)).to eql(expanded_css_output) 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /History.markdown: -------------------------------------------------------------------------------- 1 | ## 3.1.0 / 2025-02-02 2 | 3 | ### Minor Enhancements 4 | 5 | * Display sass error in browser with livereload (#160) 6 | * Bump Ruby requirement to `>= 3.1.0` (#166) 7 | * Add support to configure more deprecation warnings from Dart Sass (#164) 8 | 9 | ### Development Fixes 10 | 11 | * Bump `actions/checkout` to v4 (#165) 12 | * Bump RuboCop version to v1.57.x (#167) 13 | * Add Ruby 3.4 to CI matrix (#171) 14 | 15 | ### Documentation 16 | 17 | * Update README with requirements for sass-embedded (#150) 18 | * Fix "implementation" typos (#151) 19 | * Update dart-sass repo link in README.md (#154) 20 | * Update sass embedded protocol documentation link (#157) 21 | * Fix links in README (#168) 22 | 23 | ## 3.0.0 / 2022-12-21 24 | 25 | ### Major Enhancements 26 | 27 | * Drop support for sassc (#140) 28 | * Add quiet_deps and verbose option (#143) 29 | * Remove extra newline in css output (#144) 30 | 31 | ## 2.2.0 / 2022-02-28 32 | 33 | ### Minor Enhancements 34 | 35 | * Support sass-embedded as alternative implementation (#124) 36 | 37 | ### Bug Fixes 38 | 39 | * Source map sources should to be relative to site.source (#119) 40 | * Sourcemaps should not be rendered by Liquid (#123) 41 | 42 | ### Development Fixes 43 | 44 | * Migrate from AppVeyor CI to GH Actions (#125) 45 | * Refactor specs to reduce repetition (#126) 46 | * Reduce overall class size (#132) 47 | * Use new sass-embedded api (#131) 48 | * Add workflow to release gem via GH Actions (#134) 49 | 50 | ### Documentation 51 | 52 | * Update CI status badge (#127) 53 | * Update `sass-embedded` info in `README.md` (#133) 54 | 55 | ## 2.1.0 / 2020-02-05 56 | 57 | ### Development Fixes 58 | 59 | * chore(ci): use Ubuntu 18.04 (bionic) (#100) 60 | 61 | ### Minor Enhancements 62 | 63 | * Fix `Scss#sass_dir_relative_to_site_source` logic (#99) 64 | 65 | ## 2.0.1 / 2019-09-26 66 | 67 | ### Bug Fixes 68 | 69 | * Do not register hooks for documents of type :pages (#94) 70 | * Append theme's sass path after all sanitizations (#96) 71 | 72 | ## 2.0.0 / 2019-08-14 73 | 74 | ### Major Enhancements 75 | 76 | * Migrate to sassc gem (#75) 77 | * Use and test sassc-2.1.0 pre-releases and beyond (#86) 78 | * Drop support for Ruby 2.3 (#90) 79 | 80 | ### Minor Enhancements 81 | 82 | * Generate Sass Sourcemaps (#79) 83 | * Configure Sass to load from theme-gem if possible (#80) 84 | * SyntaxError line and filename are set by SassC (#85) 85 | * Memoize #jekyll_sass_configuration (#82) 86 | 87 | ### Development Fixes 88 | 89 | * Target Ruby 2.3 (#70) 90 | * Lint with rubocop-jekyll (#73) 91 | * Clear out RuboCop TODO (#87) 92 | * Cache stateless regexes in class constants (#83) 93 | * Add appveyor.yml (#76) 94 | 95 | ### Bug Fixes 96 | 97 | * Fix rendering of sourcemap page (#89) 98 | 99 | ## 1.5.2 / 2017-02-03 100 | 101 | ### Development Fixes 102 | 103 | * Test against Ruby 2.5 (#68) 104 | 105 | ## 1.5.1 / 2017-12-02 106 | 107 | ### Minor 108 | 109 | * Security: Bump Rubocop to 0.51 110 | 111 | ### Development Fixes 112 | 113 | * Drop support for Jekyll 2.x and Ruby 2.0 (#62) 114 | * Inherit Jekyll's rubocop config for consistency (#61) 115 | * Define path with __dir__ (#60) 116 | * Fix script/release 117 | 118 | ## 1.5.0 / 2016-11-14 119 | 120 | * Allow `load_paths` in safe mode with sanitization (#50) 121 | * SCSS converter: expand @config["source"] to be "safer". (#55) 122 | * Match Ruby versions with jekyll/jekyll (#46) 123 | * Don't test Jekyll 2.5 against Ruby 2.3. (#52) 124 | 125 | ## 1.4.0 / 2015-12-25 126 | 127 | ### Minor Enhancements 128 | 129 | * Bump Sass to v3.4 and above. (#40) 130 | * Strip byte order mark from generated compressed Sass/SCSS (#39) 131 | * Strip BOM by default, but don't add in the `@charset` by default (#42) 132 | 133 | ### Development Fixes 134 | 135 | * Add Jekyll 2 & 3 to test matrix (#41) 136 | 137 | ## 1.3.0 / 2014-12-07 138 | 139 | ### Minor Enhancements 140 | 141 | * Include line number in syntax error message (#26) 142 | * Raise a `Jekyll::Converters::Scss::SyntaxError` instead of just a `StandardError` (#29) 143 | 144 | ### Development Fixes 145 | 146 | * Fix typo in SCSS converter spec filename (#27) 147 | * Add tests for custom syntax error handling (#29) 148 | 149 | ## 1.2.1 / 2014-08-30 150 | 151 | * Only include something in the sass load path if it exists (#23) 152 | 153 | ## 1.2.0 / 2014-07-31 154 | 155 | ### Minor Enhancements 156 | 157 | * Allow user to specify style in safe mode. (#16) 158 | 159 | ### Development Fixes 160 | 161 | * Only include the `lib/` files in the gem. (#17) 162 | 163 | ## 1.1.0 / 2014-07-29 164 | 165 | ### Minor Enhancements 166 | 167 | * Implement custom load paths (#14) 168 | * Lock down sass configuration when in safe mode. (#15) 169 | 170 | ## 1.0.0 / 2014-05-06 171 | 172 | * Birthday! 173 | * Don't use core extensions (#2) 174 | * Allow users to set style of outputted CSS (#4) 175 | * Determine input syntax based on file extension (#9) 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jekyll Sass Converter 2 | 3 | Let Jekyll build your Sass and SCSS! 4 | 5 | [![Continuous Integration](https://github.com/jekyll/jekyll-sass-converter/actions/workflows/ci.yml/badge.svg)](https://github.com/jekyll/jekyll-sass-converter/actions/workflows/ci.yml) 6 | 7 | 8 | ## Installation 9 | 10 | **Jekyll Sass Converter requires Jekyll 2.0.0 or greater and is bundled 11 | with Jekyll so you don't need to install it if you're already using Jekyll.** 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | gem 'jekyll-sass-converter' 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install jekyll-sass-converter 24 | 25 | ## Usage 26 | 27 | Jekyll Sass Converter comes bundled with Jekyll 2.0.0 and greater. For more 28 | information about usage, visit the [Jekyll Assets Documentation 29 | page](https://jekyllrb.com/docs/assets/). 30 | 31 | ### Sass Implementations 32 | 33 | Starting with `v3.0`, Jekyll Sass Converter uses `sass-embedded` for Sass implementation. 34 | 35 | Please see [migrate from 2.x to 3.x](#migrate-from-2x-to-3x) for more information. 36 | 37 | #### Sass Embedded 38 | 39 | [sass-embedded](https://rubygems.org/gems/sass-embedded) is a host for the 40 | [Sass embedded protocol](https://github.com/sass/sass/blob/HEAD/spec/embedded-protocol.md). 41 | 42 | The host runs [Dart Sass compiler](https://github.com/sass/dart-sass#embedded-dart-sass) as a subprocess 43 | and communicates with the dart-sass compiler by sending / receiving 44 | [protobuf](https://github.com/protocolbuffers/protobuf) messages via the standard 45 | input-output channel. 46 | 47 | ### Source Maps 48 | 49 | Starting with `v2.0`, the Converter will by default generate a _source map_ file along with 50 | the `.css` output file. The _source map_ is useful when we use the web developers tools of 51 | [Chrome](https://developers.google.com/web/tools/chrome-devtools/) or 52 | [Firefox](https://developer.mozilla.org/en-US/docs/Tools) to debug our `.sass` or `.scss` 53 | stylesheets. 54 | 55 | The _source map_ is a file that maps from the output `.css` file to the original source 56 | `.sass` or `.scss` style sheets. Thus enabling the browser to reconstruct the original source 57 | and present the reconstructed original in the debugger. 58 | 59 | ### Configuration Options 60 | 61 | Configuration options are specified in the `_config.yml` file in the following way: 62 | 63 | ```yml 64 | sass: 65 | : 66 | : 67 | ``` 68 | 69 | Available options are: 70 | 71 | * **`style`** 72 | 73 | Sets the style of the CSS-output. 74 | Can be `compressed` or `expanded`. 75 | See the [Sass documentation](https://sass-lang.com/documentation/js-api/types/outputstyle/) 76 | for details. 77 | 78 | Defaults to `expanded`. 79 | 80 | * **`sass_dir`** 81 | 82 | A filesystem-path which should be searched for Sass partials. 83 | 84 | Defaults to `_sass` 85 | 86 | * **`load_paths`** 87 | 88 | An array of additional filesystem-paths which should be searched for Sass partials. 89 | 90 | Defaults to `[]` 91 | 92 | * **`sourcemap`** 93 | 94 | Controls when source maps shall be generated. 95 | 96 | - `never` — causes no source maps to be generated at all. 97 | - `always` — source maps will always be generated. 98 | - `development` — source maps will only be generated if the site is in development 99 | [environment](https://jekyllrb.com/docs/configuration/environments/). 100 | That is, when the environment variable `JEKYLL_ENV` is set to `development`. 101 | 102 | Defaults to `always`. 103 | 104 | * **`quiet_deps`** 105 | 106 | If this option is set to `true`, Sass won’t print warnings that are caused by dependencies. 107 | A “dependency” is defined as any file that’s loaded through `sass_dir` or `load_paths`. 108 | Stylesheets that are imported relative to the entrypoint are not considered dependencies. 109 | 110 | Defaults to `false`. 111 | 112 | * **`verbose`** 113 | 114 | By default, Dart Sass will print only five instances of the same deprecation warning per 115 | compilation to avoid deluging users in console noise. If you set `verbose` to `true`, it will 116 | instead print every deprecation warning it encounters. 117 | 118 | Defaults to `false`. 119 | 120 | * **`fatal_deprecations`** 121 | 122 | An array of deprecations or versions to treat as fatal. 123 | If a deprecation warning of any provided type is encountered during compilation, the compiler will error instead. 124 | If a version is provided, then all deprecations that were active in that compiler version will be treated as fatal. 125 | See the [Sass documentation][sass-deprecation-docs] for all of the deprecations currently used by Sass. 126 | 127 | Defaults to `[]` 128 | 129 | * **`future_deprecations`** 130 | 131 | An array of future deprecations to opt into early. 132 | Future deprecations passed here will be treated as active by the compiler, emitting warnings as necessary. 133 | See the [Sass documentation][sass-deprecation-docs] for all of the deprecations currently used by Sass. 134 | 135 | Defaults to `[]` 136 | 137 | * **`silence_deprecations`** 138 | 139 | An array of active deprecations to ignore. 140 | If a deprecation warning of any provided type is encountered during compilation, the compiler will ignore it instead. 141 | See the [Sass documentation][sass-deprecation-docs] for all of the deprecations currently used by Sass. 142 | 143 | Defaults to `[]` 144 | 145 | [sass-deprecation-docs]: https://sass-lang.com/documentation/js-api/interfaces/deprecations/ 146 | 147 | ## Migrate from 2.x to 3.x 148 | 149 | Classic GitHub Pages experience still uses [1.x version of jekyll-sass-converter](https://pages.github.com/versions/). 150 | 151 | To use latest Jekyll and Jekyll Sass Converter on GitHub Pages, 152 | [you can now deploy to a GitHub Pages site using GitHub Actions](https://jekyllrb.com/docs/continuous-integration/github-actions/). 153 | 154 | ### Requirements 155 | 156 | - Minimum Ruby Version: `Ruby 3.1.0` (all platforms). 157 | 158 | ### Dropped `implementation` Option 159 | 160 | In `v3.0.0`, `sass-embedded` gem becomes the default Sass implementation, and `sassc` gem 161 | is no longer supported. As part of this change, support for Ruby 2.5 is dropped. 162 | 163 | ### Dropped `add_charset` Option 164 | 165 | The Converter will no longer emit `@charset "UTF-8";` or a U+FEFF (byte-order marker) for 166 | `sassify` and `scssify` Jekyll filters so that this option is no longer needed. 167 | 168 | ### Dropped `line_comments` Option 169 | 170 | `sass-embedded` does not support `line_comments` option. 171 | 172 | ### Dropped support of importing files with non-standard extension names 173 | 174 | `sass-embedded` only allows importing files that have extension names of `.sass`, `.scss` 175 | or `.css`. Scss syntax in files with `.css` extension name will result in a syntax error. 176 | 177 | ### Dropped support of importing files relative to site source 178 | 179 | In `v2.x`, the Converter allowed imports using paths relative to site source directory, 180 | even if the site source directory is not in Sass `load_paths`. This is a side effect of a 181 | bug in the Converter, which will remain as is in `v2.x` due to its usage in the wild. 182 | 183 | In `v3.x`, imports using paths relative to site source directory will not work out of box. 184 | To allow these imports, `.` (meaning current directory, or site source directory) need to 185 | be explicitly added to `load_paths` option. 186 | 187 | ### Dropped support of importing files with the same filename as their parent file 188 | 189 | In `v2.x`, the Converter allowed imports of files with the same filename as their parent 190 | file from `sass_dir` or `load_paths`. This is a side effect of a bug in the Converter, 191 | which will remain as is in `v2.x` due to its usage in the wild. 192 | 193 | In `v3.x`, imports using the same filename of parent file will create a circular import. 194 | To fix these imports, rename either of the files, or use complete relative path from the 195 | parent file. 196 | 197 | ### Behavioral Differences in Sass Implementation 198 | 199 | Please see https://github.com/sass/dart-sass#behavioral-differences-from-ruby-sass. 200 | 201 | ## Contributing 202 | 203 | 1. Fork it ( https://github.com/jekyll/jekyll-sass-converter/fork ) 204 | 2. Create your feature branch (`git checkout -b my-new-feature`) 205 | 3. Commit your changes (`git commit -am 'Add some feature'`) 206 | 4. Push to the branch (`git push origin my-new-feature`) 207 | 5. Create new Pull Request 208 | -------------------------------------------------------------------------------- /lib/jekyll/converters/scss.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # stdlib 4 | require "json" 5 | 6 | # 3rd party 7 | require "addressable/uri" 8 | require "sass-embedded" 9 | 10 | # internal 11 | require_relative "../source_map_page" 12 | 13 | module Jekyll 14 | module Converters 15 | class Scss < Converter 16 | EXTENSION_PATTERN = %r!^\.scss$!i 17 | 18 | SyntaxError = Class.new(ArgumentError) 19 | 20 | safe true 21 | priority :low 22 | 23 | # This hook is triggered just before the method {#convert(content)} is executed, it 24 | # associates the Scss (and Sass) converters with their respective sass_page objects. 25 | Jekyll::Hooks.register :pages, :pre_render do |page| 26 | next unless page.is_a?(Jekyll::Page) 27 | 28 | page.converters.each do |converter| 29 | converter.associate_page(page) if converter.is_a?(Jekyll::Converters::Scss) 30 | end 31 | end 32 | 33 | # This hook is triggered just after the method {#convert(content)} has been executed, it 34 | # dissociates the Scss (and Sass) converters with their respective sass_page objects. 35 | Jekyll::Hooks.register :pages, :post_render do |page| 36 | next unless page.is_a?(Jekyll::Page) 37 | 38 | page.converters.each do |converter| 39 | converter.dissociate_page(page) if converter.is_a?(Jekyll::Converters::Scss) 40 | end 41 | end 42 | 43 | ALLOWED_STYLES = %w(expanded compressed).freeze 44 | 45 | # Associate this Converter with the "page" object that manages input and output files for 46 | # this converter. 47 | # 48 | # Note: changing the associated sass_page during the live time of this Converter instance 49 | # may result in inconsistent results. 50 | # 51 | # @param [Jekyll:Page] page The sass_page for which this object acts as converter. 52 | def associate_page(page) 53 | if @sass_page 54 | Jekyll.logger.debug "Sass Converter:", 55 | "sass_page re-assigned: #{@sass_page.name} to #{page.name}" 56 | dissociate_page(page) 57 | return 58 | end 59 | @sass_page = page 60 | end 61 | 62 | # Dissociate this Converter with the "page" object. 63 | # 64 | # @param [Jekyll:Page] page The sass_page for which this object has acted as a converter. 65 | def dissociate_page(page) 66 | unless page.equal?(@sass_page) 67 | Jekyll.logger.debug "Sass Converter:", 68 | "dissociating a page that was never associated #{page.name}" 69 | end 70 | 71 | @source_map_page = nil 72 | @sass_page = nil 73 | @site = nil 74 | end 75 | 76 | def matches(ext) 77 | ext =~ self.class::EXTENSION_PATTERN 78 | end 79 | 80 | def output_ext(_ext) 81 | ".css" 82 | end 83 | 84 | def safe? 85 | !!@config["safe"] 86 | end 87 | 88 | def jekyll_sass_configuration 89 | @jekyll_sass_configuration ||= begin 90 | options = @config["sass"] || {} 91 | unless options["style"].nil? 92 | options["style"] = options["style"].to_s.delete_prefix(":").to_sym 93 | end 94 | options 95 | end 96 | end 97 | 98 | def syntax 99 | :scss 100 | end 101 | 102 | def sass_dir 103 | return "_sass" if jekyll_sass_configuration["sass_dir"].to_s.empty? 104 | 105 | jekyll_sass_configuration["sass_dir"] 106 | end 107 | 108 | def sass_style 109 | style = jekyll_sass_configuration["style"] 110 | ALLOWED_STYLES.include?(style.to_s) ? style.to_sym : :expanded 111 | end 112 | 113 | def user_sass_load_paths 114 | Array(jekyll_sass_configuration["load_paths"]) 115 | end 116 | 117 | def sass_dir_relative_to_site_source 118 | @sass_dir_relative_to_site_source ||= 119 | Jekyll.sanitized_path(site_source, sass_dir).delete_prefix("#{site.source}/") 120 | end 121 | 122 | # rubocop:disable Metrics/AbcSize 123 | def sass_load_paths 124 | paths = user_sass_load_paths + [sass_dir_relative_to_site_source] 125 | 126 | # Sanitize paths to prevent any attack vectors (.e.g. `/**/*`) 127 | paths.map! { |path| Jekyll.sanitized_path(site_source, path) } if safe? 128 | 129 | # Expand file globs (e.g. `node_modules/*/node_modules` ) 130 | Dir.chdir(site_source) do 131 | paths = paths.flat_map { |path| Dir.glob(path) } 132 | 133 | paths.map! do |path| 134 | # Sanitize again in case globbing was able to do something crazy. 135 | safe? ? Jekyll.sanitized_path(site_source, path) : File.expand_path(path) 136 | end 137 | end 138 | 139 | paths.uniq! 140 | paths << site.theme.sass_path if site.theme&.sass_path 141 | paths.select { |path| File.directory?(path) } 142 | end 143 | # rubocop:enable Metrics/AbcSize 144 | 145 | def sass_configs 146 | { 147 | :load_paths => sass_load_paths, 148 | :charset => !associate_page_failed?, 149 | :source_map => sourcemap_required?, 150 | :source_map_include_sources => true, 151 | :style => sass_style, 152 | :syntax => syntax, 153 | :url => sass_file_url, 154 | :quiet_deps => quiet_deps_option, 155 | :verbose => verbose_option, 156 | :fatal_deprecations => fatal_deprecations, 157 | :future_deprecations => future_deprecations, 158 | :silence_deprecations => silence_deprecations, 159 | } 160 | end 161 | 162 | def convert(content) 163 | output = ::Sass.compile_string(content, **sass_configs) 164 | result = output.css 165 | 166 | if sourcemap_required? 167 | source_map = process_source_map(output.source_map) 168 | generate_source_map_page(source_map) 169 | 170 | if (sm_url = source_mapping_url) 171 | result += "#{sass_style == :compressed ? "" : "\n\n"}/*# sourceMappingURL=#{sm_url} */" 172 | end 173 | end 174 | 175 | result 176 | rescue ::Sass::CompileError => e 177 | Jekyll.logger.error e.full_message 178 | if livereload? 179 | e.to_css # Render error message in browser window 180 | else 181 | raise SyntaxError, e.message 182 | end 183 | end 184 | 185 | private 186 | 187 | # The Page instance for which this object acts as a converter. 188 | attr_reader :sass_page 189 | 190 | def associate_page_failed? 191 | !sass_page 192 | end 193 | 194 | # Returns `true` if jekyll is serving with livereload. 195 | def livereload? 196 | !!(@config["serving"] && @config["livereload"]) 197 | end 198 | 199 | # The URL of the input scss (or sass) file. This information will be used for error reporting. 200 | def sass_file_url 201 | return if associate_page_failed? 202 | 203 | file_url_from_path(Jekyll.sanitized_path(site_source, sass_page.relative_path)) 204 | end 205 | 206 | # The value of the `sourcemap` option chosen by the user. 207 | # 208 | # This option controls when sourcemaps shall be generated or not. 209 | # 210 | # Returns the value of the `sourcemap`-option chosen by the user or ':always' by default. 211 | def sourcemap_option 212 | jekyll_sass_configuration.fetch("sourcemap", :always).to_sym 213 | end 214 | 215 | # Determines whether a sourcemap shall be generated or not. 216 | # 217 | # Returns `true` if a sourcemap shall be generated, `false` otherwise. 218 | def sourcemap_required? 219 | return false if associate_page_failed? || sourcemap_option == :never 220 | return true if sourcemap_option == :always 221 | 222 | !(sourcemap_option == :development && Jekyll.env != "development") 223 | end 224 | 225 | def source_map_page 226 | return if associate_page_failed? 227 | 228 | @source_map_page ||= SourceMapPage.new(sass_page) 229 | end 230 | 231 | # Returns the directory that source map sources are relative to. 232 | def sass_source_root 233 | if associate_page_failed? 234 | site_source 235 | else 236 | Jekyll.sanitized_path(site_source, File.dirname(sass_page.relative_path)) 237 | end 238 | end 239 | 240 | # Converts file urls in source map to relative paths. 241 | # 242 | # Returns processed source map string. 243 | def process_source_map(source_map) 244 | map_data = JSON.parse(source_map) 245 | unless associate_page_failed? 246 | map_data["file"] = Addressable::URI.encode("#{sass_page.basename}.css") 247 | end 248 | source_root_url = Addressable::URI.parse(file_url_from_path("#{sass_source_root}/")) 249 | map_data["sources"].map! do |s| 250 | s.start_with?("file:") ? Addressable::URI.parse(s).route_from(source_root_url).to_s : s 251 | end 252 | JSON.generate(map_data) 253 | end 254 | 255 | # Adds the source-map to the source-map-page and adds it to `site.pages`. 256 | def generate_source_map_page(source_map) 257 | return if associate_page_failed? 258 | 259 | source_map_page.source_map(source_map) 260 | site.pages << source_map_page 261 | end 262 | 263 | # Returns a source mapping url for given source-map. 264 | def source_mapping_url 265 | return if associate_page_failed? 266 | 267 | Addressable::URI.encode("#{sass_page.basename}.css.map") 268 | end 269 | 270 | def site 271 | associate_page_failed? ? Jekyll.sites.last : sass_page.site 272 | end 273 | 274 | def site_source 275 | site.source 276 | end 277 | 278 | def file_url_from_path(path) 279 | Addressable::URI.encode("file://#{path.start_with?("/") ? "" : "/"}#{path}") 280 | end 281 | 282 | # Returns the value of the `quiet_deps`-option chosen by the user or 'false' by default. 283 | def quiet_deps_option 284 | !!jekyll_sass_configuration.fetch("quiet_deps", false) 285 | end 286 | 287 | # Returns the value of the `verbose`-option chosen by the user or 'false' by default. 288 | def verbose_option 289 | !!jekyll_sass_configuration.fetch("verbose", false) 290 | end 291 | 292 | # Returns the value of the `fatal_deprecations`-option or '[]' by default. 293 | def fatal_deprecations 294 | Array(jekyll_sass_configuration["fatal_deprecations"]) 295 | end 296 | 297 | # Returns the value of the `future_deprecations`-option or '[]' by default. 298 | def future_deprecations 299 | Array(jekyll_sass_configuration["future_deprecations"]) 300 | end 301 | 302 | # Returns the value of the `silence_deprecations`-option or '[]' by default. 303 | def silence_deprecations 304 | Array(jekyll_sass_configuration["silence_deprecations"]) 305 | end 306 | end 307 | end 308 | end 309 | -------------------------------------------------------------------------------- /spec/scss_converter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "tmpdir" 5 | 6 | describe(Jekyll::Converters::Scss) do 7 | let(:site) do 8 | Jekyll::Site.new(site_configuration) 9 | end 10 | 11 | let(:scss_converter) do 12 | scss_converter_instance(site) 13 | end 14 | 15 | let(:content) do 16 | <<~SCSS 17 | $font-stack: Helvetica, sans-serif; 18 | body { 19 | font-family: $font-stack; 20 | font-color: fuschia; 21 | } 22 | SCSS 23 | end 24 | 25 | let(:expanded_css_output) do 26 | <<~CSS.chomp 27 | body { 28 | font-family: Helvetica, sans-serif; 29 | font-color: fuschia; 30 | } 31 | CSS 32 | end 33 | 34 | let(:invalid_content) do 35 | <<~SCSS 36 | $font-stack: Helvetica 37 | body { 38 | font-family: $font-stack; 39 | SCSS 40 | end 41 | 42 | def converter(overrides = {}) 43 | scss_converter_instance(site).dup.tap do |obj| 44 | obj.instance_variable_get(:@config)["sass"] = overrides 45 | end 46 | end 47 | 48 | context "matching file extensions" do 49 | it "matches .scss files" do 50 | expect(converter.matches(".scss")).to be_truthy 51 | end 52 | 53 | it "does not match .sass files" do 54 | expect(converter.matches(".sass")).to be_falsey 55 | end 56 | end 57 | 58 | context "determining the output file extension" do 59 | it "always outputs the .css file extension" do 60 | expect(converter.output_ext(".always-css")).to eql(".css") 61 | end 62 | end 63 | 64 | context "when building configurations" do 65 | it "set the load paths to the _sass dir relative to site source" do 66 | expect(converter.sass_configs[:load_paths]).to eql([source_dir("_sass")]) 67 | end 68 | 69 | it "allow for other styles" do 70 | expect(converter("style" => :compressed).sass_configs[:style]).to eql(:compressed) 71 | end 72 | 73 | context "when specifying sass dirs" do 74 | context "when the sass dir exists" do 75 | it "allow the user to specify a different sass dir" do 76 | create_directory(source_dir("_scss")) 77 | override = { "sass_dir" => "_scss" } 78 | expect(converter(override).sass_configs[:load_paths]).to eql([source_dir("_scss")]) 79 | remove_directory(source_dir("_scss")) 80 | end 81 | 82 | it "not allow sass_dirs outside of site source" do 83 | expect( 84 | converter("sass_dir" => "/etc/passwd").sass_dir_relative_to_site_source 85 | ).to eql("etc/passwd") 86 | end 87 | end 88 | end 89 | 90 | context "in safe mode" do 91 | let(:verter) do 92 | Jekyll::Converters::Scss.new( 93 | site.config.merge( 94 | "sass" => {}, 95 | "safe" => true 96 | ) 97 | ) 98 | end 99 | 100 | it "does not allow caching" do 101 | expect(verter.sass_configs[:cache]).to be_falsey 102 | end 103 | 104 | it "forces load_paths to be just the local load path" do 105 | expect(verter.sass_configs[:load_paths]).to eql([source_dir("_sass")]) 106 | end 107 | 108 | it "allows the user to specify the style" do 109 | allow(verter).to receive(:sass_style).and_return(:compressed) 110 | expect(verter.sass_configs[:style]).to eql(:compressed) 111 | end 112 | 113 | it "defaults style to :expanded for sass-embedded" do 114 | expect(verter.sass_configs[:style]).to eql(:expanded) 115 | end 116 | 117 | it "at least contains :syntax and :load_paths keys" do 118 | expect(verter.sass_configs.keys).to include(:load_paths, :syntax) 119 | end 120 | end 121 | end 122 | 123 | context "converting SCSS" do 124 | it "produces CSS" do 125 | expect(converter.convert(content)).to eql(expanded_css_output) 126 | end 127 | 128 | it "includes the syntax error line in the syntax error message" do 129 | expected = %r!expected ";"!i 130 | expect { scss_converter.convert(invalid_content) }.to( 131 | raise_error(Jekyll::Converters::Scss::SyntaxError, expected) 132 | ) 133 | end 134 | 135 | it "does not include the charset without an associated page" do 136 | overrides = { "style" => :expanded } 137 | result = converter(overrides).convert(%(a{content:"あ"})) 138 | expect(result).to eql(%(a {\n content: "あ";\n})) 139 | end 140 | 141 | it "does not include the BOM without an associated page" do 142 | overrides = { "style" => :compressed } 143 | result = converter(overrides).convert(%(a{content:"あ"})) 144 | expect(result).to eql(%(a{content:"あ"})) 145 | expect(result.bytes.to_a[0..2]).not_to eql([0xEF, 0xBB, 0xBF]) 146 | end 147 | end 148 | 149 | context "importing partials" do 150 | let(:test_css_file) { dest_dir("css/main.css") } 151 | before(:each) { site.process } 152 | 153 | it "outputs the CSS file" do 154 | expect(File.exist?(test_css_file)).to be_truthy 155 | end 156 | 157 | it "imports SCSS partial" do 158 | expect(File.read(test_css_file)).to eql( 159 | ".half{width:50%}/*# sourceMappingURL=main.css.map */" 160 | ) 161 | end 162 | 163 | it "uses a compressed style" do 164 | instance = scss_converter_instance(site) 165 | expect(instance.jekyll_sass_configuration).to eql("style" => :compressed) 166 | expect(instance.sass_configs[:style]).to eql(:compressed) 167 | end 168 | end 169 | 170 | context "importing from external libraries" do 171 | let(:external_library) { source_dir("bower_components/jquery") } 172 | let(:test_css_file) { dest_dir("css", "main.css") } 173 | 174 | context "in unsafe mode" do 175 | let(:site) do 176 | make_site( 177 | "source" => sass_lib, 178 | "sass" => { 179 | "load_paths" => external_library, 180 | } 181 | ) 182 | end 183 | 184 | before(:each) { create_directory external_library } 185 | after(:each) { remove_directory external_library } 186 | 187 | it "recognizes the new load path" do 188 | expect(scss_converter.sass_load_paths).to include(external_library) 189 | end 190 | 191 | it "ensures the sass_dir is still in the load path" do 192 | expect(scss_converter.sass_load_paths).to include(sass_lib("_sass")) 193 | end 194 | 195 | it "brings in the grid partial" do 196 | site.process 197 | 198 | expected = "a {\n color: #999999;\n}\n\n/*# sourceMappingURL=main.css.map */" 199 | expect(File.read(test_css_file)).to eql(expected) 200 | end 201 | 202 | context "with the sass_dir specified twice" do 203 | let(:site) do 204 | make_site( 205 | "source" => sass_lib, 206 | "sass" => { 207 | "load_paths" => [ 208 | external_library, 209 | sass_lib("_sass"), 210 | ], 211 | } 212 | ) 213 | end 214 | 215 | it "ensures the sass_dir only occurrs once in the load path" do 216 | expect(scss_converter.sass_load_paths).to eql([external_library, sass_lib("_sass")]) 217 | end 218 | end 219 | end 220 | 221 | context "in safe mode" do 222 | let(:site) do 223 | make_site( 224 | "safe" => true, 225 | "source" => sass_lib, 226 | "sass" => { 227 | "load_paths" => external_library, 228 | } 229 | ) 230 | end 231 | 232 | it "ignores the new load path" do 233 | expect(scss_converter.sass_load_paths).not_to include(external_library) 234 | end 235 | 236 | it "ensures the sass_dir is the entire load path" do 237 | expect(scss_converter.sass_load_paths).to eql([sass_lib("_sass")]) 238 | end 239 | end 240 | end 241 | 242 | context "importing from internal libraries" do 243 | let(:internal_library) { source_dir("bower_components/jquery") } 244 | 245 | before(:each) { create_directory internal_library } 246 | after(:each) { remove_directory internal_library } 247 | 248 | context "in unsafe mode" do 249 | let(:site) do 250 | make_site( 251 | "sass" => { 252 | "load_paths" => ["bower_components/*"], 253 | } 254 | ) 255 | end 256 | 257 | it "expands globs" do 258 | expect(scss_converter.sass_load_paths).to include(internal_library) 259 | end 260 | end 261 | 262 | context "in safe mode" do 263 | let(:site) do 264 | make_site( 265 | "safe" => true, 266 | "sass" => { 267 | "load_paths" => [ 268 | Dir.tmpdir, 269 | "bower_components/*", 270 | "../..", 271 | ], 272 | } 273 | ) 274 | end 275 | 276 | it "allows local load paths" do 277 | expect(scss_converter.sass_load_paths).to include(internal_library) 278 | end 279 | 280 | it "ignores external load paths" do 281 | expect(scss_converter.sass_load_paths).not_to include(Dir.tmpdir) 282 | end 283 | 284 | it "does not allow traversing outside source directory" do 285 | scss_converter.sass_load_paths.each do |path| 286 | expect(path).to include(source_dir) 287 | expect(path).not_to include("..") 288 | end 289 | end 290 | end 291 | end 292 | 293 | context "with valid sass paths in a theme" do 294 | context "in unsafe mode" do 295 | let(:site) do 296 | make_site("theme" => "minima") 297 | end 298 | 299 | it "includes the theme's sass directory" do 300 | expect(site.theme.sass_path).to be_truthy 301 | expect(scss_converter.sass_load_paths).to include(site.theme.sass_path) 302 | end 303 | end 304 | 305 | context "in safe mode" do 306 | let(:site) do 307 | make_site( 308 | "theme" => "minima", 309 | "safe" => true 310 | ) 311 | end 312 | 313 | it "includes the theme's sass directory" do 314 | expect(site.safe).to be true 315 | expect(site.theme.sass_path).to be_truthy 316 | expect(converter.sass_load_paths).to include(site.theme.sass_path) 317 | end 318 | end 319 | end 320 | 321 | context "in a site with a collection labelled 'pages'" do 322 | let(:site) do 323 | make_site( 324 | "source" => File.expand_path("pages-collection", __dir__), 325 | "sass" => { 326 | "style" => :expanded, 327 | }, 328 | "collections" => { 329 | "pages" => { 330 | "output" => true, 331 | }, 332 | } 333 | ) 334 | end 335 | 336 | it "produces CSS without raising errors" do 337 | expect { site.process }.not_to raise_error 338 | expect(scss_converter.convert(content)).to eql(expanded_css_output) 339 | end 340 | end 341 | 342 | context "in a site nested inside directory with square brackets" do 343 | let(:site) do 344 | make_site( 345 | "source" => File.expand_path("[alpha]beta", __dir__), 346 | "sass" => { 347 | "style" => :expanded, 348 | } 349 | ) 350 | end 351 | 352 | it "produces CSS without raising errors" do 353 | expect { site.process }.not_to raise_error 354 | expect(scss_converter.convert(content)).to eql(expanded_css_output) 355 | end 356 | end 357 | 358 | context "generating sourcemap" do 359 | let(:sourcemap_file) { dest_dir("css/app.css.map") } 360 | let(:sourcemap_contents) { File.binread(sourcemap_file) } 361 | before { site.process } 362 | 363 | it "outputs the sourcemap file" do 364 | expect(File.exist?(sourcemap_file)).to be true 365 | end 366 | 367 | it "should not have Liquid expressions rendered" do 368 | expect(sourcemap_contents).to include("{{ site.mytheme.skin }}") 369 | end 370 | 371 | context "in a site with source not equal to its default value of `Dir.pwd`" do 372 | let(:site) do 373 | make_site( 374 | "source" => File.expand_path("nested_source/src", __dir__) 375 | ) 376 | end 377 | let(:sourcemap_file) { dest_dir("css/main.css.map") } 378 | let(:sourcemap_data) { JSON.parse(File.binread(sourcemap_file)) } 379 | 380 | before(:each) { site.process } 381 | 382 | it "outputs the sourcemap file" do 383 | expect(File.exist?(sourcemap_file)).to be_truthy 384 | end 385 | 386 | it "contains relevant sass sources" do 387 | sources = sourcemap_data["sources"] 388 | # paths are relative to input file 389 | expect(sources).to include("../_sass/_grid.scss") 390 | expect(sources).to_not include("../_sass/_color.scss") # not imported into "main.scss" 391 | end 392 | 393 | it "does not leak directory structure outside of `site.source`" do 394 | site_source_relative_from_pwd = \ 395 | Pathname.new(site.source) 396 | .relative_path_from(Pathname.new(Dir.pwd)) 397 | .to_s 398 | relative_path_parts = site_source_relative_from_pwd.split(File::SEPARATOR) 399 | 400 | expect(site_source_relative_from_pwd).to eql("spec/nested_source/src") 401 | expect(relative_path_parts).to eql(%w(spec nested_source src)) 402 | 403 | relative_path_parts.each do |dirname| 404 | sourcemap_data["sources"].each do |fpath| 405 | expect(fpath).to_not include(dirname) 406 | end 407 | end 408 | end 409 | end 410 | end 411 | end 412 | --------------------------------------------------------------------------------