You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

uglifycss-lib.js 28KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  1. /**
  2. * UglifyCSS
  3. * Port of YUI CSS Compressor to NodeJS
  4. * Author: Franck Marcia - https://github.com/fmarcia
  5. * MIT licenced
  6. */
  7. /**
  8. * cssmin.js
  9. * Author: Stoyan Stefanov - http://phpied.com/
  10. * This is a JavaScript port of the CSS minification tool
  11. * distributed with YUICompressor, itself a port
  12. * of the cssmin utility by Isaac Schlueter - http://foohack.com/
  13. * Permission is hereby granted to use the JavaScript version under the same
  14. * conditions as the YUICompressor (original YUICompressor note below).
  15. */
  16. /**
  17. * YUI Compressor
  18. * http://developer.yahoo.com/yui/compressor/
  19. * Author: Julien Lecomte - http://www.julienlecomte.net/
  20. * Copyright (c) 2011 Yahoo! Inc. All rights reserved.
  21. * The copyrights embodied in the content of this file are licensed
  22. * by Yahoo! Inc. under the BSD (revised) open source license.
  23. */
  24. 'use strict';
  25. var fs = require('fs');
  26. var path = require('path');
  27. var SEP = "/";
  28. var PATH_SEP = path.sep;
  29. var ___PRESERVED_TOKEN_ = "___PRESERVED_TOKEN_";
  30. var defaultOptions = {
  31. maxLineLen: 0,
  32. expandVars: false,
  33. uglyComments: false,
  34. cuteComments: false,
  35. convertUrls: "",
  36. debug: false
  37. };
  38. /**
  39. * Utility method to convert relative urls and replace them with tokens before
  40. * we start compressing. It must be called *after* extractDataUrls
  41. *
  42. * @private
  43. * @function convertRelativeUrls
  44. * @param {String} css The input css
  45. * @param {Object} options Options
  46. * @param {Array} The global array of tokens to preserve
  47. * @returns String The processed css
  48. */
  49. function convertRelativeUrls(css, options, preservedTokens) {
  50. var maxIndex = css.length - 1,
  51. appendIndex = 0,
  52. startIndex,
  53. endIndex,
  54. terminator,
  55. foundTerminator,
  56. sb = [],
  57. m,
  58. preserver,
  59. token,
  60. url,
  61. file,
  62. target,
  63. pattern = /(url\s*\()\s*(["']?)/g;
  64. // Since we need to account for non-base64 data urls, we need to handle
  65. // ' and ) being part of the data string. Hence switching to indexOf,
  66. // to determine whether or not we have matching string terminators and
  67. // handling sb appends directly, instead of using matcher.append* methods.
  68. while ((m = pattern.exec(css)) !== null) {
  69. startIndex = m.index + m[1].length; // "url(".length()
  70. terminator = m[2]; // ', " or empty (not quoted)
  71. if (terminator.length === 0) {
  72. terminator = ")";
  73. }
  74. foundTerminator = false;
  75. endIndex = pattern.lastIndex - 1;
  76. while(foundTerminator === false && endIndex+1 <= maxIndex) {
  77. endIndex = css.indexOf(terminator, endIndex + 1);
  78. // endIndex == 0 doesn't really apply here
  79. if ((endIndex > 0) && (css.charAt(endIndex - 1) !== '\\')) {
  80. foundTerminator = true;
  81. if (")" != terminator) {
  82. endIndex = css.indexOf(")", endIndex);
  83. }
  84. }
  85. }
  86. // Enough searching, start moving stuff over to the buffer
  87. sb.push(css.substring(appendIndex, m.index));
  88. if (foundTerminator) {
  89. token = css.substring(startIndex, endIndex).replace(/(^\s*|\s*$)/g, "");
  90. if (token.slice(0, 19) !== ___PRESERVED_TOKEN_) {
  91. if (terminator === "'" || terminator === '"') {
  92. token = token.slice(1, -1);
  93. } else if (terminator === ")") {
  94. terminator = "";
  95. }
  96. if (options.convertUrls && token.charAt(0) !== SEP && token.slice(0, 7) !== "http://" && token.slice(0, 8) !== "https://") {
  97. // build path of detected urls
  98. target = options.target.slice();
  99. token = token.split(SEP).join(PATH_SEP); // assuming urls in css use "/"
  100. url = path.resolve(options.source.join(PATH_SEP), token).split(PATH_SEP);
  101. file = url.pop();
  102. // remove common part of both paths
  103. while (target[0] === url[0]) {
  104. target.shift();
  105. url.shift();
  106. }
  107. for (var i = 0, l = target.length; i < l; ++i) {
  108. target[i] = "..";
  109. }
  110. url = terminator + target.concat(url, file).join(SEP) + terminator;
  111. } else {
  112. url = terminator + token + terminator;
  113. }
  114. preservedTokens.push(url);
  115. preserver = "url(" + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___)";
  116. sb.push(preserver);
  117. } else {
  118. sb.push("url(" + token + ")");
  119. }
  120. appendIndex = endIndex + 1;
  121. } else {
  122. // No end terminator found, re-add the whole match. Should we throw/warn here?
  123. sb.push(css.substring(m.index, pattern.lastIndex));
  124. appendIndex = pattern.lastIndex;
  125. }
  126. }
  127. sb.push(css.substring(appendIndex));
  128. return sb.join("");
  129. }
  130. /**
  131. * Utility method to replace all data urls with tokens before we start
  132. * compressing, to avoid performance issues running some of the subsequent
  133. * regexes against large strings chunks.
  134. *
  135. * @private
  136. * @function extractDataUrls
  137. * @param {String} css The input css
  138. * @param {Array} The global array of tokens to preserve
  139. * @returns String The processed css
  140. */
  141. function extractDataUrls(css, preservedTokens) {
  142. // Leave data urls alone to increase parse performance.
  143. var maxIndex = css.length - 1,
  144. appendIndex = 0,
  145. startIndex,
  146. endIndex,
  147. terminator,
  148. foundTerminator,
  149. sb = [],
  150. m,
  151. preserver,
  152. token,
  153. pattern = /url\(\s*(["']?)data\:/g;
  154. // Since we need to account for non-base64 data urls, we need to handle
  155. // ' and ) being part of the data string. Hence switching to indexOf,
  156. // to determine whether or not we have matching string terminators and
  157. // handling sb appends directly, instead of using matcher.append* methods.
  158. while ((m = pattern.exec(css)) !== null) {
  159. startIndex = m.index + 4; // "url(".length()
  160. terminator = m[1]; // ', " or empty (not quoted)
  161. if (terminator.length === 0) {
  162. terminator = ")";
  163. }
  164. foundTerminator = false;
  165. endIndex = pattern.lastIndex - 1;
  166. while(foundTerminator === false && endIndex+1 <= maxIndex) {
  167. endIndex = css.indexOf(terminator, endIndex + 1);
  168. // endIndex == 0 doesn't really apply here
  169. if ((endIndex > 0) && (css.charAt(endIndex - 1) !== '\\')) {
  170. foundTerminator = true;
  171. if (")" != terminator) {
  172. endIndex = css.indexOf(")", endIndex);
  173. }
  174. }
  175. }
  176. // Enough searching, start moving stuff over to the buffer
  177. sb.push(css.substring(appendIndex, m.index));
  178. if (foundTerminator) {
  179. token = css.substring(startIndex, endIndex);
  180. var parts = token.split(",");
  181. if (parts.length > 1 && parts[0].slice(-7) == ";base64") {
  182. token = token.replace(/\s+/g, "");
  183. } else {
  184. token = token.replace(/\n/g, " ");
  185. token = token.replace(/\s+/g, " ");
  186. token = token.replace(/(^\s+|\s+$)/g, "");
  187. }
  188. preservedTokens.push(token);
  189. preserver = "url(" + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___)";
  190. sb.push(preserver);
  191. appendIndex = endIndex + 1;
  192. } else {
  193. // No end terminator found, re-add the whole match. Should we throw/warn here?
  194. sb.push(css.substring(m.index, pattern.lastIndex));
  195. appendIndex = pattern.lastIndex;
  196. }
  197. }
  198. sb.push(css.substring(appendIndex));
  199. return sb.join("");
  200. }
  201. /**
  202. * Utility method to compress hex color values of the form #AABBCC to #ABC.
  203. *
  204. * DOES NOT compress CSS ID selectors which match the above pattern (which would break things).
  205. * e.g. #AddressForm { ... }
  206. *
  207. * DOES NOT compress IE filters, which have hex color values (which would break things).
  208. * e.g. filter: chroma(color="#FFFFFF");
  209. *
  210. * DOES NOT compress invalid hex values.
  211. * e.g. background-color: #aabbccdd
  212. *
  213. * @private
  214. * @function compressHexColors
  215. * @param {String} css The input css
  216. * @returns String The processed css
  217. */
  218. function compressHexColors(css) {
  219. // Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters)
  220. var pattern = /(\=\s*?["']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/gi,
  221. m,
  222. index = 0,
  223. isFilter,
  224. sb = [];
  225. while ((m = pattern.exec(css)) !== null) {
  226. sb.push(css.substring(index, m.index));
  227. isFilter = m[1];
  228. if (isFilter) {
  229. // Restore, maintain case, otherwise filter will break
  230. sb.push(m[1] + "#" + (m[2] + m[3] + m[4] + m[5] + m[6] + m[7]));
  231. } else {
  232. if (m[2].toLowerCase() == m[3].toLowerCase() &&
  233. m[4].toLowerCase() == m[5].toLowerCase() &&
  234. m[6].toLowerCase() == m[7].toLowerCase()) {
  235. // Compress.
  236. sb.push("#" + (m[3] + m[5] + m[7]).toLowerCase());
  237. } else {
  238. // Non compressible color, restore but lower case.
  239. sb.push("#" + (m[2] + m[3] + m[4] + m[5] + m[6] + m[7]).toLowerCase());
  240. }
  241. }
  242. index = pattern.lastIndex = pattern.lastIndex - m[8].length;
  243. }
  244. sb.push(css.substring(index));
  245. return sb.join("");
  246. }
  247. // Preserve 0 followed by unit in keyframes steps
  248. function keyframes(content, preservedTokens) {
  249. var level,
  250. buffer,
  251. buffers,
  252. pattern = /@[a-z0-9-_]*keyframes\s+[a-z0-9-_]+\s*{/gi,
  253. index = 0,
  254. len,
  255. c,
  256. startIndex;
  257. var preserve = function (part, index) {
  258. part = part.replace(/(^\s|\s$)/g, '');
  259. if (part.charAt(0) === '0') {
  260. preservedTokens.push(part);
  261. buffer[index] = ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
  262. }
  263. };
  264. while (true) {
  265. level = 0;
  266. buffer = '';
  267. startIndex = content.slice(index).search(pattern);
  268. if (startIndex < 0) {
  269. break;
  270. }
  271. index += startIndex;
  272. startIndex = index;
  273. len = content.length;
  274. buffers = [];
  275. for (; index < len; ++index) {
  276. c = content.charAt(index);
  277. if (c === '{') {
  278. if (level === 0) {
  279. buffers.push(buffer.replace(/(^\s|\s$)/g, ''));
  280. } else if (level === 1) {
  281. buffer = buffer.split(',');
  282. buffer.forEach(preserve);
  283. buffers.push(buffer.join(',').replace(/(^\s|\s$)/g, ''));
  284. }
  285. buffer = '';
  286. level += 1;
  287. } else if (c === '}') {
  288. if (level === 2) {
  289. buffers.push('{' + buffer.replace(/(^\s|\s$)/g, '') + '}');
  290. buffer = '';
  291. } else if (level === 1) {
  292. content = content.slice(0, startIndex) +
  293. buffers.shift() + '{' +
  294. buffers.join('') +
  295. content.slice(index);
  296. break;
  297. }
  298. level -= 1;
  299. }
  300. if (level < 0) {
  301. break;
  302. } else if (c !== '{' && c !== '}') {
  303. buffer += c;
  304. }
  305. }
  306. }
  307. return content;
  308. }
  309. // Collect all comment blocks and return new content with comment placeholders
  310. // (comments is an array thus passed by reference)
  311. function collectComments(content, comments) {
  312. var table = [];
  313. var from = 0;
  314. var start, end;
  315. while (true) {
  316. start = content.indexOf("/*", from);
  317. if (start > -1) {
  318. end = content.indexOf("*/", start + 2);
  319. if (end > -1) {
  320. comments.push(content.slice(start + 2, end));
  321. table.push(content.slice(from, start));
  322. table.push("/*___PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___*/");
  323. from = end + 2;
  324. } else {
  325. // unterminated comment
  326. end = -2;
  327. break;
  328. }
  329. } else {
  330. break;
  331. }
  332. }
  333. table.push(content.slice(end + 2));
  334. return table.join("");
  335. }
  336. // Uglify a CSS string
  337. function processString(content, options) {
  338. var startIndex,
  339. comments = [],
  340. preservedTokens = [],
  341. token,
  342. len = content.length,
  343. pattern,
  344. quote,
  345. rgbcolors,
  346. hexcolor,
  347. placeholder,
  348. val,
  349. i,
  350. c,
  351. line = [],
  352. lines = [],
  353. vars = {};
  354. options = options || defaultOptions;
  355. content = extractDataUrls(content, preservedTokens);
  356. content = convertRelativeUrls(content, options, preservedTokens);
  357. content = collectComments(content, comments);
  358. // preserve strings so their content doesn't get accidentally minified
  359. pattern = /("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')/g;
  360. content = content.replace(pattern, function (token) {
  361. quote = token.substring(0, 1);
  362. token = token.slice(1, -1);
  363. // maybe the string contains a comment-like substring or more? put'em back then
  364. if (token.indexOf("___PRESERVE_CANDIDATE_COMMENT_") >= 0) {
  365. for (i = 0, len = comments.length; i < len; i += 1) {
  366. token = token.replace("___PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments[i]);
  367. }
  368. }
  369. // minify alpha opacity in filter strings
  370. token = token.replace(/progid:DXImageTransform.Microsoft.Alpha\(Opacity=/gi, "alpha(opacity=");
  371. preservedTokens.push(token);
  372. return quote + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___" + quote;
  373. });
  374. // strings are safe, now wrestle the comments
  375. for (i = 0, len = comments.length; i < len; i += 1) {
  376. token = comments[i];
  377. placeholder = "___PRESERVE_CANDIDATE_COMMENT_" + i + "___";
  378. // ! in the first position of the comment means preserve
  379. // so push to the preserved tokens keeping the !
  380. if (token.charAt(0) === "!") {
  381. if (options.cuteComments) {
  382. preservedTokens.push(token.substring(1));
  383. } else if (options.uglyComments) {
  384. preservedTokens.push(token.substring(1).replace(/[\r\n]/g, ''));
  385. } else {
  386. preservedTokens.push(token);
  387. }
  388. content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
  389. continue;
  390. }
  391. // \ in the last position looks like hack for Mac/IE5
  392. // shorten that to /*\*/ and the next one to /**/
  393. if (token.charAt(token.length - 1) === "\\") {
  394. preservedTokens.push("\\");
  395. content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
  396. i = i + 1; // attn: advancing the loop
  397. preservedTokens.push("");
  398. content = content.replace(
  399. "___PRESERVE_CANDIDATE_COMMENT_" + i + "___",
  400. ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___"
  401. );
  402. continue;
  403. }
  404. // keep empty comments after child selectors (IE7 hack)
  405. // e.g. html >/**/ body
  406. if (token.length === 0) {
  407. startIndex = content.indexOf(placeholder);
  408. if (startIndex > 2) {
  409. if (content.charAt(startIndex - 3) === '>') {
  410. preservedTokens.push("");
  411. content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
  412. }
  413. }
  414. }
  415. // in all other cases kill the comment
  416. content = content.replace("/*" + placeholder + "*/", "");
  417. }
  418. if (options.expandVars) {
  419. // parse simple @variables blocks and remove them
  420. pattern = /@variables\s*\{\s*([^\}]+)\s*\}/g;
  421. content = content.replace(pattern, function (ignore, f1) {
  422. pattern = /\s*([a-z0-9\-]+)\s*:\s*([^;\}]+)\s*/gi;
  423. f1.replace(pattern, function (ignore, f1, f2) {
  424. if (f1 && f2) {
  425. vars[f1] = f2;
  426. }
  427. return '';
  428. });
  429. return '';
  430. });
  431. // replace var(x) with the value of x
  432. pattern = /var\s*\(\s*([^\)]+)\s*\)/g;
  433. content = content.replace(pattern, function (ignore, f1) {
  434. return vars[f1] || 'none';
  435. });
  436. }
  437. // normalize all whitespace strings to single spaces. Easier to work with that way.
  438. content = content.replace(/\s+/g, " ");
  439. // preserve formulas in calc() before removing spaces
  440. pattern = /calc\(([^;}]*)\)/g;
  441. content = content.replace(pattern, function (ignore, f1) {
  442. preservedTokens.push(
  443. 'calc(' +
  444. f1.replace(/(^\s*|\s*$)/g, "")
  445. .replace(/\( /g, "(")
  446. .replace(/ \)/g, ")") +
  447. ')'
  448. );
  449. return ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
  450. });
  451. // preserve matrix
  452. pattern = /\s*filter:\s*progid:DXImageTransform.Microsoft.Matrix\(([^\)]+)\);/g;
  453. content = content.replace(pattern, function (ignore, f1) {
  454. preservedTokens.push(f1);
  455. return "filter:progid:DXImageTransform.Microsoft.Matrix(" + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___);";
  456. });
  457. // remove the spaces before the things that should not have spaces before them.
  458. // but, be careful not to turn "p :link {...}" into "p:link{...}"
  459. // swap out any pseudo-class colons with the token, and then swap back.
  460. pattern = /(^|\})(([^\{:])+:)+([^\{]*\{)/g;
  461. content = content.replace(pattern, function (token) {
  462. return token.replace(/:/g, "___PSEUDOCLASSCOLON___");
  463. });
  464. // remove spaces before the things that should not have spaces before them.
  465. content = content.replace(/\s+([!{};:>+\(\)\],])/g, "$1");
  466. // restore spaces for !important
  467. content = content.replace(/!important/g, " !important");
  468. // bring back the colon
  469. content = content.replace(/___PSEUDOCLASSCOLON___/g, ":");
  470. // preserve 0 followed by a time unit for properties using time units
  471. pattern = /\s*(animation|animation-delay|animation-duration|transition|transition-delay|transition-duration):\s*([^;}]+)/gi;
  472. content = content.replace(pattern, function (ignore, f1, f2) {
  473. f2 = f2.replace(/(^|\D)0?\.?0(m?s)/gi, function (ignore, g1, g2) {
  474. preservedTokens.push('0' + g2);
  475. return g1 + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
  476. });
  477. return f1 + ":" + f2;
  478. });
  479. // preserve 0% in hsl and hsla color definitions
  480. content = content.replace(/(hsla?)\(([^)]+)\)/g, function (ignore, f1, f2) {
  481. var f0 = [];
  482. f2.split(',').forEach(function (part) {
  483. part = part.replace(/(^\s+|\s+$)/g, "");
  484. if (part === '0%') {
  485. preservedTokens.push('0%');
  486. f0.push(___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
  487. } else {
  488. f0.push(part);
  489. }
  490. });
  491. return f1 + '(' + f0.join(',') + ')';
  492. });
  493. // preserve 0 followed by unit in keyframes steps (WIP)
  494. content = keyframes(content, preservedTokens);
  495. // retain space for special IE6 cases
  496. content = content.replace(/:first-(line|letter)(\{|,)/gi, function (ignore, f1, f2) {
  497. return ":first-" + f1.toLowerCase() + " " + f2;
  498. });
  499. // newlines before and after the end of a preserved comment
  500. if (options.cuteComments) {
  501. content = content.replace(/\s*\/\*/g, "___PRESERVED_NEWLINE___/*");
  502. content = content.replace(/\*\/\s*/g, "*/___PRESERVED_NEWLINE___");
  503. // no space after the end of a preserved comment
  504. } else {
  505. content = content.replace(/\*\/\s*/g, '*/');
  506. }
  507. // If there are multiple @charset directives, push them to the top of the file.
  508. pattern = /^(.*)(@charset)( "[^"]*";)/gi;
  509. content = content.replace(pattern, function (ignore, f1, f2, f3) {
  510. return f2.toLowerCase() + f3 + f1;
  511. });
  512. // When all @charset are at the top, remove the second and after (as they are completely ignored).
  513. pattern = /^((\s*)(@charset)( [^;]+;\s*))+/gi;
  514. content = content.replace(pattern, function (ignore, ignore2, f2, f3, f4) {
  515. return f2 + f3.toLowerCase() + f4;
  516. });
  517. // lowercase some popular @directives (@charset is done right above)
  518. pattern = /@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)/gi;
  519. content = content.replace(pattern, function (ignore, f1) {
  520. return '@' + f1.toLowerCase();
  521. });
  522. // lowercase some more common pseudo-elements
  523. pattern = /:(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)/gi;
  524. content = content.replace(pattern, function (ignore, f1) {
  525. return ':' + f1.toLowerCase();
  526. });
  527. // if there is a @charset, then only allow one, and push to the top of the file.
  528. content = content.replace(/^(.*)(@charset \"[^\"]*\";)/g, "$2$1");
  529. content = content.replace(/^(\s*@charset [^;]+;\s*)+/g, "$1");
  530. // lowercase some more common functions
  531. pattern = /:(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?any)\(/gi;
  532. content = content.replace(pattern, function (ignore, f1) {
  533. return ':' + f1.toLowerCase() + '(';
  534. });
  535. // lower case some common function that can be values
  536. // NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us right after this
  537. pattern = /([:,\( ]\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)/gi;
  538. content = content.replace(pattern, function (ignore, f1, f2) {
  539. return f1 + f2.toLowerCase();
  540. });
  541. // put the space back in some cases, to support stuff like
  542. // @media screen and (-webkit-min-device-pixel-ratio:0){
  543. content = content.replace(/\band\(/gi, "and (");
  544. // remove the spaces after the things that should not have spaces after them.
  545. content = content.replace(/([!{}:;>+\(\[,])\s+/g, "$1");
  546. // remove unnecessary semicolons
  547. content = content.replace(/;+\}/g, "}");
  548. // replace 0(px,em,%) with 0.
  549. content = content.replace(/(^|[^.0-9\\])(?:0?\.)?0(?:ex|ch|r?em|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|g?rad|turn|m?s|k?Hz|dpi|dpcm|dppx|%)/gi, "$10");
  550. // Replace x.0(px,em,%) with x(px,em,%).
  551. content = content.replace(/([0-9])\.0(ex|ch|r?em|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|g?rad|turn|m?s|k?Hz|dpi|dpcm|dppx|%| |;)/gi, "$1$2");
  552. // replace 0 0 0 0; with 0.
  553. content = content.replace(/:0 0 0 0(;|\})/g, ":0$1");
  554. content = content.replace(/:0 0 0(;|\})/g, ":0$1");
  555. content = content.replace(/:0 0(;|\})/g, ":0$1");
  556. // replace background-position:0; with background-position:0 0;
  557. // same for transform-origin and box-shadow
  558. pattern = /(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin|box-shadow):0(;|\})/gi;
  559. content = content.replace(pattern, function (ignore, f1, f2) {
  560. return f1.toLowerCase() + ":0 0" + f2;
  561. });
  562. // replace 0.6 to .6, but only when preceded by : or a white-space
  563. content = content.replace(/(:|\s)0+\.(\d+)/g, "$1.$2");
  564. // shorten colors from rgb(51,102,153) to #336699
  565. // this makes it more likely that it'll get further compressed in the next step.
  566. pattern = /rgb\s*\(\s*([0-9,\s]+)\s*\)/gi;
  567. content = content.replace(pattern, function (ignore, f1) {
  568. rgbcolors = f1.split(",");
  569. hexcolor = "#";
  570. for (i = 0; i < rgbcolors.length; i += 1) {
  571. val = parseInt(rgbcolors[i], 10);
  572. if (val < 16) {
  573. hexcolor += "0";
  574. }
  575. if (val > 255) {
  576. val = 255;
  577. }
  578. hexcolor += val.toString(16);
  579. }
  580. return hexcolor;
  581. });
  582. // Shorten colors from #AABBCC to #ABC.
  583. content = compressHexColors(content);
  584. // Replace #f00 -> red
  585. content = content.replace(/(:|\s)(#f00)(;|})/g, "$1red$3");
  586. // Replace other short color keywords
  587. content = content.replace(/(:|\s)(#000080)(;|})/g, "$1navy$3");
  588. content = content.replace(/(:|\s)(#808080)(;|})/g, "$1gray$3");
  589. content = content.replace(/(:|\s)(#808000)(;|})/g, "$1olive$3");
  590. content = content.replace(/(:|\s)(#800080)(;|})/g, "$1purple$3");
  591. content = content.replace(/(:|\s)(#c0c0c0)(;|})/g, "$1silver$3");
  592. content = content.replace(/(:|\s)(#008080)(;|})/g, "$1teal$3");
  593. content = content.replace(/(:|\s)(#ffa500)(;|})/g, "$1orange$3");
  594. content = content.replace(/(:|\s)(#800000)(;|})/g, "$1maroon$3");
  595. // border: none -> border:0
  596. pattern = /(border|border-top|border-right|border-bottom|border-left|outline|background):none(;|\})/gi;
  597. content = content.replace(pattern, function (ignore, f1, f2) {
  598. return f1.toLowerCase() + ":0" + f2;
  599. });
  600. // shorter opacity IE filter
  601. content = content.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity=");
  602. // Find a fraction that is used for Opera's -o-device-pixel-ratio query
  603. // Add token to add the "\" back in later
  604. content = content.replace(/\(([\-A-Za-z]+):([0-9]+)\/([0-9]+)\)/g, "($1:$2___QUERY_FRACTION___$3)");
  605. // remove empty rules.
  606. content = content.replace(/[^\};\{\/]+\{\}/g, "");
  607. // Add "\" back to fix Opera -o-device-pixel-ratio query
  608. content = content.replace(/___QUERY_FRACTION___/g, "/");
  609. // some source control tools don't like it when files containing lines longer
  610. // than, say 8000 characters, are checked in. The linebreak option is used in
  611. // that case to split long lines after a specific column.
  612. if (options.maxLineLen > 0) {
  613. for (i = 0, len = content.length; i < len; i += 1) {
  614. c = content.charAt(i);
  615. line.push(c);
  616. if (c === '}' && line.length > options.maxLineLen) {
  617. lines.push(line.join(''));
  618. line = [];
  619. }
  620. }
  621. if (line.length) {
  622. lines.push(line.join(''));
  623. }
  624. content = lines.join('\n');
  625. }
  626. // replace multiple semi-colons in a row by a single one
  627. // see SF bug #1980989
  628. content = content.replace(/;;+/g, ";");
  629. // trim the final string (for any leading or trailing white spaces)
  630. content = content.replace(/(^\s*|\s*$)/g, "");
  631. // restore preserved tokens
  632. for (i = preservedTokens.length - 1; i >= 0 ; i--) {
  633. content = content.replace(___PRESERVED_TOKEN_ + i + "___", preservedTokens[i], "g");
  634. }
  635. // restore preserved newlines
  636. content = content.replace(/___PRESERVED_NEWLINE___/g, '\n');
  637. // return
  638. return content;
  639. }
  640. // Uglify CSS files
  641. function processFiles(filenames, options) {
  642. var nFiles = filenames.length,
  643. uglies = [],
  644. index,
  645. filename,
  646. content;
  647. options = options || defaultOptions;
  648. if (options.convertUrls) {
  649. options.target = path.resolve(process.cwd(), options.convertUrls).split(PATH_SEP);
  650. }
  651. // process files
  652. for (index = 0; index < nFiles; index += 1) {
  653. filename = filenames[index];
  654. try {
  655. content = fs.readFileSync(filename, 'utf8');
  656. if (content.length) {
  657. if (options.convertUrls) {
  658. options.source = path.resolve(process.cwd(), filename).split(PATH_SEP);
  659. options.source.pop();
  660. }
  661. uglies.push(processString(content, options));
  662. }
  663. } catch (e) {
  664. if (options.debug) {
  665. console.error('uglifycss: unable to process "' + filename + '"\n' + e.stack);
  666. } else {
  667. console.error('uglifycss: unable to process "' + filename + '"\n\t' + e);
  668. }
  669. process.exit(1);
  670. }
  671. }
  672. // return concat'd results
  673. return uglies.join('');
  674. }
  675. module.exports = {
  676. defaultOptions: defaultOptions,
  677. processString: processString,
  678. processFiles: processFiles
  679. };