Ver Fonte

better handling of pseudo classes and pseudo elements

Gildas há 7 anos atrás
pai
commit
dd0458403e
2 ficheiros alterados com 89 adições e 57 exclusões
  1. 85 39
      lib/single-file/css-rules-matcher.js
  2. 4 18
      lib/single-file/css-rules-minifier.js

+ 85 - 39
lib/single-file/css-rules-matcher.js

@@ -23,7 +23,8 @@
 this.RulesMatcher = this.RulesMatcher || (() => {
 
 	const MEDIA_ALL = "all";
-	const IGNORED_PSEUDOS = [":focus", ":focus-within", ":hover", ":link", ":visited", ":active", "::after", "::before", "::first-line", "::first-letter", "::placeholder", "::selection", "::marker", "::cue"];
+	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";
@@ -109,7 +110,12 @@ this.RulesMatcher = this.RulesMatcher || (() => {
 					}
 					if (selectors) {
 						const selectorsText = selectors.map(selector => cssWhat.stringify([selector]));
-						selectors.forEach((selector, selectorIndex) => getMatchedElementsSelector(doc, cssRule, selector, selectorsText[selectorIndex], selectorsText, mediaInfo, ruleIndex, sheetIndex, matchedElementsCache));
+						selectors.forEach((selector, selectorIndex) => {
+							const selectorText = selectorsText[selectorIndex];
+							getMatchedElementsSelector(doc,
+								{ cssRule, mediaInfo, ruleIndex, sheetIndex, selectors, selectorsText, selector, selectorText },
+								matchedElementsCache);
+						});
 					}
 				}
 			}
@@ -119,60 +125,53 @@ this.RulesMatcher = this.RulesMatcher || (() => {
 		}
 	}
 
-	function getMatchedElementsSelector(doc, cssRule, selector, selectorText, selectorsText, mediaInfo, ruleIndex, sheetIndex, matchedElementsCache) {
+	function getMatchedElementsSelector(doc, ruleData, matchedElementsCache) {
+		let selectorText;
+		const selectorContainsPseudo = containsPseudo(ruleData.selectorText);
+		if (selectorContainsPseudo) {
+			selectorText = getFilteredSelector(ruleData.selectorText);
+		} else {
+			selectorText = ruleData.selectorText;
+		}
 		const cachedMatchedElements = matchedElementsCache.get(selectorText);
 		const matchedElements = cachedMatchedElements || doc.querySelectorAll(selectorText);
 		if (!cachedMatchedElements) {
 			matchedElementsCache.set(selectorText, matchedElements);
 		}
 		if (matchedElements.length) {
-			matchedElements.forEach(element => addRule(element, cssRule, selector, selectorText, selectorsText, mediaInfo, ruleIndex, sheetIndex));
-		}
-	}
-
-	function addRule(element, cssRule, selector, selectorText, selectorsText, mediaInfo, ruleIndex, sheetIndex) {
-		const info = getInfo(element, cssRule, selector, mediaInfo, ruleIndex, sheetIndex);
-		const { elementInfo, ruleInfo, specificity } = info;
-		if (ruleInfo) {
-			if (compareSpecificity(ruleInfo.specificity, specificity) == 1) {
-				let pseudoIndex = 0, pseudoLength = IGNORED_PSEUDOS.length, ignoredPseudo;
-				while (pseudoIndex < pseudoLength && !(ignoredPseudo = selectorText.includes(IGNORED_PSEUDOS[pseudoIndex]))) {
-					pseudoIndex++;
-				}
-				if (!ignoredPseudo || pseudoIndex < pseudoLength) {
-					ruleInfo.specificity = specificity;
-					ruleInfo.selectorText = selectorText;
-				}
+			if (selectorContainsPseudo) {
+				ruleData.mediaInfo.pseudoSelectors.add(ruleData.cssRule.selectorText);
+				matchedElements.forEach(element => addPseudoRule(element, ruleData));
+			} else {
+				matchedElements.forEach(element => addRule(element, ruleData));
 			}
-		} else {
-			elementInfo.push({ cssRule, specificity, selectorText, selectorsText });
 		}
 	}
 
-	function getInfo(element, cssRule, selector, mediaInfo, ruleIndex, sheetIndex) {
-		let elementInfo = mediaInfo.elements.get(element);
+	function addRule(element, ruleData) {
+		let elementInfo = ruleData.mediaInfo.elements.get(element);
 		if (!elementInfo) {
 			elementInfo = [];
 			const elementStyle = element.style;
 			if (elementStyle && elementStyle.length) {
 				elementInfo.push({ cssStyle: elementStyle });
 			}
-			mediaInfo.elements.set(element, elementInfo);
+			ruleData.mediaInfo.elements.set(element, elementInfo);
 		}
-		const specificity = computeSpecificity(selector);
-		specificity.ruleIndex = ruleIndex;
-		specificity.sheetIndex = sheetIndex;
-		let ruleInfo;
-		if (elementInfo.length) {
-			let elementRuleIndex = 0, elementRulesLength = elementInfo.length, cssRuleFound;
-			while (elementRuleIndex < elementRulesLength && !(cssRuleFound = elementInfo[elementRuleIndex].cssRule == cssRule)) {
-				elementRuleIndex++;
-			}
-			if (elementRuleIndex < elementRulesLength && cssRuleFound) {
-				ruleInfo = elementInfo[elementRuleIndex];
-			}
+		const specificity = computeSpecificity(ruleData.selector);
+		specificity.ruleIndex = ruleData.ruleIndex;
+		specificity.sheetIndex = ruleData.sheetIndex;
+		ruleData.specificity = specificity;
+		elementInfo.push(ruleData);
+	}
+
+	function addPseudoRule(element, ruleData) {
+		let elementInfo = ruleData.mediaInfo.pseudos.get(element);
+		if (!elementInfo) {
+			elementInfo = [];
+			ruleData.mediaInfo.pseudos.set(element, elementInfo);
 		}
-		return { elementInfo, ruleInfo, specificity };
+		elementInfo.push(ruleData);
 	}
 
 	function computeCascade(mediaInfo, parentMediaInfos, mediaAllInfo) {
@@ -232,7 +231,7 @@ this.RulesMatcher = this.RulesMatcher || (() => {
 	}
 
 	function createMediaInfo(media) {
-		const mediaInfo = { media: media, elements: new Map(), medias: new Map(), rules: new Map() };
+		const mediaInfo = { media: media, elements: new Map(), pseudos: new Map(), medias: new Map(), rules: new Map(), pseudoSelectors: new Set() };
 		if (media == MEDIA_ALL) {
 			mediaInfo.styles = new Map();
 		}
@@ -295,6 +294,53 @@ this.RulesMatcher = this.RulesMatcher || (() => {
 		}
 	}
 
+	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" || !PSEUDOS_CLASSES.includes(":" + token.name))
+					&& (token.type != "pseudo-element"));
+				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;
+		}
+	}
+
+	function containsPseudo(selectorText) {
+		let ignoredPseudo;
+		if (selectorText.includes("::")) {
+			ignoredPseudo = true;
+		} else {
+			let pseudoIndex = 0;
+			while (pseudoIndex < PSEUDOS_CLASSES.length && !ignoredPseudo) {
+				ignoredPseudo = selectorText.includes(PSEUDOS_CLASSES[pseudoIndex]);
+				if (!ignoredPseudo) {
+					pseudoIndex++;
+				}
+			}
+		}
+		return ignoredPseudo;
+	}
+
 	function log(...args) {
 		console.log("S-File <css-mat>", ...args); // eslint-disable-line no-console
 	}

+ 4 - 18
lib/single-file/css-rules-minifier.js

@@ -22,10 +22,6 @@
 
 this.cssMinifier = this.cssMinifier || (() => {
 
-	const FILTERED_PSEUDO_CLASSES = [":focus", ":focus-within", ":hover", ":link", ":visited", ":active"];
-	const FILTERED_PSEUDO_ELEMENTS = ["::after", "::before", "::first-line", "::first-letter", "::placeholder", "::-webkit-input-placeholder", "::selection", "::marker", "::cue", "::-webkit-progress-bar", "::-webkit-progress-value", "::-webkit-inner-spin-button", "::-webkit-outer-spin-button", "::-webkit-search-cancel-button", "::-webkit-search-cancel-button"];
-	const FILTERED_SELECTORS = ["::-webkit-scrollbar", "::-webkit-scrollbar-button", "::-webkit-scrollbar-thumb", "::-webkit-scrollbar-track", "::-webkit-scrollbar-track-piece", "::-webkit-scrollbar-corner", "::-webkit-resizer"];
-	const IGNORED_SELECTORS = FILTERED_SELECTORS.concat(FILTERED_PSEUDO_CLASSES).concat(FILTERED_PSEUDO_ELEMENTS);
 	const DEBUG = false;
 
 	return {
@@ -82,8 +78,10 @@ this.cssMinifier = this.cssMinifier || (() => {
 				sheetContent += "}";
 			} else if (cssRule.type == CSSRule.STYLE_RULE) {
 				const ruleInfo = mediaInfo.rules.get(cssRule);
-				if (ruleInfo || testIgnoredSelector(cssRule.selectorText)) {
+				if (ruleInfo) {
 					sheetContent += processRuleInfo(cssRule, ruleInfo);
+				} else if (mediaInfo.pseudoSelectors.has(cssRule.selectorText)) {
+					sheetContent += cssRule.cssText;
 				}
 			} else {
 				sheetContent += cssRule.cssText;
@@ -112,7 +110,7 @@ this.cssMinifier = this.cssMinifier || (() => {
 			if (ruleInfo.matchedSelectors.size < ruleInfo.selectorsText.length) {
 				for (let selectorTextIndex = 0; selectorTextIndex < ruleInfo.selectorsText.length; selectorTextIndex++) {
 					const ruleSelectorText = ruleInfo.selectorsText[selectorTextIndex];
-					if (ruleInfo.matchedSelectors.has(ruleSelectorText) || testIgnoredSelector(ruleSelectorText)) {
+					if (ruleInfo.matchedSelectors.has(ruleSelectorText)) {
 						if (selectorText) {
 							selectorText += ",";
 						}
@@ -143,18 +141,6 @@ this.cssMinifier = this.cssMinifier || (() => {
 		return (styleCssText || cssStyle.cssText);
 	}
 
-	function testIgnoredSelector(selectorText) {
-		let indexSelector = 0, found;
-		selectorText = selectorText.toLowerCase();
-		while (indexSelector < IGNORED_SELECTORS.length && !found) {
-			found = selectorText.includes(IGNORED_SELECTORS[indexSelector]);
-			if (!found) {
-				indexSelector++;
-			}
-		}
-		return found;
-	}
-
 	function log(...args) {
 		console.log("S-File <css-min>", ...args); // eslint-disable-line no-console
 	}