This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "jekyll/converters/scss"
|
||||
|
||||
module Jekyll
|
||||
module Converters
|
||||
class Sass < Scss
|
||||
EXTENSION_PATTERN = %r!^\.sass$!i
|
||||
|
||||
safe true
|
||||
priority :low
|
||||
|
||||
def syntax
|
||||
:indented
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,308 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# stdlib
|
||||
require "json"
|
||||
|
||||
# 3rd party
|
||||
require "addressable/uri"
|
||||
require "sass-embedded"
|
||||
|
||||
# internal
|
||||
require_relative "../source_map_page"
|
||||
|
||||
module Jekyll
|
||||
module Converters
|
||||
class Scss < Converter
|
||||
EXTENSION_PATTERN = %r!^\.scss$!i
|
||||
|
||||
SyntaxError = Class.new(ArgumentError)
|
||||
|
||||
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|
|
||||
next unless page.is_a?(Jekyll::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|
|
||||
next unless page.is_a?(Jekyll::Page)
|
||||
|
||||
page.converters.each do |converter|
|
||||
converter.dissociate_page(page) if converter.is_a?(Jekyll::Converters::Scss)
|
||||
end
|
||||
end
|
||||
|
||||
ALLOWED_STYLES = %w(expanded 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
|
||||
|
||||
def output_ext(_ext)
|
||||
".css"
|
||||
end
|
||||
|
||||
def safe?
|
||||
!!@config["safe"]
|
||||
end
|
||||
|
||||
def jekyll_sass_configuration
|
||||
@jekyll_sass_configuration ||= begin
|
||||
options = @config["sass"] || {}
|
||||
unless options["style"].nil?
|
||||
options["style"] = options["style"].to_s.delete_prefix(":").to_sym
|
||||
end
|
||||
options
|
||||
end
|
||||
end
|
||||
|
||||
def syntax
|
||||
:scss
|
||||
end
|
||||
|
||||
def sass_dir
|
||||
return "_sass" if jekyll_sass_configuration["sass_dir"].to_s.empty?
|
||||
|
||||
jekyll_sass_configuration["sass_dir"]
|
||||
end
|
||||
|
||||
def sass_style
|
||||
style = jekyll_sass_configuration["style"]
|
||||
ALLOWED_STYLES.include?(style.to_s) ? style.to_sym : :expanded
|
||||
end
|
||||
|
||||
def user_sass_load_paths
|
||||
Array(jekyll_sass_configuration["load_paths"])
|
||||
end
|
||||
|
||||
def sass_dir_relative_to_site_source
|
||||
@sass_dir_relative_to_site_source ||=
|
||||
Jekyll.sanitized_path(site_source, sass_dir).delete_prefix("#{site.source}/")
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def sass_load_paths
|
||||
paths = user_sass_load_paths + [sass_dir_relative_to_site_source]
|
||||
|
||||
# Sanitize paths to prevent any attack vectors (.e.g. `/**/*`)
|
||||
paths.map! { |path| Jekyll.sanitized_path(site_source, path) } if safe?
|
||||
|
||||
# Expand file globs (e.g. `node_modules/*/node_modules` )
|
||||
Dir.chdir(site_source) do
|
||||
paths = paths.flat_map { |path| Dir.glob(path) }
|
||||
|
||||
paths.map! do |path|
|
||||
# Sanitize again in case globbing was able to do something crazy.
|
||||
safe? ? Jekyll.sanitized_path(site_source, path) : File.expand_path(path)
|
||||
end
|
||||
end
|
||||
|
||||
paths.uniq!
|
||||
paths << site.theme.sass_path if site.theme&.sass_path
|
||||
paths.select { |path| File.directory?(path) }
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
def sass_configs
|
||||
{
|
||||
:load_paths => sass_load_paths,
|
||||
:charset => !associate_page_failed?,
|
||||
:source_map => sourcemap_required?,
|
||||
:source_map_include_sources => true,
|
||||
:style => sass_style,
|
||||
:syntax => syntax,
|
||||
:url => sass_file_url,
|
||||
:quiet_deps => quiet_deps_option,
|
||||
:verbose => verbose_option,
|
||||
:fatal_deprecations => fatal_deprecations,
|
||||
:future_deprecations => future_deprecations,
|
||||
:silence_deprecations => silence_deprecations,
|
||||
}
|
||||
end
|
||||
|
||||
def convert(content)
|
||||
output = ::Sass.compile_string(content, **sass_configs)
|
||||
result = output.css
|
||||
|
||||
if sourcemap_required?
|
||||
source_map = process_source_map(output.source_map)
|
||||
generate_source_map_page(source_map)
|
||||
|
||||
if (sm_url = source_mapping_url)
|
||||
result += "#{sass_style == :compressed ? "" : "\n\n"}/*# sourceMappingURL=#{sm_url} */"
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
rescue ::Sass::CompileError => e
|
||||
Jekyll.logger.error e.full_message
|
||||
if livereload?
|
||||
e.to_css # Render error message in browser window
|
||||
else
|
||||
raise SyntaxError, e.message
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# The Page instance for which this object acts as a converter.
|
||||
attr_reader :sass_page
|
||||
|
||||
def associate_page_failed?
|
||||
!sass_page
|
||||
end
|
||||
|
||||
# Returns `true` if jekyll is serving with livereload.
|
||||
def livereload?
|
||||
!!(@config["serving"] && @config["livereload"])
|
||||
end
|
||||
|
||||
# The URL of the input scss (or sass) file. This information will be used for error reporting.
|
||||
def sass_file_url
|
||||
return if associate_page_failed?
|
||||
|
||||
file_url_from_path(Jekyll.sanitized_path(site_source, sass_page.relative_path))
|
||||
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
|
||||
|
||||
def source_map_page
|
||||
return if associate_page_failed?
|
||||
|
||||
@source_map_page ||= SourceMapPage.new(sass_page)
|
||||
end
|
||||
|
||||
# Returns the directory that source map sources are relative to.
|
||||
def sass_source_root
|
||||
if associate_page_failed?
|
||||
site_source
|
||||
else
|
||||
Jekyll.sanitized_path(site_source, File.dirname(sass_page.relative_path))
|
||||
end
|
||||
end
|
||||
|
||||
# Converts file urls in source map to relative paths.
|
||||
#
|
||||
# Returns processed source map string.
|
||||
def process_source_map(source_map)
|
||||
map_data = JSON.parse(source_map)
|
||||
unless associate_page_failed?
|
||||
map_data["file"] = Addressable::URI.encode("#{sass_page.basename}.css")
|
||||
end
|
||||
source_root_url = Addressable::URI.parse(file_url_from_path("#{sass_source_root}/"))
|
||||
map_data["sources"].map! do |s|
|
||||
s.start_with?("file:") ? Addressable::URI.parse(s).route_from(source_root_url).to_s : s
|
||||
end
|
||||
JSON.generate(map_data)
|
||||
end
|
||||
|
||||
# Adds the source-map to the source-map-page and adds it to `site.pages`.
|
||||
def generate_source_map_page(source_map)
|
||||
return if associate_page_failed?
|
||||
|
||||
source_map_page.source_map(source_map)
|
||||
site.pages << source_map_page
|
||||
end
|
||||
|
||||
# Returns a source mapping url for given source-map.
|
||||
def source_mapping_url
|
||||
return if associate_page_failed?
|
||||
|
||||
Addressable::URI.encode("#{sass_page.basename}.css.map")
|
||||
end
|
||||
|
||||
def site
|
||||
associate_page_failed? ? Jekyll.sites.last : sass_page.site
|
||||
end
|
||||
|
||||
def site_source
|
||||
site.source
|
||||
end
|
||||
|
||||
def file_url_from_path(path)
|
||||
Addressable::URI.encode("file://#{path.start_with?("/") ? "" : "/"}#{path}")
|
||||
end
|
||||
|
||||
# Returns the value of the `quiet_deps`-option chosen by the user or 'false' by default.
|
||||
def quiet_deps_option
|
||||
!!jekyll_sass_configuration.fetch("quiet_deps", false)
|
||||
end
|
||||
|
||||
# Returns the value of the `verbose`-option chosen by the user or 'false' by default.
|
||||
def verbose_option
|
||||
!!jekyll_sass_configuration.fetch("verbose", false)
|
||||
end
|
||||
|
||||
# Returns the value of the `fatal_deprecations`-option or '[]' by default.
|
||||
def fatal_deprecations
|
||||
Array(jekyll_sass_configuration["fatal_deprecations"])
|
||||
end
|
||||
|
||||
# Returns the value of the `future_deprecations`-option or '[]' by default.
|
||||
def future_deprecations
|
||||
Array(jekyll_sass_configuration["future_deprecations"])
|
||||
end
|
||||
|
||||
# Returns the value of the `silence_deprecations`-option or '[]' by default.
|
||||
def silence_deprecations
|
||||
Array(jekyll_sass_configuration["silence_deprecations"])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
# 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.content = map
|
||||
end
|
||||
|
||||
def ext
|
||||
".map"
|
||||
end
|
||||
|
||||
def asset_file?
|
||||
true
|
||||
end
|
||||
|
||||
def render_with_liquid?
|
||||
false
|
||||
end
|
||||
|
||||
# @return[String] the object as a debug String.
|
||||
def inspect
|
||||
"#<#{self.class} @name=#{name.inspect}>"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user