csscomb.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. var Comb = require('csscomb-core');
  2. var gonzales = require('gonzales-pe');
  3. var fs = require('fs');
  4. var path = require('path');
  5. /**
  6. * Converts CSS string to AST.
  7. *
  8. * @param {String} text CSS string
  9. * @param {String} [syntax] Syntax name (e.g., `scss`)
  10. * @param {String} [filename]
  11. * @returns {Array} AST
  12. */
  13. function cssToAST(text, syntax, filename) {
  14. var string = JSON.stringify;
  15. var fileInfo = filename ? ' at ' + filename : '';
  16. var tree;
  17. try {
  18. tree = gonzales.parse(text, { syntax: syntax });
  19. } catch (e) {
  20. throw new Error('Parsing error' + fileInfo + ': ' + e.message);
  21. }
  22. // TODO: When can tree be undefined? <tg>
  23. if (typeof tree === 'undefined') {
  24. throw new Error('Undefined tree' + fileInfo + ': ' + string(text) + ' => ' + string(tree));
  25. }
  26. return tree;
  27. }
  28. /**
  29. * Gets option's data needed for detection
  30. *
  31. * @param {String} optionName
  32. * @returns {Object} Object with option's name, link to `detect()` method
  33. * and default value for the case when nothing can be detected
  34. */
  35. function getHandler(optionName) {
  36. var option = require('./options/' + optionName);
  37. if (!option.detect) throw new Error('Option does not have `detect()` method.');
  38. return {
  39. name: option.name,
  40. detect: option.detect,
  41. detectDefault: option.detectDefault
  42. };
  43. }
  44. /**
  45. * Processes tree node and detects options.
  46. *
  47. * @param {Array} node Tree node
  48. * @param {Number} level Indent level
  49. * @param {Object} handler Object with option's data
  50. * @param {Object} detectedOptions
  51. */
  52. function detectInNode(node, level, handler, detectedOptions) {
  53. node.map(function(tree) {
  54. var detected = handler.detect(tree);
  55. var variants = detectedOptions[handler.name];
  56. if (typeof detected === 'object') {
  57. variants.push.apply(variants, detected);
  58. } else if (typeof detected !== 'undefined') {
  59. variants.push(detected);
  60. }
  61. //if (nodeType === 'atrulers' || nodeType === 'block') level++;
  62. });
  63. }
  64. /**
  65. * Processes tree and detects options.
  66. *
  67. * @param {Array} tree
  68. * @param {Array} handlers List of options that we should look for
  69. * @returns {Object} Map with detected options and all variants of possible
  70. * values
  71. */
  72. function detectInTree(tree, handlers) {
  73. var detectedOptions = {};
  74. // We walk across complete tree for each handler,
  75. // because we need strictly maintain order in which handlers work,
  76. // despite fact that handlers work on different level of the tree.
  77. handlers.forEach(function(handler) {
  78. detectedOptions[handler.name] = [];
  79. // TODO: Pass all parameters as one object? <tg>
  80. detectInNode(tree, 0, handler, detectedOptions);
  81. });
  82. return detectedOptions;
  83. }
  84. /**
  85. * Gets the detected options.
  86. *
  87. * @param {Object} detected
  88. * @param {Array} handlers
  89. * @returns {Object}
  90. */
  91. function getDetectedOptions(detected, handlers) {
  92. var options = {};
  93. Object.keys(detected).forEach(function(option) {
  94. // List of all the detected variants from the stylesheet for the given option:
  95. var values = detected[option];
  96. var i;
  97. if (!values.length) {
  98. // If there are no values for the option, check if there is a default one:
  99. for (i = handlers.length; i--;) {
  100. if (handlers[i].name === option &&
  101. handlers[i].detectDefault !== undefined) {
  102. options[option] = handlers[i].detectDefault;
  103. break;
  104. }
  105. }
  106. } else if (values.length === 1) {
  107. options[option] = values[0];
  108. } else {
  109. // If there are more than one value for the option, find the most popular one;
  110. // `variants` would be populated with the popularity for different values.
  111. var variants = {};
  112. var bestGuess = null;
  113. var maximum = 0;
  114. for (i = values.length; i--;) {
  115. var currentValue = values[i];
  116. // Count the current value:
  117. if (variants[currentValue]) {
  118. variants[currentValue]++;
  119. } else {
  120. variants[currentValue] = 1;
  121. }
  122. // If the current variant is the most popular one, treat
  123. // it as the best guess:
  124. if (variants[currentValue] >= maximum) {
  125. maximum = variants[currentValue];
  126. bestGuess = currentValue;
  127. }
  128. }
  129. if (bestGuess !== null) {
  130. options[option] = bestGuess;
  131. }
  132. }
  133. });
  134. return options;
  135. }
  136. /**
  137. * Starts Code Style processing process.
  138. *
  139. * @param {String|Object} config
  140. * @constructor
  141. * @name CSScomb
  142. */
  143. var CSScomb = function(config) {
  144. var options = fs.readdirSync(__dirname + '/options').map(function(option) {
  145. return require('./options/' + option);
  146. });
  147. var comb = new Comb(options, 'css', 'less', 'scss', 'sass');
  148. // If config was passed, configure:
  149. if (typeof config === 'string') {
  150. config = CSScomb.getConfig(config);
  151. }
  152. if (typeof config === 'object') {
  153. comb.configure(config);
  154. }
  155. return comb;
  156. };
  157. /**
  158. * STATIC METHODS
  159. * Methods that can be called without creating an instance:
  160. * - getConfig;
  161. * - getCustomConfig;
  162. * - getCustomConfigPath;
  163. * - detectInFile;
  164. * - detectInString.
  165. * For example: `CSScomb.getConfig('zen')`
  166. */
  167. /**
  168. * Gets one of configuration files from configs' directory.
  169. *
  170. * @param {String} name Config's name, e.g. 'yandex'
  171. * @returns {Object} Configuration object
  172. */
  173. CSScomb.getConfig = function getConfig(name) {
  174. var DEFAULT_CONFIG_NAME = 'csscomb';
  175. name = name || DEFAULT_CONFIG_NAME;
  176. if (typeof name !== 'string') {
  177. throw new Error('Config name must be a string.');
  178. }
  179. var CONFIG_DIR_PATH = '../config';
  180. var availableConfigsNames = fs.readdirSync(__dirname + '/' + CONFIG_DIR_PATH)
  181. .map(function(configFileName) {
  182. return configFileName.split('.')[0]; // strip file extension(s)
  183. });
  184. if (availableConfigsNames.indexOf(name) < 0) {
  185. var configsNamesAsString = availableConfigsNames
  186. .map(function(configName) {
  187. return '\'' + configName + '\'';
  188. })
  189. .join(', ');
  190. throw new Error('"' + name + '" is not a valid config name. Try one of ' +
  191. 'the following: ' + configsNamesAsString + '.');
  192. }
  193. return require(CONFIG_DIR_PATH + '/' + name + '.json');
  194. };
  195. /**
  196. * Gets configuration from provided config path or looks for it in common
  197. * places.
  198. *
  199. * @param {String} [configPath]
  200. * @returns {Object|null}
  201. */
  202. CSScomb.getCustomConfig = function getCustomConfig(configPath) {
  203. var config;
  204. configPath = configPath || CSScomb.getCustomConfigPath();
  205. try {
  206. config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
  207. } catch (e) {
  208. config = null;
  209. }
  210. return config;
  211. };
  212. /**
  213. * Looks for a config file: recursively from current (process) directory
  214. * up to $HOME dir
  215. * If no custom config file is found, return `null`.
  216. *
  217. * @param {String} [configPath]
  218. * @returns {String | null}
  219. */
  220. CSScomb.getCustomConfigPath = function getCustomConfigPath(configPath) {
  221. var HOME = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
  222. configPath = configPath || path.join(process.cwd(), '.csscomb.json');
  223. // If we've finally found a config, return its path:
  224. if (fs.existsSync(configPath)) return fs.realpathSync(configPath);
  225. // If we are in HOME dir already and yet no config file, return a default
  226. // one from our package.
  227. // If project is located not under HOME, compare to root instead.
  228. // Since there appears to be no good way to get root path in
  229. // Windows, assume that if current dir has no parent dir, we're in
  230. // root.
  231. var dirname = path.dirname(configPath);
  232. var parentDirname = path.dirname(dirname);
  233. if (dirname === HOME || dirname === parentDirname) return null;
  234. // If there is no config in this directory, go one level up and look for
  235. // a config there:
  236. configPath = path.join(parentDirname, '.csscomb.json');
  237. return CSScomb.getCustomConfigPath(configPath);
  238. };
  239. /**
  240. * Detects the options in the given file
  241. *
  242. * @param {String} path Path to the stylesheet
  243. * @param {Array} options List of options to detect
  244. * @returns {Object} Detected options
  245. */
  246. CSScomb.detectInFile = function detectInFile(path, options) {
  247. var stylesheet = fs.readFileSync(path, 'utf8');
  248. return CSScomb.detectInString(stylesheet, options);
  249. };
  250. /**
  251. * Detects the options in the given string
  252. *
  253. * @param {String} text Stylesheet
  254. * @param {Array} options List of options to detect
  255. * @returns {Object} Detected options
  256. */
  257. CSScomb.detectInString = function detectInString(text, options) {
  258. var result;
  259. var handlers = [];
  260. if (!text) return text;
  261. var optionNames = fs.readdirSync(__dirname + '/options');
  262. optionNames.forEach(function(option) {
  263. option = option.slice(0, -3);
  264. if (options && options.indexOf(option) < 0) return;
  265. try {
  266. handlers.push(getHandler(option));
  267. } catch (e) {
  268. console.warn('\nFailed to load "%s" option:\n%s', option, e.message);
  269. }
  270. });
  271. var tree = cssToAST(text);
  272. var detectedOptions = detectInTree(tree, handlers);
  273. result = getDetectedOptions(detectedOptions, handlers);
  274. // Handle conflicting options with spaces around braces:
  275. var blockIndent = result['block-indent'];
  276. var spaceAfterOpeningBrace = result['space-after-opening-brace'];
  277. if (typeof blockIndent === 'string' &&
  278. spaceAfterOpeningBrace &&
  279. spaceAfterOpeningBrace.indexOf('\n') > -1) {
  280. result['space-after-opening-brace'] = '\n';
  281. }
  282. return result;
  283. };
  284. module.exports = CSScomb;