65.9K
CodeProject 正在变化。 阅读更多。
Home

桌面浏览器“阅读模式”

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2019年2月2日

CPOL

4分钟阅读

viewsIcon

8308

为您的浏览器提供更可定制的“阅读模式”或“文章视图”,以便阅读新闻和文章。

引言

本文介绍了一个用户 JavaScript (User JS) 脚本,该脚本将使桌面浏览器能够模拟移动设备电子书阅读器应用程序中常见的功能。 它将尝试仅显示网页的主要内容,并删除所有或大多数无关信息。 推荐用于显示新闻或文章的网页。 它在 HTML5 页面上效果最好,但在结构良好的普通 HTML 页面上应该也能正常运行。

背景

User JS 脚本也称为浏览器脚本,类似于书签小程序。 它们也被称为 Greasemonkey 脚本,以 Firefox 插件命名,该插件允许浏览器用户通过从客户端注入自己的 Javascript 脚本来定制他们喜欢的网站。

我开发了这个脚本,用于包含在我开发的同名 Android 浏览器应用程序中。 该应用程序对 User CSS 和 User JS 文件有特殊支持。 在开发过程中,我使用了带有 GreaseMonkey 插件的 Firefox 浏览器,因为在 Android 设备模拟器上进行编码和测试非常麻烦。 当我使用 Firefox 的 Bamboo RSS 阅读器插件在桌面电脑或笔记本电脑上阅读文章时,我会使用此版本的脚本。

Using the Code

我的 Android 应用程序要求将 JavaScript 代码转换为书签小程序样式。 因此,大部分代码都是那种风格的。 对于 Firefox(桌面)浏览器,我为 "DOMContentLoaded" 事件添加了一个事件处理程序。 它会在延迟 5 秒后运行图书阅读器模式。 这给了我足够的时间点击链接,即使在那些不期望该脚本运行良好的网站的主页上也是如此。

这是 GreaseMonkey Javascript。 如果代码在发布时被损坏,则可以从其 GitHub 位置 复制原始 Javascript 源代码文本。

// ==UserScript==
// @name        BookReaderView
// @namespace   com.vsubhash.js.BookReaderView
// @version     1
// @grant       none
// ==/UserScript==

if (subhash_browser_js == null) {
  var subhash_browser_js = {};
}

