lib/sprockets/sassc_processor.rb



# frozen_string_literal: true
require 'rack/utils'
require 'sprockets/autoload'
require 'sprockets/source_map_utils'
require 'uri'

module Sprockets
  # Processor engine class for the SASS/SCSS compiler. Depends on the `sassc` gem.
  #
  # For more information see:
  #
  #   https://github.com/sass/sassc-ruby
  #   https://github.com/sass/sassc-rails
  #
  class SasscProcessor

    # Internal: Defines default sass syntax to use. Exposed so the ScsscProcessor
    # may override it.
    def self.syntax
      :sass
    end

    # Public: Return singleton instance with default options.
    #
    # Returns SasscProcessor object.
    def self.instance
      @instance ||= new
    end

    def self.call(input)
      instance.call(input)
    end

    def self.cache_key
      instance.cache_key
    end

    attr_reader :cache_key

    def initialize(options = {}, &block)
      @cache_version = options[:cache_version]
      @cache_key = "#{self.class.name}:#{VERSION}:#{Autoload::SassC::VERSION}:#{@cache_version}".freeze
      @importer_class = options[:importer]
      @sass_config = options[:sass_config] || {}
      @functions = Module.new do
        include Functions
        include options[:functions] if options[:functions]
        class_eval(&block) if block_given?
      end
    end

    def call(input)
      context = input[:environment].context_class.new(input)

      options = engine_options(input, context)
      engine = Autoload::SassC::Engine.new(input[:data], options)

      css = Utils.module_include(Autoload::SassC::Script::Functions, @functions) do
        engine.render.sub(/^\n^\/\*# sourceMappingURL=.*\*\/$/m, '')
      end

      begin
        map = SourceMapUtils.format_source_map(JSON.parse(engine.source_map), input)
        map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)

        engine.dependencies.each do |dependency|
          context.metadata[:dependencies] << URIUtils.build_file_digest_uri(dependency.filename)
        end
      rescue SassC::NotRenderedError
        map = input[:metadata][:map]
      end

      context.metadata.merge(data: css, map: map)
    end

    private

    def merge_options(options)
      defaults = @sass_config.dup

      if load_paths = defaults.delete(:load_paths)
        options[:load_paths] += load_paths
      end

      options.merge!(defaults)
      options
    end

    # Public: Functions injected into Sass context during Sprockets evaluation.
    #
    # This module may be extended to add global functionality to all Sprockets
    # Sass environments. Though, scoping your functions to just your environment
    # is preferred.
    #
    # module Sprockets::SasscProcessor::Functions
    #   def asset_path(path, options = {})
    #   end
    # end
    #
    module Functions
      # Public: Generate a url for asset path.
      #
      # Default implementation is deprecated. Currently defaults to
      # Context#asset_path.
      #
      # Will raise NotImplementedError in the future. Users should provide their
      # own base implementation.
      #
      # Returns a SassC::Script::Value::String.
      def asset_path(path, options = {})
        path = path.value

        path, _, query, fragment = URI.split(path)[5..8]
        path     = sprockets_context.asset_path(path, options)
        query    = "?#{query}" if query
        fragment = "##{fragment}" if fragment

        Autoload::SassC::Script::Value::String.new("#{path}#{query}#{fragment}", :string)
      end

      # Public: Generate a asset url() link.
      #
      # path - SassC::Script::Value::String URL path
      #
      # Returns a SassC::Script::Value::String.
      def asset_url(path, options = {})
        Autoload::SassC::Script::Value::String.new("url(#{asset_path(path, options).value})")
      end

      # Public: Generate url for image path.
      #
      # path - SassC::Script::Value::String URL path
      #
      # Returns a SassC::Script::Value::String.
      def image_path(path)
        asset_path(path, type: :image)
      end

      # Public: Generate a image url() link.
      #
      # path - SassC::Script::Value::String URL path
      #
      # Returns a SassC::Script::Value::String.
      def image_url(path)
        asset_url(path, type: :image)
      end

      # Public: Generate url for video path.
      #
      # path - SassC::Script::Value::String URL path
      #
      # Returns a SassC::Script::Value::String.
      def video_path(path)
        asset_path(path, type: :video)
      end

      # Public: Generate a video url() link.
      #
      # path - SassC::Script::Value::String URL path
      #
      # Returns a SassC::Script::Value::String.
      def video_url(path)
        asset_url(path, type: :video)
      end

      # Public: Generate url for audio path.
      #
      # path - SassC::Script::Value::String URL path
      #
      # Returns a SassC::Script::Value::String.
      def audio_path(path)
        asset_path(path, type: :audio)
      end

      # Public: Generate a audio url() link.
      #
      # path - SassC::Script::Value::String URL path
      #
      # Returns a SassC::Script::Value::String.
      def audio_url(path)
        asset_url(path, type: :audio)
      end

      # Public: Generate url for font path.
      #
      # path - SassC::Script::Value::String URL path
      #
      # Returns a SassC::Script::Value::String.
      def font_path(path)
        asset_path(path, type: :font)
      end

      # Public: Generate a font url() link.
      #
      # path - SassC::Script::Value::String URL path
      #
      # Returns a SassC::Script::Value::String.
      def font_url(path)
        asset_url(path, type: :font)
      end

      # Public: Generate url for javascript path.
      #
      # path - SassC::Script::Value::String URL path
      #
      # Returns a SassC::Script::Value::String.
      def javascript_path(path)
        asset_path(path, type: :javascript)
      end

      # Public: Generate a javascript url() link.
      #
      # path - SassC::Script::Value::String URL path
      #
      # Returns a SassC::Script::Value::String.
      def javascript_url(path)
        asset_url(path, type: :javascript)
      end

      # Public: Generate url for stylesheet path.
      #
      # path - SassC::Script::Value::String URL path
      #
      # Returns a SassC::Script::Value::String.
      def stylesheet_path(path)
        asset_path(path, type: :stylesheet)
      end

      # Public: Generate a stylesheet url() link.
      #
      # path - SassC::Script::Value::String URL path
      #
      # Returns a SassC::Script::Value::String.
      def stylesheet_url(path)
        asset_url(path, type: :stylesheet)
      end

      # Public: Generate a data URI for asset path.
      #
      # path - SassC::Script::Value::String logical asset path
      #
      # Returns a SassC::Script::Value::String.
      def asset_data_url(path)
        url = sprockets_context.asset_data_uri(path.value)
        Autoload::SassC::Script::Value::String.new("url(" + url + ")")
      end

      protected
        # Public: The Environment.
        #
        # Returns Sprockets::Environment.
        def sprockets_environment
          options[:sprockets][:environment]
        end

        # Public: Mutatable set of dependencies.
        #
        # Returns a Set.
        def sprockets_dependencies
          options[:sprockets][:dependencies]
        end

        # Deprecated: Get the Context instance. Use APIs on
        # sprockets_environment or sprockets_dependencies directly.
        #
        # Returns a Context instance.
        def sprockets_context
          options[:sprockets][:context]
        end

    end

    def engine_options(input, context)
      merge_options({
        filename: input[:filename],
        syntax: self.class.syntax,
        load_paths: input[:environment].paths,
        importer: @importer_class,
        source_map_contents: false,
        source_map_file: "#{input[:filename]}.map",
        omit_source_map_url: true,
        sprockets: {
          context: context,
          environment: input[:environment],
          dependencies: context.metadata[:dependencies]
        }
      })
    end
  end


  class ScsscProcessor < SasscProcessor
    def self.syntax
      :scss
    end
  end
end