Browse Source

added CSSTree library to remove any usage of CSSOM

Gildas 7 năm trước cách đây
mục cha
commit
1d40bbb186

+ 6 - 7
extension/core/bg/script-loader.js

@@ -25,8 +25,9 @@ singlefile.scriptLoader = (() => {
 	const contentScriptFiles = [
 		"/lib/browser-polyfill/custom-browser-polyfill.js",
 		"/lib/single-file/doc-helper.js",
-		"/lib/single-file/timeout.js",
-		"/lib/single-file/base64.js",
+		"/lib/single-file/util/timeout.js",
+		"/lib/single-file/util/base64.js",
+		"/lib/single-file/css-tree.js",
 		"/lib/single-file/html-srcset-parser.js",
 		"/lib/fetch/content/fetch.js",
 		"/lib/single-file/single-file-core.js",
@@ -41,7 +42,7 @@ singlefile.scriptLoader = (() => {
 		"/lib/single-file/font-face-proxy.js",
 		"/extension/index.js",
 		"/lib/single-file/doc-helper.js",
-		"/lib/single-file/timeout.js",
+		"/lib/single-file/util/timeout.js",
 		"/lib/fetch/content/fetch.js",
 		"/lib/single-file/frame-tree.js",
 		"/extension/core/content/content-frame.js"
@@ -63,13 +64,11 @@ singlefile.scriptLoader = (() => {
 			"/lib/single-file/css-medias-minifier.js"
 		],
 		removeAlternativeImages: [
-			"/lib/single-file/html-alt-images.js"
+			"/lib/single-file/html-images-minifier.js"
 		],
 		removeUnusedStyles: [
-			"/lib/single-file/css-selector-parser.js",
-			"/lib/single-file/css-declarations-parser.js",
 			"/lib/single-file/css-medias-minifier.js",
-			"/lib/single-file/css-rules-matcher.js",
+			"/lib/single-file/css-matched-rules.js",
 			"/lib/single-file/css-rules-minifier.js",
 			"/lib/single-file/css-fonts-minifier.js"
 		],

+ 0 - 695
lib/single-file/css-declarations-parser.js

@@ -1,695 +0,0 @@
-/*
- * The code in this file is licensed under the CC0 license.
- *
- * http://creativecommons.org/publicdomain/zero/1.0/
- *
- * It is free to use for any purpose. No attribution, permission, or reproduction of this license is require
- */
-
-// Modified by Gildas Lormeau (ES5 -> ES6, removed unused code)
-
-// https://github.com/tabatkins/parse-css
-this.parseCss = this.parseCss || (() => {
-
-	const BAD_STRING_TOKEN_TYPE = "BADSTRING";
-	const BAD_URL_TOKEN_TYPE = "BADURL";
-	const WHITESPACE_TOKEN_TYPE = "WHITESPACE";
-	const CDO_TOKEN_TYPE = "CDO";
-	const CDC_TOKEN_TYPE = "CDO";
-	const COLON_TOKEN_TYPE = ":";
-	const SEMICOLON_TOKEN_TYPE = ";";
-	const COMMA_TOKEN_TYPE = ",";
-	const OPEN_CURLY_TOKEN_TYPE = "{";
-	const CLOSE_CURLY_TOKEN_TYPE = "}";
-	const OPEN_SQUARE_TOKEN_TYPE = "[";
-	const CLOSE_SQUARE_TOKEN_TYPE = "]";
-	const OPEN_PAREN_TOKEN_TYPE = "(";
-	const CLOSE_PAREN_TOKEN_TYPE = ")";
-	const INCLUDE_MATCH_TOKEN_TYPE = "~=";
-	const DASH_MATCH_TOKEN_TYPE = "|=";
-	const PREFIX_MATCH_TOKEN_TYPE = "^=";
-	const SUFFIX_MATCH_TOKEN_TYPE = "$=";
-	const SUBSTRING_MATCH_TOKEN_TYPE = "*=";
-	const COLUMN_TOKEN_TYPE = "||";
-	const EOF_TOKEN_TYPE = "EOF";
-	const DELIM_TOKEN_TYPE = "DELIM";
-	const IDENT_TOKEN_TYPE = "IDENT";
-	const FUNCTION_TOKEN_TYPE = "FUNCTION";
-	const HASH_TOKEN_TYPE = "HASH";
-	const STRING_TOKEN_TYPE = "STRING";
-	const URL_TOKEN_TYPE = "URL";
-	const NUMBER_TOKEN_TYPE = "NUMBER";
-	const PERCENTAGE_TOKEN_TYPE = "PERCENTAGE";
-	const DIMENSION_TOKEN_TYPE = "DIMENSION";
-	const DECLARATION_TYPE = "DECLARATION";
-	const FUNCTION_TYPE = "FUNCTION";
-
-	function digit(code) { return code >= 0x30 && code <= 0x39; }
-	function hexdigit(code) { return digit(code) || code >= 0x41 && code <= 0x46 || code >= 0x61 && code <= 0x66; }
-	function namestartchar(code) { return code >= 0x41 && code <= 0x5a || code >= 0x61 && code <= 0x7a || code >= 0x80 || code == 0x5f; }
-	function namechar(code) { return namestartchar(code) || digit(code) || code == 0x2d; }
-	function nonprintable(code) { return code >= 0 && code <= 8 || code == 0xb || code >= 0xe && code <= 0x1f || code == 0x7f; }
-	function newline(code) { return code == 0xa; }
-	function whitespace(code) { return newline(code) || code == 9 || code == 0x20; }
-
-	const maximumallowedcodepoint = 0x10ffff;
-
-	function preprocess(str) {
-		// Turn a string into an array of code points,
-		// following the preprocessing cleanup rules.
-		const codepoints = [];
-		for (let i = 0; i < str.length; i++) {
-			let code = str.codePointAt(i);
-			if (code == 0xd && str.codePointAt(i + 1) == 0xa) {
-				code = 0xa; i++;
-			}
-			if (code == 0xd || code == 0xc) code = 0xa;
-			if (code == 0x0) code = 0xfffd;
-			codepoints.push(code);
-		}
-		return codepoints;
-	}
-
-	function consumeAToken(consume, next, eof, reconsume, parseerror, donothing) {
-		consumeComments(consume, next, eof, parseerror);
-		let code = consume();
-		if (whitespace(code)) {
-			while (whitespace(next())) code = consume();
-			return new Token(WHITESPACE_TOKEN_TYPE);
-		} else {
-			switch (code) {
-				case 0x22:
-					return consumeAStringToken(consume, next, eof, reconsume, parseerror, donothing, code);
-				case 0x23:
-					if (namechar(next()) || areAValidEscape(next(1), next(2))) {
-						const token = new Token(HASH_TOKEN_TYPE);
-						if (wouldStartAnIdentifier(next(1), next(2), next(3))) token.type = "id";
-						token.value = consumeAName(consume, next, eof, reconsume);
-						return token;
-					} else {
-						return new Token(DELIM_TOKEN_TYPE, String.fromCodePoint(code));
-					}
-				case 0x24:
-					if (next() == 0x3d) {
-						code = consume();
-						return new Token(SUFFIX_MATCH_TOKEN_TYPE);
-					} else {
-						return new Token(DELIM_TOKEN_TYPE, String.fromCodePoint(code));
-					}
-				case 0x27:
-					return consumeAStringToken(consume, next, eof, reconsume, parseerror, donothing, code);
-				case 0x28:
-					return new Token(OPEN_PAREN_TOKEN_TYPE);
-				case 0x29:
-					return new Token(CLOSE_PAREN_TOKEN_TYPE);
-				case 0x2a:
-					if (next() == 0x3d) {
-						code = consume();
-						return new Token(SUBSTRING_MATCH_TOKEN_TYPE);
-					} else {
-						return new Token(DELIM_TOKEN_TYPE, String.fromCodePoint(code));
-					}
-				case 0x2b:
-					if (startsWithANumber(next, code)) {
-						reconsume();
-						return consumeANumericToken(consume, next, eof, reconsume);
-					} else {
-						return new Token(DELIM_TOKEN_TYPE, String.fromCodePoint(code));
-					}
-				case 0x2c:
-					return new Token(COMMA_TOKEN_TYPE);
-				case 0x2d:
-					if (startsWithANumber(next, code)) {
-						reconsume();
-						return consumeANumericToken(consume, next, eof, reconsume);
-					} else if (next(1) == 0x2d && next(2) == 0x3e) {
-						consume(2);
-						return new Token(CDC_TOKEN_TYPE);
-					} else if (wouldStartAnIdentifier(code, next(1), next(2))) {
-						reconsume();
-						return consumeAnIdentlikeToken(consume, next, eof, reconsume, parseerror, donothing);
-					} else {
-						return new Token(DELIM_TOKEN_TYPE, String.fromCodePoint(code));
-					}
-				case 0x2e:
-					if (startsWithANumber(next, code)) {
-						reconsume();
-						return consumeANumericToken(consume, next, eof, reconsume);
-					} else {
-						return new Token(DELIM_TOKEN_TYPE, String.fromCodePoint(code));
-					}
-				case 0x3a:
-					return new Token(COLON_TOKEN_TYPE);
-				case 0x3b:
-					return new Token(SEMICOLON_TOKEN_TYPE);
-				case 0x3c:
-					if (next(1) == 0x21 && next(2) == 0x2d && next(3) == 0x2d) {
-						consume(3);
-						return new Token(CDO_TOKEN_TYPE);
-					} else {
-						return new Token(DELIM_TOKEN_TYPE, String.fromCodePoint(code));
-					}
-				case 0x40:
-					return new Token(DELIM_TOKEN_TYPE, String.fromCodePoint(code));
-				case 0x5b:
-					return new Token(OPEN_SQUARE_TOKEN_TYPE);
-				case 0x5c:
-					if (startsWithAValidEscape(next, code)) {
-						reconsume();
-						return consumeAnIdentlikeToken(consume, next, eof, reconsume, parseerror, donothing);
-					} else {
-						parseerror();
-						return new Token(DELIM_TOKEN_TYPE, String.fromCodePoint(code));
-					}
-				case 0x5d:
-					return new Token(CLOSE_SQUARE_TOKEN_TYPE);
-				case 0x5e:
-					if (next() == 0x3d) {
-						code = consume();
-						return new Token(PREFIX_MATCH_TOKEN_TYPE);
-					} else {
-						return new Token(DELIM_TOKEN_TYPE, String.fromCodePoint(code));
-					}
-				case 0x7b:
-					return new Token(OPEN_CURLY_TOKEN_TYPE);
-				case 0x7c:
-					if (next() == 0x3d) {
-						code = consume();
-						return new Token(DASH_MATCH_TOKEN_TYPE);
-					} else if (next() == 0x7c) {
-						code = consume();
-						return new Token(COLUMN_TOKEN_TYPE);
-					} else {
-						return new Token(DELIM_TOKEN_TYPE, String.fromCodePoint(code));
-					}
-				case 0x7d:
-					return new Token(CLOSE_CURLY_TOKEN_TYPE);
-				case 0x7e:
-					if (next() == 0x3d) {
-						code = consume();
-						return new Token(INCLUDE_MATCH_TOKEN_TYPE);
-					} else {
-						return new Token(DELIM_TOKEN_TYPE, String.fromCodePoint(code));
-					}
-				default:
-					if (digit(code)) {
-						reconsume();
-						return consumeANumericToken(consume, next, eof, reconsume);
-					}
-					else if (namestartchar(code)) {
-						reconsume();
-						return consumeAnIdentlikeToken(consume, next, eof, reconsume, parseerror, donothing);
-					}
-					else if (eof()) return new Token(EOF_TOKEN_TYPE);
-					else return new Token(DELIM_TOKEN_TYPE, String.fromCodePoint(code));
-			}
-		}
-	}
-
-
-	function consumeComments(consume, next, eof, parseerror) {
-		while (next(1) == 0x2f && next(2) == 0x2a) {
-			consume(2);
-			while (true) { // eslint-disable-line no-constant-condition
-				let code = consume();
-				if (code == 0x2a && next() == 0x2f) {
-					code = consume();
-					break;
-				} else if (eof()) {
-					parseerror();
-					return;
-				}
-			}
-		}
-	}
-
-	function consumeANumericToken(consume, next, eof, reconsume) {
-		const num = consumeANumber(consume, next);
-		if (wouldStartAnIdentifier(next(1), next(2), next(3))) {
-			const token = new Token(DIMENSION_TOKEN_TYPE, num.value);
-			token.repr = num.repr;
-			token.type = num.type;
-			token.unit = consumeAName(consume, next, eof, reconsume);
-			return token;
-		} else if (next() == 0x25) {
-			consume();
-			const token = new Token(PERCENTAGE_TOKEN_TYPE, num.value);
-			token.repr = num.repr;
-			return token;
-		} else {
-			const token = new Token(NUMBER_TOKEN_TYPE, num.value);
-			token.type = "integer";
-			token.repr = num.repr;
-			token.type = num.type;
-			return token;
-		}
-	}
-
-	function consumeAnIdentlikeToken(consume, next, eof, reconsume, parseerror, donothing) {
-		const str = consumeAName(consume, next, eof, reconsume);
-		if (str.toLowerCase() == "url" && next() == 0x28) {
-			consume();
-			while (whitespace(next(1)) && whitespace(next(2))) consume();
-			if (next() == 0x22 || next() == 0x27) {
-				return new Token(FUNCTION_TOKEN_TYPE, str);
-			} else if (whitespace(next()) && (next(2) == 0x22 || next(2) == 0x27)) {
-				return new Token(FUNCTION_TOKEN_TYPE, str);
-			} else {
-				return consumeAURLToken(consume, next, eof, parseerror, donothing);
-			}
-		} else if (next() == 0x28) {
-			consume();
-			return new Token(FUNCTION_TOKEN_TYPE, str);
-		} else {
-			return new Token(IDENT_TOKEN_TYPE, str);
-		}
-	}
-
-	function consumeAStringToken(consume, next, eof, reconsume, parseerror, donothing, code) {
-		const endingCodePoint = code;
-		let string = "";
-		while (code = consume()) { // eslint-disable-line no-cond-assign
-			if (code == endingCodePoint || eof()) {
-				return new Token(STRING_TOKEN_TYPE, string);
-			} else if (newline(code)) {
-				parseerror();
-				reconsume();
-				return new Token(BAD_STRING_TOKEN_TYPE);
-			} else if (code == 0x5c) {
-				if (eof(next())) {
-					donothing();
-				} else if (newline(next())) {
-					code = consume();
-				} else {
-					string += String.fromCodePoint(consumeEscape(consume, next, eof));
-				}
-			} else {
-				string += String.fromCodePoint(code);
-			}
-		}
-	}
-
-	function consumeAURLToken(consume, next, eof, parseerror, donothing) {
-		const token = new Token(URL_TOKEN_TYPE, "");
-		while (whitespace(next())) consume();
-		if (eof(next())) return token;
-		let code;
-		while (code = consume()) { // eslint-disable-line no-cond-assign
-			if (code == 0x29 || eof()) {
-				return token;
-			} else if (whitespace(code)) {
-				while (whitespace(next())) code = consume();
-				if (next() == 0x29 || eof(next())) {
-					code = consume();
-					return token;
-				} else {
-					consumeTheRemnantsOfABadURL(consume, next, eof, donothing);
-					return new Token(BAD_URL_TOKEN_TYPE);
-				}
-			} else if (code == 0x22 || code == 0x27 || code == 0x28 || nonprintable(code)) {
-				parseerror();
-				consumeTheRemnantsOfABadURL(consume, next, eof, donothing);
-				return new Token(BAD_URL_TOKEN_TYPE);
-			} else if (code == 0x5c) {
-				if (startsWithAValidEscape(next, code)) {
-					token.value += String.fromCodePoint(consumeEscape(consume, next, eof));
-				} else {
-					parseerror();
-					consumeTheRemnantsOfABadURL(consume, next, eof, donothing);
-					return new Token(BAD_URL_TOKEN_TYPE);
-				}
-			} else {
-				token.value += String.fromCodePoint(code);
-			}
-		}
-	}
-
-	function consumeEscape(consume, next, eof) {
-		// Assume the the current character is the \
-		// and the next code point is not a newline.
-		let code = consume();
-		if (hexdigit(code)) {
-			// Consume 1-6 hex digits
-			const digits = [code];
-			for (let total = 0; total < 5; total++) {
-				if (hexdigit(next())) {
-					code = consume();
-					digits.push(code);
-				} else {
-					break;
-				}
-			}
-			if (whitespace(next())) code = consume();
-			let value = parseInt(digits.map(function (x) { return String.fromCharCode(x); }).join(""), 16);
-			if (value > maximumallowedcodepoint) value = 0xfffd;
-			return value;
-		} else if (eof()) {
-			return 0xfffd;
-		} else {
-			return code;
-		}
-	}
-
-	function areAValidEscape(c1, c2) {
-		if (c1 != 0x5c) return false;
-		if (newline(c2)) return false;
-		return true;
-	}
-
-	function startsWithAValidEscape(next, code) {
-		return areAValidEscape(code, next());
-	}
-
-	function wouldStartAnIdentifier(c1, c2, c3) {
-		if (c1 == 0x2d) {
-			return namestartchar(c2) || c2 == 0x2d || areAValidEscape(c2, c3);
-		} else if (namestartchar(c1)) {
-			return true;
-		} else if (c1 == 0x5c) {
-			return areAValidEscape(c1, c2);
-		} else {
-			return false;
-		}
-	}
-
-	function wouldStartANumber(c1, c2, c3) {
-		if (c1 == 0x2b || c1 == 0x2d) {
-			if (digit(c2)) return true;
-			if (c2 == 0x2e && digit(c3)) return true;
-			return false;
-		} else if (c1 == 0x2e) {
-			if (digit(c2)) return true;
-			return false;
-		} else if (digit(c1)) {
-			return true;
-		} else {
-			return false;
-		}
-	}
-
-	function startsWithANumber(next, code) {
-		return wouldStartANumber(code, next(1), next(2));
-	}
-
-	function consumeAName(consume, next, eof, reconsume) {
-		let result = "", code;
-		while (code = consume()) { // eslint-disable-line no-cond-assign
-			if (namechar(code)) {
-				result += String.fromCodePoint(code);
-			} else if (startsWithAValidEscape(next, code)) {
-				result += String.fromCodePoint(consumeEscape(consume, next, eof));
-			} else {
-				reconsume();
-				return result;
-			}
-		}
-	}
-
-	function consumeANumber(consume, next) {
-		let repr = [], type = "integer";
-		let code;
-		if (next() == 0x2b || next() == 0x2d) {
-			code = consume();
-			repr += String.fromCodePoint(code);
-		}
-		while (digit(next())) {
-			code = consume();
-			repr += String.fromCodePoint(code);
-		}
-		if (next(1) == 0x2e && digit(next(2))) {
-			code = consume();
-			repr += String.fromCodePoint(code);
-			code = consume();
-			repr += String.fromCodePoint(code);
-			type = "number";
-			while (digit(next())) {
-				code = consume();
-				repr += String.fromCodePoint(code);
-			}
-		}
-		const c1 = next(1), c2 = next(2), c3 = next(3);
-		if ((c1 == 0x45 || c1 == 0x65) && digit(c2)) {
-			code = consume();
-			repr += String.fromCodePoint(code);
-			code = consume();
-			repr += String.fromCodePoint(code);
-			type = "number";
-			while (digit(next())) {
-				code = consume();
-				repr += String.fromCodePoint(code);
-			}
-		} else if ((c1 == 0x45 || c1 == 0x65) && (c2 == 0x2b || c2 == 0x2d) && digit(c3)) {
-			code = consume();
-			repr += String.fromCodePoint(code);
-			code = consume();
-			repr += String.fromCodePoint(code);
-			code = consume();
-			repr += String.fromCodePoint(code);
-			type = "number";
-			while (digit(next())) {
-				code = consume();
-				repr += String.fromCodePoint(code);
-			}
-		}
-		const value = convertAStringToANumber(repr);
-		return { type, value, repr };
-	}
-
-	function convertAStringToANumber(string) {
-		// CSS's number rules are identical to JS, afaik.
-		return Number(string);
-	}
-
-	function consumeTheRemnantsOfABadURL(consume, next, eof, donothing) {
-		let code;
-		while (code = consume()) { // eslint-disable-line no-cond-assign
-			if (code == 0x29 || eof()) {
-				return;
-			} else if (startsWithAValidEscape(next, code)) {
-				consumeEscape(consume, next, eof);
-				donothing();
-			} else {
-				donothing();
-			}
-		}
-	}
-
-	function tokenize(str) {
-		str = preprocess(str);
-		let i = -1;
-		const tokens = [];
-		const strLength = str.length;
-		let code;
-
-		// Line number information.
-		let line = 0, column = 0;
-
-		// The only use of lastLineLength is in reconsume().
-		let lastLineLength = 0;
-		const incrLineno = function () {
-			line += 1;
-			lastLineLength = column;
-			column = 0;
-		};
-		const locStart = { line, column };
-
-		const codepoint = function (i) {
-			if (i >= strLength) {
-				return -1;
-			}
-			return str[i];
-		};
-		const next = function (num) {
-			if (num === undefined)
-				num = 1;
-			if (num > 3)
-				throw "Spec Error: no more than three codepoints of lookahead.";
-			return codepoint(i + num);
-		};
-		const consume = function (num) {
-			if (num === undefined)
-				num = 1;
-			i += num;
-			const code = codepoint(i);
-			if (newline(code)) incrLineno();
-			else column += num;
-			//console.log('Consume '+i+' '+String.fromCharCode(code) + ' 0x' + code.toString(16));
-			return code;
-		};
-		const reconsume = function () {
-			i -= 1;
-			if (newline(code)) {
-				line -= 1;
-				column = lastLineLength;
-			} else {
-				column -= 1;
-			}
-			locStart.line = line;
-			locStart.column = column;
-			return true;
-		};
-		const eof = function (codepoint) {
-			if (codepoint === undefined) codepoint = code;
-			return codepoint == -1;
-		};
-		const donothing = function () { };
-		const parseerror = function () { throw new Error("Parse error at index " + i + ", processing codepoint 0x" + code.toString(16) + "."); };
-
-		let iterationCount = 0;
-		while (!eof(next())) {
-			tokens.push(consumeAToken(consume, next, eof, reconsume, parseerror, donothing));
-			iterationCount++;
-			if (iterationCount > strLength * 2) return "I'm infinite-looping!";
-		}
-		return tokens;
-	}
-
-	class Token {
-		constructor(tokenType, value) {
-			this.tokenType = tokenType;
-			this.value = value;
-			this.repr = null;
-			this.type = null;
-			this.unit = null;
-		}
-	}
-
-	// ---
-	class TokenStream {
-		constructor(tokens) {
-			// Assume that tokens is an array.
-			this.tokens = tokens;
-			this.i = -1;
-		}
-		tokenAt(i) {
-			if (i < this.tokens.length)
-				return this.tokens[i];
-			return new Token(EOF_TOKEN_TYPE);
-		}
-		consume(num) {
-			if (num === undefined)
-				num = 1;
-			this.i += num;
-			this.token = this.tokenAt(this.i);
-			//console.log(this.i, this.token);
-			return true;
-		}
-		next() {
-			return this.tokenAt(this.i + 1);
-		}
-		reconsume() {
-			this.i--;
-		}
-	}
-
-	function parseerror(s, msg) {
-		throw new Error("Parse error at token " + s.i + ": " + s.token + ".\n" + msg);
-	}
-	function donothing() { return true; }
-
-	function consumeAListOfDeclarations(s) {
-		const decls = [];
-		while (s.consume()) {
-			if (s.token.tokenType == WHITESPACE_TOKEN_TYPE || s.token.tokenType == SEMICOLON_TOKEN_TYPE) {
-				donothing();
-			} else if (s.token.tokenType == EOF_TOKEN_TYPE) {
-				return decls;
-			} else if (s.token.tokenType == IDENT_TOKEN_TYPE) {
-				const temp = [s.token];
-				while (!(s.next().tokenType == SEMICOLON_TOKEN_TYPE || s.next().tokenType == EOF_TOKEN_TYPE))
-					temp.push(consumeAComponentValue(s));
-				const decl = consumeADeclaration(new TokenStream(temp));
-				if (decl) decls.push(decl);
-			} else {
-				parseerror(s);
-				s.reconsume();
-				while (!(s.next().tokenType == SEMICOLON_TOKEN_TYPE || s.next().tokenType == EOF_TOKEN_TYPE))
-					consumeAComponentValue(s);
-			}
-		}
-	}
-
-	function consumeADeclaration(s) {
-		// Assumes that the next input token will be an ident token.
-		s.consume();
-		const decl = new Declaration(s.token.value);
-		while (s.next().tokenType == WHITESPACE_TOKEN_TYPE) s.consume();
-		if (!(s.next().tokenType == COLON_TOKEN_TYPE)) {
-			parseerror(s);
-			return;
-		} else {
-			s.consume();
-		}
-		while (!(s.next().tokenType == EOF_TOKEN_TYPE)) {
-			decl.value.push(consumeAComponentValue(s));
-		}
-		let foundImportant = false;
-		for (let i = decl.value.length - 1; i >= 0; i--) {
-			if (decl.value[i].tokenType == WHITESPACE_TOKEN_TYPE) {
-				continue;
-			} else if (decl.value[i].tokenType == IDENT_TOKEN_TYPE && decl.value[i].value.toLowerCase() == "important") {
-				foundImportant = true;
-			} else if (foundImportant && decl.value[i].tokenType == DELIM_TOKEN_TYPE && decl.value[i].value == "!") {
-				decl.value.splice(i, decl.value.length);
-				decl.important = true;
-				break;
-			} else {
-				break;
-			}
-		}
-		return decl;
-	}
-
-	function consumeAComponentValue(s) {
-		s.consume();
-		if (s.token.tokenType == FUNCTION_TOKEN_TYPE)
-			return consumeAFunction(s);
-		return s.token;
-	}
-
-	function consumeAFunction(s) {
-		const func = new Func(s.token.value);
-		while (s.consume()) {
-			if (s.token.tokenType == EOF_TOKEN_TYPE || s.token.tokenType == CLOSE_PAREN_TOKEN_TYPE)
-				return func;
-			else {
-				s.reconsume();
-				func.value.push(consumeAComponentValue(s));
-			}
-		}
-	}
-
-	function normalizeInput(input) {
-		if (typeof input == "string")
-			return new TokenStream(tokenize(input));
-		else throw SyntaxError(input);
-	}
-
-	function parseAListOfDeclarations(s) {
-		s = normalizeInput(s);
-		return consumeAListOfDeclarations(s);
-	}
-	class Declaration {
-		constructor(name) {
-			this.name = name;
-			this.value = [];
-			this.important = false;
-			this.type = DECLARATION_TYPE;
-		}
-	}
-
-	class Func {
-		constructor(name) {
-			this.name = name;
-			this.value = [];
-			this.type = FUNCTION_TYPE;
-		}
-	}
-
-	// Exportation.
-
-	return {
-		parseAListOfDeclarations
-	};
-
-})();

+ 160 - 170
lib/single-file/css-fonts-minifier.js

@@ -18,7 +18,7 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/* global CSSRule */
+/* global cssTree */
 
 this.fontsMinifier = this.fontsMinifier || (() => {
 
@@ -56,30 +56,20 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 	};
 
 	return {
-		removeUnusedFonts: (doc, options) => {
-			const stats = {
-				rules: {
-					processed: 0,
-					discarded: 0
-				},
-				fonts: {
-					processed: 0,
-					discarded: 0
-				}
-			};
+		removeUnusedFonts: (doc, stylesheets, styles, options) => {
+			const stats = { rules: { processed: 0, discarded: 0 }, fonts: { processed: 0, discarded: 0 } };
 			const fontsInfo = { declared: [], used: [] };
 			let pseudoElementsContent = "";
-			doc.querySelectorAll("style").forEach(style => {
-				if (style.sheet) {
-					stats.rules.processed += style.sheet.cssRules.length;
-					stats.rules.discarded += style.sheet.cssRules.length;
-					getFontsInfo(doc, style.sheet.cssRules, fontsInfo);
-					pseudoElementsContent += getPseudoElementsContent(doc, style.sheet.cssRules);
-				}
+			stylesheets.forEach(stylesheetInfo => {
+				const cssRules = stylesheetInfo.stylesheet.children;
+				stats.processed += cssRules.getSize();
+				stats.discarded += cssRules.getSize();
+				getFontsInfo(cssRules, fontsInfo);
+				pseudoElementsContent += getPseudoElementsContent(cssRules);
 			});
-			doc.querySelectorAll("[style]").forEach(element => {
-				if (element.style && element.style.fontFamily) {
-					const fontFamilyNames = element.style.fontFamily.split(",").map(fontFamilyName => removeQuotes(fontFamilyName));
+			styles.forEach(style => {
+				const fontFamilyNames = getFontFamilyNames(style);
+				if (fontFamilyNames.length) {
 					fontsInfo.used.push(fontFamilyNames);
 				}
 			});
@@ -90,47 +80,35 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 			} else {
 				filteredUsedFonts = new Map();
 				fontsInfo.used.forEach(fontNames => fontNames.forEach(familyName => {
-					if (fontsInfo.declared.find(fontInfo => fontInfo.familyName == familyName)) {
+					if (fontsInfo.declared.find(fontInfo => fontInfo.fontFamily == familyName)) {
 						const optionalData = options.usedFonts && options.usedFonts.filter(fontInfo => fontInfo.fontFamily == familyName);
 						filteredUsedFonts.set(familyName, optionalData);
 					}
 				}));
-				unusedFonts = fontsInfo.declared.filter(fontInfo => !filteredUsedFonts.has(fontInfo.familyName));
+				unusedFonts = fontsInfo.declared.filter(fontInfo => !filteredUsedFonts.has(fontInfo.fontFamily));
 			}
 			const docContent = doc.body.innerText + pseudoElementsContent;
-			doc.querySelectorAll("style").forEach(style => {
-				if (style.sheet) {
-					style.textContent = filterUnusedFonts(doc, style.sheet.cssRules, fontsInfo.declared, unusedFonts, filteredUsedFonts, docContent);
-					stats.rules.discarded -= style.sheet.cssRules.length;
-				}
+			stylesheets.forEach(stylesheetInfo => {
+				const cssRules = stylesheetInfo.stylesheet.children;
+				filterUnusedFonts(cssRules, fontsInfo.declared, unusedFonts, filteredUsedFonts, docContent);
+				stats.rules.discarded -= cssRules.getSize();
 			});
 			return stats;
 		},
-		removeAlternativeFonts: doc => {
+		removeAlternativeFonts: (doc, stylesheets) => {
 			const fontsDetails = new Map();
-			const stats = {
-				rules: {
-					processed: 0,
-					discarded: 0
-				},
-				fonts: {
-					processed: 0,
-					discarded: 0
-				}
-			};
-			doc.querySelectorAll("style").forEach(style => {
-				if (style.sheet) {
-					stats.rules.processed += style.sheet.cssRules.length;
-					stats.rules.discarded += style.sheet.cssRules.length;
-					getFontsDetails(doc, style.sheet.cssRules, fontsDetails);
-				}
+			const stats = { rules: { processed: 0, discarded: 0 }, fonts: { processed: 0, discarded: 0 } };
+			stylesheets.forEach(stylesheetInfo => {
+				const cssRules = stylesheetInfo.stylesheet.children;
+				stats.rules.processed += cssRules.getSize();
+				stats.rules.discarded += cssRules.getSize();
+				getFontsDetails(doc, cssRules, fontsDetails);
 			});
 			processFontDetails(fontsDetails);
-			doc.querySelectorAll("style").forEach(style => {
-				if (style.sheet) {
-					style.textContent = processFontFaceRules(style.sheet.cssRules, fontsDetails, "all", stats);
-					stats.rules.discarded -= style.sheet.cssRules.length;
-				}
+			stylesheets.forEach(stylesheetInfo => {
+				const cssRules = stylesheetInfo.stylesheet.children;
+				processFontFaceRules(cssRules, fontsDetails, "all", stats);
+				stats.rules.discarded -= cssRules.getSize();
 			});
 			return stats;
 		}
@@ -170,78 +148,93 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 		});
 	}
 
-	function getFontsInfo(doc, rules, fontsInfo) {
-		if (rules) {
-			Array.from(rules).forEach(rule => {
-				if (rule.type == CSSRule.MEDIA_RULE) {
-					getFontsInfo(doc, rule.cssRules, fontsInfo);
-				} else if (rule.type == CSSRule.STYLE_RULE) {
-					if (rule.style && rule.style.fontFamily) {
-						const fontFamilyNames = rule.style.fontFamily.split(",").map(fontFamilyName => removeQuotes(fontFamilyName));
-						fontsInfo.used.push(fontFamilyNames);
-					}
-				} else {
-					if (rule.type == CSSRule.FONT_FACE_RULE && rule.style) {
-						const familyName = removeQuotes(rule.style.getPropertyValue("font-family"));
-						const fontWeight = getFontWeight(rule.style.getPropertyValue("font-weight")) || "400";
-						const fontStyle = rule.style.getPropertyValue("font-style") || "normal";
-						const fontVariant = rule.style.getPropertyValue("font-variant") || "normal";
-						if (familyName) {
-							fontsInfo.declared.push({ familyName, fontWeight, fontStyle, fontVariant });
-						}
+	function getFontsInfo(cssRules, fontsInfo) {
+		cssRules.forEach(cssRule => {
+			if (cssRule.type == "Atrule" && cssRule.name == "media") {
+				getFontsInfo(cssRule.block.children, fontsInfo);
+			} else if (cssRule.type == "Rule") {
+				const fontFamilyNames = getFontFamilyNames(cssRule.block);
+				if (fontFamilyNames.length) {
+					fontsInfo.used.push(fontFamilyNames);
+				}
+			} else {
+				if (cssRule.type == "Atrule" && cssRule.name == "font-face") {
+					const fontFamily = getFontFamily(getPropertyValue(cssRule, "font-family"));
+					if (fontFamily) {
+						const fontWeight = getFontWeight(getPropertyValue(cssRule, "font-weight") || "400");
+						const fontStyle = getPropertyValue(cssRule, "font-style") || "normal";
+						const fontVariant = getPropertyValue(cssRule, "font-variant") || "normal";
+						fontsInfo.declared.push({ fontFamily, fontWeight, fontStyle, fontVariant });
 					}
 				}
-			});
+			}
+		});
+	}
+
+	function getPropertyValue(cssRule, propertyName) {
+		const property = cssRule.block.children.filter(node => node.property == propertyName).tail;
+		if (property) {
+			return cssTree.generate(property.data.value);
 		}
 	}
 
-	function getFontsDetails(doc, rules, fontsDetails) {
-		if (rules) {
-			Array.from(rules).forEach(rule => {
-				if (rule.type == CSSRule.MEDIA_RULE) {
-					getFontsDetails(doc, rule.cssRules, fontsDetails);
-				} else {
-					if (rule.type == CSSRule.FONT_FACE_RULE && rule.style) {
-						const fontKey = getFontKey(rule.style);
-						let fontInfo = fontsDetails.get(fontKey);
-						if (!fontInfo) {
-							fontInfo = [];
-							fontsDetails.set(fontKey, fontInfo);
-						}
-						const src = rule.style.getPropertyValue("src");
-						if (src) {
-							const fontSources = src.match(REGEXP_URL_FUNCTION);
-							if (fontSources) {
-								fontSources.forEach(source => fontInfo.unshift(source));
-							}
+	function getFontFamilyNames(declarations) {
+		let fontFamilyName = declarations.children.filter(node => node.property == "font-family").tail;
+		let fontFamilyNames = [];
+		if (fontFamilyName) {
+			fontFamilyNames = fontFamilyName.data.value.children.filter(node => node.type == "String" || node.type == "Identifier").toArray().map(property => getFontFamily(cssTree.generate(property)));
+		}
+		const font = declarations.children.filter(node => node.property == "font").tail;
+		if (font) {
+			for (let node = font.data.value.children.tail; node && node.type != "WhiteSpace"; node = node.prev) {
+				if (node.data.type == "String" || node.data.type == "Identifier") {
+					fontFamilyNames.push(getFontFamily(cssTree.generate(node.data)));
+				}
+			}
+		}
+		return fontFamilyNames;
+	}
+
+	function getFontsDetails(doc, cssRules, fontsDetails) {
+		cssRules.forEach(cssRule => {
+			if (cssRule.type == "Atrule" && cssRule.name == "media") {
+				getFontsDetails(doc, cssRule.block.children, fontsDetails);
+			} else {
+				if (cssRule.type == "Atrule" && cssRule.name == "font-face") {
+					const fontKey = getFontKey(cssRule);
+					let fontInfo = fontsDetails.get(fontKey);
+					if (!fontInfo) {
+						fontInfo = [];
+						fontsDetails.set(fontKey, fontInfo);
+					}
+					const src = getPropertyValue(cssRule, "src");
+					if (src) {
+						const fontSources = src.match(REGEXP_URL_FUNCTION);
+						if (fontSources) {
+							fontSources.forEach(source => fontInfo.unshift(source));
 						}
 					}
 				}
-			});
-		}
+			}
+		});
 	}
 
-	function processFontFaceRules(rules, fontsDetails, media, stats) {
-		let stylesheetContent = "";
-		Array.from(rules).forEach(rule => {
-			if (rule.type == CSSRule.MEDIA_RULE) {
-				stylesheetContent += "@media " + Array.prototype.join.call(rule.media, ",") + "{";
-				stylesheetContent += processFontFaceRules(rule.cssRules, fontsDetails, rule.media.mediaText, stats);
-				stylesheetContent += "}";
-			} else if (rule.type == CSSRule.FONT_FACE_RULE && (media.includes("all") || media.includes("screen"))) {
-				const fontInfo = fontsDetails.get(getFontKey(rule.style));
+	function processFontFaceRules(cssRules, fontsDetails, media, stats) {
+		cssRules.forEach(cssRule => {
+			if (cssRule.type == "Atrule" && cssRule.name == "media") {
+				const mediaText = cssTree.generate(cssRule.prelude);
+				processFontFaceRules(cssRule.block.children, fontsDetails, mediaText, stats);
+			} else if (cssRule.type == "Atrule" && cssRule.name == "font-face" && (media.includes("all") || media.includes("screen"))) {
+				const fontInfo = fontsDetails.get(getFontKey(cssRule));
 				if (fontInfo) {
-					fontsDetails.delete(getFontKey(rule.style));
-					stylesheetContent += "@font-face {" + processFontFaceRule(rule, fontInfo, stats) + "}";
+					fontsDetails.delete(getFontKey(cssRule));
+					processFontFaceRule(cssRule, fontInfo, stats);
 				}
-			} else {
-				stylesheetContent += rule.cssText;
 			}
 		});
-		return stylesheetContent;
 	}
 
-	function processFontFaceRule(rule, fontInfo, stats) {
+	function processFontFaceRule(cssRule, fontInfo, stats) {
 		const fontTest = (fontSource, format) => !fontSource.src.startsWith(EMPTY_URL_SOURCE) && fontSource.format == format;
 		let woffFontFound = fontInfo.find(fontSource => fontTest(fontSource, "woff2-variations"));
 		if (!woffFontFound) {
@@ -272,53 +265,46 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 			}
 		}
 		stats.fonts.discarded -= fontInfo.length;
-		let cssText = "";
-		Array.from(rule.style).forEach(propertyName => {
-			cssText += propertyName + ":";
-			if (propertyName == "src") {
-				cssText += fontInfo.map(fontSource => fontSource.src).join(",");
-			} else {
-				cssText += rule.style.getPropertyValue(propertyName);
+		const removedNodes = [];
+		for (let node = cssRule.block.children.head; node; node = node.next) {
+			if (node.data.property == "src") {
+				removedNodes.push(node);
 			}
-			cssText += ";";
-		});
-		return cssText;
+		}
+		removedNodes.pop();
+		removedNodes.forEach(node => cssRule.block.children.remove(node));
+		const srcDeclaration = cssRule.block.children.filter(node => node.property == "src").tail;
+		srcDeclaration.data.value = cssTree.parse(fontInfo.map(fontSource => fontSource.src).join(","), { context: "value" });
 	}
 
-	function filterUnusedFonts(doc, rules, declaredFonts, unusedFonts, filteredUsedFonts, docContent) {
-		let stylesheetContent = "";
-		if (rules) {
-			Array.from(rules).forEach(rule => {
-				if (rule.media) {
-					stylesheetContent += "@media " + Array.prototype.join.call(rule.media, ",") + "{";
-					stylesheetContent += filterUnusedFonts(doc, rule.cssRules, declaredFonts, unusedFonts, filteredUsedFonts, docContent);
-					stylesheetContent += "}";
-				} else if (rule.type == CSSRule.FONT_FACE_RULE) {
-					if (rule.style) {
-						const familyName = removeQuotes(rule.style.getPropertyValue("font-family"));
-						if (familyName && !unusedFonts.find(fontInfo => fontInfo.familyName == familyName)) {
-							if (testUnicodeRange(docContent, rule.style.getPropertyValue("unicode-range")) && testUsedFont(rule, familyName, declaredFonts, filteredUsedFonts)) {
-								stylesheetContent += rule.cssText;
-							}
-						}
+	function filterUnusedFonts(rules, declaredFonts, unusedFonts, filteredUsedFonts, docContent) {
+		const removedRules = [];
+		for (let rule = rules.head; rule; rule = rule.next) {
+			const ruleData = rule.data;
+			if (ruleData.type == "Atrule" && ruleData.name == "media") {
+				filterUnusedFonts(ruleData.block.children, declaredFonts, unusedFonts, filteredUsedFonts, docContent);
+			} else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
+				const fontFamily = getFontFamily(getPropertyValue(ruleData, "font-family"));
+				if (fontFamily) {
+					const unicodeRange = getPropertyValue(ruleData, "unicode-range");
+					if (unusedFonts.find(fontInfo => fontInfo.fontFamily == fontFamily) || !testUnicodeRange(docContent, unicodeRange) || !testUsedFont(ruleData, fontFamily, declaredFonts, filteredUsedFonts)) {
+						removedRules.push(rule);
 					}
-				} else {
-					stylesheetContent += rule.cssText;
 				}
-			});
+			}
 		}
-		return stylesheetContent;
+		removedRules.forEach(rule => rules.remove(rule));
 	}
 
 	function testUsedFont(rule, familyName, declaredFonts, filteredUsedFonts) {
 		let test;
 		const optionalUsedFonts = filteredUsedFonts && filteredUsedFonts.get(familyName);
 		if (optionalUsedFonts && optionalUsedFonts.length) {
-			const fontStyle = rule.style.getPropertyValue("font-style") || "normal";
-			const fontWeight = getFontWeight(rule.style.getPropertyValue("font-weight")) || "400";
-			const fontVariant = rule.style.getPropertyValue("font-variant") || "normal";
+			const fontStyle = getPropertyValue(rule, "font-style") || "normal";
+			const fontWeight = getFontWeight(getPropertyValue(rule, "font-weight") || "400");
+			const fontVariant = getPropertyValue(rule, "font-variant") || "normal";
 			const declaredFontsWeights = declaredFonts
-				.filter(fontInfo => fontInfo.familyName == familyName && fontInfo.fontStyle == fontStyle && testFontVariant(fontInfo, fontVariant))
+				.filter(fontInfo => fontInfo.fontFamily == familyName && fontInfo.fontStyle == fontStyle && testFontVariant(fontInfo, fontVariant))
 				.map(fontInfo => fontInfo.fontWeight)
 				.sort((weight1, weight2) => weight1 - weight2);
 			const usedFontWeights = optionalUsedFonts.map(fontInfo => findFontWeight(fontInfo.fontWeight, declaredFontsWeights));
@@ -363,20 +349,17 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 		return fontWeights.find(weight => weight > fontWeight);
 	}
 
-	function getPseudoElementsContent(doc, rules) {
-		if (rules) {
-			return Array.from(rules).map(rule => {
-				if (rule.type == CSSRule.MEDIA_RULE) {
-					return getPseudoElementsContent(doc, rule.cssRules);
-				} else if (rule.type == CSSRule.STYLE_RULE && testPseudoElements(rule.selectorText)) {
-					let content = rule.style.getPropertyValue("content");
-					content = content && removeQuotes(content);
-					return content;
+	function getPseudoElementsContent(cssRules) {
+		return cssRules.toArray().map(cssRule => {
+			if (cssRule.type == "Atrule" && cssRule.name == "media") {
+				return getPseudoElementsContent(cssRule.block.children);
+			} else if (cssRule.type == "Rule") {
+				const selector = cssTree.generate(cssRule.prelude); // TODO use OM
+				if (testPseudoElements(selector)) {
+					return getPropertyValue(cssRule, "content");
 				}
-			}).join("");
-		} else {
-			return "";
-		}
+			}
+		}).join("");
 	}
 
 	function testFontVariant(fontInfo, fontVariant) {
@@ -386,28 +369,35 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 	function testUnicodeRange(docContent, unicodeRange) {
 		if (unicodeRange) {
 			const unicodeRanges = unicodeRange.split(REGEXP_COMMA);
+			let invalid;
 			const result = unicodeRanges.filter(rangeValue => {
 				const range = rangeValue.split(REGEXP_DASH);
+				let regExpString;
 				if (range.length == 2) {
 					range[0] = transformRange(range[0]);
-					const regExpString = "[" + range[0] + "-" + transformRange("U+" + range[1]) + "]";
-					return (new RegExp(regExpString, "u")).test(docContent);
+					regExpString = "[" + range[0] + "-" + transformRange("U+" + range[1]) + "]";
 				}
 				if (range.length == 1) {
 					if (range[0].includes("?")) {
 						const firstRange = transformRange(range[0]);
 						const secondRange = firstRange;
-						const regExpString = "[" + firstRange.replace(REGEXP_QUESTION_MARK, "0") + "-" + secondRange.replace(REGEXP_QUESTION_MARK, "F") + "]";
-						return (new RegExp(regExpString, "u")).test(docContent);
+						regExpString = "[" + firstRange.replace(REGEXP_QUESTION_MARK, "0") + "-" + secondRange.replace(REGEXP_QUESTION_MARK, "F") + "]";
 
 					} else {
-						const regExpString = "[" + transformRange(range[0]) + "]";
+						regExpString = "[" + transformRange(range[0]) + "]";
+					}
+				}
+				if (regExpString) {
+					try {
 						return (new RegExp(regExpString, "u")).test(docContent);
+					} catch (error) {
+						invalid = true;
+						return false;
 					}
 				}
 				return true;
 			});
-			return result.length;
+			return !invalid && (!unicodeRanges.length || result.length);
 		}
 		return true;
 	}
@@ -432,20 +422,20 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 		return "\\u{" + range + "}";
 	}
 
-	function getFontKey(style) {
+	function getFontKey(cssRule) {
 		return JSON.stringify([
-			removeQuotes(style.getPropertyValue("font-family")),
-			getFontWeight(style.getPropertyValue("font-weight")),
-			style.getPropertyValue("font-style"),
-			style.getPropertyValue("unicode-range"),
-			getFontStretch(style.getPropertyValue("font-stretch")),
-			style.getPropertyValue("font-variant"),
-			style.getPropertyValue("font-feature-settings"),
-			style.getPropertyValue("font-variation-settings")
+			getFontFamily(getPropertyValue(cssRule, "font-family")),
+			getFontWeight(getPropertyValue(cssRule, "font-weight") || "400"),
+			getPropertyValue(cssRule, "font-style") || "normal",
+			getPropertyValue(cssRule, "unicode-range"),
+			getFontStretch(getPropertyValue(cssRule, "font-stretch")),
+			getPropertyValue(cssRule, "font-variant") || "normal",
+			getPropertyValue(cssRule, "font-feature-settings"),
+			getPropertyValue(cssRule, "font-variation-settings")
 		]);
 	}
 
-	function removeQuotes(string) {
+	function getFontFamily(string) {
 		string = string.toLowerCase().trim();
 		if (string.match(REGEXP_SIMPLE_QUOTES_STRING)) {
 			string = string.replace(REGEXP_SIMPLE_QUOTES_STRING, "$1");

+ 314 - 0
lib/single-file/css-matched-rules.js

@@ -0,0 +1,314 @@
+/*
+ * 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 cssTree */
+
+this.matchedRules = this.matchedRules || (() => {
+
+	const MEDIA_ALL = "all";
+	const PSEUDO_CLASSES_AS_ELEMENTS = ["after", "before"];
+	const IGNORED_PSEUDO_CLASSES = ["blank", "current", "dir", "drop", "first", "focus-visible", "future", "has", "host-context", "left", "matches", "read-only", "read-write", "right",];
+	const DEBUG = false;
+
+	class MatchedRules {
+		constructor(doc, docStyle) {
+			this.doc = doc;
+			this.mediaAllInfo = createMediaInfo(MEDIA_ALL);
+			const matchedElementsCache = new Map();
+			let sheetIndex = 0;
+			docStyle.stylesheets.forEach(stylesheetInfo => {
+				if (stylesheetInfo.media && stylesheetInfo.media != MEDIA_ALL) {
+					const mediaInfo = createMediaInfo(stylesheetInfo.media);
+					this.mediaAllInfo.medias.set("style-" + sheetIndex + "-" + stylesheetInfo.media, mediaInfo);
+					getMatchedElementsRules(doc, stylesheetInfo.stylesheet.children, mediaInfo, sheetIndex, docStyle, matchedElementsCache);
+				} else {
+					getMatchedElementsRules(doc, stylesheetInfo.stylesheet.children, this.mediaAllInfo, sheetIndex, docStyle, matchedElementsCache);
+				}
+				sheetIndex++;
+			});
+			let startTime;
+			if (DEBUG) {
+				startTime = Date.now();
+				log("  -- STARTED sortRules");
+			}
+			sortRules(this.mediaAllInfo);
+			if (DEBUG) {
+				log("  -- ENDED sortRules", Date.now() - startTime);
+				startTime = Date.now();
+				log("  -- STARTED computeCascade");
+			}
+			computeCascade(this.mediaAllInfo, [], this.mediaAllInfo);
+			if (DEBUG) {
+				log("  -- ENDED computeCascade", Date.now() - startTime);
+			}
+		}
+
+		getMediaAllInfo() {
+			return this.mediaAllInfo;
+		}
+	}
+
+	return {
+		getMediaAllInfo(doc, docStyle) {
+			return new MatchedRules(doc, docStyle).getMediaAllInfo();
+		}
+	};
+
+	function createMediaInfo(media) {
+		const mediaInfo = { media: media, elements: new Map(), pseudos: new Map(), medias: new Map(), rules: new Map(), pseudoSelectors: new Set() };
+		if (media == MEDIA_ALL) {
+			mediaInfo.matchedStyles = new Map();
+		}
+		return mediaInfo;
+	}
+
+	function getMatchedElementsRules(doc, cssRules, mediaInfo, sheetIndex, docStyle, matchedElementsCache) {
+		let mediaIndex = 0;
+		let ruleIndex = 0;
+		let startTime;
+		if (DEBUG && cssRules.length > 1) {
+			startTime = Date.now();
+			log("  -- STARTED getMatchedElementsRules", " index =", sheetIndex, "rules.length =", cssRules.length);
+		}
+		cssRules.forEach(cssRule => {
+			if (cssRule.type == "Atrule" && cssRule.name == "media") {
+				const mediaText = cssTree.generate(cssRule.prelude);
+				const ruleMediaInfo = createMediaInfo(mediaText);
+				mediaInfo.medias.set("rule-" + sheetIndex + "-" + mediaIndex + "-" + mediaText, ruleMediaInfo);
+				mediaIndex++;
+				getMatchedElementsRules(doc, cssRule.block.children, ruleMediaInfo, sheetIndex, docStyle, matchedElementsCache);
+			} else if (cssRule.type == "Rule" && cssRule.prelude.children) {
+				const selectors = cssRule.prelude.children.toArray();
+				const selectorsText = cssRule.prelude.children.toArray().map(selector => cssTree.generate(selector));
+				const ruleInfo = { cssRule, mediaInfo, ruleIndex, sheetIndex, matchedSelectors: new Set(), style: new Map(), selectors, selectorsText };
+				ruleIndex++;
+				for (let selector = cssRule.prelude.children.head, selectorIndex = 0; selector; selector = selector.next, selectorIndex++) {
+					const selectorText = selectorsText[selectorIndex];
+					const selectorInfo = { selector, selectorText, ruleInfo };
+					getMatchedElementsSelector(doc, selectorInfo, docStyle, matchedElementsCache);
+				}
+			}
+		});
+		if (DEBUG && cssRules.length > 1) {
+			log("  -- ENDED   getMatchedElementsRules", "delay =", Date.now() - startTime);
+		}
+	}
+
+	function getMatchedElementsSelector(doc, selectorInfo, docStyle, matchedElementsCache) {
+		let selectorText;
+		const selectorData = cssTree.parse(cssTree.generate(selectorInfo.selector.data), { context: "selector" });
+		const filteredSelectorText = getFilteredSelector({ data: selectorData });
+		if (filteredSelectorText != selectorInfo.selectorText) {
+			selectorText = filteredSelectorText;
+		} else {
+			selectorText = selectorInfo.selectorText;
+		}
+		const cachedMatchedElements = matchedElementsCache.get(selectorText);
+		let matchedElements = cachedMatchedElements;
+		if (!matchedElements) {
+			try {
+				matchedElements = doc.querySelectorAll(selectorText);
+			} catch (error) {
+				// ignored
+				console.warn("(SingleFile) Invalid selector", selectorText);
+			}
+		}
+		if (matchedElements) {
+			if (!cachedMatchedElements) {
+				matchedElementsCache.set(selectorText, matchedElements);
+			}
+			if (matchedElements.length) {
+				if (filteredSelectorText == selectorInfo.selectorText) {
+					matchedElements.forEach(element => addRule(element, selectorInfo, docStyle.styles));
+				} else {
+					selectorInfo.ruleInfo.mediaInfo.pseudoSelectors.add(selectorInfo.ruleInfo.cssRule);
+					matchedElements.forEach(element => addPseudoRule(element, selectorInfo));
+				}
+			}
+		}
+	}
+
+	function getFilteredSelector(selector) {
+		const removedSelectors = [];
+		filterPseudoClasses(selector);
+		if (removedSelectors.length) {
+			removedSelectors.forEach(({ parentSelector, selector }) => {
+				if (parentSelector.data.children.getSize() == 0 || !selector.prev || selector.prev.data.type == "Combinator") {
+					parentSelector.data.children.replace(selector, cssTree.parse("*", { context: "selector" }).children.head);
+				} else {
+					parentSelector.data.children.remove(selector);
+				}
+			});
+		}
+		const selectorText = cssTree.generate(selector.data).trim();
+		return selectorText;
+
+		function filterPseudoClasses(selector, parentSelector) {
+			if (selector.data.children) {
+				for (let childSelector = selector.data.children.head; childSelector; childSelector = childSelector.next) {
+					filterPseudoClasses(childSelector, selector);
+				}
+			}
+			if ((selector.data.type == "PseudoClassSelector" && (testVendorPseudo(selector) || IGNORED_PSEUDO_CLASSES.includes(selector.data.name))) ||
+				(selector.data.type == "PseudoElementSelector" && (testVendorPseudo(selector) || PSEUDO_CLASSES_AS_ELEMENTS.includes(selector.data.name)))) {
+				removedSelectors.push({ parentSelector, selector });
+			}
+		}
+
+		function testVendorPseudo(selector) {
+			const name = selector.data.name;
+			return name.startsWith("-") || name.startsWith("\\-");
+		}
+	}
+
+	function addRule(element, selectorInfo, styles) {
+		const mediaInfo = selectorInfo.ruleInfo.mediaInfo;
+		const elementStyle = styles.get(element);
+		let elementInfo = mediaInfo.elements.get(element);
+		if (!elementInfo) {
+			elementInfo = [];
+			if (elementStyle) {
+				elementInfo.push({ styleInfo: { cssStyle: elementStyle, style: new Map() } });
+			}
+			mediaInfo.elements.set(element, elementInfo);
+		}
+		const specificity = computeSpecificity(selectorInfo.selector.data);
+		specificity.ruleIndex = selectorInfo.ruleInfo.ruleIndex;
+		specificity.sheetIndex = selectorInfo.ruleInfo.sheetIndex;
+		selectorInfo.specificity = specificity;
+		elementInfo.push(selectorInfo);
+	}
+
+	function addPseudoRule(element, selectorInfo) {
+		let elementInfo = selectorInfo.ruleInfo.mediaInfo.pseudos.get(element);
+		if (!elementInfo) {
+			elementInfo = [];
+			selectorInfo.ruleInfo.mediaInfo.pseudos.set(element, elementInfo);
+		}
+		elementInfo.push(selectorInfo);
+	}
+
+	function computeCascade(mediaInfo, parentMediaInfos, mediaAllInfo) {
+		mediaInfo.elements.forEach((elementInfo) => getStylesInfo(elementInfo).forEach((elementStyleInfo, styleName) => {
+			if (elementStyleInfo.selectorInfo.ruleInfo || mediaInfo == mediaAllInfo) {
+				let info;
+				if (elementStyleInfo.selectorInfo.ruleInfo) {
+					info = elementStyleInfo.selectorInfo.ruleInfo;
+					const cssRule = info.cssRule;
+					const ascendantMedia = [mediaInfo, ...parentMediaInfos].find(media => media.rules.get(cssRule)) || mediaInfo;
+					ascendantMedia.rules.set(cssRule, info);
+					if (cssRule) {
+						info.matchedSelectors.add(elementStyleInfo.selectorInfo.selectorText);
+					}
+				} else {
+					info = elementStyleInfo.selectorInfo.styleInfo;
+					const cssStyle = info.cssStyle;
+					const matchedStyleInfo = mediaAllInfo.matchedStyles.get(cssStyle);
+					if (!matchedStyleInfo) {
+						mediaAllInfo.matchedStyles.set(cssStyle, info);
+					}
+				}
+				const styleValue = info.style.get(styleName);
+				if (!styleValue) {
+					info.style.set(styleName, elementStyleInfo.styleValue);
+				}
+			}
+		}));
+		delete mediaInfo.elements;
+		mediaInfo.medias.forEach(childMediaInfo => computeCascade(childMediaInfo, [mediaInfo, ...parentMediaInfos], mediaAllInfo));
+	}
+
+	function getStylesInfo(elementInfo) {
+		const elementStylesInfo = new Map();
+		elementInfo.forEach(selectorInfo => {
+			if (selectorInfo.styleInfo) {
+				const cssStyle = selectorInfo.styleInfo.cssStyle;
+				cssStyle.children.forEach(declaration => {
+					const styleValue = cssTree.generate(declaration);
+					elementStylesInfo.set(declaration.property, { selectorInfo, styleValue, important: declaration.important });
+				});
+			} else {
+				selectorInfo.ruleInfo.cssRule.block.children.forEach(declaration => {
+					const styleValue = cssTree.generate(declaration);
+					const elementStyleInfo = elementStylesInfo.get(declaration.property);
+					if (!elementStyleInfo || (declaration.important && !elementStyleInfo.important)) {
+						elementStylesInfo.set(declaration.property, { selectorInfo, styleValue, important: declaration.important });
+					}
+				});
+			}
+		});
+		return elementStylesInfo;
+	}
+
+	function sortRules(media) {
+		media.elements.forEach(elementRules => elementRules.sort((ruleInfo1, ruleInfo2) =>
+			ruleInfo1.styleInfo && !ruleInfo2.styleInfo ? -1 :
+				!ruleInfo1.styleInfo && ruleInfo2.styleInfo ? 1 :
+					compareSpecificity(ruleInfo1.specificity, ruleInfo2.specificity)));
+		media.medias.forEach(sortRules);
+	}
+
+	function computeSpecificity(selector, specificity = { a: 0, b: 0, c: 0 }) {
+		if (selector.type == "IdSelector") {
+			specificity.a++;
+		}
+		if (selector.type == "ClassSelector" || selector.type == "AttributeSelector" || (selector.type == "PseudoClassSelector" && selector.name != "not")) {
+			specificity.b++;
+		}
+		if ((selector.type == "TypeSelector" && selector.name != "*") || selector.type == "PseudoElementSelector") {
+			specificity.c++;
+		}
+		if (selector.children) {
+			selector.children.forEach(selector => computeSpecificity(selector, specificity));
+		}
+		return specificity;
+	}
+
+	function compareSpecificity(specificity1, specificity2) {
+		if (specificity1.a > specificity2.a) {
+			return -1;
+		} else if (specificity1.a < specificity2.a) {
+			return 1;
+		} else if (specificity1.b > specificity2.b) {
+			return -1;
+		} else if (specificity1.b < specificity2.b) {
+			return 1;
+		} else if (specificity1.c > specificity2.c) {
+			return -1;
+		} else if (specificity1.c < specificity2.c) {
+			return 1;
+		} else if (specificity1.sheetIndex > specificity2.sheetIndex) {
+			return -1;
+		} else if (specificity1.sheetIndex < specificity2.sheetIndex) {
+			return 1;
+		} else if (specificity1.ruleIndex > specificity2.ruleIndex) {
+			return -1;
+		} else if (specificity1.ruleIndex < specificity2.ruleIndex) {
+			return 1;
+		} else {
+			return -1;
+		}
+	}
+
+	function log(...args) {
+		console.log("S-File <css-mat>", ...args); // eslint-disable-line no-console
+	}
+
+})();

+ 20 - 23
lib/single-file/css-medias-minifier.js

@@ -18,43 +18,40 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/* global CSSRule, mediaQueryParser */
+/* global cssTree, mediaQueryParser */
 
 this.mediasMinifier = this.mediasMinifier || (() => {
 
 	return {
-		process: doc => {
+		process: stylesheets => {
 			const stats = { processed: 0, discarded: 0 };
-			doc.querySelectorAll("style").forEach(styleElement => {
-				if (styleElement.sheet) {
-					styleElement.textContent = processRules(doc, styleElement.sheet.cssRules, styleElement.media || "all", stats);
+			stylesheets.forEach((stylesheet, element) => {
+				const media = stylesheet.media || "all";
+				if (matchesMediaType(media, "screen")) {
+					const removedRules = processRules(stylesheet.stylesheet.children, stats);
+					removedRules.forEach(({ cssRules, cssRule }) => cssRules.remove(cssRule));
+				} else {
+					stylesheets.delete(element);
 				}
 			});
 			return stats;
 		}
 	};
 
-	function processRules(doc, cssRules, media, stats) {
-		let sheetContent = "";
-		if (matchesMediaType(media, "screen")) {
-			Array.from(cssRules).forEach(cssRule => {
-				if (cssRule.type == CSSRule.MEDIA_RULE) {
-					stats.processed++;
-					if (matchesMediaType(cssRule.media.mediaText, "screen")) {
-						if (cssRule.cssRules.length) {
-							sheetContent += "@media " + Array.from(cssRule.media).join(",") + "{";
-							sheetContent += processRules(doc, cssRule.cssRules, cssRule.media.mediaText, stats);
-							sheetContent += "}";
-						}
-					} else {
-						stats.discarded++;
-					}
+	function processRules(cssRules, stats, removedRules = []) {
+		for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
+			const cssRuleData = cssRule.data;
+			if (cssRuleData.type == "Atrule" && cssRuleData.name == "media") {
+				stats.processed++;
+				if (matchesMediaType(cssTree.generate(cssRuleData.prelude), "screen")) {
+					processRules(cssRuleData.block.children, stats, removedRules);
 				} else {
-					sheetContent += cssRule.cssText;
+					removedRules.push({ cssRules, cssRule });
+					stats.discarded++;
 				}
-			});
+			}
 		}
-		return sheetContent;
+		return removedRules;
 	}
 
 	function flatten(array) {

+ 1 - 1
lib/single-file/css-minifier.js

@@ -31,7 +31,7 @@
  * by Yahoo! Inc. under the BSD (revised) open source license.
  */
 
-this.uglifycss = this.uglifycss || (() => {
+this.cssMinifier = this.cssMinifier || (() => {
 
 	/**
 	 * @type {string} - placeholder prefix

+ 0 - 340
lib/single-file/css-rules-matcher.js

@@ -1,340 +0,0 @@
-/*
- * 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 */
-
-this.RulesMatcher = this.RulesMatcher || (() => {
-
-	const MEDIA_ALL = "all";
-	const SEPARATOR_TYPES = ["descendant", "child", "sibling", "adjacent"];
-	const PSEUDOS_CLASSES = [":focus", ":focus-within", ":hover", ":link", ":visited", ":active"];
-	const SELECTOR_TOKEN_TYPE_TAG = "tag";
-	const SELECTOR_TOKEN_TYPE_ATTRIBUTE = "attribute";
-	const SELECTOR_TOKEN_TYPE_PSEUDO = "pseudo";
-	const SELECTOR_TOKEN_TYPE_PSEUDO_ELEMENT = "pseudo-element";
-	const SELECTOR_TOKEN_NAME_ID = "id";
-	const SELECTOR_TOKEN_NAME_CLASS = "class";
-	const SELECTOR_TOKEN_NAME_NOT = "not";
-	const SELECTOR_TOKEN_ACTION_EQUALS = "equals";
-	const SELECTOR_TOKEN_ACTION_ELEMENT = "element";
-	const SELECTOR_TOKEN_VALUE_STAR = "*";
-	const DEBUG = false;
-
-	class RulesMatcher {
-		constructor(doc) {
-			this.doc = doc;
-			this.mediaAllInfo = createMediaInfo(MEDIA_ALL);
-			const matchedElementsCache = new Map();
-			doc.querySelectorAll("style").forEach((styleElement, sheetIndex) => {
-				if (styleElement.sheet) {
-					const cssRules = styleElement.sheet.cssRules;
-					if (styleElement.media && styleElement.media != MEDIA_ALL) {
-						const mediaInfo = createMediaInfo(styleElement.media);
-						this.mediaAllInfo.medias.set("style-" + sheetIndex + "-" + styleElement.media, mediaInfo);
-						getMatchedElementsRules(doc, cssRules, mediaInfo, sheetIndex, matchedElementsCache);
-					} else {
-						getMatchedElementsRules(doc, cssRules, this.mediaAllInfo, sheetIndex, matchedElementsCache);
-					}
-				}
-			});
-			let startTime;
-			if (DEBUG) {
-				startTime = Date.now();
-				log("  -- STARTED sortRules");
-			}
-			sortRules(this.mediaAllInfo);
-			if (DEBUG) {
-				log("  -- ENDED sortRules", Date.now() - startTime);
-				startTime = Date.now();
-				log("  -- STARTED computeCascade");
-			}
-			computeCascade(this.mediaAllInfo, [], this.mediaAllInfo);
-			if (DEBUG) {
-				log("  -- ENDED computeCascade", Date.now() - startTime);
-			}
-		}
-
-		getMediaAllInfo() {
-			return this.mediaAllInfo;
-		}
-
-		getMatchedRules(element) {
-			this.mediaAllInfo.elements.get(element);
-		}
-	}
-
-	return {
-		create(doc) {
-			return new RulesMatcher(doc);
-		}
-	};
-
-	function getMatchedElementsRules(doc, cssRules, mediaInfo, sheetIndex, matchedElementsCache) {
-		let mediaIndex = 0;
-		let startTime;
-		if (DEBUG && cssRules.length > 1) {
-			startTime = Date.now();
-			log("  -- STARTED getMatchedElementsRules", " index =", sheetIndex, "rules.length =", cssRules.length);
-		}
-		Array.from(cssRules).forEach((cssRule, ruleIndex) => {
-			const cssRuleType = cssRule.type;
-			if (cssRuleType == CSSRule.MEDIA_RULE) {
-				const cssRuleMedia = cssRule.media;
-				const ruleMediaInfo = createMediaInfo(cssRuleMedia);
-				mediaInfo.medias.set("rule-" + sheetIndex + "-" + mediaIndex + "-" + cssRuleMedia.mediaText, ruleMediaInfo);
-				mediaIndex++;
-				getMatchedElementsRules(doc, cssRule.cssRules, ruleMediaInfo, sheetIndex, matchedElementsCache);
-			} else if (cssRuleType == CSSRule.STYLE_RULE) {
-				const selectorText = cssRule.selectorText;
-				if (selectorText) {
-					let selectors;
-					try {
-						selectors = cssWhat.parse(selectorText.trim());
-					} catch (error) {
-						/* ignored */
-					}
-					if (selectors) {
-						const selectorsText = selectors.map(selector => cssWhat.stringify([selector]));
-						const ruleInfo = { cssRule, mediaInfo, ruleIndex, sheetIndex, matchedSelectors: new Set(), style: new Map(), selectors, selectorsText };
-						selectors.forEach((selector, selectorIndex) => {
-							const selectorText = selectorsText[selectorIndex];
-							const selectorInfo = { selector, selectorText, ruleInfo };
-							getMatchedElementsSelector(doc, selectorInfo, matchedElementsCache);
-						});
-					}
-				}
-			}
-		});
-		if (DEBUG && cssRules.length > 1) {
-			log("  -- ENDED   getMatchedElementsRules", "delay =", Date.now() - startTime);
-		}
-	}
-
-	function getMatchedElementsSelector(doc, selectorInfo, matchedElementsCache) {
-		let selectorText;
-		const filteredSelectorText = getFilteredSelector(selectorInfo.selectorText);
-		if (filteredSelectorText != selectorInfo.selectorText) {
-			selectorText = filteredSelectorText;
-		} else {
-			selectorText = selectorInfo.selectorText;
-		}
-		const cachedMatchedElements = matchedElementsCache.get(selectorText);
-		const matchedElements = cachedMatchedElements || doc.querySelectorAll(selectorText);
-		if (!cachedMatchedElements) {
-			matchedElementsCache.set(selectorText, matchedElements);
-		}
-		if (matchedElements.length) {
-			if (filteredSelectorText != selectorInfo.selectorText) {
-				selectorInfo.ruleInfo.mediaInfo.pseudoSelectors.add(selectorInfo.ruleInfo.cssRule.selectorText);
-				matchedElements.forEach(element => addPseudoRule(element, selectorInfo));
-			} else {
-				matchedElements.forEach(element => addRule(element, selectorInfo));
-			}
-		}
-	}
-
-	function addRule(element, selectorInfo) {
-		const mediaInfo = selectorInfo.ruleInfo.mediaInfo;
-		let elementInfo = mediaInfo.elements.get(element);
-		if (!elementInfo) {
-			elementInfo = [];
-			const elementStyle = element.style;
-			if (elementStyle && elementStyle.length) {
-				elementInfo.push({ styleInfo: { cssStyle: elementStyle, style: new Map() } });
-			}
-			mediaInfo.elements.set(element, elementInfo);
-		}
-		const specificity = computeSpecificity(selectorInfo.selector);
-		specificity.ruleIndex = selectorInfo.ruleInfo.ruleIndex;
-		specificity.sheetIndex = selectorInfo.ruleInfo.sheetIndex;
-		selectorInfo.specificity = specificity;
-		elementInfo.push(selectorInfo);
-	}
-
-	function addPseudoRule(element, selectorInfo) {
-		let elementInfo = selectorInfo.ruleInfo.mediaInfo.pseudos.get(element);
-		if (!elementInfo) {
-			elementInfo = [];
-			selectorInfo.ruleInfo.mediaInfo.pseudos.set(element, elementInfo);
-		}
-		elementInfo.push(selectorInfo);
-	}
-
-	function computeCascade(mediaInfo, parentMediaInfos, mediaAllInfo) {
-		mediaInfo.elements.forEach(elementInfo => getStylesInfo(elementInfo).forEach((elementStyleInfo, styleName) => {
-			if (elementStyleInfo.selectorInfo.ruleInfo) {
-				const ruleInfo = elementStyleInfo.selectorInfo.ruleInfo;
-				const cssRule = ruleInfo.cssRule;
-				const ascendantMedia = [mediaInfo, ...parentMediaInfos].find(media => media.rules.get(cssRule)) || mediaInfo;
-				ascendantMedia.rules.set(cssRule, ruleInfo);
-				if (cssRule) {
-					ruleInfo.matchedSelectors.add(elementStyleInfo.selectorInfo.selectorText);
-				}
-				const styleValue = ruleInfo.style.get(styleName);
-				if (!styleValue) {
-					ruleInfo.style.set(styleName, elementStyleInfo.styleValue);
-				}
-			} else if (mediaInfo == mediaAllInfo) {
-				const styleInfo = elementStyleInfo.selectorInfo.styleInfo;
-				const cssStyle = styleInfo.cssStyle;
-				const matchedStyleInfo = mediaAllInfo.matchedStyles.get(cssStyle);
-				if (!matchedStyleInfo) {
-					mediaAllInfo.matchedStyles.set(cssStyle, styleInfo);
-				}
-				const styleValue = styleInfo.style.get(styleName);
-				if (!styleValue) {
-					styleInfo.style.set(styleName, elementStyleInfo.styleValue);
-				}
-			}
-		}));
-		mediaInfo.medias.forEach(childMediaInfo => computeCascade(childMediaInfo, [mediaInfo, ...parentMediaInfos], mediaAllInfo));
-	}
-
-	function getStylesInfo(elementInfo) {
-		const elementStylesInfo = new Map();
-		elementInfo.forEach(selectorInfo => {
-			if (selectorInfo.styleInfo) {
-				const cssStyle = selectorInfo.styleInfo.cssStyle;
-				const stylesInfo = parseCss.parseAListOfDeclarations(cssStyle.cssText);
-				stylesInfo.forEach(styleInfo => {
-					const important = cssStyle.getPropertyPriority(styleInfo.name);
-					const styleValue = cssStyle.getPropertyValue(styleInfo.name) + (important && "!" + important);
-					elementStylesInfo.set(styleInfo.name, { selectorInfo, styleValue, important });
-				});
-			} else {
-				const cssStyle = selectorInfo.ruleInfo.cssRule.style;
-				const stylesInfo = parseCss.parseAListOfDeclarations(cssStyle.cssText);
-				stylesInfo.forEach(styleInfo => {
-					const important = cssStyle.getPropertyPriority(styleInfo.name);
-					const styleValue = cssStyle.getPropertyValue(styleInfo.name) + (important && "!" + important);
-					const elementStyleInfo = elementStylesInfo.get(styleInfo.name);
-					if (!elementStyleInfo || (important && !elementStyleInfo.important)) {
-						elementStylesInfo.set(styleInfo.name, { selectorInfo, styleValue, important });
-					}
-				});
-			}
-		});
-		return elementStylesInfo;
-	}
-
-	function createMediaInfo(media) {
-		const mediaInfo = { media: media, elements: new Map(), pseudos: new Map(), medias: new Map(), rules: new Map(), pseudoSelectors: new Set() };
-		if (media == MEDIA_ALL) {
-			mediaInfo.matchedStyles = new Map();
-		}
-		return mediaInfo;
-	}
-
-	function sortRules(media) {
-		media.elements.forEach(elementRules => elementRules.sort((ruleInfo1, ruleInfo2) =>
-			ruleInfo1.styleInfo && !ruleInfo2.styleInfo ? -1 :
-				!ruleInfo1.styleInfo && ruleInfo2.styleInfo ? 1 :
-					compareSpecificity(ruleInfo1.specificity, ruleInfo2.specificity)));
-		media.medias.forEach(sortRules);
-	}
-
-	function computeSpecificity(selector, specificity = { a: 0, b: 0, c: 0 }) {
-		selector.forEach(token => {
-			if (token.expandedSelector && token.type == SELECTOR_TOKEN_TYPE_ATTRIBUTE && token.name == SELECTOR_TOKEN_NAME_ID && token.action == SELECTOR_TOKEN_ACTION_EQUALS) {
-				specificity.a++;
-			}
-			if ((!token.expandedSelector && token.type == SELECTOR_TOKEN_TYPE_ATTRIBUTE) ||
-				(token.expandedSelector && token.type == SELECTOR_TOKEN_TYPE_ATTRIBUTE && token.name == SELECTOR_TOKEN_NAME_CLASS && token.action == SELECTOR_TOKEN_ACTION_ELEMENT) ||
-				(token.type == SELECTOR_TOKEN_TYPE_PSEUDO && token.name != SELECTOR_TOKEN_NAME_NOT)) {
-				specificity.b++;
-			}
-			if ((token.type == SELECTOR_TOKEN_TYPE_TAG && token.value != SELECTOR_TOKEN_VALUE_STAR) || (token.type == SELECTOR_TOKEN_TYPE_PSEUDO_ELEMENT)) {
-				specificity.c++;
-			}
-			if (token.data) {
-				if (Array.isArray(token.data)) {
-					token.data.forEach(selector => computeSpecificity(selector, specificity));
-				}
-			}
-		});
-		return specificity;
-	}
-
-	function compareSpecificity(specificity1, specificity2) {
-		if (specificity1.a > specificity2.a) {
-			return -1;
-		} else if (specificity1.a < specificity2.a) {
-			return 1;
-		} else if (specificity1.b > specificity2.b) {
-			return -1;
-		} else if (specificity1.b < specificity2.b) {
-			return 1;
-		} else if (specificity1.c > specificity2.c) {
-			return -1;
-		} else if (specificity1.c < specificity2.c) {
-			return 1;
-		} else if (specificity1.sheetIndex > specificity2.sheetIndex) {
-			return -1;
-		} else if (specificity1.sheetIndex < specificity2.sheetIndex) {
-			return 1;
-		} else if (specificity1.ruleIndex > specificity2.ruleIndex) {
-			return -1;
-		} else if (specificity1.ruleIndex < specificity2.ruleIndex) {
-			return 1;
-		} else {
-			return -1;
-		}
-	}
-
-	function getFilteredSelector(selector) {
-		let selectors;
-		try {
-			selectors = cssWhat.parse(selector.trim());
-		} catch (error) {
-			return selector;
-		}
-		return cssWhat.stringify(selectors.map(selector => filterPseudoClasses(selector)));
-
-		function filterPseudoClasses(selector, negatedData) {
-			const tokens = selector.filter(token => {
-				if (token.data) {
-					if (Array.isArray(token.data)) {
-						token.data = token.data.map(selector => filterPseudoClasses(selector, token.name == "not" && token.type == "pseudo"));
-					}
-				}
-				return negatedData || ((token.type != "pseudo" || !PSEUDOS_CLASSES.includes(":" + token.name))
-					&& (token.type != "pseudo-element"));
-			});
-			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;
-		}
-	}
-
-	function log(...args) {
-		console.log("S-File <css-mat>", ...args); // eslint-disable-line no-console
-	}
-
-})();

+ 53 - 81
lib/single-file/css-rules-minifier.js

@@ -18,45 +18,36 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/* global CSSRule, parseCss */
+/* global cssTree */
 
-this.cssMinifier = this.cssMinifier || (() => {
+this.cssRulesMinifier = this.cssRulesMinifier || (() => {
 
 	const DEBUG = false;
 
 	return {
-		process: (doc, mediaAllInfo) => {
+		process: (stylesheets, styles, mediaAllInfo) => {
 			const stats = { processed: 0, discarded: 0 };
-			doc.querySelectorAll("style").forEach((styleElement, sheetIndex) => {
-				if (styleElement.sheet) {
-					const cssRules = styleElement.sheet.cssRules;
-					let mediaInfo;
-					if (styleElement.media && styleElement.media != "all") {
-						mediaInfo = mediaAllInfo.medias.get("style-" + sheetIndex + "-" + styleElement.media);
-					} else {
-						mediaInfo = mediaAllInfo;
-					}
-					stats.processed += cssRules.length;
-					stats.discarded += cssRules.length;
-					styleElement.textContent = processRules(doc, cssRules, sheetIndex, mediaInfo);
-					stats.discarded -= cssRules.length;
+			let sheetIndex = 0;
+			stylesheets.forEach(stylesheetInfo => {
+				let mediaInfo;
+				if (stylesheetInfo.media && stylesheetInfo.media != "all") {
+					mediaInfo = mediaAllInfo.medias.get("style-" + sheetIndex + "-" + stylesheetInfo.media);
+				} else {
+					mediaInfo = mediaAllInfo;
 				}
+				const cssRules = stylesheetInfo.stylesheet.children;
+				stats.processed += cssRules.getSize();
+				stats.discarded += cssRules.getSize();
+				processRules(cssRules, sheetIndex, mediaInfo);
+				sheetIndex++;
+				stats.discarded -= stylesheetInfo.stylesheet.children.getSize();
 			});
 			let startTime;
 			if (DEBUG) {
 				startTime = Date.now();
 				log("  -- STARTED processStyleAttribute");
 			}
-			doc.querySelectorAll("[style]").forEach(element => {
-				if (element.style) {
-					const textContent = processStyleAttribute(element.style, mediaAllInfo);
-					if (textContent) {
-						element.setAttribute("style", textContent);
-					} else {
-						element.removeAttribute("style");
-					}
-				}
-			});
+			styles.forEach(style => processStyleAttribute(style, mediaAllInfo));
 			if (DEBUG) {
 				log("  -- ENDED   processStyleAttribute delay =", Date.now() - startTime);
 			}
@@ -64,85 +55,66 @@ this.cssMinifier = this.cssMinifier || (() => {
 		}
 	};
 
-	function processRules(doc, cssRules, sheetIndex, mediaInfo) {
-		let sheetContent = "", mediaRuleIndex = 0, startTime;
+	function processRules(cssRules, sheetIndex, mediaInfo) {
+		let mediaRuleIndex = 0, startTime;
 		if (DEBUG && cssRules.length > 1) {
 			startTime = Date.now();
-			log("  -- STARTED processRules", "rules.length =", cssRules.length);
+			log("  -- STARTED processRules", "rules.length =", cssRules.children.toArray().length);
 		}
-		Array.from(cssRules).forEach(cssRule => {
-			if (cssRule.type == CSSRule.MEDIA_RULE) {
-				sheetContent += "@media " + Array.from(cssRule.media).join(",") + "{";
-				sheetContent += processRules(doc, cssRule.cssRules, sheetIndex, mediaInfo.medias.get("rule-" + sheetIndex + "-" + mediaRuleIndex + "-" + cssRule.media.mediaText));
+		const removedCssRules = [];
+		for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
+			const cssRuleData = cssRule.data;
+			if (cssRuleData.type == "Atrule" && cssRuleData.name == "media") {
+				const mediaText = cssTree.generate(cssRuleData.prelude);
+				processRules(cssRuleData.block.children, sheetIndex, mediaInfo.medias.get("rule-" + sheetIndex + "-" + mediaRuleIndex + "-" + mediaText));
 				mediaRuleIndex++;
-				sheetContent += "}";
-			} else if (cssRule.type == CSSRule.STYLE_RULE) {
-				const ruleInfo = mediaInfo.rules.get(cssRule);
-				if (mediaInfo.pseudoSelectors.has(cssRule.selectorText)) {
-					sheetContent += cssRule.cssText;
+			} else if (cssRuleData.type == "Rule") {
+				const ruleInfo = mediaInfo.rules.get(cssRuleData);
+				if (!ruleInfo && !mediaInfo.pseudoSelectors.has(cssRuleData)) {
+					removedCssRules.push(cssRule);
 				} else if (ruleInfo) {
-					sheetContent += processRuleInfo(cssRule, ruleInfo);
+					processRuleInfo(cssRuleData, ruleInfo);
+					if (!cssRuleData.prelude.children.getSize() || !cssRuleData.block.children.getSize()) {
+						removedCssRules.push(cssRule);
+					}
 				}
-			} else {
-				sheetContent += cssRule.cssText;
 			}
-		});
+		}
+		removedCssRules.forEach(cssRule => cssRules.remove(cssRule));
 		if (DEBUG && cssRules.length > 1) {
 			log("  -- ENDED   processRules delay =", Date.now() - startTime);
 		}
-		return sheetContent;
 	}
 
 	function processRuleInfo(cssRule, ruleInfo) {
-		let selectorText = "", styleCssText = "";
-		const stylesInfo = parseCss.parseAListOfDeclarations(cssRule.style.cssText);
-		for (let styleIndex = 0; styleIndex < stylesInfo.length; styleIndex++) {
-			const style = stylesInfo[styleIndex];
-			if (ruleInfo.style.get(style.name)) {
-				if (styleCssText) {
-					styleCssText += ";";
-				}
-				const priority = cssRule.style.getPropertyPriority(style.name);
-				styleCssText += style.name + ":" + cssRule.style.getPropertyValue(style.name) + (priority && ("!" + priority));
+		const removedDeclarations = [];
+		const removedSelectors = [];
+		for (let declaration = cssRule.block.children.head; declaration; declaration = declaration.next) {
+			if (!ruleInfo.style.get(declaration.data.property)) {
+				removedDeclarations.push(declaration);
 			}
 		}
-		if (ruleInfo.matchedSelectors.size < ruleInfo.selectorsText.length) {
-			const newSelectors = [];
-			const newSelectorsText = [];
-			for (let selectorTextIndex = 0; selectorTextIndex < ruleInfo.selectorsText.length; selectorTextIndex++) {
-				const ruleSelectorText = ruleInfo.selectorsText[selectorTextIndex];
-				if (ruleInfo.matchedSelectors.has(ruleSelectorText)) {
-					if (selectorText) {
-						selectorText += ",";
-					}
-					newSelectors.push(ruleInfo.selectors[selectorTextIndex]);
-					newSelectorsText.push(ruleSelectorText);
-					selectorText += ruleSelectorText;
-				}
+		for (let selector = cssRule.prelude.children.head; selector; selector = selector.next) {
+			if (!ruleInfo.selectorsText.includes(cssTree.generate(selector.data))) {
+				removedSelectors.push(selector);
 			}
 		}
-		return (selectorText || cssRule.selectorText) + "{" + styleCssText + "}";
+		removedDeclarations.forEach(declaration => cssRule.block.children.remove(declaration));
+		removedSelectors.forEach(selector => cssRule.prelude.children.remove(selector));
 	}
 
 	function processStyleAttribute(cssStyle, mediaAllInfo) {
-		let styleCssText = "";
+		const removedDeclarations = [];
 		const styleInfos = mediaAllInfo.matchedStyles.get(cssStyle);
 		if (styleInfos) {
-			styleInfos.style.forEach((styleValue, styleName) => {
-				const stylesInfo = parseCss.parseAListOfDeclarations(cssStyle.cssText);
-				for (let styleIndex = 0; styleIndex < stylesInfo.length; styleIndex++) {
-					const style = stylesInfo[styleIndex];
-					if (styleName == style.name) {
-						if (styleCssText) {
-							styleCssText += ";";
-						}
-						const priority = cssStyle.getPropertyPriority(style.name);
-						styleCssText += style.name + ":" + cssStyle.getPropertyValue(style.name) + (priority && ("!" + priority));
-					}
+			let propertyFound;
+			for (let declaration = cssStyle.children.head; declaration && !propertyFound; declaration = declaration.next) {
+				if (!styleInfos.style.get(declaration.data.property)) {
+					removedDeclarations.push(declaration);
 				}
-			});
+			}
+			removedDeclarations.forEach(declaration => cssStyle.children.remove(declaration));
 		}
-		return (styleCssText || cssStyle.cssText);
 	}
 
 	function log(...args) {

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

@@ -1,410 +0,0 @@
-/*
- * Copyright (c) Felix Böhm
- * All rights reserved.
- * 
- * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- * 
- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- * 
- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- * 
- * THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS,
- * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-// Modified by Gildas Lormeau
-
-/* global CSS */
-
-this.cssWhat = this.cssWhat || (() => {
-	"use strict";
-
-
-	/*! https://mths.be/cssescape v1.5.1 by @mathias | MIT license */
-	(function (root) {
-
-		if (root.CSS && root.CSS.escape) {
-			return root.CSS.escape;
-		}
-
-		// https://drafts.csswg.org/cssom/#serialize-an-identifier
-		var cssEscape = function (value) {
-			if (arguments.length == 0) {
-				throw new TypeError("`CSS.escape` requires an argument.");
-			}
-			var string = String(value);
-			var length = string.length;
-			var index = -1;
-			var codeUnit;
-			var result = "";
-			var firstCodeUnit = string.charCodeAt(0);
-			while (++index < length) {
-				codeUnit = string.charCodeAt(index);
-				// Note: there’s no need to special-case astral symbols, surrogate
-				// pairs, or lone surrogates.
-
-				// If the character is NULL (U+0000), then the REPLACEMENT CHARACTER
-				// (U+FFFD).
-				if (codeUnit == 0x0000) {
-					result += "\uFFFD";
-					continue;
-				}
-
-				if (
-					// If the character is in the range [\1-\1F] (U+0001 to U+001F) or is
-					// U+007F, […]
-					(codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F ||
-					// If the character is the first character and is in the range [0-9]
-					// (U+0030 to U+0039), […]
-					(index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
-					// If the character is the second character and is in the range [0-9]
-					// (U+0030 to U+0039) and the first character is a `-` (U+002D), […]
-					(
-						index == 1 &&
-						codeUnit >= 0x0030 && codeUnit <= 0x0039 &&
-						firstCodeUnit == 0x002D
-					)
-				) {
-					// https://drafts.csswg.org/cssom/#escape-a-character-as-code-point
-					result += "\\" + codeUnit.toString(16) + " ";
-					continue;
-				}
-
-				if (
-					// If the character is the first character and is a `-` (U+002D), and
-					// there is no second character, […]
-					index == 0 &&
-					length == 1 &&
-					codeUnit == 0x002D
-				) {
-					result += "\\" + string.charAt(index);
-					continue;
-				}
-
-				// If the character is not handled by one of the above rules and is
-				// greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
-				// is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to
-				// U+005A), or [a-z] (U+0061 to U+007A), […]
-				if (
-					codeUnit >= 0x0080 ||
-					codeUnit == 0x002D ||
-					codeUnit == 0x005F ||
-					codeUnit >= 0x0030 && codeUnit <= 0x0039 ||
-					codeUnit >= 0x0041 && codeUnit <= 0x005A ||
-					codeUnit >= 0x0061 && codeUnit <= 0x007A
-				) {
-					// the character itself
-					result += string.charAt(index);
-					continue;
-				}
-
-				// Otherwise, the escaped character.
-				// https://drafts.csswg.org/cssom/#escape-a-character
-				result += "\\" + string.charAt(index);
-
-			}
-			return result;
-		};
-
-		if (!root.CSS) {
-			root.CSS = {};
-		}
-
-		root.CSS.escape = cssEscape;
-		return cssEscape;
-
-	})(this);
-
-	const re_name = /^(?:\\.|[\w\-\u00c0-\uFFFF])+/,
-		re_escape = /\\([\da-f]{1,6}\s?|(\s)|.)/ig,
-		//modified version of https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L87
-		re_attr = /^\s*((?:\\.|[\w\u00c0-\uFFFF-])+)\s*(?:(\S?)=\s*(?:(['"])([^]*?)\3|(#?(?:\\.|[\w\u00c0-\uFFFF-])*)|)|)\s*(i)?\]/;
-	const actionTypes = {
-		__proto__: null,
-		"undefined": "exists",
-		"": "equals",
-		"~": "element",
-		"^": "start",
-		"$": "end",
-		"*": "any",
-		"!": "not",
-		"|": "hyphen"
-	};
-	const simpleSelectors = {
-		__proto__: null,
-		">": "child",
-		"~": "sibling",
-		"+": "adjacent"
-	};
-	const attribSelectors = {
-		__proto__: null,
-		"#": ["id", "equals"],
-		".": ["class", "element"]
-	};
-	//pseudos, whose data-property is parsed as well
-	const unpackPseudos = {
-		__proto__: null,
-		"has": true,
-		"not": true,
-		"matches": true
-	};
-	const stripQuotesFromPseudos = {
-		__proto__: null,
-		"contains": true,
-		"icontains": true
-	};
-	const quotes = {
-		__proto__: null,
-		"\"": true,
-		"'": true
-	};
-	const pseudoElements = [
-		"after", "before", "cue", "first-letter", "first-line", "selection", "slotted"
-	];
-	const stringify = (() => {
-		const actionTypes = {
-			"equals": "",
-			"element": "~",
-			"start": "^",
-			"end": "$",
-			"any": "*",
-			"not": "!",
-			"hyphen": "|"
-		};
-		const simpleSelectors = {
-			__proto__: null,
-			child: " > ",
-			sibling: " ~ ",
-			adjacent: " + ",
-			descendant: " ",
-			universal: "*"
-		};
-		function stringify(token) {
-			let value = "";
-			token.forEach(token => value += stringifySubselector(token) + ",");
-			return value.substring(0, value.length - 1);
-		}
-		function stringifySubselector(token) {
-			let value = "";
-			token.forEach(token => value += stringifyToken(token));
-			return value;
-		}
-		function stringifyToken(token) {
-			if (token.type in simpleSelectors) return simpleSelectors[token.type];
-			if (token.type == "tag") return escapeName(token.name);
-			if (token.type == "attribute") {
-				if (token.action == "exists") return "[" + escapeName(token.name) + "]";
-				if (token.expandedSelector && token.name == "id" && token.action == "equals" && !token.ignoreCase) return "#" + escapeName(token.value);
-				if (token.expandedSelector && token.name == "class" && token.action == "element" && !token.ignoreCase) return "." + escapeName(token.value);
-				return "[" +
-					escapeName(token.name) + actionTypes[token.action] + "=\"" +
-					escapeName(token.value) + "\"" + (token.ignoreCase ? "i" : "") + "]";
-			}
-			if (token.type == "pseudo") {
-				if (token.data == null) return ":" + escapeName(token.name);
-				if (typeof token.data == "string") return ":" + escapeName(token.name) + "(" + token.data + ")";
-				return ":" + escapeName(token.name) + "(" + stringify(token.data) + ")";
-			}
-			if (token.type == "pseudo-element") {
-				return "::" + escapeName(token.name);
-			}
-		}
-		function escapeName(str) {
-			return CSS.escape(str);
-		}
-		return stringify;
-	})();
-
-	return {
-		parse,
-		stringify
-	};
-
-	// unescape function taken from https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L139
-	function funescape(_, escaped, escapedWhitespace) {
-		const high = "0x" + escaped - 0x10000;
-		// NaN means non-codepoint
-		// Support: Firefox
-		// Workaround erroneous numeric interpretation of +"0x"
-		return high != high || escapedWhitespace ?
-			escaped :
-			// BMP codepoint
-			high < 0 ?
-				String.fromCharCode(high + 0x10000) :
-				// Supplemental Plane codepoint (surrogate pair)
-				String.fromCharCode(high >> 10 | 0xD800, high & 0x3FF | 0xDC00);
-	}
-
-	function unescapeCSS(str) {
-		return str.replace(re_escape, funescape);
-	}
-
-	function isWhitespace(c) {
-		return c == " " || c == "\n" || c == "\t" || c == "\f" || c == "\r";
-	}
-
-	function parse(selector, options) {
-		const subselects = [];
-		selector = parseSelector(subselects, selector + "", options);
-		if (selector != "") {
-			throw new SyntaxError("Unmatched selector: " + selector);
-		}
-		return subselects;
-	}
-
-	function parseSelector(subselects, selector, options) {
-		let tokens = [], sawWS = false, data, firstChar, name, quot;
-		stripWhitespace(0);
-		while (selector != "") {
-			firstChar = selector.charAt(0);
-			if (isWhitespace(firstChar)) {
-				sawWS = true;
-				stripWhitespace(1);
-			} else if (firstChar in simpleSelectors) {
-				tokens.push({ type: simpleSelectors[firstChar] });
-				sawWS = false;
-				stripWhitespace(1);
-			} else if (firstChar == ",") {
-				if (tokens.length == 0) {
-					throw new SyntaxError("empty sub-selector");
-				}
-				subselects.push(tokens);
-				tokens = [];
-				sawWS = false;
-				stripWhitespace(1);
-			} else {
-				if (sawWS) {
-					if (tokens.length > 0) {
-						tokens.push({ type: "descendant" });
-					}
-					sawWS = false;
-				}
-				if (firstChar == "*") {
-					selector = selector.substr(1);
-					tokens.push({ type: "universal" });
-				} else if (firstChar in attribSelectors) {
-					selector = selector.substr(1);
-					tokens.push({
-						expandedSelector: true,
-						type: "attribute",
-						name: attribSelectors[firstChar][0],
-						action: attribSelectors[firstChar][1],
-						value: getName(),
-						ignoreCase: false
-					});
-				} else if (firstChar == "[") {
-					selector = selector.substr(1);
-					data = selector.match(re_attr);
-					if (!data) {
-						throw new SyntaxError("Malformed attribute selector: " + selector);
-					}
-					selector = selector.substr(data[0].length);
-					name = unescapeCSS(data[1]);
-					if (
-						!options || (
-							"lowerCaseAttributeNames" in options ?
-								options.lowerCaseAttributeNames :
-								!options.xmlMode
-						)
-					) {
-						name = name.toLowerCase();
-					}
-					tokens.push({
-						type: "attribute",
-						name: name,
-						action: actionTypes[data[2]],
-						value: unescapeCSS(data[4] || data[5] || ""),
-						ignoreCase: !!data[6]
-					});
-				} else if (firstChar == ":") {
-					if (selector.charAt(1) == ":") {
-						selector = selector.substr(2);
-						tokens.push({ type: "pseudo-element", name: getName().toLowerCase() });
-						continue;
-					}
-					selector = selector.substr(1);
-					name = getName().toLowerCase();
-					data = null;
-					if (selector.charAt(0) == "(") {
-						if (name in unpackPseudos) {
-							quot = selector.charAt(1);
-							const quoted = quot in quotes;
-							selector = selector.substr(quoted + 1);
-							data = [];
-							selector = parseSelector(data, selector, options);
-							if (quoted) {
-								if (selector.charAt(0) != quot) {
-									throw new SyntaxError("unmatched quotes in :" + name);
-								} else {
-									selector = selector.substr(1);
-								}
-							}
-							if (selector.charAt(0) != ")") {
-								throw new SyntaxError("missing closing parenthesis in :" + name + " " + selector);
-							}
-							selector = selector.substr(1);
-						} else {
-							let pos = 1, counter = 1;
-							for (; counter > 0 && pos < selector.length; pos++) {
-								if (selector.charAt(pos) == "(" && !isEscaped(pos)) counter++;
-								else if (selector.charAt(pos) == ")" && !isEscaped(pos)) counter--;
-							}
-							if (counter) {
-								throw new SyntaxError("parenthesis not matched");
-							}
-							data = selector.substr(1, pos - 2);
-							selector = selector.substr(pos);
-							if (name in stripQuotesFromPseudos) {
-								quot = data.charAt(0);
-								if (quot == data.slice(-1) && quot in quotes) {
-									data = data.slice(1, -1);
-								}
-								data = unescapeCSS(data);
-							}
-						}
-					}
-					tokens.push({ type: pseudoElements.indexOf(name) == -1 ? "pseudo" : "pseudo-element", name: name, data: data });
-				} else if (re_name.test(selector)) {
-					name = getName();
-					if (!options || ("lowerCaseTags" in options ? options.lowerCaseTags : !options.xmlMode)) {
-						name = name.toLowerCase();
-					}
-					tokens.push({ type: "tag", name: name });
-				} else {
-					if (tokens.length && tokens[tokens.length - 1].type == "descendant") {
-						tokens.pop();
-					}
-					addToken(subselects, tokens);
-					return selector;
-				}
-			}
-		}
-		addToken(subselects, tokens);
-		return selector;
-
-		function getName() {
-			const sub = selector.match(re_name)[0];
-			selector = selector.substr(sub.length);
-			return unescapeCSS(sub);
-		}
-
-		function stripWhitespace(start) {
-			while (isWhitespace(selector.charAt(start))) start++;
-			selector = selector.substr(start);
-		}
-
-		function isEscaped(pos) {
-			let slashCount = 0;
-			while (selector.charAt(--pos) == "\\") slashCount++;
-			return (slashCount & 1) == 1;
-		}
-	}
-
-	function addToken(subselects, tokens) {
-		if (subselects.length > 0 && tokens.length == 0) {
-			throw new SyntaxError("empty sub-selector");
-		}
-		subselects.push(tokens);
-	}
-
-})();

+ 4771 - 0
lib/single-file/css-tree.js

@@ -0,0 +1,4771 @@
+/*
+ * Copyright (C) 2016 by Roman Dvornov
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+this.cssTree = this.cssTree || (() => {
+
+	function createItem(data) {
+		return {
+			prev: null,
+			next: null,
+			data: data
+		};
+	}
+
+	function allocateCursor(node, prev, next) {
+		let cursor;
+
+		if (cursors !== null) {
+			cursor = cursors;
+			cursors = cursors.cursor;
+			cursor.prev = prev;
+			cursor.next = next;
+			cursor.cursor = node.cursor;
+		} else {
+			cursor = {
+				prev: prev,
+				next: next,
+				cursor: node.cursor
+			};
+		}
+
+		node.cursor = cursor;
+
+		return cursor;
+	}
+
+	function releaseCursor(node) {
+		const cursor = node.cursor;
+
+		node.cursor = cursor.cursor;
+		cursor.prev = null;
+		cursor.next = null;
+		cursor.cursor = cursors;
+		cursors = cursor;
+	}
+
+	let cursors = null;
+
+	function List() {
+		this.cursor = null;
+		this.head = null;
+		this.tail = null;
+	}
+
+	List.createItem = createItem;
+	List.prototype.createItem = createItem;
+
+	List.prototype.updateCursors = function (prevOld, prevNew, nextOld, nextNew) {
+		let cursor = this.cursor;
+
+		while (cursor !== null) {
+			if (cursor.prev === prevOld) {
+				cursor.prev = prevNew;
+			}
+
+			if (cursor.next === nextOld) {
+				cursor.next = nextNew;
+			}
+
+			cursor = cursor.cursor;
+		}
+	};
+
+	List.prototype.getSize = function () {
+		let size = 0;
+		let cursor = this.head;
+
+		while (cursor) {
+			size++;
+			cursor = cursor.next;
+		}
+
+		return size;
+	};
+
+	List.prototype.fromArray = function (array) {
+		let cursor = null;
+
+		this.head = null;
+
+		for (let i = 0; i < array.length; i++) {
+			const item = createItem(array[i]);
+
+			if (cursor !== null) {
+				cursor.next = item;
+			} else {
+				this.head = item;
+			}
+
+			item.prev = cursor;
+			cursor = item;
+		}
+
+		this.tail = cursor;
+
+		return this;
+	};
+
+	List.prototype.toArray = function () {
+		let cursor = this.head;
+		const result = [];
+
+		while (cursor) {
+			result.push(cursor.data);
+			cursor = cursor.next;
+		}
+
+		return result;
+	};
+
+	List.prototype.toJSON = List.prototype.toArray;
+
+	List.prototype.isEmpty = function () {
+		return this.head === null;
+	};
+
+	List.prototype.first = function () {
+		return this.head && this.head.data;
+	};
+
+	List.prototype.last = function () {
+		return this.tail && this.tail.data;
+	};
+
+	List.prototype.each = function (fn, context) {
+		let item;
+
+		if (context === undefined) {
+			context = this;
+		}
+
+		// push cursor
+		const cursor = allocateCursor(this, null, this.head);
+
+		while (cursor.next !== null) {
+			item = cursor.next;
+			cursor.next = item.next;
+
+			fn.call(context, item.data, item, this);
+		}
+
+		// pop cursor
+		releaseCursor(this);
+	};
+
+	List.prototype.forEach = List.prototype.each;
+
+	List.prototype.eachRight = function (fn, context) {
+		let item;
+
+		if (context === undefined) {
+			context = this;
+		}
+
+		// push cursor
+		const cursor = allocateCursor(this, this.tail, null);
+
+		while (cursor.prev !== null) {
+			item = cursor.prev;
+			cursor.prev = item.prev;
+
+			fn.call(context, item.data, item, this);
+		}
+
+		// pop cursor
+		releaseCursor(this);
+	};
+
+	List.prototype.forEachRight = List.prototype.eachRight;
+
+	List.prototype.nextUntil = function (start, fn, context) {
+		if (start === null) {
+			return;
+		}
+
+		let item;
+
+		if (context === undefined) {
+			context = this;
+		}
+
+		// push cursor
+		const cursor = allocateCursor(this, null, start);
+
+		while (cursor.next !== null) {
+			item = cursor.next;
+			cursor.next = item.next;
+
+			if (fn.call(context, item.data, item, this)) {
+				break;
+			}
+		}
+
+		// pop cursor
+		releaseCursor(this);
+	};
+
+	List.prototype.prevUntil = function (start, fn, context) {
+		if (start === null) {
+			return;
+		}
+
+		let item;
+
+		if (context === undefined) {
+			context = this;
+		}
+
+		// push cursor
+		const cursor = allocateCursor(this, start, null);
+
+		while (cursor.prev !== null) {
+			item = cursor.prev;
+			cursor.prev = item.prev;
+
+			if (fn.call(context, item.data, item, this)) {
+				break;
+			}
+		}
+
+		// pop cursor
+		releaseCursor(this);
+	};
+
+	List.prototype.some = function (fn, context) {
+		let cursor = this.head;
+
+		if (context === undefined) {
+			context = this;
+		}
+
+		while (cursor !== null) {
+			if (fn.call(context, cursor.data, cursor, this)) {
+				return true;
+			}
+
+			cursor = cursor.next;
+		}
+
+		return false;
+	};
+
+	List.prototype.map = function (fn, context) {
+		const result = new List();
+		let cursor = this.head;
+
+		if (context === undefined) {
+			context = this;
+		}
+
+		while (cursor !== null) {
+			result.appendData(fn.call(context, cursor.data, cursor, this));
+			cursor = cursor.next;
+		}
+
+		return result;
+	};
+
+	List.prototype.filter = function (fn, context) {
+		const result = new List();
+		let cursor = this.head;
+
+		if (context === undefined) {
+			context = this;
+		}
+
+		while (cursor !== null) {
+			if (fn.call(context, cursor.data, cursor, this)) {
+				result.appendData(cursor.data);
+			}
+			cursor = cursor.next;
+		}
+
+		return result;
+	};
+
+	List.prototype.clear = function () {
+		this.head = null;
+		this.tail = null;
+	};
+
+	List.prototype.copy = function () {
+		const result = new List();
+		let cursor = this.head;
+
+		while (cursor !== null) {
+			result.insert(createItem(cursor.data));
+			cursor = cursor.next;
+		}
+
+		return result;
+	};
+
+	List.prototype.prepend = function (item) {
+		//      head
+		//    ^
+		// item
+		this.updateCursors(null, item, this.head, item);
+
+		// insert to the beginning of the list
+		if (this.head !== null) {
+			// new item <- first item
+			this.head.prev = item;
+
+			// new item -> first item
+			item.next = this.head;
+		} else {
+			// if list has no head, then it also has no tail
+			// in this case tail points to the new item
+			this.tail = item;
+		}
+
+		// head always points to new item
+		this.head = item;
+
+		return this;
+	};
+
+	List.prototype.prependData = function (data) {
+		return this.prepend(createItem(data));
+	};
+
+	List.prototype.append = function (item) {
+		return this.insert(item);
+	};
+
+	List.prototype.appendData = function (data) {
+		return this.insert(createItem(data));
+	};
+
+	List.prototype.insert = function (item, before) {
+		if (before !== undefined && before !== null) {
+			// prev   before
+			//      ^
+			//     item
+			this.updateCursors(before.prev, item, before, item);
+
+			if (before.prev === null) {
+				// insert to the beginning of list
+				if (this.head !== before) {
+					throw new Error("before doesn\"t belong to list");
+				}
+
+				// since head points to before therefore list doesn"t empty
+				// no need to check tail
+				this.head = item;
+				before.prev = item;
+				item.next = before;
+
+				this.updateCursors(null, item);
+			} else {
+
+				// insert between two items
+				before.prev.next = item;
+				item.prev = before.prev;
+
+				before.prev = item;
+				item.next = before;
+			}
+		} else {
+			// tail
+			//      ^
+			//      item
+			this.updateCursors(this.tail, item, null, item);
+
+			// insert to the ending of the list
+			if (this.tail !== null) {
+				// last item -> new item
+				this.tail.next = item;
+
+				// last item <- new item
+				item.prev = this.tail;
+			} else {
+				// if list has no tail, then it also has no head
+				// in this case head points to new item
+				this.head = item;
+			}
+
+			// tail always points to new item
+			this.tail = item;
+		}
+
+		return this;
+	};
+
+	List.prototype.insertData = function (data, before) {
+		return this.insert(createItem(data), before);
+	};
+
+	List.prototype.remove = function (item) {
+		//      item
+		//       ^
+		// prev     next
+		this.updateCursors(item, item.prev, item, item.next);
+
+		if (item.prev !== null) {
+			item.prev.next = item.next;
+		} else {
+			if (this.head !== item) {
+				throw new Error("item doesn\"t belong to list");
+			}
+
+			this.head = item.next;
+		}
+
+		if (item.next !== null) {
+			item.next.prev = item.prev;
+		} else {
+			if (this.tail !== item) {
+				throw new Error("item doesn\"t belong to list");
+			}
+
+			this.tail = item.prev;
+		}
+
+		item.prev = null;
+		item.next = null;
+
+		return item;
+	};
+
+	List.prototype.push = function (data) {
+		this.insert(createItem(data));
+	};
+
+	List.prototype.pop = function () {
+		if (this.tail !== null) {
+			return this.remove(this.tail);
+		}
+	};
+
+	List.prototype.unshift = function (data) {
+		this.prepend(createItem(data));
+	};
+
+	List.prototype.shift = function () {
+		if (this.head !== null) {
+			return this.remove(this.head);
+		}
+	};
+
+	List.prototype.prependList = function (list) {
+		return this.insertList(list, this.head);
+	};
+
+	List.prototype.appendList = function (list) {
+		return this.insertList(list);
+	};
+
+	List.prototype.insertList = function (list, before) {
+		// ignore empty lists
+		if (list.head === null) {
+			return this;
+		}
+
+		if (before !== undefined && before !== null) {
+			this.updateCursors(before.prev, list.tail, before, list.head);
+
+			// insert in the middle of dist list
+			if (before.prev !== null) {
+				// before.prev <-> list.head
+				before.prev.next = list.head;
+				list.head.prev = before.prev;
+			} else {
+				this.head = list.head;
+			}
+
+			before.prev = list.tail;
+			list.tail.next = before;
+		} else {
+			this.updateCursors(this.tail, list.tail, null, list.head);
+
+			// insert to end of the list
+			if (this.tail !== null) {
+				// if destination list has a tail, then it also has a head,
+				// but head doesn"t change
+
+				// dest tail -> source head
+				this.tail.next = list.head;
+
+				// dest tail <- source head
+				list.head.prev = this.tail;
+			} else {
+				// if list has no a tail, then it also has no a head
+				// in this case points head to new item
+				this.head = list.head;
+			}
+
+			// tail always start point to new item
+			this.tail = list.tail;
+		}
+
+		list.head = null;
+		list.tail = null;
+
+		return this;
+	};
+
+	List.prototype.replace = function (oldItem, newItemOrList) {
+		if ("head" in newItemOrList) {
+			this.insertList(newItemOrList, oldItem);
+		} else {
+			this.insert(newItemOrList, oldItem);
+		}
+
+		this.remove(oldItem);
+	};
+
+	// ---
+	function createCustomError(name, message) {
+		// use Object.create(), because some VMs prevent setting line/column otherwise
+		// (iOS Safari 10 even throws an exception)
+		const error = Object.create(SyntaxError.prototype);
+		const errorStack = new Error();
+
+		error.name = name;
+		error.message = message;
+
+		Object.defineProperty(error, "stack", {
+			get: function () {
+				return (errorStack.stack || "").replace(/^(.+\n){1,3}/, name + ": " + message + "\n");
+			}
+		});
+
+		return error;
+	}
+
+	// ---
+
+	const MAX_LINE_LENGTH = 100;
+	const OFFSET_CORRECTION = 60;
+	const TAB_REPLACEMENT = "    ";
+
+	function sourceFragment(error, extraLines) {
+		function processLines(start, end) {
+			return lines.slice(start, end).map(function (line, idx) {
+				let num = String(start + idx + 1);
+
+				while (num.length < maxNumLength) {
+					num = " " + num;
+				}
+
+				return num + " |" + line;
+			}).join("\n");
+		}
+
+		const lines = error.source.split(/\r\n?|\n|\f/);
+		let line = error.line;
+		let column = error.column;
+		const startLine = Math.max(1, line - extraLines) - 1;
+		const endLine = Math.min(line + extraLines, lines.length + 1);
+		const maxNumLength = Math.max(4, String(endLine).length) + 1;
+		let cutLeft = 0;
+
+		// column correction according to replaced tab before column
+		column += (TAB_REPLACEMENT.length - 1) * (lines[line - 1].substr(0, column - 1).match(/\t/g) || []).length;
+
+		if (column > MAX_LINE_LENGTH) {
+			cutLeft = column - OFFSET_CORRECTION + 3;
+			column = OFFSET_CORRECTION - 2;
+		}
+
+		for (let i = startLine; i <= endLine; i++) {
+			if (i >= 0 && i < lines.length) {
+				lines[i] = lines[i].replace(/\t/g, TAB_REPLACEMENT);
+				lines[i] =
+					(cutLeft > 0 && lines[i].length > cutLeft ? "\u2026" : "") +
+					lines[i].substr(cutLeft, MAX_LINE_LENGTH - 2) +
+					(lines[i].length > cutLeft + MAX_LINE_LENGTH - 1 ? "\u2026" : "");
+			}
+		}
+
+		return [
+			processLines(startLine, line),
+			new Array(column + maxNumLength + 2).join("-") + "^",
+			processLines(line, endLine)
+		].filter(Boolean).join("\n");
+	}
+
+	function CssSyntaxError(message, source, offset, line, column) {
+		const error = createCustomError("CssSyntaxError", message);
+
+		error.source = source;
+		error.offset = offset;
+		error.line = line;
+		error.column = column;
+
+		error.sourceFragment = function (extraLines) {
+			return sourceFragment(error, isNaN(extraLines) ? 0 : extraLines);
+		};
+		Object.defineProperty(error, "formattedMessage", {
+			get: function () {
+				return (
+					"Parse error: " + error.message + "\n" +
+					sourceFragment(error, 2)
+				);
+			}
+		});
+
+		// for backward capability
+		error.parseError = {
+			offset: offset,
+			line: line,
+			column: column
+		};
+
+		return error;
+	}
+
+	// ---
+
+	// token types (note: value shouldn't intersect with used char codes)
+	const WHITESPACE = 1;
+	const IDENTIFIER = 2;
+	const NUMBER = 3;
+	const STRING = 4;
+	const COMMENT = 5;
+	const PUNCTUATOR = 6;
+	const CDO = 7;
+	const CDC = 8;
+	const ATKEYWORD = 14;
+	const FUNCTION = 15;
+	const URL = 16;
+	const RAW = 17;
+
+	const TAB = 9;
+	const NEW_LINE = 10;
+	const F = 12;
+	const R = 13;
+	const SPACE = 32;
+
+	const TYPE = {
+		WhiteSpace: WHITESPACE,
+		Identifier: IDENTIFIER,
+		Number: NUMBER,
+		String: STRING,
+		Comment: COMMENT,
+		Punctuator: PUNCTUATOR,
+		CDO: CDO,
+		CDC: CDC,
+		AtKeyword: ATKEYWORD,
+		Function: FUNCTION,
+		Url: URL,
+		Raw: RAW,
+
+		ExclamationMark: 33,  // !
+		QuotationMark: 34,  // "
+		NumberSign: 35,  // #
+		DollarSign: 36,  // $
+		PercentSign: 37,  // %
+		Ampersand: 38,  // &
+		Apostrophe: 39,  // '
+		LeftParenthesis: 40,  // (
+		RightParenthesis: 41,  // )
+		Asterisk: 42,  // *
+		PlusSign: 43,  // +
+		Comma: 44,  // ,
+		HyphenMinus: 45,  // -
+		FullStop: 46,  // .
+		Solidus: 47,  // /
+		Colon: 58,  // :
+		Semicolon: 59,  // ;
+		LessThanSign: 60,  // <
+		EqualsSign: 61,  // =
+		GreaterThanSign: 62,  // >
+		QuestionMark: 63,  // ?
+		CommercialAt: 64,  // @
+		LeftSquareBracket: 91,  // [
+		Backslash: 92,  // \
+		RightSquareBracket: 93,  // ]
+		CircumflexAccent: 94,  // ^
+		LowLine: 95,  // _
+		GraveAccent: 96,  // `
+		LeftCurlyBracket: 123,  // {
+		VerticalLine: 124,  // |
+		RightCurlyBracket: 125,  // }
+		Tilde: 126   // ~
+	};
+
+	const NAME = Object.keys(TYPE).reduce(function (result, key) {
+		result[TYPE[key]] = key;
+		return result;
+	}, {});
+
+	// https://drafts.csswg.org/css-syntax/#tokenizer-definitions
+	// > non-ASCII code point
+	// >   A code point with a value equal to or greater than U+0080 <control>
+	// > name-start code point
+	// >   A letter, a non-ASCII code point, or U+005F LOW LINE (_).
+	// > name code point
+	// >   A name-start code point, a digit, or U+002D HYPHEN-MINUS (-)
+	// That means only ASCII code points has a special meaning and we a maps for 0..127 codes only
+	const SafeUint32Array = typeof Uint32Array !== "undefined" ? Uint32Array : Array; // fallback on Array when TypedArray is not supported
+	const SYMBOL_TYPE = new SafeUint32Array(0x80);
+	const PUNCTUATION = new SafeUint32Array(0x80);
+	const STOP_URL_RAW = new SafeUint32Array(0x80);
+
+	for (let i = 0; i < SYMBOL_TYPE.length; i++) {
+		SYMBOL_TYPE[i] = IDENTIFIER;
+	}
+
+	// fill categories
+	[
+		TYPE.ExclamationMark,    // !
+		TYPE.QuotationMark,      // "
+		TYPE.NumberSign,         // #
+		TYPE.DollarSign,         // $
+		TYPE.PercentSign,        // %
+		TYPE.Ampersand,          // &
+		TYPE.Apostrophe,         // '
+		TYPE.LeftParenthesis,    // (
+		TYPE.RightParenthesis,   // )
+		TYPE.Asterisk,           // *
+		TYPE.PlusSign,           // +
+		TYPE.Comma,              // ,
+		TYPE.HyphenMinus,        // -
+		TYPE.FullStop,           // .
+		TYPE.Solidus,            // /
+		TYPE.Colon,              // :
+		TYPE.Semicolon,          // ;
+		TYPE.LessThanSign,       // <
+		TYPE.EqualsSign,         // =
+		TYPE.GreaterThanSign,    // >
+		TYPE.QuestionMark,       // ?
+		TYPE.CommercialAt,       // @
+		TYPE.LeftSquareBracket,  // [
+		// TYPE.Backslash,          // \
+		TYPE.RightSquareBracket, // ]
+		TYPE.CircumflexAccent,   // ^
+		// TYPE.LowLine,            // _
+		TYPE.GraveAccent,        // `
+		TYPE.LeftCurlyBracket,   // {
+		TYPE.VerticalLine,       // |
+		TYPE.RightCurlyBracket,  // }
+		TYPE.Tilde               // ~
+	].forEach(function (key) {
+		SYMBOL_TYPE[Number(key)] = PUNCTUATOR;
+		PUNCTUATION[Number(key)] = PUNCTUATOR;
+	});
+
+	for (let i = 48; i <= 57; i++) {
+		SYMBOL_TYPE[i] = NUMBER;
+	}
+
+	SYMBOL_TYPE[SPACE] = WHITESPACE;
+	SYMBOL_TYPE[TAB] = WHITESPACE;
+	SYMBOL_TYPE[NEW_LINE] = WHITESPACE;
+	SYMBOL_TYPE[R] = WHITESPACE;
+	SYMBOL_TYPE[F] = WHITESPACE;
+
+	SYMBOL_TYPE[TYPE.Apostrophe] = STRING;
+	SYMBOL_TYPE[TYPE.QuotationMark] = STRING;
+
+	STOP_URL_RAW[SPACE] = 1;
+	STOP_URL_RAW[TAB] = 1;
+	STOP_URL_RAW[NEW_LINE] = 1;
+	STOP_URL_RAW[R] = 1;
+	STOP_URL_RAW[F] = 1;
+	STOP_URL_RAW[TYPE.Apostrophe] = 1;
+	STOP_URL_RAW[TYPE.QuotationMark] = 1;
+	STOP_URL_RAW[TYPE.LeftParenthesis] = 1;
+	STOP_URL_RAW[TYPE.RightParenthesis] = 1;
+
+	// whitespace is punctuation ...
+	PUNCTUATION[SPACE] = PUNCTUATOR;
+	PUNCTUATION[TAB] = PUNCTUATOR;
+	PUNCTUATION[NEW_LINE] = PUNCTUATOR;
+	PUNCTUATION[R] = PUNCTUATOR;
+	PUNCTUATION[F] = PUNCTUATOR;
+	// ... hyper minus is not
+	PUNCTUATION[TYPE.HyphenMinus] = 0;
+
+	const constants = {
+		TYPE: TYPE,
+		NAME: NAME,
+
+		SYMBOL_TYPE: SYMBOL_TYPE,
+		PUNCTUATION: PUNCTUATION,
+		STOP_URL_RAW: STOP_URL_RAW
+	};
+
+	// ---
+
+	const BACK_SLASH = 92;
+	const E = 101; // 'e'.charCodeAt(0)
+
+	function firstCharOffset(source) {
+		// detect BOM (https://en.wikipedia.org/wiki/Byte_order_mark)
+		if (source.charCodeAt(0) === 0xFEFF ||  // UTF-16BE
+			source.charCodeAt(0) === 0xFFFE) {  // UTF-16LE
+			return 1;
+		}
+
+		return 0;
+	}
+
+	function isHex(code) {
+		return (code >= 48 && code <= 57) || // 0 .. 9
+			(code >= 65 && code <= 70) || // A .. F
+			(code >= 97 && code <= 102);  // a .. f
+	}
+
+	function isNumber(code) {
+		return code >= 48 && code <= 57;
+	}
+
+	function isWhiteSpace(code) {
+		return code === SPACE || code === TAB || isNewline(code);
+	}
+
+	function isNewline(code) {
+		return code === R || code === NEW_LINE || code === F;
+	}
+
+	function getNewlineLength(source, offset, code) {
+		if (isNewline(code)) {
+			if (code === R && offset + 1 < source.length && source.charCodeAt(offset + 1) === NEW_LINE) {
+				return 2;
+			}
+
+			return 1;
+		}
+
+		return 0;
+	}
+
+	function cmpChar(testStr, offset, referenceCode) {
+		let code = testStr.charCodeAt(offset);
+
+		// code.toLowerCase() for A..Z
+		if (code >= 65 && code <= 90) {
+			code = code | 32;
+		}
+
+		return code === referenceCode;
+	}
+
+	function cmpStr(testStr, start, end, referenceStr) {
+		if (end - start !== referenceStr.length) {
+			return false;
+		}
+
+		if (start < 0 || end > testStr.length) {
+			return false;
+		}
+
+		for (let i = start; i < end; i++) {
+			let testCode = testStr.charCodeAt(i);
+			const refCode = referenceStr.charCodeAt(i - start);
+
+			// testCode.toLowerCase() for A..Z
+			if (testCode >= 65 && testCode <= 90) {
+				testCode = testCode | 32;
+			}
+
+			if (testCode !== refCode) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	function findWhiteSpaceStart(source, offset) {
+		while (offset >= 0 && isWhiteSpace(source.charCodeAt(offset))) {
+			offset--;
+		}
+
+		return offset + 1;
+	}
+
+	function findWhiteSpaceEnd(source, offset) {
+		while (offset < source.length && isWhiteSpace(source.charCodeAt(offset))) {
+			offset++;
+		}
+
+		return offset;
+	}
+
+	function findCommentEnd(source, offset) {
+		const commentEnd = source.indexOf("*/", offset);
+
+		if (commentEnd === -1) {
+			return source.length;
+		}
+
+		return commentEnd + 2;
+	}
+
+	function findStringEnd(source, offset, quote) {
+		for (; offset < source.length; offset++) {
+			const code = source.charCodeAt(offset);
+
+			// TODO: bad string
+			if (code === BACK_SLASH) {
+				offset++;
+			} else if (code === quote) {
+				offset++;
+				break;
+			}
+		}
+
+		return offset;
+	}
+
+	function findDecimalNumberEnd(source, offset) {
+		while (offset < source.length && isNumber(source.charCodeAt(offset))) {
+			offset++;
+		}
+
+		return offset;
+	}
+
+	function findNumberEnd(source, offset, allowFraction) {
+		let code;
+
+		offset = findDecimalNumberEnd(source, offset);
+
+		// fraction: .\d+
+		if (allowFraction && offset + 1 < source.length && source.charCodeAt(offset) === FULLSTOP) {
+			code = source.charCodeAt(offset + 1);
+
+			if (isNumber(code)) {
+				offset = findDecimalNumberEnd(source, offset + 1);
+			}
+		}
+
+		// exponent: e[+-]\d+
+		if (offset + 1 < source.length) {
+			if ((source.charCodeAt(offset) | 32) === E) { // case insensitive check for `e`
+				code = source.charCodeAt(offset + 1);
+
+				if (code === PLUSSIGN || code === HYPHENMINUS) {
+					if (offset + 2 < source.length) {
+						code = source.charCodeAt(offset + 2);
+					}
+				}
+
+				if (isNumber(code)) {
+					offset = findDecimalNumberEnd(source, offset + 2);
+				}
+			}
+		}
+
+		return offset;
+	}
+
+	// skip escaped unicode sequence that can ends with space
+	// [0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?
+	function findEscapeEnd(source, offset) {
+		for (let i = 0; i < 7 && offset + i < source.length; i++) {
+			const code = source.charCodeAt(offset + i);
+
+			if (i !== 6 && isHex(code)) {
+				continue;
+			}
+
+			if (i > 0) {
+				offset += i - 1 + getNewlineLength(source, offset + i, code);
+				if (code === SPACE || code === TAB) {
+					offset++;
+				}
+			}
+
+			break;
+		}
+
+		return offset;
+	}
+
+	function findIdentifierEnd(source, offset) {
+		for (; offset < source.length; offset++) {
+			const code = source.charCodeAt(offset);
+
+			if (code === BACK_SLASH) {
+				offset = findEscapeEnd(source, offset + 1);
+			} else if (code < 0x80 && PUNCTUATION[code] === PUNCTUATOR) {
+				break;
+			}
+		}
+
+		return offset;
+	}
+
+	function findUrlRawEnd(source, offset) {
+		for (; offset < source.length; offset++) {
+			const code = source.charCodeAt(offset);
+
+			if (code === BACK_SLASH) {
+				offset = findEscapeEnd(source, offset + 1);
+			} else if (code < 0x80 && STOP_URL_RAW[code] === 1) {
+				break;
+			}
+		}
+
+		return offset;
+	}
+
+	const utils = {
+		firstCharOffset: firstCharOffset,
+
+		isHex: isHex,
+		isNumber: isNumber,
+		isWhiteSpace: isWhiteSpace,
+		isNewline: isNewline,
+		getNewlineLength: getNewlineLength,
+
+		cmpChar: cmpChar,
+		cmpStr: cmpStr,
+
+		findWhiteSpaceStart: findWhiteSpaceStart,
+		findWhiteSpaceEnd: findWhiteSpaceEnd,
+		findCommentEnd: findCommentEnd,
+		findStringEnd: findStringEnd,
+		findDecimalNumberEnd: findDecimalNumberEnd,
+		findNumberEnd: findNumberEnd,
+		findEscapeEnd: findEscapeEnd,
+		findIdentifierEnd: findIdentifierEnd,
+		findUrlRawEnd: findUrlRawEnd
+	};
+
+	// ---
+
+	const STAR = TYPE.Asterisk;
+	const SLASH = TYPE.Solidus;
+	const FULLSTOP = TYPE.FullStop;
+	const PLUSSIGN = TYPE.PlusSign;
+	const HYPHENMINUS = TYPE.HyphenMinus;
+	const GREATERTHANSIGN = TYPE.GreaterThanSign;
+	const LESSTHANSIGN = TYPE.LessThanSign;
+	const EXCLAMATIONMARK = TYPE.ExclamationMark;
+	const COMMERCIALAT = TYPE.CommercialAt;
+	const QUOTATIONMARK = TYPE.QuotationMark;
+	const APOSTROPHE = TYPE.Apostrophe;
+	const LEFTPARENTHESIS = TYPE.LeftParenthesis;
+	const RIGHTPARENTHESIS = TYPE.RightParenthesis;
+	const LEFTCURLYBRACKET = TYPE.LeftCurlyBracket;
+	const RIGHTCURLYBRACKET = TYPE.RightCurlyBracket;
+	const LEFTSQUAREBRACKET = TYPE.LeftSquareBracket;
+	const RIGHTSQUAREBRACKET = TYPE.RightSquareBracket;
+	const NUMBERSIGN = TYPE.NumberSign;
+	const COMMA = TYPE.Comma;
+	const SOLIDUS = TYPE.Solidus;
+	const ASTERISK = TYPE.Asterisk;
+	const PERCENTSIGN = TYPE.PercentSign;
+	const BACKSLASH = TYPE.Backslash;
+	const VERTICALLINE = TYPE.VerticalLine;
+	const TILDE = TYPE.Tilde;
+	const SEMICOLON = TYPE.Semicolon;
+	const COLON = TYPE.Colon;
+	const DOLLARSIGN = TYPE.DollarSign;
+	const EQUALSSIGN = TYPE.EqualsSign;
+	const CIRCUMFLEXACCENT = TYPE.CircumflexAccent;
+	const TYPE_CDC = TYPE.CDC;
+	const TYPE_CDO = TYPE.CDO;
+	const QUESTIONMARK = TYPE.QuestionMark;
+
+	const NULL = 0;
+	const MIN_BUFFER_SIZE = 16 * 1024;
+	const OFFSET_MASK = 0x00FFFFFF;
+	const TYPE_SHIFT = 24;
+
+	function computeLinesAndColumns(tokenizer, source) {
+		const sourceLength = source.length;
+		const start = firstCharOffset(source);
+		let lines = tokenizer.lines;
+		let line = tokenizer.startLine;
+		let columns = tokenizer.columns;
+		let column = tokenizer.startColumn;
+
+		if (lines === null || lines.length < sourceLength + 1) {
+			lines = new SafeUint32Array(Math.max(sourceLength + 1024, MIN_BUFFER_SIZE));
+			columns = new SafeUint32Array(lines.length);
+		}
+
+		let i;
+		for (i = start; i < sourceLength; i++) {
+			const code = source.charCodeAt(i);
+
+			lines[i] = line;
+			columns[i] = column++;
+
+			if (code === NEW_LINE || code === R || code === F) {
+				if (code === R && i + 1 < sourceLength && source.charCodeAt(i + 1) === NEW_LINE) {
+					i++;
+					lines[i] = line;
+					columns[i] = column;
+				}
+
+				line++;
+				column = 1;
+			}
+		}
+
+		lines[i] = line;
+		columns[i] = column;
+
+		tokenizer.linesAnsColumnsComputed = true;
+		tokenizer.lines = lines;
+		tokenizer.columns = columns;
+	}
+
+	function tokenLayout(tokenizer, source, startPos) {
+		const sourceLength = source.length;
+		let offsetAndType = tokenizer.offsetAndType;
+		let balance = tokenizer.balance;
+		let tokenCount = 0;
+		let prevType = 0;
+		let offset = startPos;
+		let anchor = 0;
+		let balanceCloseCode = 0;
+		let balanceStart = 0;
+		let balancePrev = 0;
+
+		if (offsetAndType === null || offsetAndType.length < sourceLength + 1) {
+			offsetAndType = new SafeUint32Array(sourceLength + 1024);
+			balance = new SafeUint32Array(sourceLength + 1024);
+		}
+
+		while (offset < sourceLength) {
+			let code = source.charCodeAt(offset);
+			let type = code < 0x80 ? SYMBOL_TYPE[code] : IDENTIFIER;
+
+			balance[tokenCount] = sourceLength;
+
+			switch (type) {
+				case WHITESPACE:
+					offset = findWhiteSpaceEnd(source, offset + 1);
+					break;
+
+				case PUNCTUATOR:
+					switch (code) {
+						case balanceCloseCode:
+							balancePrev = balanceStart & OFFSET_MASK;
+							balanceStart = balance[balancePrev];
+							balanceCloseCode = balanceStart >> TYPE_SHIFT;
+							balance[tokenCount] = balancePrev;
+							balance[balancePrev++] = tokenCount;
+							for (; balancePrev < tokenCount; balancePrev++) {
+								if (balance[balancePrev] === sourceLength) {
+									balance[balancePrev] = tokenCount;
+								}
+							}
+							break;
+
+						case LEFTSQUAREBRACKET:
+							balance[tokenCount] = balanceStart;
+							balanceCloseCode = RIGHTSQUAREBRACKET;
+							balanceStart = (balanceCloseCode << TYPE_SHIFT) | tokenCount;
+							break;
+
+						case LEFTCURLYBRACKET:
+							balance[tokenCount] = balanceStart;
+							balanceCloseCode = RIGHTCURLYBRACKET;
+							balanceStart = (balanceCloseCode << TYPE_SHIFT) | tokenCount;
+							break;
+
+						case LEFTPARENTHESIS:
+							balance[tokenCount] = balanceStart;
+							balanceCloseCode = RIGHTPARENTHESIS;
+							balanceStart = (balanceCloseCode << TYPE_SHIFT) | tokenCount;
+							break;
+					}
+
+					// /*
+					if (code === STAR && prevType === SLASH) {
+						type = COMMENT;
+						offset = findCommentEnd(source, offset + 1);
+						tokenCount--; // rewrite prev token
+						break;
+					}
+
+					// edge case for -.123 and +.123
+					if (code === FULLSTOP && (prevType === PLUSSIGN || prevType === HYPHENMINUS)) {
+						if (offset + 1 < sourceLength && isNumber(source.charCodeAt(offset + 1))) {
+							type = NUMBER;
+							offset = findNumberEnd(source, offset + 2, false);
+							tokenCount--; // rewrite prev token
+							break;
+						}
+					}
+
+					// <!--
+					if (code === EXCLAMATIONMARK && prevType === LESSTHANSIGN) {
+						if (offset + 2 < sourceLength &&
+							source.charCodeAt(offset + 1) === HYPHENMINUS &&
+							source.charCodeAt(offset + 2) === HYPHENMINUS) {
+							type = CDO;
+							offset = offset + 3;
+							tokenCount--; // rewrite prev token
+							break;
+						}
+					}
+
+					// -->
+					if (code === HYPHENMINUS && prevType === HYPHENMINUS) {
+						if (offset + 1 < sourceLength && source.charCodeAt(offset + 1) === GREATERTHANSIGN) {
+							type = CDC;
+							offset = offset + 2;
+							tokenCount--; // rewrite prev token
+							break;
+						}
+					}
+
+					// ident(
+					if (code === LEFTPARENTHESIS && prevType === IDENTIFIER) {
+						offset = offset + 1;
+						tokenCount--; // rewrite prev token
+						balance[tokenCount] = balance[tokenCount + 1];
+						balanceStart--;
+
+						// 4 char length identifier and equal to `url(` (case insensitive)
+						if (offset - anchor === 4 && cmpStr(source, anchor, offset, "url(")) {
+							// special case for url() because it can contain any symbols sequence with few exceptions
+							anchor = findWhiteSpaceEnd(source, offset);
+							code = source.charCodeAt(anchor);
+							if (code !== LEFTPARENTHESIS &&
+								code !== RIGHTPARENTHESIS &&
+								code !== QUOTATIONMARK &&
+								code !== APOSTROPHE) {
+								// url(
+								offsetAndType[tokenCount++] = (URL << TYPE_SHIFT) | offset;
+								balance[tokenCount] = sourceLength;
+
+								// ws*
+								if (anchor !== offset) {
+									offsetAndType[tokenCount++] = (WHITESPACE << TYPE_SHIFT) | anchor;
+									balance[tokenCount] = sourceLength;
+								}
+
+								// raw
+								type = RAW;
+								offset = findUrlRawEnd(source, anchor);
+							} else {
+								type = URL;
+							}
+						} else {
+							type = FUNCTION;
+						}
+						break;
+					}
+
+					type = code;
+					offset = offset + 1;
+					break;
+
+				case NUMBER:
+					offset = findNumberEnd(source, offset + 1, prevType !== FULLSTOP);
+
+					// merge number with a preceding dot, dash or plus
+					if (prevType === FULLSTOP ||
+						prevType === HYPHENMINUS ||
+						prevType === PLUSSIGN) {
+						tokenCount--; // rewrite prev token
+					}
+
+					break;
+
+				case STRING:
+					offset = findStringEnd(source, offset + 1, code);
+					break;
+
+				default:
+					anchor = offset;
+					offset = findIdentifierEnd(source, offset);
+
+					// merge identifier with a preceding dash
+					if (prevType === HYPHENMINUS) {
+						// rewrite prev token
+						tokenCount--;
+						// restore prev prev token type
+						// for case @-prefix-ident
+						prevType = tokenCount === 0 ? 0 : offsetAndType[tokenCount - 1] >> TYPE_SHIFT;
+					}
+
+					if (prevType === COMMERCIALAT) {
+						// rewrite prev token and change type to <at-keyword-token>
+						tokenCount--;
+						type = ATKEYWORD;
+					}
+			}
+
+			offsetAndType[tokenCount++] = (type << TYPE_SHIFT) | offset;
+			prevType = type;
+		}
+
+		// finalize arrays
+		offsetAndType[tokenCount] = offset;
+		balance[tokenCount] = sourceLength;
+		balance[sourceLength] = sourceLength; // prevents false positive balance match with any token
+		while (balanceStart !== 0) {
+			balancePrev = balanceStart & OFFSET_MASK;
+			balanceStart = balance[balancePrev];
+			balance[balancePrev] = sourceLength;
+		}
+
+		tokenizer.offsetAndType = offsetAndType;
+		tokenizer.tokenCount = tokenCount;
+		tokenizer.balance = balance;
+	}
+
+	//
+	// tokenizer
+	//
+
+	function Tokenizer(source, startOffset, startLine, startColumn) {
+		this.offsetAndType = null;
+		this.balance = null;
+		this.lines = null;
+		this.columns = null;
+
+		this.setSource(source, startOffset, startLine, startColumn);
+	}
+
+	Tokenizer.prototype = {
+		setSource: function (source, startOffset, startLine, startColumn) {
+			const safeSource = String(source || "");
+			const start = firstCharOffset(safeSource);
+
+			this.source = safeSource;
+			this.firstCharOffset = start;
+			this.startOffset = typeof startOffset === "undefined" ? 0 : startOffset;
+			this.startLine = typeof startLine === "undefined" ? 1 : startLine;
+			this.startColumn = typeof startColumn === "undefined" ? 1 : startColumn;
+			this.linesAnsColumnsComputed = false;
+
+			this.eof = false;
+			this.currentToken = -1;
+			this.tokenType = 0;
+			this.tokenStart = start;
+			this.tokenEnd = start;
+
+			tokenLayout(this, safeSource, start);
+			this.next();
+		},
+
+		lookupType: function (offset) {
+			offset += this.currentToken;
+
+			if (offset < this.tokenCount) {
+				return this.offsetAndType[offset] >> TYPE_SHIFT;
+			}
+
+			return NULL;
+		},
+		lookupNonWSType: function (offset) {
+			offset += this.currentToken;
+
+			for (let type; offset < this.tokenCount; offset++) {
+				type = this.offsetAndType[offset] >> TYPE_SHIFT;
+
+				if (type !== WHITESPACE) {
+					return type;
+				}
+			}
+
+			return NULL;
+		},
+		lookupValue: function (offset, referenceStr) {
+			offset += this.currentToken;
+
+			if (offset < this.tokenCount) {
+				return cmpStr(
+					this.source,
+					this.offsetAndType[offset - 1] & OFFSET_MASK,
+					this.offsetAndType[offset] & OFFSET_MASK,
+					referenceStr
+				);
+			}
+
+			return false;
+		},
+		getTokenStart: function (tokenNum) {
+			if (tokenNum === this.currentToken) {
+				return this.tokenStart;
+			}
+
+			if (tokenNum > 0) {
+				return tokenNum < this.tokenCount
+					? this.offsetAndType[tokenNum - 1] & OFFSET_MASK
+					: this.offsetAndType[this.tokenCount] & OFFSET_MASK;
+			}
+
+			return this.firstCharOffset;
+		},
+		getOffsetExcludeWS: function () {
+			if (this.currentToken > 0) {
+				if ((this.offsetAndType[this.currentToken - 1] >> TYPE_SHIFT) === WHITESPACE) {
+					return this.currentToken > 1
+						? this.offsetAndType[this.currentToken - 2] & OFFSET_MASK
+						: this.firstCharOffset;
+				}
+			}
+			return this.tokenStart;
+		},
+		getRawLength: function (startToken, endTokenType1, endTokenType2, includeTokenType2) {
+			let cursor = startToken;
+			let balanceEnd;
+
+			loop:
+			for (; cursor < this.tokenCount; cursor++) {
+				balanceEnd = this.balance[cursor];
+
+				// belance end points to offset before start
+				if (balanceEnd < startToken) {
+					break loop;
+				}
+
+				// check token is stop type
+				switch (this.offsetAndType[cursor] >> TYPE_SHIFT) {
+					case endTokenType1:
+						break loop;
+
+					case endTokenType2:
+						if (includeTokenType2) {
+							cursor++;
+						}
+						break loop;
+
+					default:
+						// fast forward to the end of balanced block
+						if (this.balance[balanceEnd] === cursor) {
+							cursor = balanceEnd;
+						}
+				}
+
+			}
+
+			return cursor - this.currentToken;
+		},
+		isBalanceEdge: function (pos) {
+			const balanceStart = this.balance[this.currentToken];
+			return balanceStart < pos;
+		},
+
+		getTokenValue: function () {
+			return this.source.substring(this.tokenStart, this.tokenEnd);
+		},
+		substrToCursor: function (start) {
+			return this.source.substring(start, this.tokenStart);
+		},
+
+		skipWS: function () {
+			let skipTokenCount = 0;
+			for (let i = this.currentToken; i < this.tokenCount; i++ , skipTokenCount++) {
+				if ((this.offsetAndType[i] >> TYPE_SHIFT) !== WHITESPACE) {
+					break;
+				}
+			}
+
+			if (skipTokenCount > 0) {
+				this.skip(skipTokenCount);
+			}
+		},
+		skipSC: function () {
+			while (this.tokenType === WHITESPACE || this.tokenType === COMMENT) {
+				this.next();
+			}
+		},
+		skip: function (tokenCount) {
+			let next = this.currentToken + tokenCount;
+
+			if (next < this.tokenCount) {
+				this.currentToken = next;
+				this.tokenStart = this.offsetAndType[next - 1] & OFFSET_MASK;
+				next = this.offsetAndType[next];
+				this.tokenType = next >> TYPE_SHIFT;
+				this.tokenEnd = next & OFFSET_MASK;
+			} else {
+				this.currentToken = this.tokenCount;
+				this.next();
+			}
+		},
+		next: function () {
+			let next = this.currentToken + 1;
+
+			if (next < this.tokenCount) {
+				this.currentToken = next;
+				this.tokenStart = this.tokenEnd;
+				next = this.offsetAndType[next];
+				this.tokenType = next >> TYPE_SHIFT;
+				this.tokenEnd = next & OFFSET_MASK;
+			} else {
+				this.currentToken = this.tokenCount;
+				this.eof = true;
+				this.tokenType = NULL;
+				this.tokenStart = this.tokenEnd = this.source.length;
+			}
+		},
+
+		eat: function (tokenType) {
+			if (this.tokenType !== tokenType) {
+				let offset = this.tokenStart;
+				let message = NAME[tokenType] + " is expected";
+
+				// tweak message and offset
+				if (tokenType === IDENTIFIER) {
+					// when identifier is expected but there is a function or url
+					if (this.tokenType === FUNCTION || this.tokenType === URL) {
+						offset = this.tokenEnd - 1;
+						message += " but function found";
+					}
+				} else {
+					// when test type is part of another token show error for current position + 1
+					// e.g. eat(HYPHENMINUS) will fail on "-foo", but pointing on "-" is odd
+					if (this.source.charCodeAt(this.tokenStart) === tokenType) {
+						offset = offset + 1;
+					}
+				}
+
+				this.error(message, offset);
+			}
+
+			this.next();
+		},
+		eatNonWS: function (tokenType) {
+			this.skipWS();
+			this.eat(tokenType);
+		},
+
+		consume: function (tokenType) {
+			const value = this.getTokenValue();
+
+			this.eat(tokenType);
+
+			return value;
+		},
+		consumeFunctionName: function () {
+			const name = this.source.substring(this.tokenStart, this.tokenEnd - 1);
+
+			this.eat(FUNCTION);
+
+			return name;
+		},
+		consumeNonWS: function (tokenType) {
+			this.skipWS();
+
+			return this.consume(tokenType);
+		},
+
+		expectIdentifier: function (name) {
+			if (this.tokenType !== IDENTIFIER || cmpStr(this.source, this.tokenStart, this.tokenEnd, name) === false) {
+				this.error("Identifier `" + name + "` is expected");
+			}
+
+			this.next();
+		},
+
+		getLocation: function (offset, filename) {
+			if (!this.linesAnsColumnsComputed) {
+				computeLinesAndColumns(this, this.source);
+			}
+
+			return {
+				source: filename,
+				offset: this.startOffset + offset,
+				line: this.lines[offset],
+				column: this.columns[offset]
+			};
+		},
+
+		getLocationRange: function (start, end, filename) {
+			if (!this.linesAnsColumnsComputed) {
+				computeLinesAndColumns(this, this.source);
+			}
+
+			return {
+				source: filename,
+				start: {
+					offset: this.startOffset + start,
+					line: this.lines[start],
+					column: this.columns[start]
+				},
+				end: {
+					offset: this.startOffset + end,
+					line: this.lines[end],
+					column: this.columns[end]
+				}
+			};
+		},
+
+		error: function (message, offset) {
+			const location = typeof offset !== "undefined" && offset < this.source.length
+				? this.getLocation(offset)
+				: this.eof
+					? this.getLocation(findWhiteSpaceStart(this.source, this.source.length - 1))
+					: this.getLocation(this.tokenStart);
+
+			throw new CssSyntaxError(
+				message || "Unexpected input",
+				this.source,
+				location.offset,
+				location.line,
+				location.column
+			);
+		},
+
+		dump: function () {
+			let offset = 0;
+
+			return Array.prototype.slice.call(this.offsetAndType, 0, this.tokenCount).map(function (item, idx) {
+				const start = offset;
+				const end = item & OFFSET_MASK;
+
+				offset = end;
+
+				return {
+					idx: idx,
+					type: NAME[item >> TYPE_SHIFT],
+					chunk: this.source.substring(start, end),
+					balance: this.balance[idx]
+				};
+			}, this);
+		}
+	};
+
+	// extend with error class
+	Tokenizer.CssSyntaxError = CssSyntaxError;
+
+	// extend tokenizer with constants
+	Object.keys(constants).forEach(function (key) {
+		Tokenizer[key] = constants[key];
+	});
+
+	// extend tokenizer with static methods from utils
+	Object.keys(utils).forEach(function (key) {
+		Tokenizer[key] = utils[key];
+	});
+
+	// warm up tokenizer to elimitate code branches that never execute
+	// fix soft deoptimizations (insufficient type feedback)
+	new Tokenizer("\n\r\r\n\f<!---->//\"\"''/*\r\n\f*/1a;.\\31\t+2{url(a);func();+1.2e3 -.4e-5 .6e+7}").getLocation();
+
+	// ---
+
+	const sequence = function readSequence(recognizer) {
+		const children = this.createList();
+		let child = null;
+		const context = {
+			recognizer: recognizer,
+			space: null,
+			ignoreWS: false,
+			ignoreWSAfter: false
+		};
+
+		this.scanner.skipSC();
+
+		while (!this.scanner.eof) {
+			switch (this.scanner.tokenType) {
+				case COMMENT:
+					this.scanner.next();
+					continue;
+
+				case WHITESPACE:
+					if (context.ignoreWS) {
+						this.scanner.next();
+					} else {
+						context.space = this.WhiteSpace();
+					}
+					continue;
+			}
+
+			child = recognizer.getNode.call(this, context);
+
+			if (child === undefined) {
+				break;
+			}
+
+			if (context.space !== null) {
+				children.push(context.space);
+				context.space = null;
+			}
+
+			children.push(child);
+
+			if (context.ignoreWSAfter) {
+				context.ignoreWSAfter = false;
+				context.ignoreWS = true;
+			} else {
+				context.ignoreWS = false;
+			}
+		}
+
+		return children;
+	};
+
+	// ---
+
+	const noop = function () { };
+
+	function createParseContext(name) {
+		return function () {
+			return this[name]();
+		};
+	}
+
+	function processConfig(config) {
+		const parserConfig = {
+			context: {},
+			scope: {},
+			atrule: {},
+			pseudo: {}
+		};
+
+		if (config.parseContext) {
+			for (let name in config.parseContext) {
+				switch (typeof config.parseContext[name]) {
+					case "function":
+						parserConfig.context[name] = config.parseContext[name];
+						break;
+
+					case "string":
+						parserConfig.context[name] = createParseContext(config.parseContext[name]);
+						break;
+				}
+			}
+		}
+
+		if (config.scope) {
+			for (let name in config.scope) {
+				parserConfig.scope[name] = config.scope[name];
+			}
+		}
+
+		if (config.atrule) {
+			for (let name in config.atrule) {
+				const atrule = config.atrule[name];
+
+				if (atrule.parse) {
+					parserConfig.atrule[name] = atrule.parse;
+				}
+			}
+		}
+
+		if (config.pseudo) {
+			for (let name in config.pseudo) {
+				const pseudo = config.pseudo[name];
+
+				if (pseudo.parse) {
+					parserConfig.pseudo[name] = pseudo.parse;
+				}
+			}
+		}
+
+		if (config.node) {
+			for (let name in config.node) {
+				parserConfig[name] = config.node[name].parse;
+			}
+		}
+
+		return parserConfig;
+	}
+
+	function createParser(config) {
+		const parser = {
+			scanner: new Tokenizer(),
+			filename: "<unknown>",
+			needPositions: false,
+			onParseError: noop,
+			onParseErrorThrow: false,
+			parseAtrulePrelude: true,
+			parseRulePrelude: true,
+			parseValue: true,
+			parseCustomProperty: false,
+
+			readSequence: sequence,
+
+			createList: function () {
+				return new List();
+			},
+			createSingleNodeList: function (node) {
+				return new List().appendData(node);
+			},
+			getFirstListNode: function (list) {
+				return list && list.first();
+			},
+			getLastListNode: function (list) {
+				return list.last();
+			},
+
+			parseWithFallback: function (consumer, fallback) {
+				const startToken = this.scanner.currentToken;
+
+				try {
+					return consumer.call(this);
+				} catch (e) {
+					if (this.onParseErrorThrow) {
+						throw e;
+					}
+
+					const fallbackNode = fallback.call(this, startToken);
+
+					this.onParseErrorThrow = true;
+					this.onParseError(e, fallbackNode);
+					this.onParseErrorThrow = false;
+
+					return fallbackNode;
+				}
+			},
+
+			getLocation: function (start, end) {
+				if (this.needPositions) {
+					return this.scanner.getLocationRange(
+						start,
+						end,
+						this.filename
+					);
+				}
+
+				return null;
+			},
+			getLocationFromList: function (list) {
+				if (this.needPositions) {
+					const head = this.getFirstListNode(list);
+					const tail = this.getLastListNode(list);
+					return this.scanner.getLocationRange(
+						head !== null ? head.loc.start.offset - this.scanner.startOffset : this.scanner.tokenStart,
+						tail !== null ? tail.loc.end.offset - this.scanner.startOffset : this.scanner.tokenStart,
+						this.filename
+					);
+				}
+
+				return null;
+			}
+		};
+
+		config = processConfig(config || {});
+		for (let key in config) {
+			parser[key] = config[key];
+		}
+
+		return function (source, options) {
+			options = options || {};
+
+			const context = options.context || "default";
+			let ast;
+
+			parser.scanner.setSource(source, options.offset, options.line, options.column);
+			parser.filename = options.filename || "<unknown>";
+			parser.needPositions = Boolean(options.positions);
+			parser.onParseError = typeof options.onParseError === "function" ? options.onParseError : noop;
+			parser.onParseErrorThrow = false;
+			parser.parseAtrulePrelude = "parseAtrulePrelude" in options ? Boolean(options.parseAtrulePrelude) : true;
+			parser.parseRulePrelude = "parseRulePrelude" in options ? Boolean(options.parseRulePrelude) : true;
+			parser.parseValue = "parseValue" in options ? Boolean(options.parseValue) : true;
+			parser.parseCustomProperty = "parseCustomProperty" in options ? Boolean(options.parseCustomProperty) : false;
+
+			if (!parser.context.hasOwnProperty(context)) {
+				throw new Error("Unknown context `" + context + "`");
+			}
+
+			ast = parser.context[context].call(parser, options);
+
+			if (!parser.scanner.eof) {
+				parser.scanner.error();
+			}
+
+			return ast;
+		};
+	}
+
+	// ---
+
+	const U = 117; // 'u'.charCodeAt(0)
+
+	const getNode = function defaultRecognizer(context) {
+		switch (this.scanner.tokenType) {
+			case NUMBERSIGN:
+				return this.HexColor();
+
+			case COMMA:
+				context.space = null;
+				context.ignoreWSAfter = true;
+				return this.Operator();
+
+			case SOLIDUS:
+			case ASTERISK:
+			case PLUSSIGN:
+			case HYPHENMINUS:
+				return this.Operator();
+
+			case LEFTPARENTHESIS:
+				return this.Parentheses(this.readSequence, context.recognizer);
+
+			case LEFTSQUAREBRACKET:
+				return this.Brackets(this.readSequence, context.recognizer);
+
+			case STRING:
+				return this.String();
+
+			case NUMBER:
+				switch (this.scanner.lookupType(1)) {
+					case PERCENTSIGN:
+						return this.Percentage();
+
+					case IDENTIFIER:
+						// edge case: number with folowing \0 and \9 hack shouldn"t to be a Dimension
+						if (cmpChar(this.scanner.source, this.scanner.tokenEnd, BACKSLASH)) {
+							return this.Number();
+						} else {
+							return this.Dimension();
+						}
+
+					default:
+						return this.Number();
+				}
+
+			case FUNCTION:
+				return this.Function(this.readSequence, context.recognizer);
+
+			case URL:
+				return this.Url();
+
+			case IDENTIFIER:
+				// check for unicode range, it should start with u+ or U+
+				if (cmpChar(this.scanner.source, this.scanner.tokenStart, U) &&
+					cmpChar(this.scanner.source, this.scanner.tokenStart + 1, PLUSSIGN)) {
+					return this.UnicodeRange();
+				} else {
+					return this.Identifier();
+				}
+		}
+	};
+
+	// ---
+
+	const AtrulePrelude = {
+		getNode: getNode
+	};
+
+	// ---
+
+	function Selector_getNode(context) {
+		switch (this.scanner.tokenType) {
+			case PLUSSIGN:
+			case GREATERTHANSIGN:
+			case TILDE:
+				context.space = null;
+				context.ignoreWSAfter = true;
+				return this.Combinator();
+
+			case SOLIDUS:  // /deep/
+				return this.Combinator();
+
+			case FULLSTOP:
+				return this.ClassSelector();
+
+			case LEFTSQUAREBRACKET:
+				return this.AttributeSelector();
+
+			case NUMBERSIGN:
+				return this.IdSelector();
+
+			case COLON:
+				if (this.scanner.lookupType(1) === COLON) {
+					return this.PseudoElementSelector();
+				} else {
+					return this.PseudoClassSelector();
+				}
+
+			case IDENTIFIER:
+			case ASTERISK:
+			case VERTICALLINE:
+				return this.TypeSelector();
+
+			case NUMBER:
+				return this.Percentage();
+		}
+	}
+
+	const Selector = {
+		getNode: Selector_getNode
+	};
+
+	// ---
+
+	const Value_getNode = function defaultRecognizer(context) {
+		switch (this.scanner.tokenType) {
+			case NUMBERSIGN:
+				return this.HexColor();
+
+			case COMMA:
+				context.space = null;
+				context.ignoreWSAfter = true;
+				return this.Operator();
+
+			case SOLIDUS:
+			case ASTERISK:
+			case PLUSSIGN:
+			case HYPHENMINUS:
+				return this.Operator();
+
+			case LEFTPARENTHESIS:
+				return this.Parentheses(this.readSequence, context.recognizer);
+
+			case LEFTSQUAREBRACKET:
+				return this.Brackets(this.readSequence, context.recognizer);
+
+			case STRING:
+				return this.String();
+
+			case NUMBER:
+				switch (this.scanner.lookupType(1)) {
+					case PERCENTSIGN:
+						return this.Percentage();
+
+					case IDENTIFIER:
+						// edge case: number with folowing \0 and \9 hack shouldn't to be a Dimension
+						if (cmpChar(this.scanner.source, this.scanner.tokenEnd, BACKSLASH)) {
+							return this.Number();
+						} else {
+							return this.Dimension();
+						}
+
+					default:
+						return this.Number();
+				}
+
+			case FUNCTION:
+				return this.Function(this.readSequence, context.recognizer);
+
+			case URL:
+				return this.Url();
+
+			case IDENTIFIER:
+				// check for unicode range, it should start with u+ or U+
+				if (cmpChar(this.scanner.source, this.scanner.tokenStart, U) &&
+					cmpChar(this.scanner.source, this.scanner.tokenStart + 1, PLUSSIGN)) {
+					return this.UnicodeRange();
+				} else {
+					return this.Identifier();
+				}
+		}
+	};
+
+	// ---
+
+	// https://drafts.csswg.org/css-images-4/#element-notation
+	// https://developer.mozilla.org/en-US/docs/Web/CSS/element
+	const Value_Element = function () {
+		this.scanner.skipSC();
+
+		const children = this.createSingleNodeList(
+			this.IdSelector()
+		);
+
+		this.scanner.skipSC();
+
+		return children;
+	};
+
+	// ---
+
+	// legacy IE function
+	// expression '(' raw ')'
+	const Value_expression = function () {
+		return this.createSingleNodeList(
+			this.Raw(this.scanner.currentToken, 0, 0, false, false)
+		);
+	};
+
+	// ---	
+
+	// let '(' ident (',' <value>? )? ')'
+	const Value_var = function () {
+		const children = this.createList();
+
+		this.scanner.skipSC();
+
+		const identStart = this.scanner.tokenStart;
+
+		this.scanner.eat(HYPHENMINUS);
+		if (this.scanner.source.charCodeAt(this.scanner.tokenStart) !== HYPHENMINUS) {
+			this.scanner.error("HyphenMinus is expected");
+		}
+		this.scanner.eat(IDENTIFIER);
+
+		children.push({
+			type: "Identifier",
+			loc: this.getLocation(identStart, this.scanner.tokenStart),
+			name: this.scanner.substrToCursor(identStart)
+		});
+
+		this.scanner.skipSC();
+
+		if (this.scanner.tokenType === COMMA) {
+			children.push(this.Operator());
+			children.push(this.parseCustomProperty
+				? this.Value(null)
+				: this.Raw(this.scanner.currentToken, EXCLAMATIONMARK, SEMICOLON, false, false)
+			);
+		}
+
+		return children;
+	};
+
+	// ---
+
+	const Value = {
+		getNode: Value_getNode,
+		"-moz-element": Value_Element,
+		"element": Value_Element,
+		"expression": Value_expression,
+		"let": Value_var
+	};
+
+	// ---
+
+	const scope = {
+		AtrulePrelude: AtrulePrelude,
+		Selector: Selector,
+		Value: Value
+	};
+
+	// ---
+
+	const fontFace = {
+		parse: {
+			prelude: null,
+			block: function () {
+				return this.Block(true);
+			}
+		}
+	};
+
+	// ---
+
+	const _import = {
+		parse: {
+			prelude: function () {
+				const children = this.createList();
+
+				this.scanner.skipSC();
+
+				switch (this.scanner.tokenType) {
+					case STRING:
+						children.push(this.String());
+						break;
+
+					case URL:
+						children.push(this.Url());
+						break;
+
+					default:
+						this.scanner.error("String or url() is expected");
+				}
+
+				if (this.scanner.lookupNonWSType(0) === IDENTIFIER ||
+					this.scanner.lookupNonWSType(0) === LEFTPARENTHESIS) {
+					children.push(this.WhiteSpace());
+					children.push(this.MediaQueryList());
+				}
+
+				return children;
+			},
+			block: null
+		}
+	};
+
+	// ---
+
+	const media = {
+		parse: {
+			prelude: function () {
+				return this.createSingleNodeList(
+					this.MediaQueryList()
+				);
+			},
+			block: function () {
+				return this.Block(false);
+			}
+		}
+	};
+
+	// ---
+
+	const page = {
+		parse: {
+			prelude: function () {
+				return this.createSingleNodeList(
+					this.SelectorList()
+				);
+			},
+			block: function () {
+				return this.Block(true);
+			}
+		}
+	};
+
+	// ---
+
+	function supports_consumeRaw() {
+		return this.createSingleNodeList(
+			this.Raw(this.scanner.currentToken, 0, 0, false, false)
+		);
+	}
+
+	function parentheses() {
+		let index = 0;
+
+		this.scanner.skipSC();
+
+		// TODO: make it simplier
+		if (this.scanner.tokenType === IDENTIFIER) {
+			index = 1;
+		} else if (this.scanner.tokenType === HYPHENMINUS &&
+			this.scanner.lookupType(1) === IDENTIFIER) {
+			index = 2;
+		}
+
+		if (index !== 0 && this.scanner.lookupNonWSType(index) === COLON) {
+			return this.createSingleNodeList(
+				this.Declaration()
+			);
+		}
+
+		return readSequence.call(this);
+	}
+
+	function readSequence() {
+		const children = this.createList();
+		let space = null;
+		let child;
+
+		this.scanner.skipSC();
+
+		scan:
+		while (!this.scanner.eof) {
+			switch (this.scanner.tokenType) {
+				case WHITESPACE:
+					space = this.WhiteSpace();
+					continue;
+
+				case COMMENT:
+					this.scanner.next();
+					continue;
+
+				case FUNCTION:
+					child = this.Function(supports_consumeRaw, this.scope.AtrulePrelude);
+					break;
+
+				case IDENTIFIER:
+					child = this.Identifier();
+					break;
+
+				case LEFTPARENTHESIS:
+					child = this.Parentheses(parentheses, this.scope.AtrulePrelude);
+					break;
+
+				default:
+					break scan;
+			}
+
+			if (space !== null) {
+				children.push(space);
+				space = null;
+			}
+
+			children.push(child);
+		}
+
+		return children;
+	}
+
+	const supports = {
+		parse: {
+			prelude: function () {
+				const children = readSequence.call(this);
+
+				if (this.getFirstListNode(children) === null) {
+					this.scanner.error("Condition is expected");
+				}
+
+				return children;
+			},
+			block: function () {
+				return this.Block(false);
+			}
+		}
+	};
+
+	// ---
+
+	const atrule = {
+		"font-face": fontFace,
+		"import": _import,
+		media: media,
+		page: page,
+		supports: supports
+	};
+
+	// ---
+
+	const dir = {
+		parse: function () {
+			return this.createSingleNodeList(
+				this.Identifier()
+			);
+		}
+	};
+
+	// ---
+
+	const has = {
+		parse: function () {
+			return this.createSingleNodeList(
+				this.SelectorList()
+			);
+		}
+	};
+
+	// ---
+
+	const lang = {
+		parse: function () {
+			return this.createSingleNodeList(
+				this.Identifier()
+			);
+		}
+	};
+
+	// ---
+
+	const matches = {
+		parse: function selectorList() {
+			return this.createSingleNodeList(
+				this.SelectorList()
+			);
+		}
+	};
+	const not = matches;
+
+	// ---
+
+	const ALLOW_OF_CLAUSE = true;
+
+	const nthChild = {
+		parse: function nthWithOfClause() {
+			return this.createSingleNodeList(
+				this.Nth(ALLOW_OF_CLAUSE)
+			);
+		}
+	};
+	const nthLastChild = nthChild;
+
+	// ---
+
+	const DISALLOW_OF_CLAUSE = false;
+
+	const nthLastOfType = {
+		parse: function nth() {
+			return this.createSingleNodeList(
+				this.Nth(DISALLOW_OF_CLAUSE)
+			);
+		}
+	};
+	const nthOfType = nthLastOfType;
+
+	// ---
+
+	const slotted = {
+		parse: function compoundSelector() {
+			return this.createSingleNodeList(
+				this.Selector()
+			);
+		}
+	};
+
+	// ---
+
+	const pseudo = {
+		dir: dir,
+		has: has,
+		lang: lang,
+		matches: matches,
+		not: not,
+		"nth-child": nthChild,
+		"nth-last-child": nthLastChild,
+		"nth-last-of-type": nthLastOfType,
+		"nth-of-type": nthOfType,
+		slotted: slotted
+	};
+
+	// ---
+
+	const AnPlusB_N = 110; // 'n'.charCodeAt(0)
+	const DISALLOW_SIGN = true;
+	const ALLOW_SIGN = false;
+
+	function checkTokenIsInteger(scanner, disallowSign) {
+		let pos = scanner.tokenStart;
+
+		if (scanner.source.charCodeAt(pos) === PLUSSIGN ||
+			scanner.source.charCodeAt(pos) === HYPHENMINUS) {
+			if (disallowSign) {
+				scanner.error();
+			}
+			pos++;
+		}
+
+		for (; pos < scanner.tokenEnd; pos++) {
+			if (!isNumber(scanner.source.charCodeAt(pos))) {
+				scanner.error("Unexpected input", pos);
+			}
+		}
+	}
+
+	// An+B microsyntax https://www.w3.org/TR/css-syntax-3/#anb
+	const AnPlusB = {
+		name: "AnPlusB",
+		structure: {
+			a: [String, null],
+			b: [String, null]
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+			let end = start;
+			let prefix = "";
+			let a = null;
+			let b = null;
+
+			if (this.scanner.tokenType === NUMBER ||
+				this.scanner.tokenType === PLUSSIGN) {
+				checkTokenIsInteger(this.scanner, ALLOW_SIGN);
+				prefix = this.scanner.getTokenValue();
+				this.scanner.next();
+				end = this.scanner.tokenStart;
+			}
+
+			if (this.scanner.tokenType === IDENTIFIER) {
+				let bStart = this.scanner.tokenStart;
+
+				if (cmpChar(this.scanner.source, bStart, HYPHENMINUS)) {
+					if (prefix === "") {
+						prefix = "-";
+						bStart++;
+					} else {
+						this.scanner.error("Unexpected hyphen minus");
+					}
+				}
+
+				if (!cmpChar(this.scanner.source, bStart, AnPlusB_N)) {
+					this.scanner.error();
+				}
+
+				a = prefix === "" ? "1" :
+					prefix === "+" ? "+1" :
+						prefix === "-" ? "-1" :
+							prefix;
+
+				const len = this.scanner.tokenEnd - bStart;
+				if (len > 1) {
+					// ..n-..
+					if (this.scanner.source.charCodeAt(bStart + 1) !== HYPHENMINUS) {
+						this.scanner.error("Unexpected input", bStart + 1);
+					}
+
+					if (len > 2) {
+						// ..n-{number}..
+						this.scanner.tokenStart = bStart + 2;
+					} else {
+						// ..n- {number}
+						this.scanner.next();
+						this.scanner.skipSC();
+					}
+
+					checkTokenIsInteger(this.scanner, DISALLOW_SIGN);
+					b = "-" + this.scanner.getTokenValue();
+					this.scanner.next();
+					end = this.scanner.tokenStart;
+				} else {
+					prefix = "";
+					this.scanner.next();
+					end = this.scanner.tokenStart;
+					this.scanner.skipSC();
+
+					if (this.scanner.tokenType === HYPHENMINUS ||
+						this.scanner.tokenType === PLUSSIGN) {
+						prefix = this.scanner.getTokenValue();
+						this.scanner.next();
+						this.scanner.skipSC();
+					}
+
+					if (this.scanner.tokenType === NUMBER) {
+						checkTokenIsInteger(this.scanner, prefix !== "");
+
+						if (!isNumber(this.scanner.source.charCodeAt(this.scanner.tokenStart))) {
+							prefix = this.scanner.source.charAt(this.scanner.tokenStart);
+							this.scanner.tokenStart++;
+						}
+
+						if (prefix === "") {
+							// should be an operator before number
+							this.scanner.error();
+						} else if (prefix === "+") {
+							// plus is using by default
+							prefix = "";
+						}
+
+						b = prefix + this.scanner.getTokenValue();
+
+						this.scanner.next();
+						end = this.scanner.tokenStart;
+					} else {
+						if (prefix) {
+							this.scanner.eat(NUMBER);
+						}
+					}
+				}
+			} else {
+				if (prefix === "" || prefix === "+") { // no number
+					this.scanner.error(
+						"Number or identifier is expected",
+						this.scanner.tokenStart + (
+							this.scanner.tokenType === PLUSSIGN ||
+							this.scanner.tokenType === HYPHENMINUS
+						)
+					);
+				}
+
+				b = prefix;
+			}
+
+			return {
+				type: "AnPlusB",
+				loc: this.getLocation(start, end),
+				a: a,
+				b: b
+			};
+		},
+		generate: function (node) {
+			const a = node.a !== null && node.a !== undefined;
+			let b = node.b !== null && node.b !== undefined;
+
+			if (a) {
+				this.chunk(
+					node.a === "+1" ? "+n" :
+						node.a === "1" ? "n" :
+							node.a === "-1" ? "-n" :
+								node.a + "n"
+				);
+
+				if (b) {
+					b = String(node.b);
+					if (b.charAt(0) === "-" || b.charAt(0) === "+") {
+						this.chunk(b.charAt(0));
+						this.chunk(b.substr(1));
+					} else {
+						this.chunk("+");
+						this.chunk(b);
+					}
+				}
+			} else {
+				this.chunk(String(node.b));
+			}
+		}
+	};
+
+	// ---
+
+	function Atrule_consumeRaw(startToken) {
+		return this.Raw(startToken, SEMICOLON, LEFTCURLYBRACKET, false, true);
+	}
+
+	function isDeclarationBlockAtrule() {
+		for (let offset = 1, type; type = this.scanner.lookupType(offset); offset++) { // eslint-disable-line no-cond-assign
+			if (type === RIGHTCURLYBRACKET) {
+				return true;
+			}
+
+			if (type === LEFTCURLYBRACKET ||
+				type === ATKEYWORD) {
+				return false;
+			}
+		}
+
+		return false;
+	}
+
+	const Atrule = {
+		name: "Atrule",
+		structure: {
+			name: String,
+			prelude: ["AtrulePrelude", "Raw", null],
+			block: ["Block", null]
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+			let name;
+			let nameLowerCase;
+			let prelude = null;
+			let block = null;
+
+			this.scanner.eat(ATKEYWORD);
+
+			name = this.scanner.substrToCursor(start + 1);
+			nameLowerCase = name.toLowerCase();
+			this.scanner.skipSC();
+
+			// parse prelude
+			if (this.scanner.eof === false &&
+				this.scanner.tokenType !== LEFTCURLYBRACKET &&
+				this.scanner.tokenType !== SEMICOLON) {
+				if (this.parseAtrulePrelude) {
+					prelude = this.parseWithFallback(this.AtrulePrelude.bind(this, name), Atrule_consumeRaw);
+
+					// turn empty AtrulePrelude into null
+					if (prelude.type === "AtrulePrelude" && prelude.children.head === null) {
+						prelude = null;
+					}
+				} else {
+					prelude = Atrule_consumeRaw.call(this, this.scanner.currentToken);
+				}
+
+				this.scanner.skipSC();
+			}
+
+			switch (this.scanner.tokenType) {
+				case SEMICOLON:
+					this.scanner.next();
+					break;
+
+				case LEFTCURLYBRACKET:
+					if (this.atrule.hasOwnProperty(nameLowerCase) &&
+						typeof this.atrule[nameLowerCase].block === "function") {
+						block = this.atrule[nameLowerCase].block.call(this);
+					} else {
+						// TODO: should consume block content as Raw?
+						block = this.Block(isDeclarationBlockAtrule.call(this));
+					}
+
+					break;
+			}
+
+			return {
+				type: "Atrule",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				name: name,
+				prelude: prelude,
+				block: block
+			};
+		},
+		generate: function (node) {
+			this.chunk("@");
+			this.chunk(node.name);
+
+			if (node.prelude !== null) {
+				this.chunk(" ");
+				this.node(node.prelude);
+			}
+
+			if (node.block) {
+				this.node(node.block);
+			} else {
+				this.chunk(";");
+			}
+		},
+		walkContext: "atrule"
+	};
+
+	// ---
+
+	const Syntax_AtrulePrelude = {
+		name: "AtrulePrelude",
+		structure: {
+			children: [[]]
+		},
+		parse: function (name) {
+			let children = null;
+
+			if (name !== null) {
+				name = name.toLowerCase();
+			}
+
+			this.scanner.skipSC();
+
+			if (this.atrule.hasOwnProperty(name) &&
+				typeof this.atrule[name].prelude === "function") {
+				// custom consumer
+				children = this.atrule[name].prelude.call(this);
+			} else {
+				// default consumer
+				children = this.readSequence(this.scope.AtrulePrelude);
+			}
+
+			this.scanner.skipSC();
+
+			if (this.scanner.eof !== true &&
+				this.scanner.tokenType !== LEFTCURLYBRACKET &&
+				this.scanner.tokenType !== SEMICOLON) {
+				this.scanner.error("Semicolon or block is expected");
+			}
+
+			if (children === null) {
+				children = this.createList();
+			}
+
+			return {
+				type: "AtrulePrelude",
+				loc: this.getLocationFromList(children),
+				children: children
+			};
+		},
+		generate: function (node) {
+			this.children(node);
+		},
+		walkContext: "atrulePrelude"
+	};
+
+	// ---
+
+	function getAttributeName() {
+		if (this.scanner.eof) {
+			this.scanner.error("Unexpected end of input");
+		}
+
+		const start = this.scanner.tokenStart;
+		let expectIdentifier = false;
+		let checkColon = true;
+
+		if (this.scanner.tokenType === ASTERISK) {
+			expectIdentifier = true;
+			checkColon = false;
+			this.scanner.next();
+		} else if (this.scanner.tokenType !== VERTICALLINE) {
+			this.scanner.eat(IDENTIFIER);
+		}
+
+		if (this.scanner.tokenType === VERTICALLINE) {
+			if (this.scanner.lookupType(1) !== EQUALSSIGN) {
+				this.scanner.next();
+				this.scanner.eat(IDENTIFIER);
+			} else if (expectIdentifier) {
+				this.scanner.error("Identifier is expected", this.scanner.tokenEnd);
+			}
+		} else if (expectIdentifier) {
+			this.scanner.error("Vertical line is expected");
+		}
+
+		if (checkColon && this.scanner.tokenType === COLON) {
+			this.scanner.next();
+			this.scanner.eat(IDENTIFIER);
+		}
+
+		return {
+			type: "Identifier",
+			loc: this.getLocation(start, this.scanner.tokenStart),
+			name: this.scanner.substrToCursor(start)
+		};
+	}
+
+	function getOperator() {
+		const start = this.scanner.tokenStart;
+		const tokenType = this.scanner.tokenType;
+
+		if (tokenType !== EQUALSSIGN &&        // =
+			tokenType !== TILDE &&             // ~=
+			tokenType !== CIRCUMFLEXACCENT &&  // ^=
+			tokenType !== DOLLARSIGN &&        // $=
+			tokenType !== ASTERISK &&          // *=
+			tokenType !== VERTICALLINE         // |=
+		) {
+			this.scanner.error("Attribute selector (=, ~=, ^=, $=, *=, |=) is expected");
+		}
+
+		if (tokenType === EQUALSSIGN) {
+			this.scanner.next();
+		} else {
+			this.scanner.next();
+			this.scanner.eat(EQUALSSIGN);
+		}
+
+		return this.scanner.substrToCursor(start);
+	}
+
+	// "[" S* attrib_name "]"
+	// "[" S* attrib_name S* attrib_matcher S* [ IDENT | STRING ] S* attrib_flags? S* "]"
+	const AttributeSelector = {
+		name: "AttributeSelector",
+		structure: {
+			name: "Identifier",
+			matcher: [String, null],
+			value: ["String", "Identifier", null],
+			flags: [String, null]
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+			let name;
+			let matcher = null;
+			let value = null;
+			let flags = null;
+
+			this.scanner.eat(LEFTSQUAREBRACKET);
+			this.scanner.skipSC();
+
+			name = getAttributeName.call(this);
+			this.scanner.skipSC();
+
+			if (this.scanner.tokenType !== RIGHTSQUAREBRACKET) {
+				// avoid case `[name i]`
+				if (this.scanner.tokenType !== IDENTIFIER) {
+					matcher = getOperator.call(this);
+
+					this.scanner.skipSC();
+
+					value = this.scanner.tokenType === STRING
+						? this.String()
+						: this.Identifier();
+
+					this.scanner.skipSC();
+				}
+
+				// attribute flags
+				if (this.scanner.tokenType === IDENTIFIER) {
+					flags = this.scanner.getTokenValue();
+					this.scanner.next();
+
+					this.scanner.skipSC();
+				}
+			}
+
+			this.scanner.eat(RIGHTSQUAREBRACKET);
+
+			return {
+				type: "AttributeSelector",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				name: name,
+				matcher: matcher,
+				value: value,
+				flags: flags
+			};
+		},
+		generate: function (node) {
+			let flagsPrefix = " ";
+
+			this.chunk("[");
+			this.node(node.name);
+
+			if (node.matcher !== null) {
+				this.chunk(node.matcher);
+
+				if (node.value !== null) {
+					this.node(node.value);
+
+					// space between string and flags is not required
+					if (node.value.type === "String") {
+						flagsPrefix = "";
+					}
+				}
+			}
+
+			if (node.flags !== null) {
+				this.chunk(flagsPrefix);
+				this.chunk(node.flags);
+			}
+
+			this.chunk("]");
+		}
+	};
+
+	// ---
+
+	function Block_consumeRaw(startToken) {
+		return this.Raw(startToken, 0, 0, false, true);
+	}
+	function consumeRule() {
+		return this.parseWithFallback(this.Rule, Block_consumeRaw);
+	}
+	function consumeRawDeclaration(startToken) {
+		return this.Raw(startToken, 0, SEMICOLON, true, true);
+	}
+	function consumeDeclaration() {
+		if (this.scanner.tokenType === SEMICOLON) {
+			return consumeRawDeclaration.call(this, this.scanner.currentToken);
+		}
+
+		const node = this.parseWithFallback(this.Declaration, consumeRawDeclaration);
+
+		if (this.scanner.tokenType === SEMICOLON) {
+			this.scanner.next();
+		}
+
+		return node;
+	}
+
+	const Block = {
+		name: "Block",
+		structure: {
+			children: [[
+				"Atrule",
+				"Rule",
+				"Declaration"
+			]]
+		},
+		parse: function (isDeclaration) {
+			const consumer = isDeclaration ? consumeDeclaration : consumeRule;
+
+			const start = this.scanner.tokenStart;
+			const children = this.createList();
+
+			this.scanner.eat(LEFTCURLYBRACKET);
+
+			scan:
+			while (!this.scanner.eof) {
+				switch (this.scanner.tokenType) {
+					case RIGHTCURLYBRACKET:
+						break scan;
+
+					case WHITESPACE:
+					case COMMENT:
+						this.scanner.next();
+						break;
+
+					case ATKEYWORD:
+						children.push(this.parseWithFallback(this.Atrule, Block_consumeRaw));
+						break;
+
+					default:
+						children.push(consumer.call(this));
+				}
+			}
+
+			if (!this.scanner.eof) {
+				this.scanner.eat(RIGHTCURLYBRACKET);
+			}
+
+			return {
+				type: "Block",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				children: children
+			};
+		},
+		generate: function (node) {
+			this.chunk("{");
+			this.children(node, function (prev) {
+				if (prev.type === "Declaration") {
+					this.chunk(";");
+				}
+			});
+			this.chunk("}");
+		},
+		walkContext: "block"
+	};
+
+	// ---
+
+	const Brackets = {
+		name: "Brackets",
+		structure: {
+			children: [[]]
+		},
+		parse: function (readSequence, recognizer) {
+			const start = this.scanner.tokenStart;
+			let children = null;
+
+			this.scanner.eat(LEFTSQUAREBRACKET);
+
+			children = readSequence.call(this, recognizer);
+
+			if (!this.scanner.eof) {
+				this.scanner.eat(RIGHTSQUAREBRACKET);
+			}
+
+			return {
+				type: "Brackets",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				children: children
+			};
+		},
+		generate: function (node) {
+			this.chunk("[");
+			this.children(node);
+			this.chunk("]");
+		}
+	};
+
+	// ---
+
+	const Syntax_CDC = {
+		name: "CDC",
+		structure: [],
+		parse: function () {
+			const start = this.scanner.tokenStart;
+
+			this.scanner.eat(TYPE_CDC); // -->
+
+			return {
+				type: "CDC",
+				loc: this.getLocation(start, this.scanner.tokenStart)
+			};
+		},
+		generate: function () {
+			this.chunk("-->");
+		}
+	};
+
+	// ---
+
+	const Syntax_CDO = {
+		name: "CDO",
+		structure: [],
+		parse: function () {
+			const start = this.scanner.tokenStart;
+
+			this.scanner.eat(TYPE_CDO); // <!--
+
+			return {
+				type: "CDO",
+				loc: this.getLocation(start, this.scanner.tokenStart)
+			};
+		},
+		generate: function () {
+			this.chunk("<!--");
+		}
+	};
+
+	// ---
+
+	// '.' ident
+	const ClassSelector = {
+		name: "ClassSelector",
+		structure: {
+			name: String
+		},
+		parse: function () {
+			this.scanner.eat(FULLSTOP);
+
+			return {
+				type: "ClassSelector",
+				loc: this.getLocation(this.scanner.tokenStart - 1, this.scanner.tokenEnd),
+				name: this.scanner.consume(IDENTIFIER)
+			};
+		},
+		generate: function (node) {
+			this.chunk(".");
+			this.chunk(node.name);
+		}
+	};
+
+	// ---
+
+	// + | > | ~ | /deep/
+	const Combinator = {
+		name: "Combinator",
+		structure: {
+			name: String
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+
+			switch (this.scanner.tokenType) {
+				case GREATERTHANSIGN:
+				case PLUSSIGN:
+				case TILDE:
+					this.scanner.next();
+					break;
+
+				case SOLIDUS:
+					this.scanner.next();
+					this.scanner.expectIdentifier("deep");
+					this.scanner.eat(SOLIDUS);
+					break;
+
+				default:
+					this.scanner.error("Combinator is expected");
+			}
+
+			return {
+				type: "Combinator",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				name: this.scanner.substrToCursor(start)
+			};
+		},
+		generate: function (node) {
+			this.chunk(node.name);
+		}
+	};
+
+	// ---
+
+	// '/*' .* '*/'
+	const Syntax_Comment = {
+		name: "Comment",
+		structure: {
+			value: String
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+			let end = this.scanner.tokenEnd;
+
+			if ((end - start + 2) >= 2 &&
+				this.scanner.source.charCodeAt(end - 2) === ASTERISK &&
+				this.scanner.source.charCodeAt(end - 1) === SOLIDUS) {
+				end -= 2;
+			}
+
+			this.scanner.next();
+
+			return {
+				type: "Comment",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				value: this.scanner.source.substring(start + 2, end)
+			};
+		},
+		generate: function (node) {
+			this.chunk("/*");
+			this.chunk(node.value);
+			this.chunk("*/");
+		}
+	};
+
+	// ---
+
+	const hasOwnProperty = Object.prototype.hasOwnProperty;
+	const keywords = Object.create(null);
+	const properties = Object.create(null);
+	const NAMES_HYPHENMINUS = 45; // "-".charCodeAt()
+
+	function isCustomProperty(str, offset) {
+		offset = offset || 0;
+
+		return str.length - offset >= 2 &&
+			str.charCodeAt(offset) === NAMES_HYPHENMINUS &&
+			str.charCodeAt(offset + 1) === NAMES_HYPHENMINUS;
+	}
+
+	function getVendorPrefix(str, offset) {
+		offset = offset || 0;
+
+		// verdor prefix should be at least 3 chars length
+		if (str.length - offset >= 3) {
+			// vendor prefix starts with hyper minus following non-hyper minus
+			if (str.charCodeAt(offset) === NAMES_HYPHENMINUS &&
+				str.charCodeAt(offset + 1) !== NAMES_HYPHENMINUS) {
+				// vendor prefix should contain a hyper minus at the ending
+				const secondDashIndex = str.indexOf("-", offset + 2);
+
+				if (secondDashIndex !== -1) {
+					return str.substring(offset, secondDashIndex + 1);
+				}
+			}
+		}
+
+		return "";
+	}
+
+	function getKeywordDescriptor(keyword) {
+		if (hasOwnProperty.call(keywords, keyword)) {
+			return keywords[keyword];
+		}
+
+		const name = keyword.toLowerCase();
+
+		if (hasOwnProperty.call(keywords, name)) {
+			return keywords[keyword] = keywords[name];
+		}
+
+		const custom = names.isCustomProperty(name, 0);
+		const vendor = !custom ? getVendorPrefix(name, 0) : "";
+
+		return keywords[keyword] = Object.freeze({
+			basename: name.substr(vendor.length),
+			name: name,
+			vendor: vendor,
+			prefix: vendor,
+			custom: custom
+		});
+	}
+
+	function getPropertyDescriptor(property) {
+		if (hasOwnProperty.call(properties, property)) {
+			return properties[property];
+		}
+
+		let name = property;
+		let hack = property[0];
+
+		if (hack === "/") {
+			hack = property[1] === "/" ? "//" : "/";
+		} else if (hack !== "_" &&
+			hack !== "*" &&
+			hack !== "$" &&
+			hack !== "#" &&
+			hack !== "+") {
+			hack = "";
+		}
+
+		const custom = isCustomProperty(name, hack.length);
+
+		// re-use result when possible (the same as for lower case)
+		if (!custom) {
+			name = name.toLowerCase();
+			if (hasOwnProperty.call(properties, name)) {
+				return properties[property] = properties[name];
+			}
+		}
+
+		const vendor = !custom ? getVendorPrefix(name, hack.length) : "";
+		const prefix = name.substr(0, hack.length + vendor.length);
+
+		return properties[property] = Object.freeze({
+			basename: name.substr(prefix.length),
+			name: name.substr(hack.length),
+			hack: hack,
+			vendor: vendor,
+			prefix: prefix,
+			custom: custom
+		});
+	}
+
+	const names = {
+		keyword: getKeywordDescriptor,
+		property: getPropertyDescriptor,
+		isCustomProperty: isCustomProperty,
+		vendorPrefix: getVendorPrefix
+	};
+
+	// ---
+
+	function consumeValueRaw(startToken) {
+		return this.Raw(startToken, EXCLAMATIONMARK, SEMICOLON, false, true);
+	}
+
+	function consumeCustomPropertyRaw(startToken) {
+		return this.Raw(startToken, EXCLAMATIONMARK, SEMICOLON, false, false);
+	}
+
+	function consumeValue() {
+		const startValueToken = this.scanner.currentToken;
+		const value = this.Value();
+
+		if (value.type !== "Raw" &&
+			this.scanner.eof === false &&
+			this.scanner.tokenType !== SEMICOLON &&
+			this.scanner.tokenType !== EXCLAMATIONMARK &&
+			this.scanner.isBalanceEdge(startValueToken) === false) {
+			this.scanner.error();
+		}
+
+		return value;
+	}
+
+	const Declaration = {
+		name: "Declaration",
+		structure: {
+			important: [Boolean, String],
+			property: String,
+			value: ["Value", "Raw"]
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+			const startToken = this.scanner.currentToken;
+			const property = readProperty.call(this);
+			const customProperty = names.isCustomProperty(property);
+			const parseValue = customProperty ? this.parseCustomProperty : this.parseValue;
+			const consumeRaw = customProperty ? consumeCustomPropertyRaw : consumeValueRaw;
+			let important = false;
+			let value;
+
+			this.scanner.skipSC();
+			this.scanner.eat(COLON);
+
+			if (!customProperty) {
+				this.scanner.skipSC();
+			}
+
+			if (parseValue) {
+				value = this.parseWithFallback(consumeValue, consumeRaw);
+			} else {
+				value = consumeRaw.call(this, this.scanner.currentToken);
+			}
+
+			if (this.scanner.tokenType === EXCLAMATIONMARK) {
+				important = getImportant(this.scanner);
+				this.scanner.skipSC();
+			}
+
+			// Do not include semicolon to range per spec
+			// https://drafts.csswg.org/css-syntax/#declaration-diagram
+
+			if (this.scanner.eof === false &&
+				this.scanner.tokenType !== SEMICOLON &&
+				this.scanner.isBalanceEdge(startToken) === false) {
+				this.scanner.error();
+			}
+
+			return {
+				type: "Declaration",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				important: important,
+				property: property,
+				value: value
+			};
+		},
+		generate: function (node) {
+			this.chunk(node.property);
+			this.chunk(":");
+			this.node(node.value);
+
+			if (node.important) {
+				this.chunk(node.important === true ? "!important" : "!" + node.important);
+			}
+		},
+		walkContext: "declaration"
+	};
+
+	function readProperty() {
+		const start = this.scanner.tokenStart;
+		let prefix = 0;
+
+		// hacks
+		switch (this.scanner.tokenType) {
+			case ASTERISK:
+			case DOLLARSIGN:
+			case PLUSSIGN:
+			case NUMBERSIGN:
+				prefix = 1;
+				break;
+
+			// TODO: not sure we should support this hack
+			case SOLIDUS:
+				prefix = this.scanner.lookupType(1) === SOLIDUS ? 2 : 1;
+				break;
+		}
+
+		if (this.scanner.lookupType(prefix) === HYPHENMINUS) {
+			prefix++;
+		}
+
+		if (prefix) {
+			this.scanner.skip(prefix);
+		}
+
+		this.scanner.eat(IDENTIFIER);
+
+		return this.scanner.substrToCursor(start);
+	}
+
+	// ! ws* important
+	function getImportant(scanner) {
+		scanner.eat(EXCLAMATIONMARK);
+		scanner.skipSC();
+
+		const important = scanner.consume(IDENTIFIER);
+
+		// store original value in case it differ from `important`
+		// for better original source restoring and hacks like `!ie` support
+		return important === "important" ? true : important;
+	}
+
+	// ---
+
+	function DeclarationList_consumeRaw(startToken) {
+		return this.Raw(startToken, 0, SEMICOLON, true, true);
+	}
+
+	const DeclarationList = {
+		name: "DeclarationList",
+		structure: {
+			children: [[
+				"Declaration"
+			]]
+		},
+		parse: function () {
+			const children = this.createList();
+
+			while (!this.scanner.eof) {
+				switch (this.scanner.tokenType) {
+					case WHITESPACE:
+					case COMMENT:
+					case SEMICOLON:
+						this.scanner.next();
+						break;
+
+					default:
+						children.push(this.parseWithFallback(this.Declaration, DeclarationList_consumeRaw));
+				}
+			}
+
+			return {
+				type: "DeclarationList",
+				loc: this.getLocationFromList(children),
+				children: children
+			};
+		},
+		generate: function (node) {
+			this.children(node, function (prev) {
+				if (prev.type === "Declaration") {
+					this.chunk(";");
+				}
+			});
+		}
+	};
+
+	// ---
+
+	// special reader for units to avoid adjoined IE hacks (i.e. '1px\9')
+	function readUnit(scanner) {
+		const unit = scanner.getTokenValue();
+		const backSlashPos = unit.indexOf("\\");
+
+		if (backSlashPos > 0) {
+			// patch token offset
+			scanner.tokenStart += backSlashPos;
+
+			// return part before backslash
+			return unit.substring(0, backSlashPos);
+		}
+
+		// no backslash in unit name
+		scanner.next();
+
+		return unit;
+	}
+
+	// number ident
+	const Dimension = {
+		name: "Dimension",
+		structure: {
+			value: String,
+			unit: String
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+			const value = this.scanner.consume(NUMBER);
+			const unit = readUnit(this.scanner);
+
+			return {
+				type: "Dimension",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				value: value,
+				unit: unit
+			};
+		},
+		generate: function (node) {
+			this.chunk(node.value);
+			this.chunk(node.unit);
+		}
+	};
+
+	// ---
+
+	// <function-token> <sequence> ')'
+	const Syntax_Function = {
+		name: "Function",
+		structure: {
+			name: String,
+			children: [[]]
+		},
+		parse: function (readSequence, recognizer) {
+			const start = this.scanner.tokenStart;
+			const name = this.scanner.consumeFunctionName();
+			const nameLowerCase = name.toLowerCase();
+			let children;
+
+			children = recognizer.hasOwnProperty(nameLowerCase)
+				? recognizer[nameLowerCase].call(this, recognizer)
+				: readSequence.call(this, recognizer);
+
+			if (!this.scanner.eof) {
+				this.scanner.eat(RIGHTPARENTHESIS);
+			}
+
+			return {
+				type: "Function",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				name: name,
+				children: children
+			};
+		},
+		generate: function (node) {
+			this.chunk(node.name);
+			this.chunk("(");
+			this.children(node);
+			this.chunk(")");
+		},
+		walkContext: "function"
+	};
+
+	// ---
+
+	function consumeHexSequence(scanner, required) {
+		if (!isHex(scanner.source.charCodeAt(scanner.tokenStart))) {
+			if (required) {
+				scanner.error("Unexpected input", scanner.tokenStart);
+			} else {
+				return;
+			}
+		}
+
+		for (let pos = scanner.tokenStart + 1; pos < scanner.tokenEnd; pos++) {
+			const code = scanner.source.charCodeAt(pos);
+
+			// break on non-hex char
+			if (!isHex(code)) {
+				// break token, exclude symbol
+				scanner.tokenStart = pos;
+				return;
+			}
+		}
+
+		// token is full hex sequence, go to next token
+		scanner.next();
+	}
+
+	// # ident
+	const HexColor = {
+		name: "HexColor",
+		structure: {
+			value: String
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+
+			this.scanner.eat(NUMBERSIGN);
+
+			switch (this.scanner.tokenType) {
+				case NUMBER:
+					consumeHexSequence(this.scanner, true);
+
+					// if token is identifier then number consists of hex only,
+					// try to add identifier to result
+					if (this.scanner.tokenType === IDENTIFIER) {
+						consumeHexSequence(this.scanner, false);
+					}
+
+					break;
+
+				case IDENTIFIER:
+					consumeHexSequence(this.scanner, true);
+					break;
+
+				default:
+					this.scanner.error("Number or identifier is expected");
+			}
+
+			return {
+				type: "HexColor",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				value: this.scanner.substrToCursor(start + 1) // skip #
+			};
+		},
+		generate: function (node) {
+			this.chunk("#");
+			this.chunk(node.value);
+		}
+	};
+
+	// ---
+
+	const Identifier = {
+		name: "Identifier",
+		structure: {
+			name: String
+		},
+		parse: function () {
+			return {
+				type: "Identifier",
+				loc: this.getLocation(this.scanner.tokenStart, this.scanner.tokenEnd),
+				name: this.scanner.consume(IDENTIFIER)
+			};
+		},
+		generate: function (node) {
+			this.chunk(node.name);
+		}
+	};
+
+	// ---
+
+	// '#' ident
+	const IdSelector = {
+		name: "IdSelector",
+		structure: {
+			name: String
+		},
+		parse: function () {
+			this.scanner.eat(NUMBERSIGN);
+
+			return {
+				type: "IdSelector",
+				loc: this.getLocation(this.scanner.tokenStart - 1, this.scanner.tokenEnd),
+				name: this.scanner.consume(IDENTIFIER)
+			};
+		},
+		generate: function (node) {
+			this.chunk("#");
+			this.chunk(node.name);
+		}
+	};
+
+	// ---
+
+	const MediaFeature = {
+		name: "MediaFeature",
+		structure: {
+			name: String,
+			value: ["Identifier", "Number", "Dimension", "Ratio", null]
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+			let name;
+			let value = null;
+
+			this.scanner.eat(LEFTPARENTHESIS);
+			this.scanner.skipSC();
+
+			name = this.scanner.consume(IDENTIFIER);
+			this.scanner.skipSC();
+
+			if (this.scanner.tokenType !== RIGHTPARENTHESIS) {
+				this.scanner.eat(COLON);
+				this.scanner.skipSC();
+
+				switch (this.scanner.tokenType) {
+					case NUMBER:
+						if (this.scanner.lookupType(1) === IDENTIFIER) {
+							value = this.Dimension();
+						} else if (this.scanner.lookupNonWSType(1) === SOLIDUS) {
+							value = this.Ratio();
+						} else {
+							value = this.Number();
+						}
+
+						break;
+
+					case IDENTIFIER:
+						value = this.Identifier();
+
+						break;
+
+					default:
+						this.scanner.error("Number, dimension, ratio or identifier is expected");
+				}
+
+				this.scanner.skipSC();
+			}
+
+			this.scanner.eat(RIGHTPARENTHESIS);
+
+			return {
+				type: "MediaFeature",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				name: name,
+				value: value
+			};
+		},
+		generate: function (node) {
+			this.chunk("(");
+			this.chunk(node.name);
+			if (node.value !== null) {
+				this.chunk(":");
+				this.node(node.value);
+			}
+			this.chunk(")");
+		}
+	};
+
+	// ---
+
+	const MediaQuery = {
+		name: "MediaQuery",
+		structure: {
+			children: [[
+				"Identifier",
+				"MediaFeature",
+				"WhiteSpace"
+			]]
+		},
+		parse: function () {
+			this.scanner.skipSC();
+
+			const children = this.createList();
+			let child = null;
+			let space = null;
+
+			scan:
+			while (!this.scanner.eof) {
+				switch (this.scanner.tokenType) {
+					case COMMENT:
+						this.scanner.next();
+						continue;
+
+					case WHITESPACE:
+						space = this.WhiteSpace();
+						continue;
+
+					case IDENTIFIER:
+						child = this.Identifier();
+						break;
+
+					case LEFTPARENTHESIS:
+						child = this.MediaFeature();
+						break;
+
+					default:
+						break scan;
+				}
+
+				if (space !== null) {
+					children.push(space);
+					space = null;
+				}
+
+				children.push(child);
+			}
+
+			if (child === null) {
+				this.scanner.error("Identifier or parenthesis is expected");
+			}
+
+			return {
+				type: "MediaQuery",
+				loc: this.getLocationFromList(children),
+				children: children
+			};
+		},
+		generate: function (node) {
+			this.children(node);
+		}
+	};
+
+	// ---
+
+	const MediaQueryList = {
+		name: "MediaQueryList",
+		structure: {
+			children: [[
+				"MediaQuery"
+			]]
+		},
+		parse: function (relative) {
+			const children = this.createList();
+
+			this.scanner.skipSC();
+
+			while (!this.scanner.eof) {
+				children.push(this.MediaQuery(relative));
+
+				if (this.scanner.tokenType !== COMMA) {
+					break;
+				}
+
+				this.scanner.next();
+			}
+
+			return {
+				type: "MediaQueryList",
+				loc: this.getLocationFromList(children),
+				children: children
+			};
+		},
+		generate: function (node) {
+			this.children(node, function () {
+				this.chunk(",");
+			});
+		}
+	};
+
+	// ---
+
+	// https://drafts.csswg.org/css-syntax-3/#the-anb-type
+	const Nth = {
+		name: "Nth",
+		structure: {
+			nth: ["AnPlusB", "Identifier"],
+			selector: ["SelectorList", null]
+		},
+		parse: function (allowOfClause) {
+			this.scanner.skipSC();
+
+			const start = this.scanner.tokenStart;
+			let end = start;
+			let selector = null;
+			let query;
+
+			if (this.scanner.lookupValue(0, "odd") || this.scanner.lookupValue(0, "even")) {
+				query = this.Identifier();
+			} else {
+				query = this.AnPlusB();
+			}
+
+			this.scanner.skipSC();
+
+			if (allowOfClause && this.scanner.lookupValue(0, "of")) {
+				this.scanner.next();
+
+				selector = this.SelectorList();
+
+				if (this.needPositions) {
+					end = this.getLastListNode(selector.children).loc.end.offset;
+				}
+			} else {
+				if (this.needPositions) {
+					end = query.loc.end.offset;
+				}
+			}
+
+			return {
+				type: "Nth",
+				loc: this.getLocation(start, end),
+				nth: query,
+				selector: selector
+			};
+		},
+		generate: function (node) {
+			this.node(node.nth);
+			if (node.selector !== null) {
+				this.chunk(" of ");
+				this.node(node.selector);
+			}
+		}
+	};
+
+	// ---
+
+	const Syntax_Number = {
+		name: "Number",
+		structure: {
+			value: String
+		},
+		parse: function () {
+			return {
+				type: "Number",
+				loc: this.getLocation(this.scanner.tokenStart, this.scanner.tokenEnd),
+				value: this.scanner.consume(NUMBER)
+			};
+		},
+		generate: function (node) {
+			this.chunk(node.value);
+		}
+	};
+
+	// ---
+
+	// '/' | '*' | ',' | ':' | '+' | '-'
+	const Operator = {
+		name: "Operator",
+		structure: {
+			value: String
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+
+			this.scanner.next();
+
+			return {
+				type: "Operator",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				value: this.scanner.substrToCursor(start)
+			};
+		},
+		generate: function (node) {
+			this.chunk(node.value);
+		}
+	};
+
+	// ---
+
+	const Parentheses = {
+		name: "Parentheses",
+		structure: {
+			children: [[]]
+		},
+		parse: function (readSequence, recognizer) {
+			const start = this.scanner.tokenStart;
+			let children = null;
+
+			this.scanner.eat(LEFTPARENTHESIS);
+
+			children = readSequence.call(this, recognizer);
+
+			if (!this.scanner.eof) {
+				this.scanner.eat(RIGHTPARENTHESIS);
+			}
+
+			return {
+				type: "Parentheses",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				children: children
+			};
+		},
+		generate: function (node) {
+			this.chunk("(");
+			this.children(node);
+			this.chunk(")");
+		}
+	};
+
+	// ---
+
+	const Percentage = {
+		name: "Percentage",
+		structure: {
+			value: String
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+			const number = this.scanner.consume(NUMBER);
+
+			this.scanner.eat(PERCENTSIGN);
+
+			return {
+				type: "Percentage",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				value: number
+			};
+		},
+		generate: function (node) {
+			this.chunk(node.value);
+			this.chunk("%");
+		}
+	};
+
+	// ---
+
+	// : ident [ "(" .. ")" ]?
+	const PseudoClassSelector = {
+		name: "PseudoClassSelector",
+		structure: {
+			name: String,
+			children: [["Raw"], null]
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+			let children = null;
+			let name;
+			let nameLowerCase;
+
+			this.scanner.eat(COLON);
+
+			if (this.scanner.tokenType === FUNCTION) {
+				name = this.scanner.consumeFunctionName();
+				nameLowerCase = name.toLowerCase();
+
+				if (this.pseudo.hasOwnProperty(nameLowerCase)) {
+					this.scanner.skipSC();
+					children = this.pseudo[nameLowerCase].call(this);
+					this.scanner.skipSC();
+				} else {
+					children = this.createList();
+					children.push(
+						this.Raw(this.scanner.currentToken, 0, 0, false, false)
+					);
+				}
+
+				this.scanner.eat(RIGHTPARENTHESIS);
+			} else {
+				name = this.scanner.consume(IDENTIFIER);
+			}
+
+			return {
+				type: "PseudoClassSelector",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				name: name,
+				children: children
+			};
+		},
+		generate: function (node) {
+			this.chunk(":");
+			this.chunk(node.name);
+
+			if (node.children !== null) {
+				this.chunk("(");
+				this.children(node);
+				this.chunk(")");
+			}
+		},
+		walkContext: "function"
+	};
+
+	// ---
+
+	// :: ident [ "(" .. ")" ]?
+	const PseudoElementSelector = {
+		name: "PseudoElementSelector",
+		structure: {
+			name: String,
+			children: [["Raw"], null]
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+			let children = null;
+			let name;
+			let nameLowerCase;
+
+			this.scanner.eat(COLON);
+			this.scanner.eat(COLON);
+
+			if (this.scanner.tokenType === FUNCTION) {
+				name = this.scanner.consumeFunctionName();
+				nameLowerCase = name.toLowerCase();
+
+				if (this.pseudo.hasOwnProperty(nameLowerCase)) {
+					this.scanner.skipSC();
+					children = this.pseudo[nameLowerCase].call(this);
+					this.scanner.skipSC();
+				} else {
+					children = this.createList();
+					children.push(
+						this.Raw(this.scanner.currentToken, 0, 0, false, false)
+					);
+				}
+
+				this.scanner.eat(RIGHTPARENTHESIS);
+			} else {
+				name = this.scanner.consume(IDENTIFIER);
+			}
+
+			return {
+				type: "PseudoElementSelector",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				name: name,
+				children: children
+			};
+		},
+		generate: function (node) {
+			this.chunk("::");
+			this.chunk(node.name);
+
+			if (node.children !== null) {
+				this.chunk("(");
+				this.children(node);
+				this.chunk(")");
+			}
+		},
+		walkContext: "function"
+	};
+
+	// ---
+
+	// Terms of <ratio> should to be a positive number (not zero or negative)
+	// (see https://drafts.csswg.org/mediaqueries-3/#values)
+	// However, -o-min-device-pixel-ratio takes fractional values as a ratio"s term
+	// and this is using by letious sites. Therefore we relax checking on parse
+	// to test a term is unsigned number without exponent part.
+	// Additional checks may to be applied on lexer validation.
+	function consumeNumber(scanner) {
+		const value = scanner.consumeNonWS(NUMBER);
+
+		for (let i = 0; i < value.length; i++) {
+			const code = value.charCodeAt(i);
+			if (!isNumber(code) && code !== FULLSTOP) {
+				scanner.error("Unsigned number is expected", scanner.tokenStart - value.length + i);
+			}
+		}
+
+		if (Number(value) === 0) {
+			scanner.error("Zero number is not allowed", scanner.tokenStart - value.length);
+		}
+
+		return value;
+	}
+
+	// <positive-integer> S* "/" S* <positive-integer>
+	const Ratio = {
+		name: "Ratio",
+		structure: {
+			left: String,
+			right: String
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+			const left = consumeNumber(this.scanner);
+			let right;
+
+			this.scanner.eatNonWS(SOLIDUS);
+			right = consumeNumber(this.scanner);
+
+			return {
+				type: "Ratio",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				left: left,
+				right: right
+			};
+		},
+		generate: function (node) {
+			this.chunk(node.left);
+			this.chunk("/");
+			this.chunk(node.right);
+		}
+	};
+
+	// ---
+
+	const Raw = {
+		name: "Raw",
+		structure: {
+			value: String
+		},
+		parse: function (startToken, endTokenType1, endTokenType2, includeTokenType2, excludeWhiteSpace) {
+			const startOffset = this.scanner.getTokenStart(startToken);
+			let endOffset;
+
+			this.scanner.skip(
+				this.scanner.getRawLength(
+					startToken,
+					endTokenType1,
+					endTokenType2,
+					includeTokenType2
+				)
+			);
+
+			if (excludeWhiteSpace && this.scanner.tokenStart > startOffset) {
+				endOffset = this.scanner.getOffsetExcludeWS();
+			} else {
+				endOffset = this.scanner.tokenStart;
+			}
+
+			return {
+				type: "Raw",
+				loc: this.getLocation(startOffset, endOffset),
+				value: this.scanner.source.substring(startOffset, endOffset)
+			};
+		},
+		generate: function (node) {
+			this.chunk(node.value);
+		}
+	};
+
+	// ---
+
+	function Rule_consumeRaw(startToken) {
+		return this.Raw(startToken, LEFTCURLYBRACKET, 0, false, true);
+	}
+
+	function consumePrelude() {
+		const prelude = this.SelectorList();
+
+		if (prelude.type !== "Raw" &&
+			this.scanner.eof === false &&
+			this.scanner.tokenType !== LEFTCURLYBRACKET) {
+			this.scanner.error();
+		}
+
+		return prelude;
+	}
+
+	const Rule = {
+		name: "Rule",
+		structure: {
+			prelude: ["SelectorList", "Raw"],
+			block: ["Block"]
+		},
+		parse: function () {
+			const startToken = this.scanner.currentToken;
+			const startOffset = this.scanner.tokenStart;
+			let prelude;
+			let block;
+
+			if (this.parseRulePrelude) {
+				prelude = this.parseWithFallback(consumePrelude, Rule_consumeRaw);
+			} else {
+				prelude = Rule_consumeRaw.call(this, startToken);
+			}
+
+			block = this.Block(true);
+
+			return {
+				type: "Rule",
+				loc: this.getLocation(startOffset, this.scanner.tokenStart),
+				prelude: prelude,
+				block: block
+			};
+		},
+		generate: function (node) {
+			this.node(node.prelude);
+			this.node(node.block);
+		},
+		walkContext: "rule"
+	};
+
+	// ---
+
+	const Syntax_Selector = {
+		name: "Selector",
+		structure: {
+			children: [[
+				"TypeSelector",
+				"IdSelector",
+				"ClassSelector",
+				"AttributeSelector",
+				"PseudoClassSelector",
+				"PseudoElementSelector",
+				"Combinator",
+				"WhiteSpace"
+			]]
+		},
+		parse: function () {
+			const children = this.readSequence(this.scope.Selector);
+
+			// nothing were consumed
+			if (this.getFirstListNode(children) === null) {
+				this.scanner.error("Selector is expected");
+			}
+
+			return {
+				type: "Selector",
+				loc: this.getLocationFromList(children),
+				children: children
+			};
+		},
+		generate: function (node) {
+			this.children(node);
+		}
+	};
+
+	// ---
+
+	const SelectorList = {
+		name: "SelectorList",
+		structure: {
+			children: [[
+				"Selector",
+				"Raw"
+			]]
+		},
+		parse: function () {
+			const children = this.createList();
+
+			while (!this.scanner.eof) {
+				children.push(this.Selector());
+
+				if (this.scanner.tokenType === COMMA) {
+					this.scanner.next();
+					continue;
+				}
+
+				break;
+			}
+
+			return {
+				type: "SelectorList",
+				loc: this.getLocationFromList(children),
+				children: children
+			};
+		},
+		generate: function (node) {
+			this.children(node, function () {
+				this.chunk(",");
+			});
+		},
+		walkContext: "selector"
+	};
+
+	// ---
+
+	const Syntax_String = {
+		name: "String",
+		structure: {
+			value: String
+		},
+		parse: function () {
+			return {
+				type: "String",
+				loc: this.getLocation(this.scanner.tokenStart, this.scanner.tokenEnd),
+				value: this.scanner.consume(STRING)
+			};
+		},
+		generate: function (node) {
+			this.chunk(node.value);
+		}
+	};
+
+	// ---
+
+	function StyleSheet_consumeRaw(startToken) {
+		return this.Raw(startToken, 0, 0, false, false);
+	}
+
+	const Syntax_StyleSheet = {
+		name: "StyleSheet",
+		structure: {
+			children: [[
+				"Comment",
+				"CDO",
+				"CDC",
+				"Atrule",
+				"Rule",
+				"Raw"
+			]]
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+			const children = this.createList();
+			let child;
+
+			while (!this.scanner.eof) {
+				switch (this.scanner.tokenType) {
+					case WHITESPACE:
+						this.scanner.next();
+						continue;
+
+					case COMMENT:
+						// ignore comments except exclamation comments (i.e. /*! .. */) on top level
+						if (this.scanner.source.charCodeAt(this.scanner.tokenStart + 2) !== EXCLAMATIONMARK) {
+							this.scanner.next();
+							continue;
+						}
+
+						child = this.Comment();
+						break;
+
+					case CDO: // <!--
+						child = this.CDO();
+						break;
+
+					case CDC: // -->
+						child = this.CDC();
+						break;
+
+					// CSS Syntax Module Level 3
+					// §2.2 Error handling
+					// At the "top level" of a stylesheet, an <at-keyword-token> starts an at-rule.
+					case ATKEYWORD:
+						child = this.parseWithFallback(this.Atrule, StyleSheet_consumeRaw);
+						break;
+
+					// Anything else starts a qualified rule ...
+					default:
+						child = this.parseWithFallback(this.Rule, StyleSheet_consumeRaw);
+				}
+
+				children.push(child);
+			}
+
+			return {
+				type: "StyleSheet",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				children: children
+			};
+		},
+		generate: function (node) {
+			this.children(node);
+		},
+		walkContext: "stylesheet"
+	};
+
+	// ---
+
+	function eatIdentifierOrAsterisk() {
+		if (this.scanner.tokenType !== IDENTIFIER &&
+			this.scanner.tokenType !== ASTERISK) {
+			this.scanner.error("Identifier or asterisk is expected");
+		}
+
+		this.scanner.next();
+	}
+
+	// ident
+	// ident|ident
+	// ident|*
+	// *
+	// *|ident
+	// *|*
+	// |ident
+	// |*
+	const TypeSelector = {
+		name: "TypeSelector",
+		structure: {
+			name: String
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+
+			if (this.scanner.tokenType === VERTICALLINE) {
+				this.scanner.next();
+				eatIdentifierOrAsterisk.call(this);
+			} else {
+				eatIdentifierOrAsterisk.call(this);
+
+				if (this.scanner.tokenType === VERTICALLINE) {
+					this.scanner.next();
+					eatIdentifierOrAsterisk.call(this);
+				}
+			}
+
+			return {
+				type: "TypeSelector",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				name: this.scanner.substrToCursor(start)
+			};
+		},
+		generate: function (node) {
+			this.chunk(node.name);
+		}
+	};
+
+	// ---	
+
+	function scanUnicodeNumber(scanner) {
+		for (let pos = scanner.tokenStart + 1; pos < scanner.tokenEnd; pos++) {
+			const code = scanner.source.charCodeAt(pos);
+
+			// break on fullstop or hyperminus/plussign after exponent
+			if (code === FULLSTOP || code === PLUSSIGN) {
+				// break token, exclude symbol
+				scanner.tokenStart = pos;
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	// https://drafts.csswg.org/css-syntax-3/#urange
+	function scanUnicodeRange(scanner) {
+		const hexStart = scanner.tokenStart + 1; // skip +
+		let hexLength = 0;
+
+		scan: {
+			if (scanner.tokenType === NUMBER) {
+				if (scanner.source.charCodeAt(scanner.tokenStart) !== FULLSTOP && scanUnicodeNumber(scanner)) {
+					scanner.next();
+				} else if (scanner.source.charCodeAt(scanner.tokenStart) !== HYPHENMINUS) {
+					break scan;
+				}
+			} else {
+				scanner.next(); // PLUSSIGN
+			}
+
+			if (scanner.tokenType === HYPHENMINUS) {
+				scanner.next();
+			}
+
+			if (scanner.tokenType === NUMBER) {
+				scanner.next();
+			}
+
+			if (scanner.tokenType === IDENTIFIER) {
+				scanner.next();
+			}
+
+			if (scanner.tokenStart === hexStart) {
+				scanner.error("Unexpected input", hexStart);
+			}
+		}
+
+		// validate for U+x{1,6} or U+x{1,6}-x{1,6}
+		// where x is [0-9a-fA-F]
+		let i;
+		let wasHyphenMinus = false;
+		for (i = hexStart; i < scanner.tokenStart; i++) {
+			const code = scanner.source.charCodeAt(i);
+
+			if (isHex(code) === false && (code !== HYPHENMINUS || wasHyphenMinus)) {
+				scanner.error("Unexpected input", i);
+			}
+
+			if (code === HYPHENMINUS) {
+				// hex sequence shouldn"t be an empty
+				if (hexLength === 0) {
+					scanner.error("Unexpected input", i);
+				}
+
+				wasHyphenMinus = true;
+				hexLength = 0;
+			} else {
+				hexLength++;
+
+				// too long hex sequence
+				if (hexLength > 6) {
+					scanner.error("Too long hex sequence", i);
+				}
+			}
+
+		}
+
+		// check we have a non-zero sequence
+		if (hexLength === 0) {
+			scanner.error("Unexpected input", i - 1);
+		}
+
+		// U+abc???
+		if (!wasHyphenMinus) {
+			// consume as many U+003F QUESTION MARK (?) code points as possible
+			for (; hexLength < 6 && !scanner.eof; scanner.next()) {
+				if (scanner.tokenType !== QUESTIONMARK) {
+					break;
+				}
+
+				hexLength++;
+			}
+		}
+	}
+
+	const UnicodeRange = {
+		name: "UnicodeRange",
+		structure: {
+			value: String
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+
+			this.scanner.next(); // U or u
+			scanUnicodeRange(this.scanner);
+
+			return {
+				type: "UnicodeRange",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				value: this.scanner.substrToCursor(start)
+			};
+		},
+		generate: function (node) {
+			this.chunk(node.value);
+		}
+	};
+
+	// ---
+
+	// url "(" S* (string | raw) S* ")"
+	const Url = {
+		name: "Url",
+		structure: {
+			value: ["String", "Raw"]
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+			let value;
+
+			this.scanner.eat(URL);
+			this.scanner.skipSC();
+
+			switch (this.scanner.tokenType) {
+				case STRING:
+					value = this.String();
+					break;
+
+				case RAW:
+					value = this.Raw(this.scanner.currentToken, 0, RAW, true, false);
+					break;
+
+				default:
+					this.scanner.error("String or Raw is expected");
+			}
+
+			this.scanner.skipSC();
+			this.scanner.eat(RIGHTPARENTHESIS);
+
+			return {
+				type: "Url",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				value: value
+			};
+		},
+		generate: function (node) {
+			this.chunk("url");
+			this.chunk("(");
+			this.node(node.value);
+			this.chunk(")");
+		}
+	};
+
+	// ---
+
+	const Syntax_Value = {
+		name: "Value",
+		structure: {
+			children: [[]]
+		},
+		parse: function () {
+			const start = this.scanner.tokenStart;
+			const children = this.readSequence(this.scope.Value);
+
+			return {
+				type: "Value",
+				loc: this.getLocation(start, this.scanner.tokenStart),
+				children: children
+			};
+		},
+		generate: function (node) {
+			this.children(node);
+		}
+	};
+
+	// ---
+
+	const WhiteSpace_SPACE = Object.freeze({
+		type: "WhiteSpace",
+		loc: null,
+		value: " "
+	});
+
+	const WhiteSpace = {
+		name: "WhiteSpace",
+		structure: {
+			value: String
+		},
+		parse: function () {
+			this.scanner.eat(WHITESPACE);
+			return WhiteSpace_SPACE;
+
+			// return {
+			//     type: "WhiteSpace",
+			//     loc: this.getLocation(this.scanner.tokenStart, this.scanner.tokenEnd),
+			//     value: this.scanner.consume(WHITESPACE)
+			// };
+		},
+		generate: function (node) {
+			this.chunk(node.value);
+		}
+	};
+
+	// ---
+
+	function processChildren(node, delimeter) {
+		const list = node.children;
+		let prev = null;
+
+		if (typeof delimeter !== "function") {
+			list.forEach(this.node, this);
+		} else {
+			list.forEach(function (node) {
+				if (prev !== null) {
+					delimeter.call(this, prev);
+				}
+
+				this.node(node);
+				prev = node;
+			}, this);
+		}
+	}
+
+	function createGenerator(config) {
+		function processNode(node) {
+			if (hasOwnProperty.call(types, node.type)) {
+				types[node.type].call(this, node);
+			} else {
+				throw new Error("Unknown node type: " + node.type);
+			}
+		}
+
+		const types = {};
+
+		if (config.node) {
+			for (const name in config.node) {
+				types[name] = config.node[name].generate;
+			}
+		}
+
+		return function (node, options) {
+			let buffer = "";
+			let handlers = {
+				children: processChildren,
+				node: processNode,
+				chunk: function (chunk) {
+					buffer += chunk;
+				},
+				result: function () {
+					return buffer;
+				}
+			};
+
+			if (options) {
+				if (typeof options.decorator === "function") {
+					handlers = options.decorator(handlers);
+				}
+			}
+
+			handlers.node(node);
+
+			return handlers.result();
+		};
+	}
+
+	// ---
+
+	const node = {
+		AnPlusB: AnPlusB,
+		Atrule: Atrule,
+		AtrulePrelude: Syntax_AtrulePrelude,
+		AttributeSelector: AttributeSelector,
+		Block: Block,
+		Brackets: Brackets,
+		CDC: Syntax_CDC,
+		CDO: Syntax_CDO,
+		ClassSelector: ClassSelector,
+		Combinator: Combinator,
+		Comment: Syntax_Comment,
+		Declaration: Declaration,
+		DeclarationList: DeclarationList,
+		Dimension: Dimension,
+		Function: Syntax_Function,
+		HexColor: HexColor,
+		Identifier: Identifier,
+		IdSelector: IdSelector,
+		MediaFeature: MediaFeature,
+		MediaQuery: MediaQuery,
+		MediaQueryList: MediaQueryList,
+		Nth: Nth,
+		Number: Syntax_Number,
+		Operator: Operator,
+		Parentheses: Parentheses,
+		Percentage: Percentage,
+		PseudoClassSelector: PseudoClassSelector,
+		PseudoElementSelector: PseudoElementSelector,
+		Ratio: Ratio,
+		Raw: Raw,
+		Rule: Rule,
+		Selector: Syntax_Selector,
+		SelectorList: SelectorList,
+		String: Syntax_String,
+		StyleSheet: Syntax_StyleSheet,
+		TypeSelector: TypeSelector,
+		UnicodeRange: UnicodeRange,
+		Url: Url,
+		Value: Syntax_Value,
+		WhiteSpace: WhiteSpace
+	};
+
+	// ---
+
+	const config = {
+		parseContext: {
+			default: "StyleSheet",
+			stylesheet: "StyleSheet",
+			atrule: "Atrule",
+			atrulePrelude: function (options) {
+				return this.AtrulePrelude(options.atrule ? String(options.atrule) : null);
+			},
+			mediaQueryList: "MediaQueryList",
+			mediaQuery: "MediaQuery",
+			rule: "Rule",
+			selectorList: "SelectorList",
+			selector: "Selector",
+			block: function () {
+				return this.Block(true);
+			},
+			declarationList: "DeclarationList",
+			declaration: "Declaration",
+			value: "Value"
+		},
+		scope: scope,
+		atrule: atrule,
+		pseudo: pseudo,
+		node: node
+	};
+
+	return {
+		parse: createParser(config),
+		generate: createGenerator(config)
+	};
+
+})();

+ 2 - 8
lib/single-file/doc-helper.js

@@ -27,7 +27,6 @@ this.docHelper = this.docHelper || (() => {
 	const WIN_ID_ATTRIBUTE_NAME = "data-frame-tree-win-id";
 	const IMAGE_ATTRIBUTE_NAME = "data-single-file-image";
 	const INPUT_VALUE_ATTRIBUTE_NAME = "data-single-file-value";
-	const SHEET_ATTRIBUTE_NAME = "data-single-file-sheet";
 	const IGNORED_REMOVED_TAG_NAMES = ["NOSCRIPT", "DISABLED-NOSCRIPT", "META", "LINK", "STYLE", "TITLE", "TEMPLATE", "SOURCE", "OBJECT"];
 	const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
 	const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
@@ -45,7 +44,7 @@ this.docHelper = this.docHelper || (() => {
 		removedContentAttributeName,
 		imagesAttributeName,
 		inputValueAttributeName,
-		sheetAttributeName
+		removeQuotes
 	};
 
 	function preProcessDoc(doc, win, options) {
@@ -107,7 +106,7 @@ this.docHelper = this.docHelper || (() => {
 		styles.forEach(style => {
 			const fontFamilyNames = style.fontFamily.split(",");
 			fontFamilyNames.forEach(fontFamilyName => {
-				style.fontFamily = removeQuotes(fontFamilyName);
+				style.fontFamily = removeQuotes(fontFamilyName).toLowerCase().trim();
 				usedFonts.add(getFontKey(style));
 			});
 		});
@@ -209,10 +208,6 @@ this.docHelper = this.docHelper || (() => {
 		return INPUT_VALUE_ATTRIBUTE_NAME + (sessionId || "");
 	}
 
-	function sheetAttributeName(sessionId) {
-		return SHEET_ATTRIBUTE_NAME + (sessionId || "");
-	}
-
 	function getCanvasData(doc, win) {
 		if (doc) {
 			const canvasData = [];
@@ -379,7 +374,6 @@ this.docHelper = this.docHelper || (() => {
 	}
 
 	function removeQuotes(string) {
-		string = string.toLowerCase().trim();
 		if (string.match(REGEXP_SIMPLE_QUOTES_STRING)) {
 			string = string.replace(REGEXP_SIMPLE_QUOTES_STRING, "$1");
 		} else {

+ 1 - 13
lib/single-file/frame-tree.js

@@ -28,8 +28,6 @@ this.frameTree = this.frameTree || (() => {
 	const INIT_RESPONSE_MESSAGE = "initResponse";
 	const TARGET_ORIGIN = "*";
 	const TIMEOUT_INIT_REQUEST_MESSAGE = 500;
-	const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
-	const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
 	const PREFIX_VALID_FRAME_URL = /^https?:\/\//;
 	const TOP_WINDOW_ID = "0";
 	const WINDOW_ID_SEPARATOR = ".";
@@ -222,7 +220,7 @@ this.frameTree = this.frameTree || (() => {
 				if (charSetValue) {
 					const matchCharSet = charSetValue.match(/^charset=(.*)/);
 					if (matchCharSet) {
-						charSet = removeQuotes(matchCharSet[1]);
+						charSet = docHelper.removeQuotes(matchCharSet[1]);
 					}
 				}
 			}
@@ -246,16 +244,6 @@ this.frameTree = this.frameTree || (() => {
 		}
 	}
 
-	function removeQuotes(string) {
-		string = string.toLowerCase().trim();
-		if (string.match(REGEXP_SIMPLE_QUOTES_STRING)) {
-			string = string.replace(REGEXP_SIMPLE_QUOTES_STRING, "$1");
-		} else {
-			string = string.replace(REGEXP_DOUBLE_QUOTES_STRING, "$1");
-		}
-		return string.trim();
-	}
-
 	function getFrameData(document, window, windowId, options) {
 		const docData = docHelper.preProcessDoc(document, window, options);
 		const content = docHelper.serialize(document);

+ 3 - 3
lib/single-file/html-alt-images.js → lib/single-file/html-images-minifier.js

@@ -18,9 +18,9 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/* global parseSrcset */
+/* global srcsetParser */
 
-this.altImages = this.altImages || (() => {
+this.imagesMinifier = this.imagesMinifier || (() => {
 
 	const EMPTY_IMAGE = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
 
@@ -100,7 +100,7 @@ this.altImages = this.altImages || (() => {
 
 	function getSourceSrc(sourceSrcSet) {
 		if (sourceSrcSet) {
-			const srcset = parseSrcset.process(sourceSrcSet);
+			const srcset = srcsetParser.process(sourceSrcSet);
 			return (srcset.find(srcset => srcset.url)).url;
 		}
 	}

+ 1 - 1
lib/single-file/html-minifier.js

@@ -29,7 +29,7 @@
 
 /* global Node, NodeFilter */
 
-this.htmlmini = this.htmlmini || (() => {
+this.htmlMinifier = this.htmlMinifier || (() => {
 
 	// Source: https://github.com/kangax/html-minifier/issues/63
 	const booleanAttributes = [

+ 1 - 1
lib/single-file/html-srcset-parser.js

@@ -14,7 +14,7 @@
  * (except for comments in parens).
  */
 
-this.parseSrcset = this.parseSrcset || (() => {
+this.srcsetParser = this.srcsetParser || (() => {
 
 	return {
 		process

+ 22 - 36
lib/single-file/single-file-browser.js

@@ -18,7 +18,7 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/* global SingleFileCore, DOMParser, TextDecoder, Blob, fetch, base64, superFetch, parseSrcset, uglifycss, htmlmini, cssMinifier, fontsMinifier, serializer, docHelper, mediasMinifier, TextEncoder, crypto, RulesMatcher, altImages, FontFace */
+/* global SingleFileCore, DOMParser, TextDecoder, Blob, fetch, base64, superFetch, srcsetParser, cssMinifier, htmlMinifier, cssRulesMinifier, fontsMinifier, serializer, docHelper, mediasMinifier, TextEncoder, crypto, matchedRules, imagesMinifier, FontFace, cssTree */
 
 this.SingleFile = this.SingleFile || (() => {
 
@@ -84,7 +84,7 @@ this.SingleFile = this.SingleFile || (() => {
 				if (charSetValue) {
 					const matchCharSet = charSetValue.match(/^charset=(.*)/);
 					if (matchCharSet) {
-						charSet = removeQuotes(matchCharSet[1]);
+						charSet = docHelper.removeQuotes(matchCharSet[1]);
 					}
 				}
 			}
@@ -142,19 +142,6 @@ this.SingleFile = this.SingleFile || (() => {
 		}
 	}
 
-	const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
-	const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
-
-	function removeQuotes(string) {
-		string = string.toLowerCase().trim();
-		if (string.match(REGEXP_SIMPLE_QUOTES_STRING)) {
-			string = string.replace(REGEXP_SIMPLE_QUOTES_STRING, "$1");
-		} else {
-			string = string.replace(REGEXP_DOUBLE_QUOTES_STRING, "$1");
-		}
-		return string.trim();
-	}
-
 	// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
 	function hex(buffer) {
 		var hexCodes = [];
@@ -222,44 +209,43 @@ this.SingleFile = this.SingleFile || (() => {
 		}
 
 		static minifyHTML(doc, options) {
-			return htmlmini.process(doc, options);
+			return htmlMinifier.process(doc, options);
 		}
 
 		static postMinifyHTML(doc) {
-			return htmlmini.postProcess(doc);
+			return htmlMinifier.postProcess(doc);
 		}
 
-		static minifyCSS(doc, mediaAllInfo) {
-			return cssMinifier.process(doc, mediaAllInfo);
+		static minifyCSSRules(stylesheets, styles, mediaAllInfo) {
+			return cssRulesMinifier.process(stylesheets, styles, mediaAllInfo);
 		}
 
-		static removeUnusedFonts(doc, options) {
-			return fontsMinifier.removeUnusedFonts(doc, options);
+		static removeUnusedFonts(doc, stylesheets, styles, options) {
+			return fontsMinifier.removeUnusedFonts(doc, stylesheets, styles, options);
 		}
 
-		static removeAlternativeFonts(doc) {
-			return fontsMinifier.removeAlternativeFonts(doc);
+		static removeAlternativeFonts(doc, stylesheets) {
+			return fontsMinifier.removeAlternativeFonts(doc, stylesheets);
 		}
 
-		static getMediaAllInfo(doc) {
-			const rulesMatcher = RulesMatcher.create(doc);
-			return rulesMatcher.getMediaAllInfo();
+		static getMediaAllInfo(doc, docStyle) {
+			return matchedRules.getMediaAllInfo(doc, docStyle);
 		}
 
 		static compressCSS(content, options) {
-			return uglifycss.processString(content, options);
+			return cssMinifier.processString(content, options);
 		}
 
-		static minifyMedias(doc) {
-			return mediasMinifier.process(doc);
+		static minifyMedias(stylesheets) {
+			return mediasMinifier.process(stylesheets);
 		}
 
 		static removeAlternativeImages(doc, options) {
-			return altImages.process(doc, options);
+			return imagesMinifier.process(doc, options);
 		}
 
 		static parseSrcset(srcset) {
-			return parseSrcset.process(srcset);
+			return srcsetParser.process(srcset);
 		}
 
 		static preProcessDoc(doc, win, options) {
@@ -274,6 +260,10 @@ this.SingleFile = this.SingleFile || (() => {
 			return serializer.process(doc, compressHTML);
 		}
 
+		static removeQuotes(string) {
+			return docHelper.removeQuotes(string);
+		}
+
 		static windowIdAttributeName(sessionId) {
 			return docHelper.windowIdAttributeName(sessionId);
 		}
@@ -293,16 +283,12 @@ this.SingleFile = this.SingleFile || (() => {
 		static inputValueAttributeName(sessionId) {
 			return docHelper.inputValueAttributeName(sessionId);
 		}
-
-		static sheetAttributeName(sessionId) {
-			return docHelper.sheetAttributeName(sessionId);
-		}
 	}
 
 	function log(...args) {
 		console.log("S-File <browser>", ...args); // eslint-disable-line no-console
 	}
 
-	return { getClass: () => SingleFileCore.getClass(Download, DOM, URL) };
+	return { getClass: () => SingleFileCore.getClass(Download, DOM, URL, cssTree) };
 
 })();

+ 183 - 147
lib/single-file/single-file-core.js

@@ -21,18 +21,16 @@
  *   Source.
  */
 
-/* global CSSRule */
-
 this.SingleFileCore = this.SingleFileCore || (() => {
 
 	const SELECTED_CONTENT_ATTRIBUTE_NAME = "data-single-file-selected-content";
 	const SELECTED_CONTENT_ROOT_ATTRIBUTE_NAME = "data-single-file-selected-content-root";
 	const DEBUG = false;
 
-	let Download, DOM, URL, sessionId = 0;
+	let Download, DOM, URL, cssTree, sessionId = 0;
 
 	function getClass(...args) {
-		[Download, DOM, URL] = args;
+		[Download, DOM, URL, cssTree] = args;
 		return SingleFileClass;
 	}
 
@@ -104,7 +102,6 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 		],
 		parallel: [
 			{ action: "resolveStylesheetURLs" },
-			{ action: "resolveLinkedStylesheetURLs" },
 			{ option: "!removeFrames", action: "resolveFrameURLs" },
 			{ option: "!removeImports", action: "resolveHtmlImportURLs" }
 		]
@@ -123,8 +120,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 	}, {
 		sequential: [
 			{ option: "removeAlternativeImages", action: "removeAlternativeImages" },
-			{ option: "removeAlternativeFonts", action: "removeAlternativeFonts" },
-			{ option: "compressCSS", action: "compressCSS" }
+			{ option: "removeAlternativeFonts", action: "removeAlternativeFonts" }
 		],
 		parallel: [
 			{ option: "!removeFrames", action: "processFrames" },
@@ -132,6 +128,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 		]
 	}, {
 		sequential: [
+			{ action: "replaceStylesheets" },
+			{ action: "replaceStyleAttributes" },
+			{ action: "insertVariables" },
 			{ option: "compressHTML", action: "compressHTML" }
 		]
 	}];
@@ -295,6 +294,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			this.stats = new Stats(options);
 			this.baseURI = DomUtil.normalizeURL(options.baseURI || options.url);
 			this.batchRequest = batchRequest;
+			this.stylesheets = new Map();
+			this.styles = new Map();
+			this.cssVariables = new Map();
 		}
 
 		initialize() {
@@ -436,7 +438,6 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			if (metaCharset) {
 				this.doc.head.insertBefore(metaCharset, this.doc.head.firstChild);
 			}
-			this.doc.querySelectorAll("style[data-single-file-sheet]").forEach(element => element.removeAttribute("data-single-file-sheet"));
 			this.doc.querySelectorAll("base").forEach(element => element.remove());
 			if (this.doc.head.querySelectorAll("*").length == 1 && metaCharset && this.doc.body.childNodes.length == 0) {
 				this.doc.head.querySelector("meta[charset]").remove();
@@ -559,19 +560,19 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 
 		removeUnusedStyles() {
 			if (!this.mediaAllInfo) {
-				this.mediaAllInfo = DOM.getMediaAllInfo(this.doc);
+				this.mediaAllInfo = DOM.getMediaAllInfo(this.doc, { stylesheets: this.stylesheets, styles: this.styles });
 			}
-			const stats = DOM.minifyCSS(this.doc, this.mediaAllInfo);
+			const stats = DOM.minifyCSSRules(this.stylesheets, this.styles, this.mediaAllInfo);
 			this.stats.set("processed", "CSS rules", stats.processed);
 			this.stats.set("discarded", "CSS rules", stats.discarded);
 		}
 
 		removeUnusedFonts() {
-			DOM.removeUnusedFonts(this.doc, this.options);
+			DOM.removeUnusedFonts(this.doc, this.stylesheets, this.styles, this.options);
 		}
 
 		removeAlternativeFonts() {
-			DOM.removeAlternativeFonts(this.doc);
+			DOM.removeAlternativeFonts(this.doc, this.stylesheets);
 		}
 
 		removeAlternativeImages() {
@@ -597,22 +598,11 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 		}
 
 		removeAlternativeMedias() {
-			const stats = DOM.minifyMedias(this.doc);
+			const stats = DOM.minifyMedias(this.stylesheets);
 			this.stats.set("processed", "medias", stats.processed);
 			this.stats.set("discarded", "medias", stats.discarded);
 		}
 
-		compressCSS() {
-			this.doc.querySelectorAll("style").forEach(styleElement => {
-				if (styleElement) {
-					styleElement.textContent = DOM.compressCSS(styleElement.textContent);
-				}
-			});
-			this.doc.querySelectorAll("[style]").forEach(element => {
-				element.setAttribute("style", DOM.compressCSS(element.getAttribute("style")));
-			});
-		}
-
 		replaceCanvasElements() {
 			if (this.options.canvasData) {
 				this.doc.querySelectorAll("canvas").forEach((canvasElement, indexCanvasElement) => {
@@ -682,21 +672,21 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 
 		async processPageResources() {
 			const resourcePromises = [
-				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("link[href][rel*=\"icon\"]"), "href", "data:", this.baseURI, this.options, this.batchRequest, false, true),
-				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("object[type=\"image/svg+xml\"], object[type=\"image/svg-xml\"]"), "data", PREFIX_DATA_URI_IMAGE_SVG, this.baseURI, this.options, this.batchRequest),
-				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("img[src], input[src][type=image]"), "src", PREFIX_DATA_URI_IMAGE, this.baseURI, this.options, this.batchRequest, true),
-				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("embed[src*=\".svg\"]"), "src", PREFIX_DATA_URI_IMAGE_SVG, this.baseURI, this.options, this.batchRequest),
-				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("video[poster]"), "poster", PREFIX_DATA_URI_IMAGE, this.baseURI, this.options, this.batchRequest),
-				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("*[background]"), "background", PREFIX_DATA_URI_IMAGE, this.baseURI, this.options, this.batchRequest),
-				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("image"), "xlink:href", PREFIX_DATA_URI_IMAGE, this.baseURI, this.options, this.batchRequest),
+				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("link[href][rel*=\"icon\"]"), "href", "data:", this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest, false, true),
+				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("object[type=\"image/svg+xml\"], object[type=\"image/svg-xml\"]"), "data", PREFIX_DATA_URI_IMAGE_SVG, this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest),
+				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("img[src], input[src][type=image]"), "src", PREFIX_DATA_URI_IMAGE, this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest, true),
+				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("embed[src*=\".svg\"]"), "src", PREFIX_DATA_URI_IMAGE_SVG, this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest),
+				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("video[poster]"), "poster", PREFIX_DATA_URI_IMAGE, this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest),
+				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("*[background]"), "background", PREFIX_DATA_URI_IMAGE, this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest),
+				DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("image"), "xlink:href", PREFIX_DATA_URI_IMAGE, this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest),
 				DomProcessorHelper.processXLinks(this.doc.querySelectorAll("use"), this.baseURI, this.options, this.batchRequest),
 				DomProcessorHelper.processSrcset(this.doc.querySelectorAll("img[srcset], source[srcset]"), "srcset", PREFIX_DATA_URI_IMAGE, this.baseURI, this.options, this.batchRequest)
 			];
 			if (!this.options.removeAudioSrc) {
-				resourcePromises.push(DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("audio[src], audio > source[src]"), "src", PREFIX_DATA_URI_AUDIO, this.baseURI, this.options, this.batchRequest));
+				resourcePromises.push(DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("audio[src], audio > source[src]"), "src", PREFIX_DATA_URI_AUDIO, this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest));
 			}
 			if (!this.options.removeVideoSrc) {
-				resourcePromises.push(DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("video[src], video > source[src]"), "src", PREFIX_DATA_URI_VIDEO, this.baseURI, this.options, this.batchRequest));
+				resourcePromises.push(DomProcessorHelper.processAttribute(this.doc, this.doc.querySelectorAll("video[src], video > source[src]"), "src", PREFIX_DATA_URI_VIDEO, this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest));
 			}
 			await Promise.all(resourcePromises);
 			if (this.options.removeAlternativeImages) {
@@ -724,23 +714,84 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 		}
 
 		async resolveStylesheetURLs() {
-			await Promise.all(Array.from(this.doc.querySelectorAll("style"))
-				.map(async styleElement => {
-					const options = { url: this.options.url, maxResourceSize: this.options.maxResourceSize, maxResourceSizeEnabled: this.options.maxResourceSizeEnabled };
-					const textContent = await DomProcessorHelper.resolveImportURLs(styleElement.textContent, this.baseURI, options);
-					styleElement.textContent = textContent;
+			await Promise.all(Array.from(this.doc.querySelectorAll("style, link[rel*=stylesheet]"))
+				.map(async element => {
+					this.stylesheets.set(element, { media: element.media });
+					const options = { maxResourceSize: this.options.maxResourceSize, maxResourceSizeEnabled: this.options.maxResourceSizeEnabled, url: this.options.url, charSet: this.charSet };
+					const isLinkTag = element.tagName.toLowerCase() == "link";
+					if (isLinkTag && element.rel.includes("alternate") && element.title) {
+						element.remove();
+					} else {
+						let stylesheetContent;
+						if (isLinkTag) {
+							stylesheetContent = await DomProcessorHelper.resolveLinkStylesheetURLs(element.href, this.baseURI, options);
+						} else {
+							stylesheetContent = await DomProcessorHelper.resolveImportURLs(element.textContent, this.baseURI, options);
+						}
+						const stylesheet = cssTree.parse(stylesheetContent);
+						this.stylesheets.get(element).stylesheet = stylesheet;
+					}
 				}));
 		}
 
 		async processStylesheets() {
-			await Promise.all(Array.from(this.doc.querySelectorAll("style")).map(async styleElement => {
-				this.stats.add("processed", "stylesheets", 1);
-				if (styleElement.sheet) {
-					styleElement.textContent = await DomProcessorHelper.processStylesheet(this.doc, styleElement.textContent, styleElement.sheet.cssRules, this.baseURI, this.options, this.batchRequest);
+			await Promise.all(Array.from(this.stylesheets).map(async ([, stylesheetInfo]) => {
+				await DomProcessorHelper.processStylesheet(stylesheetInfo.stylesheet.children.toArray(), this.baseURI, this.options, this.cssVariables, this.batchRequest);
+			}));
+		}
+
+		replaceStylesheets() {
+			this.doc.querySelectorAll("style").forEach(styleElement => {
+				const stylesheetInfo = this.stylesheets.get(styleElement);
+				if (stylesheetInfo) {
+					let stylesheetContent = cssTree.generate(stylesheetInfo.stylesheet);
+					if (this.options.compressCSS) {
+						stylesheetContent = DOM.compressCSS(stylesheetContent);
+					}
+					styleElement.textContent = stylesheetContent;
+					if (stylesheetInfo.media) {
+						styleElement.media = stylesheetInfo.media;
+					}
 				} else {
 					styleElement.remove();
 				}
-			}));
+			});
+			this.doc.querySelectorAll("link[rel*=stylesheet]").forEach(linkElement => {
+				const stylesheetInfo = this.stylesheets.get(linkElement);
+				if (stylesheetInfo) {
+					const styleElement = this.doc.createElement("style");
+					if (stylesheetInfo.media) {
+						styleElement.media = stylesheetInfo.media;
+					}
+					let stylesheetContent = cssTree.generate(stylesheetInfo.stylesheet);
+					if (this.options.compressCSS) {
+						stylesheetContent = DOM.compressCSS(stylesheetContent);
+					}
+					styleElement.textContent = stylesheetContent;
+					linkElement.parentElement.replaceChild(styleElement, linkElement);
+				} else {
+					linkElement.remove();
+				}
+			});
+		}
+
+		insertVariables() {
+			if (this.cssVariables.size) {
+				const styleElement = this.doc.createElement("style");
+				if (this.doc.head.firstChild) {
+					this.doc.head.insertBefore(styleElement, this.doc.head.firstChild);
+				} else {
+					this.doc.head.appendChild(styleElement);
+				}
+				let stylesheetContent = "";
+				this.cssVariables.forEach((content, indexResource) => {
+					if (stylesheetContent) {
+						stylesheetContent += ";";
+					}
+					stylesheetContent += `${SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource}:url("${content}")`;
+				});
+				styleElement.textContent = ":root{" + stylesheetContent + "}";
+			}
 		}
 
 		async processScripts() {
@@ -861,34 +912,32 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 		}
 
 		resolveStyleAttributeURLs() {
-			Array.from(this.doc.querySelectorAll("[style]"))
-				.map(element => element.setAttribute("style", DomProcessorHelper.resolveStylesheetURLs(element.getAttribute("style"), this.baseURI, this.options)));
+			Array.from(this.doc.querySelectorAll("[style]")).map(element => {
+				element.setAttribute("style", DomProcessorHelper.resolveStylesheetURLs(element.getAttribute("style"), this.baseURI, this.options));
+				const declarationList = cssTree.parse(element.getAttribute("style"), { context: "declarationList" });
+				this.styles.set(element, declarationList);
+			});
 		}
 
 		async processStyleAttributes() {
-			await Promise.all(Array.from(this.doc.querySelectorAll("[style]"))
-				.map(async element => {
-					const rules = [{ type: CSSRule.STYLE_RULE, cssText: element.getAttribute("style") }];
-					const textContent = await DomProcessorHelper.processStylesheet(this.doc, element.getAttribute("style"), rules, this.baseURI, this.options, this.batchRequest);
-					element.setAttribute("style", textContent);
-				}));
+			await Promise.all(Array.from(this.doc.querySelectorAll("[style]")).map(async element => {
+				await DomProcessorHelper.processStyle(this.styles.get(element).children.toArray(), this.baseURI, this.options, this.cssVariables, this.batchRequest);
+			}));
 		}
 
-		async resolveLinkedStylesheetURLs() {
-			await Promise.all(Array.from(this.doc.querySelectorAll("link[rel*=stylesheet]")).map(async linkElement => {
-				const options = { maxResourceSize: this.options.maxResourceSize, maxResourceSizeEnabled: this.options.maxResourceSizeEnabled, charSet: this.charSet };
-				const stylesheetContent = await DomProcessorHelper.resolveLinkStylesheetURLs(linkElement.href, this.baseURI, options);
-				if (stylesheetContent && (!linkElement.rel.includes("alternate") || !linkElement.title)) {
-					const styleElement = this.doc.createElement("style");
-					if (linkElement.media) {
-						styleElement.media = linkElement.media;
+		replaceStyleAttributes() {
+			this.doc.querySelectorAll("[style]").forEach(async element => {
+				const declarations = this.styles.get(element);
+				if (declarations) {
+					let styleContent = cssTree.generate(declarations);
+					if (this.options.compressCSS) {
+						styleContent = DOM.compressCSS(styleContent);
 					}
-					styleElement.textContent = stylesheetContent;
-					linkElement.parentElement.replaceChild(styleElement, linkElement);
+					element.setAttribute("style", styleContent);
 				} else {
-					linkElement.remove();
+					element.setAttribute("style", "");
 				}
-			}));
+			});
 		}
 	}
 
@@ -1079,73 +1128,80 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			}
 		}
 
-		static async processStylesheet(doc, stylesheetContent, cssRules, baseURI, options, batchRequest) {
-			const urlFunctions = getURLFunctions(cssRules);
-			await Promise.all([
-				Promise.all(urlFunctions.style.map(urlFunction => processURLFunction(urlFunction))),
-				Promise.all(urlFunctions.font.map(urlFunction => processURLFunction(urlFunction, true)))]);
-			return stylesheetContent;
-
-			async function processURLFunction(urlFunction, isFont) {
-				const originalResourceURL = DomUtil.matchURL(urlFunction);
-				const resourceURL = DomUtil.normalizeURL(originalResourceURL);
-				if (!DomUtil.testIgnoredPath(resourceURL)) {
-					if (DomUtil.testValidURL(resourceURL, baseURI, options.url)) {
-						let { content, indexResource, duplicate } = await batchRequest.addURL(resourceURL);
-						const validResource = !isFont || content == EMPTY_DATA_URI || content.startsWith(PREFIX_DATA_URI_VND) || content.startsWith(PREFIX_DATA_URI_IMAGE_SVG) || await DOM.validFont(urlFunction);
-						if (!validResource) {
-							content = EMPTY_DATA_URI;
-						}
-						let regExpUrlFunction = DomUtil.getRegExp(urlFunction);
-						if (!stylesheetContent.match(regExpUrlFunction)) {
-							urlFunction = "url(" + originalResourceURL + ")";
-							regExpUrlFunction = DomUtil.getRegExp(urlFunction);
-						}
-						if (!stylesheetContent.match(regExpUrlFunction)) {
-							urlFunction = "url('" + originalResourceURL + "')";
-							regExpUrlFunction = DomUtil.getRegExp(urlFunction);
-						}
-						if (stylesheetContent.match(regExpUrlFunction)) {
-							if (duplicate && options.groupDuplicateImages && !isFont) {
-								stylesheetContent = stylesheetContent.replace(regExpUrlFunction, "var(" + SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource + ")");
-								DomUtil.insertVariable(doc, indexResource, content, options);
-							} else {
-								stylesheetContent = stylesheetContent.replace(regExpUrlFunction, urlFunction.replace(originalResourceURL, content));
-							}
+		static async processStylesheet(cssRules, baseURI, options, cssVariables, batchRequest) {
+			await Promise.all(cssRules.map(async cssRule => {
+				if (cssRule.type == "Rule") {
+					await this.processStyle(cssRule.block.children.toArray(), baseURI, options, cssVariables, batchRequest);
+				} else if (cssRule.type == "Atrule" && cssRule.name == "media") {
+					await this.processStylesheet(cssRule.block.children.toArray(), baseURI, options, cssVariables, batchRequest);
+				} else if (cssRule.type == "Atrule" && cssRule.name == "font-face") {
+					await Promise.all(cssRule.block.children.toArray().map(async declaration => {
+						if (declaration.type == "Declaration") {
+							const declarationValue = cssTree.generate(declaration.value);
+							const urlFunctions = DomUtil.getUrlFunctions(declarationValue); // TODO: use OM
+							await Promise.all(urlFunctions.map(async urlFunction => {
+								const originalResourceURL = DomUtil.matchURL(urlFunction);
+								const resourceURL = DomUtil.normalizeURL(originalResourceURL);
+								if (!DomUtil.testIgnoredPath(resourceURL)) {
+									if (DomUtil.testValidURL(resourceURL, baseURI, options.url)) {
+										let { content } = await batchRequest.addURL(resourceURL);
+										let validResource = content == EMPTY_DATA_URI || content.startsWith(PREFIX_DATA_URI_VND) || content.startsWith(PREFIX_DATA_URI_IMAGE_SVG);
+										if (!validResource) {
+											validResource = await DOM.validFont(urlFunction);
+										}
+										if (!validResource) {
+											content = EMPTY_DATA_URI;
+										}
+										declaration.value.children.forEach(token => {
+											if (token.type == "Url" && DOM.removeQuotes(cssTree.generate(token.value, "value")) == originalResourceURL) {
+												token.value.value = content;
+											}
+										});
+									}
+								}
+							}));
 						}
-					}
+					}));
 				}
-			}
-
-			function getURLFunctions(cssRules, urlFunctions = { style: [], font: [] }) {
-				Array.from(cssRules).forEach(cssRule => {
-					if (cssRule.type == CSSRule.MEDIA_RULE) {
-						getURLFunctions(cssRule.cssRules, urlFunctions);
-					} else if (cssRule.type == CSSRule.FONT_FACE_RULE) {
-						getURLFunctionsRule(cssRule.cssText, urlFunctions.font);
-					} else {
-						getURLFunctionsRule(cssRule.cssText, urlFunctions.style);
-					}
-				});
-				return { style: Array.from(urlFunctions.style), font: Array.from(urlFunctions.font) };
-			}
+			}));
+		}
 
-			function getURLFunctionsRule(cssText, urlFunctions) {
-				const urlFunctionsRule = DomUtil.getUrlFunctions(cssText);
-				urlFunctionsRule.forEach(urlFunction => {
-					const originalResourceURL = DomUtil.matchURL(urlFunction);
-					const resourceURL = DomUtil.normalizeURL(originalResourceURL);
-					if (!DomUtil.testIgnoredPath(resourceURL)) {
-						if (DomUtil.testValidURL(resourceURL, baseURI, options.url) && cssText.includes(urlFunction)) {
-							urlFunctions.push(urlFunction);
+		static async processStyle(declarations, baseURI, options, cssVariables, batchRequest) {
+			await Promise.all(declarations.map(async declaration => {
+				if (declaration.type == "Declaration") {
+					const declarationValue = cssTree.generate(declaration.value);
+					const urlFunctions = DomUtil.getUrlFunctions(declarationValue);
+					await Promise.all(urlFunctions.map(async urlFunction => {
+						const originalResourceURL = DomUtil.matchURL(urlFunction);
+						const resourceURL = DomUtil.normalizeURL(originalResourceURL);
+						if (!DomUtil.testIgnoredPath(resourceURL)) {
+							if (DomUtil.testValidURL(resourceURL, baseURI, options.url)) {
+								let { content, indexResource, duplicate } = await batchRequest.addURL(resourceURL);
+								if (duplicate && options.groupDuplicateImages) {
+									const tokens = [];
+									for (let token = declaration.value.children.head; token; token = token.next) {
+										if (token.data.type == "Url") {
+											const value = cssTree.parse("var(" + SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource + ")", { context: "value" }).children.head;
+											tokens.push({ token, value });
+										}
+									}
+									tokens.forEach(({ token, value }) => declaration.value.children.replace(token, value));
+									cssVariables.set(indexResource, content);
+								} else {
+									declaration.value.children.forEach(token => {
+										if (token.type == "Url") {
+											token.value.value = content;
+										}
+									});
+								}
+							}
 						}
-					}
-				});
-				return urlFunctions;
-			}
+					}));
+				}
+			}));
 		}
 
-		static async processAttribute(doc, resourceElements, attributeName, prefixDataURI, baseURI, options, batchRequest, processDuplicates, removeElementIfMissing) {
+		static async processAttribute(doc, resourceElements, attributeName, prefixDataURI, baseURI, options, cssVariables, styles, batchRequest, processDuplicates, removeElementIfMissing) {
 			await Promise.all(Array.from(resourceElements).map(async resourceElement => {
 				let resourceURL = resourceElement.getAttribute(attributeName);
 				resourceURL = DomUtil.normalizeURL(resourceURL);
@@ -1162,7 +1218,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 									const isSVG = content.startsWith(PREFIX_DATA_URI_IMAGE_SVG);
 									if (processDuplicates && duplicate && options.groupDuplicateImages && !isSVG) {
 										if (DomUtil.replaceImageSource(resourceElement, SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource, options)) {
-											DomUtil.insertVariable(doc, indexResource, content, options);
+											cssVariables.set(indexResource, content);
+											const declarationList = cssTree.parse(resourceElement.getAttribute("style"), { context: "declarationList" });
+											styles.set(resourceElement, declarationList);
 										} else {
 											resourceElement.setAttribute(attributeName, content);
 										}
@@ -1367,28 +1425,6 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			}
 		}
 
-		static insertVariable(doc, indexResource, content, options) {
-			const sheetAttributeName = DOM.sheetAttributeName(options.sessionId);
-			let styleElement = doc.querySelector("style[" + sheetAttributeName + "]"), insertedVariables;
-			if (!styleElement) {
-				styleElement = doc.createElement("style");
-				if (doc.head.firstChild) {
-					doc.head.insertBefore(styleElement, doc.head.firstChild);
-				} else {
-					doc.head.appendChild(styleElement);
-				}
-				styleElement.setAttribute(sheetAttributeName, "[]");
-				insertedVariables = [];
-			} else {
-				insertedVariables = JSON.parse(styleElement.getAttribute(sheetAttributeName));
-			}
-			if (!insertedVariables.includes(indexResource)) {
-				insertedVariables.push(indexResource);
-				styleElement.textContent = styleElement.textContent + `:root{${SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource}:url("${content}")}`;
-				styleElement.setAttribute(sheetAttributeName, JSON.stringify(insertedVariables));
-			}
-		}
-
 		static replaceImageSource(imgElement, variableName, options) {
 			const dataAttributeName = DOM.imagesAttributeName(options.sessionId);
 			if (imgElement.getAttribute(dataAttributeName) != null) {

+ 0 - 0
lib/single-file/base64.js → lib/single-file/util/base64.js


+ 0 - 0
lib/single-file/timeout.js → lib/single-file/util/timeout.js


+ 5 - 6
manifest.json

@@ -20,7 +20,7 @@
 				"lib/single-file/font-face-proxy.js",
 				"extension/index.js",
 				"lib/single-file/doc-helper.js",
-				"lib/single-file/timeout.js",
+				"lib/single-file/util/timeout.js",
 				"lib/fetch/content/fetch.js",
 				"lib/single-file/frame-tree.js",
 				"extension/core/content/content-frame.js"
@@ -63,19 +63,18 @@
 			"extension/ui/bg/ui-button.js",
 			"extension/ui/bg/ui-autosave.js",
 			"lib/single-file/doc-helper.js",
-			"lib/single-file/base64.js",
+			"lib/single-file/util/base64.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-tree.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-matched-rules.js",
 			"lib/single-file/css-rules-minifier.js",
 			"lib/single-file/html-srcset-parser.js",
 			"lib/single-file/html-minifier.js",
 			"lib/single-file/html-serializer.js",
-			"lib/single-file/html-alt-images.js",
+			"lib/single-file/html-images-minifier.js",
 			"lib/single-file/single-file-core.js",
 			"lib/single-file/single-file-browser.js"
 		],