subhash_browser_js.book_reader_js = {
	sHtml: "", 
	bTitleFound: false,
	arYucks: [ "-ads", "_ads", "advert", "adcode", "adselect", "addthis", "alsoread", "comment", 
               "discuss", "email", "facebook", "float", "follow", "franchise", "googlead",  
               "hide_", "hidden", "hover", "jump", "lazy", "linkedin", "navig", "notifi", 
               "outbrain", "partner", "popular", "popup", "print", "reddit", "share", "sharing", 
               "short-url", "social", "sponsor", "sprite", "subscribe", "taboola", "trend", 
               "twitter", "url-short", "zipr" ],	
	
	createHeader: function() {
		subhash_browser_js.book_reader_js.sHtml = "<head>\n" +
			"	<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" +
			"	<title>" + document.title + "</title>\n" +
			"	<style>\n" +
			" a { border-bottom: 1px dotted navy; }\n" +
			" body { background-color: rgb(200,200,220); color: black; font-family: sans-serif;\n" +
            "        font-size: 0.5cm; margin: 1em auto; padding: 1em; max-width: 9in; }\n" +
			" code { font-family: monospace; }\n" +
			" h1 { text-align: center; border-bottom: 1px solid black; padding-bottom: 0.2em; }\n" +
			" a h1, a h2, a h3, a h4, a h5, a h6, h1 a, h2 a, h3 a, h4 a, h5 a, h6 a\n" +
            "       { color: black; border-bottom: 1px dotted black; }\n" +
			" pre, figure { margin: 1em auto; padding: 1em;  }\n" +		
			" img { display: block; margin: 1em auto; max-height: 40%; max-width: 40%; }\n" +
			" img[src*='.svg'] { display: none!important; }\n" +
			" figcaption { font-weight: bold; font-size: 0.8em; text-align: center; }\n" +
			" header, footer, aside, nav { display: none; }\n" +
			"	</style>\n" +
			"</head>\n" +
			"<body>\n";
	},	
	
	removeUnwantedTags: function() {
		var arrTagsToHide = [ "aside", "footer", "iframe", "nav", "noscript", "script"];
		for (var i = 0; i < arrTagsToHide.length; i++) {
			var arElementsToHide = document.getElementsByTagName(arrTagsToHide[i]);
			var j = arElementsToHide.length;
			while (j > 0) {
				arElementsToHide[j-1].parentNode.removeChild(arElementsToHide[j-1]);
				--j;
			}
		}
	},	
	
	addNoYuckiesStyle: function() {
		var sStyle = "\n<style>";
		for (var i = 0; i < subhash_browser_js.book_reader_js.arYucks.length; i++) {
			sStyle += "*[class*=\"" + subhash_browser_js.book_reader_js.arYucks[i] + "\"], *[id*=\"" + subhash_browser_js.book_reader_js.arYucks[i] + "\"] ";
			if (i < (subhash_browser_js.book_reader_js.arYucks.length-1)) {
				sStyle += ",";
			}
		}
		sStyle += " { display: none!important; }\n</style>\n";
		document.getElementsByTagName("body")[0].innerHTML += sStyle;
		//console.error(sStyle);
	},	
	
	parseFiniteElement: function(aoEl) {
		//console.error("Finite tag: " + aoEl.tagName);
		subhash_browser_js.book_reader_js.isTitleTag(aoEl.tagName.toLowerCase());
		if (!subhash_browser_js.book_reader_js.bTitleFound || 
			!subhash_browser_js.book_reader_js.hasNoYuckiness(aoEl)) { return; }
	
		var sElTag = aoEl.tagName.toLowerCase();
		if (!subhash_browser_js.book_reader_js.isUsefulTag(sElTag)) { return; }
		//console.error(sElTag + " outed");
		if (sElTag == "a" && aoEl.href) {
			if (aoEl.href.indexOf("#") == 0) {
				subhash_browser_js.book_reader_js.sHtml += aoEl.textContent;
			} else {
				subhash_browser_js.book_reader_js.sHtml += "<a href=\"" + aoEl.getAttribute("href") + "\">" + aoEl.textContent + "</a>";
			}
		} else if (sElTag == "abbr") {
			subhash_browser_js.book_reader_js.sHtml += aoEl.textContent + 
            " (" + aoEl.getAttribute("title") + ") " + "\n";
		} else if ((sElTag == "b") || (sElTag == "em") || (sElTag == "strong")) {
			subhash_browser_js.book_reader_js.sHtml += "<b>" + aoEl.textContent + "</b>";
		} else if (sElTag == "br") {
			subhash_browser_js.book_reader_js.sHtml += "<br />";
		} else if ((sElTag == "cite") || (sElTag == "i") || (sElTag == "time")) {
			subhash_browser_js.book_reader_js.sHtml += "<i>" + aoEl.textContent + "</i>";
		} else if ((sElTag == "ins") || (sElTag == "kbd") || (sElTag == "mark") || (sElTag == "u")) {
			subhash_browser_js.book_reader_js.sHtml += "<u>" + aoEl.textContent + "</u>";
		} else if (sElTag == "img") {
			subhash_browser_js.book_reader_js.sHtml += "<img src=\"" + 
                                      aoEl.getAttribute("src") + "\" />";
		} else if ((sElTag == "cite") || (sElTag == "s") || (sElTag == "strike")) {
			subhash_browser_js.book_reader_js.sHtml += "<s>" + aoEl.textContent + "</s>";
		} else if ((sElTag == "code") || (sElTag == "samp") || (sElTag == "var")) {
			subhash_browser_js.book_reader_js.sHtml += "<code>" + aoEl.textContent + "</code>";
		} else if ((sElTag == "sub")) {
			subhash_browser_js.book_reader_js.sHtml += "<sub>" + aoEl.textContent + "</sub>";
		} else if (sElTag == "sup") {
			subhash_browser_js.book_reader_js.sHtml += "<sup>" + aoEl.textContent + "</sup>";
		} else if ((sElTag == "label") || (sElTag == "span") || (sElTag == "wbr")) {
			subhash_browser_js.book_reader_js.sHtml += aoEl.textContent;  // ignore
	
	
		} else if ((sElTag == "h1") || (sElTag == "h2") || (sElTag == "h3") || 
							(sElTag == "h4") || (sElTag == "h5") || (sElTag == "h6") ||
							(sElTag == "figcaption") || (sElTag == "p")) {
			subhash_browser_js.book_reader_js.sHtml += "<" + sElTag + ">" + 
                             aoEl.textContent + "</" + sElTag + ">";
		}
	},
	
	isUsefulTag: function(asTag) {
		var arTags = [ "a", "b", "i", "s", "u", "abbr", "article", "br", "code", 
                       "cite", "em", "figure", "figcaption", "h1", "h2", "h3", "h4", "h5", 
                       "h6", "img", "ins", "kbd", "label", "li", "main", "mark", "navig", "ol", 
                       "p", "pre", "samp", "strike", "sub", "sup", "span", 
                       "strong", "time", "ul", "var", "wbr" ]; 
		for (var i = 0; i < arTags.length; i++) {
			if (asTag == arTags[i]) { 
				//console.error(asTag + " is valid");
				return(true);
			}
		}
		//console.error(asTag + " is not valid");
		return(false);
	},	
	
	isTitleTag: function(asTag) {
		if ((!subhash_browser_js.book_reader_js.bTitleFound) && 
            ((asTag == "h1") || (asTag == "h2") || (asTag == "h3"))) {
			subhash_browser_js.book_reader_js.bTitleFound = true;
			//console.error("found");
		}
		return(subhash_browser_js.book_reader_js.bTitleFound);
	},

	hasNoYuckiness: function(aoNode) {
		if (aoNode.className) {
			if (aoNode.className.indexOf) {
			  for (var i = 0; i < subhash_browser_js.book_reader_js.arYucks.length; i++) {
			  	if (aoNode.className.toLowerCase().indexOf(subhash_browser_js.book_reader_js.arYucks[i]) > -1) {
			  		//console.error("Yucky " + aoNode.className);
			  		return(false);
			  	} else {
			  		//console.error("Yucky no find " + subhash_browser_js.book_reader_js.arYucks[i]);
			  	}
			  }
			}
		}
	
		if (aoNode.getAttribute) {
			if (aoNode.getAttribute("id")) {
				if (aoNode.getAttribute("id").indexOf) {
					for (var i = 0; i < subhash_browser_js.book_reader_js.arYucks.length; i++) {
						if (aoNode.getAttribute("id").toLowerCase().indexOf(subhash_browser_js.book_reader_js.arYucks[i]) > -1) {
							//console.error("Yucky " + aoNode.getAttribute("id"));
							return(false);
						}
					}
				}
			}
		}

		return(true);
	},

	parseNode: function(aoNode) {
		var sTag = aoNode.nodeName.toLowerCase();
		//console.error("Node checking " + sTag);
		subhash_browser_js.book_reader_js.isTitleTag(aoNode.nodeName.toLowerCase());
		if (subhash_browser_js.book_reader_js.bTitleFound && 
				subhash_browser_js.book_reader_js.isUsefulTag(sTag) && 
				subhash_browser_js.book_reader_js.hasNoYuckiness(aoNode)) { 
			if (sTag == "a" && (aoNode.href)) {
				subhash_browser_js.book_reader_js.sHtml += "<" + sTag + 
                                         " href=\"" + aoNode.href  + "\">"; 
			} else {
				subhash_browser_js.book_reader_js.sHtml += "<" + sTag + ">"; 
			}
		}
		for (var i = 0; i < aoNode.childNodes.length; i++) {
			var oNode = aoNode.childNodes[i];
			subhash_browser_js.book_reader_js.isTitleTag(oNode.nodeName.toLowerCase());
			if (oNode.nodeType == Node.ELEMENT_NODE) {
				subhash_browser_js.book_reader_js.parseElement(oNode);
			} else if (oNode.nodeType == Node.TEXT_NODE) {
				if (subhash_browser_js.book_reader_js.bTitleFound) { 
					subhash_browser_js.book_reader_js.sHtml += oNode.nodeValue;
				}
			}
		}
		if (subhash_browser_js.book_reader_js.bTitleFound && 
				subhash_browser_js.book_reader_js.isUsefulTag(sTag)) {
			subhash_browser_js.book_reader_js.sHtml += "</" + sTag + ">";
		}
		//console.error("Html is : " + subhash_browser_js.book_reader_js.sHtml);
	},
	
	parseElement: function(aoEl) {
		if (window.getComputedStyle(aoEl)) {
			if (window.getComputedStyle(aoEl).getPropertyValue("display") == "none") {
				try { console.error("Ignoring hidden element: " + 
                      aoEl.outerHTML.substr(0,300)); } catch (e) {}
		  	return;
			}
		}
		//console.error("Checking element " + aoEl.tagName);
		subhash_browser_js.book_reader_js.isTitleTag(aoEl.tagName.toLowerCase());
		if (aoEl.children.length > 0) {
			subhash_browser_js.book_reader_js.parseNode(aoEl);
		} else if (subhash_browser_js.book_reader_js.bTitleFound) {
			subhash_browser_js.book_reader_js.parseFiniteElement(aoEl);
		}
	},

	changeToReader: function() {
		try {
			subhash_browser_js.book_reader_js.addNoYuckiesStyle(); 
			subhash_browser_js.book_reader_js.removeUnwantedTags();
			subhash_browser_js.book_reader_js.createHeader();
			var	oEl = document.getElementsByTagName("body")[0];
			subhash_browser_js.book_reader_js.parseElement(oEl);
			subhash_browser_js.book_reader_js.sHtml += "</body>\n";
			document.getElementsByTagName("html")[0].innerHTML = subhash_browser_js.book_reader_js.sHtml;
		} catch (e) {
			console.error("Subhash Browser BRV Error" + e);
		}
	},

	handle_DOMLoaded: function() {
		try {
			window.setTimeout(
				function() {
					subhash_browser_js.book_reader_js.changeToReader();
				}, 
				5*1000);
		} catch (e) {
			console.error("Subhash Browser BRV Error: " + e);
		}
	}
}

