uglifycss.js 24 KB

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