123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- var Comb = require('csscomb-core');
- var gonzales = require('gonzales-pe');
- var fs = require('fs');
- var path = require('path');
- /**
- * Converts CSS string to AST.
- *
- * @param {String} text CSS string
- * @param {String} [syntax] Syntax name (e.g., `scss`)
- * @param {String} [filename]
- * @returns {Array} AST
- */
- function cssToAST(text, syntax, filename) {
- var string = JSON.stringify;
- var fileInfo = filename ? ' at ' + filename : '';
- var tree;
- try {
- tree = gonzales.parse(text, { syntax: syntax });
- } catch (e) {
- throw new Error('Parsing error' + fileInfo + ': ' + e.message);
- }
- // TODO: When can tree be undefined? <tg>
- if (typeof tree === 'undefined') {
- throw new Error('Undefined tree' + fileInfo + ': ' + string(text) + ' => ' + string(tree));
- }
- return tree;
- }
- /**
- * Gets option's data needed for detection
- *
- * @param {String} optionName
- * @returns {Object} Object with option's name, link to `detect()` method
- * and default value for the case when nothing can be detected
- */
- function getHandler(optionName) {
- var option = require('./options/' + optionName);
- if (!option.detect) throw new Error('Option does not have `detect()` method.');
- return {
- name: option.name,
- detect: option.detect,
- detectDefault: option.detectDefault
- };
- }
- /**
- * Processes tree node and detects options.
- *
- * @param {Array} node Tree node
- * @param {Number} level Indent level
- * @param {Object} handler Object with option's data
- * @param {Object} detectedOptions
- */
- function detectInNode(node, level, handler, detectedOptions) {
- node.map(function(tree) {
- var detected = handler.detect(tree);
- var variants = detectedOptions[handler.name];
- if (typeof detected === 'object') {
- variants.push.apply(variants, detected);
- } else if (typeof detected !== 'undefined') {
- variants.push(detected);
- }
- //if (nodeType === 'atrulers' || nodeType === 'block') level++;
- });
- }
- /**
- * Processes tree and detects options.
- *
- * @param {Array} tree
- * @param {Array} handlers List of options that we should look for
- * @returns {Object} Map with detected options and all variants of possible
- * values
- */
- function detectInTree(tree, handlers) {
- var detectedOptions = {};
- // 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) {
- detectedOptions[handler.name] = [];
- // TODO: Pass all parameters as one object? <tg>
- detectInNode(tree, 0, handler, detectedOptions);
- });
- return detectedOptions;
- }
- /**
- * Gets the detected options.
- *
- * @param {Object} detected
- * @param {Array} handlers
- * @returns {Object}
- */
- function getDetectedOptions(detected, handlers) {
- var options = {};
- Object.keys(detected).forEach(function(option) {
- // List of all the detected variants from the stylesheet for the given option:
- var values = detected[option];
- var i;
- if (!values.length) {
- // If there are no values for the option, check if there is a default one:
- for (i = handlers.length; i--;) {
- if (handlers[i].name === option &&
- handlers[i].detectDefault !== undefined) {
- options[option] = handlers[i].detectDefault;
- break;
- }
- }
- } else if (values.length === 1) {
- options[option] = values[0];
- } else {
- // If there are more than one value for the option, find the most popular one;
- // `variants` would be populated with the popularity for different values.
- var variants = {};
- var bestGuess = null;
- var maximum = 0;
- for (i = values.length; i--;) {
- var currentValue = values[i];
- // Count the current value:
- if (variants[currentValue]) {
- variants[currentValue]++;
- } else {
- variants[currentValue] = 1;
- }
- // If the current variant is the most popular one, treat
- // it as the best guess:
- if (variants[currentValue] >= maximum) {
- maximum = variants[currentValue];
- bestGuess = currentValue;
- }
- }
- if (bestGuess !== null) {
- options[option] = bestGuess;
- }
- }
- });
- return options;
- }
- /**
- * Starts Code Style processing process.
- *
- * @param {String|Object} config
- * @constructor
- * @name CSScomb
- */
- var CSScomb = function(config) {
- var options = fs.readdirSync(__dirname + '/options').map(function(option) {
- return require('./options/' + option);
- });
- var comb = new Comb(options, 'css', 'less', 'scss', 'sass');
- // If config was passed, configure:
- if (typeof config === 'string') {
- config = CSScomb.getConfig(config);
- }
- if (typeof config === 'object') {
- comb.configure(config);
- }
- return comb;
- };
- /**
- * STATIC METHODS
- * Methods that can be called without creating an instance:
- * - getConfig;
- * - getCustomConfig;
- * - getCustomConfigPath;
- * - detectInFile;
- * - detectInString.
- * For example: `CSScomb.getConfig('zen')`
- */
- /**
- * Gets one of configuration files from configs' directory.
- *
- * @param {String} name Config's name, e.g. 'yandex'
- * @returns {Object} Configuration object
- */
- CSScomb.getConfig = function getConfig(name) {
- var DEFAULT_CONFIG_NAME = 'csscomb';
- name = name || DEFAULT_CONFIG_NAME;
- if (typeof name !== 'string') {
- throw new Error('Config name must be a string.');
- }
- var CONFIG_DIR_PATH = '../config';
- var availableConfigsNames = fs.readdirSync(__dirname + '/' + CONFIG_DIR_PATH)
- .map(function(configFileName) {
- return configFileName.split('.')[0]; // strip file extension(s)
- });
- if (availableConfigsNames.indexOf(name) < 0) {
- var configsNamesAsString = availableConfigsNames
- .map(function(configName) {
- return '\'' + configName + '\'';
- })
- .join(', ');
- throw new Error('"' + name + '" is not a valid config name. Try one of ' +
- 'the following: ' + configsNamesAsString + '.');
- }
- return require(CONFIG_DIR_PATH + '/' + name + '.json');
- };
- /**
- * Gets configuration from provided config path or looks for it in common
- * places.
- *
- * @param {String} [configPath]
- * @returns {Object|null}
- */
- CSScomb.getCustomConfig = function getCustomConfig(configPath) {
- var config;
- configPath = configPath || CSScomb.getCustomConfigPath();
- try {
- config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
- } catch (e) {
- config = null;
- }
- return config;
- };
- /**
- * Looks for a config file: recursively from current (process) directory
- * up to $HOME dir
- * If no custom config file is found, return `null`.
- *
- * @param {String} [configPath]
- * @returns {String | null}
- */
- CSScomb.getCustomConfigPath = function getCustomConfigPath(configPath) {
- var HOME = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
- configPath = configPath || path.join(process.cwd(), '.csscomb.json');
- // If we've finally found a config, return its path:
- if (fs.existsSync(configPath)) return fs.realpathSync(configPath);
- // If we are in HOME dir already and yet no config file, return a default
- // one from our package.
- // If project is located not under HOME, compare to root instead.
- // Since there appears to be no good way to get root path in
- // Windows, assume that if current dir has no parent dir, we're in
- // root.
- var dirname = path.dirname(configPath);
- var parentDirname = path.dirname(dirname);
- if (dirname === HOME || dirname === parentDirname) return null;
- // If there is no config in this directory, go one level up and look for
- // a config there:
- configPath = path.join(parentDirname, '.csscomb.json');
- return CSScomb.getCustomConfigPath(configPath);
- };
- /**
- * Detects the options in the given file
- *
- * @param {String} path Path to the stylesheet
- * @param {Array} options List of options to detect
- * @returns {Object} Detected options
- */
- CSScomb.detectInFile = function detectInFile(path, options) {
- var stylesheet = fs.readFileSync(path, 'utf8');
- return CSScomb.detectInString(stylesheet, options);
- };
- /**
- * Detects the options in the given string
- *
- * @param {String} text Stylesheet
- * @param {Array} options List of options to detect
- * @returns {Object} Detected options
- */
- CSScomb.detectInString = function detectInString(text, options) {
- var result;
- var handlers = [];
- if (!text) return text;
- var optionNames = fs.readdirSync(__dirname + '/options');
- optionNames.forEach(function(option) {
- option = option.slice(0, -3);
- if (options && options.indexOf(option) < 0) return;
- try {
- handlers.push(getHandler(option));
- } catch (e) {
- console.warn('\nFailed to load "%s" option:\n%s', option, e.message);
- }
- });
- var tree = cssToAST(text);
- var detectedOptions = detectInTree(tree, handlers);
- result = getDetectedOptions(detectedOptions, handlers);
- // Handle conflicting options with spaces around braces:
- var blockIndent = result['block-indent'];
- var spaceAfterOpeningBrace = result['space-after-opening-brace'];
- if (typeof blockIndent === 'string' &&
- spaceAfterOpeningBrace &&
- spaceAfterOpeningBrace.indexOf('\n') > -1) {
- result['space-after-opening-brace'] = '\n';
- }
- return result;
- };
- module.exports = CSScomb;
|