clean.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. /**
  2. * Clean-css - https://github.com/GoalSmashers/clean-css
  3. * Released under the terms of MIT license
  4. *
  5. * Copyright (C) 2011-2014 GoalSmashers.com
  6. */
  7. var ColorShortener = require('./colors/shortener');
  8. var ColorHSLToHex = require('./colors/hsl-to-hex');
  9. var ColorRGBToHex = require('./colors/rgb-to-hex');
  10. var ColorLongToShortHex = require('./colors/long-to-short-hex');
  11. var ImportInliner = require('./imports/inliner');
  12. var UrlRebase = require('./images/url-rebase');
  13. var EmptyRemoval = require('./selectors/empty-removal');
  14. var CommentsProcessor = require('./text/comments');
  15. var ExpressionsProcessor = require('./text/expressions');
  16. var FreeTextProcessor = require('./text/free');
  17. var UrlsProcessor = require('./text/urls');
  18. var NameQuotesProcessor = require('./text/name-quotes');
  19. var Splitter = require('./text/splitter');
  20. var SelectorsOptimizer = require('./selectors/optimizer');
  21. var CleanCSS = module.exports = function CleanCSS(options) {
  22. options = options || {};
  23. // back compat
  24. if (!(this instanceof CleanCSS))
  25. return new CleanCSS(options);
  26. options.keepBreaks = options.keepBreaks || false;
  27. //active by default
  28. if (undefined === options.processImport)
  29. options.processImport = true;
  30. this.options = options;
  31. this.stats = {};
  32. this.context = {
  33. errors: [],
  34. warnings: [],
  35. debug: options.debug
  36. };
  37. this.errors = this.context.errors;
  38. this.warnings = this.context.warnings;
  39. this.lineBreak = process.platform == 'win32' ? '\r\n' : '\n';
  40. };
  41. CleanCSS.prototype.minify = function(data, callback) {
  42. var options = this.options;
  43. if (Buffer.isBuffer(data))
  44. data = data.toString();
  45. if (options.processImport || data.indexOf('@shallow') > 0) {
  46. // inline all imports
  47. var self = this;
  48. var runner = callback ?
  49. process.nextTick :
  50. function(callback) { return callback(); };
  51. return runner(function() {
  52. return new ImportInliner(self.context, options.inliner).process(data, {
  53. localOnly: !callback,
  54. root: options.root || process.cwd(),
  55. relativeTo: options.relativeTo,
  56. whenDone: function(data) {
  57. return minify.call(self, data, callback);
  58. }
  59. });
  60. });
  61. } else {
  62. return minify.call(this, data, callback);
  63. }
  64. };
  65. var minify = function(data, callback) {
  66. var startedAt;
  67. var stats = this.stats;
  68. var options = this.options;
  69. var context = this.context;
  70. var lineBreak = this.lineBreak;
  71. var commentsProcessor = new CommentsProcessor(
  72. context,
  73. 'keepSpecialComments' in options ? options.keepSpecialComments : '*',
  74. options.keepBreaks,
  75. lineBreak
  76. );
  77. var expressionsProcessor = new ExpressionsProcessor();
  78. var freeTextProcessor = new FreeTextProcessor();
  79. var urlsProcessor = new UrlsProcessor(context);
  80. var nameQuotesProcessor = new NameQuotesProcessor();
  81. if (options.debug) {
  82. this.startedAt = process.hrtime();
  83. this.stats.originalSize = data.length;
  84. }
  85. var replace = function() {
  86. if (typeof arguments[0] == 'function')
  87. arguments[0]();
  88. else
  89. data = data.replace.apply(data, arguments);
  90. };
  91. // replace function
  92. if (options.benchmark) {
  93. var originalReplace = replace;
  94. replace = function(pattern, replacement) {
  95. var name = typeof pattern == 'function' ?
  96. /function (\w+)\(/.exec(pattern.toString())[1] :
  97. pattern;
  98. var start = process.hrtime();
  99. originalReplace(pattern, replacement);
  100. var itTook = process.hrtime(start);
  101. console.log('%d ms: ' + name, 1000 * itTook[0] + itTook[1] / 1000000);
  102. };
  103. }
  104. if (options.debug) {
  105. startedAt = process.hrtime();
  106. stats.originalSize = data.length;
  107. }
  108. replace(function escapeComments() {
  109. data = commentsProcessor.escape(data);
  110. });
  111. // replace all escaped line breaks
  112. replace(/\\(\r\n|\n)/gm, '');
  113. // strip parentheses in urls if possible (no spaces inside)
  114. replace(/url\((['"])([^\)]+)['"]\)/g, function(match, quote, url) {
  115. var unsafeDataURI = url.indexOf('data:') === 0 && url.match(/data:\w+\/[^;]+;base64,/) === null;
  116. if (url.match(/[ \t]/g) !== null || unsafeDataURI)
  117. return 'url(' + quote + url + quote + ')';
  118. else
  119. return 'url(' + url + ')';
  120. });
  121. // strip parentheses in animation & font names
  122. replace(function removeQuotes() {
  123. data = nameQuotesProcessor.process(data);
  124. });
  125. // strip parentheses in @keyframes
  126. replace(/@(\-moz\-|\-o\-|\-webkit\-)?keyframes ([^{]+)/g, function(match, prefix, name) {
  127. prefix = prefix || '';
  128. return '@' + prefix + 'keyframes ' + (name.indexOf(' ') > -1 ? name : name.replace(/['"]/g, ''));
  129. });
  130. // IE shorter filters, but only if single (IE 7 issue)
  131. replace(/progid:DXImageTransform\.Microsoft\.(Alpha|Chroma)(\([^\)]+\))([;}'"])/g, function(match, filter, args, suffix) {
  132. return filter.toLowerCase() + args + suffix;
  133. });
  134. replace(function escapeExpressions() {
  135. data = expressionsProcessor.escape(data);
  136. });
  137. // strip parentheses in attribute values
  138. replace(/\[([^\]]+)\]/g, function(match, content) {
  139. var eqIndex = content.indexOf('=');
  140. var singleQuoteIndex = content.indexOf('\'');
  141. var doubleQuoteIndex = content.indexOf('"');
  142. if (eqIndex < 0 && singleQuoteIndex < 0 && doubleQuoteIndex < 0)
  143. return match;
  144. if (singleQuoteIndex === 0 || doubleQuoteIndex === 0)
  145. return match;
  146. var key = content.substring(0, eqIndex);
  147. var value = content.substring(eqIndex + 1, content.length);
  148. if (/^['"](?:[a-zA-Z][a-zA-Z\d\-_]+)['"]$/.test(value))
  149. return '[' + key + '=' + value.substring(1, value.length - 1) + ']';
  150. else
  151. return match;
  152. });
  153. replace(function escapeFreeText() {
  154. data = freeTextProcessor.escape(data);
  155. });
  156. replace(function escapeUrls() {
  157. data = urlsProcessor.escape(data);
  158. });
  159. // remove invalid special declarations
  160. replace(/@charset [^;]+;/ig, function (match) {
  161. return match.indexOf('@charset') > -1 ? match : '';
  162. });
  163. // whitespace inside attribute selectors brackets
  164. replace(/\[([^\]]+)\]/g, function(match) {
  165. return match.replace(/\s/g, '');
  166. });
  167. // line breaks
  168. replace(/[\r]?\n/g, ' ');
  169. // multiple whitespace
  170. replace(/[\t ]+/g, ' ');
  171. // multiple semicolons (with optional whitespace)
  172. replace(/;[ ]?;+/g, ';');
  173. // multiple line breaks to one
  174. replace(/ (?:\r\n|\n)/g, lineBreak);
  175. replace(/(?:\r\n|\n)+/g, lineBreak);
  176. // remove spaces around selectors
  177. replace(/ ([+~>]) /g, '$1');
  178. // remove extra spaces inside content
  179. replace(/([!\(\{\}:;=,\n]) /g, '$1');
  180. replace(/ ([!\)\{\};=,\n])/g, '$1');
  181. replace(/(?:\r\n|\n)\}/g, '}');
  182. replace(/([\{;,])(?:\r\n|\n)/g, '$1');
  183. replace(/ :([^\{\};]+)([;}])/g, ':$1$2');
  184. // restore spaces inside IE filters (IE 7 issue)
  185. replace(/progid:[^(]+\(([^\)]+)/g, function(match) {
  186. return match.replace(/,/g, ', ');
  187. });
  188. // trailing semicolons
  189. replace(/;\}/g, '}');
  190. replace(function hsl2Hex() {
  191. data = new ColorHSLToHex(data).process();
  192. });
  193. replace(function rgb2Hex() {
  194. data = new ColorRGBToHex(data).process();
  195. });
  196. replace(function longToShortHex() {
  197. data = new ColorLongToShortHex(data).process();
  198. });
  199. replace(function shortenColors() {
  200. data = new ColorShortener(data).process();
  201. });
  202. // replace font weight with numerical value
  203. replace(/(font\-weight|font):(normal|bold)([ ;\}!])(\w*)/g, function(match, property, weight, suffix, next) {
  204. if (suffix == ' ' && (next.indexOf('/') > -1 || next == 'normal' || /[1-9]00/.test(next)))
  205. return match;
  206. if (weight == 'normal')
  207. return property + ':400' + suffix + next;
  208. else if (weight == 'bold')
  209. return property + ':700' + suffix + next;
  210. else
  211. return match;
  212. });
  213. // minus zero to zero
  214. // repeated twice on purpose as if not it doesn't process rgba(-0,-0,-0,-0) correctly
  215. var zerosRegexp = /(\s|:|,|\()\-0([^\.])/g;
  216. replace(zerosRegexp, '$10$2');
  217. replace(zerosRegexp, '$10$2');
  218. // zero(s) + value to value
  219. replace(/(\s|:|,)0+([1-9])/g, '$1$2');
  220. // round pixels to 2nd decimal place
  221. var precision = 'roundingPrecision' in options ? options.roundingPrecision : 2;
  222. var decimalMultiplier = Math.pow(10, precision);
  223. replace(new RegExp('(\\d*\\.\\d{' + (precision + 1) + ',})px', 'g'), function(match, number) {
  224. return Math.round(parseFloat(number) * decimalMultiplier) / decimalMultiplier + 'px';
  225. });
  226. // .0 to 0
  227. // repeated twice on purpose as if not it doesn't process {padding: .0 .0 .0 .0} correctly
  228. var leadingDecimalRegexp = /(\D)\.0+(\D)/g;
  229. replace(leadingDecimalRegexp, '$10$2');
  230. replace(leadingDecimalRegexp, '$10$2');
  231. // fraction zeros removal
  232. replace(/\.([1-9]*)0+(\D)/g, function(match, nonZeroPart, suffix) {
  233. return (nonZeroPart.length > 0 ? '.' : '') + nonZeroPart + suffix;
  234. });
  235. // zero + unit to zero
  236. var units = ['px', 'em', 'ex', 'cm', 'mm', 'in', 'pt', 'pc', '%'];
  237. if (['ie7', 'ie8'].indexOf(options.compatibility) == -1)
  238. units.push('rem');
  239. replace(new RegExp('(\\s|:|,)\\-?0(?:' + units.join('|') + ')', 'g'), '$1' + '0');
  240. replace(new RegExp('(\\s|:|,)\\-?(\\d+)\\.(\\D)', 'g'), '$1$2$3');
  241. replace(new RegExp('rect\\(0(?:' + units.join('|') + ')', 'g'), 'rect(0');
  242. // restore % in rgb/rgba and hsl/hsla
  243. replace(/(rgb|rgba|hsl|hsla)\(([^\)]+)\)/g, function(match, colorFunction, colorDef) {
  244. var tokens = colorDef.split(',');
  245. var applies = colorFunction == 'hsl' || colorFunction == 'hsla' || tokens[0].indexOf('%') > -1;
  246. if (!applies)
  247. return match;
  248. if (tokens[1].indexOf('%') == -1)
  249. tokens[1] += '%';
  250. if (tokens[2].indexOf('%') == -1)
  251. tokens[2] += '%';
  252. return colorFunction + '(' + tokens.join(',') + ')';
  253. });
  254. // transparent rgba/hsla to 'transparent' unless in compatibility mode
  255. if (!options.compatibility) {
  256. replace(/:([^;]*)(?:rgba|hsla)\(0,0%?,0%?,0\)/g, function (match, prefix) {
  257. if (new Splitter(',').split(match).pop().indexOf('gradient(') > -1)
  258. return match;
  259. return ':' + prefix + 'transparent';
  260. });
  261. }
  262. // none to 0
  263. replace(/outline:none/g, 'outline:0');
  264. // background:none to background:0 0
  265. replace(/background:(?:none|transparent)([;}])/g, 'background:0 0$1');
  266. // multiple zeros into one
  267. replace(/box-shadow:0 0 0 0([^\.])/g, 'box-shadow:0 0$1');
  268. replace(/:0 0 0 0([^\.])/g, ':0$1');
  269. replace(/([: ,=\-])0\.(\d)/g, '$1.$2');
  270. // restore rect(...) zeros syntax for 4 zeros
  271. replace(/rect\(\s?0(\s|,)0[ ,]0[ ,]0\s?\)/g, 'rect(0$10$10$10)');
  272. // remove universal selector when not needed (*#id, *.class etc)
  273. // pending a better fix
  274. if (options.compatibility != 'ie7') {
  275. replace(/([^,]?)(\*[^ \+\{]*\+html[^\{]*)(\{[^\}]*\})/g, function (match, prefix, selector, body) {
  276. var notHackedSelectors = new Splitter(',').split(selector).filter(function (m) {
  277. return !/^\*[^ \+\{]*\+html/.test(m);
  278. });
  279. return notHackedSelectors.length > 0 ?
  280. prefix + notHackedSelectors.join(',') + body :
  281. prefix;
  282. });
  283. replace(/\*([\.#:\[])/g, '$1');
  284. }
  285. // Restore spaces inside calc back
  286. replace(/calc\([^\}]+\}/g, function(match) {
  287. return match.replace(/\+/g, ' + ');
  288. });
  289. // get rid of IE hacks if not in compatibility mode
  290. if (!options.compatibility)
  291. replace(/([;\{])[\*_][\w\-]+:[^;\}]+/g, '$1');
  292. if (options.noAdvanced) {
  293. if (options.keepBreaks)
  294. replace(/\}/g, '}' + lineBreak);
  295. } else {
  296. replace(function optimizeSelectors() {
  297. data = new SelectorsOptimizer(data, context, {
  298. keepBreaks: options.keepBreaks,
  299. lineBreak: lineBreak,
  300. compatibility: options.compatibility,
  301. aggressiveMerging: !options.noAggressiveMerging
  302. }).process();
  303. });
  304. }
  305. // replace ' / ' in border-*-radius with '/'
  306. replace(/(border-\w+-\w+-radius:\S+)\s+\/\s+/g, '$1/');
  307. // replace same H/V values in border-radius
  308. replace(/(border-\w+-\w+-radius):([^;\}]+)/g, function (match, property, value) {
  309. var parts = value.split('/');
  310. if (parts.length > 1 && parts[0] == parts[1])
  311. return property + ':' + parts[0];
  312. else
  313. return match;
  314. });
  315. replace(function restoreUrls() {
  316. data = urlsProcessor.restore(data);
  317. });
  318. replace(function rebaseUrls() {
  319. data = options.noRebase ? data : new UrlRebase(options, context).process(data);
  320. });
  321. replace(function restoreFreeText() {
  322. data = freeTextProcessor.restore(data);
  323. });
  324. replace(function restoreComments() {
  325. data = commentsProcessor.restore(data);
  326. });
  327. replace(function restoreExpressions() {
  328. data = expressionsProcessor.restore(data);
  329. });
  330. // move first charset to the beginning
  331. replace(function moveCharset() {
  332. // get first charset in stylesheet
  333. var match = data.match(/@charset [^;]+;/);
  334. var firstCharset = match ? match[0] : null;
  335. if (!firstCharset)
  336. return;
  337. // reattach first charset and remove all subsequent
  338. data = firstCharset +
  339. (options.keepBreaks ? lineBreak : '') +
  340. data.replace(new RegExp('@charset [^;]+;(' + lineBreak + ')?', 'g'), '').trim();
  341. });
  342. if (options.noAdvanced) {
  343. replace(function removeEmptySelectors() {
  344. data = new EmptyRemoval(data).process();
  345. });
  346. }
  347. // trim spaces at beginning and end
  348. data = data.trim();
  349. if (options.debug) {
  350. var elapsed = process.hrtime(startedAt);
  351. stats.timeSpent = ~~(elapsed[0] * 1e3 + elapsed[1] / 1e6);
  352. stats.efficiency = 1 - data.length / stats.originalSize;
  353. stats.minifiedSize = data.length;
  354. }
  355. return callback ?
  356. callback.call(this, this.context.errors.length > 0 ? this.context.errors : null, data) :
  357. data;
  358. };