Quellcode durchsuchen

handle same font family in multiple rules

Gildas vor 7 Jahren
Ursprung
Commit
b5c8dc7351
1 geänderte Dateien mit 119 neuen und 79 gelöschten Zeilen
  1. 119 79
      lib/single-file/css-fonts-minifier.js

+ 119 - 79
lib/single-file/css-fonts-minifier.js

@@ -37,10 +37,15 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 	const REGEXP_FONT_FORMAT = /\.([^.?#]+)((\?|#).*?)?$/;
 	const REGEXP_FONT_FORMAT_VALUE = /format\((.*?)\)\s*,?$/;
 	const REGEXP_FONT_SRC = /(.*?)\s*,?$/;
+	const FONT_WEIGHTS = {
+		normal: 400,
+		bold: 700
+	};
 
 	return {
 		process: (doc, secondPass) => {
 			const declaredFonts = new Set();
+			const fontsDetails = new Map();
 			const usedFonts = [];
 			const stats = {
 				rules: {
@@ -56,8 +61,9 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 				if (style.sheet) {
 					const processedRules = style.sheet.cssRules.length;
 					stats.rules.processed += processedRules;
-					style.textContent = processRules(doc, style.sheet.cssRules, declaredFonts, usedFonts, stats, secondPass);
-					stats.rules.discarded += processedRules - style.sheet.cssRules.length;
+					stats.rules.discarded += processedRules;
+					processRules(doc, style.sheet.cssRules, fontsDetails, declaredFonts, usedFonts, secondPass);
+					style.textContent = processFontFaceRules(style.sheet.cssRules, fontsDetails, "all", stats);
 				}
 			});
 			doc.querySelectorAll("[style]").forEach(element => {
@@ -74,120 +80,95 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 				const filteredUsedFonts = new Set(usedFonts.map(fontNames => fontNames.find(fontName => declaredFonts.has(fontName))).filter(fontName => fontName));
 				unusedFonts = Array.from(declaredFonts).filter(fontFamilyName => !filteredUsedFonts.has(fontFamilyName));
 			}
-			if (unusedFonts.length) {
-				doc.querySelectorAll("style").forEach(style => {
-					if (style.sheet) {
-						const processedRules = style.sheet.cssRules.length;
-						style.textContent = deleteUnusedFonts(doc, style.sheet.cssRules, unusedFonts);
-						stats.rules.discarded += processedRules - style.sheet.cssRules.length;
-					}
-				});
-			}
+			doc.querySelectorAll("style").forEach(style => {
+				if (style.sheet) {
+					style.textContent = deleteUnusedFonts(doc, style.sheet.cssRules, unusedFonts);
+					stats.rules.discarded -= style.sheet.cssRules.length;
+				}
+			});
 			return stats;
 		}
 	};
 
-	function processRules(doc, rules, declaredFonts, usedFonts, stats, secondPass) {
-		let stylesheetContent = "";
+	function processRules(doc, rules, fontsDetails, declaredFonts, usedFonts, secondPass) {
 		if (rules) {
 			Array.from(rules).forEach(rule => {
 				if (rule.type == CSSRule.MEDIA_RULE) {
-					stylesheetContent += "@media " + Array.prototype.join.call(rule.media, ",") + " {";
-					stylesheetContent += processRules(doc, rule.cssRules, declaredFonts, usedFonts, stats, secondPass);
-					stylesheetContent += "}";
+					processRules(doc, rule.cssRules, fontsDetails, declaredFonts, usedFonts, secondPass);
 				} else if (rule.type == CSSRule.STYLE_RULE) {
 					if (rule.style && rule.style.fontFamily) {
 						const fontFamilyNames = rule.style.fontFamily.split(",").map(fontFamilyName => getFontFamilyName(fontFamilyName));
 						usedFonts.push(fontFamilyNames);
 					}
-					stylesheetContent += rule.cssText;
 				} else {
-					let cssText = rule.cssText;
 					if (rule.type == CSSRule.FONT_FACE_RULE && rule.style) {
-						const fontFamilyName = rule.style.getPropertyValue("font-family");
+						const fontFamilyName = getFontFamilyName(rule.style.getPropertyValue("font-family"));
 						if (fontFamilyName) {
-							declaredFonts.add(getFontFamilyName(fontFamilyName));
+							declaredFonts.add(fontFamilyName);
 						}
-						const src = rule.style.getPropertyValue("src");
-						if (src) {
-							const fontSources = src.match(REGEXP_URL_FUNCTION);
-							if (fontSources) {
-								if (secondPass || testUnicodeRange(doc, rule)) {
-									cssText = processFontFaceRule(rule, fontSources, stats);
-								} else {
-									cssText = "";
+						if (secondPass || testUnicodeRange(doc, rule)) {
+							const fontKey = getFontKey(rule.style);
+							let fontInfo = fontsDetails.get(fontKey);
+							if (!fontInfo) {
+								fontInfo = new Set();
+								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.add(source));
 								}
 							}
 						}
 					}
-					stylesheetContent += cssText;
 				}
 			});
 		}
-		return stylesheetContent;
 	}
 
-	function testUnicodeRange(doc, rule) {
-		const unicodeRange = rule.style.getPropertyValue("unicode-range");
-		const docContent = doc.body.outerText;
-		if (unicodeRange) {
-			const unicodeRanges = unicodeRange.split(REGEXP_COMMA);
-			const result = unicodeRanges.filter(rangeValue => {
-				const range = rangeValue.split(REGEXP_DASH);
-				if (range.length == 2) {
-					range[0] = transformRange(range[0]);
-					const regExpString = "[" + range[0] + "-" + transformRange("U+" + range[1]) + "]";
-					return (new RegExp(regExpString, "u")).test(docContent);
-				}
-				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);
-
-					} else {
-						const regExpString = "[" + transformRange(range[0]) + "]";
-						return (new RegExp(regExpString, "u")).test(docContent);
-					}
+	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, stats);
+				stylesheetContent += "}";
+			} else if (rule.type == CSSRule.FONT_FACE_RULE && (media.includes("all") || media.includes("screen"))) {
+				const fontInfo = fontsDetails.get(getFontKey(rule.style));
+				if (fontInfo) {
+					fontsDetails.delete(getFontKey(rule.style));
+					stylesheetContent += "@font-face {" + processFontFaceRule(rule, fontInfo, stats) + "}";
 				}
-				return true;
-			});
-			return result.length;
-		}
-		return true;
-	}
-
-	function transformRange(range) {
-		range = range.replace(REGEXP_STARTS_U_PLUS, "");
-		while (range.length < 6) {
-			range = "0" + range;
-		}
-		return "\\u{" + range + "}";
+			} else {
+				stylesheetContent += rule.cssText;
+			}
+		});
+		return stylesheetContent;
 	}
 
