You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
458 lines
9.6 KiB
458 lines
9.6 KiB
/*! |
|
* node-sass: lib/index.js |
|
*/ |
|
|
|
var path = require('path'), |
|
clonedeep = require('lodash/cloneDeep'), |
|
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 (Object.prototype.hasOwnProperty.call(process.env, '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 (Object.prototype.hasOwnProperty.call(options, '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 = Object.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 Object.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;
|
|
|