├── 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 |
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 | [](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 |
--------------------------------------------------------------------------------