瀏覽代碼

added "remove alternative fonts to woff" option

Gildas 7 年之前
父節點
當前提交
e5ebf574ca

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

@@ -28,6 +28,7 @@ singlefile.core = (() => {
 		"/extension/ui/content/ui.js",
 		"/lib/single-file/base64.js",
 		"/lib/single-file/uglifycss.js",
+		"/lib/single-file/fonts-minifier.js",
 		"/lib/single-file/rules-minifier.js",
 		"/lib/single-file/htmlmini.js",
 		"/lib/single-file/parse-srcset.js",

+ 5 - 1
extension/core/bg/config.js

@@ -46,7 +46,8 @@ singlefile.config = (() => {
 		autoSaveDelay: 1,
 		autoSaveLoad: false,
 		autoSaveUnload: false,
-		autoSaveLoadOrUnload: true
+		autoSaveLoadOrUnload: true,
+		removeAlternativeFonts: true
 	};
 
 	let pendingUpgradePromise;
@@ -121,6 +122,9 @@ singlefile.config = (() => {
 		if (config.autoSaveDelay === undefined) {
 			config.autoSaveDelay = 1;
 		}
+		if (config.removeAlternativeFonts === undefined) {
+			config.removeAlternativeFonts = true;
+		}		
 		if (config.autoSaveLoadOrUnload === undefined && !config.autoSaveUnload) {
 			config.autoSaveLoadOrUnload = true;
 			config.autoSaveLoad = false;

+ 4 - 1
extension/ui/bg/options.js

@@ -47,6 +47,7 @@
 	const autoSaveLoadInput = document.getElementById("autoSaveLoadInput");
 	const autoSaveUnloadInput = document.getElementById("autoSaveUnloadInput");
 	const autoSaveLoadOrUnloadInput = document.getElementById("autoSaveLoadOrUnloadInput");
+	const removeAlternativeFontsInput = document.getElementById("removeAlternativeFontsInput");
 	let pendingSave = Promise.resolve();
 	document.getElementById("resetButton").addEventListener("click", async () => {
 		await bgPage.singlefile.config.reset();
@@ -103,6 +104,7 @@
 		autoSaveUnloadInput.disabled = config.autoSaveUnloadDisabled;
 		autoSaveLoadInput.disabled = config.autoSaveLoadOrUnload;
 		autoSaveUnloadInput.disabled = config.autoSaveUnloadDisabled || config.autoSaveLoadOrUnload;
+		removeAlternativeFontsInput.checked = config.removeAlternativeFonts;
 	}
 
 	async function update() {
@@ -131,7 +133,8 @@
 			autoSaveDelay: autoSaveDelayInput.value,
 			autoSaveLoad: autoSaveLoadInput.checked,
 			autoSaveUnload: autoSaveUnloadInput.checked,
-			autoSaveLoadOrUnload: autoSaveLoadOrUnloadInput.checked
+			autoSaveLoadOrUnload: autoSaveLoadOrUnloadInput.checked,
+			removeAlternativeFonts: removeAlternativeFontsInput.checked
 		});
 		await pendingSave;
 		await bgPage.singlefile.ui.refreshContextMenu();

+ 8 - 0
extension/ui/pages/help.html

@@ -195,6 +195,14 @@
 							<u>check</u> this option</p>
 					</li>
 
+					<li>
+						<span class="option">remove alternative fonts to woff</span>
+						<p>Check this option to remove fonts that are alternatives to the Web Open Font Format. Checking this this option should
+							not alter the document for modern browsers and can considerably reduce the size of the file.</p>
+						<p class="notice">It is recommended to
+							<u>check</u> this option</p>
+					</li>
+
 					<li>
 						<span class="option">save lazy loaded images</span>
 						<p>Check this option to save all the lazy loaded images that are not displayed. This may help to save all the images without

+ 4 - 0
extension/ui/pages/options.html

@@ -80,6 +80,10 @@
 			<label for="removeUnusedCSSRulesInput">remove unused CSS rules</label>
 			<input type="checkbox" id="removeUnusedCSSRulesInput">
 		</div>
+		<div class="option">
+			<label for="removeAlternativeFontsInput">remove alternative fonts to woff</label>
+			<input type="checkbox" id="removeAlternativeFontsInput">
+		</div>
 		<div class="option">
 			<label for="lazyLoadImagesInput">save lazy loaded images</label>
 			<input type="checkbox" id="lazyLoadImagesInput">

+ 183 - 0
lib/single-file/fonts-minifier.js

@@ -0,0 +1,183 @@
+/*
+ * 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 */
+
+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;
+
+	return {
+		process: (doc, removeUnusedCSSRules) => {
+			const declaredFonts = new Set();
+			const usedFonts = new Set();
+			const stats = {
+				rules: {
+					processed: 0,
+					discarded: 0
+				},
+				fonts: {
+					processed: 0,
+					discarded: 0
+				}
+			};
+			doc.querySelectorAll("style").forEach(style => {
+				if (style.sheet) {
+					const processedRules = style.sheet.cssRules.length;
+					stats.rules.processed += processedRules;
+					style.textContent = processRules(doc, style.sheet.cssRules, declaredFonts, usedFonts, removeUnusedCSSRules, stats);
+					stats.rules.discarded += processedRules - style.sheet.cssRules.length;
+				}
+			});
+			if (removeUnusedCSSRules) {
+				doc.querySelectorAll("[style]").forEach(element => {
+					if (element.style.fontFamily) {
+						element.style.fontFamily.split(",").forEach(fontFamilyName => usedFonts.add(getFontFamilyName(fontFamilyName)));
+					}
+				});
+				const unusedFonts = Array.from(declaredFonts).filter(fontFamilyName => !usedFonts.has(fontFamilyName));
+				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;
+					}
+				});
+			}
+			return stats;
+		}
+	};
+
+	function processRules(doc, rules, declaredFonts, usedFonts, removeUnusedCSSRules, stats) {
+		let stylesheetContent = "";
+		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, removeUnusedCSSRules, stats);
+					stylesheetContent += "}";
+				} else if (removeUnusedCSSRules && rule.type == CSSRule.STYLE_RULE) {
+					if (rule.style && rule.style.fontFamily) {
+						rule.style.fontFamily.split(",").forEach(fontFamilyName => usedFonts.add(getFontFamilyName(fontFamilyName)));
+					}
+					stylesheetContent += rule.cssText;
+				} else {
+					let cssText = rule.cssText;
+					if (rule.type == CSSRule.FONT_FACE_RULE && rule.style) {
+						const fontFamilyName = rule.style.getPropertyValue("font-family");
+						if (fontFamilyName) {
+							declaredFonts.add(getFontFamilyName(fontFamilyName));
+						}
+						const src = rule.style.getPropertyValue("src");
+						if (src) {
+							const fontSources = src.match(/url\(.*?\)\s*(,|$)/g);
+							if (fontSources) {
+								cssText = processFontFaceRule(rule, fontSources, stats);
+							}
+						}
+					}
+					stylesheetContent += cssText;
+				}
+			});
+		}
+		return stylesheetContent;
+	}
+
+	function processFontFaceRule(rule, fontSources, stats) {
+		fontSources = fontSources.map(fontSrc => {
+			const fontFormatMatch = fontSrc.match(/format\((.*?)\)\s*,?$/);
+			let fontFormat;
+			if (fontFormatMatch && fontFormatMatch[1]) {
+				fontFormat = fontFormatMatch[1].replace(/^'(.*?)'$/, "$1").replace(/^"(.*?)"$/, "$1").toLowerCase();
+			}
+			if (!fontFormat) {
+				const fontFormatMatch = fontSrc.match(/^url\(\s*["']?data:font\/(woff2?)/);
+				if (fontFormatMatch && fontFormatMatch[1]) {
+					fontFormat = fontFormatMatch[1];
+				} else {
+					const fontFormatMatch = fontSrc.match(/^url\(\s*["']?data:application\/x-font-(woff)/);
+					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 fontUrl = urlMatch && urlMatch[1];
+				if (fontUrl) {
+					const fontFormatMatch = fontUrl.match(/\.([^.?#]+)((\?|#).*?)?$/);
+					if (fontFormatMatch && fontFormatMatch[1]) {
+						fontFormat = fontFormatMatch[1];
+					}
+				}
+			}
+			return { src: fontSrc.match(/(.*?)\s*,?$/)[1], format: fontFormat };
+		});
+		const fontTest = (fontSource, format) => fontSource.format == format;
+		const woff2FontFound = fontSources.find(fontSource => fontTest(fontSource, "woff2"));
+		const woffFontFound = fontSources.find(fontSource => fontTest(fontSource, "woff"));
+		stats.fonts.processed += fontSources.length;
+		if (woffFontFound || woff2FontFound) {
+			fontSources = fontSources.filter(fontSource => woff2FontFound ? fontTest(fontSource, "woff2") : fontTest(fontSource, "woff"));
+		}
+		stats.fonts.processed += stats.fonts.processed - fontSources.length;
+		const cssStyles = [];
+		Array.from(rule.style).forEach(propertyName => {
+			if (propertyName == "src") {
+				cssStyles.push("src:" + fontSources.map(fontSource => fontSource.src).join(","));
+			} else {
+				cssStyles.push(propertyName + ":" + rule.style.getPropertyValue(propertyName));
+			}
+		});
+		return "@font-face {" + cssStyles.join(";") + "}";
+	}
+
+	function deleteUnusedFonts(doc, rules, unusedFonts) {
+		let stylesheetContent = "";
+		if (rules) {
+			Array.from(rules).forEach(rule => {
+				const fontFamilyName = rule.style && rule.style.getPropertyValue("font-family");
+				if (rule.media) {
+					stylesheetContent += "@media " + Array.prototype.join.call(rule.media, ",") + " {";
+					stylesheetContent += deleteUnusedFonts(doc, rule.cssRules, unusedFonts);
+					stylesheetContent += "}";
+				} else if (rule.type != CSSRule.FONT_FACE_RULE || (rule.type == CSSRule.FONT_FACE_RULE && rule.style && fontFamilyName && !unusedFonts.includes(getFontFamilyName(fontFamilyName)))) {
+					stylesheetContent += rule.cssText;
+				}
+			});
+		}
+		return stylesheetContent;
+	}
+
+	function getFontFamilyName(fontFamilyName) {
+		fontFamilyName = fontFamilyName.toLowerCase().trim();
+		if (fontFamilyName.match(/^'(.*)'$/)) {
+			fontFamilyName = fontFamilyName.replace(/^'(.*)'$/, "$1");
+		} else {
+			fontFamilyName = fontFamilyName.replace(/^"(.*)"$/, "$1");
+		}
+		return fontFamilyName.trim();
+	}
+
+})();

+ 6 - 59
lib/single-file/rules-minifier.js

@@ -26,13 +26,7 @@ this.rulesMinifier = this.rulesMinifier || (() => {
 
 	return {
 		process: doc => {
-			const rulesData = {
-				selectors: new Set(),
-				fonts: {
-					declared: new Set(),
-					used: new Set()
-				}
-			};
+			const selectorsData = new Set();
 			const stats = {
 				processed: 0,
 				discarded: 0
@@ -41,20 +35,7 @@ this.rulesMinifier = this.rulesMinifier || (() => {
 				if (style.sheet) {
 					const processed = style.sheet.cssRules.length;
 					stats.processed += processed;
-					style.textContent = processRules(doc, style.sheet.cssRules, rulesData);
-					stats.discarded += processed - style.sheet.cssRules.length;
-				}
-			});
-			doc.querySelectorAll("[style]").forEach(element => {
-				if (element.style.fontFamily) {
-					element.style.fontFamily.split(",").forEach(fontFamily => rulesData.fonts.used.add(getFontFamily(fontFamily)));
-				}
-			});
-			const unusedFonts = Array.from(rulesData.fonts.declared).filter(fontFamily => !rulesData.fonts.used.has(fontFamily));
-			doc.querySelectorAll("style").forEach(style => {
-				if (style.sheet) {
-					const processed = style.sheet.cssRules.length;
-					style.textContent = deleteUnusedFonts(doc, style.sheet.cssRules, unusedFonts);
+					style.textContent = processRules(doc, style.sheet.cssRules, selectorsData);
 					stats.discarded += processed - style.sheet.cssRules.length;
 				}
 			});
@@ -62,51 +43,27 @@ this.rulesMinifier = this.rulesMinifier || (() => {
 		}
 	};
 
-	function processRules(doc, rules, rulesData) {
+	function processRules(doc, rules, selectorsData) {
 		let stylesheetContent = "";
 		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, rulesData);
+					stylesheetContent += processRules(doc, rule.cssRules, selectorsData);
 					stylesheetContent += "}";
 				} else if (rule.type == CSSRule.STYLE_RULE) {
 					const selector = getFilteredSelector(rule.selectorText);
 					if (selector) {
 						try {
-							if (rulesData.selectors.has(selector) || doc.querySelector(selector)) {
+							if (selectorsData.has(selector) || doc.querySelector(selector)) {
 								stylesheetContent += rule.cssText;
-								rulesData.selectors.add(selector);
-								if (rule.style && rule.style.fontFamily) {
-									rule.style.fontFamily.split(",").forEach(fontFamily => rulesData.fonts.used.add(getFontFamily(fontFamily)));
-								}
+								selectorsData.add(selector);
 							}
 						} catch (error) {
 							stylesheetContent += rule.cssText;
 						}
 					}
 				} else {
-					const fontFamily = rule.style && rule.style.getPropertyValue("font-family");
-					if (rule.type == CSSRule.FONT_FACE_RULE && rule.style && fontFamily) {
-						rulesData.fonts.declared.add(getFontFamily(fontFamily));
-					}
-					stylesheetContent += rule.cssText;
-				}
-			});
-		}
-		return stylesheetContent;
-	}
-
-	function deleteUnusedFonts(doc, rules, unusedFonts) {
-		let stylesheetContent = "";
-		if (rules) {
-			Array.from(rules).forEach(rule => {
-				const fontFamily = rule.style && rule.style.getPropertyValue("font-family");
-				if (rule.media) {
-					stylesheetContent += "@media " + Array.prototype.join.call(rule.media, ",") + " {";
-					stylesheetContent += deleteUnusedFonts(doc, rule.cssRules, unusedFonts);
-					stylesheetContent += "}";
-				} else if (rule.type != CSSRule.FONT_FACE_RULE || (rule.type == CSSRule.FONT_FACE_RULE && rule.style && fontFamily && !unusedFonts.includes(getFontFamily(fontFamily)))) {
 					stylesheetContent += rule.cssText;
 				}
 			});
@@ -135,14 +92,4 @@ this.rulesMinifier = this.rulesMinifier || (() => {
 		return selector;
 	}
 
-	function getFontFamily(fontFamily) {
-		fontFamily = fontFamily.toLowerCase().trim();
-		if (fontFamily.match(/^'(.*)'$/)) {
-			fontFamily = fontFamily.replace(/^'(.*)'$/, "$1");
-		} else {
-			fontFamily = fontFamily.replace(/^"(.*)"$/, "$1");
-		}
-		return fontFamily.trim();
-	}
-
 })();

+ 5 - 1
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, lazyLoader, serializer, docHelper */
+/* global SingleFileCore, DOMParser, TextDecoder, Blob, fetch, base64, superFetch, parseSrcset, uglifycss, htmlmini, rulesMinifier, fontsMinifier, lazyLoader, serializer, docHelper */
 
 this.SingleFile = this.SingleFile || (() => {
 
@@ -129,6 +129,10 @@ this.SingleFile = this.SingleFile || (() => {
 			return rulesMinifier.process(doc);
 		}
 
+		static fontsMinifier(doc, removeUnusedCSSRules) {
+			return fontsMinifier.process(doc, removeUnusedCSSRules);
+		}
+
 		static uglifycss(content, options) {
 			return uglifycss.processString(content, options);
 		}

+ 10 - 0
lib/single-file/single-file-core.js

@@ -131,6 +131,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			if (this.options.removeUnusedCSSRules) {
 				this.processor.removeUnusedCSSRules();
 			}
+			if (this.options.removeAlternativeFonts) {
+				this.processor.removeAlternativeFonts();
+			}
 			this.pendingPromises = [this.processor.inlineStylesheets(), this.processor.attributeStyles(), this.processor.pageResources()];
 			if (!this.options.removeScripts) {
 				this.pendingPromises.push(this.processor.scripts());
@@ -153,6 +156,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			if (this.options.lazyLoadImages) {
 				this.processor.lazyLoadImages();
 			}
+			if (this.options.removeAlternativeFonts) {
+				this.processor.removeAlternativeFonts();
+			}
 			if (!this.options.removeFrames) {
 				await this.processor.frames();
 			}
@@ -392,6 +398,10 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			this.stats.set("discarded", "cssRules", removedRules.discarded);
 		}
 
+		removeAlternativeFonts() {
+			DOM.fontsMinifier(this.doc, this.options.removeUnusedCSSRules);
+		}
+
 		removeHiddenElements(sessionId) {
 			const hiddenElements = this.doc.querySelectorAll("[" + DOM.removedContentAttributeName(sessionId) + "]");
 			this.stats.set("discarded", "hiddenElements", hiddenElements.length);