-	function processFontFaceRule(rule, fontSources, stats) {
-		fontSources = fontSources.map(fontSrc => {
-			const fontFormatMatch = fontSrc.match(REGEXP_FONT_FORMAT_VALUE);
+	function processFontFaceRule(rule, fontInfo, stats) {
+		let fontSources = Array.from(fontInfo).map(fontSource => {
+			const fontFormatMatch = fontSource.match(REGEXP_FONT_FORMAT_VALUE);
 			let fontFormat;
 			if (fontFormatMatch && fontFormatMatch[1]) {
 				fontFormat = fontFormatMatch[1].replace(REGEXP_SIMPLE_QUOTES_STRING, "$1").replace(REGEXP_DOUBLE_QUOTES_STRING, "$1").toLowerCase();
 			}
 			if (!fontFormat) {
-				const fontFormatMatch = fontSrc.match(REGEXP_URL_FUNCTION_WOFF);
+				const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF);
 				if (fontFormatMatch && fontFormatMatch[1]) {
 					fontFormat = fontFormatMatch[1];
 				} else {
-					const fontFormatMatch = fontSrc.match(REGEXP_URL_FUNCTION_WOFF_ALT);
+					const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF_ALT);
 					if (fontFormatMatch && fontFormatMatch[1]) {
 						fontFormat = fontFormatMatch[1];
 					}
 				}
 			}
 			if (!fontFormat) {
-				const urlMatch = fontSrc.match(REGEXP_URL_SIMPLE_QUOTES_FN) ||
-					fontSrc.match(REGEXP_URL_DOUBLE_QUOTES_FN) ||
-					fontSrc.match(REGEXP_URL_NO_QUOTES_FN);
+				const urlMatch = fontSource.match(REGEXP_URL_SIMPLE_QUOTES_FN) ||
+					fontSource.match(REGEXP_URL_DOUBLE_QUOTES_FN) ||
+					fontSource.match(REGEXP_URL_NO_QUOTES_FN);
 				const fontUrl = urlMatch && urlMatch[1];
 				if (fontUrl) {
 					const fontFormatMatch = fontUrl.match(REGEXP_FONT_FORMAT);
@@ -196,7 +177,7 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 					}
 				}
 			}
-			return { src: fontSrc.match(REGEXP_FONT_SRC)[1], format: fontFormat };
+			return { src: fontSource.match(REGEXP_FONT_SRC)[1], format: fontFormat };
 		});
 		const fontTest = (fontSource, format) => fontSource.format == format;
 		const woff2FontFound = fontSources.find(fontSource => fontTest(fontSource, "woff2"));
@@ -212,15 +193,17 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 			}
 		}
 		stats.fonts.processed += stats.fonts.processed - fontSources.length;
