478 lines
10 KiB
JavaScript
478 lines
10 KiB
JavaScript
|
/*!
|
||
|
* node-sass: lib/index.js
|
||
|
*/
|
||
|
|
||
|
var path = require('path'),
|
||
|
clonedeep = require('lodash/cloneDeep'),
|
||
|
assign = require('lodash/assign'),
|
||
|
sass = require('./extensions');
|
||
|
|
||
|
/**
|
||
|
* Require binding
|
||
|
*/
|
||
|
|
||
|
var binding = require('./binding')(sass);
|
||
|
|
||
|
/**
|
||
|
* Get input file
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function getInputFile(options) {
|
||
|
return options.file ? path.resolve(options.file) : null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get output file
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function getOutputFile(options) {
|
||
|
var outFile = options.outFile;
|
||
|
|
||
|
if (!outFile || typeof outFile !== 'string' || (!options.data && !options.file)) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
return path.resolve(outFile);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get source map
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function getSourceMap(options) {
|
||
|
var sourceMap = options.sourceMap;
|
||
|
|
||
|
if (sourceMap && typeof sourceMap !== 'string' && options.outFile) {
|
||
|
sourceMap = options.outFile + '.map';
|
||
|
}
|
||
|
|
||
|
return sourceMap && typeof sourceMap === 'string' ? path.resolve(sourceMap) : null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get stats
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function getStats(options) {
|
||
|
var stats = {};
|
||
|
|
||
|
stats.entry = options.file || 'data';
|
||
|
stats.start = Date.now();
|
||
|
|
||
|
return stats;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* End stats
|
||
|
*
|
||
|
* @param {Object} stats
|
||
|
* @param {Object} sourceMap
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function endStats(stats) {
|
||
|
stats.end = Date.now();
|
||
|
stats.duration = stats.end - stats.start;
|
||
|
|
||
|
return stats;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get style
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function getStyle(options) {
|
||
|
var styles = {
|
||
|
nested: 0,
|
||
|
expanded: 1,
|
||
|
compact: 2,
|
||
|
compressed: 3
|
||
|
};
|
||
|
|
||
|
return styles[options.outputStyle] || 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get indent width
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function getIndentWidth(options) {
|
||
|
var width = parseInt(options.indentWidth) || 2;
|
||
|
|
||
|
return width > 10 ? 2 : width;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get indent type
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function getIndentType(options) {
|
||
|
var types = {
|
||
|
space: 0,
|
||
|
tab: 1
|
||
|
};
|
||
|
|
||
|
return types[options.indentType] || 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get linefeed
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function getLinefeed(options) {
|
||
|
var feeds = {
|
||
|
cr: '\r',
|
||
|
crlf: '\r\n',
|
||
|
lf: '\n',
|
||
|
lfcr: '\n\r'
|
||
|
};
|
||
|
|
||
|
return feeds[options.linefeed] || '\n';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Build an includePaths string
|
||
|
* from the options.includePaths array and the SASS_PATH environment variable
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function buildIncludePaths(options) {
|
||
|
options.includePaths = options.includePaths || [];
|
||
|
|
||
|
if (process.env.hasOwnProperty('SASS_PATH')) {
|
||
|
options.includePaths = options.includePaths.concat(
|
||
|
process.env.SASS_PATH.split(path.delimiter)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Preserve the behaviour people have come to expect.
|
||
|
// This behaviour was removed from Sass in 3.4 and
|
||
|
// LibSass in 3.5.
|
||
|
options.includePaths.unshift(process.cwd());
|
||
|
|
||
|
return options.includePaths.join(path.delimiter);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get options
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function getOptions(opts, cb) {
|
||
|
if (typeof opts !== 'object') {
|
||
|
throw new Error('Invalid: options is not an object.');
|
||
|
}
|
||
|
var options = clonedeep(opts || {});
|
||
|
|
||
|
options.sourceComments = options.sourceComments || false;
|
||
|
if (options.hasOwnProperty('file')) {
|
||
|
options.file = getInputFile(options);
|
||
|
}
|
||
|
options.outFile = getOutputFile(options);
|
||
|
options.includePaths = buildIncludePaths(options);
|
||
|
options.precision = parseInt(options.precision) || 5;
|
||
|
options.sourceMap = getSourceMap(options);
|
||
|
options.style = getStyle(options);
|
||
|
options.indentWidth = getIndentWidth(options);
|
||
|
options.indentType = getIndentType(options);
|
||
|
options.linefeed = getLinefeed(options);
|
||
|
|
||
|
// context object represents node-sass environment
|
||
|
options.context = { options: options, callback: cb };
|
||
|
|
||
|
options.result = {
|
||
|
stats: getStats(options)
|
||
|
};
|
||
|
|
||
|
return options;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Executes a callback and transforms any exception raised into a sass error
|
||
|
*
|
||
|
* @param {Function} callback
|
||
|
* @param {Array} arguments
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function tryCallback(callback, args) {
|
||
|
try {
|
||
|
return callback.apply(this, args);
|
||
|
} catch (e) {
|
||
|
if (typeof e === 'string') {
|
||
|
return new binding.types.Error(e);
|
||
|
} else if (e instanceof Error) {
|
||
|
return new binding.types.Error(e.message);
|
||
|
} else {
|
||
|
return new binding.types.Error('An unexpected error occurred');
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Normalizes the signature of custom functions to make it possible to just supply the
|
||
|
* function name and have the signature default to `fn(...)`. The callback is adjusted
|
||
|
* to transform the input sass list into discrete arguments.
|
||
|
*
|
||
|
* @param {String} signature
|
||
|
* @param {Function} callback
|
||
|
* @return {Object}
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function normalizeFunctionSignature(signature, callback) {
|
||
|
if (!/^\*|@warn|@error|@debug|\w+\(.*\)$/.test(signature)) {
|
||
|
if (!/\w+/.test(signature)) {
|
||
|
throw new Error('Invalid function signature format "' + signature + '"');
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
signature: signature + '(...)',
|
||
|
callback: function() {
|
||
|
var args = Array.prototype.slice.call(arguments),
|
||
|
list = args.shift(),
|
||
|
i;
|
||
|
|
||
|
for (i = list.getLength() - 1; i >= 0; i--) {
|
||
|
args.unshift(list.getValue(i));
|
||
|
}
|
||
|
|
||
|
return callback.apply(this, args);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
signature: signature,
|
||
|
callback: callback
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Render
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
module.exports.render = function(opts, cb) {
|
||
|
var options = getOptions(opts, cb);
|
||
|
|
||
|
// options.error and options.success are for libsass binding
|
||
|
options.error = function(err) {
|
||
|
var payload = assign(new Error(), JSON.parse(err));
|
||
|
|
||
|
if (cb) {
|
||
|
options.context.callback.call(options.context, payload, null);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
options.success = function() {
|
||
|
var result = options.result;
|
||
|
var stats = endStats(result.stats);
|
||
|
var payload = {
|
||
|
css: result.css,
|
||
|
stats: stats
|
||
|
};
|
||
|
if (result.map) {
|
||
|
payload.map = result.map;
|
||
|
}
|
||
|
|
||
|
if (cb) {
|
||
|
options.context.callback.call(options.context, null, payload);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var importer = options.importer;
|
||
|
|
||
|
if (importer) {
|
||
|
if (Array.isArray(importer)) {
|
||
|
options.importer = [];
|
||
|
importer.forEach(function(subject, index) {
|
||
|
options.importer[index] = function(file, prev, bridge) {
|
||
|
function done(result) {
|
||
|
bridge.success(result === module.exports.NULL ? null : result);
|
||
|
}
|
||
|
|
||
|
var result = subject.call(options.context, file, prev, done);
|
||
|
|
||
|
if (result !== undefined) {
|
||
|
done(result);
|
||
|
}
|
||
|
};
|
||
|
});
|
||
|
} else {
|
||
|
options.importer = function(file, prev, bridge) {
|
||
|
function done(result) {
|
||
|
bridge.success(result === module.exports.NULL ? null : result);
|
||
|
}
|
||
|
|
||
|
var result = importer.call(options.context, file, prev, done);
|
||
|
|
||
|
if (result !== undefined) {
|
||
|
done(result);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var functions = clonedeep(options.functions);
|
||
|
|
||
|
if (functions) {
|
||
|
options.functions = {};
|
||
|
|
||
|
Object.keys(functions).forEach(function(subject) {
|
||
|
var cb = normalizeFunctionSignature(subject, functions[subject]);
|
||
|
|
||
|
options.functions[cb.signature] = function() {
|
||
|
var args = Array.prototype.slice.call(arguments),
|
||
|
bridge = args.pop();
|
||
|
|
||
|
function done(data) {
|
||
|
bridge.success(data);
|
||
|
}
|
||
|
|
||
|
var result = tryCallback(cb.callback.bind(options.context), args.concat(done));
|
||
|
|
||
|
if (result) {
|
||
|
done(result);
|
||
|
}
|
||
|
};
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (options.data) {
|
||
|
binding.render(options);
|
||
|
} else if (options.file) {
|
||
|
binding.renderFile(options);
|
||
|
} else {
|
||
|
cb({status: 3, message: 'No input specified: provide a file name or a source string to process' });
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Render sync
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
module.exports.renderSync = function(opts) {
|
||
|
var options = getOptions(opts);
|
||
|
var importer = options.importer;
|
||
|
|
||
|
if (importer) {
|
||
|
if (Array.isArray(importer)) {
|
||
|
options.importer = [];
|
||
|
importer.forEach(function(subject, index) {
|
||
|
options.importer[index] = function(file, prev) {
|
||
|
var result = subject.call(options.context, file, prev);
|
||
|
|
||
|
return result === module.exports.NULL ? null : result;
|
||
|
};
|
||
|
});
|
||
|
} else {
|
||
|
options.importer = function(file, prev) {
|
||
|
var result = importer.call(options.context, file, prev);
|
||
|
|
||
|
return result === module.exports.NULL ? null : result;
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var functions = clonedeep(options.functions);
|
||
|
|
||
|
if (options.functions) {
|
||
|
options.functions = {};
|
||
|
|
||
|
Object.keys(functions).forEach(function(signature) {
|
||
|
var cb = normalizeFunctionSignature(signature, functions[signature]);
|
||
|
|
||
|
options.functions[cb.signature] = function() {
|
||
|
return tryCallback(cb.callback.bind(options.context), arguments);
|
||
|
};
|
||
|
});
|
||
|
}
|
||
|
|
||
|
var status;
|
||
|
if (options.data) {
|
||
|
status = binding.renderSync(options);
|
||
|
} else if (options.file) {
|
||
|
status = binding.renderFileSync(options);
|
||
|
} else {
|
||
|
throw new Error('No input specified: provide a file name or a source string to process');
|
||
|
}
|
||
|
|
||
|
var result = options.result;
|
||
|
|
||
|
if (status) {
|
||
|
result.stats = endStats(result.stats);
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
throw assign(new Error(), JSON.parse(result.error));
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* API Info
|
||
|
*
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
module.exports.info = sass.getVersionInfo(binding);
|
||
|
|
||
|
/**
|
||
|
* Expose sass types
|
||
|
*/
|
||
|
|
||
|
module.exports.types = binding.types;
|
||
|
module.exports.TRUE = binding.types.Boolean.TRUE;
|
||
|
module.exports.FALSE = binding.types.Boolean.FALSE;
|
||
|
module.exports.NULL = binding.types.Null.NULL;
|
||
|
|
||
|
/**
|
||
|
* Polyfill the old API
|
||
|
*
|
||
|
* TODO: remove for 4.0
|
||
|
*/
|
||
|
|
||
|
function processSassDeprecationMessage() {
|
||
|
console.log('Deprecation warning: `process.sass` is an undocumented internal that will be removed in future versions of Node Sass.');
|
||
|
}
|
||
|
|
||
|
process.sass = process.sass || {
|
||
|
get versionInfo() { processSassDeprecationMessage(); return module.exports.info; },
|
||
|
get binaryName() { processSassDeprecationMessage(); return sass.getBinaryName(); },
|
||
|
get binaryUrl() { processSassDeprecationMessage(); return sass.getBinaryUrl(); },
|
||
|
get binaryPath() { processSassDeprecationMessage(); return sass.getBinaryPath(); },
|
||
|
get getBinaryPath() { processSassDeprecationMessage(); return sass.getBinaryPath; },
|
||
|
};
|