Browse Source

extracted rules-matcher.js from css-minifier.js

Gildas 7 năm trước cách đây
mục cha
commit
581e2fff2a
4 tập tin đã thay đổi với 237 bổ sung186 xóa
  1. 1 0
      extension/core/bg/core.js
  2. 15 186
      lib/single-file/css-minifier.js
  3. 220 0
      lib/single-file/rules-matcher.js
  4. 1 0
      manifest.json

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

@@ -51,6 +51,7 @@ singlefile.core = (() => {
 		removeUnusedStyles: [
 			"/lib/single-file/css-what.js",
 			"/lib/single-file/parse-css.js",
+			"/lib/single-file/rules-matcher.js",
 			"/lib/single-file/css-minifier.js"
 		],
 		lazyLoadImages: [

+ 15 - 186
lib/single-file/css-minifier.js

@@ -18,163 +18,48 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/* global CSSRule, cssWhat, parseCss */
+/* global CSSRule, cssWhat, parseCss, RulesMatcher */
 
 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 rulesMatcher = new RulesMatcher(doc);
+			const mediaAllInfo = rulesMatcher.getAllMatchedRules();
 			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);
+					processRules(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);
+				processStyle(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) {
-					let selectors = cssWhat.parse(cssRule.selectorText);
-					let removedSelectors;
-					selectors.forEach((selector, selectorIndex) => {
-						const matchedElements = doc.querySelectorAll(cssWhat.stringify([selector]));
-						if (matchedElements.length) {
-							matchedElements.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 });
-							});
-						} else {
-							selectors = selectors.filter(s => s != selector);
-							removedSelectors = true;
-						}
-					});
-					if (selectors.length && removedSelectors) {
-						cssRule.selectorText = cssWhat.stringify(selectors);
-					}
-				}
-			}
-		});
-	}
-
-	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) {
+	function processRules(doc, cssRules, mediaInfo) {
 		Array.from(cssRules).forEach(cssRule => {
 			if (cssRule.type == CSSRule.MEDIA_RULE) {
-				replaceRules(doc, cssRule.cssRules, ruleMedia.medias.get(cssRule.media));
+				processRules(doc, cssRule.cssRules, mediaInfo.medias.get(cssRule.media));
 			} else if (cssRule.type == CSSRule.STYLE_RULE) {
-				const ruleInfo = ruleMedia.rules.get(cssRule);
+				const ruleInfo = mediaInfo.rules.get(cssRule);
 				if (ruleInfo) {
 					const stylesInfo = parseCss.parseAListOfDeclarations(cssRule.style.cssText);
-					const unusedStyles = stylesInfo.filter(style => !ruleInfo.get(style.name));
+					const unusedStyles = stylesInfo.filter(style => !ruleInfo.style.get(style.name));
 					if (unusedStyles.length) {
 						unusedStyles.forEach(style => cssRule.style.removeProperty(style.name));
 					}
+					if (ruleInfo.matchedSelectors.size < ruleInfo.selectorsText.length) {
+						cssRule.selectorText = ruleInfo.selectorsText.filter(selector => ruleInfo.matchedSelectors.has(selector)).join(",");
+					}
 				} else {
 					if (!testFilterSelector(cssRule.selectorText) || !doc.querySelector(getFilteredSelector(cssRule.selectorText))) {
 						const parent = cssRule.parentRule || cssRule.parentStyleSheet;
@@ -191,11 +76,11 @@ this.cssMinifier = this.stylesMinifier || (() => {
 		});
 	}
 
-	function replaceStyle(doc, cssStyle, ruleMedia) {
-		const styleInfo = ruleMedia.styles.get(cssStyle);
+	function processStyle(doc, cssStyle, mediaInfo) {
+		const styleInfo = mediaInfo.styles.get(cssStyle);
 		if (styleInfo) {
 			const stylesInfo = parseCss.parseAListOfDeclarations(cssStyle.cssText);
-			const unusedStyles = stylesInfo.filter(style => !styleInfo.get(style.name));
+			const unusedStyles = stylesInfo.filter(style => !styleInfo.style.get(style.name));
 			if (unusedStyles.length) {
 				unusedStyles.forEach(style => cssStyle.removeProperty(style.name));
 			}
@@ -216,62 +101,6 @@ this.cssMinifier = this.stylesMinifier || (() => {
 		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));
 	}

+ 220 - 0
lib/single-file/rules-matcher.js

@@ -0,0 +1,220 @@
+/*
+ * 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 PRIORITY_IMPORTANT = "important";
+
+	return class {
+		constructor(doc) {
+			this.doc = doc;
+			this.mediaAllInfo = createMediaInfo(MEDIA_ALL);
+			doc.querySelectorAll("style").forEach((styleElement, styleIndex) => {
+				if (styleElement.sheet) {
+					if (styleElement.media && styleElement.media != MEDIA_ALL) {
+						const mediaInfo = createMediaInfo(styleElement.media);
+						this.mediaAllInfo.medias.set(styleElement.media, mediaInfo);
+						getMatchedElements(doc, styleElement.sheet.cssRules, mediaInfo, styleIndex);
+					} else {
+						getMatchedElements(doc, styleElement.sheet.cssRules, this.mediaAllInfo, styleIndex);
+					}
+				}
+			});
+			sortRules(this.mediaAllInfo);
+			computeCascade(this.mediaAllInfo);
+		}
+
+		getAllMatchedRules() {
+			return this.mediaAllInfo;
+		}
+
+		getMatchedRules(element) {
+			this.mediaAllInfo.elements.get(element);
+		}
+	};
+
+	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) {
+					let selectors = cssWhat.parse(cssRule.selectorText);
+					const selectorsText = selectors.map(selector => cssWhat.stringify([selector]));
+					selectors.forEach((selector) => {
+						const selectorText = cssWhat.stringify([selector]);
+						const matchedElements = doc.querySelectorAll(selectorText);
+						if (matchedElements.length) {
+							matchedElements.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);
+								}
+								const specificity = computeSpecificity(selector);
+								specificity.ruleIndex = ruleIndex;
+								specificity.sheetIndex = sheetIndex;
+								let ruleInfo = elementInfo.find(ruleInfo => ruleInfo.cssRule == cssRule);
+								if (ruleInfo) {
+									if (compareSpecificity(ruleInfo.specificity, specificity)) {
+										ruleInfo.specificity = specificity;
+										ruleInfo.selectorText = selectorText;
+									}
+								} else {
+									ruleInfo = { cssRule, specificity, selectorText, selectorsText };
+									elementInfo.push(ruleInfo);
+								}
+							});
+						}
+					});
+				}
+			}
+		});
+	}
+
+	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 });
+					});
+				} 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, selectorText: ruleInfo.selectorText, selectorsText: ruleInfo.selectorsText });
+						}
+					});
+				}
+			});
+			elementStylesInfo.forEach((elementStyleInfo, styleName) => {
+				let ruleInfo, ascendantMedia, allMedia;
+				if (elementStyleInfo.cssRule) {
+					ascendantMedia = [mediaInfo, ...parentMediaInfos].find(media => media.rules.get(elementStyleInfo.cssRule)) || mediaInfo;
+					ruleInfo = ascendantMedia.rules.get(elementStyleInfo.cssRule);
+				}
+				if (elementStyleInfo.cssStyle) {
+					allMedia = parentMediaInfos[parentMediaInfos.length - 1] || mediaInfo;
+					ruleInfo = allMedia.styles.get(elementStyleInfo.cssStyle);
+				}
+				if (!ruleInfo) {
+					ruleInfo = { style: new Map(), matchedSelectors: new Set(), selectorsText: elementStyleInfo.selectorsText };
+					if (elementStyleInfo.cssRule) {
+						ascendantMedia.rules.set(elementStyleInfo.cssRule, ruleInfo);
+					} else {
+						allMedia.styles.set(elementStyleInfo.cssStyle, ruleInfo);
+					}
+				}
+				ruleInfo.matchedSelectors.add(elementStyleInfo.selectorText);
+				const styleValue = ruleInfo.style.get(styleName);
+				if (!styleValue) {
+					ruleInfo.style.set(styleName, elementStyleInfo.styleValue);
+				}
+			});
+		});
+		mediaInfo.medias.forEach(childMediaInfo => computeCascade(childMediaInfo, [mediaInfo, ...parentMediaInfos]));
+	}
+
+	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((ruleInfo1, ruleInfo2) =>
+			ruleInfo1.cssStyle && !ruleInfo2.cssStyle ? -1 :
+				!ruleInfo1.cssStyle && ruleInfo2.cssStyle ? 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 == "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 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;
+		}
+	}
+
+})();

+ 1 - 0
manifest.json

@@ -57,6 +57,7 @@
 			"lib/single-file/css-what.js",
 			"lib/single-file/parse-css.js",
 			"lib/single-file/fonts-minifier.js",
+			"lib/single-file/rules-matcher.js",
 			"lib/single-file/css-minifier.js",
 			"lib/single-file/htmlmini.js",
 			"lib/single-file/parse-srcset.js",