-
Notifications
You must be signed in to change notification settings - Fork 789
/
directive_processor.rb
406 lines (369 loc) · 12.9 KB
/
directive_processor.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# frozen_string_literal: true
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
# 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.instance
# Default to C comment styles
@instance ||= new(comments: ["//", ["/*", "*/"]])
end
def self.call(input)
instance.call(input)
end
def initialize(comments: [])
@header_pattern = compile_header_pattern(Array(comments))
end
def call(input)
dup._call(input)
end
def _call(input)
@environment = input[:environment]
@uri = input[:uri]
@filename = input[:filename]
@dirname = File.dirname(@filename)
@content_type = input[:content_type]
@required = Set.new(input[:metadata][:required])
@stubbed = Set.new(input[:metadata][:stubbed])
@links = Set.new(input[:metadata][:links])
@dependencies = Set.new(input[:metadata][:dependencies])
data, directives = process_source(input[:data])
process_directives(directives)
{ data: data,
required: @required,
stubbed: @stubbed,
links: @links,
dependencies: @dependencies }
end
protected
# 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.
def compile_header_pattern(comments)
re = comments.map { |c|
case c
when String
"(?:#{Regexp.escape(c)}.*\\n?)+"
when Array
"(?:#{Regexp.escape(c[0])}(?m:.*?)#{Regexp.escape(c[1])})"
else
raise TypeError, "unknown comment type: #{c.class}"
end
}.join("|")
Regexp.compile("\\A(?:(?m:\\s*)(?:#{re}))+")
end
def process_source(source)
header = source[@header_pattern, 0] || ""
body = $' || source
header, directives = extract_directives(header)
data = String.new("")
data.force_encoding(body.encoding)
data << header unless header.empty?
data << body
# Ensure body ends in a new line
data << "\n" if data.length > 0 && data[-1] != "\n"
return data, 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 = String.new("")
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
processed_header.chomp!
# Ensure header ends in a new line like before it was processed
processed_header << "\n" if processed_header.length > 0 && header[-1] == "\n"
return processed_header, 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(glob)
# 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 it's 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(path, accept: @content_type, pipeline: :self)
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 = ".")
path = expand_relative_dirname(:require_directory, path)
require_paths(*@environment.stat_directory_with_dependencies(path))
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 = ".")
path = expand_relative_dirname(:require_tree, path)
require_paths(*@environment.stat_sorted_tree_with_dependencies(path))
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)
resolve(path)
end
# Allows you to state a dependency on an asset without including
# it.
#
# This is used for caching purposes. Any changes that would
# invalidate the asset dependency will invalidate the cache of
# 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)
load(resolve(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)
@stubbed << resolve(path, accept: @content_type, pipeline: :self)
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)
@links << load(resolve(path)).uri
end
# `link_directory` links all the files inside a single
# directory. It's similar to `path/*` since it does not follow
# nested directories.
#
# //= link_directory "./fonts"
#
# Use caution when linking against JS or CSS assets. Include an explicit
# extension or content type in these cases.
#
# //= link_directory "./scripts" .js
#
def process_link_directory_directive(path = ".", accept = nil)
path = expand_relative_dirname(:link_directory, path)
accept = expand_accept_shorthand(accept)
link_paths(*@environment.stat_directory_with_dependencies(path), accept)
end
# `link_tree` links all the nested files in a directory.
# Its glob equivalent is `path/**/*`.
#
# //= link_tree "./images"
#
# Use caution when linking against JS or CSS assets. Include an explicit
# extension or content type in these cases.
#
# //= link_tree "./styles" .css
#
def process_link_tree_directive(path = ".", accept = nil)
path = expand_relative_dirname(:link_tree, path)
accept = expand_accept_shorthand(accept)
link_paths(*@environment.stat_sorted_tree_with_dependencies(path), accept)
end
private
def expand_accept_shorthand(accept)
if accept.nil?
nil
elsif accept.include?("/")
accept
elsif accept.start_with?(".")
@environment.mime_exts[accept]
else
@environment.mime_exts[".#{accept}"]
end
end
def require_paths(paths, deps)
resolve_paths(paths, deps, accept: @content_type, pipeline: :self) do |uri|
@required << uri
end
end
def link_paths(paths, deps, accept)
resolve_paths(paths, deps, accept: accept) do |uri|
@links << load(uri).uri
end
end
def resolve_paths(paths, deps, **kargs)
@dependencies.merge(deps)
paths.each do |subpath, stat|
next if subpath == @filename || stat.directory?
uri, deps = @environment.resolve(subpath, **kargs)
@dependencies.merge(deps)
yield uri if uri
end
end
def expand_relative_dirname(directive, path)
if @environment.relative_path?(path)
path = File.expand_path(path, @dirname)
stat = @environment.stat(path)
if stat && stat.directory?
path
else
raise ArgumentError, "#{directive} argument must be a directory"
end
else
# The path must be relative and start with a `./`.
raise ArgumentError, "#{directive} argument must be a relative path"
end
end
def load(uri)
asset = @environment.load(uri)
@dependencies.merge(asset.metadata[:dependencies])
asset
end
def resolve(path, **kargs)
# Prevent absolute paths in directives
if @environment.absolute_path?(path)
raise FileOutsidePaths, "can't require absolute file: #{path}"
end
kargs[:base_path] = @dirname
uri, deps = @environment.resolve!(path, **kargs)
@dependencies.merge(deps)
uri
end
end
end