clean.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. /**
  2. * Clean-css - https://github.com/GoalSmashers/clean-css
  3. * Released under the terms of MIT license
  4. *
  5. * Copyright (C) 2011-2013 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 ShorthandNotations = require('./properties/shorthand-notations');
  12. var ImportInliner = require('./imports/inliner');
  13. var UrlRebase = require('./images/url-rebase');
  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 CleanCSS = {
  19. process: function(data, options) {
  20. var replace = function() {
  21. if (typeof arguments[0] == 'function')
  22. arguments[0]();
  23. else
  24. data = data.replace.apply(data, arguments);
  25. };
  26. var lineBreak = process.platform == 'win32' ? '\r\n' : '\n';
  27. this.lineBreak = lineBreak;
  28. options = options || {};
  29. options.keepBreaks = options.keepBreaks || false;
  30. //active by default
  31. if (options.processImport === undefined)
  32. options.processImport = true;
  33. // replace function
  34. if (options.benchmark) {
  35. var originalReplace = replace;
  36. replace = function(pattern, replacement) {
  37. var name = typeof pattern == 'function' ?
  38. /function (\w+)\(/.exec(pattern.toString())[1] :
  39. pattern;
  40. var start = process.hrtime();
  41. originalReplace(pattern, replacement);
  42. var itTook = process.hrtime(start);
  43. console.log('%d ms: ' + name, 1000 * itTook[0] + itTook[1] / 1000000.0);
  44. };
  45. }
  46. var commentsProcessor = new CommentsProcessor(
  47. 'keepSpecialComments' in options ? options.keepSpecialComments : '*',
  48. options.keepBreaks,
  49. lineBreak
  50. );
  51. var expressionsProcessor = new ExpressionsProcessor();
  52. var freeTextProcessor = new FreeTextProcessor();
  53. var urlsProcessor = new UrlsProcessor();
  54. var importInliner = new ImportInliner();
  55. if (options.processImport) {
  56. // inline all imports
  57. replace(function inlineImports() {
  58. data = importInliner.process(data, {
  59. root: options.root || process.cwd(),
  60. relativeTo: options.relativeTo
  61. });
  62. });
  63. }
  64. this.originalSize = data.length;
  65. replace(function escapeComments() {
  66. data = commentsProcessor.escape(data);
  67. });
  68. // replace all escaped line breaks
  69. replace(/\\(\r\n|\n)/mg, '');
  70. // strip parentheses in urls if possible (no spaces inside)
  71. replace(/url\((['"])([^\)]+)['"]\)/g, function(match, quote, url) {
  72. if (url.match(/[ \t]/g) !== null || url.indexOf('data:') === 0)
  73. return 'url(' + quote + url + quote + ')';
  74. else
  75. return 'url(' + url + ')';
  76. });
  77. // strip parentheses in animation & font names
  78. replace(/(animation|animation\-name|font|font\-family):([^;}]+)/g, function(match, propertyName, fontDef) {
  79. return propertyName + ':' + fontDef.replace(/['"]([\w\-]+)['"]/g, '$1');
  80. });
  81. // strip parentheses in @keyframes
  82. replace(/@(\-moz\-|\-o\-|\-webkit\-)?keyframes ([^{]+)/g, function(match, prefix, name) {
  83. prefix = prefix || '';
  84. return '@' + prefix + 'keyframes ' + (name.indexOf(' ') > -1 ? name : name.replace(/['"]/g, ''));
  85. });
  86. // IE shorter filters, but only if single (IE 7 issue)
  87. replace(/progid:DXImageTransform\.Microsoft\.(Alpha|Chroma)(\([^\)]+\))([;}'"])/g, function(match, filter, args, suffix) {
  88. return filter.toLowerCase() + args + suffix;
  89. });
  90. replace(function escapeExpressions() {
  91. data = expressionsProcessor.escape(data);
  92. });
  93. // strip parentheses in attribute values
  94. replace(/\[([^\]]+)\]/g, function(match, content) {
  95. var eqIndex = content.indexOf('=');
  96. var singleQuoteIndex = content.indexOf('\'');
  97. var doubleQuoteIndex = content.indexOf('"');
  98. if (eqIndex < 0 && singleQuoteIndex < 0 && doubleQuoteIndex < 0)
  99. return match;
  100. if (singleQuoteIndex === 0 || doubleQuoteIndex === 0)
  101. return match;
  102. var key = content.substring(0, eqIndex);
  103. var value = content.substring(eqIndex + 1, content.length);
  104. if (/^['"](?:[a-zA-Z][a-zA-Z\d\-_]+)['"]$/.test(value))
  105. return '[' + key + '=' + value.substring(1, value.length - 1) + ']';
  106. else
  107. return match;
  108. });
  109. replace(function escapeFreeText() {
  110. data = freeTextProcessor.escape(data);
  111. });
  112. replace(function escapeUrls() {
  113. data = urlsProcessor.escape(data);
  114. });
  115. // line breaks
  116. if (!options.keepBreaks)
  117. replace(/[\r]?\n/g, ' ');
  118. // multiple whitespace
  119. replace(/[\t ]+/g, ' ');
  120. // multiple semicolons (with optional whitespace)
  121. replace(/;[ ]?;+/g, ';');
  122. // multiple line breaks to one
  123. replace(/ (?:\r\n|\n)/g, lineBreak);
  124. replace(/(?:\r\n|\n)+/g, lineBreak);
  125. // remove spaces around selectors
  126. replace(/ ([+~>]) /g, '$1');
  127. // remove extra spaces inside content
  128. replace(/([!\(\{\}:;=,\n]) /g, '$1');
  129. replace(/ ([!\)\{\};=,\n])/g, '$1');
  130. replace(/(?:\r\n|\n)\}/g, '}');
  131. replace(/([\{;,])(?:\r\n|\n)/g, '$1');
  132. replace(/ :([^\{\};]+)([;}])/g, ':$1$2');
  133. // restore spaces inside IE filters (IE 7 issue)
  134. replace(/progid:[^(]+\(([^\)]+)/g, function(match) {
  135. return match.replace(/,/g, ', ');
  136. });
  137. // trailing semicolons
  138. replace(/;\}/g, '}');
  139. replace(function hsl2Hex() {
  140. data = new ColorHSLToHex(data).process();
  141. });
  142. replace(function rgb2Hex() {
  143. data = new ColorRGBToHex(data).process();
  144. });
  145. replace(function longToShortHex() {
  146. data = new ColorLongToShortHex(data).process();
  147. });
  148. replace(function shortenColors() {
  149. data = new ColorShortener(data).process();
  150. });
  151. // replace font weight with numerical value
  152. replace(/(font\-weight|font):(normal|bold)([ ;\}!])(\w*)/g, function(match, property, weight, suffix, next) {
  153. if (suffix == ' ' && next.length > 0 && !/[.\d]/.test(next))
  154. return match;
  155. if (weight == 'normal')
  156. return property + ':400' + suffix + next;
  157. else if (weight == 'bold')
  158. return property + ':700' + suffix + next;
  159. else
  160. return match;
  161. });
  162. // zero + unit to zero
  163. replace(/(\s|:|,)0(?:px|em|ex|cm|mm|in|pt|pc|%)/g, '$1' + '0');
  164. replace(/rect\(0(?:px|em|ex|cm|mm|in|pt|pc|%)/g, 'rect(0');
  165. // fraction zeros removal
  166. replace(/\.([1-9]*)0+(\D)/g, function(match, nonZeroPart, suffix) {
  167. return (nonZeroPart ? '.' : '') + nonZeroPart + suffix;
  168. });
  169. // restore 0% in hsl/hsla
  170. replace(/(hsl|hsla)\(([^\)]+)\)/g, function(match, colorFunction, colorDef) {
  171. var tokens = colorDef.split(',');
  172. if (tokens[1] == '0')
  173. tokens[1] = '0%';
  174. if (tokens[2] == '0')
  175. tokens[2] = '0%';
  176. return colorFunction + '(' + tokens.join(',') + ')';
  177. });
  178. // none to 0
  179. replace(/(border|border-top|border-right|border-bottom|border-left|outline):none/g, '$1:0');
  180. // background:none to background:0 0
  181. replace(/background:none([;}])/g, 'background:0 0$1');
  182. // multiple zeros into one
  183. replace(/box-shadow:0 0 0 0([^\.])/g, 'box-shadow:0 0$1');
  184. replace(/:0 0 0 0([^\.])/g, ':0$1');
  185. replace(/([: ,=\-])0\.(\d)/g, '$1.$2');
  186. replace(function shorthandNotations() {
  187. data = new ShorthandNotations(data).process();
  188. });
  189. // restore rect(...) zeros syntax for 4 zeros
  190. replace(/rect\(\s?0(\s|,)0[ ,]0[ ,]0\s?\)/g, 'rect(0$10$10$10)');
  191. // remove universal selector when not needed (*#id, *.class etc)
  192. replace(/\*([\.#:\[])/g, '$1');
  193. // Restore spaces inside calc back
  194. replace(/calc\([^\}]+\}/g, function(match) {
  195. return match.replace(/\+/g, ' + ');
  196. });
  197. replace(function restoreUrls() {
  198. data = urlsProcessor.restore(data);
  199. });
  200. replace(function rebaseUrls() {
  201. data = options.noRebase ? data : UrlRebase.process(data, options);
  202. });
  203. replace(function restoreFreeText() {
  204. data = freeTextProcessor.restore(data);
  205. });
  206. replace(function restoreComments() {
  207. data = commentsProcessor.restore(data);
  208. });
  209. replace(function restoreExpressions() {
  210. data = expressionsProcessor.restore(data);
  211. });
  212. // move first charset to the beginning
  213. replace(function moveCharset() {
  214. // get first charset in stylesheet
  215. var match = data.match(/@charset [^;]+;/);
  216. var firstCharset = match ? match[0] : null;
  217. if (!firstCharset)
  218. return;
  219. // reattach first charset and remove all subsequent
  220. data = firstCharset +
  221. (options.keepBreaks ? lineBreak : '') +
  222. data.replace(new RegExp('@charset [^;]+;(' + lineBreak + ')?', 'g'), '');
  223. });
  224. if (options.removeEmpty) {
  225. // empty elements
  226. replace(/[^\{\}]+\{\}/g, '');
  227. // empty @media declarations
  228. replace(/@media [^\{]+\{\}/g, '');
  229. }
  230. // trim spaces at beginning and end
  231. return data.trim();
  232. }
  233. };
  234. module.exports = CleanCSS;