Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
409 lines (364 sloc) 12.2 kB
require 'pathname'
require 'shellwords'
require 'tilt'
require 'yaml'
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"
# */
#
# The Processor is implemented as a `Tilt::Template` and is loosely
# coupled to Sprockets. 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 < Tilt::Template
# 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
attr_reader :pathname
attr_reader :header, :body
def prepare
@pathname = Pathname.new(file)
@header = data[HEADER_PATTERN, 0] || ""
@body = $' || data
# Ensure body ends in a new line
@body += "\n" if @body != "" && @body !~ /\n\Z/m
@included_pathnames = []
@compat = false
end
# Implemented for Tilt#render.
#
# `context` is a `Context` instance with methods that allow you to
# access the environment and append to the bundle. See `Context`
# for the complete API.
def evaluate(context, locals, &block)
@context = context
@result = ""
@result.force_encoding(body.encoding) if body.respond_to?(:encoding)
@has_written_body = false
process_directives
process_source
@result
end
# Returns the header String with any directives stripped.
def processed_header
lineno = 0
@processed_header ||= header.lines.map { |line|
lineno += 1
# Replace directive line with a clean break
directives.assoc(lineno) ? "\n" : line
}.join.chomp
end
# Returns the source String with any directives stripped.
def processed_source
@processed_source ||= processed_header + body
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 directives
@directives ||= header.lines.each_with_index.map { |line, index|
if directive = line[DIRECTIVE_PATTERN, 1]
name, *args = Shellwords.shellwords(directive)
if respond_to?("process_#{name}_directive", true)
[index + 1, name, *args]
end
end
}.compact
end
protected
attr_reader :included_pathnames
attr_reader :context
# 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["#{pathname.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.each do |line_number, name, *args|
context.__LINE__ = line_number
send("process_#{name}_directive", *args)
context.__LINE__ = nil
end
end
def process_source
unless @has_written_body || processed_header.empty?
@result << processed_header << "\n"
end
included_pathnames.each do |pathname|
@result << context.evaluate(pathname)
end
unless @has_written_body
@result << body
end
if compat? && constants.any?
@result.gsub!(/<%=(.*?)%>/) { constants[$1.strip] }
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)
if @compat
if path =~ /<([^>]+)>/
path = $1
else
path = "./#{path}" unless relative?(path)
end
end
context.require_asset(path)
end
# `require_self` causes the body of the current file to be
# inserted before any subsequent `require` or `include`
# 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 @has_written_body
raise ArgumentError, "require_self can only be called once per source file"
end
context.require_asset(pathname)
process_source
included_pathnames.clear
@has_written_body = true
end
# The `include` directive works similar to `require` but
# inserts the contents of the dependency even if it already
# has been required.
#
# //= include "header"
#
def process_include_directive(path)
pathname = context.resolve(path)
context.depend_on_asset(pathname)
included_pathnames << pathname
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 relative?(path)
root = pathname.dirname.join(path).expand_path
unless (stats = stat(root)) && stats.directory?
raise ArgumentError, "require_directory argument must be a directory"
end
context.depend_on(root)
entries(root).each do |pathname|
pathname = root.join(pathname)
if pathname.to_s == self.file
next
elsif context.asset_requirable?(pathname)
context.require_asset(pathname)
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 relative?(path)
root = pathname.dirname.join(path).expand_path
unless (stats = stat(root)) && stats.directory?
raise ArgumentError, "require_tree argument must be a directory"
end
context.depend_on(root)
each_entry(root) do |pathname|
if pathname.to_s == self.file
next
elsif stat(pathname).directory?
context.depend_on(pathname)
elsif context.asset_requirable?(pathname)
context.require_asset(pathname)
end
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)
context.depend_on(path)
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)
context.depend_on_asset(path)
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)
context.stub_asset(path)
end
# Enable Sprockets 1.x compat mode.
#
# Makes it possible to use the same JavaScript source
# file in both Sprockets 1 and 2.
#
# //= compat
#
def process_compat_directive
@compat = true
end
# Checks if Sprockets 1.x compat mode enabled
def compat?
@compat
end
# Sprockets 1.x allowed for constant interpolation if a
# constants.yml was present. This is only available if
# compat mode is on.
def constants
if compat?
pathname = Pathname.new(context.root_path).join("constants.yml")
stat(pathname) ? YAML.load_file(pathname) : {}
else
{}
end
end
# `provide` is stubbed out for Sprockets 1.x compat.
# Mutating the path when an asset is being built is
# not permitted.
def process_provide_directive(path)
end
private
def relative?(path)
path =~ /^\.($|\.?\/)/
end
def stat(path)
context.environment.stat(path)
end
def entries(path)
context.environment.entries(path)
end
def each_entry(root, &block)
context.environment.each_entry(root, &block)
end
end
end
Jump to Line
Something went wrong with that request. Please try again.