Просмотр исходного кода

added "remove stylesheets for alternative devices" option

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

+ 4 - 0
extension/core/bg/config.js

@@ -48,6 +48,7 @@ singlefile.config = (() => {
 		autoSaveUnload: false,
 		autoSaveLoadOrUnload: true,
 		removeAlternativeFonts: true,
+		removeAlternativeMedias: true,
 		removeSrcSet: false
 	};
 
@@ -133,6 +134,9 @@ singlefile.config = (() => {
 		if (config.removeAlternativeFonts === undefined) {
 			config.removeAlternativeFonts = true;
 		}
+		if (config.removeAlternativeMedias === undefined) {
+			config.removeAlternativeMedias = true;
+		}
 		if (config.autoSaveLoadOrUnload === undefined && !config.autoSaveUnload) {
 			config.autoSaveLoadOrUnload = true;
 			config.autoSaveLoad = false;

+ 2 - 0
extension/core/bg/script-loader.js

@@ -58,6 +58,8 @@ singlefile.scriptLoader = (() => {
 			"/lib/single-file/css-what.js",
 			"/lib/single-file/css-declarations-parser.js",
 			"/lib/single-file/css-rules-matcher.js",
+			"/lib/single-file/css-media-query-parser.js",
+			"/lib/single-file/css-medias-minifier.js",
 			"/lib/single-file/css-minifier.js"
 		],
 		lazyLoadImages: [

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

@@ -49,6 +49,7 @@
 	const autoSaveLoadOrUnloadInput = document.getElementById("autoSaveLoadOrUnloadInput");
 	const removeAlternativeFontsInput = document.getElementById("removeAlternativeFontsInput");
 	const removeSrcSetInput = document.getElementById("removeSrcSetInput");
+	const removeAlternativeMediasInput = document.getElementById("removeAlternativeMediasInput");
 	let pendingSave = Promise.resolve();
 	document.getElementById("resetButton").addEventListener("click", async () => {
 		await bgPage.singlefile.config.reset();
@@ -105,6 +106,7 @@
 		autoSaveUnloadInput.disabled = config.autoSaveLoadOrUnload;
 		removeAlternativeFontsInput.checked = config.removeAlternativeFonts;
 		removeSrcSetInput.checked = config.removeSrcSet;
+		removeAlternativeMediasInput.checked = config.removeAlternativeMedias;
 	}
 
 	async function update() {
@@ -135,7 +137,8 @@
 			autoSaveUnload: autoSaveUnloadInput.checked,
 			autoSaveLoadOrUnload: autoSaveLoadOrUnloadInput.checked,
 			removeAlternativeFonts: removeAlternativeFontsInput.checked,
-			removeSrcSet: removeSrcSetInput.checked
+			removeSrcSet: removeSrcSetInput.checked,
+			removeAlternativeMedias: removeAlternativeMediasInput.checked
 		});
 		await pendingSave;
 		await bgPage.singlefile.ui.menu.refresh();

+ 52 - 40
extension/ui/pages/help.html

@@ -125,7 +125,7 @@
 							<u>uncheck</u> this option</p>
 					</li>
 				</ul>
-				<p>Page content</p>
+				<p>HTML content</p>
 				<ul>
 					<li>
 						<span class="option">compress HTML</span>
@@ -157,15 +157,8 @@
 						<p>Check this option to remove all hidden elements. This option can considerably reduce the size of the file without
 							altering the document most of the time.</p>
 					</li>
-
-					<li>
-						<span class="option">save raw page</span>
-						<p>Check this option to save the page without interpreting JavaScript. Checking this option may alter the document.</p>
-						<p class="notice">It is recommended to
-							<u>uncheck</u> this option</p>
-					</li>
 				</ul>
-				<p>Pages resources</p>
+				<p>Images</p>
 				<ul>
 					<li>
 						<span class="option">save lazy loaded images</span>
@@ -176,26 +169,15 @@
 					</li>
 
 					<li>
-						<span class="option">remove scripts</span>
-						<p>Check this option to remove all scripts. Unchecking this option may alter the document.</p>
-						<p class="notice">It is recommended to
-							<u>check</u> this option</p>
-					</li>
-
-					<li>
-						<span class="option">remove video sources</span>
-						<p>Check this option to empty the "src" attribute of all video elements.</p>
-						<p class="notice">It is recommended to
-							<u>check</u> this option</p>
-					</li>
-
-					<li>
-						<span class="option">remove audio sources</span>
-						<p>Check this option to empty the "src" attribute of all audio elements.</p>
+						<span class="option">remove alternative images in other resolutions</span>
+						<p>Check this option to remove images that are alternatives in lower and/or higher resolutions to the ones displayed
+							by default. Checking this this option can considerably reduce the size of the file in some cases.</p>
 						<p class="notice">It is recommended to
-							<u>check</u> this option</p>
+							<u>uncheck</u> this option</p>
 					</li>
-
+				</ul>
+				<p>Stylesheets</p>
+				<ul>
 					<li>
 						<span class="option">compress CSS</span>
 						<p>Check this option to minify CSS stylesheets. This helps to reduce the size of the file without altering the document. 
@@ -224,31 +206,48 @@
 					</li>
 
 					<li>
-						<span class="option">remove alternative images in other resolutions</span>
-						<p>Check this option to remove images that are alternatives in lower and/or higher resolutions to the ones displayed
-							by default. Checking this this option can considerably reduce the size of the file in some cases.</p>
+						<span class="option">remove stylesheets for alternative devices</span>
+						<p>Check this option to remove stylesheets that are not used for the screen display like stylesheets for print preview 
+							and speech synthesizers. Checking this this option can reduce the size of the file.</p>
 						<p class="notice">It is recommended to
-							<u>uncheck</u> this option</p>
+							<u>check</u> this option</p>
+					</li>
+				</ul>
+				<p>Other resources</p>
+				<ul>
+					<li>
+						<span class="option">remove scripts</span>
+						<p>Check this option to remove all scripts. Unchecking this option may alter the document.</p>
+						<p class="notice">It is recommended to
+							<u>check</u> this option</p>
 					</li>
 
 					<li>
-						<span class="option">set a maximum size for embedded resources (Mb)</span>
-						<p>Specify the maximum size of embedded resources (i.e. images, stylesheets, scripts and iframes) in megabytes.
-						</p>
+						<span class="option">remove video sources</span>
+						<p>Check this option to empty the "src" attribute of all video elements.</p>
+						<p class="notice">It is recommended to
+							<u>check</u> this option</p>
+					</li>
+
+					<li>
+						<span class="option">remove audio sources</span>
+						<p>Check this option to empty the "src" attribute of all audio elements.</p>
+						<p class="notice">It is recommended to
+							<u>check</u> this option</p>
 					</li>
 				</ul>
 				<p>Auto-save</p>
 				<ul>
 					<li>
-						<span class="option">auto-save on page load or on page </span>
+						<span class="option">auto-save on page load or unload</span>
 						<p>Check this option to auto-save pages after being loaded. If you browse to another page before the page is fully
 							loaded then the page will be saved just before being unloaded. With this option active, you are guaranteed pages will
 							always be saved but some frame contents may be missing (if you unchecked "remove frames") when pages are saved before
 							being unloaded.
 						</p>
+						<p class="notice">It is recommended to
+							<u>check</u> this option</p>
 					</li>
-					<p class="notice">It is recommended to
-						<u>check</u> this option</p>
 
 					<li>
 						<span class="option">auto-save on page load</span>
@@ -274,9 +273,9 @@
 				<ul>
 					<li>
 						<span class="option">save pages in background</span>
-						<p>Uncheck this option if you get invalid file names like "37bec68b-446a-46a5-8642-19a89c231b46.html" when saving pages
-							or if you prefer to use the prompt dialog instead of the "Save as" dialog when "open the 'Save as' dialog to confirm
-							the file name" option is checked.
+						<p>Uncheck this option if you get invalid file names like "37bec68b-446a-46a5-8642-19a89c231b46.html" or interupted downloads 
+							when saving pages or if you prefer to use the prompt dialog instead of the "Save as" dialog when "open the 'Save as' dialog
+							to confirm the file name" option is checked.
 						</p>
 						<p class="notice">It is recommended to
 							<u>check</u> this option</p>
@@ -290,6 +289,19 @@
 						<p class="notice">It is recommended to
 							<u>uncheck</u> this option</p>
 					</li>
+
+					<li>
+						<span class="option">set a maximum size for embedded resources (Mb)</span>
+						<p>Specify the maximum size of embedded resources (i.e. images, stylesheets, scripts and iframes) in megabytes.
+						</p>
+					</li>
+
+					<li>
+						<span class="option">save raw page</span>
+						<p>Check this option to save the page without interpreting JavaScript. Checking this option may alter the document.</p>
+						<p class="notice">It is recommended to
+							<u>uncheck</u> this option</p>
+					</li>
 				</ul>
 				<p>Form buttons</p>
 				<ul>

+ 33 - 23
extension/ui/pages/options.html

@@ -36,7 +36,7 @@
 		</div>
 	</details>
 	<details>
-		<summary>Page content</summary>
+		<summary>HTML content</summary>
 		<div class="option">
 			<label for="compressHTMLInput">compress HTML</label>
 			<input type="checkbox" id="compressHTMLInput">
@@ -53,29 +53,20 @@
 			<label for="removeHiddenElementsInput">remove hidden elements</label>
 			<input type="checkbox" id="removeHiddenElementsInput">
 		</div>
-		<div class="option">
-			<label for="saveRawPageInput">save raw page</label>
-			<input type="checkbox" id="saveRawPageInput">
-		</div>
 	</details>
 	<details>
-		<summary>Pages resources</summary>
+		<summary>Images</summary>
 		<div class="option">
 			<label for="lazyLoadImagesInput">save lazy loaded images</label>
 			<input type="checkbox" id="lazyLoadImagesInput">
 		</div>
 		<div class="option">
-			<label for="removeScriptsInput">remove scripts</label>
-			<input type="checkbox" id="removeScriptsInput">
-		</div>
-		<div class="option">
-			<label for="removeVideoSrcInput">remove video sources</label>
-			<input type="checkbox" id="removeVideoSrcInput">
-		</div>
-		<div class="option">
-			<label for="removeAudioSrcInput">remove audio sources</label>
-			<input type="checkbox" id="removeAudioSrcInput">
+			<label for="removeSrcSetInput">remove alternative images in other resolutions</label>
+			<input type="checkbox" id="removeSrcSetInput">
 		</div>
+	</details>
+	<details>
+		<summary>Stylesheets</summary>
 		<div class="option">
 			<label for="compressCSSInput">compress CSS</label>
 			<input type="checkbox" id="compressCSSInput">
@@ -89,16 +80,23 @@
 			<input type="checkbox" id="removeAlternativeFontsInput">
 		</div>
 		<div class="option">
-			<label for="removeSrcSetInput">remove alternative images in other resolutions</label>
-			<input type="checkbox" id="removeSrcSetInput">
+			<label for="removeAlternativeFontsInput">remove stylesheets for alternative devices</label>
+			<input type="checkbox" id="removeAlternativeMediasInput">
 		</div>
+	</details>
+	<details>
+		<summary>Other resources</summary>
 		<div class="option">
-			<label for="maxResourceSizeEnabledInput">set a maximum size for embedded resources</label>
-			<input type="checkbox" id="maxResourceSizeEnabledInput">
+			<label for="removeScriptsInput">remove scripts</label>
+			<input type="checkbox" id="removeScriptsInput">
 		</div>
-		<div class="option second-level">
-			<label for="maxResourceSizeInput">maximum size (Mb)</label>
-			<input type="number" id="maxResourceSizeInput" min="1">
+		<div class="option">
+			<label for="removeVideoSrcInput">remove video sources</label>
+			<input type="checkbox" id="removeVideoSrcInput">
+		</div>
+		<div class="option">
+			<label for="removeAudioSrcInput">remove audio sources</label>
+			<input type="checkbox" id="removeAudioSrcInput">
 		</div>
 	</details>
 	<details>
@@ -130,6 +128,18 @@
 			<label for="displayStatsInput">display stats in the console after processing</label>
 			<input type="checkbox" id="displayStatsInput">
 		</div>
+		<div class="option">
+			<label for="maxResourceSizeEnabledInput">set a maximum size for embedded resources</label>
+			<input type="checkbox" id="maxResourceSizeEnabledInput">
+		</div>
+		<div class="option second-level">
+			<label for="maxResourceSizeInput">maximum size (Mb)</label>
+			<input type="number" id="maxResourceSizeInput" min="1">
+		</div>
+		<div class="option">
+			<label for="saveRawPageInput">save raw page</label>
+			<input type="checkbox" id="saveRawPageInput">
+		</div>
 	</details>
 	<div class="option bottom">
 		<a href="help.html" target="SingleFileHelpPage">help</a>

+ 460 - 0
lib/single-file/css-media-query-parser.js

@@ -0,0 +1,460 @@
+/*
+ * The MIT License (MIT)
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+*/
+
+// https://github.com/dryoma/postcss-media-query-parser (modified by Gildas Lormeau)
+
+this.mediaQueryParser = (() => {
+
+	/**
+	 * Parses a media feature expression, e.g. `max-width: 10px`, `(color)`
+	 *
+	 * @param {string} string - the source expression string, can be inside parens
+	 * @param {Number} index - the index of `string` in the overall input
+	 *
+	 * @return {Array} an array of Nodes, the first element being a media feature,
+	 *    the second - its value (may be missing)
+	 */
+
+	function parseMediaFeature(string, index = 0) {
+		const modesEntered = [{
+			mode: "normal",
+			character: null,
+		}];
+		const result = [];
+		let lastModeIndex = 0;
+		let mediaFeature = "";
+		let colon = null;
+		let mediaFeatureValue = null;
+		let indexLocal = index;
+
+		let stringNormalized = string;
+		// Strip trailing parens (if any), and correct the starting index
+		if (string[0] === "(" && string[string.length - 1] === ")") {
+			stringNormalized = string.substring(1, string.length - 1);
+			indexLocal++;
+		}
+
+		for (let i = 0; i < stringNormalized.length; i++) {
+			const character = stringNormalized[i];
+
+			// If entering/exiting a string
+			if (character === "'" || character === "\"") {
+				if (modesEntered[lastModeIndex].isCalculationEnabled === true) {
+					modesEntered.push({
+						mode: "string",
+						isCalculationEnabled: false,
+						character,
+					});
+					lastModeIndex++;
+				} else if (modesEntered[lastModeIndex].mode === "string" &&
+					modesEntered[lastModeIndex].character === character &&
+					stringNormalized[i - 1] !== "\\"
+				) {
+					modesEntered.pop();
+					lastModeIndex--;
+				}
+			}
+
+			// If entering/exiting interpolation
+			if (character === "{") {
+				modesEntered.push({
+					mode: "interpolation",
+					isCalculationEnabled: true,
+				});
+				lastModeIndex++;
+			} else if (character === "}") {
+				modesEntered.pop();
+				lastModeIndex--;
+			}
+
+			// If a : is met outside of a string, function call or interpolation, than
+			// this : separates a media feature and a value
+			if (modesEntered[lastModeIndex].mode === "normal" && character === ":") {
+				const mediaFeatureValueStr = stringNormalized.substring(i + 1);
+				mediaFeatureValue = {
+					type: "value",
+					before: /^(\s*)/.exec(mediaFeatureValueStr)[1],
+					after: /(\s*)$/.exec(mediaFeatureValueStr)[1],
+					value: mediaFeatureValueStr.trim(),
+				};
+				// +1 for the colon
+				mediaFeatureValue.sourceIndex =
+					mediaFeatureValue.before.length + i + 1 + indexLocal;
+				colon = {
+					type: "colon",
+					sourceIndex: i + indexLocal,
+					after: mediaFeatureValue.before,
+					value: ":", // for consistency only
+				};
+				break;
+			}
+
+			mediaFeature += character;
+		}
+
+		// Forming a media feature node
+		mediaFeature = {
+			type: "media-feature",
+			before: /^(\s*)/.exec(mediaFeature)[1],
+			after: /(\s*)$/.exec(mediaFeature)[1],
+			value: mediaFeature.trim(),
+		};
+		mediaFeature.sourceIndex = mediaFeature.before.length + indexLocal;
+		result.push(mediaFeature);
+
+		if (colon !== null) {
+			colon.before = mediaFeature.after;
+			result.push(colon);
+		}
+
+		if (mediaFeatureValue !== null) {
+			result.push(mediaFeatureValue);
+		}
+
+		return result;
+	}
+
+	/**
+	 * Parses a media query, e.g. `screen and (color)`, `only tv`
+	 *
+	 * @param {string} string - the source media query string
+	 * @param {Number} index - the index of `string` in the overall input
+	 *
+	 * @return {Array} an array of Nodes and Containers
+	 */
+
+	function parseMediaQuery(string, index = 0) {
+		const result = [];
+
+		// How many times the parser entered parens/curly braces
+		let localLevel = 0;
+		// Has any keyword, media type, media feature expression or interpolation
+		// ('element' hereafter) started
+		let insideSomeValue = false;
+		let node;
+
+		function resetNode() {
+			return {
+				before: "",
+				after: "",
+				value: "",
+			};
+		}
+
+		node = resetNode();
+
+		for (let i = 0; i < string.length; i++) {
+			const character = string[i];
+			// If not yet entered any element
+			if (!insideSomeValue) {
+				if (character.search(/\s/) !== -1) {
+					// A whitespace
+					// Don't form 'after' yet; will do it later
+					node.before += character;
+				} else {
+					// Not a whitespace - entering an element
+					// Expression start
+					if (character === "(") {
+						node.type = "media-feature-expression";
+						localLevel++;
+					}
+					node.value = character;
+					node.sourceIndex = index + i;
+					insideSomeValue = true;
+				}
+			} else {
+				// Already in the middle of some element
+				node.value += character;
+
+				// Here parens just increase localLevel and don't trigger a start of
+				// a media feature expression (since they can't be nested)
+				// Interpolation start
+				if (character === "{" || character === "(") { localLevel++; }
+				// Interpolation/function call/media feature expression end
+				if (character === ")" || character === "}") { localLevel--; }
+			}
+
+			// If exited all parens/curlies and the next symbol
+			if (insideSomeValue && localLevel === 0 &&
+				(character === ")" || i === string.length - 1 ||
+					string[i + 1].search(/\s/) !== -1)
+			) {
+				if (["not", "only", "and"].indexOf(node.value) !== -1) {
+					node.type = "keyword";
+				}
+				// if it's an expression, parse its contents
+				if (node.type === "media-feature-expression") {
+					node.nodes = parseMediaFeature(node.value, node.sourceIndex);
+				}
+				result.push(Array.isArray(node.nodes) ?
+					new Container(node) : new Node(node));
+				node = resetNode();
+				insideSomeValue = false;
+			}
+		}
+
+		// Now process the result array - to specify undefined types of the nodes
+		// and specify the `after` prop
+		for (let i = 0; i < result.length; i++) {
+			node = result[i];
+			if (i > 0) { result[i - 1].after = node.before; }
+
+			// Node types. Might not be set because contains interpolation/function
+			// calls or fully consists of them
+			if (node.type === undefined) {
+				if (i > 0) {
+					// only `and` can follow an expression
+					if (result[i - 1].type === "media-feature-expression") {
+						node.type = "keyword";
+						continue;
+					}
+					// Anything after 'only|not' is a media type
+					if (result[i - 1].value === "not" || result[i - 1].value === "only") {
+						node.type = "media-type";
+						continue;
+					}
+					// Anything after 'and' is an expression
+					if (result[i - 1].value === "and") {
+						node.type = "media-feature-expression";
+						continue;
+					}
+
+					if (result[i - 1].type === "media-type") {
+						// if it is the last element - it might be an expression
+						// or 'and' depending on what is after it
+						if (!result[i + 1]) {
+							node.type = "media-feature-expression";
+						} else {
+							node.type = result[i + 1].type === "media-feature-expression" ?
+								"keyword" : "media-feature-expression";
+						}
+					}
+				}
+
+				if (i === 0) {
+					// `screen`, `fn( ... )`, `#{ ... }`. Not an expression, since then
+					// its type would have been set by now
+					if (!result[i + 1]) {
+						node.type = "media-type";
+						continue;
+					}
+
+					// `screen and` or `#{...} (max-width: 10px)`
+					if (result[i + 1] &&
+						(result[i + 1].type === "media-feature-expression" ||
+							result[i + 1].type === "keyword")
+					) {
+						node.type = "media-type";
+						continue;
+					}
+					if (result[i + 2]) {
+						// `screen and (color) ...`
+						if (result[i + 2].type === "media-feature-expression") {
+							node.type = "media-type";
+							result[i + 1].type = "keyword";
+							continue;
+						}
+						// `only screen and ...`
+						if (result[i + 2].type === "keyword") {
+							node.type = "keyword";
+							result[i + 1].type = "media-type";
+							continue;
+						}
+					}
+					if (result[i + 3]) {
+						// `screen and (color) ...`
+						if (result[i + 3].type === "media-feature-expression") {
+							node.type = "keyword";
+							result[i + 1].type = "media-type";
+							result[i + 2].type = "keyword";
+							continue;
+						}
+					}
+				}
+			}
+		}
+		return result;
+	}
+
+	/**
+	 * Parses a media query list. Takes a possible `url()` at the start into
+	 * account, and divides the list into media queries that are parsed separately
+	 *
+	 * @param {string} string - the source media query list string
+	 *
+	 * @return {Array} an array of Nodes/Containers
+	 */
+
+	function parseMediaList(string) {
+		const result = [];
+		let interimIndex = 0;
+		let levelLocal = 0;
+
+		// Check for a `url(...)` part (if it is contents of an @import rule)
+		const doesHaveUrl = /^(\s*)url\s*\(/.exec(string);
+		if (doesHaveUrl !== null) {
+			let i = doesHaveUrl[0].length;
+			let parenthesesLv = 1;
+			while (parenthesesLv > 0) {
+				const character = string[i];
+				if (character === "(") { parenthesesLv++; }
+				if (character === ")") { parenthesesLv--; }
+				i++;
+			}
+			result.unshift(new Node({
+				type: "url",
+				value: string.substring(0, i).trim(),
+				sourceIndex: doesHaveUrl[1].length,
+				before: doesHaveUrl[1],
+				after: /^(\s*)/.exec(string.substring(i))[1],
+			}));
+			interimIndex = i;
+		}
+
+		// Start processing the media query list
+		for (let i = interimIndex; i < string.length; i++) {
+			const character = string[i];
+
+			// Dividing the media query list into comma-separated media queries
+			// Only count commas that are outside of any parens
+			// (i.e., not part of function call params list, etc.)
+			if (character === "(") { levelLocal++; }
+			if (character === ")") { levelLocal--; }
+			if (levelLocal === 0 && character === ",") {
+				const mediaQueryString = string.substring(interimIndex, i);
+				const spaceBefore = /^(\s*)/.exec(mediaQueryString)[1];
+				result.push(new Container({
+					type: "media-query",
+					value: mediaQueryString.trim(),
+					sourceIndex: interimIndex + spaceBefore.length,
+					nodes: parseMediaQuery(mediaQueryString, interimIndex),
+					before: spaceBefore,
+					after: /(\s*)$/.exec(mediaQueryString)[1],
+				}));
+				interimIndex = i + 1;
+			}
+		}
+
+		const mediaQueryString = string.substring(interimIndex);
+		const spaceBefore = /^(\s*)/.exec(mediaQueryString)[1];
+		result.push(new Container({
+			type: "media-query",
+			value: mediaQueryString.trim(),
+			sourceIndex: interimIndex + spaceBefore.length,
+			nodes: parseMediaQuery(mediaQueryString, interimIndex),
+			before: spaceBefore,
+			after: /(\s*)$/.exec(mediaQueryString)[1],
+		}));
+
+		return result;
+	}
+
+	function Container(opts) {
+		this.constructor(opts);
+
+		this.nodes = opts.nodes;
+
+		if (this.after === undefined) {
+			this.after = this.nodes.length > 0 ?
+				this.nodes[this.nodes.length - 1].after : "";
+		}
+
+		if (this.before === undefined) {
+			this.before = this.nodes.length > 0 ?
+				this.nodes[0].before : "";
+		}
+
+		if (this.sourceIndex === undefined) {
+			this.sourceIndex = this.before.length;
+		}
+
+		this.nodes.forEach(node => {
+			node.parent = this; // eslint-disable-line no-param-reassign
+		});
+	}
+
+	Container.prototype = Object.create(Node.prototype);
+	Container.constructor = Node;
+
+	/**
+	 * Iterate over descendant nodes of the node
+	 *
+	 * @param {RegExp|string} filter - Optional. Only nodes with node.type that
+	 *    satisfies the filter will be traversed over
+	 * @param {function} cb - callback to call on each node. Takes these params:
+	 *    node - the node being processed, i - it's index, nodes - the array
+	 *    of all nodes
+	 *    If false is returned, the iteration breaks
+	 *
+	 * @return (boolean) false, if the iteration was broken
+	 */
+	Container.prototype.walk = function walk(filter, cb) {
+		const hasFilter = typeof filter === "string" || filter instanceof RegExp;
+		const callback = hasFilter ? cb : filter;
+		const filterReg = typeof filter === "string" ? new RegExp(filter) : filter;
+
+		for (let i = 0; i < this.nodes.length; i++) {
+			const node = this.nodes[i];
+			const filtered = hasFilter ? filterReg.test(node.type) : true;
+			if (filtered && callback && callback(node, i, this.nodes) === false) {
+				return false;
+			}
+			if (node.nodes && node.walk(filter, cb) === false) { return false; }
+		}
+		return true;
+	};
+
+	/**
+	 * Iterate over immediate children of the node
+	 *
+	 * @param {function} cb - callback to call on each node. Takes these params:
+	 *    node - the node being processed, i - it's index, nodes - the array
+	 *    of all nodes
+	 *    If false is returned, the iteration breaks
+	 *
+	 * @return (boolean) false, if the iteration was broken
+	 */
+	Container.prototype.each = function each(cb = () => { }) {
+		for (let i = 0; i < this.nodes.length; i++) {
+			const node = this.nodes[i];
+			if (cb(node, i, this.nodes) === false) { return false; }
+		}
+		return true;
+	};
+
+	/**
+	 * A very generic node. Pretty much any element of a media query
+	 */
+
+	function Node(opts) {
+		this.after = opts.after;
+		this.before = opts.before;
+		this.type = opts.type;
+		this.value = opts.value;
+		this.sourceIndex = opts.sourceIndex;
+	}
+
+	return {
+		parseMediaList
+	};
+
+})();

+ 81 - 0
lib/single-file/css-medias-minifier.js

@@ -0,0 +1,81 @@
+/*
+ * 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, mediaQueryParser */
+
+this.mediasMinifier = this.mediasMinifier || (() => {
+
+	return {
+		process: doc => {
+			const stats = { processed: 0, discarded: 0 };
+			doc.querySelectorAll("style").forEach(styleElement => {
+				if (styleElement.sheet) {
+					styleElement.textContent = processRules(doc, styleElement.sheet.cssRules, stats);
+				}
+			});
+			return stats;
+		}
+	};
+
+	function processRules(doc, cssRules, stats) {
+		let sheetContent = "";
+		Array.from(cssRules).forEach(cssRule => {
+			if (cssRule.type == CSSRule.MEDIA_RULE) {
+				stats.processed++;
+				if (matchesMediaType(cssRule.media, "screen")) {
+					sheetContent += "@media " + Array.from(cssRule.media).join(",") + "{";
+					sheetContent += processRules(doc, cssRule.cssRules, stats);
+					sheetContent += "}";
+				} else {
+					stats.discarded++;
+				}
+			} else {
+				sheetContent += cssRule.cssText;
+			}
+		});
+		return sheetContent;
+	}
+
+	function matchesMediaType(media, mediaType) {
+		const foundMediaTypes = mediaQueryParser.parseMediaList(media.mediaText).map(node => getMediaTypes(node, mediaType)).flat();
+		return foundMediaTypes.find(mediaTypeInfo => !mediaTypeInfo.not && (mediaTypeInfo.value == mediaType || mediaTypeInfo.value == "all"));
+	}
+
+	function getMediaTypes(parentNode, mediaType, mediaTypes = []) {
+		parentNode.nodes.map((node, indexNode) => {
+			if (node.type == "media-query") {
+				return getMediaTypes(node, mediaType, mediaTypes);
+			} else {
+				let nodeMediaType;
+				if (node.type == "media-type") {
+					nodeMediaType = { not: Boolean(indexNode && parentNode.nodes[0].type == "keyword" && parentNode.nodes[0].value == "not"), value: node.value };
+					if (!mediaTypes.find(mediaType => nodeMediaType.not == mediaType.not && nodeMediaType.value == mediaType.value)) {
+						mediaTypes.push(nodeMediaType);
+					}
+				}
+			}
+		});
+		if (!mediaTypes.length) {
+			mediaTypes.push({ not: false, value: "all" });
+		}
+		return mediaTypes;
+	}
+
+})();

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

+ 13 - 7
lib/single-file/single-file-core.js

@@ -97,7 +97,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			{ option: "removeUnusedStyles", action: "removeUnusedStyles" },
 			{ option: "compressHTML", action: "compressHTML" },
 			{ option: "removeAlternativeFonts", action: "removeAlternativeFonts" },
-			{ option: "compressCSS", action: "compressCSS" },
+			{ option: "removeAlternativeMedias", action: "removeAlternativeMedias" }
 		],
 		async: [
 			{ action: "inlineStylesheets" },
@@ -108,7 +108,8 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 	}, {
 		sync: [
 			{ option: "lazyLoadImages", action: "lazyLoadImages" },
-			{ option: "removeAlternativeFonts", action: "postRemoveAlternativeFonts" }
+			{ option: "removeAlternativeFonts", action: "postRemoveAlternativeFonts" },
+			{ option: "compressCSS", action: "compressCSS" }
 		],
 		async: [
 			{ option: "!removeFrames", action: "frames" },
@@ -503,9 +504,6 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 
 		postRemoveAlternativeFonts() {
 			DOM.minifyFonts(this.doc, true);
-			if (this.options.compressCSS) {
-				this.compressCSS();
-			}
 		}
 
 		removeHiddenElements() {
@@ -536,6 +534,12 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			}
 		}
 
+		removeAlternativeMedias() {
+			const stats = DOM.minifyMedias(this.doc);
+			this.stats.set("processed", "medias", stats.processed);
+			this.stats.set("discarded", "medias", stats.discarded);
+		}
+
 		compressCSS() {
 			this.doc.querySelectorAll("style").forEach(styleElement => {
 				if (styleElement) {
@@ -1044,7 +1048,8 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			audioSource: 0,
 			videoSource: 0,
 			frames: 0,
-			cssRules: 0
+			cssRules: 0,
+			medias: 0
 		},
 		processed: {
 			htmlBytes: 0,
@@ -1054,7 +1059,8 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			cssRules: 0,
 			canvas: 0,
 			styleSheets: 0,
-			resources: 0
+			resources: 0,
+			medias: 0
 		}
 	};
 

+ 2 - 0
manifest.json

@@ -61,6 +61,8 @@
 			"lib/single-file/css-what.js",
 			"lib/single-file/css-declarations-parser.js",
 			"lib/single-file/css-fonts-minifier.js",
+			"lib/single-file/css-media-query-parser.js",
+			"lib/single-file/css-medias-minifier.js",
 			"lib/single-file/css-rules-matcher.js",
 			"lib/single-file/css-minifier.js",
 			"lib/single-file/css-srcset-parser.js",