123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561 |
- var gonzales = require('gonzales-pe');
- var minimatch = require('minimatch');
- var vow = require('vow');
- var vfs = require('vow-fs');
- /**
- * @param {Array} predefinedOptions
- * @constructor
- * @name Comb
- */
- var Comb = function(predefinedOptions) {
- // For unambiguity, use `that` to access public methods
- // and `_this` to access private ones.
- var that = this;
- /**
- * PRIVATE AREA
- * Those properties and methods are made available for easy testing.
- */
- Object.defineProperty(that, '_', {
- enumerable: false,
- writable: true,
- configurable: true
- });
- var _this = that._ = {};
- /**
- * PRIVATE PROPERTIES
- */
- // List of handlers:
- _this.handlers = null;
- // List of supported syntaxes:
- _this.supportedSyntaxes = Array.prototype.slice.call(arguments, 1);
- // List of file paths that should be excluded from processing:
- _this.exclude = null;
- // List of configured options with values:
- _this.configuredOptions = null;
- // Whether lint mode is on:
- _this.lint = null;
- // Whether verbose mode is on:
- _this.verbose = null;
- // Map of used options:
- _this.options = {};
- // List of option names in exact order they should be processed:
- _this.optionsOrder = [];
- _this.unmetOptions = {};
- /**
- * PRIVATE METHODS
- * - addHandler;
- * - lintTree;
- * - processTree;
- * - setValue;
- * - shouldProcess;
- * - shouldProcessFile;
- * - updateOptionOrder;
- * - usePredefinedOptions;
- */
- /**
- * Adds an option to list of configured options.
- *
- * @param {Object} option Option's object
- * @param {Object} value Value that should be set for the option
- * @returns {Object} Object with option's name, value and link to
- * `process()` method
- */
- _this.addHandler = function addHandler(option, value) {
- value = option.setValue ?
- option.setValue(value) :
- _this.setValue(option.accepts, value);
- _this.handlers.push(option);
- _this.configuredOptions[option.name] = value;
- };
- /**
- * Processes stylesheet tree node.
- *
- * @param {Object} tree Parsed tree
- * @returns {Array} List of found errors
- */
- _this.lintTree = function lintTree(tree) {
- var _handlers;
- _handlers = _this.handlers.filter(function(handler) {
- var syntax = that.getSyntax();
- return handler.syntax.indexOf(syntax) > -1 &&
- typeof handler.lint === 'function';
- }).map(function(handler) {
- return handler.lint.bind(that);
- });
- // We walk across complete tree for each handler,
- // because we need strictly maintain order in which handlers work,
- // despite fact that handlers work on different level of the tree.
- var errors = [];
- _handlers.forEach(function(handler) {
- tree.map(function(node) {
- var error = handler(node);
- if (!error) return;
- if (Array.isArray(error)) {
- errors = errors.concat(error);
- } else {
- errors.push(error);
- }
- });
- });
- return errors;
- };
- /**
- * Processes stylesheet tree node.
- *
- * @param {Array} tree Parsed tree
- * @returns {Array} Modified tree
- */
- _this.processTree = function processTree(tree) {
- var _handlers;
- _handlers = _this.handlers.filter(function(handler) {
- var syntax = that.getSyntax();
- return handler.syntax.indexOf(syntax) > -1;
- }).map(function(handler) {
- return handler.process.bind(that);
- });
- // We walk across complete tree for each handler,
- // because we need strictly maintain order in which handlers work,
- // despite fact that handlers work on different level of the tree.
- _handlers.forEach(function(handler) {
- tree.map(handler);
- });
- return tree;
- };
- /**
- * Processes value and checks if it is acceptable by the option.
- *
- * @param {Object} acceptableValues Map of value types that are acceptable
- * by option. If `string` property is present, its value is a regular
- * expression that is used to validate value passed to the function.
- * @param {Object|undefined} value
- * @returns {Boolean|String} Valid option's value
- */
- _this.setValue = function setValue(acceptableValues, value) {
- if (!acceptableValues) throw new Error('Option\'s module must either' +
- ' implement `setValue()` method or provide `accepts` object' +
- ' with acceptable values.');
- var valueType = typeof value;
- var pattern = acceptableValues[valueType];
- if (!pattern) throw new Error('The option does not accept values of type "' +
- valueType + '".\nValue\'s type must be one the following: ' +
- Object.keys(acceptableValues).join(', ') + '.');
- switch (valueType) {
- case 'boolean':
- if (pattern.indexOf(value) < 0) throw new Error(' Value must be ' +
- 'one of the following: ' + pattern.join(', ') + '.');
- return value;
- case 'number':
- if (value !== parseInt(value)) throw new Error('Value must be an integer.');
- return new Array(value + 1).join(' ');
- case 'string':
- if (!value.match(pattern)) throw new Error('Value must match pattern ' +
- pattern + '.');
- return value;
- default:
- throw new Error('If you see this message and you are not' +
- ' a developer adding a new option, please open an issue here:' +
- ' https://github.com/csscomb/csscomb.js/issues/new' +
- '\nFor option to accept values of type "' + valueType +
- '" you need to implement custom `setValue()` method. See' +
- ' `lib/options/sort-order.js` for example.');
- }
- };
- /**
- * Checks if path is present in `exclude` list.
- *
- * @param {String} path
- * @returns {Boolean} False if specified path is present in `exclude` list.
- * Otherwise returns true.
- */
- _this.shouldProcess = function shouldProcess(path) {
- path = path.replace(/^\.\//, '');
- for (var i = _this.exclude.length; i--;) {
- if (_this.exclude[i].match(path)) return false;
- }
- return true;
- };
- /**
- * Checks if specified path is not present in `exclude` list and it has one of
- * acceptable extensions.
- *
- * @param {String} path
- * @returns {Boolean} False if the path either has unacceptable extension or
- * is present in `exclude` list. True if everything is ok.
- */
- _this.shouldProcessFile = function shouldProcessFile(path) {
- // Get file's extension:
- var syntax = path.split('.').pop();
- // Check if syntax is supported. If not, ignore the file:
- if (_this.supportedSyntaxes.indexOf(syntax) < 0) {
- return false;
- }
- return _this.shouldProcess(path);
- };
- /**
- * @param {Object} option
- */
- _this.updateOptionOrder = function updateOptionOrder(option) {
- var name = option.name;
- var runBefore = option.runBefore;
- var runBeforeIndex;
- _this.options[name] = option;
- if (runBefore) {
- runBeforeIndex = _this.optionsOrder.indexOf(runBefore);
- if (runBeforeIndex > -1) {
- _this.optionsOrder.splice(runBeforeIndex, 0, name);
- } else {
- _this.optionsOrder.push(name);
- if (!_this.unmetOptions[runBefore]) _this.unmetOptions[runBefore] = [];
- _this.unmetOptions[runBefore].push(name);
- }
- } else {
- _this.optionsOrder.push(name);
- }
- var unmet = _this.unmetOptions[name];
- if (unmet) {
- unmet.forEach(function(name) {
- var i = _this.optionsOrder.indexOf(name);
- _this.optionsOrder.splice(i, 1);
- _this.optionsOrder.splice( -1, 0, name);
- });
- }
- };
- _this.usePredefinedOptions = function usePredefinedOptions() {
- if (!predefinedOptions) return;
- predefinedOptions.forEach(function(option) {
- that.use(option);
- });
- };
- /**
- * PUBLIC INSTANCE METHODS
- * Methods that depend on certain instance variables, e.g. configuration:
- * - configure;
- * - getOptionsOrder;
- * - getSyntax;
- * - getValue;
- * - lintFile;
- * - lintDirectory;
- * - lintPath;
- * - lintString;
- * - processFile;
- * - processDirectory;
- * - processPath;
- * - processString;
- * - use;
- */
- /**
- * Loads configuration from JSON.
- * Activates and configures required options.
- *
- * @param {Object} config
- * @returns {Object} Comb's object (that makes the method chainable).
- */
- that.configure = function configure(config) {
- _this.handlers = [];
- _this.configuredOptions = {};
- _this.verbose = config.verbose;
- _this.lint = config.lint;
- _this.exclude = (config.exclude || []).map(function(pattern) {
- return new minimatch.Minimatch(pattern);
- });
- _this.optionsOrder.forEach(function(optionName) {
- if (config[optionName] === undefined) return;
- try {
- _this.addHandler(_this.options[optionName], config[optionName]);
- } catch (e) {
- // Show warnings about illegal config values only in verbose mode:
- if (_this.verbose) {
- console.warn('\nFailed to configure "%s" option:\n%s',
- optionName, e.message);
- }
- }
- });
- return that;
- };
- that.getOptionsOrder = function getOptionsOrder() {
- return _this.optionsOrder.slice();
- };
- that.getSyntax = function getSyntax() {
- return that.syntax;
- };
- /**
- * Gets option's value.
- *
- * @param {String} optionName
- * @returns {String|Boolean|undefined}
- */
- that.getValue = function getValue(optionName) {
- return _this.configuredOptions[optionName];
- };
- /**
- * @param {String} path
- * @returns {Promise}
- */
- that.lintDirectory = function lintDirectory(path) {
- return vfs.listDir(path).then(function(filenames) {
- return vow.all(filenames.map(function(filename) {
- var fullname = path + '/' + filename;
- return vfs.stat(fullname).then(function(stat) {
- if (stat.isDirectory()) {
- return _this.shouldProcess(fullname) && that.lintDirectory(fullname);
- } else {
- return that.lintFile(fullname);
- }
- });
- })).then(function(results) {
- return [].concat.apply([], results);
- });
- });
- };
- /**
- * @param {String} path
- * @returns {Promise}
- */
- that.lintFile = function lintFile(path) {
- if (!_this.shouldProcessFile(path)) return;
- return vfs.read(path, 'utf8').then(function(data) {
- var syntax = path.split('.').pop();
- return that.lintString(data, { syntax: syntax, filename: path });
- });
- };
- /**
- * @param {String} path
- * @returns {Promise}
- */
- that.lintPath = function lintPath(path) {
- path = path.replace(/\/$/, '');
- return vfs.exists(path).then(function(exists) {
- if (!exists) {
- console.warn('Path ' + path + ' was not found.');
- return;
- }
- return vfs.stat(path).then(function(stat) {
- if (stat.isDirectory()) {
- return that.lintDirectory(path);
- } else {
- return that.lintFile(path);
- }
- });
- });
- };
- /**
- * @param {String} text
- * @param {{context: String, filename: String, syntax: String}} options
- * @returns {Array} List of found errors
- */
- that.lintString = function lintString(text, options) {
- var syntax = options && options.syntax;
- var filename = options && options.filename;
- var context = options && options.context;
- var tree;
- if (!text) return [];
- if (!syntax) syntax = 'css';
- that.syntax = syntax;
- try {
- tree = gonzales.parse(text, { syntax: syntax, rule: context });
- } catch (e) {
- var version = require('../package.json').version;
- var message = filename ? [filename] : [];
- message.push(e.message);
- message.push('CSScomb Core version: ' + version);
- e.stack = e.message = message.join('\n');
- throw e;
- }
- var errors = _this.lintTree(tree);
- if (filename) {
- errors.map(function(error) {
- return error.filename = filename;
- })
- }
- return errors;
- };
- /**
- * Processes directory recursively.
- *
- * @param {String} path
- * @returns {Promise}
- */
- that.processDirectory = function processDirectory(path) {
- return vfs.listDir(path).then(function(filenames) {
- return vow.all(filenames.map(function(filename) {
- var fullname = path + '/' + filename;
- return vfs.stat(fullname).then(function(stat) {
- if (stat.isDirectory()) {
- return _this.shouldProcess(fullname) && that.processDirectory(fullname);
- } else {
- return that.processFile(fullname);
- }
- });
- })).then(function(results) {
- return [].concat.apply([], results);
- });
- });
- };
- /**
- * Processes single file.
- *
- * @param {String} path
- * @returns {Promise}
- */
- that.processFile = function processFile(path) {
- if (!_this.shouldProcessFile(path)) return;
- return vfs.read(path, 'utf8').then(function(data) {
- var syntax = path.split('.').pop();
- var processedData = that.processString(data, { syntax: syntax, filename: path });
- var isChanged = data !== processedData;
- var tick = isChanged ? (_this.lint ? '!' : '✓') : ' ';
- var output = function() {
- if (_this.verbose) console.log(tick, path);
- return isChanged ? 1 : 0;
- };
- if (!isChanged || _this.lint) {
- return output();
- } else {
- return vfs.write(path, processedData, 'utf8').then(output);
- }
- });
- };
- /**
- * Processes directory or file.
- *
- * @param {String} path
- * @returns {Promise}
- */
- that.processPath = function processPath(path) {
- path = path.replace(/\/$/, '');
- return vfs.exists(path).then(function(exists) {
- if (!exists) {
- console.warn('Path ' + path + ' was not found.');
- return;
- }
- return vfs.stat(path).then(function(stat) {
- if (stat.isDirectory()) {
- return that.processDirectory(path);
- } else {
- return that.processFile(path);
- }
- });
- });
- };
- /**
- * Processes a string.
- *
- * @param {String} text
- * @param {{context: String, filename: String, syntax: String}} options
- * @returns {String} Processed string
- */
- that.processString = function processString(text, options) {
- var syntax = options && options.syntax;
- var filename = options && options.filename || '';
- var context = options && options.context;
- var tree;
- if (!text) return text;
- // TODO: Parse different syntaxes
- syntax = syntax || 'css';
- that.syntax = syntax || 'css';
- try {
- tree = gonzales.parse(text, { syntax: syntax, rule: context });
- } catch (e) {
- var version = require('../package.json').version;
- var message = [filename,
- e.message,
- 'CSScomb Core version: ' + version];
- e.stack = e.message = message.join('\n');
- throw e;
- }
- tree = _this.processTree(tree);
- return tree.toCSS(syntax);
- };
- /**
- *
- * @param {Object} option
- * @returns {Object} Comb's object
- */
- that.use = function use(option) {
- var name;
- if (typeof option !== 'object') {
- throw new Error('Can\'t use option because it is not an object');
- }
- name = option.name;
- if (typeof name !== 'string' || !name) {
- throw new Error('Can\'t use option because it has invalid name: ' +
- name);
- }
- if (typeof option.accepts !== 'object' &&
- typeof option.setValue !== 'function') {
- throw new Error('Can\'t use option "' + name + '"');
- }
- if (typeof option.process !== 'function') {
- throw new Error('Can\'t use option "' + name + '"');
- }
- _this.updateOptionOrder(option);
- return that;
- };
- _this.usePredefinedOptions();
- };
- module.exports = Comb;
|