document.addEventListener
  ("DOMContentLoaded", subhash_browser_js.book_reader_js.handle_DOMLoaded, false);

一些示例

许多网站提供 HTML5,但它们没有以在上下文中具有意义的方式使用这些标签。 这是一个 Verge 网站文章的样子。 它没有太多问题。

一些网站将其 H1(标题)标签放在 header 标签内,而不是 article 或 main 标签内。 显然,他们的理由是文章标题应该放在 header 中! 有些网站根本不使用 h-tag 层次结构,所有内容都放在带有内联 CSS 样式的 DIV 框中。 在这些网站上使用此 JavaScript 绝对不值得。

这是一个随机选择的 CodeProject 文章页面,顺便说一句,是我写的。

我不得不对我的原始代码进行更改,因为 CodeProject 将整个文章放在一个 form 标签中。 原始版本会删除 form 标签以及其他标签,例如 script、footer 和 aside。 为什么 CodeProject 会将其文章放在 form 标签中? 这是一个 ASP.NET 的东西吗?

流行的博客工具 WordPress 更糟糕。 它获取博主添加到博客文章的标签/类别,并将它们附加到包含帖子内容的 div 的 class 属性中。 因此,如果博主向博客文章添加类别 "aside",则此 JavaScript 将丢弃整个帖子。 因此,某些网站软件的失败可能性很高。

