UNPKG

mincer

Version:

Web assets processor. Native JavaScript port of Sprockets.

436 lines (348 loc) 11.9 kB
/** * 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. // Original idea of cross-platform line splitting taken from Sugar.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 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" // 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(); this.data = 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__ && self.processedHeader.length > 0) { 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 _.some(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) { var parts; if (is_directive(this.directives, index + 1)) { parts = line.split('='); line = parts.shift() + '(=)' + parts.join('='); } return 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); };