Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 409 lines (364 sloc) 12.2 kB
8fbca7a @josh EnginePathname uses composition instead of inheritance
josh authored
1 require 'pathname'
41ac92f @josh Make DirectiveParser an internal class of Processor
josh authored
2 require 'shellwords'
ca60bca @josh Convert Processor into a Tilt::Template
josh authored
3 require 'tilt'
64cb9b7 @josh Support legacy constants.yml
josh authored
4 require 'yaml'
5
67e5bc0 @sstephenson It works!
authored
6 module Sprockets
ebd683e @josh Docco DirectiveProcessor
josh authored
7 # The `DirectiveProcessor` is responsible for parsing and evaluating
8 # directive comments in a source file.
9 #
a4c3a85 Fix typos.
Daniel Brockman authored
10 # A directive comment starts with a comment prefix, followed by an "=",
11 # then the directive name, then any arguments.
ebd683e @josh Docco DirectiveProcessor
josh authored
12 #
13 # // JavaScript
14 # //= require "foo"
15 #
16 # # CoffeeScript
17 # #= require "bar"
18 #
19 # /* CSS
20 # *= require "baz"
21 # */
22 #
23 # The Processor is implemented as a `Tilt::Template` and is loosely
24 # coupled to Sprockets. This makes it possible to disable or modify
25 # the processor to do whatever you'd like. You could add your own
26 # custom directives or invent your own directive syntax.
27 #
ae31388 @josh Rename formats to processors
josh authored
28 # `Environment#processors` includes `DirectiveProcessor` by default.
ebd683e @josh Docco DirectiveProcessor
josh authored
29 #
30 # To remove the processor entirely:
31 #
1724d06 @josh Change register_processor to take a mime_type instead of a extension
josh authored
32 # env.unregister_processor('text/css', Sprockets::DirectiveProcessor)
a4c3a85 Fix typos.
Daniel Brockman authored
33 # env.unregister_processor('application/javascript', Sprockets::DirectiveProcessor)
ebd683e @josh Docco DirectiveProcessor
josh authored
34 #
35 # Then inject your own preprocessor:
36 #
1724d06 @josh Change register_processor to take a mime_type instead of a extension
josh authored
37 # env.register_processor('text/css', MyProcessor)
ebd683e @josh Docco DirectiveProcessor
josh authored
38 #
104dd00 @josh Rename Processor to DirectiveProcessor
josh authored
39 class DirectiveProcessor < Tilt::Template
6031edc @josh Ignore unknown directives
josh authored
40 # Directives will only be picked up if they are in the header
41 # of the source file. C style (/* */), JavaScript (//), and
42 # Ruby (#) comments are supported.
43 #
44 # Directives in comments after the first non-whitespace line
45 # of code will not be processed.
46 #
47 HEADER_PATTERN = /
48 \A (
49 (?m:\s*) (
50 (\/\* (?m:.*?) \*\/) |
51 (\#\#\# (?m:.*?) \#\#\#) |
52 (\/\/ .* \n?)+ |
53 (\# .* \n?)+
54 )
55 )+
56 /x
57
58 # Directives are denoted by a `=` followed by the name, then
59 # argument list.
60 #
61 # A few different styles are allowed:
62 #
63 # // =require foo
64 # //= require foo
65 # //= require "foo"
66 #
67 DIRECTIVE_PATTERN = /
21a3f81 @josh Fix redundant regexp range warning in ruby 2.x
josh authored
68 ^ \W* = \s* (\w+.*?) (\*\/)? $
6031edc @josh Ignore unknown directives
josh authored
69 /x
70
8456986 @josh Axe SourceFile
josh authored
71 attr_reader :pathname
6031edc @josh Ignore unknown directives
josh authored
72 attr_reader :header, :body
8456986 @josh Axe SourceFile
josh authored
73
ca60bca @josh Convert Processor into a Tilt::Template
josh authored
74 def prepare
8fbca7a @josh EnginePathname uses composition instead of inheritance
josh authored
75 @pathname = Pathname.new(file)
ca60bca @josh Convert Processor into a Tilt::Template
josh authored
76
6031edc @josh Ignore unknown directives
josh authored
77 @header = data[HEADER_PATTERN, 0] || ""
78 @body = $' || data
79 # Ensure body ends in a new line
80 @body += "\n" if @body != "" && @body !~ /\n\Z/m
81
26e8a7e @sstephenson Simplify ConcatenatedAsset#initialize
authored
82 @included_pathnames = []
5de0db7 @josh Basic compat mode support
josh authored
83 @compat = false
ca60bca @josh Convert Processor into a Tilt::Template
josh authored
84 end
85
ebd683e @josh Docco DirectiveProcessor
josh authored
86 # Implemented for Tilt#render.
87 #
36a4378 @josh Cleaned up context namespaced methods
josh authored
88 # `context` is a `Context` instance with methods that allow you to
4199996 @sstephenson ConcatenatedAsset -> BundledAsset
authored
89 # access the environment and append to the bundle. See `Context`
90 # for the complete API.
f22f61a @josh Expose public concatenation methods on Context
josh authored
91 def evaluate(context, locals, &block)
c1bed19 @josh Fix require_self include ordering
josh authored
92 @context = context
93
94 @result = ""
83b5f11 Make the DirectiveProcessor encoding aware
Lewis Marshall authored
95 @result.force_encoding(body.encoding) if body.respond_to?(:encoding)
96
c1bed19 @josh Fix require_self include ordering
josh authored
97 @has_written_body = false
ca60bca @josh Convert Processor into a Tilt::Template
josh authored
98
67e5bc0 @sstephenson It works!
authored
99 process_directives
ca60bca @josh Convert Processor into a Tilt::Template
josh authored
100 process_source
c1bed19 @josh Fix require_self include ordering
josh authored
101
102 @result
ca60bca @josh Convert Processor into a Tilt::Template
josh authored
103 end
104
6031edc @josh Ignore unknown directives
josh authored
105 # Returns the header String with any directives stripped.
1308728 @josh Test directive processor public api
josh authored
106 def processed_header
6031edc @josh Ignore unknown directives
josh authored
107 lineno = 0
def9a81 @josh Preserve directive whitespace
josh authored
108 @processed_header ||= header.lines.map { |line|
6031edc @josh Ignore unknown directives
josh authored
109 lineno += 1
def9a81 @josh Preserve directive whitespace
josh authored
110 # Replace directive line with a clean break
111 directives.assoc(lineno) ? "\n" : line
6031edc @josh Ignore unknown directives
josh authored
112 }.join.chomp
1308728 @josh Test directive processor public api
josh authored
113 end
114
6031edc @josh Ignore unknown directives
josh authored
115 # Returns the source String with any directives stripped.
1308728 @josh Test directive processor public api
josh authored
116 def processed_source
6031edc @josh Ignore unknown directives
josh authored
117 @processed_source ||= processed_header + body
1308728 @josh Test directive processor public api
josh authored
118 end
119
6031edc @josh Ignore unknown directives
josh authored
120 # Returns an Array of directive structures. Each structure
121 # is an Array with the line number as the first element, the
122 # directive name as the second element, followed by any
123 # arguments.
124 #
125 # [[1, "require", "foo"], [2, "require", "bar"]]
126 #
1308728 @josh Test directive processor public api
josh authored
127 def directives
6031edc @josh Ignore unknown directives
josh authored
128 @directives ||= header.lines.each_with_index.map { |line, index|
129 if directive = line[DIRECTIVE_PATTERN, 1]
130 name, *args = Shellwords.shellwords(directive)
6ac4248 @tenderlove protected methods return false unless a second argument is passed to
tenderlove authored
131 if respond_to?("process_#{name}_directive", true)
6031edc @josh Ignore unknown directives
josh authored
132 [index + 1, name, *args]
133 end
134 end
135 }.compact
1308728 @josh Test directive processor public api
josh authored
136 end
137
f22f61a @josh Expose public concatenation methods on Context
josh authored
138 protected
e28d7a4 @josh Require and depend on paths directly on concatenation
josh authored
139 attr_reader :included_pathnames
a66a2a9 @josh Recursively build up Assets
josh authored
140 attr_reader :context
f22f61a @josh Expose public concatenation methods on Context
josh authored
141
ebd683e @josh Docco DirectiveProcessor
josh authored
142 # Gathers comment directives in the source and processes them.
143 # Any directive method matching `process_*_directive` will
144 # automatically be available. This makes it easy to extend the
145 # processor.
146 #
147 # To implement a custom directive called `require_glob`, subclass
148 # `Sprockets::DirectiveProcessor`, then add a method called
149 # `process_require_glob_directive`.
150 #
151 # class DirectiveProcessor < Sprockets::DirectiveProcessor
152 # def process_require_glob_directive
a905358 @josh Remove base_path
josh authored
153 # Dir["#{pathname.dirname}/#{glob}"].sort.each do |filename|
36a4378 @josh Cleaned up context namespaced methods
josh authored
154 # require(filename)
ebd683e @josh Docco DirectiveProcessor
josh authored
155 # end
156 # end
157 # end
158 #
159 # Replace the current processor on the environment with your own:
160 #
a4c3a85 Fix typos.
Daniel Brockman authored
161 # env.unregister_processor('text/css', Sprockets::DirectiveProcessor)
1724d06 @josh Change register_processor to take a mime_type instead of a extension
josh authored
162 # env.register_processor('text/css', DirectiveProcessor)
ebd683e @josh Docco DirectiveProcessor
josh authored
163 #
f22f61a @josh Expose public concatenation methods on Context
josh authored
164 def process_directives
ce67550 @sstephenson DirectiveProcessor#directives includes line numbers
authored
165 directives.each do |line_number, name, *args|
63e54d0 @sstephenson Store line number on the context instead
authored
166 context.__LINE__ = line_number
f22f61a @josh Expose public concatenation methods on Context
josh authored
167 send("process_#{name}_directive", *args)
63e54d0 @sstephenson Store line number on the context instead
authored
168 context.__LINE__ = nil
f22f61a @josh Expose public concatenation methods on Context
josh authored
169 end
ca60bca @josh Convert Processor into a Tilt::Template
josh authored
170 end
171
f22f61a @josh Expose public concatenation methods on Context
josh authored
172 def process_source
c1bed19 @josh Fix require_self include ordering
josh authored
173 unless @has_written_body || processed_header.empty?
174 @result << processed_header << "\n"
8456986 @josh Axe SourceFile
josh authored
175 end
176
36a4378 @josh Cleaned up context namespaced methods
josh authored
177 included_pathnames.each do |pathname|
c1bed19 @josh Fix require_self include ordering
josh authored
178 @result << context.evaluate(pathname)
36a4378 @josh Cleaned up context namespaced methods
josh authored
179 end
8456986 @josh Axe SourceFile
josh authored
180
c1bed19 @josh Fix require_self include ordering
josh authored
181 unless @has_written_body
6031edc @josh Ignore unknown directives
josh authored
182 @result << body
c1bed19 @josh Fix require_self include ordering
josh authored
183 end
8456986 @josh Axe SourceFile
josh authored
184
f22f61a @josh Expose public concatenation methods on Context
josh authored
185 if compat? && constants.any?
c1bed19 @josh Fix require_self include ordering
josh authored
186 @result.gsub!(/<%=(.*?)%>/) { constants[$1.strip] }
f22f61a @josh Expose public concatenation methods on Context
josh authored
187 end
188 end
67e5bc0 @sstephenson It works!
authored
189
ebd683e @josh Docco DirectiveProcessor
josh authored
190 # The `require` directive functions similar to Ruby's own `require`.
191 # It provides a way to declare a dependency on a file in your path
192 # and ensures its only loaded once before the source file.
193 #
194 # `require` works with files in the environment path:
195 #
196 # //= require "foo.js"
197 #
198 # Extensions are optional. If your source file is ".js", it
199 # assumes you are requiring another ".js".
200 #
201 # //= require "foo"
202 #
203 # Relative paths work too. Use a leading `./` to denote a relative
204 # path:
205 #
206 # //= require "./bar"
207 #
f22f61a @josh Expose public concatenation methods on Context
josh authored
208 def process_require_directive(path)
209 if @compat
210 if path =~ /<([^>]+)>/
211 path = $1
212 else
213 path = "./#{path}" unless relative?(path)
214 end
5de0db7 @josh Basic compat mode support
josh authored
215 end
216
7b201ad @josh Less concatenation exposure
josh authored
217 context.require_asset(path)
f22f61a @josh Expose public concatenation methods on Context
josh authored
218 end
7c4f5d0 @sstephenson Require and include can take relative paths
authored
219
1919fa3 @sstephenson require_self
authored
220 # `require_self` causes the body of the current file to be
221 # inserted before any subsequent `require` or `include`
222 # directives. Useful in CSS files, where it's common for the
223 # index file to contain global styles that need to be defined
224 # before other dependencies are loaded.
225 #
226 # /*= require "reset"
227 # *= require_self
228 # *= require_tree .
229 # */
230 #
231 def process_require_self_directive
7b86c89 @josh Calling require_self twice raises an error
josh authored
232 if @has_written_body
233 raise ArgumentError, "require_self can only be called once per source file"
c1bed19 @josh Fix require_self include ordering
josh authored
234 end
7b86c89 @josh Calling require_self twice raises an error
josh authored
235
c103b99 @josh Merge branch 'master' into debug-assets
josh authored
236 context.require_asset(pathname)
237 process_source
7b86c89 @josh Calling require_self twice raises an error
josh authored
238 included_pathnames.clear
239 @has_written_body = true
1919fa3 @sstephenson require_self
authored
240 end
241
ebd683e @josh Docco DirectiveProcessor
josh authored
242 # The `include` directive works similar to `require` but
a4c3a85 Fix typos.
Daniel Brockman authored
243 # inserts the contents of the dependency even if it already
ebd683e @josh Docco DirectiveProcessor
josh authored
244 # has been required.
245 #
246 # //= include "header"
247 #
248 def process_include_directive(path)
54be242 @josh Add `depend_on_asset` directive
josh authored
249 pathname = context.resolve(path)
250 context.depend_on_asset(pathname)
251 included_pathnames << pathname
ebd683e @josh Docco DirectiveProcessor
josh authored
252 end
253
254 # `require_directory` requires all the files inside a single
a4c3a85 Fix typos.
Daniel Brockman authored
255 # directory. It's similar to `path/*` since it does not follow
ebd683e @josh Docco DirectiveProcessor
josh authored
256 # nested directories.
257 #
258 # //= require_directory "./javascripts"
259 #
f22f61a @josh Expose public concatenation methods on Context
josh authored
260 def process_require_directory_directive(path = ".")
261 if relative?(path)
a905358 @josh Remove base_path
josh authored
262 root = pathname.dirname.join(path).expand_path
2c888e8 @josh Improve error message for require_tree with nonexistent directory
josh authored
263
8be6fd5 @josh Use cached iterator in directive processor
josh authored
264 unless (stats = stat(root)) && stats.directory?
b3201f8 @KODerFunk Update lib/sprockets/directive_processor.rb
KODerFunk authored
265 raise ArgumentError, "require_directory argument must be a directory"
2c888e8 @josh Improve error message for require_tree with nonexistent directory
josh authored
266 end
267
36a4378 @josh Cleaned up context namespaced methods
josh authored
268 context.depend_on(root)
5d84cb0 @josh Add depend directive
josh authored
269
8be6fd5 @josh Use cached iterator in directive processor
josh authored
270 entries(root).each do |pathname|
271 pathname = root.join(pathname)
272 if pathname.to_s == self.file
4e6333c @josh require_tree and require_directory should skip self
josh authored
273 next
8be6fd5 @josh Use cached iterator in directive processor
josh authored
274 elsif context.asset_requirable?(pathname)
275 context.require_asset(pathname)
f22f61a @josh Expose public concatenation methods on Context
josh authored
276 end
1d4a9f7 @josh Add require_directory directive
josh authored
277 end
f22f61a @josh Expose public concatenation methods on Context
josh authored
278 else
ebd683e @josh Docco DirectiveProcessor
josh authored
279 # The path must be relative and start with a `./`.
f22f61a @josh Expose public concatenation methods on Context
josh authored
280 raise ArgumentError, "require_directory argument must be a relative path"
1d4a9f7 @josh Add require_directory directive
josh authored
281 end
282 end
283
ebd683e @josh Docco DirectiveProcessor
josh authored
284 # `require_tree` requires all the nested files in a directory.
285 # Its glob equivalent is `path/**/*`.
286 #
287 # //= require_tree "./public"
288 #
f22f61a @josh Expose public concatenation methods on Context
josh authored
289 def process_require_tree_directive(path = ".")
290 if relative?(path)
a905358 @josh Remove base_path
josh authored
291 root = pathname.dirname.join(path).expand_path
2c888e8 @josh Improve error message for require_tree with nonexistent directory
josh authored
292
8be6fd5 @josh Use cached iterator in directive processor
josh authored
293 unless (stats = stat(root)) && stats.directory?
2c888e8 @josh Improve error message for require_tree with nonexistent directory
josh authored
294 raise ArgumentError, "require_tree argument must be a directory"
295 end
296
36a4378 @josh Cleaned up context namespaced methods
josh authored
297 context.depend_on(root)
8c6fdd9 @josh Track directories from requrie_tree in source paths
josh authored
298
8be6fd5 @josh Use cached iterator in directive processor
josh authored
299 each_entry(root) do |pathname|
300 if pathname.to_s == self.file
4e6333c @josh require_tree and require_directory should skip self
josh authored
301 next
8be6fd5 @josh Use cached iterator in directive processor
josh authored
302 elsif stat(pathname).directory?
303 context.depend_on(pathname)
304 elsif context.asset_requirable?(pathname)
305 context.require_asset(pathname)
e28d7a4 @josh Require and depend on paths directly on concatenation
josh authored
306 end
f22f61a @josh Expose public concatenation methods on Context
josh authored
307 end
308 else
ebd683e @josh Docco DirectiveProcessor
josh authored
309 # The path must be relative and start with a `./`.
f22f61a @josh Expose public concatenation methods on Context
josh authored
310 raise ArgumentError, "require_tree argument must be a relative path"
41069da @sstephenson Implement require_tree
authored
311 end
312 end
313
ebd683e @josh Docco DirectiveProcessor
josh authored
314 # Allows you to state a dependency on a file without
315 # including it.
316 #
317 # This is used for caching purposes. Any changes made to
54be242 @josh Add `depend_on_asset` directive
josh authored
318 # the dependency file will invalidate the cache of the
ebd683e @josh Docco DirectiveProcessor
josh authored
319 # source file.
320 #
321 # This is useful if you are using ERB and File.read to pull
322 # in contents from another file.
323 #
c347176 @josh Rename "depend" directive to "depend_on"
josh authored
324 # //= depend_on "foo.png"
ebd683e @josh Docco DirectiveProcessor
josh authored
325 #
c347176 @josh Rename "depend" directive to "depend_on"
josh authored
326 def process_depend_on_directive(path)
54be242 @josh Add `depend_on_asset` directive
josh authored
327 context.depend_on(path)
328 end
329
330 # Allows you to state a dependency on an asset without including
331 # it.
332 #
333 # This is used for caching purposes. Any changes that would
334 # invalid the asset dependency will invalidate the cache our the
335 # source file.
336 #
337 # Unlike `depend_on`, the path must be a requirable asset.
338 #
339 # //= depend_on_asset "bar.js"
340 #
341 def process_depend_on_asset_directive(path)
342 context.depend_on_asset(path)
ebd683e @josh Docco DirectiveProcessor
josh authored
343 end
344
95ae9b7 @josh Add `stub` directive
josh authored
345 # Allows dependency to be excluded from the asset bundle.
346 #
347 # The `path` must be a valid asset and may or may not already
348 # be part of the bundle. Once stubbed, it is blacklisted and
349 # can't be brought back by any other `require`.
350 #
351 # //= stub "jquery"
352 #
353 def process_stub_directive(path)
354 context.stub_asset(path)
355 end
356
ebd683e @josh Docco DirectiveProcessor
josh authored
357 # Enable Sprockets 1.x compat mode.
358 #
a4c3a85 Fix typos.
Daniel Brockman authored
359 # Makes it possible to use the same JavaScript source
ebd683e @josh Docco DirectiveProcessor
josh authored
360 # file in both Sprockets 1 and 2.
361 #
362 # //= compat
363 #
364 def process_compat_directive
365 @compat = true
366 end
367
368 # Checks if Sprockets 1.x compat mode enabled
369 def compat?
370 @compat
371 end
372
373 # Sprockets 1.x allowed for constant interpolation if a
374 # constants.yml was present. This is only available if
375 # compat mode is on.
376 def constants
377 if compat?
8be6fd5 @josh Use cached iterator in directive processor
josh authored
378 pathname = Pathname.new(context.root_path).join("constants.yml")
379 stat(pathname) ? YAML.load_file(pathname) : {}
ebd683e @josh Docco DirectiveProcessor
josh authored
380 else
7b31431 @josh Whitespace
josh authored
381 {}
ebd683e @josh Docco DirectiveProcessor
josh authored
382 end
383 end
384
385 # `provide` is stubbed out for Sprockets 1.x compat.
386 # Mutating the path when an asset is being built is
387 # not permitted.
f22f61a @josh Expose public concatenation methods on Context
josh authored
388 def process_provide_directive(path)
389 end
5de0db7 @josh Basic compat mode support
josh authored
390
f22f61a @josh Expose public concatenation methods on Context
josh authored
391 private
392 def relative?(path)
393 path =~ /^\.($|\.?\/)/
394 end
8be6fd5 @josh Use cached iterator in directive processor
josh authored
395
396 def stat(path)
397 context.environment.stat(path)
398 end
399
400 def entries(path)
401 context.environment.entries(path)
402 end
403
404 def each_entry(root, &block)
405 context.environment.each_entry(root, &block)
406 end
d3a1c5b @josh Indent
josh authored
407 end
67e5bc0 @sstephenson It works!
authored
408 end
Something went wrong with that request. Please try again.