Преглед изворни кода

replaced rules-minifier.js with css-minifier.js

Gildas пре 7 година
родитељ
комит
c753220cd0

+ 3 - 3
extension/core/bg/config.js

@@ -24,7 +24,7 @@ singlefile.config = (() => {
 
 	const DEFAULT_CONFIG = {
 		removeHiddenElements: true,
-		removeUnusedCSSRules: true,
+		removeUnusedStyles: true,
 		removeFrames: true,
 		removeImports: true,
 		removeScripts: true,
@@ -98,8 +98,8 @@ singlefile.config = (() => {
 		if (config.maxResourceSize === 0) {
 			config.maxResourceSize = 1;
 		}
-		if (config.removeUnusedCSSRules === undefined) {
-			config.removeUnusedCSSRules = true;
+		if (config.removeUnusedCSSRules === undefined || config.removeUnusedCSSRules) {
+			config.removeUnusedStyles = true;
 		}
 		if (config.removeFrames === undefined) {
 			config.removeFrames = true;

+ 2 - 1
extension/core/bg/core.js

@@ -32,8 +32,9 @@ singlefile.core = (() => {
 		"/lib/single-file/base64.js",
 		"/lib/single-file/uglifycss.js",
 		"/lib/single-file/css-what.js",
+		"/lib/single-file/parse-css.js",
 		"/lib/single-file/fonts-minifier.js",
-		"/lib/single-file/rules-minifier.js",
+		"/lib/single-file/css-minifier.js",
 		"/lib/single-file/htmlmini.js",
 		"/lib/single-file/parse-srcset.js",
 		"/lib/single-file/lazy-loader.js",

+ 3 - 3
extension/ui/bg/options.js

@@ -24,7 +24,7 @@
 
 	const bgPage = await browser.runtime.getBackgroundPage();
 	const removeHiddenElementsInput = document.getElementById("removeHiddenElementsInput");
-	const removeUnusedCSSRulesInput = document.getElementById("removeUnusedCSSRulesInput");
+	const removeUnusedStylesInput = document.getElementById("removeUnusedStylesInput");
 	const removeFramesInput = document.getElementById("removeFramesInput");
 	const removeImportsInput = document.getElementById("removeImportsInput");
 	const removeScriptsInput = document.getElementById("removeScriptsInput");
@@ -75,7 +75,7 @@
 	async function refresh() {
 		const config = await bgPage.singlefile.config.get();
 		removeHiddenElementsInput.checked = config.removeHiddenElements;
-		removeUnusedCSSRulesInput.checked = config.removeUnusedCSSRules;
+		removeUnusedStylesInput.checked = config.removeUnusedStyles;
 		removeFramesInput.checked = config.removeFrames;
 		removeImportsInput.checked = config.removeImports;
 		removeScriptsInput.checked = config.removeScripts;
@@ -111,7 +111,7 @@
 		await pendingSave;
 		pendingSave = bgPage.singlefile.config.set({
 			removeHiddenElements: removeHiddenElementsInput.checked,
-			removeUnusedCSSRules: removeUnusedCSSRulesInput.checked,
+			removeUnusedStyles: removeUnusedStylesInput.checked,
 			removeFrames: removeFramesInput.checked,
 			removeImports: removeImportsInput.checked,
 			removeScripts: removeScriptsInput.checked,

+ 3 - 3
extension/ui/pages/help.html

@@ -193,9 +193,9 @@
 					</li>
 
 					<li>
-						<span class="option">remove unused CSS rules</span>
-						<p>Check this option to remove all CSS rules that do not match any element. Checking this this option should not alter
-							the document and can considerably reduce the size of the file.</p>
+						<span class="option">remove unused styles</span>
+						<p>Check this option to remove all CSS rules and styles that do not match any element. Checking this this option should
+							not alter the document and can considerably reduce the size of the file.</p>
 						<p class="notice">It is recommended to
 							<u>check</u> this option</p>
 					</li>

+ 2 - 2
extension/ui/pages/options.html

@@ -77,8 +77,8 @@
 			<input type="checkbox" id="compressCSSInput">
 		</div>
 		<div class="option">
-			<label for="removeUnusedCSSRulesInput">remove unused CSS rules</label>
-			<input type="checkbox" id="removeUnusedCSSRulesInput">
+			<label for="removeUnusedStylesInput">remove unused styles</label>
+			<input type="checkbox" id="removeUnusedStylesInput">
 		</div>
 		<div class="option">
 			<label for="removeAlternativeFontsInput">remove alternative fonts to woff and unused font rules</label>

+ 300 - 0
lib/single-file/css-minifier.js

@@ -0,0 +1,300 @@
+/*
+ * 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.cssMinifier = this.stylesMinifier || (() => {
+
+	const SEPARATOR_TYPES = ["descendant", "child", "sibling", "adjacent"];
+	const REMOVED_PSEUDO_CLASSES = ["focus", "focus-within", "hover", "link", "visited", "active"];
+	const REMOVED_PSEUDO_ELEMENTS = ["after", "before", "first-line", "first-letter"];
+	const MEDIA_ALL = "all";
+	const PRIORITY_IMPORTANT = "important";
+
+	return {
+		process: doc => {
+			const mediaAllInfo = createMediaInfo(MEDIA_ALL);
+			const stats = { processed: 0, discarded: 0 };
+			doc.querySelectorAll("style").forEach((styleElement, styleIndex) => {
+				if (styleElement.sheet) {
+					stats.processed += styleElement.sheet.cssRules.length;
+					stats.discarded += styleElement.sheet.cssRules.length;
+					if (styleElement.media && styleElement.media != MEDIA_ALL) {
+						const mediaInfo = createMediaInfo(styleElement.media);
+						mediaAllInfo.medias.set(styleElement.media, mediaInfo);
+						getMatchedElements(doc, styleElement.sheet.cssRules, mediaInfo, styleIndex);
+					} else {
+						getMatchedElements(doc, styleElement.sheet.cssRules, mediaAllInfo, styleIndex);
+					}
+				}
+			});
+			sortRules(mediaAllInfo);
+			computeCascade(mediaAllInfo);
+			doc.querySelectorAll("style").forEach(styleElement => {
+				if (styleElement.sheet) {
+					replaceRules(doc, styleElement.sheet.cssRules, mediaAllInfo);
+					styleElement.textContent = serializeRules(styleElement.sheet.cssRules);
+					stats.discarded -= styleElement.sheet.cssRules.length;
+				}
+			});
+			doc.querySelectorAll("[style]").forEach(element => {
+				replaceStyle(doc, element.style, mediaAllInfo);
+			});
+			return stats;
+		}
+	};
+
+	function getMatchedElements(doc, cssRules, mediaInfo, sheetIndex) {
+		Array.from(cssRules).forEach((cssRule, ruleIndex) => {
+			if (cssRule.type == CSSRule.MEDIA_RULE) {
+				const ruleMediaInfo = createMediaInfo(cssRule.media);
+				mediaInfo.medias.set(cssRule.media, ruleMediaInfo);
+				getMatchedElements(doc, cssRule.cssRules, ruleMediaInfo, sheetIndex);
+			} else if (cssRule.type == CSSRule.STYLE_RULE) {
+				if (cssRule.selectorText) {
+					const selectors = cssWhat.parse(cssRule.selectorText);
+					selectors.forEach((selector, selectorIndex) => {
+						doc.querySelectorAll(cssWhat.stringify([selector])).forEach(element => {
+							let elementInfo;
+							if (mediaInfo.elements.has(element)) {
+								elementInfo = mediaInfo.elements.get(element);
+							} else {
+								elementInfo = [];
+								const elementStyle = element.getAttribute("style");
+								if (elementStyle) {
+									elementInfo.push({ cssStyle: element.style });
+								}
+								mediaInfo.elements.set(element, elementInfo);
+							}
+							elementInfo.push({ cssRule, specificity: computeSpecificity(selector), selectorIndex, ruleIndex, sheetIndex });
+						});
+					});
+				}
+			}
+		});
+	}
+
+	function createMediaInfo(media) {
+		const mediaInfo = { media: media, elements: new Map(), medias: new Map(), rules: new Map() };
+		if (media == MEDIA_ALL) {
+			mediaInfo.styles = new Map();
+		}
+		return mediaInfo;
+	}
+
+	function sortRules(media) {
+		media.elements.forEach(elementRules => elementRules.sort(compareRules));
+		media.medias.forEach(sortRules);
+	}
+
+	function computeCascade(mediaInfo, parentMediaInfos = []) {
+		mediaInfo.elements.forEach((elementInfo) => {
+			const elementStylesInfo = new Map();
+			elementInfo.forEach(ruleInfo => {
+				if (ruleInfo.cssStyle) {
+					const cssStyle = ruleInfo.cssStyle;
+					const stylesInfo = parseCss.parseAListOfDeclarations(cssStyle.cssText);
+					stylesInfo.forEach(styleInfo => {
+						const important = cssStyle.getPropertyPriority(styleInfo.name) == PRIORITY_IMPORTANT;
+						const styleValue = cssStyle.getPropertyValue(styleInfo.name) + (important ? " !" + PRIORITY_IMPORTANT : "");
+						elementStylesInfo.set(styleInfo.name, { styleValue, cssStyle: ruleInfo.cssStyle, important });
+					});
+				} else {
+					const cssStyle = ruleInfo.cssRule.style;
+					const stylesInfo = parseCss.parseAListOfDeclarations(cssStyle.cssText);
+					stylesInfo.forEach(styleInfo => {
+						const important = cssStyle.getPropertyPriority(styleInfo.name) == PRIORITY_IMPORTANT;
+						const styleValue = cssStyle.getPropertyValue(styleInfo.name) + (important ? " !" + PRIORITY_IMPORTANT : "");
+						let elementStyleInfo = elementStylesInfo.get(styleInfo.name);
+						if (!elementStyleInfo || (important && !elementStyleInfo.important)) {
+							elementStylesInfo.set(styleInfo.name, { styleValue, cssRule: ruleInfo.cssRule, important });
+						}
+					});
+				}
+			});
+			elementStylesInfo.forEach((styleInfo, styleName) => {
+				let ruleInfo, ascendantMedia, allMedia;
+				if (styleInfo.cssRule) {
+					ascendantMedia = [mediaInfo, ...parentMediaInfos].find(media => media.rules.get(styleInfo.cssRule)) || mediaInfo;
+					ruleInfo = ascendantMedia.rules.get(styleInfo.cssRule);
+				}
+				if (styleInfo.cssStyle) {
+					allMedia = parentMediaInfos[parentMediaInfos.length - 1] || mediaInfo;
+					ruleInfo = allMedia.styles.get(styleInfo.cssStyle);
+				}
+				if (!ruleInfo) {
+					ruleInfo = new Map();
+					if (styleInfo.cssRule) {
+						ascendantMedia.rules.set(styleInfo.cssRule, ruleInfo);
+					} else {
+						allMedia.styles.set(styleInfo.cssStyle, ruleInfo);
+					}
+				}
+				ruleInfo.set(styleName, styleInfo.styleValue);
+			});
+		});
+		mediaInfo.medias.forEach(childMediaInfo => computeCascade(childMediaInfo, [mediaInfo, ...parentMediaInfos]));
+	}
+
+	function replaceRules(doc, cssRules, ruleMedia) {
+		Array.from(cssRules).forEach(cssRule => {
+			if (cssRule.type == CSSRule.MEDIA_RULE) {
+				replaceRules(doc, cssRule.cssRules, ruleMedia.medias.get(cssRule.media));
+			} else if (cssRule.type == CSSRule.STYLE_RULE) {
+				const ruleInfo = ruleMedia.rules.get(cssRule);
+				if (ruleInfo) {
+					const stylesInfo = parseCss.parseAListOfDeclarations(cssRule.style.cssText);
+					const unusedStyles = stylesInfo.filter(style => !ruleInfo.get(style.name));
+					if (unusedStyles.length) {
+						unusedStyles.forEach(style => cssRule.style.removeProperty(style.name));
+					}
+				} else {
+					if (!testFilterSelector(cssRule.selectorText) || !doc.querySelector(getFilteredSelector(cssRule.selectorText))) {
+						const parent = cssRule.parentRule || cssRule.parentStyleSheet;
+						let indexRule = 0;
+						while (cssRule != parent.cssRules[indexRule] && indexRule < parent.cssRules.length) {
+							indexRule++;
+						}
+						if (cssRule == parent.cssRules[indexRule]) {
+							parent.deleteRule(indexRule);
+						}
+					}
+				}
+			}
+		});
+	}
+
+	function replaceStyle(doc, cssStyle, ruleMedia) {
+		const styleInfo = ruleMedia.styles.get(cssStyle);
+		if (styleInfo) {
+			const stylesInfo = parseCss.parseAListOfDeclarations(cssStyle.cssText);
+			const unusedStyles = stylesInfo.filter(style => !styleInfo.get(style.name));
+			if (unusedStyles.length) {
+				unusedStyles.forEach(style => cssStyle.removeProperty(style.name));
+			}
+		}
+	}
+
+	function serializeRules(rules) {
+		let sheetContent = "";
+		Array.from(rules).forEach(rule => {
+			if (rule.media) {
+				sheetContent += "@media " + Array.from(rule.media).join(",") + "{";
+				sheetContent += serializeRules(rule.cssRules);
+				sheetContent += "}";
+			} else {
+				sheetContent += rule.cssText;
+			}
+		});
+		return sheetContent;
+	}
+
+	function computeSpecificity(selector, specificity = { a: 0, b: 0, c: 0 }) {
+		selector.forEach(token => {
+			if (token.expandedSelector && token.type == "attribute" && token.name === "id" && token.action === "equals") {
+				specificity.a++;
+			}
+			if ((!token.expandedSelector && token.type == "attribute") ||
+				(token.expandedSelector && token.type == "attribute" && token.name === "class" && token.action === "element") ||
+				(token.type == "pseudo" && token.name != "not")) {
+				specificity.b++;
+			}
+			if ((token.type == "tag" && token.value != "*") || (token.type == "pseudo-element")) {
+				specificity.c++;
+			}
+			if (token.data) {
+				if (Array.isArray(token.data)) {
+					token.data.forEach(selector => computeSpecificity(selector, specificity));
+				}
+			}
+		});
+		return specificity;
+	}
+
+	function compareRules(ruleInfo1, ruleInfo2) {
+		if (ruleInfo1.cssStyle && !ruleInfo2.cssStyle) {
+			return -1;
+		} else if (!ruleInfo1.cssStyle && ruleInfo2.cssStyle) {
+			return 1;
+		} else if (ruleInfo1.specificity.a > ruleInfo2.specificity.a) {
+			return -1;
+		} else if (ruleInfo1.specificity.a < ruleInfo2.specificity.a) {
+			return 1;
+		} else if (ruleInfo1.specificity.b > ruleInfo2.specificity.b) {
+			return -1;
+		} else if (ruleInfo1.specificity.b < ruleInfo2.specificity.b) {
+			return 1;
+		} else if (ruleInfo1.specificity.c > ruleInfo2.specificity.c) {
+			return -1;
+		} else if (ruleInfo1.specificity.c < ruleInfo2.specificity.c) {
+			return 1;
+		} else if (ruleInfo1.sheetIndex > ruleInfo2.sheetIndex) {
+			return -1;
+		} else if (ruleInfo1.sheetIndex < ruleInfo2.sheetIndex) {
+			return 1;
+		} else if (ruleInfo1.ruleIndex > ruleInfo2.ruleIndex) {
+			return -1;
+		} else if (ruleInfo1.ruleIndex < ruleInfo2.ruleIndex) {
+			return 1;
+		} else if (ruleInfo1.selectorIndex > ruleInfo2.selectorIndex) {
+			return -1;
+		} else if (ruleInfo1.selectorIndex < ruleInfo2.selectorIndex) {
+			return 1;
+		} else {
+			return -1;
+		}
+	}
+
+	function testFilterSelector(selector) {
+		return REMOVED_PSEUDO_CLASSES.find(pseudoClass => selector.includes(":" + pseudoClass)) || REMOVED_PSEUDO_ELEMENTS.find(pseudoElement => selector.includes("::" + pseudoElement));
+	}
+
+	function getFilteredSelector(selector) {
+		const selectors = cssWhat.parse(selector);
+		return cssWhat.stringify(selectors.map(selector => filterPseudoClasses(selector)));
+
+		function filterPseudoClasses(selector) {
+			const tokens = selector.filter(token => {
+				if (token.data) {
+					if (Array.isArray(token.data)) {
+						token.data = token.data.map(selector => filterPseudoClasses(selector));
+					}
+				}
+				const test = ((token.type != "pseudo" || !REMOVED_PSEUDO_CLASSES.includes(token.name))
+					&& (token.type != "pseudo-element" || !REMOVED_PSEUDO_ELEMENTS.includes(token.name)));
+				return test;
+			});
+			let insertedTokens = 0;
+			tokens.forEach((token, index) => {
+				if (SEPARATOR_TYPES.includes(token.type)) {
+					if (!tokens[index - 1] || SEPARATOR_TYPES.includes(tokens[index - 1].type)) {
+						tokens.splice(index + insertedTokens, 0, { type: "universal" });
+						insertedTokens++;
+					}
+				}
+			});
+			if (!tokens.length || SEPARATOR_TYPES.includes(tokens[tokens.length - 1].type)) {
+				tokens.push({ type: "universal" });
+			}
+			return tokens;
+		}
+	}
+
+})();

+ 1413 - 0
lib/single-file/parse-css.js

@@ -0,0 +1,1413 @@
+/*
+ * 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)
+
+// https://github.com/tabatkins/parse-css
+this.parseCss = this.parseCss || (() => {
+
+	function between(num, first, last) { return num >= first && num <= last; }
+	function digit(code) { return between(code, 0x30, 0x39); }
+	function hexdigit(code) { return digit(code) || between(code, 0x41, 0x46) || between(code, 0x61, 0x66); }
+	function uppercaseletter(code) { return between(code, 0x41, 0x5a); }
+	function lowercaseletter(code) { return between(code, 0x61, 0x7a); }
+	function letter(code) { return uppercaseletter(code) || lowercaseletter(code); }
+	function nonascii(code) { return code >= 0x80; }
+	function namestartchar(code) { return letter(code) || nonascii(code) || code == 0x5f; }
+	function namechar(code) { return namestartchar(code) || digit(code) || code == 0x2d; }
+	function nonprintable(code) { return between(code, 0, 8) || code == 0xb || between(code, 0xe, 0x1f) || code == 0x7f; }
+	function newline(code) { return code == 0xa; }
+	function whitespace(code) { return newline(code) || code == 9 || code == 0x20; }
+	// function badescape(code) { return newline(code) || isNaN(code); }
+
+	const maximumallowedcodepoint = 0x10ffff;
+
+	const InvalidCharacterError = function (message) {
+		this.message = message;
+	};
+	InvalidCharacterError.prototype = new Error;
+	InvalidCharacterError.prototype.name = "InvalidCharacterError";
+
+	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.charCodeAt(i);
+			if (code == 0xd && str.charCodeAt(i + 1) == 0xa) {
+				code = 0xa; i++;
+			}
+			if (code == 0xd || code == 0xc) code = 0xa;
+			if (code == 0x0) code = 0xfffd;
+			if (between(code, 0xd800, 0xdbff) && between(str.charCodeAt(i + 1), 0xdc00, 0xdfff)) {
+				// Decode a surrogate pair into an astral codepoint.
+				const lead = code - 0xd800;
+				const trail = str.charCodeAt(i + 1) - 0xdc00;
+				code = Math.pow(2, 20) + lead * Math.pow(2, 10) + trail;
+				i++;
+			}
+			codepoints.push(code);
+		}
+		return codepoints;
+	}
+
+	function stringFromCode(code) {
+		if (code <= 0xffff) return String.fromCharCode(code);
+		// Otherwise, encode astral char as surrogate pair.
+		code -= Math.pow(2, 20);
+		const lead = Math.floor(code / Math.pow(2, 10)) + 0xd800;
+		const trail = code % Math.pow(2, 10) + 0xdc00;
+		return String.fromCharCode(lead) + String.fromCharCode(trail);
+	}
+
+	function tokenize(str) {
+		str = preprocess(str);
+		let i = -1;
+		const tokens = [];
+		let code;
+
+		// Line number information.
+		let line = 0;
+		let 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: line, column: column };
+
+		const codepoint = function (i) {
+			if (i >= str.length) {
+				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;
+			code = codepoint(i);
+			if (newline(code)) incrLineno();
+			else column += num;
+			//console.log('Consume '+i+' '+String.fromCharCode(code) + ' 0x' + code.toString(16));
+			return true;
+		};
+		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) + "."); };
+
+		const consumeAToken = function () {
+			consumeComments();
+			consume();
+			if (whitespace(code)) {
+				while (whitespace(next())) consume();
+				return new WhitespaceToken;
+			}
+			else if (code == 0x22) return consumeAStringToken();
+			else if (code == 0x23) {
+				if (namechar(next()) || areAValidEscape(next(1), next(2))) {
+					const token = new HashToken();
+					if (wouldStartAnIdentifier(next(1), next(2), next(3))) token.type = "id";
+					token.value = consumeAName();
+					return token;
+				} else {
+					return new DelimToken(code);
+				}
+			}
+			else if (code == 0x24) {
+				if (next() == 0x3d) {
+					consume();
+					return new SuffixMatchToken();
+				} else {
+					return new DelimToken(code);
+				}
+			}
+			else if (code == 0x27) return consumeAStringToken();
+			else if (code == 0x28) return new OpenParenToken();
+			else if (code == 0x29) return new CloseParenToken();
+			else if (code == 0x2a) {
+				if (next() == 0x3d) {
+					consume();
+					return new SubstringMatchToken();
+				} else {
+					return new DelimToken(code);
+				}
+			}
+			else if (code == 0x2b) {
+				if (startsWithANumber()) {
+					reconsume();
+					return consumeANumericToken();
+				} else {
+					return new DelimToken(code);
+				}
+			}
+			else if (code == 0x2c) return new CommaToken();
+			else if (code == 0x2d) {
+				if (startsWithANumber()) {
+					reconsume();
+					return consumeANumericToken();
+				} else if (next(1) == 0x2d && next(2) == 0x3e) {
+					consume(2);
+					return new CDCToken();
+				} else if (startsWithAnIdentifier()) {
+					reconsume();
+					return consumeAnIdentlikeToken();
+				} else {
+					return new DelimToken(code);
+				}
+			}
+			else if (code == 0x2e) {
+				if (startsWithANumber()) {
+					reconsume();
+					return consumeANumericToken();
+				} else {
+					return new DelimToken(code);
+				}
+			}
+			else if (code == 0x3a) return new ColonToken;
+			else if (code == 0x3b) return new SemicolonToken;
+			else if (code == 0x3c) {
+				if (next(1) == 0x21 && next(2) == 0x2d && next(3) == 0x2d) {
+					consume(3);
+					return new CDOToken();
+				} else {
+					return new DelimToken(code);
+				}
+			}
+			else if (code == 0x40) {
+				if (wouldStartAnIdentifier(next(1), next(2), next(3))) {
+					return new AtKeywordToken(consumeAName());
+				} else {
+					return new DelimToken(code);
+				}
+			}
+			else if (code == 0x5b) return new OpenSquareToken();
+			else if (code == 0x5c) {
+				if (startsWithAValidEscape()) {
+					reconsume();
+					return consumeAnIdentlikeToken();
+				} else {
+					parseerror();
+					return new DelimToken(code);
+				}
+			}
+			else if (code == 0x5d) return new CloseSquareToken();
+			else if (code == 0x5e) {
+				if (next() == 0x3d) {
+					consume();
+					return new PrefixMatchToken();
+				} else {
+					return new DelimToken(code);
+				}
+			}
+			else if (code == 0x7b) return new OpenCurlyToken();
+			else if (code == 0x7c) {
+				if (next() == 0x3d) {
+					consume();
+					return new DashMatchToken();
+				} else if (next() == 0x7c) {
+					consume();
+					return new ColumnToken();
+				} else {
+					return new DelimToken(code);
+				}
+			}
+			else if (code == 0x7d) return new CloseCurlyToken();
+			else if (code == 0x7e) {
+				if (next() == 0x3d) {
+					consume();
+					return new IncludeMatchToken();
+				} else {
+					return new DelimToken(code);
+				}
+			}
+			else if (digit(code)) {
+				reconsume();
+				return consumeANumericToken();
+			}
+			else if (namestartchar(code)) {
+				reconsume();
+				return consumeAnIdentlikeToken();
+			}
+			else if (eof()) return new EOFToken();
+			else return new DelimToken(code);
+		};
+
+		const consumeComments = function () {
+			while (next(1) == 0x2f && next(2) == 0x2a) {
+				consume(2);
+				while (true) {
+					consume();
+					if (code == 0x2a && next() == 0x2f) {
+						consume();
+						break;
+					} else if (eof()) {
+						parseerror();
+						return;
+					}
+				}
+			}
+		};
+
+		const consumeANumericToken = function () {
+			const num = consumeANumber();
+			if (wouldStartAnIdentifier(next(1), next(2), next(3))) {
+				const token = new DimensionToken();
+				token.value = num.value;
+				token.repr = num.repr;
+				token.type = num.type;
+				token.unit = consumeAName();
+				return token;
+			} else if (next() == 0x25) {
+				consume();
+				const token = new PercentageToken();
+				token.value = num.value;
+				token.repr = num.repr;
+				return token;
+			} else {
+				const token = new NumberToken();
+				token.value = num.value;
+				token.repr = num.repr;
+				token.type = num.type;
+				return token;
+			}
+		};
+
+		const consumeAnIdentlikeToken = function () {
+			const str = consumeAName();
+			if (str.toLowerCase() == "url" && next() == 0x28) {
+				consume();
+				while (whitespace(next(1)) && whitespace(next(2))) consume();
+				if (next() == 0x22 || next() == 0x27) {
+					return new FunctionToken(str);
+				} else if (whitespace(next()) && (next(2) == 0x22 || next(2) == 0x27)) {
+					return new FunctionToken(str);
+				} else {
+					return consumeAURLToken();
+				}
+			} else if (next() == 0x28) {
+				consume();
+				return new FunctionToken(str);
+			} else {
+				return new IdentToken(str);
+			}
+		};
+
+		const consumeAStringToken = function (endingCodePoint) {
+			if (endingCodePoint === undefined) endingCodePoint = code;
+			let string = "";
+			while (consume()) {
+				if (code == endingCodePoint || eof()) {
+					return new StringToken(string);
+				} else if (newline(code)) {
+					parseerror();
+					reconsume();
+					return new BadStringToken();
+				} else if (code == 0x5c) {
+					if (eof(next())) {
+						donothing();
+					} else if (newline(next())) {
+						consume();
+					} else {
+						string += stringFromCode(consumeEscape());
+					}
+				} else {
+					string += stringFromCode(code);
+				}
+			}
+		};
+
+		const consumeAURLToken = function () {
+			const token = new URLToken("");
+			while (whitespace(next())) consume();
+			if (eof(next())) return token;
+			while (consume()) {
+				if (code == 0x29 || eof()) {
+					return token;
+				} else if (whitespace(code)) {
+					while (whitespace(next())) consume();
+					if (next() == 0x29 || eof(next())) {
+						consume();
+						return token;
+					} else {
+						consumeTheRemnantsOfABadURL();
+						return new BadURLToken();
+					}
+				} else if (code == 0x22 || code == 0x27 || code == 0x28 || nonprintable(code)) {
+					parseerror();
+					consumeTheRemnantsOfABadURL();
+					return new BadURLToken();
+				} else if (code == 0x5c) {
+					if (startsWithAValidEscape()) {
+						token.value += stringFromCode(consumeEscape());
+					} else {
+						parseerror();
+						consumeTheRemnantsOfABadURL();
+						return new BadURLToken();
+					}
+				} else {
+					token.value += stringFromCode(code);
+				}
+			}
+		};
+
+		const consumeEscape = function () {
+			// Assume the the current character is the \
+			// and the next code point is not a newline.
+			consume();
+			if (hexdigit(code)) {
+				// Consume 1-6 hex digits
+				const digits = [code];
+				for (let total = 0; total < 5; total++) {
+					if (hexdigit(next())) {
+						consume();
+						digits.push(code);
+					} else {
+						break;
+					}
+				}
+				if (whitespace(next())) 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;
+			}
+		};
+
+		const areAValidEscape = function (c1, c2) {
+			if (c1 != 0x5c) return false;
+			if (newline(c2)) return false;
+			return true;
+		};
+		const startsWithAValidEscape = function () {
+			return areAValidEscape(code, next());
+		};
+
+		const wouldStartAnIdentifier = function (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;
+			}
+		};
+		const startsWithAnIdentifier = function () {
+			return wouldStartAnIdentifier(code, next(1), next(2));
+		};
+
+		const wouldStartANumber = function (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;
+			}
+		};
+		const startsWithANumber = function () {
+			return wouldStartANumber(code, next(1), next(2));
+		};
+
+		const consumeAName = function () {
+			let result = "";
+			while (consume()) {
+				if (namechar(code)) {
+					result += stringFromCode(code);
+				} else if (startsWithAValidEscape()) {
+					result += stringFromCode(consumeEscape());
+				} else {
+					reconsume();
+					return result;
+				}
+			}
+		};
+
+		const consumeANumber = function () {
+			let repr = [];
+			let type = "integer";
+			if (next() == 0x2b || next() == 0x2d) {
+				consume();
+				repr += stringFromCode(code);
+			}
+			while (digit(next())) {
+				consume();
+				repr += stringFromCode(code);
+			}
+			if (next(1) == 0x2e && digit(next(2))) {
+				consume();
+				repr += stringFromCode(code);
+				consume();
+				repr += stringFromCode(code);
+				type = "number";
+				while (digit(next())) {
+					consume();
+					repr += stringFromCode(code);
+				}
+			}
+			const c1 = next(1), c2 = next(2), c3 = next(3);
+			if ((c1 == 0x45 || c1 == 0x65) && digit(c2)) {
+				consume();
+				repr += stringFromCode(code);
+				consume();
+				repr += stringFromCode(code);
+				type = "number";
+				while (digit(next())) {
+					consume();
+					repr += stringFromCode(code);
+				}
+			} else if ((c1 == 0x45 || c1 == 0x65) && (c2 == 0x2b || c2 == 0x2d) && digit(c3)) {
+				consume();
+				repr += stringFromCode(code);
+				consume();
+				repr += stringFromCode(code);
+				consume();
+				repr += stringFromCode(code);
+				type = "number";
+				while (digit(next())) {
+					consume();
+					repr += stringFromCode(code);
+				}
+			}
+			const value = convertAStringToANumber(repr);
+			return { type: type, value: value, repr: repr };
+		};
+
+		const convertAStringToANumber = function (string) {
+			// CSS's number rules are identical to JS, afaik.
+			return +string;
+		};
+
+		const consumeTheRemnantsOfABadURL = function () {
+			while (consume()) {
+				if (code == 0x29 || eof()) {
+					return;
+				} else if (startsWithAValidEscape()) {
+					consumeEscape();
+					donothing();
+				} else {
+					donothing();
+				}
+			}
+		};
+
+
+
+		let iterationCount = 0;
+		while (!eof(next())) {
+			tokens.push(consumeAToken());
+			iterationCount++;
+			if (iterationCount > str.length * 2) return "I'm infinite-looping!";
+		}
+		return tokens;
+	}
+
+	function CSSParserToken() { throw "Abstract Base Class"; }
+	CSSParserToken.prototype.toJSON = function () {
+		return { token: this.tokenType };
+	};
+	CSSParserToken.prototype.toString = function () { return this.tokenType; };
+	CSSParserToken.prototype.toSource = function () { return "" + this; };
+
+	function BadStringToken() { return this; }
+	BadStringToken.prototype = Object.create(CSSParserToken.prototype);
+	BadStringToken.prototype.tokenType = "BADSTRING";
+
+	function BadURLToken() { return this; }
+	BadURLToken.prototype = Object.create(CSSParserToken.prototype);
+	BadURLToken.prototype.tokenType = "BADURL";
+
+	function WhitespaceToken() { return this; }
+	WhitespaceToken.prototype = Object.create(CSSParserToken.prototype);
+	WhitespaceToken.prototype.tokenType = "WHITESPACE";
+	WhitespaceToken.prototype.toString = function () { return "WS"; };
+	WhitespaceToken.prototype.toSource = function () { return " "; };
+
+	function CDOToken() { return this; }
+	CDOToken.prototype = Object.create(CSSParserToken.prototype);
+	CDOToken.prototype.tokenType = "CDO";
+	CDOToken.prototype.toSource = function () { return "<!--"; };
+
+	function CDCToken() { return this; }
+	CDCToken.prototype = Object.create(CSSParserToken.prototype);
+	CDCToken.prototype.tokenType = "CDC";
+	CDCToken.prototype.toSource = function () { return "-->"; };
+
+	function ColonToken() { return this; }
+	ColonToken.prototype = Object.create(CSSParserToken.prototype);
+	ColonToken.prototype.tokenType = ":";
+
+	function SemicolonToken() { return this; }
+	SemicolonToken.prototype = Object.create(CSSParserToken.prototype);
+	SemicolonToken.prototype.tokenType = ";";
+
+	function CommaToken() { return this; }
+	CommaToken.prototype = Object.create(CSSParserToken.prototype);
+	CommaToken.prototype.tokenType = ",";
+
+	function GroupingToken() { throw "Abstract Base Class"; }
+	GroupingToken.prototype = Object.create(CSSParserToken.prototype);
+
+	function OpenCurlyToken() { this.value = "{"; this.mirror = "}"; return this; }
+	OpenCurlyToken.prototype = Object.create(GroupingToken.prototype);
+	OpenCurlyToken.prototype.tokenType = "{";
+
+	function CloseCurlyToken() { this.value = "}"; this.mirror = "{"; return this; }
+	CloseCurlyToken.prototype = Object.create(GroupingToken.prototype);
+	CloseCurlyToken.prototype.tokenType = "}";
+
+	function OpenSquareToken() { this.value = "["; this.mirror = "]"; return this; }
+	OpenSquareToken.prototype = Object.create(GroupingToken.prototype);
+	OpenSquareToken.prototype.tokenType = "[";
+
+	function CloseSquareToken() { this.value = "]"; this.mirror = "["; return this; }
+	CloseSquareToken.prototype = Object.create(GroupingToken.prototype);
+	CloseSquareToken.prototype.tokenType = "]";
+
+	function OpenParenToken() { this.value = "("; this.mirror = ")"; return this; }
+	OpenParenToken.prototype = Object.create(GroupingToken.prototype);
+	OpenParenToken.prototype.tokenType = "(";
+
+	function CloseParenToken() { this.value = ")"; this.mirror = "("; return this; }
+	CloseParenToken.prototype = Object.create(GroupingToken.prototype);
+	CloseParenToken.prototype.tokenType = ")";
+
+	function IncludeMatchToken() { return this; }
+	IncludeMatchToken.prototype = Object.create(CSSParserToken.prototype);
+	IncludeMatchToken.prototype.tokenType = "~=";
+
+	function DashMatchToken() { return this; }
+	DashMatchToken.prototype = Object.create(CSSParserToken.prototype);
+	DashMatchToken.prototype.tokenType = "|=";
+
+	function PrefixMatchToken() { return this; }
+	PrefixMatchToken.prototype = Object.create(CSSParserToken.prototype);
+	PrefixMatchToken.prototype.tokenType = "^=";
+
+	function SuffixMatchToken() { return this; }
+	SuffixMatchToken.prototype = Object.create(CSSParserToken.prototype);
+	SuffixMatchToken.prototype.tokenType = "$=";
+
+	function SubstringMatchToken() { return this; }
+	SubstringMatchToken.prototype = Object.create(CSSParserToken.prototype);
+	SubstringMatchToken.prototype.tokenType = "*=";
+
+	function ColumnToken() { return this; }
+	ColumnToken.prototype = Object.create(CSSParserToken.prototype);
+	ColumnToken.prototype.tokenType = "||";
+
+	function EOFToken() { return this; }
+	EOFToken.prototype = Object.create(CSSParserToken.prototype);
+	EOFToken.prototype.tokenType = "EOF";
+	EOFToken.prototype.toSource = function () { return ""; };
+
+	function DelimToken(code) {
+		this.value = stringFromCode(code);
+		return this;
+	}
+	DelimToken.prototype = Object.create(CSSParserToken.prototype);
+	DelimToken.prototype.tokenType = "DELIM";
+	DelimToken.prototype.toString = function () { return "DELIM(" + this.value + ")"; };
+	DelimToken.prototype.toJSON = function () {
+		const json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+		json.value = this.value;
+		return json;
+	};
+	DelimToken.prototype.toSource = function () {
+		if (this.value == "\\")
+			return "\\\n";
+		else
+			return this.value;
+	};
+
+	function StringValuedToken() { throw "Abstract Base Class"; }
+	StringValuedToken.prototype = Object.create(CSSParserToken.prototype);
+	StringValuedToken.prototype.ASCIIMatch = function (str) {
+		return this.value.toLowerCase() == str.toLowerCase();
+	};
+	StringValuedToken.prototype.toJSON = function () {
+		const json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+		json.value = this.value;
+		return json;
+	};
+
+	function IdentToken(val) {
+		this.value = val;
+	}
+	IdentToken.prototype = Object.create(StringValuedToken.prototype);
+	IdentToken.prototype.tokenType = "IDENT";
+	IdentToken.prototype.toString = function () { return "IDENT(" + this.value + ")"; };
+	IdentToken.prototype.toSource = function () {
+		return escapeIdent(this.value);
+	};
+
+	function FunctionToken(val) {
+		this.value = val;
+		this.mirror = ")";
+	}
+	FunctionToken.prototype = Object.create(StringValuedToken.prototype);
+	FunctionToken.prototype.tokenType = "FUNCTION";
+	FunctionToken.prototype.toString = function () { return "FUNCTION(" + this.value + ")"; };
+	FunctionToken.prototype.toSource = function () {
+		return escapeIdent(this.value) + "(";
+	};
+
+	function AtKeywordToken(val) {
+		this.value = val;
+	}
+	AtKeywordToken.prototype = Object.create(StringValuedToken.prototype);
+	AtKeywordToken.prototype.tokenType = "AT-KEYWORD";
+	AtKeywordToken.prototype.toString = function () { return "AT(" + this.value + ")"; };
+	AtKeywordToken.prototype.toSource = function () {
+		return "@" + escapeIdent(this.value);
+	};
+
+	function HashToken(val) {
+		this.value = val;
+		this.type = "unrestricted";
+	}
+	HashToken.prototype = Object.create(StringValuedToken.prototype);
+	HashToken.prototype.tokenType = "HASH";
+	HashToken.prototype.toString = function () { return "HASH(" + this.value + ")"; };
+	HashToken.prototype.toJSON = function () {
+		const json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+		json.value = this.value;
+		json.type = this.type;
+		return json;
+	};
+	HashToken.prototype.toSource = function () {
+		if (this.type == "id") {
+			return "#" + escapeIdent(this.value);
+		} else {
+			return "#" + escapeHash(this.value);
+		}
+	};
+
+	function StringToken(val) {
+		this.value = val;
+	}
+	StringToken.prototype = Object.create(StringValuedToken.prototype);
+	StringToken.prototype.tokenType = "STRING";
+	StringToken.prototype.toString = function () {
+		return "\"" + escapeString(this.value) + "\"";
+	};
+
+	function URLToken(val) {
+		this.value = val;
+	}
+	URLToken.prototype = Object.create(StringValuedToken.prototype);
+	URLToken.prototype.tokenType = "URL";
+	URLToken.prototype.toString = function () { return "URL(" + this.value + ")"; };
+	URLToken.prototype.toSource = function () {
+		return "url(\"" + escapeString(this.value) + "\")";
+	};
+
+	function NumberToken() {
+		this.value = null;
+		this.type = "integer";
+		this.repr = "";
+	}
+	NumberToken.prototype = Object.create(CSSParserToken.prototype);
+	NumberToken.prototype.tokenType = "NUMBER";
+	NumberToken.prototype.toString = function () {
+		if (this.type == "integer")
+			return "INT(" + this.value + ")";
+		return "NUMBER(" + this.value + ")";
+	};
+	NumberToken.prototype.toJSON = function () {
+		const json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+		json.value = this.value;
+		json.type = this.type;
+		json.repr = this.repr;
+		return json;
+	};
+	NumberToken.prototype.toSource = function () { return this.repr; };
+
+	function PercentageToken() {
+		this.value = null;
+		this.repr = "";
+	}
+	PercentageToken.prototype = Object.create(CSSParserToken.prototype);
+	PercentageToken.prototype.tokenType = "PERCENTAGE";
+	PercentageToken.prototype.toString = function () { return "PERCENTAGE(" + this.value + ")"; };
+	PercentageToken.prototype.toJSON = function () {
+		const json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+		json.value = this.value;
+		json.repr = this.repr;
+		return json;
+	};
+	PercentageToken.prototype.toSource = function () { return this.repr + "%"; };
+
+	function DimensionToken() {
+		this.value = null;
+		this.type = "integer";
+		this.repr = "";
+		this.unit = "";
+	}
+	DimensionToken.prototype = Object.create(CSSParserToken.prototype);
+	DimensionToken.prototype.tokenType = "DIMENSION";
+	DimensionToken.prototype.toString = function () { return "DIM(" + this.value + "," + this.unit + ")"; };
+	DimensionToken.prototype.toJSON = function () {
+		const json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+		json.value = this.value;
+		json.type = this.type;
+		json.repr = this.repr;
+		json.unit = this.unit;
+		return json;
+	};
+	DimensionToken.prototype.toSource = function () {
+		const source = this.repr;
+		let unit = escapeIdent(this.unit);
+		if (unit[0].toLowerCase() == "e" && (unit[1] == "-" || between(unit.charCodeAt(1), 0x30, 0x39))) {
+			// Unit is ambiguous with scinot
+			// Remove the leading "e", replace with escape.
+			unit = "\\65 " + unit.slice(1, unit.length);
+		}
+		return source + unit;
+	};
+
+	function escapeIdent(string) {
+		string = "" + string;
+		let result = "";
+		const firstcode = string.charCodeAt(0);
+		for (let i = 0; i < string.length; i++) {
+			const code = string.charCodeAt(i);
+			if (code == 0x0) {
+				throw new InvalidCharacterError("Invalid character: the input contains U+0000.");
+			}
+
+			if (
+				between(code, 0x1, 0x1f) || code == 0x7f ||
+				(i == 0 && between(code, 0x30, 0x39)) ||
+				(i == 1 && between(code, 0x30, 0x39) && firstcode == 0x2d)
+			) {
+				result += "\\" + code.toString(16) + " ";
+			} else if (
+				code >= 0x80 ||
+				code == 0x2d ||
+				code == 0x5f ||
+				between(code, 0x30, 0x39) ||
+				between(code, 0x41, 0x5a) ||
+				between(code, 0x61, 0x7a)
+			) {
+				result += string[i];
+			} else {
+				result += "\\" + string[i];
+			}
+		}
+		return result;
+	}
+
+	function escapeHash(string) {
+		// Escapes the contents of "unrestricted"-type hash tokens.
+		// Won't preserve the ID-ness of "id"-type hash tokens;
+		// use escapeIdent() for that.
+		string = "" + string;
+		let result = "";
+		// let firstcode = string.charCodeAt(0);
+		for (let i = 0; i < string.length; i++) {
+			const code = string.charCodeAt(i);
+			if (code == 0x0) {
+				throw new InvalidCharacterError("Invalid character: the input contains U+0000.");
+			}
+
+			if (
+				code >= 0x80 ||
+				code == 0x2d ||
+				code == 0x5f ||
+				between(code, 0x30, 0x39) ||
+				between(code, 0x41, 0x5a) ||
+				between(code, 0x61, 0x7a)
+			) {
+				result += string[i];
+			} else {
+				result += "\\" + code.toString(16) + " ";
+			}
+		}
+		return result;
+	}
+
+	function escapeString(string) {
+		string = "" + string;
+		let result = "";
+		for (let i = 0; i < string.length; i++) {
+			const code = string.charCodeAt(i);
+
+			if (code == 0x0) {
+				throw new InvalidCharacterError("Invalid character: the input contains U+0000.");
+			}
+
+			if (between(code, 0x1, 0x1f) || code == 0x7f) {
+				result += "\\" + code.toString(16) + " ";
+			} else if (code == 0x22 || code == 0x5c) {
+				result += "\\" + string[i];
+			} else {
+				result += string[i];
+			}
+		}
+		return result;
+	}
+
+	// ---
+	function TokenStream(tokens) {
+		// Assume that tokens is an array.
+		this.tokens = tokens;
+		this.i = -1;
+	}
+	TokenStream.prototype.tokenAt = function (i) {
+		if (i < this.tokens.length)
+			return this.tokens[i];
+		return new EOFToken();
+	};
+	TokenStream.prototype.consume = function (num) {
+		if (num === undefined) num = 1;
+		this.i += num;
+		this.token = this.tokenAt(this.i);
+		//console.log(this.i, this.token);
+		return true;
+	};
+	TokenStream.prototype.next = function () {
+		return this.tokenAt(this.i + 1);
+	};
+	TokenStream.prototype.reconsume = function () {
+		this.i--;
+	};
+
+	function parseerror(s, msg) {
+		throw new Error("Parse error at token " + s.i + ": " + s.token + ".\n" + msg);
+	}
+	function donothing() { return true; }
+
+	function consumeAListOfRules(s, topLevel) {
+		const rules = [];
+		let rule;
+		while (s.consume()) {
+			if (s.token instanceof WhitespaceToken) {
+				continue;
+			} else if (s.token instanceof EOFToken) {
+				return rules;
+			} else if (s.token instanceof CDOToken || s.token instanceof CDCToken) {
+				if (topLevel == "top-level") continue;
+				s.reconsume();
+				if (rule = consumeAQualifiedRule(s)) rules.push(rule);
+			} else if (s.token instanceof AtKeywordToken) {
+				s.reconsume();
+				if (rule = consumeAnAtRule(s)) rules.push(rule);
+			} else {
+				s.reconsume();
+				if (rule = consumeAQualifiedRule(s)) rules.push(rule);
+			}
+		}
+	}
+
+	function consumeAnAtRule(s) {
+		s.consume();
+		const rule = new AtRule(s.token.value);
+		while (s.consume()) {
+			if (s.token instanceof SemicolonToken || s.token instanceof EOFToken) {
+				return rule;
+			} else if (s.token instanceof OpenCurlyToken) {
+				rule.value = consumeASimpleBlock(s);
+				return rule;
+			} else if (s.token instanceof SimpleBlock && s.token.name == "{") {
+				rule.value = s.token;
+				return rule;
+			} else {
+				s.reconsume();
+				rule.prelude.push(consumeAComponentValue(s));
+			}
+		}
+	}
+
+	function consumeAQualifiedRule(s) {
+		const rule = new QualifiedRule();
+		while (s.consume()) {
+			if (s.token instanceof EOFToken) {
+				parseerror(s, "Hit EOF when trying to parse the prelude of a qualified rule.");
+				return;
+			} else if (s.token instanceof OpenCurlyToken) {
+				rule.value = consumeASimpleBlock(s);
+				return rule;
+			} else if (s.token instanceof SimpleBlock && s.token.name == "{") {
+				rule.value = s.token;
+				return rule;
+			} else {
+				s.reconsume();
+				rule.prelude.push(consumeAComponentValue(s));
+			}
+		}
+	}
+
+	function consumeAListOfDeclarations(s) {
+		const decls = [];
+		while (s.consume()) {
+			if (s.token instanceof WhitespaceToken || s.token instanceof SemicolonToken) {
+				donothing();
+			} else if (s.token instanceof EOFToken) {
+				return decls;
+			} else if (s.token instanceof AtKeywordToken) {
+				s.reconsume();
+				decls.push(consumeAnAtRule(s));
+			} else if (s.token instanceof IdentToken) {
+				const temp = [s.token];
+				while (!(s.next() instanceof SemicolonToken || s.next() instanceof EOFToken))
+					temp.push(consumeAComponentValue(s));
+				let decl;
+				if (decl = consumeADeclaration(new TokenStream(temp))) decls.push(decl);
+			} else {
+				parseerror(s);
+				s.reconsume();
+				while (!(s.next() instanceof SemicolonToken || s.next() instanceof EOFToken))
+					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() instanceof WhitespaceToken) s.consume();
+		if (!(s.next() instanceof ColonToken)) {
+			parseerror(s);
+			return;
+		} else {
+			s.consume();
+		}
+		while (!(s.next() instanceof EOFToken)) {
+			decl.value.push(consumeAComponentValue(s));
+		}
+		let foundImportant = false;
+		for (let i = decl.value.length - 1; i >= 0; i--) {
+			if (decl.value[i] instanceof WhitespaceToken) {
+				continue;
+			} else if (decl.value[i] instanceof IdentToken && decl.value[i].ASCIIMatch("important")) {
+				foundImportant = true;
+			} else if (foundImportant && decl.value[i] instanceof DelimToken && 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 instanceof OpenCurlyToken || s.token instanceof OpenSquareToken || s.token instanceof OpenParenToken)
+			return consumeASimpleBlock(s);
+		if (s.token instanceof FunctionToken)
+			return consumeAFunction(s);
+		return s.token;
+	}
+
+	function consumeASimpleBlock(s) {
+		const mirror = s.token.mirror;
+		const block = new SimpleBlock(s.token.value);
+		while (s.consume()) {
+			if (s.token instanceof EOFToken || (s.token instanceof GroupingToken && s.token.value == mirror))
+				return block;
+			else {
+				s.reconsume();
+				block.value.push(consumeAComponentValue(s));
+			}
+		}
+	}
+
+	function consumeAFunction(s) {
+		const func = new Func(s.token.value);
+		while (s.consume()) {
+			if (s.token instanceof EOFToken || s.token instanceof CloseParenToken)
+				return func;
+			else {
+				s.reconsume();
+				func.value.push(consumeAComponentValue(s));
+			}
+		}
+	}
+
+	function normalizeInput(input) {
+		if (typeof input == "string")
+			return new TokenStream(tokenize(input));
+		if (input instanceof TokenStream)
+			return input;
+		if (input.length !== undefined)
+			return new TokenStream(input);
+		else throw SyntaxError(input);
+	}
+
+	function parseAStylesheet(s) {
+		s = normalizeInput(s);
+		const sheet = new Stylesheet();
+		sheet.value = consumeAListOfRules(s, "top-level");
+		return sheet;
+	}
+
+	function parseAListOfRules(s) {
+		s = normalizeInput(s);
+		return consumeAListOfRules(s);
+	}
+
+	function parseARule(s) {
+		s = normalizeInput(s);
+		while (s.next() instanceof WhitespaceToken) s.consume();
+		if (s.next() instanceof EOFToken) throw SyntaxError();
+		let rule;
+		if (s.next() instanceof AtKeywordToken) {
+			rule = consumeAnAtRule(s);
+		} else {
+			rule = consumeAQualifiedRule(s);
+			if (!rule) throw SyntaxError();
+		}
+		while (s.next() instanceof WhitespaceToken) s.consume();
+		if (s.next() instanceof EOFToken)
+			return rule;
+		throw SyntaxError();
+	}
+
+	function parseADeclaration(s) {
+		s = normalizeInput(s);
+		while (s.next() instanceof WhitespaceToken) s.consume();
+		if (!(s.next() instanceof IdentToken)) throw SyntaxError();
+		const decl = consumeADeclaration(s);
+		if (decl)
+			return decl;
+		else
+			throw SyntaxError();
+	}
+
+	function parseAListOfDeclarations(s) {
+		s = normalizeInput(s);
+		return consumeAListOfDeclarations(s);
+	}
+
+	function parseAComponentValue(s) {
+		s = normalizeInput(s);
+		while (s.next() instanceof WhitespaceToken) s.consume();
+		if (s.next() instanceof EOFToken) throw SyntaxError();
+		const val = consumeAComponentValue(s);
+		if (!val) throw SyntaxError();
+		while (s.next() instanceof WhitespaceToken) s.consume();
+		if (s.next() instanceof EOFToken)
+			return val;
+		throw SyntaxError();
+	}
+
+	function parseAListOfComponentValues(s) {
+		s = normalizeInput(s);
+		const vals = [];
+		while (true) {
+			const val = consumeAComponentValue(s);
+			if (val instanceof EOFToken)
+				return vals;
+			else
+				vals.push(val);
+		}
+	}
+
+	function parseACommaSeparatedListOfComponentValues(s) {
+		s = normalizeInput(s);
+		const listOfCVLs = [];
+		while (true) {
+			const vals = [];
+			while (true) {
+				const val = consumeAComponentValue(s);
+				if (val instanceof EOFToken) {
+					listOfCVLs.push(vals);
+					return listOfCVLs;
+				} else if (val instanceof CommaToken) {
+					listOfCVLs.push(vals);
+					break;
+				} else {
+					vals.push(val);
+				}
+			}
+		}
+	}
+
+
+	function CSSParserRule() { throw "Abstract Base Class"; }
+	CSSParserRule.prototype.toString = function (indent) {
+		return JSON.stringify(this, null, indent);
+	};
+	CSSParserRule.prototype.toJSON = function () {
+		return { type: this.type, value: this.value };
+	};
+
+	function Stylesheet() {
+		this.value = [];
+		return this;
+	}
+	Stylesheet.prototype = Object.create(CSSParserRule.prototype);
+	Stylesheet.prototype.type = "STYLESHEET";
+
+	function AtRule(name) {
+		this.name = name;
+		this.prelude = [];
+		this.value = null;
+		return this;
+	}
+	AtRule.prototype = Object.create(CSSParserRule.prototype);
+	AtRule.prototype.type = "AT-RULE";
+	AtRule.prototype.toJSON = function () {
+		const json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+		json.name = this.name;
+		json.prelude = this.prelude;
+		return json;
+	};
+
+	function QualifiedRule() {
+		this.prelude = [];
+		this.value = [];
+		return this;
+	}
+	QualifiedRule.prototype = Object.create(CSSParserRule.prototype);
+	QualifiedRule.prototype.type = "QUALIFIED-RULE";
+	QualifiedRule.prototype.toJSON = function () {
+		const json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+		json.prelude = this.prelude;
+		return json;
+	};
+
+	function Declaration(name) {
+		this.name = name;
+		this.value = [];
+		this.important = false;
+		return this;
+	}
+	Declaration.prototype = Object.create(CSSParserRule.prototype);
+	Declaration.prototype.type = "DECLARATION";
+	Declaration.prototype.toJSON = function () {
+		const json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+		json.name = this.name;
+		json.important = this.important;
+		return json;
+	};
+
+	function SimpleBlock(type) {
+		this.name = type;
+		this.value = [];
+		return this;
+	}
+	SimpleBlock.prototype = Object.create(CSSParserRule.prototype);
+	SimpleBlock.prototype.type = "BLOCK";
+	SimpleBlock.prototype.toJSON = function () {
+		const json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+		json.name = this.name;
+		return json;
+	};
+
+	function Func(name) {
+		this.name = name;
+		this.value = [];
+		return this;
+	}
+	Func.prototype = Object.create(CSSParserRule.prototype);
+	Func.prototype.type = "FUNCTION";
+	Func.prototype.toJSON = function () {
+		const json = this.constructor.prototype.constructor.prototype.toJSON.call(this);
+		json.name = this.name;
+		return json;
+	};
+
+
+	/* Grammar Application */
+
+	function canonicalize(rule, grammar, topGrammar) {
+		if (grammar === undefined) grammar = CSSGrammar;
+		if (topGrammar === undefined) topGrammar = grammar;
+		let unknownTransformer;
+		if (grammar) {
+			if (grammar.stylesheet) grammar = topGrammar;
+			unknownTransformer = grammar.unknown || function () { return; };
+		}
+		const ret = { "type": rule.type.toLowerCase() };
+		let contents, unparsedContents;
+		if (rule.type == "STYLESHEET") {
+			contents = rule.value;
+		} else if (rule.type == "BLOCK") {
+			unparsedContents = rule.value;
+			ret.name = rule.name;
+		} else if (rule.type == "QUALIFIED-RULE") {
+			unparsedContents = rule.value.value;
+			ret.prelude = rule.prelude;
+		} else if (rule.type == "AT-RULE") {
+			unparsedContents = rule.value.value;
+			ret.name = rule.name;
+			ret.prelude = rule.prelude;
+		} else if (rule.type == "DECLARATION") {
+			// I don't do grammar-checking of declarations yet.
+			ret.name = rule.name;
+			ret.value = rule.value;
+			ret.important = rule.important;
+			return ret;
+		}
+		if (unparsedContents) {
+			if (grammar.declarations) {
+				contents = parseAListOfDeclarations(unparsedContents);
+			} else if (grammar.qualified) {
+				contents = parseAListOfRules(unparsedContents);
+			}
+		}
+
+		if (!grammar) {
+			return ret;
+		} else if (grammar.declarations) {
+			ret.declarations = {}; // simple key/value map of declarations
+			ret.rules = []; // in-order list of both decls and at-rules
+			ret.errors = [];
+			for (let i = 0; i < contents.length; i++) {
+				const rule = contents[i];
+				if (rule instanceof Declaration) {
+					const decl = canonicalize(rule, {}, topGrammar);
+					ret.declarations[rule.name] = decl;
+					ret.rules.push(decl);
+				} else { // rule is instanceof AtRule
+					const subGrammar = grammar["@" + rule.name];
+					if (subGrammar) { // Rule is valid in this context
+						ret.rules.push(canonicalize(rule, subGrammar, topGrammar));
+					} else {
+						const result = unknownTransformer(rule);
+						if (result) {
+							ret.rules.push(result);
+						} else {
+							ret.errors.push(result);
+						}
+					}
+				}
+			}
+		} else {
+			ret.rules = [];
+			ret.errors = [];
+			for (let i = 0; i < contents.length; i++) {
+				const rule = contents[i];
+				if (rule instanceof QualifiedRule) {
+					ret.rules.push(canonicalize(rule, grammar.qualified, topGrammar));
+				} else {
+					const subGrammar = grammar["@" + rule.name];
+					if (subGrammar) { // Rule is valid in this context
+						ret.rules.push(canonicalize(rule, subGrammar, topGrammar));
+					} else {
+						const result = unknownTransformer(rule);
+						if (result) {
+							ret.rules.push(result);
+						} else {
+							ret.errors.push(result);
+						}
+					}
+				}
+			}
+		}
+		return ret;
+	}
+
+	const CSSGrammar = {
+		qualified: { declarations: true },
+		"@media": { stylesheet: true },
+		"@keyframes": { qualified: { declarations: true } },
+		"@font-face": { declarations: true },
+		"@supports": { stylesheet: true },
+		"@scope": { stylesheet: true },
+		"@counter-style": { declarations: true },
+		"@import": null,
+		"@font-feature-values": {
+			// No qualified rules actually allowed,
+			// but have to declare it one way or the other.
+			qualified: true,
+			"@stylistic": { declarations: true },
+			"@styleset": { declarations: true },
+			"@character-variants": { declarations: true },
+			"@swash": { declarations: true },
+			"@ornaments": { declarations: true },
+			"@annotation": { declarations: true },
+		},
+		"@viewport": { declarations: true },
+		"@page": {
+			declarations: true,
+			"@top-left-corner": { declarations: true },
+			"@top-left": { declarations: true },
+			"@top-center": { declarations: true },
+			"@top-right": { declarations: true },
+			"@top-right-corner": { declarations: true },
+			"@right-top": { declarations: true },
+			"@right-middle": { declarations: true },
+			"@right-bottom": { declarations: true },
+			"@right-bottom-corner": { declarations: true },
+			"@bottom-right": { declarations: true },
+			"@bottom-center": { declarations: true },
+			"@bottom-left": { declarations: true },
+			"@bottom-left-corner": { declarations: true },
+			"@left-bottom": { declarations: true },
+			"@left-center": { declarations: true },
+			"@left-top": { declarations: true },
+		},
+		"@custom-selector": null,
+		"@custom-media": null
+	};
+
+
+
+	// Exportation.
+
+	return {
+		CSSParserRule: CSSParserRule,
+		Stylesheet: Stylesheet,
+		AtRule: AtRule,
+		QualifiedRule: QualifiedRule,
+		Declaration: Declaration,
+		SimpleBlock: SimpleBlock,
+		Func: Func,
+		parseAStylesheet: parseAStylesheet,
+		parseAListOfRules: parseAListOfRules,
+		parseARule: parseARule,
+		parseADeclaration: parseADeclaration,
+		parseAListOfDeclarations: parseAListOfDeclarations,
+		parseAComponentValue: parseAComponentValue,
+		parseAListOfComponentValues: parseAListOfComponentValues,
+		parseACommaSeparatedListOfComponentValues: parseACommaSeparatedListOfComponentValues,
+		canonicalizeRule: canonicalize,
+		CSSGrammar: CSSGrammar
+	};
+
+})();

+ 3 - 3
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, rulesMinifier, fontsMinifier, lazyLoader, serializer, docHelper */
+/* global SingleFileCore, DOMParser, TextDecoder, Blob, fetch, base64, superFetch, parseSrcset, uglifycss, htmlmini, cssMinifier, fontsMinifier, lazyLoader, serializer, docHelper */
 
 this.SingleFile = this.SingleFile || (() => {
 
@@ -125,8 +125,8 @@ this.SingleFile = this.SingleFile || (() => {
 			return lazyLoader.imageSelectors;
 		}
 
-		static rulesMinifier(doc) {
-			return rulesMinifier.process(doc);
+		static cssMinifier(doc) {
+			return cssMinifier.process(doc);
 		}
 
 		static fontsMinifier(doc, secondPass) {

+ 6 - 6
lib/single-file/single-file-core.js

@@ -122,8 +122,8 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 				initializationPromises.push(this.processor.frames(true));
 			}
 			await Promise.all(initializationPromises);
-			if (this.options.removeUnusedCSSRules) {
-				this.processor.removeUnusedCSSRules();
+			if (this.options.removeUnusedStyles) {
+				this.processor.removeUnusedStyles();
 			}
 			if (!this.options.removeImports) {
 				initializationPromises.push(this.processor.htmlImports(true));
@@ -398,10 +398,10 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			});
 		}
 
-		removeUnusedCSSRules() {
-			const removedRules = DOM.rulesMinifier(this.doc);
-			this.stats.set("processed", "cssRules", removedRules.processed);
-			this.stats.set("discarded", "cssRules", removedRules.discarded);
+		removeUnusedStyles() {
+			const stats = DOM.cssMinifier(this.doc);
+			this.stats.set("processed", "cssRules", stats.processed);
+			this.stats.set("discarded", "cssRules", stats.discarded);
 		}
 
 		removeAlternativeFonts(secondPass) {

+ 2 - 1
manifest.json

@@ -54,8 +54,9 @@
 			"lib/single-file/base64.js",
 			"lib/single-file/uglifycss.js",
 			"lib/single-file/css-what.js",
+			"lib/single-file/parse-css.js",
 			"lib/single-file/fonts-minifier.js",
-			"lib/single-file/rules-minifier.js",
+			"lib/single-file/css-minifier.js",
 			"lib/single-file/htmlmini.js",
 			"lib/single-file/parse-srcset.js",
 			"lib/single-file/lazy-loader.js",