Bladeren bron

renamed files

Gildas 7 jaren geleden
bovenliggende
commit
5a59add645

+ 5 - 4
extension/core/bg/script-loader.js

@@ -34,6 +34,7 @@ singlefile.scriptLoader = (() => {
 		"/extension/ui/content/content-ui.js",
 		"/extension/core/content/content.js"
 	];
+
 	const frameScriptFiles = [
 		"/lib/browser-polyfill/custom-browser-polyfill.js",
 		"/extension/index.js",
@@ -49,21 +50,21 @@ singlefile.scriptLoader = (() => {
 			"/lib/single-file/html-serializer.js"
 		],
 		compressCSS: [
-			"/lib/single-file/css-uglifycss.js"
+			"/lib/single-file/css-minifier.js"
 		],
 		removeAlternativeFonts: [
 			"/lib/single-file/css-fonts-minifier.js"
 		],
 		removeUnusedStyles: [
-			"/lib/single-file/css-what.js",
+			"/lib/single-file/css-selector-parser.js",
 			"/lib/single-file/css-declarations-parser.js",
 			"/lib/single-file/css-rules-matcher.js",
 			"/lib/single-file/css-media-query-parser.js",
 			"/lib/single-file/css-medias-minifier.js",
-			"/lib/single-file/css-minifier.js"
+			"/lib/single-file/css-rules-minifier.js"
 		],
 		lazyLoadImages: [
-			"/lib/single-file/lazy-loader.js",
+			"/lib/single-file/lazy-loader.js"
 		]
 	};
 

+ 739 - 113
lib/single-file/css-minifier.js

@@ -1,148 +1,774 @@
-/*
- * Copyright 2018 Gildas Lormeau
- * contact : gildas.lormeau <at> gmail.com
- * 
- * This file is part of SingleFile.
- *
- *   SingleFile is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU Lesser General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   SingleFile is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU Lesser General Public License for more details.
- *
- *   You should have received a copy of the GNU Lesser General Public License
- *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * UglifyCSS
+ * Port of YUI CSS Compressor to Vanilla JS
+ * Author: Gildas Lormeau
+ * MIT licenced
  */
 
-/* global CSSRule, cssWhat, parseCss, RulesMatcher */
+/**
+ * UglifyCSS
+ * Port of YUI CSS Compressor to NodeJS
+ * Author: Franck Marcia - https://github.com/fmarcia
+ * MIT licenced
+ */
 
-this.cssMinifier = this.cssMinifier || (() => {
+/**
+ * cssmin.js
+ * Author: Stoyan Stefanov - http://phpied.com/
+ * This is a JavaScript port of the CSS minification tool
+ * distributed with YUICompressor, itself a port
+ * of the cssmin utility by Isaac Schlueter - http://foohack.com/
+ * Permission is hereby granted to use the JavaScript version under the same
+ * conditions as the YUICompressor (original YUICompressor note below).
+ */
 
-	const SEPARATOR_TYPES = ["descendant", "child", "sibling", "adjacent"];
-	const REMOVED_PSEUDO_CLASSES = ["focus", "focus-within", "hover", "link", "visited", "active"];
-	const REMOVED_PSEUDO_ELEMENTS = ["after", "before", "first-line", "first-letter", "placeholder", "-webkit-input-placeholder", "selection", "marker", "cue", "-webkit-progress-bar", "-webkit-progress-value", "-webkit-inner-spin-button", "-webkit-outer-spin-button", "-webkit-search-cancel-button", "-webkit-search-cancel-button"];
-	const IGNORED_SELECTORS = ["::-webkit-scrollbar", "::-webkit-scrollbar-button", "::-webkit-scrollbar-thumb", "::-webkit-scrollbar-track", "::-webkit-scrollbar-track-piece", "::-webkit-scrollbar-corner", "::-webkit-resizer"];
+/**
+ * YUI Compressor
+ * http://developer.yahoo.com/yui/compressor/
+ * Author: Julien Lecomte - http://www.julienlecomte.net/
+ * Copyright (c) 2011 Yahoo! Inc. All rights reserved.
+ * The copyrights embodied in the content of this file are licensed
+ * by Yahoo! Inc. under the BSD (revised) open source license.
+ */
 
-	return {
-		process: doc => {
-			const rulesMatcher = RulesMatcher.create(doc);
-			const mediaAllInfo = rulesMatcher.getAllMatchedRules();
-			const stats = { processed: 0, discarded: 0 };
-			doc.querySelectorAll("style").forEach(styleElement => {
-				if (styleElement.sheet) {
-					let mediaInfo;
-					if (styleElement.media && styleElement.media != "all") {
-						mediaInfo = mediaAllInfo.medias.get(styleElement.media);
-					} else {
-						mediaInfo = mediaAllInfo;
-					}
-					processRules(doc, styleElement.sheet.cssRules, mediaInfo, stats);
-					styleElement.textContent = serializeRules(styleElement.sheet.cssRules);
-				}
-			});
-			doc.querySelectorAll("[style]").forEach(element => {
-				processStyle(doc, element.style, mediaAllInfo);
-			});
-			return stats;
-		}
+this.uglifycss = this.uglifycss || (() => {
+
+	/**
+	 * @type {string} - placeholder prefix
+	 */
+
+	const ___PRESERVED_TOKEN_ = "___PRESERVED_TOKEN_";
+
+	/**
+	 * @typedef {object} options - UglifyCSS options
+	 * @property {number} [maxLineLen=0] - Maximum line length of uglified CSS
+	 * @property {boolean} [expandVars=false] - Expand variables
+	 * @property {boolean} [uglyComments=false] - Removes newlines within preserved comments
+	 * @property {boolean} [cuteComments=false] - Preserves newlines within and around preserved comments
+	 * @property {boolean} [debug=false] - Prints full error stack on error
+	 * @property {string} [output=''] - Output file name
+	 */
+
+	/**
+	 * @type {options} - UglifyCSS options
+	 */
+
+	const defaultOptions = {
+		maxLineLen: 0,
+		expandVars: false,
+		uglyComments: false,
+		cuteComments: false,
+		debug: false,
+		output: ""
 	};
 
-	function processRules(doc, cssRules, mediaInfo, stats) {
-		stats.processed += cssRules.length;
-		stats.discarded += cssRules.length;
-		Array.from(cssRules).forEach(cssRule => {
-			if (cssRule.type == CSSRule.MEDIA_RULE) {
-				processRules(doc, cssRule.cssRules, mediaInfo.medias.get(cssRule.media), stats);
-			} else if (cssRule.type == CSSRule.STYLE_RULE) {
-				const ruleInfo = mediaInfo.rules.get(cssRule);
-				if (ruleInfo) {
-					const stylesInfo = parseCss.parseAListOfDeclarations(cssRule.style.cssText);
-					const unusedStyles = stylesInfo.filter(style => !ruleInfo.style.get(style.name));
-					if (unusedStyles.length) {
-						unusedStyles.forEach(style => cssRule.style.removeProperty(style.name));
+	/**
+	 * convertRelativeUrls converts relative urls and replaces them with tokens
+	 * before we start compressing. It must be called *after* extractDataUrls
+	 *
+	 * @param {string} css - CSS content
+	 * @param {options} options - UglifyCSS Options
+	 * @param {string[]} preservedTokens - Global array of tokens to preserve
+	 *
+	 * @return {string} Processed css
+	 */
+
+	function convertRelativeUrls(css, options, preservedTokens) {
+
+		const pattern = /(url\s*\()\s*(["']?)/g;
+		const maxIndex = css.length - 1;
+		const sb = [];
+
+		let appendIndex = 0, match;
+
+		// Since we need to account for non-base64 data urls, we need to handle
+		// ' and ) being part of the data string. Hence switching to indexOf,
+		// to determine whether or not we have matching string terminators and
+		// handling sb appends directly, instead of using matcher.append* methods.
+
+		while ((match = pattern.exec(css)) !== null) {
+
+			const startIndex = match.index + match[1].length;  // 'url('.length()
+			let terminator = match[2];         // ', " or empty (not quoted)
+
+			if (terminator.length === 0) {
+				terminator = ")";
+			}
+
+			let foundTerminator = false, endIndex = pattern.lastIndex - 1;
+
+			while (foundTerminator === false && endIndex + 1 <= maxIndex) {
+				endIndex = css.indexOf(terminator, endIndex + 1);
+
+				// endIndex == 0 doesn't really apply here
+				if ((endIndex > 0) && (css.charAt(endIndex - 1) !== "\\")) {
+					foundTerminator = true;
+					if (")" != terminator) {
+						endIndex = css.indexOf(")", endIndex);
 					}
-					if (ruleInfo.matchedSelectors.size < ruleInfo.selectorsText.length) {
-						cssRule.selectorText = ruleInfo.selectorsText.filter(selector => ruleInfo.matchedSelectors.has(selector) || (testFilterSelector(selector) && doc.querySelector(getFilteredSelector(selector)))).join(",");
+				}
+			}
+
+			// Enough searching, start moving stuff over to the buffer
+			sb.push(css.substring(appendIndex, match.index));
+
+			if (foundTerminator) {
+
+				let token = css.substring(startIndex, endIndex).replace(/(^\s*|\s*$)/g, "");
+				if (token.slice(0, 19) !== ___PRESERVED_TOKEN_) {
+
+					if (terminator === "'" || terminator === "\"") {
+						token = token.slice(1, -1);
+					} else if (terminator === ")") {
+						terminator = "";
 					}
+
+					const url = terminator + token + terminator;
+
+					preservedTokens.push(url);
+
+					const preserver = "url(" + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___)";
+					sb.push(preserver);
+
 				} else {
-					if (!IGNORED_SELECTORS.includes(cssRule.selectorText.toLowerCase().trim()) && (!testFilterSelector(cssRule.selectorText) || !doc.querySelector(getFilteredSelector(cssRule.selectorText)))) {
-						const parent = cssRule.parentRule || cssRule.parentStyleSheet;
-						let indexRule = 0;
-						while (cssRule != parent.cssRules[indexRule] && indexRule < parent.cssRules.length) {
-							indexRule++;
-						}
-						if (cssRule == parent.cssRules[indexRule]) {
-							parent.deleteRule(indexRule);
-						}
-					}
+					sb.push(`url(${token})`);
 				}
+
+				appendIndex = endIndex + 1;
+
+			} else {
+				// No end terminator found, re-add the whole match. Should we throw/warn here?
+				sb.push(css.substring(match.index, pattern.lastIndex));
+				appendIndex = pattern.lastIndex;
 			}
-		});
-		stats.discarded -= cssRules.length;
+		}
+
+		sb.push(css.substring(appendIndex));
+
+		return sb.join("");
 	}
 
-	function processStyle(doc, cssStyle, mediaInfo) {
-		const styleInfo = mediaInfo.styles.get(cssStyle);
-		if (styleInfo) {
-			const stylesInfo = parseCss.parseAListOfDeclarations(cssStyle.cssText);
-			const unusedStyles = stylesInfo.filter(style => !styleInfo.style.get(style.name));
-			if (unusedStyles.length) {
-				unusedStyles.forEach(style => cssStyle.removeProperty(style.name));
+	/**
+	 * extractDataUrls replaces all data urls with tokens before we start
+	 * compressing, to avoid performance issues running some of the subsequent
+	 * regexes against large strings chunks.
+	 *
+	 * @param {string} css - CSS content
+	 * @param {string[]} preservedTokens - Global array of tokens to preserve
+	 *
+	 * @return {string} Processed CSS
+	 */
+
+	function extractDataUrls(css, preservedTokens) {
+
+		// Leave data urls alone to increase parse performance.
+		const pattern = /url\(\s*(["']?)data:/g;
+		const maxIndex = css.length - 1;
+		const sb = [];
+
+		let appendIndex = 0, match;
+
+		// Since we need to account for non-base64 data urls, we need to handle
+		// ' and ) being part of the data string. Hence switching to indexOf,
+		// to determine whether or not we have matching string terminators and
+		// handling sb appends directly, instead of using matcher.append* methods.
+
+		while ((match = pattern.exec(css)) !== null) {
+
+			const startIndex = match.index + 4;  // 'url('.length()
+			let terminator = match[1];         // ', " or empty (not quoted)
+
+			if (terminator.length === 0) {
+				terminator = ")";
+			}
+
+			let foundTerminator = false, endIndex = pattern.lastIndex - 1;
+
+			while (foundTerminator === false && endIndex + 1 <= maxIndex) {
+				endIndex = css.indexOf(terminator, endIndex + 1);
+
+				// endIndex == 0 doesn't really apply here
+				if ((endIndex > 0) && (css.charAt(endIndex - 1) !== "\\")) {
+					foundTerminator = true;
+					if (")" != terminator) {
+						endIndex = css.indexOf(")", endIndex);
+					}
+				}
+			}
+
+			// Enough searching, start moving stuff over to the buffer
+			sb.push(css.substring(appendIndex, match.index));
+
+			if (foundTerminator) {
+
+				let token = css.substring(startIndex, endIndex);
+				const parts = token.split(",");
+				if (parts.length > 1 && parts[0].slice(-7) == ";base64") {
+					token = token.replace(/\s+/g, "");
+				} else {
+					token = token.replace(/\n/g, " ");
+					token = token.replace(/\s+/g, " ");
+					token = token.replace(/(^\s+|\s+$)/g, "");
+				}
+
+				preservedTokens.push(token);
+
+				const preserver = "url(" + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___)";
+				sb.push(preserver);
+
+				appendIndex = endIndex + 1;
+			} else {
+				// No end terminator found, re-add the whole match. Should we throw/warn here?
+				sb.push(css.substring(match.index, pattern.lastIndex));
+				appendIndex = pattern.lastIndex;
 			}
 		}
+
+		sb.push(css.substring(appendIndex));
+
+		return sb.join("");
 	}
 
-	function serializeRules(rules) {
-		let sheetContent = "";
-		Array.from(rules).forEach(rule => {
-			if (rule.media) {
-				sheetContent += "@media " + Array.from(rule.media).join(",") + "{";
-				sheetContent += serializeRules(rule.cssRules);
-				sheetContent += "}";
+	/**
+	 * compressHexColors compresses hex color values of the form #AABBCC to #ABC.
+	 *
+	 * DOES NOT compress CSS ID selectors which match the above pattern (which would
+	 * break things), like #AddressForm { ... }
+	 *
+	 * DOES NOT compress IE filters, which have hex color values (which would break
+	 * things), like chroma(color='#FFFFFF');
+	 *
+	 * DOES NOT compress invalid hex values, like background-color: #aabbccdd
+	 *
+	 * @param {string} css - CSS content
+	 *
+	 * @return {string} Processed CSS
+	 */
+
+	function compressHexColors(css) {
+
+		// Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters)
+
+		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;
+		const sb = [];
+
+		let index = 0, match;
+
+		while ((match = pattern.exec(css)) !== null) {
+
+			sb.push(css.substring(index, match.index));
+
+			const isFilter = match[1];
+
+			if (isFilter) {
+				// Restore, maintain case, otherwise filter will break
+				sb.push(match[1] + "#" + (match[2] + match[3] + match[4] + match[5] + match[6] + match[7]));
 			} else {
-				sheetContent += rule.cssText;
+				if (match[2].toLowerCase() == match[3].toLowerCase() &&
+					match[4].toLowerCase() == match[5].toLowerCase() &&
+					match[6].toLowerCase() == match[7].toLowerCase()) {
+
+					// Compress.
+					sb.push("#" + (match[3] + match[5] + match[7]).toLowerCase());
+				} else {
+					// Non compressible color, restore but lower case.
+					sb.push("#" + (match[2] + match[3] + match[4] + match[5] + match[6] + match[7]).toLowerCase());
+				}
 			}
-		});
-		return sheetContent;
+
+			index = pattern.lastIndex = pattern.lastIndex - match[8].length;
+		}
+
+		sb.push(css.substring(index));
+
+		return sb.join("");
 	}
 
-	function testFilterSelector(selector) {
-		return REMOVED_PSEUDO_CLASSES.find(pseudoClass => selector.includes(":" + pseudoClass)) || REMOVED_PSEUDO_ELEMENTS.find(pseudoElement => selector.includes("::" + pseudoElement));
+	/** keyframes preserves 0 followed by unit in keyframes steps
+	 *
+	 * @param {string} content - CSS content
+	 * @param {string[]} preservedTokens - Global array of tokens to preserve
+	 *
+	 * @return {string} Processed CSS
+	 */
+
+	function keyframes(content, preservedTokens) {
+
+		const pattern = /@[a-z0-9-_]*keyframes\s+[a-z0-9-_]+\s*{/gi;
+
+		let index = 0, buffer;
+
+		const preserve = (part, i) => {
+			part = part.replace(/(^\s|\s$)/g, "");
+			if (part.charAt(0) === "0") {
+				preservedTokens.push(part);
+				buffer[i] = ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
+			}
+		};
+
+		while (true) { // eslint-disable-line no-constant-condition
+
+			let level = 0;
+			buffer = "";
+
+			let startIndex = content.slice(index).search(pattern);
+			if (startIndex < 0) {
+				break;
+			}
+
+			index += startIndex;
+			startIndex = index;
+
+			const len = content.length;
+			const buffers = [];
+
+			for (; index < len; ++index) {
+
+				const ch = content.charAt(index);
+
+				if (ch === "{") {
+
+					if (level === 0) {
+						buffers.push(buffer.replace(/(^\s|\s$)/g, ""));
+
+					} else if (level === 1) {
+
+						buffer = buffer.split(",");
+
+						buffer.forEach(preserve);
+
+						buffers.push(buffer.join(",").replace(/(^\s|\s$)/g, ""));
+					}
+
+					buffer = "";
+					level += 1;
+
+				} else if (ch === "}") {
+
+					if (level === 2) {
+						buffers.push("{" + buffer.replace(/(^\s|\s$)/g, "") + "}");
+						buffer = "";
+
+					} else if (level === 1) {
+						content = content.slice(0, startIndex) +
+							buffers.shift() + "{" +
+							buffers.join("") +
+							content.slice(index);
+						break;
+					}
+
+					level -= 1;
+				}
+
+				if (level < 0) {
+					break;
+
+				} else if (ch !== "{" && ch !== "}") {
+					buffer += ch;
+				}
+			}
+		}
+
+		return content;
+	}
+
+	/**
+	 * collectComments collects all comment blocks and return new content with comment placeholders
+	 *
+	 * @param {string} content - CSS content
+	 * @param {string[]} comments - Global array of extracted comments
+	 *
+	 * @return {string} Processed CSS
+	 */
+
+	function collectComments(content, comments) {
+
+		const table = [];
+
+		let from = 0, end;
+
+		while (true) { // eslint-disable-line no-constant-condition
+
+			const start = content.indexOf("/*", from);
+
+			if (start > -1) {
+
+				end = content.indexOf("*/", start + 2);
+
+				if (end > -1) {
+					comments.push(content.slice(start + 2, end));
+					table.push(content.slice(from, start));
+					table.push("/*___PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___*/");
+					from = end + 2;
+
+				} else {
+					// unterminated comment
+					end = -2;
+					break;
+				}
+
+			} else {
+				break;
+			}
+		}
+
+		table.push(content.slice(end + 2));
+
+		return table.join("");
 	}
 
-	function getFilteredSelector(selector) {
-		const selectors = cssWhat.parse(selector);
-		return cssWhat.stringify(selectors.map(selector => filterPseudoClasses(selector)));
+	/**
+	 * processString uglifies a CSS string
+	 *
+	 * @param {string} content - CSS string
+	 * @param {options} options - UglifyCSS options
+	 *
+	 * @return {string} Uglified result
+	 */
+
+	function processString(content = "", options = defaultOptions) {
+
+		const comments = [];
+		const preservedTokens = [];
+
+		let pattern;
+
+		content = extractDataUrls(content, preservedTokens);
+		content = convertRelativeUrls(content, options, preservedTokens);
+		content = collectComments(content, comments);
+
+		// preserve strings so their content doesn't get accidentally minified
+		pattern = /("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')/g;
+		content = content.replace(pattern, token => {
+			const quote = token.substring(0, 1);
+			token = token.slice(1, -1);
+			// maybe the string contains a comment-like substring or more? put'em back then
+			if (token.indexOf("___PRESERVE_CANDIDATE_COMMENT_") >= 0) {
+				for (let i = 0, len = comments.length; i < len; i += 1) {
+					token = token.replace("___PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments[i]);
+				}
+			}
+			// minify alpha opacity in filter strings
+			token = token.replace(/progid:DXImageTransform.Microsoft.Alpha\(Opacity=/gi, "alpha(opacity=");
+			preservedTokens.push(token);
+			return quote + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___" + quote;
+		});
+
+		// strings are safe, now wrestle the comments
+		for (let i = 0, len = comments.length; i < len; i += 1) {
+
+			const token = comments[i];
+			const placeholder = "___PRESERVE_CANDIDATE_COMMENT_" + i + "___";
+
+			// ! in the first position of the comment means preserve
+			// so push to the preserved tokens keeping the !
+			if (token.charAt(0) === "!") {
+				if (options.cuteComments) {
+					preservedTokens.push(token.substring(1).replace(/\r\n/g, "\n"));
+				} else if (options.uglyComments) {
+					preservedTokens.push(token.substring(1).replace(/[\r\n]/g, ""));
+				} else {
+					preservedTokens.push(token);
+				}
+				content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
+				continue;
+			}
+
+			// \ in the last position looks like hack for Mac/IE5
+			// shorten that to /*\*/ and the next one to /**/
+			if (token.charAt(token.length - 1) === "\\") {
+				preservedTokens.push("\\");
+				content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
+				i = i + 1; // attn: advancing the loop
+				preservedTokens.push("");
+				content = content.replace(
+					"___PRESERVE_CANDIDATE_COMMENT_" + i + "___",
+					___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___"
+				);
+				continue;
+			}
 
-		function filterPseudoClasses(selector) {
-			const tokens = selector.filter(token => {
-				if (token.data) {
-					if (Array.isArray(token.data)) {
-						token.data = token.data.map(selector => filterPseudoClasses(selector));
+			// keep empty comments after child selectors (IE7 hack)
+			// e.g. html >/**/ body
+			if (token.length === 0) {
+				const startIndex = content.indexOf(placeholder);
+				if (startIndex > 2) {
+					if (content.charAt(startIndex - 3) === ">") {
+						preservedTokens.push("");
+						content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
 					}
 				}
-				const test = ((token.type != "pseudo" || !REMOVED_PSEUDO_CLASSES.includes(token.name))
-					&& (token.type != "pseudo-element" || !REMOVED_PSEUDO_ELEMENTS.includes(token.name)));
-				return test;
-			});
-			let insertedTokens = 0;
-			tokens.forEach((token, index) => {
-				if (SEPARATOR_TYPES.includes(token.type)) {
-					if (!tokens[index - 1] || SEPARATOR_TYPES.includes(tokens[index - 1].type)) {
-						tokens.splice(index + insertedTokens, 0, { type: "universal" });
-						insertedTokens++;
+			}
+
+			// in all other cases kill the comment
+			content = content.replace(`/*${placeholder}*/`, "");
+		}
+
+		// parse simple @variables blocks and remove them
+		if (options.expandVars) {
+			const vars = {};
+			pattern = /@variables\s*\{\s*([^}]+)\s*\}/g;
+			content = content.replace(pattern, (_, f1) => {
+				pattern = /\s*([a-z0-9-]+)\s*:\s*([^;}]+)\s*/gi;
+				f1.replace(pattern, (_, f1, f2) => {
+					if (f1 && f2) {
+						vars[f1] = f2;
 					}
+					return "";
+				});
+				return "";
+			});
+
+			// replace var(x) with the value of x
+			pattern = /var\s*\(\s*([^)]+)\s*\)/g;
+			content = content.replace(pattern, (_, f1) => {
+				return vars[f1] || "none";
+			});
+		}
+
+		// normalize all whitespace strings to single spaces. Easier to work with that way.
+		content = content.replace(/\s+/g, " ");
+
+		// preserve formulas in calc() before removing spaces
+		pattern = /calc\(([^;}]*)\)/g;
+		content = content.replace(pattern, (_, f1) => {
+			preservedTokens.push(
+				"calc(" +
+				f1.replace(/(^\s*|\s*$)/g, "")
+					.replace(/\( /g, "(")
+					.replace(/ \)/g, ")") +
+				")"
+			);
+			return ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
+		});
+
+		// preserve matrix
+		pattern = /\s*filter:\s*progid:DXImageTransform.Microsoft.Matrix\(([^)]+)\);/g;
+		content = content.replace(pattern, (_, f1) => {
+			preservedTokens.push(f1);
+			return "filter:progid:DXImageTransform.Microsoft.Matrix(" + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___);";
+		});
+
+		// remove the spaces before the things that should not have spaces before them.
+		// but, be careful not to turn 'p :link {...}' into 'p:link{...}'
+		// swap out any pseudo-class colons with the token, and then swap back.
+		pattern = /(^|\})(([^{:])+:)+([^{]*{)/g;
+		content = content.replace(pattern, token => token.replace(/:/g, "___PSEUDOCLASSCOLON___"));
+
+		// remove spaces before the things that should not have spaces before them.
+		content = content.replace(/\s+([!{};:>+()\],])/g, "$1");
+
+		// restore spaces for !important
+		content = content.replace(/!important/g, " !important");
+
+		// bring back the colon
+		content = content.replace(/___PSEUDOCLASSCOLON___/g, ":");
+
+		// preserve 0 followed by a time unit for properties using time units
+		pattern = /\s*(animation|animation-delay|animation-duration|transition|transition-delay|transition-duration):\s*([^;}]+)/gi;
+		content = content.replace(pattern, (_, f1, f2) => {
+
+			f2 = f2.replace(/(^|\D)0?\.?0(m?s)/gi, (_, g1, g2) => {
+				preservedTokens.push("0" + g2);
+				return g1 + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
+			});
+
+			return f1 + ":" + f2;
+		});
+
+		// preserve unit for flex-basis within flex and flex-basis (ie10 bug)
+		pattern = /\s*(flex|flex-basis):\s*([^;}]+)/gi;
+		content = content.replace(pattern, (_, f1, f2) => {
+			let f2b = f2.split(/\s+/);
+			preservedTokens.push(f2b.pop());
+			f2b.push(___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
+			f2b = f2b.join(" ");
+			return `${f1}:${f2b}`;
+		});
+
+		// preserve 0% in hsl and hsla color definitions
+		content = content.replace(/(hsla?)\(([^)]+)\)/g, (_, f1, f2) => {
+			const f0 = [];
+			f2.split(",").forEach(part => {
+				part = part.replace(/(^\s+|\s+$)/g, "");
+				if (part === "0%") {
+					preservedTokens.push("0%");
+					f0.push(___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
+				} else {
+					f0.push(part);
 				}
 			});
-			if (!tokens.length || SEPARATOR_TYPES.includes(tokens[tokens.length - 1].type)) {
-				tokens.push({ type: "universal" });
+			return f1 + "(" + f0.join(",") + ")";
+		});
+
+		// preserve 0 followed by unit in keyframes steps (WIP)
+		content = keyframes(content, preservedTokens);
+
+		// retain space for special IE6 cases
+		content = content.replace(/:first-(line|letter)(\{|,)/gi, (_, f1, f2) => ":first-" + f1.toLowerCase() + " " + f2);
+
+		// newlines before and after the end of a preserved comment
+		if (options.cuteComments) {
+			content = content.replace(/\s*\/\*/g, "___PRESERVED_NEWLINE___/*");
+			content = content.replace(/\*\/\s*/g, "*/___PRESERVED_NEWLINE___");
+			// no space after the end of a preserved comment
+		} else {
+			content = content.replace(/\*\/\s*/g, "*/");
+		}
+
+		// If there are multiple @charset directives, push them to the top of the file.
+		pattern = /^(.*)(@charset)( "[^"]*";)/gi;
+		content = content.replace(pattern, (_, f1, f2, f3) => f2.toLowerCase() + f3 + f1);
+
+		// When all @charset are at the top, remove the second and after (as they are completely ignored).
+		pattern = /^((\s*)(@charset)( [^;]+;\s*))+/gi;
+		content = content.replace(pattern, (_, __, f2, f3, f4) => f2 + f3.toLowerCase() + f4);
+
+		// lowercase some popular @directives (@charset is done right above)
+		pattern = /@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)/gi;
+		content = content.replace(pattern, (_, f1) => "@" + f1.toLowerCase());
+
+		// lowercase some more common pseudo-elements
+		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;
+		content = content.replace(pattern, (_, f1) => ":" + f1.toLowerCase());
+
+		// if there is a @charset, then only allow one, and push to the top of the file.
+		content = content.replace(/^(.*)(@charset "[^"]*";)/g, "$2$1");
+		content = content.replace(/^(\s*@charset [^;]+;\s*)+/g, "$1");
+
+		// lowercase some more common functions
+		pattern = /:(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?any)\(/gi;
+		content = content.replace(pattern, (_, f1) => ":" + f1.toLowerCase() + "(");
+
+		// lower case some common function that can be values
+		// NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us right after this
+		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;
+		content = content.replace(pattern, (_, f1, f2) => f1 + f2.toLowerCase());
+
+		// put the space back in some cases, to support stuff like
+		// @media screen and (-webkit-min-device-pixel-ratio:0){
+		content = content.replace(/\band\(/gi, "and (");
+		content = content.replace(/([^:])not\(/gi, "$1not (");
+
+		// remove the spaces after the things that should not have spaces after them.
+		content = content.replace(/([!{}:;>+([,])\s+/g, "$1");
+
+		// remove unnecessary semicolons
+		content = content.replace(/;+\}/g, "}");
+
+		// replace 0(px,em,%) with 0.
+		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|%)(?![a-z0-9])/gi, "$10");
+
+		// Replace x.0(px,em,%) with x(px,em,%).
+		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");
+
+		// replace 0 0 0 0; with 0.
+		content = content.replace(/:0 0 0 0(;|\})/g, ":0$1");
+		content = content.replace(/:0 0 0(;|\})/g, ":0$1");
+		content = content.replace(/:0 0(;|\})/g, ":0$1");
+
+		// replace background-position:0; with background-position:0 0;
+		// same for transform-origin and box-shadow
+		pattern = /(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin|box-shadow):0(;|\})/gi;
+		content = content.replace(pattern, (_, f1, f2) => f1.toLowerCase() + ":0 0" + f2);
+
+		// replace 0.6 to .6, but only when preceded by : or a white-space
+		content = content.replace(/(:|\s)0+\.(\d+)/g, "$1.$2");
+
+		// shorten colors from rgb(51,102,153) to #336699
+		// this makes it more likely that it'll get further compressed in the next step.
+		pattern = /rgb\s*\(\s*([0-9,\s]+)\s*\)/gi;
+		content = content.replace(pattern, (_, f1) => {
+			let rgbcolors = f1.split(","), hexcolor = "#";
+			for (let i = 0; i < rgbcolors.length; i += 1) {
+				let val = parseInt(rgbcolors[i], 10);
+				if (val < 16) {
+					hexcolor += "0";
+				}
+				if (val > 255) {
+					val = 255;
+				}
+				hexcolor += val.toString(16);
+			}
+			return hexcolor;
+		});
+
+		// Shorten colors from #AABBCC to #ABC.
+		content = compressHexColors(content);
+
+		// Replace #f00 -> red
+		content = content.replace(/(:|\s)(#f00)(;|})/g, "$1red$3");
+
+		// Replace other short color keywords
+		content = content.replace(/(:|\s)(#000080)(;|})/g, "$1navy$3");
+		content = content.replace(/(:|\s)(#808080)(;|})/g, "$1gray$3");
+		content = content.replace(/(:|\s)(#808000)(;|})/g, "$1olive$3");
+		content = content.replace(/(:|\s)(#800080)(;|})/g, "$1purple$3");
+		content = content.replace(/(:|\s)(#c0c0c0)(;|})/g, "$1silver$3");
+		content = content.replace(/(:|\s)(#008080)(;|})/g, "$1teal$3");
+		content = content.replace(/(:|\s)(#ffa500)(;|})/g, "$1orange$3");
+		content = content.replace(/(:|\s)(#800000)(;|})/g, "$1maroon$3");
+
+		// border: none -> border:0
+		pattern = /(border|border-top|border-right|border-bottom|border-left|outline|background):none(;|\})/gi;
+		content = content.replace(pattern, (_, f1, f2) => f1.toLowerCase() + ":0" + f2);
+
+		// shorter opacity IE filter
+		content = content.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity=");
+
+		// Find a fraction that is used for Opera's -o-device-pixel-ratio query
+		// Add token to add the '\' back in later
+		content = content.replace(/\(([-A-Za-z]+):([0-9]+)\/([0-9]+)\)/g, "($1:$2___QUERY_FRACTION___$3)");
+
+		// remove empty rules.
+		content = content.replace(/[^};{/]+\{\}/g, "");
+
+		// Add '\' back to fix Opera -o-device-pixel-ratio query
+		content = content.replace(/___QUERY_FRACTION___/g, "/");
+
+		// some source control tools don't like it when files containing lines longer
+		// than, say 8000 characters, are checked in. The linebreak option is used in
+		// that case to split long lines after a specific column.
+		if (options.maxLineLen > 0) {
+			const lines = [];
+			let line = [];
+			for (let i = 0, len = content.length; i < len; i += 1) {
+				const ch = content.charAt(i);
+				line.push(ch);
+				if (ch === "}" && line.length > options.maxLineLen) {
+					lines.push(line.join(""));
+					line = [];
+				}
+			}
+			if (line.length) {
+				lines.push(line.join(""));
 			}
-			return tokens;
+
+			content = lines.join("\n");
+		}
+
+		// replace multiple semi-colons in a row by a single one
+		// see SF bug #1980989
+		content = content.replace(/;;+/g, ";");
+
+		// trim the final string (for any leading or trailing white spaces)
+		content = content.replace(/(^\s*|\s*$)/g, "");
+
+		// restore preserved tokens
+		for (let i = preservedTokens.length - 1; i >= 0; i--) {
+			content = content.replace(___PRESERVED_TOKEN_ + i + "___", preservedTokens[i], "g");
 		}
+
+		// restore preserved newlines
+		content = content.replace(/___PRESERVED_NEWLINE___/g, "\n");
+
+		// return
+		return content;
 	}
 
+	return {
+		defaultOptions,
+		processString
+	};
+
 })();

+ 148 - 0
lib/single-file/css-rules-minifier.js

@@ -0,0 +1,148 @@
+/*
+ * Copyright 2018 Gildas Lormeau
+ * contact : gildas.lormeau <at> gmail.com
+ * 
+ * This file is part of SingleFile.
+ *
+ *   SingleFile is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU Lesser General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   SingleFile is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU Lesser General Public License for more details.
+ *
+ *   You should have received a copy of the GNU Lesser General Public License
+ *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* global CSSRule, cssWhat, parseCss, RulesMatcher */
+
+this.cssMinifier = this.cssMinifier || (() => {
+
+	const SEPARATOR_TYPES = ["descendant", "child", "sibling", "adjacent"];
+	const REMOVED_PSEUDO_CLASSES = ["focus", "focus-within", "hover", "link", "visited", "active"];
+	const REMOVED_PSEUDO_ELEMENTS = ["after", "before", "first-line", "first-letter", "placeholder", "-webkit-input-placeholder", "selection", "marker", "cue", "-webkit-progress-bar", "-webkit-progress-value", "-webkit-inner-spin-button", "-webkit-outer-spin-button", "-webkit-search-cancel-button", "-webkit-search-cancel-button"];
+	const IGNORED_SELECTORS = ["::-webkit-scrollbar", "::-webkit-scrollbar-button", "::-webkit-scrollbar-thumb", "::-webkit-scrollbar-track", "::-webkit-scrollbar-track-piece", "::-webkit-scrollbar-corner", "::-webkit-resizer"];
+
+	return {
+		process: doc => {
+			const rulesMatcher = RulesMatcher.create(doc);
+			const mediaAllInfo = rulesMatcher.getAllMatchedRules();
+			const stats = { processed: 0, discarded: 0 };
+			doc.querySelectorAll("style").forEach(styleElement => {
+				if (styleElement.sheet) {
+					let mediaInfo;
+					if (styleElement.media && styleElement.media != "all") {
+						mediaInfo = mediaAllInfo.medias.get(styleElement.media);
+					} else {
+						mediaInfo = mediaAllInfo;
+					}
+					processRules(doc, styleElement.sheet.cssRules, mediaInfo, stats);
+					styleElement.textContent = serializeRules(styleElement.sheet.cssRules);
+				}
+			});
+			doc.querySelectorAll("[style]").forEach(element => {
+				processStyle(doc, element.style, mediaAllInfo);
+			});
+			return stats;
+		}
+	};
+
+	function processRules(doc, cssRules, mediaInfo, stats) {
+		stats.processed += cssRules.length;
+		stats.discarded += cssRules.length;
+		Array.from(cssRules).forEach(cssRule => {
+			if (cssRule.type == CSSRule.MEDIA_RULE) {
+				processRules(doc, cssRule.cssRules, mediaInfo.medias.get(cssRule.media), stats);
+			} else if (cssRule.type == CSSRule.STYLE_RULE) {
+				const ruleInfo = mediaInfo.rules.get(cssRule);
+				if (ruleInfo) {
+					const stylesInfo = parseCss.parseAListOfDeclarations(cssRule.style.cssText);
+					const unusedStyles = stylesInfo.filter(style => !ruleInfo.style.get(style.name));
+					if (unusedStyles.length) {
+						unusedStyles.forEach(style => cssRule.style.removeProperty(style.name));
+					}
+					if (ruleInfo.matchedSelectors.size < ruleInfo.selectorsText.length) {
+						cssRule.selectorText = ruleInfo.selectorsText.filter(selector => ruleInfo.matchedSelectors.has(selector) || (testFilterSelector(selector) && doc.querySelector(getFilteredSelector(selector)))).join(",");
+					}
+				} else {
+					if (!IGNORED_SELECTORS.includes(cssRule.selectorText.toLowerCase().trim()) && (!testFilterSelector(cssRule.selectorText) || !doc.querySelector(getFilteredSelector(cssRule.selectorText)))) {
+						const parent = cssRule.parentRule || cssRule.parentStyleSheet;
+						let indexRule = 0;
+						while (cssRule != parent.cssRules[indexRule] && indexRule < parent.cssRules.length) {
+							indexRule++;
+						}
+						if (cssRule == parent.cssRules[indexRule]) {
+							parent.deleteRule(indexRule);
+						}
+					}
+				}
+			}
+		});
+		stats.discarded -= cssRules.length;
+	}
+
+	function processStyle(doc, cssStyle, mediaInfo) {
+		const styleInfo = mediaInfo.styles.get(cssStyle);
+		if (styleInfo) {
+			const stylesInfo = parseCss.parseAListOfDeclarations(cssStyle.cssText);
+			const unusedStyles = stylesInfo.filter(style => !styleInfo.style.get(style.name));
+			if (unusedStyles.length) {
+				unusedStyles.forEach(style => cssStyle.removeProperty(style.name));
+			}
+		}
+	}
+
+	function serializeRules(rules) {
+		let sheetContent = "";
+		Array.from(rules).forEach(rule => {
+			if (rule.media) {
+				sheetContent += "@media " + Array.from(rule.media).join(",") + "{";
+				sheetContent += serializeRules(rule.cssRules);
+				sheetContent += "}";
+			} else {
+				sheetContent += rule.cssText;
+			}
+		});
+		return sheetContent;
+	}
+
+	function testFilterSelector(selector) {
+		return REMOVED_PSEUDO_CLASSES.find(pseudoClass => selector.includes(":" + pseudoClass)) || REMOVED_PSEUDO_ELEMENTS.find(pseudoElement => selector.includes("::" + pseudoElement));
+	}
+
+	function getFilteredSelector(selector) {
+		const selectors = cssWhat.parse(selector);
+		return cssWhat.stringify(selectors.map(selector => filterPseudoClasses(selector)));
+
+		function filterPseudoClasses(selector) {
+			const tokens = selector.filter(token => {
+				if (token.data) {
+					if (Array.isArray(token.data)) {
+						token.data = token.data.map(selector => filterPseudoClasses(selector));
+					}
+				}
+				const test = ((token.type != "pseudo" || !REMOVED_PSEUDO_CLASSES.includes(token.name))
+					&& (token.type != "pseudo-element" || !REMOVED_PSEUDO_ELEMENTS.includes(token.name)));
+				return test;
+			});
+			let insertedTokens = 0;
+			tokens.forEach((token, index) => {
+				if (SEPARATOR_TYPES.includes(token.type)) {
+					if (!tokens[index - 1] || SEPARATOR_TYPES.includes(tokens[index - 1].type)) {
+						tokens.splice(index + insertedTokens, 0, { type: "universal" });
+						insertedTokens++;
+					}
+				}
+			});
+			if (!tokens.length || SEPARATOR_TYPES.includes(tokens[tokens.length - 1].type)) {
+				tokens.push({ type: "universal" });
+			}
+			return tokens;
+		}
+	}
+
+})();

+ 0 - 0
lib/single-file/css-what.js → lib/single-file/css-selector-parser.js


+ 0 - 774
lib/single-file/css-uglifycss.js

@@ -1,774 +0,0 @@
-/**
- * UglifyCSS
- * Port of YUI CSS Compressor to Vanilla JS
- * Author: Gildas Lormeau
- * MIT licenced
- */
-
-/**
- * UglifyCSS
- * Port of YUI CSS Compressor to NodeJS
- * Author: Franck Marcia - https://github.com/fmarcia
- * MIT licenced
- */
-
-/**
- * cssmin.js
- * Author: Stoyan Stefanov - http://phpied.com/
- * This is a JavaScript port of the CSS minification tool
- * distributed with YUICompressor, itself a port
- * of the cssmin utility by Isaac Schlueter - http://foohack.com/
- * Permission is hereby granted to use the JavaScript version under the same
- * conditions as the YUICompressor (original YUICompressor note below).
- */
-
-/**
- * YUI Compressor
- * http://developer.yahoo.com/yui/compressor/
- * Author: Julien Lecomte - http://www.julienlecomte.net/
- * Copyright (c) 2011 Yahoo! Inc. All rights reserved.
- * The copyrights embodied in the content of this file are licensed
- * by Yahoo! Inc. under the BSD (revised) open source license.
- */
-
-this.uglifycss = this.uglifycss || (() => {
-
-	/**
-	 * @type {string} - placeholder prefix
-	 */
-
-	const ___PRESERVED_TOKEN_ = "___PRESERVED_TOKEN_";
-
-	/**
-	 * @typedef {object} options - UglifyCSS options
-	 * @property {number} [maxLineLen=0] - Maximum line length of uglified CSS
-	 * @property {boolean} [expandVars=false] - Expand variables
-	 * @property {boolean} [uglyComments=false] - Removes newlines within preserved comments
-	 * @property {boolean} [cuteComments=false] - Preserves newlines within and around preserved comments
-	 * @property {boolean} [debug=false] - Prints full error stack on error
-	 * @property {string} [output=''] - Output file name
-	 */
-
-	/**
-	 * @type {options} - UglifyCSS options
-	 */
-
-	const defaultOptions = {
-		maxLineLen: 0,
-		expandVars: false,
-		uglyComments: false,
-		cuteComments: false,
-		debug: false,
-		output: ""
-	};
-
-	/**
-	 * convertRelativeUrls converts relative urls and replaces them with tokens
-	 * before we start compressing. It must be called *after* extractDataUrls
-	 *
-	 * @param {string} css - CSS content
-	 * @param {options} options - UglifyCSS Options
-	 * @param {string[]} preservedTokens - Global array of tokens to preserve
-	 *
-	 * @return {string} Processed css
-	 */
-
-	function convertRelativeUrls(css, options, preservedTokens) {
-
-		const pattern = /(url\s*\()\s*(["']?)/g;
-		const maxIndex = css.length - 1;
-		const sb = [];
-
-		let appendIndex = 0, match;
-
-		// Since we need to account for non-base64 data urls, we need to handle
-		// ' and ) being part of the data string. Hence switching to indexOf,
-		// to determine whether or not we have matching string terminators and
-		// handling sb appends directly, instead of using matcher.append* methods.
-
-		while ((match = pattern.exec(css)) !== null) {
-
-			const startIndex = match.index + match[1].length;  // 'url('.length()
-			let terminator = match[2];         // ', " or empty (not quoted)
-
-			if (terminator.length === 0) {
-				terminator = ")";
-			}
-
-			let foundTerminator = false, endIndex = pattern.lastIndex - 1;
-
-			while (foundTerminator === false && endIndex + 1 <= maxIndex) {
-				endIndex = css.indexOf(terminator, endIndex + 1);
-
-				// endIndex == 0 doesn't really apply here
-				if ((endIndex > 0) && (css.charAt(endIndex - 1) !== "\\")) {
-					foundTerminator = true;
-					if (")" != terminator) {
-						endIndex = css.indexOf(")", endIndex);
-					}
-				}
-			}
-
-			// Enough searching, start moving stuff over to the buffer
-			sb.push(css.substring(appendIndex, match.index));
-
-			if (foundTerminator) {
-
-				let token = css.substring(startIndex, endIndex).replace(/(^\s*|\s*$)/g, "");
-				if (token.slice(0, 19) !== ___PRESERVED_TOKEN_) {
-
-					if (terminator === "'" || terminator === "\"") {
-						token = token.slice(1, -1);
-					} else if (terminator === ")") {
-						terminator = "";
-					}
-
-					const url = terminator + token + terminator;
-
-					preservedTokens.push(url);
-
-					const preserver = "url(" + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___)";
-					sb.push(preserver);
-
-				} else {
-					sb.push(`url(${token})`);
-				}
-
-				appendIndex = endIndex + 1;
-
-			} else {
-				// No end terminator found, re-add the whole match. Should we throw/warn here?
-				sb.push(css.substring(match.index, pattern.lastIndex));
-				appendIndex = pattern.lastIndex;
-			}
-		}
-
-		sb.push(css.substring(appendIndex));
-
-		return sb.join("");
-	}
-
-	/**
-	 * extractDataUrls replaces all data urls with tokens before we start
-	 * compressing, to avoid performance issues running some of the subsequent
-	 * regexes against large strings chunks.
-	 *
-	 * @param {string} css - CSS content
-	 * @param {string[]} preservedTokens - Global array of tokens to preserve
-	 *
-	 * @return {string} Processed CSS
-	 */
-
-	function extractDataUrls(css, preservedTokens) {
-
-		// Leave data urls alone to increase parse performance.
-		const pattern = /url\(\s*(["']?)data:/g;
-		const maxIndex = css.length - 1;
-		const sb = [];
-
-		let appendIndex = 0, match;
-
-		// Since we need to account for non-base64 data urls, we need to handle
-		// ' and ) being part of the data string. Hence switching to indexOf,
-		// to determine whether or not we have matching string terminators and
-		// handling sb appends directly, instead of using matcher.append* methods.
-
-		while ((match = pattern.exec(css)) !== null) {
-
-			const startIndex = match.index + 4;  // 'url('.length()
-			let terminator = match[1];         // ', " or empty (not quoted)
-
-			if (terminator.length === 0) {
-				terminator = ")";
-			}
-
-			let foundTerminator = false, endIndex = pattern.lastIndex - 1;
-
-			while (foundTerminator === false && endIndex + 1 <= maxIndex) {
-				endIndex = css.indexOf(terminator, endIndex + 1);
-
-				// endIndex == 0 doesn't really apply here
-				if ((endIndex > 0) && (css.charAt(endIndex - 1) !== "\\")) {
-					foundTerminator = true;
-					if (")" != terminator) {
-						endIndex = css.indexOf(")", endIndex);
-					}
-				}
-			}
-
-			// Enough searching, start moving stuff over to the buffer
-			sb.push(css.substring(appendIndex, match.index));
-
-			if (foundTerminator) {
-
-				let token = css.substring(startIndex, endIndex);
-				const parts = token.split(",");
-				if (parts.length > 1 && parts[0].slice(-7) == ";base64") {
-					token = token.replace(/\s+/g, "");
-				} else {
-					token = token.replace(/\n/g, " ");
-					token = token.replace(/\s+/g, " ");
-					token = token.replace(/(^\s+|\s+$)/g, "");
-				}
-
-				preservedTokens.push(token);
-
-				const preserver = "url(" + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___)";
-				sb.push(preserver);
-
-				appendIndex = endIndex + 1;
-			} else {
-				// No end terminator found, re-add the whole match. Should we throw/warn here?
-				sb.push(css.substring(match.index, pattern.lastIndex));
-				appendIndex = pattern.lastIndex;
-			}
-		}
-
-		sb.push(css.substring(appendIndex));
-
-		return sb.join("");
-	}
-
-	/**
-	 * compressHexColors compresses hex color values of the form #AABBCC to #ABC.
-	 *
-	 * DOES NOT compress CSS ID selectors which match the above pattern (which would
-	 * break things), like #AddressForm { ... }
-	 *
-	 * DOES NOT compress IE filters, which have hex color values (which would break
-	 * things), like chroma(color='#FFFFFF');
-	 *
-	 * DOES NOT compress invalid hex values, like background-color: #aabbccdd
-	 *
-	 * @param {string} css - CSS content
-	 *
-	 * @return {string} Processed CSS
-	 */
-
-	function compressHexColors(css) {
-
-		// Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters)
-
-		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;
-		const sb = [];
-
-		let index = 0, match;
-
-		while ((match = pattern.exec(css)) !== null) {
-
-			sb.push(css.substring(index, match.index));
-
-			const isFilter = match[1];
-
-			if (isFilter) {
-				// Restore, maintain case, otherwise filter will break
-				sb.push(match[1] + "#" + (match[2] + match[3] + match[4] + match[5] + match[6] + match[7]));
-			} else {
-				if (match[2].toLowerCase() == match[3].toLowerCase() &&
-					match[4].toLowerCase() == match[5].toLowerCase() &&
-					match[6].toLowerCase() == match[7].toLowerCase()) {
-
-					// Compress.
-					sb.push("#" + (match[3] + match[5] + match[7]).toLowerCase());
-				} else {
-					// Non compressible color, restore but lower case.
-					sb.push("#" + (match[2] + match[3] + match[4] + match[5] + match[6] + match[7]).toLowerCase());
-				}
-			}
-
-			index = pattern.lastIndex = pattern.lastIndex - match[8].length;
-		}
-
-		sb.push(css.substring(index));
-
-		return sb.join("");
-	}
-
-	/** keyframes preserves 0 followed by unit in keyframes steps
-	 *
-	 * @param {string} content - CSS content
-	 * @param {string[]} preservedTokens - Global array of tokens to preserve
-	 *
-	 * @return {string} Processed CSS
-	 */
-
-	function keyframes(content, preservedTokens) {
-
-		const pattern = /@[a-z0-9-_]*keyframes\s+[a-z0-9-_]+\s*{/gi;
-
-		let index = 0, buffer;
-
-		const preserve = (part, i) => {
-			part = part.replace(/(^\s|\s$)/g, "");
-			if (part.charAt(0) === "0") {
-				preservedTokens.push(part);
-				buffer[i] = ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
-			}
-		};
-
-		while (true) { // eslint-disable-line no-constant-condition
-
-			let level = 0;
-			buffer = "";
-
-			let startIndex = content.slice(index).search(pattern);
-			if (startIndex < 0) {
-				break;
-			}
-
-			index += startIndex;
-			startIndex = index;
-
-			const len = content.length;
-			const buffers = [];
-
-			for (; index < len; ++index) {
-
-				const ch = content.charAt(index);
-
-				if (ch === "{") {
-
-					if (level === 0) {
-						buffers.push(buffer.replace(/(^\s|\s$)/g, ""));
-
-					} else if (level === 1) {
-
-						buffer = buffer.split(",");
-
-						buffer.forEach(preserve);
-
-						buffers.push(buffer.join(",").replace(/(^\s|\s$)/g, ""));
-					}
-
-					buffer = "";
-					level += 1;
-
-				} else if (ch === "}") {
-
-					if (level === 2) {
-						buffers.push("{" + buffer.replace(/(^\s|\s$)/g, "") + "}");
-						buffer = "";
-
-					} else if (level === 1) {
-						content = content.slice(0, startIndex) +
-							buffers.shift() + "{" +
-							buffers.join("") +
-							content.slice(index);
-						break;
-					}
-
-					level -= 1;
-				}
-
-				if (level < 0) {
-					break;
-
-				} else if (ch !== "{" && ch !== "}") {
-					buffer += ch;
-				}
-			}
-		}
-
-		return content;
-	}
-
-	/**
-	 * collectComments collects all comment blocks and return new content with comment placeholders
-	 *
-	 * @param {string} content - CSS content
-	 * @param {string[]} comments - Global array of extracted comments
-	 *
-	 * @return {string} Processed CSS
-	 */
-
-	function collectComments(content, comments) {
-
-		const table = [];
-
-		let from = 0, end;
-
-		while (true) { // eslint-disable-line no-constant-condition
-
-			const start = content.indexOf("/*", from);
-
-			if (start > -1) {
-
-				end = content.indexOf("*/", start + 2);
-
-				if (end > -1) {
-					comments.push(content.slice(start + 2, end));
-					table.push(content.slice(from, start));
-					table.push("/*___PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___*/");
-					from = end + 2;
-
-				} else {
-					// unterminated comment
-					end = -2;
-					break;
-				}
-
-			} else {
-				break;
-			}
-		}
-
-		table.push(content.slice(end + 2));
-
-		return table.join("");
-	}
-
-	/**
-	 * processString uglifies a CSS string
-	 *
-	 * @param {string} content - CSS string
-	 * @param {options} options - UglifyCSS options
-	 *
-	 * @return {string} Uglified result
-	 */
-
-	function processString(content = "", options = defaultOptions) {
-
-		const comments = [];
-		const preservedTokens = [];
-
-		let pattern;
-
-		content = extractDataUrls(content, preservedTokens);
-		content = convertRelativeUrls(content, options, preservedTokens);
-		content = collectComments(content, comments);
-
-		// preserve strings so their content doesn't get accidentally minified
-		pattern = /("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')/g;
-		content = content.replace(pattern, token => {
-			const quote = token.substring(0, 1);
-			token = token.slice(1, -1);
-			// maybe the string contains a comment-like substring or more? put'em back then
-			if (token.indexOf("___PRESERVE_CANDIDATE_COMMENT_") >= 0) {
-				for (let i = 0, len = comments.length; i < len; i += 1) {
-					token = token.replace("___PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments[i]);
-				}
-			}
-			// minify alpha opacity in filter strings
-			token = token.replace(/progid:DXImageTransform.Microsoft.Alpha\(Opacity=/gi, "alpha(opacity=");
-			preservedTokens.push(token);
-			return quote + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___" + quote;
-		});
-
-		// strings are safe, now wrestle the comments
-		for (let i = 0, len = comments.length; i < len; i += 1) {
-
-			const token = comments[i];
-			const placeholder = "___PRESERVE_CANDIDATE_COMMENT_" + i + "___";
-
-			// ! in the first position of the comment means preserve
-			// so push to the preserved tokens keeping the !
-			if (token.charAt(0) === "!") {
-				if (options.cuteComments) {
-					preservedTokens.push(token.substring(1).replace(/\r\n/g, "\n"));
-				} else if (options.uglyComments) {
-					preservedTokens.push(token.substring(1).replace(/[\r\n]/g, ""));
-				} else {
-					preservedTokens.push(token);
-				}
-				content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
-				continue;
-			}
-
-			// \ in the last position looks like hack for Mac/IE5
-			// shorten that to /*\*/ and the next one to /**/
-			if (token.charAt(token.length - 1) === "\\") {
-				preservedTokens.push("\\");
-				content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
-				i = i + 1; // attn: advancing the loop
-				preservedTokens.push("");
-				content = content.replace(
-					"___PRESERVE_CANDIDATE_COMMENT_" + i + "___",
-					___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___"
-				);
-				continue;
-			}
-
-			// keep empty comments after child selectors (IE7 hack)
-			// e.g. html >/**/ body
-			if (token.length === 0) {
-				const startIndex = content.indexOf(placeholder);
-				if (startIndex > 2) {
-					if (content.charAt(startIndex - 3) === ">") {
-						preservedTokens.push("");
-						content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
-					}
-				}
-			}
-
-			// in all other cases kill the comment
-			content = content.replace(`/*${placeholder}*/`, "");
-		}
-
-		// parse simple @variables blocks and remove them
-		if (options.expandVars) {
-			const vars = {};
-			pattern = /@variables\s*\{\s*([^}]+)\s*\}/g;
-			content = content.replace(pattern, (_, f1) => {
-				pattern = /\s*([a-z0-9-]+)\s*:\s*([^;}]+)\s*/gi;
-				f1.replace(pattern, (_, f1, f2) => {
-					if (f1 && f2) {
-						vars[f1] = f2;
-					}
-					return "";
-				});
-				return "";
-			});
-
-			// replace var(x) with the value of x
-			pattern = /var\s*\(\s*([^)]+)\s*\)/g;
-			content = content.replace(pattern, (_, f1) => {
-				return vars[f1] || "none";
-			});
-		}
-
-		// normalize all whitespace strings to single spaces. Easier to work with that way.
-		content = content.replace(/\s+/g, " ");
-
-		// preserve formulas in calc() before removing spaces
-		pattern = /calc\(([^;}]*)\)/g;
-		content = content.replace(pattern, (_, f1) => {
-			preservedTokens.push(
-				"calc(" +
-				f1.replace(/(^\s*|\s*$)/g, "")
-					.replace(/\( /g, "(")
-					.replace(/ \)/g, ")") +
-				")"
-			);
-			return ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
-		});
-
-		// preserve matrix
-		pattern = /\s*filter:\s*progid:DXImageTransform.Microsoft.Matrix\(([^)]+)\);/g;
-		content = content.replace(pattern, (_, f1) => {
-			preservedTokens.push(f1);
-			return "filter:progid:DXImageTransform.Microsoft.Matrix(" + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___);";
-		});
-
-		// remove the spaces before the things that should not have spaces before them.
-		// but, be careful not to turn 'p :link {...}' into 'p:link{...}'
-		// swap out any pseudo-class colons with the token, and then swap back.
-		pattern = /(^|\})(([^{:])+:)+([^{]*{)/g;
-		content = content.replace(pattern, token => token.replace(/:/g, "___PSEUDOCLASSCOLON___"));
-
-		// remove spaces before the things that should not have spaces before them.
-		content = content.replace(/\s+([!{};:>+()\],])/g, "$1");
-
-		// restore spaces for !important
-		content = content.replace(/!important/g, " !important");
-
-		// bring back the colon
-		content = content.replace(/___PSEUDOCLASSCOLON___/g, ":");
-
-		// preserve 0 followed by a time unit for properties using time units
-		pattern = /\s*(animation|animation-delay|animation-duration|transition|transition-delay|transition-duration):\s*([^;}]+)/gi;
-		content = content.replace(pattern, (_, f1, f2) => {
-
-			f2 = f2.replace(/(^|\D)0?\.?0(m?s)/gi, (_, g1, g2) => {
-				preservedTokens.push("0" + g2);
-				return g1 + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
-			});
-
-			return f1 + ":" + f2;
-		});
-
-		// preserve unit for flex-basis within flex and flex-basis (ie10 bug)
-		pattern = /\s*(flex|flex-basis):\s*([^;}]+)/gi;
-		content = content.replace(pattern, (_, f1, f2) => {
-			let f2b = f2.split(/\s+/);
-			preservedTokens.push(f2b.pop());
-			f2b.push(___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
-			f2b = f2b.join(" ");
-			return `${f1}:${f2b}`;
-		});
-
-		// preserve 0% in hsl and hsla color definitions
-		content = content.replace(/(hsla?)\(([^)]+)\)/g, (_, f1, f2) => {
-			const f0 = [];
-			f2.split(",").forEach(part => {
-				part = part.replace(/(^\s+|\s+$)/g, "");
-				if (part === "0%") {
-					preservedTokens.push("0%");
-					f0.push(___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
-				} else {
-					f0.push(part);
-				}
-			});
-			return f1 + "(" + f0.join(",") + ")";
-		});
-
-		// preserve 0 followed by unit in keyframes steps (WIP)
-		content = keyframes(content, preservedTokens);
-
-		// retain space for special IE6 cases
-		content = content.replace(/:first-(line|letter)(\{|,)/gi, (_, f1, f2) => ":first-" + f1.toLowerCase() + " " + f2);
-
-		// newlines before and after the end of a preserved comment
-		if (options.cuteComments) {
-			content = content.replace(/\s*\/\*/g, "___PRESERVED_NEWLINE___/*");
-			content = content.replace(/\*\/\s*/g, "*/___PRESERVED_NEWLINE___");
-			// no space after the end of a preserved comment
-		} else {
-			content = content.replace(/\*\/\s*/g, "*/");
-		}
-
-		// If there are multiple @charset directives, push them to the top of the file.
-		pattern = /^(.*)(@charset)( "[^"]*";)/gi;
-		content = content.replace(pattern, (_, f1, f2, f3) => f2.toLowerCase() + f3 + f1);
-
-		// When all @charset are at the top, remove the second and after (as they are completely ignored).
-		pattern = /^((\s*)(@charset)( [^;]+;\s*))+/gi;
-		content = content.replace(pattern, (_, __, f2, f3, f4) => f2 + f3.toLowerCase() + f4);
-
-		// lowercase some popular @directives (@charset is done right above)
-		pattern = /@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)/gi;
-		content = content.replace(pattern, (_, f1) => "@" + f1.toLowerCase());
-
-		// lowercase some more common pseudo-elements
-		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;
-		content = content.replace(pattern, (_, f1) => ":" + f1.toLowerCase());
-
-		// if there is a @charset, then only allow one, and push to the top of the file.
-		content = content.replace(/^(.*)(@charset "[^"]*";)/g, "$2$1");
-		content = content.replace(/^(\s*@charset [^;]+;\s*)+/g, "$1");
-
-		// lowercase some more common functions
-		pattern = /:(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?any)\(/gi;
-		content = content.replace(pattern, (_, f1) => ":" + f1.toLowerCase() + "(");
-
-		// lower case some common function that can be values
-		// NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us right after this
-		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;
-		content = content.replace(pattern, (_, f1, f2) => f1 + f2.toLowerCase());
-
-		// put the space back in some cases, to support stuff like
-		// @media screen and (-webkit-min-device-pixel-ratio:0){
-		content = content.replace(/\band\(/gi, "and (");
-		content = content.replace(/([^:])not\(/gi, "$1not (");
-
-		// remove the spaces after the things that should not have spaces after them.
-		content = content.replace(/([!{}:;>+([,])\s+/g, "$1");
-
-		// remove unnecessary semicolons
-		content = content.replace(/;+\}/g, "}");
-
-		// replace 0(px,em,%) with 0.
-		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|%)(?![a-z0-9])/gi, "$10");
-
-		// Replace x.0(px,em,%) with x(px,em,%).
-		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");
-
-		// replace 0 0 0 0; with 0.
-		content = content.replace(/:0 0 0 0(;|\})/g, ":0$1");
-		content = content.replace(/:0 0 0(;|\})/g, ":0$1");
-		content = content.replace(/:0 0(;|\})/g, ":0$1");
-
-		// replace background-position:0; with background-position:0 0;
-		// same for transform-origin and box-shadow
-		pattern = /(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin|box-shadow):0(;|\})/gi;
-		content = content.replace(pattern, (_, f1, f2) => f1.toLowerCase() + ":0 0" + f2);
-
-		// replace 0.6 to .6, but only when preceded by : or a white-space
-		content = content.replace(/(:|\s)0+\.(\d+)/g, "$1.$2");
-
-		// shorten colors from rgb(51,102,153) to #336699
-		// this makes it more likely that it'll get further compressed in the next step.
-		pattern = /rgb\s*\(\s*([0-9,\s]+)\s*\)/gi;
-		content = content.replace(pattern, (_, f1) => {
-			let rgbcolors = f1.split(","), hexcolor = "#";
-			for (let i = 0; i < rgbcolors.length; i += 1) {
-				let val = parseInt(rgbcolors[i], 10);
-				if (val < 16) {
-					hexcolor += "0";
-				}
-				if (val > 255) {
-					val = 255;
-				}
-				hexcolor += val.toString(16);
-			}
-			return hexcolor;
-		});
-
-		// Shorten colors from #AABBCC to #ABC.
-		content = compressHexColors(content);
-
-		// Replace #f00 -> red
-		content = content.replace(/(:|\s)(#f00)(;|})/g, "$1red$3");
-
-		// Replace other short color keywords
-		content = content.replace(/(:|\s)(#000080)(;|})/g, "$1navy$3");
-		content = content.replace(/(:|\s)(#808080)(;|})/g, "$1gray$3");
-		content = content.replace(/(:|\s)(#808000)(;|})/g, "$1olive$3");
-		content = content.replace(/(:|\s)(#800080)(;|})/g, "$1purple$3");
-		content = content.replace(/(:|\s)(#c0c0c0)(;|})/g, "$1silver$3");
-		content = content.replace(/(:|\s)(#008080)(;|})/g, "$1teal$3");
-		content = content.replace(/(:|\s)(#ffa500)(;|})/g, "$1orange$3");
-		content = content.replace(/(:|\s)(#800000)(;|})/g, "$1maroon$3");
-
-		// border: none -> border:0
-		pattern = /(border|border-top|border-right|border-bottom|border-left|outline|background):none(;|\})/gi;
-		content = content.replace(pattern, (_, f1, f2) => f1.toLowerCase() + ":0" + f2);
-
-		// shorter opacity IE filter
-		content = content.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity=");
-
-		// Find a fraction that is used for Opera's -o-device-pixel-ratio query
-		// Add token to add the '\' back in later
-		content = content.replace(/\(([-A-Za-z]+):([0-9]+)\/([0-9]+)\)/g, "($1:$2___QUERY_FRACTION___$3)");
-
-		// remove empty rules.
-		content = content.replace(/[^};{/]+\{\}/g, "");
-
-		// Add '\' back to fix Opera -o-device-pixel-ratio query
-		content = content.replace(/___QUERY_FRACTION___/g, "/");
-
-		// some source control tools don't like it when files containing lines longer
-		// than, say 8000 characters, are checked in. The linebreak option is used in
-		// that case to split long lines after a specific column.
-		if (options.maxLineLen > 0) {
-			const lines = [];
-			let line = [];
-			for (let i = 0, len = content.length; i < len; i += 1) {
-				const ch = content.charAt(i);
-				line.push(ch);
-				if (ch === "}" && line.length > options.maxLineLen) {
-					lines.push(line.join(""));
-					line = [];
-				}
-			}
-			if (line.length) {
-				lines.push(line.join(""));
-			}
-
-			content = lines.join("\n");
-		}
-
-		// replace multiple semi-colons in a row by a single one
-		// see SF bug #1980989
-		content = content.replace(/;;+/g, ";");
-
-		// trim the final string (for any leading or trailing white spaces)
-		content = content.replace(/(^\s*|\s*$)/g, "");
-
-		// restore preserved tokens
-		for (let i = preservedTokens.length - 1; i >= 0; i--) {
-			content = content.replace(___PRESERVED_TOKEN_ + i + "___", preservedTokens[i], "g");
-		}
-
-		// restore preserved newlines
-		content = content.replace(/___PRESERVED_NEWLINE___/g, "\n");
-
-		// return
-		return content;
-	}
-
-	return {
-		defaultOptions,
-		processString
-	};
-
-})();

+ 3 - 3
manifest.json

@@ -57,14 +57,14 @@
 			"extension/ui/bg/ui-autosave.js",
 			"lib/single-file/doc-helper.js",
 			"lib/single-file/base64.js",
-			"lib/single-file/css-uglifycss.js",
-			"lib/single-file/css-what.js",
+			"lib/single-file/css-minifier.js",
+			"lib/single-file/css-selector-parser.js",
 			"lib/single-file/css-declarations-parser.js",
 			"lib/single-file/css-fonts-minifier.js",
 			"lib/single-file/css-media-query-parser.js",
 			"lib/single-file/css-medias-minifier.js",
 			"lib/single-file/css-rules-matcher.js",
-			"lib/single-file/css-minifier.js",
+			"lib/single-file/css-rules-minifier.js",
 			"lib/single-file/css-srcset-parser.js",
 			"lib/single-file/html-minifier.js",
 			"lib/single-file/html-serializer.js",