-		const cssStyles = [];
+		let cssText = "";
 		Array.from(rule.style).forEach(propertyName => {
+			cssText += propertyName + ":";
 			if (propertyName == "src") {
-				cssStyles.push("src:" + fontSources.map(fontSource => fontSource.src).join(","));
+				cssText += fontSources.map(fontSource => fontSource.src).join(",");
 			} else {
-				cssStyles.push(propertyName + ":" + rule.style.getPropertyValue(propertyName));
+				cssText += rule.style.getPropertyValue(propertyName);
 			}
+			cssText += ";";
 		});
-		return "@font-face{" + cssStyles.join(";") + "}";
+		return cssText;
 	}
 
 	function deleteUnusedFonts(doc, rules, unusedFonts) {
@@ -240,6 +223,59 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 		return stylesheetContent;
 	}
 
+	function testUnicodeRange(doc, rule) {
+		const unicodeRange = rule.style.getPropertyValue("unicode-range");
+		const docContent = doc.body.outerText;
+		if (unicodeRange) {
+			const unicodeRanges = unicodeRange.split(REGEXP_COMMA);
+			const result = unicodeRanges.filter(rangeValue => {
+				const range = rangeValue.split(REGEXP_DASH);
+				if (range.length == 2) {
+					range[0] = transformRange(range[0]);
+					const regExpString = "[" + range[0] + "-" + transformRange("U+" + range[1]) + "]";
+					return (new RegExp(regExpString, "u")).test(docContent);
+				}
+				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);
+
+					} else {
+						const regExpString = "[" + transformRange(range[0]) + "]";
+						return (new RegExp(regExpString, "u")).test(docContent);
+					}
+				}
+				return true;
+			});
+			return result.length;
+		}
+		return true;
+	}
+
+	function transformRange(range) {
+		range = range.replace(REGEXP_STARTS_U_PLUS, "");
+		while (range.length < 6) {
+			range = "0" + range;
+		}
+		return "\\u{" + range + "}";
+	}
+
+	function getFontKey(style) {
+		return JSON.stringify([
+			getFontFamilyName(style.getPropertyValue("font-family")),
+			getFontWeight(style.getPropertyValue("font-weight")),
+			style.getPropertyValue("font-style"),
+			style.getPropertyValue("unicode-range"),
+			style.getPropertyValue("font-display"),
+			style.getPropertyValue("font-stretch"),
+			style.getPropertyValue("font-variant"),
+			style.getPropertyValue("font-feature-settings"),
+			style.getPropertyValue("font-variation-settings")
+		]);
+	}
+
 	function getFontFamilyName(fontFamilyName) {
 		fontFamilyName = fontFamilyName.toLowerCase().trim();
 		if (fontFamilyName.match(REGEXP_SIMPLE_QUOTES_STRING)) {
@@ -250,4 +286,8 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 		return fontFamilyName.trim();
 	}
 
+	function getFontWeight(weight) {
+		return FONT_WEIGHTS[weight] || weight;
+	}
+
 })();