clean.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /**
  2. * Clean-css - https://github.com/jakubpawlowicz/clean-css
  3. * Released under the terms of MIT license
  4. *
  5. * Copyright (C) 2015 JakubPawlowicz.com
  6. */
  7. var ImportInliner = require('./imports/inliner');
  8. var rebaseUrls = require('./urls/rebase');
  9. var tokenize = require('./tokenizer/tokenize');
  10. var simpleOptimize = require('./selectors/simple');
  11. var advancedOptimize = require('./selectors/advanced');
  12. var simpleStringify = require('./stringifier/simple');
  13. var sourceMapStringify = require('./stringifier/source-maps');
  14. var CommentsProcessor = require('./text/comments-processor');
  15. var ExpressionsProcessor = require('./text/expressions-processor');
  16. var FreeTextProcessor = require('./text/free-text-processor');
  17. var UrlsProcessor = require('./text/urls-processor');
  18. var Compatibility = require('./utils/compatibility');
  19. var InputSourceMapTracker = require('./utils/input-source-map-tracker');
  20. var SourceTracker = require('./utils/source-tracker');
  21. var SourceReader = require('./utils/source-reader');
  22. var Validator = require('./properties/validator');
  23. var fs = require('fs');
  24. var path = require('path');
  25. var url = require('url');
  26. var override = require('./utils/object').override;
  27. var DEFAULT_TIMEOUT = 5000;
  28. var CleanCSS = module.exports = function CleanCSS(options) {
  29. options = options || {};
  30. this.options = {
  31. advanced: undefined === options.advanced ? true : !!options.advanced,
  32. aggressiveMerging: undefined === options.aggressiveMerging ? true : !!options.aggressiveMerging,
  33. benchmark: options.benchmark,
  34. compatibility: new Compatibility(options.compatibility).toOptions(),
  35. debug: options.debug,
  36. explicitRoot: !!options.root,
  37. explicitTarget: !!options.target,
  38. inliner: options.inliner || {},
  39. keepBreaks: options.keepBreaks || false,
  40. keepSpecialComments: 'keepSpecialComments' in options ? options.keepSpecialComments : '*',
  41. mediaMerging: undefined === options.mediaMerging ? true : !!options.mediaMerging,
  42. processImport: undefined === options.processImport ? true : !!options.processImport,
  43. processImportFrom: importOptionsFrom(options.processImportFrom),
  44. rebase: undefined === options.rebase ? true : !!options.rebase,
  45. relativeTo: options.relativeTo,
  46. restructuring: undefined === options.restructuring ? true : !!options.restructuring,
  47. root: options.root || process.cwd(),
  48. roundingPrecision: options.roundingPrecision,
  49. semanticMerging: undefined === options.semanticMerging ? false : !!options.semanticMerging,
  50. shorthandCompacting: undefined === options.shorthandCompacting ? true : !!options.shorthandCompacting,
  51. sourceMap: options.sourceMap,
  52. sourceMapInlineSources: !!options.sourceMapInlineSources,
  53. target: !options.target || missingDirectory(options.target) || presentDirectory(options.target) ? options.target : path.dirname(options.target)
  54. };
  55. this.options.inliner.timeout = this.options.inliner.timeout || DEFAULT_TIMEOUT;
  56. this.options.inliner.request = override(
  57. /* jshint camelcase: false */
  58. proxyOptionsFrom(process.env.HTTP_PROXY || process.env.http_proxy),
  59. this.options.inliner.request || {}
  60. );
  61. };
  62. function importOptionsFrom(rules) {
  63. return undefined === rules ? ['all'] : rules;
  64. }
  65. function missingDirectory(filepath) {
  66. return !fs.existsSync(filepath) && !/\.css$/.test(filepath);
  67. }
  68. function presentDirectory(filepath) {
  69. return fs.existsSync(filepath) && fs.statSync(filepath).isDirectory();
  70. }
  71. function proxyOptionsFrom(httpProxy) {
  72. return httpProxy ?
  73. {
  74. hostname: url.parse(httpProxy).hostname,
  75. port: parseInt(url.parse(httpProxy).port)
  76. } :
  77. {};
  78. }
  79. CleanCSS.prototype.minify = function (data, callback) {
  80. var context = {
  81. stats: {},
  82. errors: [],
  83. warnings: [],
  84. options: this.options,
  85. debug: this.options.debug,
  86. localOnly: !callback,
  87. sourceTracker: new SourceTracker(),
  88. validator: new Validator(this.options.compatibility)
  89. };
  90. if (context.options.sourceMap)
  91. context.inputSourceMapTracker = new InputSourceMapTracker(context);
  92. context.sourceReader = new SourceReader(context, data);
  93. data = context.sourceReader.toString();
  94. if (context.options.processImport || data.indexOf('@shallow') > 0) {
  95. // inline all imports
  96. var runner = callback ?
  97. process.nextTick :
  98. function (callback) { return callback(); };
  99. return runner(function () {
  100. return new ImportInliner(context).process(data, {
  101. localOnly: context.localOnly,
  102. imports: context.options.processImportFrom,
  103. whenDone: runMinifier(callback, context)
  104. });
  105. });
  106. } else {
  107. return runMinifier(callback, context)(data);
  108. }
  109. };
  110. function runMinifier(callback, context) {
  111. function whenSourceMapReady (data) {
  112. data = context.options.debug ?
  113. minifyWithDebug(context, data) :
  114. minify(context, data);
  115. data = withMetadata(context, data);
  116. return callback ?
  117. callback.call(null, context.errors.length > 0 ? context.errors : null, data) :
  118. data;
  119. }
  120. return function (data) {
  121. if (context.options.sourceMap) {
  122. return context.inputSourceMapTracker.track(data, function () {
  123. if (context.options.sourceMapInlineSources) {
  124. return context.inputSourceMapTracker.resolveSources(function () {
  125. return whenSourceMapReady(data);
  126. });
  127. } else {
  128. return whenSourceMapReady(data);
  129. }
  130. });
  131. } else {
  132. return whenSourceMapReady(data);
  133. }
  134. };
  135. }
  136. function withMetadata(context, data) {
  137. data.stats = context.stats;
  138. data.errors = context.errors;
  139. data.warnings = context.warnings;
  140. return data;
  141. }
  142. function minifyWithDebug(context, data) {
  143. var startedAt = process.hrtime();
  144. context.stats.originalSize = context.sourceTracker.removeAll(data).length;
  145. data = minify(context, data);
  146. var elapsed = process.hrtime(startedAt);
  147. context.stats.timeSpent = ~~(elapsed[0] * 1e3 + elapsed[1] / 1e6);
  148. context.stats.efficiency = 1 - data.styles.length / context.stats.originalSize;
  149. context.stats.minifiedSize = data.styles.length;
  150. return data;
  151. }
  152. function benchmark(runner) {
  153. return function (processor, action) {
  154. var name = processor.constructor.name + '#' + action;
  155. var start = process.hrtime();
  156. runner(processor, action);
  157. var itTook = process.hrtime(start);
  158. console.log('%d ms: ' + name, 1000 * itTook[0] + itTook[1] / 1000000);
  159. };
  160. }
  161. function minify(context, data) {
  162. var options = context.options;
  163. var commentsProcessor = new CommentsProcessor(context, options.keepSpecialComments, options.keepBreaks, options.sourceMap);
  164. var expressionsProcessor = new ExpressionsProcessor(options.sourceMap);
  165. var freeTextProcessor = new FreeTextProcessor(options.sourceMap);
  166. var urlsProcessor = new UrlsProcessor(context, options.sourceMap, options.compatibility.properties.urlQuotes);
  167. var stringify = options.sourceMap ? sourceMapStringify : simpleStringify;
  168. var run = function (processor, action) {
  169. data = typeof processor == 'function' ?
  170. processor(data) :
  171. processor[action](data);
  172. };
  173. if (options.benchmark)
  174. run = benchmark(run);
  175. run(commentsProcessor, 'escape');
  176. run(expressionsProcessor, 'escape');
  177. run(urlsProcessor, 'escape');
  178. run(freeTextProcessor, 'escape');
  179. function restoreEscapes(data, prefixContent) {
  180. data = freeTextProcessor.restore(data, prefixContent);
  181. data = urlsProcessor.restore(data);
  182. data = options.rebase ? rebaseUrls(data, context) : data;
  183. data = expressionsProcessor.restore(data);
  184. return commentsProcessor.restore(data);
  185. }
  186. var tokens = tokenize(data, context);
  187. simpleOptimize(tokens, options, context);
  188. if (options.advanced)
  189. advancedOptimize(tokens, options, context, true);
  190. return stringify(tokens, options, restoreEscapes, context.inputSourceMapTracker);
  191. }