cssmin.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. /**
  2. * node-cssmin
  3. * A simple module for Node.js that minify CSS
  4. * Author : Johan Bleuzen
  5. */
  6. /**
  7. * cssmin.js
  8. * Author: Stoyan Stefanov - http://phpied.com/
  9. * This is a JavaScript port of the CSS minification tool
  10. * distributed with YUICompressor, itself a port
  11. * of the cssmin utility by Isaac Schlueter - http://foohack.com/
  12. * Permission is hereby granted to use the JavaScript version under the same
  13. * conditions as the YUICompressor (original YUICompressor note below).
  14. */
  15. /*
  16. * YUI Compressor
  17. * http://developer.yahoo.com/yui/compressor/
  18. * Author: Julien Lecomte - http://www.julienlecomte.net/
  19. * Copyright (c) 2011 Yahoo! Inc. All rights reserved.
  20. * The copyrights embodied in the content of this file are licensed
  21. * by Yahoo! Inc. under the BSD (revised) open source license.
  22. */
  23. exports.cssmin = cssmin;
  24. function cssmin(css, linebreakpos) {
  25. var startIndex = 0,
  26. endIndex = 0,
  27. i = 0, max = 0,
  28. preservedTokens = [],
  29. comments = [],
  30. token = '',
  31. totallen = css.length,
  32. placeholder = '';
  33. // collect all comment blocks...
  34. while ((startIndex = css.indexOf("/*", startIndex)) >= 0) {
  35. endIndex = css.indexOf("*/", startIndex + 2);
  36. if (endIndex < 0) {
  37. endIndex = totallen;
  38. }
  39. token = css.slice(startIndex + 2, endIndex);
  40. comments.push(token);
  41. css = css.slice(0, startIndex + 2) + "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___" + css.slice(endIndex);
  42. startIndex += 2;
  43. }
  44. // preserve strings so their content doesn't get accidentally minified
  45. css = css.replace(/("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')/g, function (match) {
  46. var i, max, quote = match.substring(0, 1);
  47. match = match.slice(1, -1);
  48. // maybe the string contains a comment-like substring?
  49. // one, maybe more? put'em back then
  50. if (match.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) {
  51. for (i = 0, max = comments.length; i < max; i = i + 1) {
  52. match = match.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments[i]);
  53. }
  54. }
  55. // minify alpha opacity in filter strings
  56. match = match.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity=");
  57. preservedTokens.push(match);
  58. return quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___" + quote;
  59. });
  60. // strings are safe, now wrestle the comments
  61. for (i = 0, max = comments.length; i < max; i = i + 1) {
  62. token = comments[i];
  63. placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___";
  64. // ! in the first position of the comment means preserve
  65. // so push to the preserved tokens keeping the !
  66. if (token.charAt(0) === "!") {
  67. preservedTokens.push(token);
  68. css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___");
  69. continue;
  70. }
  71. // \ in the last position looks like hack for Mac/IE5
  72. // shorten that to /*\*/ and the next one to /**/
  73. if (token.charAt(token.length - 1) === "\\") {
  74. preservedTokens.push("\\");
  75. css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___");
  76. i = i + 1; // attn: advancing the loop
  77. preservedTokens.push("");
  78. css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___");
  79. continue;
  80. }
  81. // keep empty comments after child selectors (IE7 hack)
  82. // e.g. html >/**/ body
  83. if (token.length === 0) {
  84. startIndex = css.indexOf(placeholder);
  85. if (startIndex > 2) {
  86. if (css.charAt(startIndex - 3) === '>') {
  87. preservedTokens.push("");
  88. css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___");
  89. }
  90. }
  91. }
  92. // in all other cases kill the comment
  93. css = css.replace("/*" + placeholder + "*/", "");
  94. }
  95. // Normalize all whitespace strings to single spaces. Easier to work with that way.
  96. css = css.replace(/\s+/g, " ");
  97. // Remove the spaces before the things that should not have spaces before them.
  98. // But, be careful not to turn "p :link {...}" into "p:link{...}"
  99. // Swap out any pseudo-class colons with the token, and then swap back.
  100. css = css.replace(/(^|\})(([^\{:])+:)+([^\{]*\{)/g, function (m) {
  101. return m.replace(":", "___YUICSSMIN_PSEUDOCLASSCOLON___");
  102. });
  103. css = css.replace(/\s+([!{};:>+\(\)\],])/g, '$1');
  104. css = css.replace(/___YUICSSMIN_PSEUDOCLASSCOLON___/g, ":");
  105. // retain space for special IE6 cases
  106. css = css.replace(/:first-(line|letter)(\{|,)/g, ":first-$1 $2");
  107. // no space after the end of a preserved comment
  108. css = css.replace(/\*\/ /g, '*/');
  109. // If there is a @charset, then only allow one, and push to the top of the file.
  110. css = css.replace(/^(.*)(@charset "[^"]*";)/gi, '$2$1');
  111. css = css.replace(/^(\s*@charset [^;]+;\s*)+/gi, '$1');
  112. // Put the space back in some cases, to support stuff like
  113. // @media screen and (-webkit-min-device-pixel-ratio:0){
  114. css = css.replace(/\band\(/gi, "and (");
  115. // Remove the spaces after the things that should not have spaces after them.
  116. css = css.replace(/([!{}:;>+\(\[,])\s+/g, '$1');
  117. // remove unnecessary semicolons
  118. css = css.replace(/;+\}/g, "}");
  119. // Replace 0(px,em,%) with 0.
  120. css = css.replace(/([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)/gi, "$1$2");
  121. // Replace 0 0 0 0; with 0.
  122. css = css.replace(/:0 0 0 0(;|\})/g, ":0$1");
  123. css = css.replace(/:0 0 0(;|\})/g, ":0$1");
  124. css = css.replace(/:0 0(;|\})/g, ":0$1");
  125. // Replace background-position:0; with background-position:0 0;
  126. // same for transform-origin
  127. css = css.replace(/(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|\})/gi, function(all, prop, tail) {
  128. return prop.toLowerCase() + ":0 0" + tail;
  129. });
  130. // Replace 0.6 to .6, but only when preceded by : or a white-space
  131. css = css.replace(/(:|\s)0+\.(\d+)/g, "$1.$2");
  132. // Shorten colors from rgb(51,102,153) to #336699
  133. // This makes it more likely that it'll get further compressed in the next step.
  134. css = css.replace(/rgb\s*\(\s*([0-9,\s]+)\s*\)/gi, function () {
  135. var i, rgbcolors = arguments[1].split(',');
  136. for (i = 0; i < rgbcolors.length; i = i + 1) {
  137. rgbcolors[i] = parseInt(rgbcolors[i], 10).toString(16);
  138. if (rgbcolors[i].length === 1) {
  139. rgbcolors[i] = '0' + rgbcolors[i];
  140. }
  141. }
  142. return '#' + rgbcolors.join('');
  143. });
  144. // Shorten colors from #AABBCC to #ABC. Note that we want to make sure
  145. // the color is not preceded by either ", " or =. Indeed, the property
  146. // filter: chroma(color="#FFFFFF");
  147. // would become
  148. // filter: chroma(color="#FFF");
  149. // which makes the filter break in IE.
  150. css = css.replace(/([^"'=\s])(\s*)#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])/gi, function () {
  151. var group = arguments;
  152. if (
  153. group[3].toLowerCase() === group[4].toLowerCase() &&
  154. group[5].toLowerCase() === group[6].toLowerCase() &&
  155. group[7].toLowerCase() === group[8].toLowerCase()
  156. ) {
  157. return (group[1] + group[2] + '#' + group[3] + group[5] + group[7]).toLowerCase();
  158. } else {
  159. return group[0].toLowerCase();
  160. }
  161. });
  162. // border: none -> border:0
  163. css = css.replace(/(border|border-top|border-right|border-bottom|border-right|outline|background):none(;|\})/gi, function(all, prop, tail) {
  164. return prop.toLowerCase() + ":0" + tail;
  165. });
  166. // shorter opacity IE filter
  167. css = css.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity=");
  168. // Remove empty rules.
  169. css = css.replace(/[^\};\{\/]+\{\}/g, "");
  170. if (linebreakpos >= 0) {
  171. // Some source control tools don't like it when files containing lines longer
  172. // than, say 8000 characters, are checked in. The linebreak option is used in
  173. // that case to split long lines after a specific column.
  174. startIndex = 0;
  175. i = 0;
  176. while (i < css.length) {
  177. i = i + 1;
  178. if (css[i - 1] === '}' && i - startIndex > linebreakpos) {
  179. css = css.slice(0, i) + '\n' + css.slice(i);
  180. startIndex = i;
  181. }
  182. }
  183. }
  184. // Replace multiple semi-colons in a row by a single one
  185. // See SF bug #1980989
  186. css = css.replace(/;;+/g, ";");
  187. // restore preserved comments and strings
  188. for (i = 0, max = preservedTokens.length; i < max; i = i + 1) {
  189. css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens[i]);
  190. }
  191. // Trim the final string (for any leading or trailing white spaces)
  192. css = css.replace(/^\s+|\s+$/g, "");
  193. return css;
  194. };