这是一篇 AP 新闻文章。

An AP News article in Book Reader mode

它在某些地方看起来很粗糙,但这就是网页设计师向搜索引擎和辅助功能应用程序呈现内容的方式。 这是您在选择内容管理系统或服务器脚本技术之前应该仔细考虑的另一个原因。

这是在我的 Android 应用程序中的样子。

请注意,当您从移动设备浏览时,服务器可能会发送同一页面的不同版本,该版本是为该设备定制的。 因此,此 JavaScript 的工作方式可能与在桌面上不同。 但是,如果移动页面的 HTML 标签组织是内容相关的,那么应该没有问题。

关注点

该代码根据其标签、类名和 ID 属性消除 HTML 元素。 为了使代码可定制,它使用 JavaScript 数组,可以轻松地在其中添加或删除标签、类和 ID。 该代码最初通过丢弃所有标签来工作,直到找到标题。 它假设标题可能在 H1H2H3 标签中。 然后,它解析剩余的标签 - 将其自身限制为段落、列表、文本节点、图像和超链接,并丢弃其他所有内容。

这项练习让我研究了流行网站的内部结构,坦率地说,我对流行的 CMS 软件和 JavaScript 框架转储到网页中的大量无用垃圾感到厌恶。 社交媒体插件是主要罪魁祸首。 它们不仅增加了页面的重量,而且还阻止了内容的加载。 如果网页中没有那么多 JavaScript,世界上的服务器集群将消耗更少的能量。

© . All rights reserved.