css-minifier.js 27 KB

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