diff --git a/README.md b/README.md index 5fdb577..4aa1163 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,66 @@ Jekyll Sass Converter comes bundled with Jekyll 2.0.0 and greater. For more information about usage, visit the [Jekyll Assets Documentation page](http://jekyllrb.com/docs/assets/). +### Source Maps + +Starting with `v2.0`, the Converter will by default generate a _source map_ file along with +the `.css` output file. The _source map_ is useful when we use the web developers tools of +[Chrome](https://developers.google.com/web/tools/chrome-devtools/) or +[Firefox](https://developer.mozilla.org/en-US/docs/Tools) to debug our `.sass` or `.scss` +stylesheets. + +The _source map_ is a file that maps from the output `.css` file to the original source +`.sass` or `.scss` style sheets. Thus enabling the browser to reconstruct the original source +and present the reconstructed original in the debugger. + +### Configuration Options + +Configuration options are specified in the `_config.yml` file in the following way: + + ```yml + sass: + : + : + ``` + +Available options are: + + * **`style`** + + Sets the style of the CSS-output. + Can be `nested`, `compact`, `compressed`, or `expanded`. + See the [SASS_REFERENCE](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#output_style) + for details. + + Defaults to `compact`. + + * **`sass_dir`** + + An array of filesystem-paths which should be searched for Sass partials. + + Defaults to `_sass` + + * **`line_comments`** + + When set to _true_, the line number and filename of the source is included in the compiled + CSS-file. Useful for debugging when the _source map_ is not available, but might + considerably increase the size of the generated CSS files. + + Defaults to `false`. + + * **`sourcemap`** + + Controls when source maps shall be generated. + + - `never` — causes no source maps to be generated at all. + - `always` — source maps will always be generated. + - `development` — source maps will only be generated if the site is in development + [environment](https://jekyllrb.com/docs/configuration/environments/). + That is, when the environment variable `JEKYLL_ENV` is set to `development`. + + Defaults to `always`. + + ## Contributing 1. Fork it ( http://github.com/jekyll/jekyll-sass-converter/fork ) diff --git a/lib/fake-sass.rb b/lib/fake-sass.rb new file mode 100644 index 0000000..63b0236 --- /dev/null +++ b/lib/fake-sass.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# This module helps to get rid of the now deprecated +# [ruby-sass](https://github.com/sass/ruby-sass). +# +# Some modules used in jekyll depend on the function `Sass.load_paths` +# from ruby-sass. (see for example `Jekyll::Theme.configure_sass`). +# +# This module provides a workaround when ruby-sass is not installed +# by faking this functionality... +# +# Please note that this module should never be installed __together__ with ruby-sass. +module Sass + # The global load paths for Sass files. This is meant for plugins and libraries to register + # the paths to their Sass stylesheets to that they may be `@imported`. This load path is used + # by every instance of {Sass::Engine}. + # They are lower-precedence than any load paths passed in via the + # {file:SASS_REFERENCE.md#load_paths-option `:load_paths` option}. + # + # If the `SASS_PATH` environment variable is set, the initial value of `load_paths` will be + # initialized based on that. The variable should be a colon-separated list of path names + # (semicolon-separated on Windows). + # + # Note that files on the global load path are never compiled to CSS themselves, even if they + # aren't partials. They exist only to be imported. + # + # @example + # Sass.load_paths << File.dirname(__FILE__ + '/sass') + # @return [Array] + def self.load_paths + @load_paths ||= ENV["SASS_PATH"].to_s.split(File::PATH_SEPARATOR) + end +end diff --git a/lib/jekyll-sass-converter.rb b/lib/jekyll-sass-converter.rb index ed73199..e9624e4 100644 --- a/lib/jekyll-sass-converter.rb +++ b/lib/jekyll-sass-converter.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "fake-sass" require "jekyll-sass-converter/version" require "jekyll/converters/scss" require "jekyll/converters/sass" diff --git a/lib/jekyll/converters/scss.rb b/lib/jekyll/converters/scss.rb index 9715243..4330043 100644 --- a/lib/jekyll/converters/scss.rb +++ b/lib/jekyll/converters/scss.rb @@ -2,6 +2,7 @@ require "sassc" require "jekyll/utils" +require "jekyll/source_map_page" module Jekyll module Converters @@ -14,8 +15,55 @@ class Scss < Converter safe true priority :low + # This hook is triggered just before the method {#convert(content)} is executed, it + # associates the Scss (and Sass) converters with their respective sass_page objects. + Jekyll::Hooks.register :pages, :pre_render do |page| + page.converters.each do |converter| + converter.associate_page(page) if converter.is_a?(Jekyll::Converters::Scss) + end + end + + # This hook is triggered just after the method {#convert(content)} has been executed, it + # dissociates the Scss (and Sass) converters with their respective sass_page objects. + Jekyll::Hooks.register :pages, :post_render do |page| + page.converters.each do |converter| + converter.dissociate_page(page) if converter.is_a?(Jekyll::Converters::Scss) + end + end + ALLOWED_STYLES = %w(nested expanded compact compressed).freeze + # Associate this Converter with the "page" object that manages input and output files for + # this converter. + # + # Note: changing the associated sass_page during the live time of this Converter instance + # may result in inconsistent results. + # + # @param [Jekyll:Page] page The sass_page for which this object acts as converter. + def associate_page(page) + if @sass_page + Jekyll.logger.debug "Sass Converter:", + "sass_page re-assigned: #{@sass_page.name} to #{page.name}" + dissociate_page(page) + return + end + @sass_page = page + end + + # Dissociate this Converter with the "page" object. + # + # @param [Jekyll:Page] page The sass_page for which this object has acted as a converter. + def dissociate_page(page) + unless page.equal?(@sass_page) + Jekyll.logger.debug "Sass Converter:", + "dissociating a page that was never associated #{page.name}" + end + + @source_map_page = nil + @sass_page = nil + @site = nil + end + def matches(ext) ext =~ self.class::EXTENSION_PATTERN end @@ -40,12 +88,7 @@ def jekyll_sass_configuration def sass_build_configuration_options(overrides) if safe? - { - :load_paths => sass_load_paths, - :syntax => syntax, - :style => sass_style, - :cache => false, - } + overrides else Jekyll::Utils.symbolize_hash_keys( Jekyll::Utils.deep_merge_hashes( @@ -81,7 +124,9 @@ def sass_dir_relative_to_site_source # rubocop:disable Metrics/AbcSize def sass_load_paths - paths = user_sass_load_paths + [sass_dir_relative_to_site_source] + paths = user_sass_load_paths + + [sass_dir_relative_to_site_source] + + Array(::Sass.load_paths) paths << site.theme.sass_path if site.theme&.sass_path if safe? @@ -117,14 +162,23 @@ def add_charset? def sass_configs sass_build_configuration_options( - "syntax" => syntax, - "cache" => allow_caching?, - "load_paths" => sass_load_paths + :style => sass_style, + :syntax => syntax, + :filename => filename, + :output_path => output_path, + :source_map_file => source_map_file, + :load_paths => sass_load_paths, + :omit_source_map_url => !sourcemap_required?, + :source_map_contents => true, + :line_comments_option => line_comments_option ) end def convert(content) - output = SassC::Engine.new(content.dup, sass_configs).render + config = sass_configs + engine = SassC::Engine.new(content.dup, config) + output = engine.render + generate_source_map(engine) if sourcemap_required? replacement = add_charset? ? '@charset "UTF-8";' : "" output.sub(BYTE_ORDER_MARK, replacement) rescue SassC::SyntaxError => e @@ -133,8 +187,95 @@ def convert(content) private + # The Page instance for which this object acts as a converter. + attr_reader :sass_page + + def associate_page_failed? + !sass_page + end + + # The name of the input scss (or sass) file. This information will be used for error + # reporting and will written into the source map file as main source. + # + # Returns the name of the input file or "stdin" if #associate_page failed + def filename + return "stdin" if associate_page_failed? + + sass_page.name + end + + # The value of the `line_comments` option. + # When set to `true` causes the line number and filename of the source be emitted into the + # compiled CSS-file. Useful for debugging when the source-map is not available. + # + # Returns the value of the `line_comments`-option chosen by the user or 'false' by default. + def line_comments_option + jekyll_sass_configuration.fetch("line_comments", false) + end + + # The value of the `sourcemap` option chosen by the user. + # + # This option controls when sourcemaps shall be generated or not. + # + # Returns the value of the `sourcemap`-option chosen by the user or ':always' by default. + def sourcemap_option + jekyll_sass_configuration.fetch("sourcemap", :always).to_sym + end + + # Determines whether a sourcemap shall be generated or not. + # + # Returns `true` if a sourcemap shall be generated, `false` otherwise. + def sourcemap_required? + return false if associate_page_failed? || sourcemap_option == :never + return true if sourcemap_option == :always + + !(sourcemap_option == :development && Jekyll.env != "development") + end + + # The name of the generated css file. This information will be written into the source map + # file as a backward reference to the input. + # + # Returns the name of the css file or "stdin.css" if #associate_page failed + def output_path + return "stdin.css" if associate_page_failed? + + sass_page.basename + ".css" + end + + # The name of the generated source map file. This information will be written into the + # css file to reference to the source map. + # + # Returns the name of the css file or "" if #associate_page failed + def source_map_file + return "" if associate_page_failed? + + sass_page.basename + ".css.map" + end + + def source_map_page + return if associate_page_failed? + + @source_map_page ||= SourceMapPage.new(sass_page) + end + + # Reads the source-map from the engine and adds it to the source-map-page. + # + # @param [::SassC::Engine] engine The sass Compiler engine. + def generate_source_map(engine) + return if associate_page_failed? + + source_map_page.source_map(engine.source_map) + site.pages << source_map_page + rescue ::SassC::NotRenderedError => e + Jekyll.logger.warn "Could not generate source map #{e.message} => #{e.cause}" + end + def site - @site ||= Jekyll.sites.last + if associate_page_failed? + Jekyll.sites.last + else + sass_page.site + end end def site_source diff --git a/lib/jekyll/source_map_page.rb b/lib/jekyll/source_map_page.rb new file mode 100644 index 0000000..c5fe766 --- /dev/null +++ b/lib/jekyll/source_map_page.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Jekyll + # A Jekyll::Page subclass to manage the source map file associated with + # a given scss / sass page. + class SourceMapPage < Page + # Initialize a new SourceMapPage. + # + # @param [Jekyll::Page] css_page The Page object that manages the css file. + def initialize(css_page) + @site = css_page.site + @dir = css_page.dir + @data = css_page.data + @name = css_page.basename + ".css.map" + + process(@name) + Jekyll::Hooks.trigger :pages, :post_init, self + end + + def source_map(map) + self.output = map + end + + def ext + ".map" + end + + def asset_file? + true + end + + # @return[String] the object as a debug String. + def inspect + "#<#{self.class} @name=#{name.inspect}>" + end + end +end diff --git a/spec/sass_converter_spec.rb b/spec/sass_converter_spec.rb index af50649..538f2a2 100644 --- a/spec/sass_converter_spec.rb +++ b/spec/sass_converter_spec.rb @@ -17,7 +17,7 @@ end let(:css_output) do <<~CSS - body {\n font-family: Helvetica, sans-serif;\n font-color: fuschia; } + body { font-family: Helvetica, sans-serif; font-color: fuschia; } CSS end let(:invalid_content) do diff --git a/spec/scss_converter_spec.rb b/spec/scss_converter_spec.rb index f90e6b9..b5936d0 100644 --- a/spec/scss_converter_spec.rb +++ b/spec/scss_converter_spec.rb @@ -18,7 +18,7 @@ end let(:css_output) do <<~CSS - body {\n font-family: Helvetica, sans-serif;\n font-color: fuschia; } + body { font-family: Helvetica, sans-serif; font-color: fuschia; } CSS end let(:invalid_content) do @@ -56,9 +56,10 @@ def converter(overrides = {}) end context "when building configurations" do - it "allow caching in unsafe mode" do - expect(converter.sass_configs[:cache]).to be_truthy - end + # Caching is no more a feature with sassC + # it "allow caching in unsafe mode" do + # expect(converter.sass_configs[:cache]).to be_truthy + # end it "set the load paths to the _sass dir relative to site source" do expect(converter.sass_configs[:load_paths]).to eql([source_dir("_sass")]) @@ -112,8 +113,8 @@ def converter(overrides = {}) expect(verter.sass_configs[:style]).to eql(:compact) end - it "only contains :syntax, :cache, :style, and :load_paths keys" do - expect(verter.sass_configs.keys).to eql([:load_paths, :syntax, :style, :cache]) + it "at least contains :syntax and :load_paths keys" do + expect(verter.sass_configs.keys).to include(:load_paths, :syntax) end end end @@ -154,7 +155,9 @@ def converter(overrides = {}) end it "imports SCSS partial" do - expect(File.read(test_css_file)).to eql(compressed(".half {\n width: 50%; }\n")) + expect(File.read(test_css_file)).to eql( + ".half{width:50%}\n\n/*# sourceMappingURL=main.css.map */" + ) end it "uses a compressed style" do @@ -195,7 +198,9 @@ def converter(overrides = {}) it "brings in the grid partial" do site.process - expect(File.read(test_css_file)).to eql("a {\n color: #999999; }\n") + expect(File.read(test_css_file)).to eql( + "a { color: #999999; }\n\n/*# sourceMappingURL=main.css.map */" + ) end context "with the sass_dir specified twice" do