-
Notifications
You must be signed in to change notification settings - Fork 84
/
directive_processor.js
428 lines (343 loc) · 11.4 KB
/
directive_processor.js
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
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
/**
* class DirectiveProcessor
*
* 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 one-line comments:** `//= require "foo"
* - **CoffeeScript one-line comments:** `#= require "baz"
* - **JavaScript and CSS block comments:** `*= require "bar"
*
* This behavior can be disabled with:
*
* environment.unregisterPreProcessor('text/css', DirectiveProcessor);
* environment.unregisterPreProcessor('application/javascript', DirectiveProcessor);
*
*
* ##### SUBCLASS OF
*
* [[Template]]
**/
'use strict';
// stdlib
var path = require('path');
// 3rd-party
var _ = require('lodash');
var shellwords = require('shellwords').split;
// internal
var Template = require('../template');
var prop = require('../common').prop;
var getter = require('../common').getter;
var isAbsolute = require('../common').isAbsolute;
var isRelative = require('../common').isRelative;
////////////////////////////////////////////////////////////////////////////////
// Returns an Array of lines.
// Originl idea of cross-platform line splitting taken from Sugr.JS:
// https://github.com/andrewplummer/Sugar/blob/f6d1c2e9/lib/string.js#L323
function get_lines(str) {
return String(str || '').match(/^.*$/gm);
}
// 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.
var HEADER_PATTERN = new RegExp(
'^(?:\\s*' +
'(' +
'(?:\/[*](?:\\s*|.+?)*?[*]\/)' + '|' +
'(?:###\n(?:\\s*|.+?)*?\n###)' + '|' +
'(?:\/\/.*\n?)+' + '|' +
'(?:#.*\n?)+' +
')*' +
')*', 'm');
// Directives are denoted by a `=` followed by the name, then
// argument list.
//
// A few different styles are allowed:
//
// // =require foo
// //= require foo
// //= require "foo"
//
var DIRECTIVE_PATTERN = new RegExp('^\\W*=\\s*(\\w+.*?)(\\*\\/)?$');
// Real directive processors
var DIRECTIVE_HANDLERS = {
// The `require` directive functions similar to Ruby's `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"
//
require: function (self, args) {
var pathname = isRelative(args[0]) ? args[0] : ('./' + args[0]);
self.context.requireAsset(pathname);
},
// `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 .
// */
//
require_self: function (self/*, args*/) {
if (self.__hasWrittenBody__) {
throw new Error('require_self can only be called once per source file');
}
self.context.requireAsset(self.pathname);
self.processSource();
prop(self, '__hasWrittenBody__', true);
self.includedPathnames = [];
},
// The `include` directive works similar to `require` but
// inserts the contents of the dependency even if it already
// has been required.
//
// //= include "header"
//
include: function (self, args) {
var pathname = self.context.resolve(args[0]);
self.context.dependOnAsset(pathname);
self.includedPathnames.push(pathname);
},
// `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"
//
require_directory: function (self, args) {
var root, pathname = args[0] || '.', stat;
if (isAbsolute(pathname)) {
throw new Error('require_directory argument must be a relative path');
}
root = path.resolve(path.dirname(self.pathname), pathname);
stat = self.stat(root);
if (!stat || !stat.isDirectory()) {
throw new Error('require_directory argument must be a directory');
}
self.context.dependOn(root);
_.forEach(self.entries(root), function (pathname) {
pathname = path.join(root, pathname);
if (self.file === pathname) {
return;
} else if (self.context.isAssetRequirable(pathname)) {
self.context.requireAsset(pathname);
}
});
},
// `require_tree` requires all the nested files in a directory.
// Its glob equivalent is `path/**/*`.
//
// //= require_tree "./public"
//
require_tree: function (self, args) {
var root, pathname = args[0] || '.', stat;
if (isAbsolute(pathname)) {
throw new Error('require_tree argument must be a relative path');
}
root = path.resolve(path.dirname(self.pathname), pathname);
stat = self.stat(root);
if (!stat || !stat.isDirectory()) {
throw new Error('require_tree argument must be a directory');
}
self.context.dependOn(root);
self.eachEntry(root, function (pathname) {
if (self.file === pathname) {
return;
} else if (self.stat(pathname).isDirectory()) {
self.context.dependOn(pathname);
} else if (self.context.isAssetRequirable(pathname)) {
self.context.requireAsset(pathname);
}
});
},
// 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"
//
depend_on: function (self, args) {
self.context.dependOn(args[0]);
},
// 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"
//
depend_on_asset: function (self, args) {
self.context.dependOnAsset(args[0]);
},
// 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"
//
stub: function (self, args) {
self.context.stubAsset(args[0]);
}
};
////////////////////////////////////////////////////////////////////////////////
// Class constructor
var DirectiveProcessor = module.exports = function DirectiveProcessor() {
Template.apply(this, arguments);
};
require('util').inherits(DirectiveProcessor, Template);
// Run processor
DirectiveProcessor.prototype.evaluate = function (context/*, locals*/) {
var self = this,
header = (HEADER_PATTERN.exec(this.data) || []).shift() || '';
// drop trailing spaces and line breaks
header = header.trimRight();
prop(this, 'pathname', this.file);
prop(this, 'header', header);
prop(this, 'body', this.data.substr(header.length) + '\n');
prop(this, 'includedPathnames', [], {writable: true});
prop(this, 'context', context);
prop(this, 'result', '', {writable: true});
self.processDirectives();
self.processSource();
return self.result;
};
/**
* DirectiveProcessor#processDirectives() -> Void
*
* Executes handlers for found directives.
*
* ##### See Also:
*
* - [[DirectiveProcessor#directives]]
**/
DirectiveProcessor.prototype.processDirectives = function () {
var self = this;
// Execute handler for each found directive
_.forEach(this.directives, function (arr) {
self.context.__LINE__ = arr[0];
// arr = [
// 10, # 0: LINE
// 'require', # 1: DIRECTIVE
// ['foobar'] # 2: [ARGUMENTS]
// ]
DIRECTIVE_HANDLERS[arr[1]](self, arr[2]);
self.context.__LINE__ = null;
});
};
DirectiveProcessor.prototype.processSource = function () {
var self = this;
// if our own body was not yet appended, and there are header comments,
// prepend these coments first.
if (!self.__hasWrittenBody__ && 0 < self.processedHeader.length) {
self.result += self.processedHeader;
}
// process and append body of each path that should be included
_.forEach(self.includedPathnames, function (pathname) {
self.result += self.context.evaluate(pathname, {});
});
// append own body of source only, if it was not yet written
// (with `require_self` directive).
if (!self.__hasWrittenBody__) {
self.result += self.body;
}
};
// Tells whenever given line is directive or not by
// comparing found directives line indexes with `lineno`
function is_directive(directives, lineno) {
return _.any(directives, function (arr) { return arr[0] === lineno; });
}
/**
* DirectiveProcessor#processedHeader -> String
*
* Returns the header String with any directives stripped.
**/
getter(DirectiveProcessor.prototype, 'processedHeader', function () {
var header;
if (!this.__processedHeader__) {
header = get_lines(this.header).map(function (line, index) {
return is_directive(this.directives, index + 1) ? '' : line;
}, this).join('\n');
prop(this, '__processedHeader__', header);
}
return this.__processedHeader__;
});
/**
* DirectiveProcessor#processedSource -> String
*
* Returns the source String with any directives stripped.
**/
getter(DirectiveProcessor.prototype, 'processedSource', function () {
if (!this.__processedSource__) {
this.__processedSource__ = this.processedHeader + this.body;
}
return this.__processedSource__;
});
/**
* DirectiveProcessor#directives -> Array
*
* 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, third is an array of
* arguments.
*
* [[1, "require", ["foo"]], [2, "require", ["bar"]]]
**/
getter(DirectiveProcessor.prototype, 'directives', function () {
if (!this.__directives__) {
prop(this, '__directives__', []);
get_lines(this.header).forEach(function (line, index) {
var matches = DIRECTIVE_PATTERN.exec(line), name, args;
if (matches && matches[1]) {
args = shellwords(matches[1]);
name = args.shift();
if (_.isFunction(DIRECTIVE_HANDLERS[name])) {
this.__directives__.push([index + 1, name, args]);
}
}
}, this);
}
return this.__directives__;
});
DirectiveProcessor.prototype.stat = function (pathname) {
return this.context.environment.stat(pathname);
};
DirectiveProcessor.prototype.entries = function (pathname) {
return this.context.environment.entries(pathname);
};
DirectiveProcessor.prototype.eachEntry = function (path, func) {
return this.context.environment.eachEntry(path, func);
};