Gildas 7 лет назад
Родитель
Сommit
1d1cba4629

+ 1 - 1
extension/core/bg/script-loader.js

@@ -56,7 +56,7 @@ singlefile.scriptLoader = (() => {
 			"/lib/single-file/vendor/css-minifier.js"
 		],
 		removeAlternativeFonts: [
-			"/lib/single-file/css-fonts-minifier.js"
+			"/lib/single-file/css-fonts-alt-minifier.js"
 		],
 		removeAlternativeMedias: [
 			"/lib/single-file/vendor/css-media-query-parser.js",

+ 229 - 0
lib/single-file/css-fonts-alt-minifier.js

@@ -0,0 +1,229 @@
+/*
+ * Copyright 2018 Gildas Lormeau
+ * contact : gildas.lormeau <at> gmail.com
+ * 
+ * This file is part of SingleFile.
+ *
+ *   SingleFile is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU Lesser General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   SingleFile is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU Lesser General Public License for more details.
+ *
+ *   You should have received a copy of the GNU Lesser General Public License
+ *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* global cssTree */
+
+this.fontsAltMinifier = this.fontsAltMinifier || (() => {
+
+	const REGEXP_URL_SIMPLE_QUOTES_FN = /url\s*\(\s*'(.*?)'\s*\)/i;
+	const REGEXP_URL_DOUBLE_QUOTES_FN = /url\s*\(\s*"(.*?)"\s*\)/i;
+	const REGEXP_URL_NO_QUOTES_FN = /url\s*\(\s*(.*?)\s*\)/i;
+	const REGEXP_URL_FUNCTION = /(url|local)\(.*?\)\s*(,|$)/g;
+	const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
+	const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
+	const REGEXP_URL_FUNCTION_WOFF = /^url\(\s*["']?data:font\/(woff2?)/;
+	const REGEXP_URL_FUNCTION_WOFF_ALT = /^url\(\s*["']?data:application\/x-font-(woff)/;
+	const REGEXP_FONT_FORMAT = /\.([^.?#]+)((\?|#).*?)?$/;
+	const REGEXP_FONT_FORMAT_VALUE = /format\((.*?)\)\s*,?$/;
+	const REGEXP_FONT_SRC = /(.*?)\s*,?$/;
+	const EMPTY_URL_SOURCE = "url(\"data:base64,\")";
+	const LOCAL_SOURCE = "local(";
+	const FONT_WEIGHTS = {
+		normal: "400",
+		bold: "700"
+	};
+	const FONT_STRETCHES = {
+		"ultra-condensed": "50%",
+		"extra-condensed": "62.5%",
+		"condensed": "75%",
+		"semi-condensed": "87.5%",
+		"normal": "100%",
+		"semi-expanded": "112.5%",
+		"expanded": "125%",
+		"extra-expanded": "150%",
+		"ultra-expanded": "200%"
+	};
+
+	return {
+		process: (doc, stylesheets) => {
+			const fontsDetails = new Map();
+			const stats = { rules: { processed: 0, discarded: 0 }, fonts: { processed: 0, discarded: 0 } };
+			stylesheets.forEach(stylesheetInfo => {
+				const cssRules = stylesheetInfo.stylesheet.children;
+				stats.rules.processed += cssRules.getSize();
+				stats.rules.discarded += cssRules.getSize();
+				getFontsDetails(doc, cssRules, fontsDetails);
+			});
+			processFontDetails(fontsDetails);
+			stylesheets.forEach(stylesheetInfo => {
+				const cssRules = stylesheetInfo.stylesheet.children;
+				processFontFaceRules(cssRules, fontsDetails, "all", stats);
+				stats.rules.discarded -= cssRules.getSize();
+			});
+			return stats;
+		}
+	};
+
+	function processFontDetails(fontsDetails) {
+		fontsDetails.forEach((fontInfo, fontKey) => {
+			fontsDetails.set(fontKey, fontInfo.map(fontSource => {
+				const fontFormatMatch = fontSource.match(REGEXP_FONT_FORMAT_VALUE);
+				let fontFormat;
+				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 (fontFormatMatch && fontFormatMatch[1]) {
+					fontFormat = fontFormatMatch[1].replace(REGEXP_SIMPLE_QUOTES_STRING, "$1").replace(REGEXP_DOUBLE_QUOTES_STRING, "$1").toLowerCase();
+				}
+				if (!fontFormat) {
+					const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF);
+					if (fontFormatMatch && fontFormatMatch[1]) {
+						fontFormat = fontFormatMatch[1];
+					} else {
+						const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF_ALT);
+						if (fontFormatMatch && fontFormatMatch[1]) {
+							fontFormat = fontFormatMatch[1];
+						}
+					}
+				}
+				if (!fontFormat && fontUrl) {
+					const fontFormatMatch = fontUrl.match(REGEXP_FONT_FORMAT);
+					if (fontFormatMatch && fontFormatMatch[1]) {
+						fontFormat = fontFormatMatch[1];
+					}
+				}
+				return { src: fontSource.match(REGEXP_FONT_SRC)[1], fontUrl, format: fontFormat };
+			}));
+		});
+	}
+
+	function processFontFaceRules(cssRules, fontsDetails, media, stats) {
+		const removedRules = [];
+		for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
+			const ruleData = cssRule.data;
+			if (ruleData.type == "Atrule" && ruleData.name == "media" && ruleData.block && ruleData.prelude && ruleData.prelude.children) {
+				const mediaText = cssTree.generate(ruleData.prelude);
+				processFontFaceRules(ruleData.block.children, fontsDetails, mediaText, stats);
+			} else if (ruleData.type == "Atrule" && ruleData.name == "font-face" && (media.includes("all") || media.includes("screen"))) {
+				const fontInfo = fontsDetails.get(getFontKey(ruleData));
+				if (fontInfo) {
+					fontsDetails.delete(getFontKey(ruleData));
+					processFontFaceRule(ruleData, fontInfo, stats);
+				} else {
+					removedRules.push(cssRule);
+				}
+			}
+		}
+		removedRules.forEach(cssRule => cssRules.remove(cssRule));
+	}
+
+	function processFontFaceRule(cssRule, fontInfo, stats) {
+		const findSource = fontFormat => fontInfo.find(source => source.src != EMPTY_URL_SOURCE && source.format == fontFormat);
+		const filterSource = fontSource => fontInfo.filter(source => source == fontSource || source.src.startsWith(LOCAL_SOURCE));
+		stats.fonts.processed += fontInfo.length;
+		stats.fonts.discarded += fontInfo.length;
+		const woffFontFound = findSource("woff2-variations") || findSource("woff2") || findSource("woff");
+		if (woffFontFound) {
+			fontInfo = filterSource(woffFontFound);
+		} else {
+			const ttfFontFound = findSource("truetype-variations") || findSource("truetype");
+			if (ttfFontFound) {
+				fontInfo = filterSource(ttfFontFound);
+			} else {
+				const otfFontFound = findSource("opentype") || findSource("embedded-opentype");
+				if (otfFontFound) {
+					fontInfo = filterSource(otfFontFound);
+				}
+			}
+		}
+		stats.fonts.discarded -= fontInfo.length;
+		const removedNodes = [];
+		for (let node = cssRule.block.children.head; node; node = node.next) {
+			if (node.data.property == "src") {
+				removedNodes.push(node);
+			}
+		}
+		removedNodes.pop();
+		removedNodes.forEach(node => cssRule.block.children.remove(node));
+		const srcDeclaration = cssRule.block.children.filter(node => node.property == "src").tail;
+		if (srcDeclaration) {
+			fontInfo.reverse();
+			srcDeclaration.data.value = cssTree.parse(fontInfo.map(fontSource => fontSource.src).join(","), { context: "value" });
+		}
+	}
+
+	function getPropertyValue(cssRule, propertyName) {
+		const property = cssRule.block.children.filter(node => node.property == propertyName).tail;
+		if (property) {
+			try {
+				return cssTree.generate(property.data.value);
+			} catch (error) {
+				// ignored
+			}
+		}
+	}
+
+	function getFontsDetails(doc, cssRules, fontsDetails) {
+		cssRules.forEach(cssRule => {
+			if (cssRule.type == "Atrule" && cssRule.name == "media" && cssRule.block) {
+				getFontsDetails(doc, cssRule.block.children, fontsDetails);
+			} else {
+				if (cssRule.type == "Atrule" && cssRule.name == "font-face") {
+					const fontKey = getFontKey(cssRule);
+					let fontInfo = fontsDetails.get(fontKey);
+					if (!fontInfo) {
+						fontInfo = [];
+						fontsDetails.set(fontKey, fontInfo);
+					}
+					const src = getPropertyValue(cssRule, "src");
+					if (src) {
+						const fontSources = src.match(REGEXP_URL_FUNCTION);
+						if (fontSources) {
+							fontSources.forEach(source => fontInfo.unshift(source));
+						}
+					}
+				}
+			}
+		});
+	}
+
+	function getFontKey(cssRule) {
+		return JSON.stringify([
+			getFontFamily(getPropertyValue(cssRule, "font-family")),
+			getFontWeight(getPropertyValue(cssRule, "font-weight") || "400"),
+			getPropertyValue(cssRule, "font-style") || "normal",
+			getPropertyValue(cssRule, "unicode-range"),
+			getFontStretch(getPropertyValue(cssRule, "font-stretch")),
+			getPropertyValue(cssRule, "font-variant") || "normal",
+			getPropertyValue(cssRule, "font-feature-settings"),
+			getPropertyValue(cssRule, "font-variation-settings")
+		]);
+	}
+
+	function getFontFamily(string = "") {
+		string = string.toLowerCase().trim();
+		if (string.match(REGEXP_SIMPLE_QUOTES_STRING)) {
+			string = string.replace(REGEXP_SIMPLE_QUOTES_STRING, "$1");
+		} else {
+			string = string.replace(REGEXP_DOUBLE_QUOTES_STRING, "$1");
+		}
+		return string.trim();
+	}
+
+	function getFontWeight(weight) {
+		return FONT_WEIGHTS[weight] || weight;
+	}
+
+	function getFontStretch(stretch) {
+		return FONT_STRETCHES[stretch] || stretch;
+	}
+
+})();

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

@@ -22,42 +22,20 @@
 
 this.fontsMinifier = this.fontsMinifier || (() => {
 
-	const REGEXP_URL_SIMPLE_QUOTES_FN = /url\s*\(\s*'(.*?)'\s*\)/i;
-	const REGEXP_URL_DOUBLE_QUOTES_FN = /url\s*\(\s*"(.*?)"\s*\)/i;
-	const REGEXP_URL_NO_QUOTES_FN = /url\s*\(\s*(.*?)\s*\)/i;
-	const REGEXP_URL_FUNCTION = /(url|local)\(.*?\)\s*(,|$)/g;
 	const REGEXP_COMMA = /\s*,\s*/;
 	const REGEXP_DASH = /-/;
 	const REGEXP_QUESTION_MARK = /\?/g;
 	const REGEXP_STARTS_U_PLUS = /^U\+/i;
 	const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
 	const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
-	const REGEXP_URL_FUNCTION_WOFF = /^url\(\s*["']?data:font\/(woff2?)/;
-	const REGEXP_URL_FUNCTION_WOFF_ALT = /^url\(\s*["']?data:application\/x-font-(woff)/;
-	const REGEXP_FONT_FORMAT = /\.([^.?#]+)((\?|#).*?)?$/;
-	const REGEXP_FONT_FORMAT_VALUE = /format\((.*?)\)\s*,?$/;
-	const REGEXP_FONT_SRC = /(.*?)\s*,?$/;
-	const EMPTY_URL_SOURCE = "url(\"data:base64,\")";
-	const LOCAL_SOURCE = "local(";
 	const PSEUDO_ELEMENTS = ["::after", "::before", "::first-line", "::first-letter", ":before", ":after", ":first-line", ":first-letter", "::placeholder", "::selection", "::marker", "::cue", "::slotted", "::spelling-error", "::grammar-error"];
 	const FONT_WEIGHTS = {
 		normal: "400",
 		bold: "700"
 	};
-	const FONT_STRETCHES = {
-		"ultra-condensed": "50%",
-		"extra-condensed": "62.5%",
-		"condensed": "75%",
-		"semi-condensed": "87.5%",
-		"normal": "100%",
-		"semi-expanded": "112.5%",
-		"expanded": "125%",
-		"extra-expanded": "150%",
-		"ultra-expanded": "200%"
-	};
 
 	return {
-		removeUnusedFonts: (doc, stylesheets, styles, options) => {
+		process: (doc, stylesheets, styles, options) => {
 			const stats = { rules: { processed: 0, discarded: 0 }, fonts: { processed: 0, discarded: 0 } };
 			const fontsInfo = { declared: [], used: [] };
 			let pseudoElementsContent = "";
@@ -95,60 +73,9 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 				stats.rules.discarded -= cssRules.getSize();
 			});
 			return stats;
-		},
-		removeAlternativeFonts: (doc, stylesheets) => {
-			const fontsDetails = new Map();
-			const stats = { rules: { processed: 0, discarded: 0 }, fonts: { processed: 0, discarded: 0 } };
-			stylesheets.forEach(stylesheetInfo => {
-				const cssRules = stylesheetInfo.stylesheet.children;
-				stats.rules.processed += cssRules.getSize();
-				stats.rules.discarded += cssRules.getSize();
-				getFontsDetails(doc, cssRules, fontsDetails);
-			});
-			processFontDetails(fontsDetails);
-			stylesheets.forEach(stylesheetInfo => {
-				const cssRules = stylesheetInfo.stylesheet.children;
-				processFontFaceRules(cssRules, fontsDetails, "all", stats);
-				stats.rules.discarded -= cssRules.getSize();
-			});
-			return stats;
 		}
 	};
 
-	function processFontDetails(fontsDetails) {
-		fontsDetails.forEach((fontInfo, fontKey) => {
-			fontsDetails.set(fontKey, fontInfo.map(fontSource => {
-				const fontFormatMatch = fontSource.match(REGEXP_FONT_FORMAT_VALUE);
-				let fontFormat;
-				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 (fontFormatMatch && fontFormatMatch[1]) {
-					fontFormat = fontFormatMatch[1].replace(REGEXP_SIMPLE_QUOTES_STRING, "$1").replace(REGEXP_DOUBLE_QUOTES_STRING, "$1").toLowerCase();
-				}
-				if (!fontFormat) {
-					const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF);
-					if (fontFormatMatch && fontFormatMatch[1]) {
-						fontFormat = fontFormatMatch[1];
-					} else {
-						const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF_ALT);
-						if (fontFormatMatch && fontFormatMatch[1]) {
-							fontFormat = fontFormatMatch[1];
-						}
-					}
-				}
-				if (!fontFormat && fontUrl) {
-					const fontFormatMatch = fontUrl.match(REGEXP_FONT_FORMAT);
-					if (fontFormatMatch && fontFormatMatch[1]) {
-						fontFormat = fontFormatMatch[1];
-					}
-				}
-				return { src: fontSource.match(REGEXP_FONT_SRC)[1], fontUrl, format: fontFormat };
-			}));
-		});
-	}
-
 	function getFontsInfo(cssRules, fontsInfo) {
 		cssRules.forEach(cssRule => {
 			if (cssRule.type == "Atrule" && cssRule.name == "media" && cssRule.block) {
@@ -210,61 +137,6 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 		return test;
 	}
 
-	function processFontFaceRules(cssRules, fontsDetails, media, stats) {
-		const removedRules = [];
-		for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
-			const ruleData = cssRule.data;
-			if (ruleData.type == "Atrule" && ruleData.name == "media" && ruleData.block && ruleData.prelude && ruleData.prelude.children) {
-				const mediaText = cssTree.generate(ruleData.prelude);
-				processFontFaceRules(ruleData.block.children, fontsDetails, mediaText, stats);
-			} else if (ruleData.type == "Atrule" && ruleData.name == "font-face" && (media.includes("all") || media.includes("screen"))) {
-				const fontInfo = fontsDetails.get(getFontKey(ruleData));
-				if (fontInfo) {
-					fontsDetails.delete(getFontKey(ruleData));
-					processFontFaceRule(ruleData, fontInfo, stats);
-				} else {
-					removedRules.push(cssRule);
-				}
-			}
-		}
-		removedRules.forEach(cssRule => cssRules.remove(cssRule));
-	}
-
-	function processFontFaceRule(cssRule, fontInfo, stats) {
-		const findSource = fontFormat => fontInfo.find(source => source.src != EMPTY_URL_SOURCE && source.format == fontFormat);
-		const filterSource = fontSource => fontInfo.filter(source => source == fontSource || source.src.startsWith(LOCAL_SOURCE));
-		stats.fonts.processed += fontInfo.length;
-		stats.fonts.discarded += fontInfo.length;
-		const woffFontFound = findSource("woff2-variations") || findSource("woff2") || findSource("woff");
-		if (woffFontFound) {
-			fontInfo = filterSource(woffFontFound);
-		} else {
-			const ttfFontFound = findSource("truetype-variations") || findSource("truetype");
-			if (ttfFontFound) {
-				fontInfo = filterSource(ttfFontFound);
-			} else {
-				const otfFontFound = findSource("opentype") || findSource("embedded-opentype");
-				if (otfFontFound) {
-					fontInfo = filterSource(otfFontFound);
-				}
-			}
-		}
-		stats.fonts.discarded -= fontInfo.length;
-		const removedNodes = [];
-		for (let node = cssRule.block.children.head; node; node = node.next) {
-			if (node.data.property == "src") {
-				removedNodes.push(node);
-			}
-		}
-		removedNodes.pop();
-		removedNodes.forEach(node => cssRule.block.children.remove(node));
-		const srcDeclaration = cssRule.block.children.filter(node => node.property == "src").tail;
-		if (srcDeclaration) {
-			fontInfo.reverse();
-			srcDeclaration.data.value = cssTree.parse(fontInfo.map(fontSource => fontSource.src).join(","), { context: "value" });
-		}
-	}
-
 	function getPropertyValue(cssRule, propertyName) {
 		const property = cssRule.block.children.filter(node => node.property == propertyName).tail;
 		if (property) {
@@ -319,30 +191,6 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 		return fontFamilyNames;
 	}
 
-	function getFontsDetails(doc, cssRules, fontsDetails) {
-		cssRules.forEach(cssRule => {
-			if (cssRule.type == "Atrule" && cssRule.name == "media" && cssRule.block) {
-				getFontsDetails(doc, cssRule.block.children, fontsDetails);
-			} else {
-				if (cssRule.type == "Atrule" && cssRule.name == "font-face") {
-					const fontKey = getFontKey(cssRule);
-					let fontInfo = fontsDetails.get(fontKey);
-					if (!fontInfo) {
-						fontInfo = [];
-						fontsDetails.set(fontKey, fontInfo);
-					}
-					const src = getPropertyValue(cssRule, "src");
-					if (src) {
-						const fontSources = src.match(REGEXP_URL_FUNCTION);
-						if (fontSources) {
-							fontSources.forEach(source => fontInfo.unshift(source));
-						}
-					}
-				}
-			}
-		});
-	}
-
 	function findFontWeight(fontWeight, fontWeights) {
 		let foundWeight;
 		if (fontWeight >= 400 && fontWeight <= 500) {
@@ -460,19 +308,6 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 		return "\\u{" + range + "}";
 	}
 
-	function getFontKey(cssRule) {
-		return JSON.stringify([
-			getFontFamily(getPropertyValue(cssRule, "font-family")),
-			getFontWeight(getPropertyValue(cssRule, "font-weight") || "400"),
-			getPropertyValue(cssRule, "font-style") || "normal",
-			getPropertyValue(cssRule, "unicode-range"),
-			getFontStretch(getPropertyValue(cssRule, "font-stretch")),
-			getPropertyValue(cssRule, "font-variant") || "normal",
-			getPropertyValue(cssRule, "font-feature-settings"),
-			getPropertyValue(cssRule, "font-variation-settings")
-		]);
-	}
-
 	function getFontFamily(string = "") {
 		string = string.toLowerCase().trim();
 		if (string.match(REGEXP_SIMPLE_QUOTES_STRING)) {
@@ -487,8 +322,4 @@ this.fontsMinifier = this.fontsMinifier || (() => {
 		return FONT_WEIGHTS[weight] || weight;
 	}
 
-	function getFontStretch(stretch) {
-		return FONT_STRETCHES[stretch] || stretch;
-	}
-
 })();

+ 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, URL, setTimeout, TextDecoder, Blob, fetch, FileReader, superFetch, srcsetParser, cssMinifier, htmlMinifier, cssRulesMinifier, fontsMinifier, serializer, docHelper, mediasMinifier, TextEncoder, crypto, matchedRules, imagesMinifier, FontFace, cssTree */
+/* global SingleFileCore, DOMParser, URL, setTimeout, TextDecoder, Blob, fetch, FileReader, superFetch, srcsetParser, cssMinifier, htmlMinifier, cssRulesMinifier, fontsMinifier, fontsAltMinifier, serializer, docHelper, mediasMinifier, TextEncoder, crypto, matchedRules, imagesMinifier, FontFace, cssTree */
 
 this.SingleFile = this.SingleFile || (() => {
 
@@ -231,11 +231,11 @@ this.SingleFile = this.SingleFile || (() => {
 		}
 
 		static removeUnusedFonts(doc, stylesheets, styles, options) {
-			return fontsMinifier.removeUnusedFonts(doc, stylesheets, styles, options);
+			return fontsMinifier.process(doc, stylesheets, styles, options);
 		}
 
 		static removeAlternativeFonts(doc, stylesheets) {
-			return fontsMinifier.removeAlternativeFonts(doc, stylesheets);
+			return fontsAltMinifier.process(doc, stylesheets);
 		}
 
 		static getMediaAllInfo(doc, docStyle) {

+ 1 - 0
manifest.json

@@ -68,6 +68,7 @@
 			"lib/single-file/vendor/html-srcset-parser.js",
 			"lib/single-file/doc-helper.js",
 			"lib/single-file/css-fonts-minifier.js",
+			"lib/single-file/css-fonts-alt-minifier.js",
 			"lib/single-file/css-medias-minifier.js",
 			"lib/single-file/css-matched-rules.js",
 			"lib/single-file/css-rules-minifier.js",