lib/sprockets/directive_processor.rb



require 'set'
require 'shellwords'

module Sprockets
  # The `DirectiveProcessor` is responsible for parsing and evaluating
  # directive comments in a source file.
  #
  # A directive comment starts with a comment prefix, followed by an "=",
  # then the directive name, then any arguments.
  #
  #     // JavaScript
  #     //= require "foo"
  #
  #     # CoffeeScript
  #     #= require "bar"
  #
  #     /* CSS
  #      *= require "baz"
  #      */
  #
  # This makes it possible to disable or modify the processor to do whatever
  # you'd like. You could add your own custom directives or invent your own
  # directive syntax.
  #
  # `Environment#processors` includes `DirectiveProcessor` by default.
  #
  # To remove the processor entirely:
  #
  #     env.unregister_processor('text/css', Sprockets::DirectiveProcessor)
  #     env.unregister_processor('application/javascript', Sprockets::DirectiveProcessor)
  #
  # Then inject your own preprocessor:
  #
  #     env.register_processor('text/css', MyProcessor)
  #
  class DirectiveProcessor
    VERSION = '1'

    # Directives will only be picked up if they are in the header
    # of the source file. C style (/* */), JavaScript (//), and
    # Ruby (#) comments are supported.
    #
    # Directives in comments after the first non-whitespace line
    # of code will not be processed.
    #
    HEADER_PATTERN = /
      \A (
        (?m:\s*) (
          (\/\* (?m:.*?) \*\/) |
          (\#\#\# (?m:.*?) \#\#\#) |
          (\/\/ .* \n?)+ |
          (\# .* \n?)+
        )
      )+
    /x

    # Directives are denoted by a `=` followed by the name, then
    # argument list.
    #
    # A few different styles are allowed:
    #
    #     // =require foo
    #     //= require foo
    #     //= require "foo"
    #
    DIRECTIVE_PATTERN = /
      ^ \W* = \s* (\w+.*?) (\*\/)? $
    /x

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

    def call(input)
      @environment  = input[:environment]
      @uri          = input[:uri]
      @load_path    = input[:load_path]
      @filename     = input[:filename]
      @dirname      = File.dirname(@filename)
      @content_type = input[:content_type]

      data = input[:data]
      cache_key = ['DirectiveProcessor', VERSION, data]
      result = input[:cache].fetch(cache_key) do
        process_source(data)
      end

      data, directives = result.values_at(:data, :directives)

      @required         = Set.new(input[:metadata][:required])
      @stubbed          = Set.new(input[:metadata][:stubbed])
      @links            = Set.new(input[:metadata][:links])
      @dependency_paths = Set.new(input[:metadata][:dependency_paths])

      process_directives(directives)

      { data: data,
        required: @required,
        stubbed: @stubbed,
        links: @links,
        dependency_paths: @dependency_paths }
    end

    protected
      def process_source(source)
        header = source[HEADER_PATTERN, 0] || ""
        body   = $' || source

        header, directives = extract_directives(header)

        data = ""
        data.force_encoding(body.encoding)
        data << header << "\n" unless header.empty?
        data << body
        # Ensure body ends in a new line
        data << "\n" if data.length > 0 && data[-1] != "\n"

        { data: data, directives: directives }
      end

      # Returns an Array of directive structures. Each structure
      # is an Array with the line number as the first element, the
      # directive name as the second element, followed by any
      # arguments.
      #
      #     [[1, "require", "foo"], [2, "require", "bar"]]
      #
      def extract_directives(header)
        processed_header = ""
        directives = []

        header.lines.each_with_index do |line, index|
          if directive = line[DIRECTIVE_PATTERN, 1]
            name, *args = Shellwords.shellwords(directive)
            if respond_to?("process_#{name}_directive", true)
              directives << [index + 1, name, *args]
              # Replace directive line with a clean break
              line = "\n"
            end
          end
          processed_header << line
        end

        return processed_header.chomp, directives
      end

      # Gathers comment directives in the source and processes them.
      # Any directive method matching `process_*_directive` will
      # automatically be available. This makes it easy to extend the
      # processor.
      #
      # To implement a custom directive called `require_glob`, subclass
      # `Sprockets::DirectiveProcessor`, then add a method called
      # `process_require_glob_directive`.
      #
      #     class DirectiveProcessor < Sprockets::DirectiveProcessor
      #       def process_require_glob_directive
      #         Dir["#{dirname}/#{glob}"].sort.each do |filename|
      #           require(filename)
      #         end
      #       end
      #     end
      #
      # Replace the current processor on the environment with your own:
      #
      #     env.unregister_processor('text/css', Sprockets::DirectiveProcessor)
      #     env.register_processor('text/css', DirectiveProcessor)
      #
      def process_directives(directives)
        directives.each do |line_number, name, *args|
          begin
            send("process_#{name}_directive", *args)
          rescue Exception => e
            e.set_backtrace(["#{@filename}:#{line_number}"] + e.backtrace)
            raise e
          end
        end
      end

      # The `require` directive functions similar to Ruby's own `require`.
      # It provides a way to declare a dependency on a file in your path
      # and ensures its only loaded once before the source file.
      #
      # `require` works with files in the environment path:
      #
      #     //= require "foo.js"
      #
      # Extensions are optional. If your source file is ".js", it
      # assumes you are requiring another ".js".
      #
      #     //= require "foo"
      #
      # Relative paths work too. Use a leading `./` to denote a relative
      # path:
      #
      #     //= require "./bar"
      #
      def process_require_directive(path)
        @required << resolve_uri(path)
      end

      # `require_self` causes the body of the current file to be inserted
      # before any subsequent `require` directives. Useful in CSS files, where
      # it's common for the index file to contain global styles that need to
      # be defined before other dependencies are loaded.
      #
      #     /*= require "reset"
      #      *= require_self
      #      *= require_tree .
      #      */
      #
      def process_require_self_directive
        if @required.include?(@uri)
          raise ArgumentError, "require_self can only be called once per source file"
        end
        @required << @uri
      end

      # `require_directory` requires all the files inside a single
      # directory. It's similar to `path/*` since it does not follow
      # nested directories.
      #
      #     //= require_directory "./javascripts"
      #
      def process_require_directory_directive(path = ".")
        if @environment.relative_path?(path)
          root = expand_relative_path(path)

          unless (stats = @environment.stat(root)) && stats.directory?
            raise ArgumentError, "require_directory argument must be a directory"
          end

          @dependency_paths << root

          @environment.stat_directory(root).each do |subpath, stat|
            if subpath == @filename
              next
            elsif @environment.resolve_path_transform_type(subpath, @content_type)
              @required << @environment.resolve_asset_uri(subpath, accept: @content_type, bundle: false)
            end
          end
        else
          # The path must be relative and start with a `./`.
          raise ArgumentError, "require_directory argument must be a relative path"
        end
      end

      # `require_tree` requires all the nested files in a directory.
      # Its glob equivalent is `path/**/*`.
      #
      #     //= require_tree "./public"
      #
      def process_require_tree_directive(path = ".")
        if @environment.relative_path?(path)
          root = expand_relative_path(path)

          unless (stats = @environment.stat(root)) && stats.directory?
            raise ArgumentError, "require_tree argument must be a directory"
          end

          @dependency_paths << root

          required = []
          @environment.stat_tree(root).each do |subpath, stat|
            if subpath == @filename
              next
            elsif stat.directory?
              @dependency_paths << subpath
            elsif @environment.resolve_path_transform_type(subpath, @content_type)
              required << subpath
            end
          end
          required.sort_by(&:to_s).each do |subpath|
            @required << @environment.resolve_asset_uri(subpath, accept: @content_type, bundle: false)
          end
        else
          # The path must be relative and start with a `./`.
          raise ArgumentError, "require_tree argument must be a relative path"
        end
      end

      # Allows you to state a dependency on a file without
      # including it.
      #
      # This is used for caching purposes. Any changes made to
      # the dependency file will invalidate the cache of the
      # source file.
      #
      # This is useful if you are using ERB and File.read to pull
      # in contents from another file.
      #
      #     //= depend_on "foo.png"
      #
      def process_depend_on_directive(path)
        @dependency_paths << resolve(path, accept: "#{@content_type}, */*")
      end

      # Allows you to state a dependency on an asset without including
      # it.
      #
      # This is used for caching purposes. Any changes that would
      # invalid the asset dependency will invalidate the cache our the
      # source file.
      #
      # Unlike `depend_on`, the path must be a requirable asset.
      #
      #     //= depend_on_asset "bar.js"
      #
      def process_depend_on_asset_directive(path)
        if asset = @environment.find_asset(resolve(path, accept: "#{@content_type}, */*"))
          @dependency_paths.merge(asset.metadata[:dependency_paths])
        end
      end

      # Allows dependency to be excluded from the asset bundle.
      #
      # The `path` must be a valid asset and may or may not already
      # be part of the bundle. Once stubbed, it is blacklisted and
      # can't be brought back by any other `require`.
      #
      #     //= stub "jquery"
      #
      def process_stub_directive(path)
        @stubbed << resolve_uri(path)
      end

      # Declares a linked dependency on the target asset.
      #
      # The `path` must be a valid asset and should not already be part of the
      # bundle. Any linked assets will automatically be compiled along with the
      # current.
      #
      #   /*= link "logo.png" */
      #
      def process_link_directive(path)
        if asset = @environment.find_asset(resolve(path, accept: "#{@content_type}, */*"))
          @dependency_paths.merge(asset.metadata[:dependency_paths])
          @links << asset.uri
        end
      end

    private
      def expand_relative_path(path)
        File.expand_path(path, @dirname)
      end

      def resolve_uri(path)
        filename = resolve(path, accept: @content_type)
        @environment.resolve_asset_uri(filename, accept: @content_type, bundle: false)
      end

      def resolve(path, options = {})
        if @environment.absolute_path?(path)
          raise FileOutsidePaths, "can't require absolute file: #{path}"
        elsif @environment.relative_path?(path)
          path = expand_relative_path(path)
          if logical_path = @environment.split_subpath(@load_path, path)
            @environment.resolve_in_load_path(@load_path, logical_path, options)
          else
            raise FileOutsidePaths, "#{path} isn't under path: #{@load_path}"
          end
        else
          @environment.resolve(path, options)
        end
      end
  end
end