/*! * 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;