From 14bc7398d873c2fb6f41ac89dff93aaeefa96e4e Mon Sep 17 00:00:00 2001
From: Innei
Date: Thu, 1 Aug 2024 17:32:03 +0800
Subject: [PATCH 1/2] feat: init readability
Signed-off-by: Innei
---
icons/mgc/sparkles_2_cute_re.svg | 1 +
icons/mgc/sparkles_2_filled.svg | 1 +
package.json | 6 +-
patches/@mozilla__readability@0.5.0.patch | 2109 +++++++++++++++++
pnpm-lock.yaml | 301 ++-
src/main/lib/readability.ts | 24 +
src/main/tipc/index.ts | 2 +
src/main/tipc/reader.ts | 17 +
src/renderer/src/atoms/readability.ts | 53 +
.../src/hooks/biz/useEntryActions.tsx | 57 +-
.../src/modules/entry-column/item.tsx | 2 +-
.../entry-column/social-media-item.tsx | 2 +-
.../src/modules/entry-content/header.tsx | 3 +-
.../src/modules/entry-content/index.tsx | 38 +-
14 files changed, 2596 insertions(+), 20 deletions(-)
create mode 100644 icons/mgc/sparkles_2_cute_re.svg
create mode 100644 icons/mgc/sparkles_2_filled.svg
create mode 100644 patches/@mozilla__readability@0.5.0.patch
create mode 100644 src/main/lib/readability.ts
create mode 100644 src/main/tipc/reader.ts
create mode 100644 src/renderer/src/atoms/readability.ts
diff --git a/icons/mgc/sparkles_2_cute_re.svg b/icons/mgc/sparkles_2_cute_re.svg
new file mode 100644
index 0000000000..068f112798
--- /dev/null
+++ b/icons/mgc/sparkles_2_cute_re.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/mgc/sparkles_2_filled.svg b/icons/mgc/sparkles_2_filled.svg
new file mode 100644
index 0000000000..f7e901c408
--- /dev/null
+++ b/icons/mgc/sparkles_2_filled.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/package.json b/package.json
index 5ffd8794b7..ed5f56671c 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"@hono/auth-js": "1.0.10",
"@hookform/resolvers": "3.9.0",
"@iconify/tools": "4.0.4",
+ "@mozilla/readability": "^0.5.0",
"@radix-ui/react-alert-dialog": "1.1.1",
"@radix-ui/react-avatar": "1.1.0",
"@radix-ui/react-checkbox": "1.1.1",
@@ -89,7 +90,9 @@
"idb-keyval": "6.2.1",
"immer": "10.1.1",
"jotai": "2.9.1",
+ "jsdom": "^24.1.1",
"lethargy": "1.0.9",
+ "linkedom": "^0.18.4",
"lodash-es": "4.17.21",
"lowdb": "7.0.1",
"markdown-it": "14.1.0",
@@ -171,7 +174,8 @@
"patchedDependencies": {
"sonner@1.5.0": "patches/sonner@1.5.0.patch",
"hono@4.4.7": "patches/hono@4.4.7.patch",
- "immer@10.1.1": "patches/immer@10.1.1.patch"
+ "immer@10.1.1": "patches/immer@10.1.1.patch",
+ "@mozilla/readability@0.5.0": "patches/@mozilla__readability@0.5.0.patch"
}
},
"simple-git-hooks": {
diff --git a/patches/@mozilla__readability@0.5.0.patch b/patches/@mozilla__readability@0.5.0.patch
new file mode 100644
index 0000000000..7ea789420c
--- /dev/null
+++ b/patches/@mozilla__readability@0.5.0.patch
@@ -0,0 +1,2109 @@
+diff --git a/Readability.js b/Readability.js
+index b745aa01d8ea23e55c5c309c29262f6fa0a74a01..21756f48574d33a7bd35ce7d47be106f922a9908 100644
+--- a/Readability.js
++++ b/Readability.js
+@@ -30,7 +30,9 @@ function Readability(doc, options) {
+ doc = options;
+ options = arguments[2];
+ } else if (!doc || !doc.documentElement) {
+- throw new Error("First argument to Readability constructor should be a document object.");
++ throw new Error(
++ "First argument to Readability constructor should be a document object."
++ );
+ }
+ options = options || {};
+
+@@ -44,37 +46,43 @@ function Readability(doc, options) {
+
+ // Configurable options
+ this._debug = !!options.debug;
+- this._maxElemsToParse = options.maxElemsToParse || this.DEFAULT_MAX_ELEMS_TO_PARSE;
+- this._nbTopCandidates = options.nbTopCandidates || this.DEFAULT_N_TOP_CANDIDATES;
++ this._maxElemsToParse =
++ options.maxElemsToParse || this.DEFAULT_MAX_ELEMS_TO_PARSE;
++ this._nbTopCandidates =
++ options.nbTopCandidates || this.DEFAULT_N_TOP_CANDIDATES;
+ this._charThreshold = options.charThreshold || this.DEFAULT_CHAR_THRESHOLD;
+- this._classesToPreserve = this.CLASSES_TO_PRESERVE.concat(options.classesToPreserve || []);
++ this._classesToPreserve = this.CLASSES_TO_PRESERVE.concat(
++ options.classesToPreserve || []
++ );
+ this._keepClasses = !!options.keepClasses;
+- this._serializer = options.serializer || function(el) {
+- return el.innerHTML;
+- };
++ this._serializer =
++ options.serializer ||
++ function (el) {
++ return el.innerHTML;
++ };
+ this._disableJSONLD = !!options.disableJSONLD;
+ this._allowedVideoRegex = options.allowedVideoRegex || this.REGEXPS.videos;
+
+ // Start with all flags set
+- this._flags = this.FLAG_STRIP_UNLIKELYS |
+- this.FLAG_WEIGHT_CLASSES |
+- this.FLAG_CLEAN_CONDITIONALLY;
+-
++ this._flags =
++ this.FLAG_STRIP_UNLIKELYS |
++ this.FLAG_WEIGHT_CLASSES |
++ this.FLAG_CLEAN_CONDITIONALLY;
+
+ // Control whether log messages are sent to the console
+ if (this._debug) {
+- let logNode = function(node) {
++ let logNode = function (node) {
+ if (node.nodeType == node.TEXT_NODE) {
+ return `${node.nodeName} ("${node.textContent}")`;
+ }
+- let attrPairs = Array.from(node.attributes || [], function(attr) {
++ let attrPairs = Array.from(node.attributes || [], function (attr) {
+ return `${attr.name}="${attr.value}"`;
+ }).join(" ");
+ return `<${node.localName} ${attrPairs}>`;
+ };
+ this.log = function () {
+ if (typeof console !== "undefined") {
+- let args = Array.from(arguments, arg => {
++ let args = Array.from(arguments, (arg) => {
+ if (arg && arg.nodeType == this.ELEMENT_NODE) {
+ return logNode(arg);
+ }
+@@ -84,9 +92,11 @@ function Readability(doc, options) {
+ console.log.apply(console, args);
+ } else if (typeof dump !== "undefined") {
+ /* global dump */
+- var msg = Array.prototype.map.call(arguments, function(x) {
+- return (x && x.nodeName) ? logNode(x) : x;
+- }).join(" ");
++ var msg = Array.prototype.map
++ .call(arguments, function (x) {
++ return x && x.nodeName ? logNode(x) : x;
++ })
++ .join(" ");
+ dump("Reader: (Readability) " + msg + "\n");
+ }
+ };
+@@ -112,7 +122,9 @@ Readability.prototype = {
+ DEFAULT_N_TOP_CANDIDATES: 5,
+
+ // Element tags to score by default.
+- DEFAULT_TAGS_TO_SCORE: "section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","),
++ DEFAULT_TAGS_TO_SCORE: "section,h2,h3,h4,h5,h6,p,td,pre"
++ .toUpperCase()
++ .split(","),
+
+ // The default number of chars an article must have in order to return a result
+ DEFAULT_CHAR_THRESHOLD: 500,
+@@ -122,16 +134,21 @@ Readability.prototype = {
+ REGEXPS: {
+ // NOTE: These two regular expressions are duplicated in
+ // Readability-readerable.js. Please keep both copies in sync.
+- unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
++ unlikelyCandidates:
++ /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
+ okMaybeItsACandidate: /and|article|body|column|content|main|shadow/i,
+
+- positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,
+- negative: /-ad-|hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
+- extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,
++ positive:
++ /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,
++ negative:
++ /-ad-|hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
++ extraneous:
++ /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,
+ byline: /byline|author|dateline|writtenby|p-author/i,
+ replaceFonts: /<(\/?)font[^>]*>/gi,
+ normalize: /\s{2,}/g,
+- videos: /\/\/(www\.)?((dailymotion|youtube|youtube-nocookie|player\.vimeo|v\.qq)\.com|(archive|upload\.wikimedia)\.org|player\.twitch\.tv)/i,
++ videos:
++ /\/\/(www\.)?((dailymotion|youtube|youtube-nocookie|player\.vimeo|v\.qq)\.com|(archive|upload\.wikimedia)\.org|player\.twitch\.tv)/i,
+ shareElements: /(\b|_)(share|sharedaddy)(\b|_)/i,
+ nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
+ prevLink: /(prev|earl|old|new|<|«)/i,
+@@ -145,40 +162,106 @@ Readability.prototype = {
+ // see: https://en.wikipedia.org/wiki/Comma#Comma_variants
+ commas: /\u002C|\u060C|\uFE50|\uFE10|\uFE11|\u2E41|\u2E34|\u2E32|\uFF0C/g,
+ // See: https://schema.org/Article
+- jsonLdArticleTypes: /^Article|AdvertiserContentArticle|NewsArticle|AnalysisNewsArticle|AskPublicNewsArticle|BackgroundNewsArticle|OpinionNewsArticle|ReportageNewsArticle|ReviewNewsArticle|Report|SatiricalArticle|ScholarlyArticle|MedicalScholarlyArticle|SocialMediaPosting|BlogPosting|LiveBlogPosting|DiscussionForumPosting|TechArticle|APIReference$/
++ jsonLdArticleTypes:
++ /^Article|AdvertiserContentArticle|NewsArticle|AnalysisNewsArticle|AskPublicNewsArticle|BackgroundNewsArticle|OpinionNewsArticle|ReportageNewsArticle|ReviewNewsArticle|Report|SatiricalArticle|ScholarlyArticle|MedicalScholarlyArticle|SocialMediaPosting|BlogPosting|LiveBlogPosting|DiscussionForumPosting|TechArticle|APIReference$/,
+ },
+
+- UNLIKELY_ROLES: [ "menu", "menubar", "complementary", "navigation", "alert", "alertdialog", "dialog" ],
++ UNLIKELY_ROLES: [
++ "menu",
++ "menubar",
++ "complementary",
++ "navigation",
++ "alert",
++ "alertdialog",
++ "dialog",
++ ],
+
+- DIV_TO_P_ELEMS: new Set([ "BLOCKQUOTE", "DL", "DIV", "IMG", "OL", "P", "PRE", "TABLE", "UL" ]),
++ DIV_TO_P_ELEMS: new Set([
++ "BLOCKQUOTE",
++ "DL",
++ "DIV",
++ "IMG",
++ "OL",
++ "P",
++ "PRE",
++ "TABLE",
++ "UL",
++ ]),
+
+ ALTER_TO_DIV_EXCEPTIONS: ["DIV", "ARTICLE", "SECTION", "P"],
+
+- PRESENTATIONAL_ATTRIBUTES: [ "align", "background", "bgcolor", "border", "cellpadding", "cellspacing", "frame", "hspace", "rules", "style", "valign", "vspace" ],
++ PRESENTATIONAL_ATTRIBUTES: [
++ "align",
++ "background",
++ "bgcolor",
++ "border",
++ "cellpadding",
++ "cellspacing",
++ "frame",
++ "hspace",
++ "rules",
++ "style",
++ "valign",
++ "vspace",
++ ],
+
+- DEPRECATED_SIZE_ATTRIBUTE_ELEMS: [ "TABLE", "TH", "TD", "HR", "PRE" ],
++ DEPRECATED_SIZE_ATTRIBUTE_ELEMS: ["TABLE", "TH", "TD", "HR", "PRE"],
+
+ // The commented out elements qualify as phrasing content but tend to be
+ // removed by readability when put into paragraphs, so we ignore them here.
+ PHRASING_ELEMS: [
+ // "CANVAS", "IFRAME", "SVG", "VIDEO",
+- "ABBR", "AUDIO", "B", "BDO", "BR", "BUTTON", "CITE", "CODE", "DATA",
+- "DATALIST", "DFN", "EM", "EMBED", "I", "IMG", "INPUT", "KBD", "LABEL",
+- "MARK", "MATH", "METER", "NOSCRIPT", "OBJECT", "OUTPUT", "PROGRESS", "Q",
+- "RUBY", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRONG", "SUB",
+- "SUP", "TEXTAREA", "TIME", "VAR", "WBR"
++ "ABBR",
++ "AUDIO",
++ "B",
++ "BDO",
++ "BR",
++ "BUTTON",
++ "CITE",
++ "CODE",
++ "DATA",
++ "DATALIST",
++ "DFN",
++ "EM",
++ "EMBED",
++ "I",
++ "IMG",
++ "INPUT",
++ "KBD",
++ "LABEL",
++ "MARK",
++ "MATH",
++ "METER",
++ "NOSCRIPT",
++ "OBJECT",
++ "OUTPUT",
++ "PROGRESS",
++ "Q",
++ "RUBY",
++ "SAMP",
++ "SCRIPT",
++ "SELECT",
++ "SMALL",
++ "SPAN",
++ "STRONG",
++ "SUB",
++ "SUP",
++ "TEXTAREA",
++ "TIME",
++ "VAR",
++ "WBR",
+ ],
+
+ // These are the classes that readability sets itself.
+- CLASSES_TO_PRESERVE: [ "page" ],
++ CLASSES_TO_PRESERVE: ["page"],
+
+ // These are the list of HTML entities that need to be escaped.
+ HTML_ESCAPE_MAP: {
+- "lt": "<",
+- "gt": ">",
+- "amp": "&",
+- "quot": '"',
+- "apos": "'",
++ lt: "<",
++ gt: ">",
++ amp: "&",
++ quot: '"',
++ apos: "'",
+ },
+
+ /**
+@@ -186,8 +269,8 @@ Readability.prototype = {
+ *
+ * @param Element
+ * @return void
+- **/
+- _postProcessContent: function(articleContent) {
++ **/
++ _postProcessContent: function (articleContent) {
+ // Readability cannot open relative uris so we convert them to absolute uris.
+ this._fixRelativeUris(articleContent);
+
+@@ -209,7 +292,7 @@ Readability.prototype = {
+ * @param Function filterFn the function to use as a filter
+ * @return void
+ */
+- _removeNodes: function(nodeList, filterFn) {
++ _removeNodes: function (nodeList, filterFn) {
+ // Avoid ever operating on live node lists.
+ if (this._docJSDOMParser && nodeList._isLiveNodeList) {
+ throw new Error("Do not pass live node lists to _removeNodes");
+@@ -232,7 +315,7 @@ Readability.prototype = {
+ * @param String newTagName the new tag name to use
+ * @return void
+ */
+- _replaceNodeTags: function(nodeList, newTagName) {
++ _replaceNodeTags: function (nodeList, newTagName) {
+ // Avoid ever operating on live node lists.
+ if (this._docJSDOMParser && nodeList._isLiveNodeList) {
+ throw new Error("Do not pass live node lists to _replaceNodeTags");
+@@ -253,7 +336,7 @@ Readability.prototype = {
+ * @param Function fn The iterate function.
+ * @return void
+ */
+- _forEachNode: function(nodeList, fn) {
++ _forEachNode: function (nodeList, fn) {
+ Array.prototype.forEach.call(nodeList, fn, this);
+ },
+
+@@ -268,7 +351,7 @@ Readability.prototype = {
+ * @param Function fn The test function.
+ * @return void
+ */
+- _findNode: function(nodeList, fn) {
++ _findNode: function (nodeList, fn) {
+ return Array.prototype.find.call(nodeList, fn, this);
+ },
+
+@@ -283,7 +366,7 @@ Readability.prototype = {
+ * @param Function fn The iterate function.
+ * @return Boolean
+ */
+- _someNode: function(nodeList, fn) {
++ _someNode: function (nodeList, fn) {
+ return Array.prototype.some.call(nodeList, fn, this);
+ },
+
+@@ -298,7 +381,7 @@ Readability.prototype = {
+ * @param Function fn The iterate function.
+ * @return Boolean
+ */
+- _everyNode: function(nodeList, fn) {
++ _everyNode: function (nodeList, fn) {
+ return Array.prototype.every.call(nodeList, fn, this);
+ },
+
+@@ -308,23 +391,26 @@ Readability.prototype = {
+ * @return ...NodeList
+ * @return Array
+ */
+- _concatNodeLists: function() {
++ _concatNodeLists: function () {
+ var slice = Array.prototype.slice;
+ var args = slice.call(arguments);
+- var nodeLists = args.map(function(list) {
++ var nodeLists = args.map(function (list) {
+ return slice.call(list);
+ });
+ return Array.prototype.concat.apply([], nodeLists);
+ },
+
+- _getAllNodesWithTag: function(node, tagNames) {
++ _getAllNodesWithTag: function (node, tagNames) {
+ if (node.querySelectorAll) {
+ return node.querySelectorAll(tagNames.join(","));
+ }
+- return [].concat.apply([], tagNames.map(function(tag) {
+- var collection = node.getElementsByTagName(tag);
+- return Array.isArray(collection) ? collection : Array.from(collection);
+- }));
++ return [].concat.apply(
++ [],
++ tagNames.map(function (tag) {
++ var collection = node.getElementsByTagName(tag);
++ return Array.isArray(collection) ? collection : Array.from(collection);
++ })
++ );
+ },
+
+ /**
+@@ -335,11 +421,11 @@ Readability.prototype = {
+ * @param Element
+ * @return void
+ */
+- _cleanClasses: function(node) {
++ _cleanClasses: function (node) {
+ var classesToPreserve = this._classesToPreserve;
+ var className = (node.getAttribute("class") || "")
+ .split(/\s+/)
+- .filter(function(cls) {
++ .filter(function (cls) {
+ return classesToPreserve.indexOf(cls) != -1;
+ })
+ .join(" ");
+@@ -362,7 +448,7 @@ Readability.prototype = {
+ * @param Element
+ * @return void
+ */
+- _fixRelativeUris: function(articleContent) {
++ _fixRelativeUris: function (articleContent) {
+ var baseURI = this._doc.baseURI;
+ var documentURI = this._doc.documentURI;
+ function toAbsoluteURI(uri) {
+@@ -381,14 +467,17 @@ Readability.prototype = {
+ }
+
+ var links = this._getAllNodesWithTag(articleContent, ["a"]);
+- this._forEachNode(links, function(link) {
++ this._forEachNode(links, function (link) {
+ var href = link.getAttribute("href");
+ if (href) {
+ // Remove links with javascript: URIs, since
+ // they won't work after scripts have been removed from the page.
+ if (href.indexOf("javascript:") === 0) {
+ // if the link only contains simple text content, it can be converted to a text node
+- if (link.childNodes.length === 1 && link.childNodes[0].nodeType === this.TEXT_NODE) {
++ if (
++ link.childNodes.length === 1 &&
++ link.childNodes[0].nodeType === this.TEXT_NODE
++ ) {
+ var text = this._doc.createTextNode(link.textContent);
+ link.parentNode.replaceChild(text, link);
+ } else {
+@@ -406,10 +495,15 @@ Readability.prototype = {
+ });
+
+ var medias = this._getAllNodesWithTag(articleContent, [
+- "img", "picture", "figure", "video", "audio", "source"
++ "img",
++ "picture",
++ "figure",
++ "video",
++ "audio",
++ "source",
+ ]);
+
+- this._forEachNode(medias, function(media) {
++ this._forEachNode(medias, function (media) {
+ var src = media.getAttribute("src");
+ var poster = media.getAttribute("poster");
+ var srcset = media.getAttribute("srcset");
+@@ -423,27 +517,40 @@ Readability.prototype = {
+ }
+
+ if (srcset) {
+- var newSrcset = srcset.replace(this.REGEXPS.srcsetUrl, function(_, p1, p2, p3) {
+- return toAbsoluteURI(p1) + (p2 || "") + p3;
+- });
++ var newSrcset = srcset.replace(
++ this.REGEXPS.srcsetUrl,
++ function (_, p1, p2, p3) {
++ return toAbsoluteURI(p1) + (p2 || "") + p3;
++ }
++ );
+
+ media.setAttribute("srcset", newSrcset);
+ }
+ });
+ },
+
+- _simplifyNestedElements: function(articleContent) {
++ _simplifyNestedElements: function (articleContent) {
+ var node = articleContent;
+
+ while (node) {
+- if (node.parentNode && ["DIV", "SECTION"].includes(node.tagName) && !(node.id && node.id.startsWith("readability"))) {
++ if (
++ node.parentNode &&
++ ["DIV", "SECTION"].includes(node.tagName) &&
++ !(node.id && node.id.startsWith("readability"))
++ ) {
+ if (this._isElementWithoutContent(node)) {
+ node = this._removeAndGetNext(node);
+ continue;
+- } else if (this._hasSingleTagInsideElement(node, "DIV") || this._hasSingleTagInsideElement(node, "SECTION")) {
++ } else if (
++ this._hasSingleTagInsideElement(node, "DIV") ||
++ this._hasSingleTagInsideElement(node, "SECTION")
++ ) {
+ var child = node.children[0];
+ for (var i = 0; i < node.attributes.length; i++) {
+- child.setAttribute(node.attributes[i].name, node.attributes[i].value);
++ child.setAttribute(
++ node.attributes[i].name,
++ node.attributes[i].value
++ );
+ }
+ node.parentNode.replaceChild(child, node);
+ node = child;
+@@ -460,7 +567,7 @@ Readability.prototype = {
+ *
+ * @return string
+ **/
+- _getArticleTitle: function() {
++ _getArticleTitle: function () {
+ var doc = this._doc;
+ var curTitle = "";
+ var origTitle = "";
+@@ -470,8 +577,12 @@ Readability.prototype = {
+
+ // If they had an element with id "title" in their HTML
+ if (typeof curTitle !== "string")
+- curTitle = origTitle = this._getInnerText(doc.getElementsByTagName("title")[0]);
+- } catch (e) {/* ignore exceptions setting the title. */}
++ curTitle = origTitle = this._getInnerText(
++ doc.getElementsByTagName("title")[0]
++ );
++ } catch (e) {
++ /* ignore exceptions setting the title. */
++ }
+
+ var titleHadHierarchicalSeparators = false;
+ function wordCount(str) {
+@@ -479,7 +590,7 @@ Readability.prototype = {
+ }
+
+ // If there's a separator in the title, first remove the final part
+- if ((/ [\|\-\\\/>»] /).test(curTitle)) {
++ if (/ [\|\-\\\/>»] /.test(curTitle)) {
+ titleHadHierarchicalSeparators = / [\\\/>»] /.test(curTitle);
+ curTitle = origTitle.replace(/(.*)[\|\-\\\/>»] .*/gi, "$1");
+
+@@ -495,7 +606,7 @@ Readability.prototype = {
+ doc.getElementsByTagName("h2")
+ );
+ var trimmedTitle = curTitle.trim();
+- var match = this._someNode(headings, function(heading) {
++ var match = this._someNode(headings, function (heading) {
+ return heading.textContent.trim() === trimmedTitle;
+ });
+
+@@ -515,8 +626,7 @@ Readability.prototype = {
+ } else if (curTitle.length > 150 || curTitle.length < 15) {
+ var hOnes = doc.getElementsByTagName("h1");
+
+- if (hOnes.length === 1)
+- curTitle = this._getInnerText(hOnes[0]);
++ if (hOnes.length === 1) curTitle = this._getInnerText(hOnes[0]);
+ }
+
+ curTitle = curTitle.trim().replace(this.REGEXPS.normalize, " ");
+@@ -525,9 +635,12 @@ Readability.prototype = {
+ // title or we decreased the number of words by more than 1 word, use
+ // the original title.
+ var curTitleWordCount = wordCount(curTitle);
+- if (curTitleWordCount <= 4 &&
+- (!titleHadHierarchicalSeparators ||
+- curTitleWordCount != wordCount(origTitle.replace(/[\|\-\\\/>»]+/g, "")) - 1)) {
++ if (
++ curTitleWordCount <= 4 &&
++ (!titleHadHierarchicalSeparators ||
++ curTitleWordCount !=
++ wordCount(origTitle.replace(/[\|\-\\\/>»]+/g, "")) - 1)
++ ) {
+ curTitle = origTitle;
+ }
+
+@@ -540,7 +653,7 @@ Readability.prototype = {
+ *
+ * @return void
+ **/
+- _prepDocument: function() {
++ _prepDocument: function () {
+ var doc = this._doc;
+
+ // Remove all style tags in head
+@@ -560,9 +673,11 @@ Readability.prototype = {
+ */
+ _nextNode: function (node) {
+ var next = node;
+- while (next
+- && (next.nodeType != this.ELEMENT_NODE)
+- && this.REGEXPS.whitespace.test(next.textContent)) {
++ while (
++ next &&
++ next.nodeType != this.ELEMENT_NODE &&
++ this.REGEXPS.whitespace.test(next.textContent)
++ ) {
+ next = next.nextSibling;
+ }
+ return next;
+@@ -576,7 +691,7 @@ Readability.prototype = {
+ * abc
bar
elements have been found and replaced with a
+@@ -586,7 +701,7 @@ Readability.prototype = {
+ // If we find a
chain, remove the
s until we hit another node
+ // or non-whitespace. This leaves behind the first
in the chain
+ // (which will be replaced with a
later).
+- while ((next = this._nextNode(next)) && (next.tagName == "BR")) {
++ while ((next = this._nextNode(next)) && next.tagName == "BR") {
+ replaced = true;
+ var brSibling = next.nextSibling;
+ next.parentNode.removeChild(next);
+@@ -605,12 +720,10 @@ Readability.prototype = {
+ // If we've hit another
, we're done adding children to this
. + if (next.tagName == "BR") { + var nextElem = this._nextNode(next.nextSibling); +- if (nextElem && nextElem.tagName == "BR") +- break; ++ if (nextElem && nextElem.tagName == "BR") break; + } + +- if (!this._isPhrasingContent(next)) +- break; ++ if (!this._isPhrasingContent(next)) break; + + // Otherwise, make this node a child of the new
. + var sibling = next.nextSibling; +@@ -622,8 +735,7 @@ Readability.prototype = { + p.removeChild(p.lastChild); + } + +- if (p.parentNode.tagName === "P") +- this._setNodeTag(p.parentNode, "DIV"); ++ if (p.parentNode.tagName === "P") this._setNodeTag(p.parentNode, "DIV"); + } + }); + }, +@@ -641,12 +753,14 @@ Readability.prototype = { + replacement.appendChild(node.firstChild); + } + node.parentNode.replaceChild(replacement, node); +- if (node.readability) +- replacement.readability = node.readability; ++ if (node.readability) replacement.readability = node.readability; + + for (var i = 0; i < node.attributes.length; i++) { + try { +- replacement.setAttribute(node.attributes[i].name, node.attributes[i].value); ++ replacement.setAttribute( ++ node.attributes[i].name, ++ node.attributes[i].value ++ ); + } catch (ex) { + /* it's possible for setAttribute() to throw if the attribute name + * isn't a valid XML Name. Such attributes can however be parsed from +@@ -666,7 +780,7 @@ Readability.prototype = { + * @param Element + * @return void + **/ +- _prepArticle: function(articleContent) { ++ _prepArticle: function (articleContent) { + this._cleanStyles(articleContent); + + // Check for data tables before we continue, to avoid removing items in +@@ -692,7 +806,10 @@ Readability.prototype = { + + this._forEachNode(articleContent.children, function (topCandidate) { + this._cleanMatchedNodes(topCandidate, function (node, matchString) { +- return this.REGEXPS.shareElements.test(matchString) && node.textContent.length < shareElementThreshold; ++ return ( ++ this.REGEXPS.shareElements.test(matchString) && ++ node.textContent.length < shareElementThreshold ++ ); + }); + }); + +@@ -710,38 +827,56 @@ Readability.prototype = { + this._cleanConditionally(articleContent, "div"); + + // replace H1 with H2 as H1 should be only title that is displayed separately +- this._replaceNodeTags(this._getAllNodesWithTag(articleContent, ["h1"]), "h2"); ++ this._replaceNodeTags( ++ this._getAllNodesWithTag(articleContent, ["h1"]), ++ "h2" ++ ); + + // Remove extra paragraphs +- this._removeNodes(this._getAllNodesWithTag(articleContent, ["p"]), function (paragraph) { +- var imgCount = paragraph.getElementsByTagName("img").length; +- var embedCount = paragraph.getElementsByTagName("embed").length; +- var objectCount = paragraph.getElementsByTagName("object").length; +- // At this point, nasty iframes have been removed, only remain embedded video ones. +- var iframeCount = paragraph.getElementsByTagName("iframe").length; +- var totalCount = imgCount + embedCount + objectCount + iframeCount; +- +- return totalCount === 0 && !this._getInnerText(paragraph, false); +- }); ++ this._removeNodes( ++ this._getAllNodesWithTag(articleContent, ["p"]), ++ function (paragraph) { ++ var imgCount = paragraph.getElementsByTagName("img").length; ++ var embedCount = paragraph.getElementsByTagName("embed").length; ++ var objectCount = paragraph.getElementsByTagName("object").length; ++ // At this point, nasty iframes have been removed, only remain embedded video ones. ++ var iframeCount = paragraph.getElementsByTagName("iframe").length; ++ var totalCount = imgCount + embedCount + objectCount + iframeCount; ++ ++ return totalCount === 0 && !this._getInnerText(paragraph, false); ++ } ++ ); + +- this._forEachNode(this._getAllNodesWithTag(articleContent, ["br"]), function(br) { +- var next = this._nextNode(br.nextSibling); +- if (next && next.tagName == "P") +- br.parentNode.removeChild(br); +- }); ++ this._forEachNode( ++ this._getAllNodesWithTag(articleContent, ["br"]), ++ function (br) { ++ var next = this._nextNode(br.nextSibling); ++ if (next && next.tagName == "P") br.parentNode.removeChild(br); ++ } ++ ); + + // Remove single-cell tables +- this._forEachNode(this._getAllNodesWithTag(articleContent, ["table"]), function(table) { +- var tbody = this._hasSingleTagInsideElement(table, "TBODY") ? table.firstElementChild : table; +- if (this._hasSingleTagInsideElement(tbody, "TR")) { +- var row = tbody.firstElementChild; +- if (this._hasSingleTagInsideElement(row, "TD")) { +- var cell = row.firstElementChild; +- cell = this._setNodeTag(cell, this._everyNode(cell.childNodes, this._isPhrasingContent) ? "P" : "DIV"); +- table.parentNode.replaceChild(cell, table); ++ this._forEachNode( ++ this._getAllNodesWithTag(articleContent, ["table"]), ++ function (table) { ++ var tbody = this._hasSingleTagInsideElement(table, "TBODY") ++ ? table.firstElementChild ++ : table; ++ if (this._hasSingleTagInsideElement(tbody, "TR")) { ++ var row = tbody.firstElementChild; ++ if (this._hasSingleTagInsideElement(row, "TD")) { ++ var cell = row.firstElementChild; ++ cell = this._setNodeTag( ++ cell, ++ this._everyNode(cell.childNodes, this._isPhrasingContent) ++ ? "P" ++ : "DIV" ++ ); ++ table.parentNode.replaceChild(cell, table); ++ } + } + } +- }); ++ ); + }, + + /** +@@ -750,9 +885,9 @@ Readability.prototype = { + * + * @param Element + * @return void +- **/ +- _initializeNode: function(node) { +- node.readability = {"contentScore": 0}; ++ **/ ++ _initializeNode: function (node) { ++ node.readability = { contentScore: 0 }; + + switch (node.tagName) { + case "DIV": +@@ -790,7 +925,7 @@ Readability.prototype = { + node.readability.contentScore += this._getClassWeight(node); + }, + +- _removeAndGetNext: function(node) { ++ _removeAndGetNext: function (node) { + var nextNode = this._getNextNode(node, true); + node.parentNode.removeChild(node); + return nextNode; +@@ -803,7 +938,7 @@ Readability.prototype = { + * + * Calling this in a loop will traverse the DOM depth-first. + */ +- _getNextNode: function(node, ignoreSelfAndKids) { ++ _getNextNode: function (node, ignoreSelfAndKids) { + // First check for kids if those aren't being ignored + if (!ignoreSelfAndKids && node.firstElementChild) { + return node.firstElementChild; +@@ -825,18 +960,24 @@ Readability.prototype = { + // 1 = same text, 0 = completely different text + // works the way that it splits both texts into words and then finds words that are unique in second text + // the result is given by the lower length of unique parts +- _textSimilarity: function(textA, textB) { +- var tokensA = textA.toLowerCase().split(this.REGEXPS.tokenize).filter(Boolean); +- var tokensB = textB.toLowerCase().split(this.REGEXPS.tokenize).filter(Boolean); ++ _textSimilarity: function (textA, textB) { ++ var tokensA = textA ++ .toLowerCase() ++ .split(this.REGEXPS.tokenize) ++ .filter(Boolean); ++ var tokensB = textB ++ .toLowerCase() ++ .split(this.REGEXPS.tokenize) ++ .filter(Boolean); + if (!tokensA.length || !tokensB.length) { + return 0; + } +- var uniqTokensB = tokensB.filter(token => !tokensA.includes(token)); ++ var uniqTokensB = tokensB.filter((token) => !tokensA.includes(token)); + var distanceB = uniqTokensB.join(" ").length / tokensB.join(" ").length; + return 1 - distanceB; + }, + +- _checkByline: function(node, matchString) { ++ _checkByline: function (node, matchString) { + if (this._articleByline) { + return false; + } +@@ -846,7 +987,12 @@ Readability.prototype = { + var itemprop = node.getAttribute("itemprop"); + } + +- if ((rel === "author" || (itemprop && itemprop.indexOf("author") !== -1) || this.REGEXPS.byline.test(matchString)) && this._isValidByline(node.textContent)) { ++ if ( ++ (rel === "author" || ++ (itemprop && itemprop.indexOf("author") !== -1) || ++ this.REGEXPS.byline.test(matchString)) && ++ this._isValidByline(node.textContent) ++ ) { + this._articleByline = node.textContent.trim(); + return true; + } +@@ -854,13 +1000,13 @@ Readability.prototype = { + return false; + }, + +- _getNodeAncestors: function(node, maxDepth) { ++ _getNodeAncestors: function (node, maxDepth) { + maxDepth = maxDepth || 0; +- var i = 0, ancestors = []; ++ var i = 0, ++ ancestors = []; + while (node.parentNode) { + ancestors.push(node.parentNode); +- if (maxDepth && ++i === maxDepth) +- break; ++ if (maxDepth && ++i === maxDepth) break; + node = node.parentNode; + } + return ancestors; +@@ -872,7 +1018,7 @@ Readability.prototype = { + * + * @param page a document to run upon. Needs to be a full document, complete with body. + * @return Element +- **/ ++ **/ + _grabArticle: function (page) { + this.log("**** grabArticle ****"); + var doc = this._doc; +@@ -889,7 +1035,9 @@ Readability.prototype = { + + while (true) { + this.log("Starting grabArticle loop"); +- var stripUnlikelyCandidates = this._flagIsActive(this.FLAG_STRIP_UNLIKELYS); ++ var stripUnlikelyCandidates = this._flagIsActive( ++ this.FLAG_STRIP_UNLIKELYS ++ ); + + // First, node prepping. Trash nodes that look cruddy (like ones with the + // class name "comment", etc), and turn divs into P tags where they have been +@@ -900,7 +1048,6 @@ Readability.prototype = { + let shouldRemoveTitleHeader = true; + + while (node) { +- + if (node.tagName === "HTML") { + this._articleLang = node.getAttribute("lang"); + } +@@ -914,7 +1061,10 @@ Readability.prototype = { + } + + // User is not able to see elements applied with both "aria-modal = true" and "role = dialog" +- if (node.getAttribute("aria-modal") == "true" && node.getAttribute("role") == "dialog") { ++ if ( ++ node.getAttribute("aria-modal") == "true" && ++ node.getAttribute("role") == "dialog" ++ ) { + node = this._removeAndGetNext(node); + continue; + } +@@ -926,7 +1076,11 @@ Readability.prototype = { + } + + if (shouldRemoveTitleHeader && this._headerDuplicatesTitle(node)) { +- this.log("Removing header: ", node.textContent.trim(), this._articleTitle.trim()); ++ this.log( ++ "Removing header: ", ++ node.textContent.trim(), ++ this._articleTitle.trim() ++ ); + shouldRemoveTitleHeader = false; + node = this._removeAndGetNext(node); + continue; +@@ -934,29 +1088,44 @@ Readability.prototype = { + + // Remove unlikely candidates + if (stripUnlikelyCandidates) { +- if (this.REGEXPS.unlikelyCandidates.test(matchString) && +- !this.REGEXPS.okMaybeItsACandidate.test(matchString) && +- !this._hasAncestorTag(node, "table") && +- !this._hasAncestorTag(node, "code") && +- node.tagName !== "BODY" && +- node.tagName !== "A") { ++ if ( ++ this.REGEXPS.unlikelyCandidates.test(matchString) && ++ !this.REGEXPS.okMaybeItsACandidate.test(matchString) && ++ !this._hasAncestorTag(node, "table") && ++ !this._hasAncestorTag(node, "code") && ++ node.tagName !== "BODY" && ++ node.tagName !== "A" ++ ) { + this.log("Removing unlikely candidate - " + matchString); + node = this._removeAndGetNext(node); + continue; + } + + if (this.UNLIKELY_ROLES.includes(node.getAttribute("role"))) { +- this.log("Removing content with role " + node.getAttribute("role") + " - " + matchString); ++ this.log( ++ "Removing content with role " + ++ node.getAttribute("role") + ++ " - " + ++ matchString ++ ); + node = this._removeAndGetNext(node); + continue; + } + } + + // Remove DIV, SECTION, and HEADER nodes without any content(e.g. text, image, video, or iframe). +- if ((node.tagName === "DIV" || node.tagName === "SECTION" || node.tagName === "HEADER" || +- node.tagName === "H1" || node.tagName === "H2" || node.tagName === "H3" || +- node.tagName === "H4" || node.tagName === "H5" || node.tagName === "H6") && +- this._isElementWithoutContent(node)) { ++ if ( ++ (node.tagName === "DIV" || ++ node.tagName === "SECTION" || ++ node.tagName === "HEADER" || ++ node.tagName === "H1" || ++ node.tagName === "H2" || ++ node.tagName === "H3" || ++ node.tagName === "H4" || ++ node.tagName === "H5" || ++ node.tagName === "H6") && ++ this._isElementWithoutContent(node) ++ ) { + node = this._removeAndGetNext(node); + continue; + } +@@ -993,7 +1162,10 @@ Readability.prototype = { + // element. DIVs with only a P element inside and no text content can be + // safely converted into plain P elements to avoid confusing the scoring + // algorithm with DIVs with are, in practice, paragraphs. +- if (this._hasSingleTagInsideElement(node, "P") && this._getLinkDensity(node) < 0.25) { ++ if ( ++ this._hasSingleTagInsideElement(node, "P") && ++ this._getLinkDensity(node) < 0.25 ++ ) { + var newNode = node.children[0]; + node.parentNode.replaceChild(newNode, node); + node = newNode; +@@ -1011,21 +1183,22 @@ Readability.prototype = { + * Then add their score to their parent node. + * + * A score is determined by things like number of commas, class names, etc. Maybe eventually link density. +- **/ ++ **/ + var candidates = []; +- this._forEachNode(elementsToScore, function(elementToScore) { +- if (!elementToScore.parentNode || typeof(elementToScore.parentNode.tagName) === "undefined") ++ this._forEachNode(elementsToScore, function (elementToScore) { ++ if ( ++ !elementToScore.parentNode || ++ typeof elementToScore.parentNode.tagName === "undefined" ++ ) + return; + + // If this paragraph is less than 25 characters, don't even count it. + var innerText = this._getInnerText(elementToScore); +- if (innerText.length < 25) +- return; ++ if (innerText.length < 25) return; + + // Exclude nodes with no ancestor. + var ancestors = this._getNodeAncestors(elementToScore, 5); +- if (ancestors.length === 0) +- return; ++ if (ancestors.length === 0) return; + + var contentScore = 0; + +@@ -1039,11 +1212,15 @@ Readability.prototype = { + contentScore += Math.min(Math.floor(innerText.length / 100), 3); + + // Initialize and score ancestors. +- this._forEachNode(ancestors, function(ancestor, level) { +- if (!ancestor.tagName || !ancestor.parentNode || typeof(ancestor.parentNode.tagName) === "undefined") ++ this._forEachNode(ancestors, function (ancestor, level) { ++ if ( ++ !ancestor.tagName || ++ !ancestor.parentNode || ++ typeof ancestor.parentNode.tagName === "undefined" ++ ) + return; + +- if (typeof(ancestor.readability) === "undefined") { ++ if (typeof ancestor.readability === "undefined") { + this._initializeNode(ancestor); + candidates.push(ancestor); + } +@@ -1052,12 +1229,9 @@ Readability.prototype = { + // - parent: 1 (no division) + // - grandparent: 2 + // - great grandparent+: ancestor level * 3 +- if (level === 0) +- var scoreDivider = 1; +- else if (level === 1) +- scoreDivider = 2; +- else +- scoreDivider = level * 3; ++ if (level === 0) var scoreDivider = 1; ++ else if (level === 1) scoreDivider = 2; ++ else scoreDivider = level * 3; + ancestor.readability.contentScore += contentScore / scoreDivider; + }); + }); +@@ -1071,7 +1245,9 @@ Readability.prototype = { + // Scale the final candidates score based on link density. Good content + // should have a relatively small link density (5% or less) and be mostly + // unaffected by this operation. +- var candidateScore = candidate.readability.contentScore * (1 - this._getLinkDensity(candidate)); ++ var candidateScore = ++ candidate.readability.contentScore * ++ (1 - this._getLinkDensity(candidate)); + candidate.readability.contentScore = candidateScore; + + this.log("Candidate:", candidate, "with score " + candidateScore); +@@ -1079,7 +1255,10 @@ Readability.prototype = { + for (var t = 0; t < this._nbTopCandidates; t++) { + var aTopCandidate = topCandidates[t]; + +- if (!aTopCandidate || candidateScore > aTopCandidate.readability.contentScore) { ++ if ( ++ !aTopCandidate || ++ candidateScore > aTopCandidate.readability.contentScore ++ ) { + topCandidates.splice(t, 0, candidate); + if (topCandidates.length > this._nbTopCandidates) + topCandidates.pop(); +@@ -1113,8 +1292,14 @@ Readability.prototype = { + // and whose scores are quite closed with current `topCandidate` node. + var alternativeCandidateAncestors = []; + for (var i = 1; i < topCandidates.length; i++) { +- if (topCandidates[i].readability.contentScore / topCandidate.readability.contentScore >= 0.75) { +- alternativeCandidateAncestors.push(this._getNodeAncestors(topCandidates[i])); ++ if ( ++ topCandidates[i].readability.contentScore / ++ topCandidate.readability.contentScore >= ++ 0.75 ++ ) { ++ alternativeCandidateAncestors.push( ++ this._getNodeAncestors(topCandidates[i]) ++ ); + } + } + var MINIMUM_TOPCANDIDATES = 3; +@@ -1122,8 +1307,17 @@ Readability.prototype = { + parentOfTopCandidate = topCandidate.parentNode; + while (parentOfTopCandidate.tagName !== "BODY") { + var listsContainingThisAncestor = 0; +- for (var ancestorIndex = 0; ancestorIndex < alternativeCandidateAncestors.length && listsContainingThisAncestor < MINIMUM_TOPCANDIDATES; ancestorIndex++) { +- listsContainingThisAncestor += Number(alternativeCandidateAncestors[ancestorIndex].includes(parentOfTopCandidate)); ++ for ( ++ var ancestorIndex = 0; ++ ancestorIndex < alternativeCandidateAncestors.length && ++ listsContainingThisAncestor < MINIMUM_TOPCANDIDATES; ++ ancestorIndex++ ++ ) { ++ listsContainingThisAncestor += Number( ++ alternativeCandidateAncestors[ancestorIndex].includes( ++ parentOfTopCandidate ++ ) ++ ); + } + if (listsContainingThisAncestor >= MINIMUM_TOPCANDIDATES) { + topCandidate = parentOfTopCandidate; +@@ -1153,8 +1347,7 @@ Readability.prototype = { + continue; + } + var parentScore = parentOfTopCandidate.readability.contentScore; +- if (parentScore < scoreThreshold) +- break; ++ if (parentScore < scoreThreshold) break; + if (parentScore > lastScore) { + // Alright! We found a better parent to use. + topCandidate = parentOfTopCandidate; +@@ -1167,7 +1360,10 @@ Readability.prototype = { + // If the top candidate is the only child, use parent instead. This will help sibling + // joining logic when adjacent content is actually located in parent's sibling node. + parentOfTopCandidate = topCandidate.parentNode; +- while (parentOfTopCandidate.tagName != "BODY" && parentOfTopCandidate.children.length == 1) { ++ while ( ++ parentOfTopCandidate.tagName != "BODY" && ++ parentOfTopCandidate.children.length == 1 ++ ) { + topCandidate = parentOfTopCandidate; + parentOfTopCandidate = topCandidate.parentNode; + } +@@ -1180,10 +1376,12 @@ Readability.prototype = { + // that might also be related. Things like preambles, content split by ads + // that we removed, etc. + var articleContent = doc.createElement("DIV"); +- if (isPaging) +- articleContent.id = "readability-content"; ++ if (isPaging) articleContent.id = "readability-content"; + +- var siblingScoreThreshold = Math.max(10, topCandidate.readability.contentScore * 0.2); ++ var siblingScoreThreshold = Math.max( ++ 10, ++ topCandidate.readability.contentScore * 0.2 ++ ); + // Keep potential top candidate's parent node to try to get text direction of it later. + parentOfTopCandidate = topCandidate.parentNode; + var siblings = parentOfTopCandidate.children; +@@ -1192,8 +1390,17 @@ Readability.prototype = { + var sibling = siblings[s]; + var append = false; + +- this.log("Looking at sibling node:", sibling, sibling.readability ? ("with score " + sibling.readability.contentScore) : ""); +- this.log("Sibling has score", sibling.readability ? sibling.readability.contentScore : "Unknown"); ++ this.log( ++ "Looking at sibling node:", ++ sibling, ++ sibling.readability ++ ? "with score " + sibling.readability.contentScore ++ : "" ++ ); ++ this.log( ++ "Sibling has score", ++ sibling.readability ? sibling.readability.contentScore : "Unknown" ++ ); + + if (sibling === topCandidate) { + append = true; +@@ -1201,11 +1408,17 @@ Readability.prototype = { + var contentBonus = 0; + + // Give a bonus if sibling nodes and top candidates have the example same classname +- if (sibling.className === topCandidate.className && topCandidate.className !== "") ++ if ( ++ sibling.className === topCandidate.className && ++ topCandidate.className !== "" ++ ) + contentBonus += topCandidate.readability.contentScore * 0.2; + +- if (sibling.readability && +- ((sibling.readability.contentScore + contentBonus) >= siblingScoreThreshold)) { ++ if ( ++ sibling.readability && ++ sibling.readability.contentScore + contentBonus >= ++ siblingScoreThreshold ++ ) { + append = true; + } else if (sibling.nodeName === "P") { + var linkDensity = this._getLinkDensity(sibling); +@@ -1214,8 +1427,12 @@ Readability.prototype = { + + if (nodeLength > 80 && linkDensity < 0.25) { + append = true; +- } else if (nodeLength < 80 && nodeLength > 0 && linkDensity === 0 && +- nodeContent.search(/\.( |$)/) !== -1) { ++ } else if ( ++ nodeLength < 80 && ++ nodeLength > 0 && ++ linkDensity === 0 && ++ nodeContent.search(/\.( |$)/) !== -1 ++ ) { + append = true; + } + } +@@ -1286,15 +1503,27 @@ Readability.prototype = { + + if (this._flagIsActive(this.FLAG_STRIP_UNLIKELYS)) { + this._removeFlag(this.FLAG_STRIP_UNLIKELYS); +- this._attempts.push({articleContent: articleContent, textLength: textLength}); ++ this._attempts.push({ ++ articleContent: articleContent, ++ textLength: textLength, ++ }); + } else if (this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) { + this._removeFlag(this.FLAG_WEIGHT_CLASSES); +- this._attempts.push({articleContent: articleContent, textLength: textLength}); ++ this._attempts.push({ ++ articleContent: articleContent, ++ textLength: textLength, ++ }); + } else if (this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)) { + this._removeFlag(this.FLAG_CLEAN_CONDITIONALLY); +- this._attempts.push({articleContent: articleContent, textLength: textLength}); ++ this._attempts.push({ ++ articleContent: articleContent, ++ textLength: textLength, ++ }); + } else { +- this._attempts.push({articleContent: articleContent, textLength: textLength}); ++ this._attempts.push({ ++ articleContent: articleContent, ++ textLength: textLength, ++ }); + // No luck after removing flags, just return the longest text we found during the different loops + this._attempts.sort(function (a, b) { + return b.textLength - a.textLength; +@@ -1312,10 +1541,11 @@ Readability.prototype = { + + if (parseSuccessful) { + // Find out text direction from ancestors of final top candidate. +- var ancestors = [parentOfTopCandidate, topCandidate].concat(this._getNodeAncestors(parentOfTopCandidate)); +- this._someNode(ancestors, function(ancestor) { +- if (!ancestor.tagName) +- return false; ++ var ancestors = [parentOfTopCandidate, topCandidate].concat( ++ this._getNodeAncestors(parentOfTopCandidate) ++ ); ++ this._someNode(ancestors, function (ancestor) { ++ if (!ancestor.tagName) return false; + var articleDir = ancestor.getAttribute("dir"); + if (articleDir) { + this._articleDir = articleDir; +@@ -1336,10 +1566,10 @@ Readability.prototype = { + * @param possibleByline {string} - a string to check whether its a byline. + * @return Boolean - whether the input string is a byline. + */ +- _isValidByline: function(byline) { ++ _isValidByline: function (byline) { + if (typeof byline == "string" || byline instanceof String) { + byline = byline.trim(); +- return (byline.length > 0) && (byline.length < 100); ++ return byline.length > 0 && byline.length < 100; + } + return false; + }, +@@ -1350,18 +1580,23 @@ Readability.prototype = { + * @param str {string} - a string to unescape. + * @return string without HTML entity. + */ +- _unescapeHtmlEntities: function(str) { ++ _unescapeHtmlEntities: function (str) { + if (!str) { + return str; + } + + var htmlEscapeMap = this.HTML_ESCAPE_MAP; +- return str.replace(/&(quot|amp|apos|lt|gt);/g, function(_, tag) { +- return htmlEscapeMap[tag]; +- }).replace(/(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi, function(_, hex, numStr) { +- var num = parseInt(hex || numStr, hex ? 16 : 10); +- return String.fromCharCode(num); +- }); ++ return str ++ .replace(/&(quot|amp|apos|lt|gt);/g, function (_, tag) { ++ return htmlEscapeMap[tag]; ++ }) ++ .replace( ++ /(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi, ++ function (_, hex, numStr) { ++ var num = parseInt(hex || numStr, hex ? 16 : 10); ++ return String.fromCharCode(num); ++ } ++ ); + }, + + /** +@@ -1374,11 +1609,17 @@ Readability.prototype = { + + var metadata; + +- this._forEachNode(scripts, function(jsonLdElement) { +- if (!metadata && jsonLdElement.getAttribute("type") === "application/ld+json") { ++ this._forEachNode(scripts, function (jsonLdElement) { ++ if ( ++ !metadata && ++ jsonLdElement.getAttribute("type") === "application/ld+json" ++ ) { + try { + // Strip CDATA markers if present +- var content = jsonLdElement.textContent.replace(/^\s*\s*$/g, ""); ++ var content = jsonLdElement.textContent.replace( ++ /^\s*\s*$/g, ++ "" ++ ); + var parsed = JSON.parse(content); + if ( + !parsed["@context"] || +@@ -1388,10 +1629,8 @@ Readability.prototype = { + } + + if (!parsed["@type"] && Array.isArray(parsed["@graph"])) { +- parsed = parsed["@graph"].find(function(it) { +- return (it["@type"] || "").match( +- this.REGEXPS.jsonLdArticleTypes +- ); ++ parsed = parsed["@graph"].find(function (it) { ++ return (it["@type"] || "").match(this.REGEXPS.jsonLdArticleTypes); + }); + } + +@@ -1405,14 +1644,19 @@ Readability.prototype = { + + metadata = {}; + +- if (typeof parsed.name === "string" && typeof parsed.headline === "string" && parsed.name !== parsed.headline) { ++ if ( ++ typeof parsed.name === "string" && ++ typeof parsed.headline === "string" && ++ parsed.name !== parsed.headline ++ ) { + // we have both name and headline element in the JSON-LD. They should both be the same but some websites like aktualne.cz + // put their own name into "name" and the article title to "headline" which confuses Readability. So we try to check if either + // "name" or "headline" closely matches the html title, and if so, use that one. If not, then we use "name" by default. + + var title = this._getArticleTitle(); + var nameMatches = this._textSimilarity(parsed.name, title) > 0.75; +- var headlineMatches = this._textSimilarity(parsed.headline, title) > 0.75; ++ var headlineMatches = ++ this._textSimilarity(parsed.headline, title) > 0.75; + + if (headlineMatches && !nameMatches) { + metadata.title = parsed.headline; +@@ -1427,12 +1671,16 @@ Readability.prototype = { + if (parsed.author) { + if (typeof parsed.author.name === "string") { + metadata.byline = parsed.author.name.trim(); +- } else if (Array.isArray(parsed.author) && parsed.author[0] && typeof parsed.author[0].name === "string") { ++ } else if ( ++ Array.isArray(parsed.author) && ++ parsed.author[0] && ++ typeof parsed.author[0].name === "string" ++ ) { + metadata.byline = parsed.author +- .filter(function(author) { ++ .filter(function (author) { + return author && typeof author.name === "string"; + }) +- .map(function(author) { ++ .map(function (author) { + return author.name.trim(); + }) + .join(", "); +@@ -1441,10 +1689,7 @@ Readability.prototype = { + if (typeof parsed.description === "string") { + metadata.excerpt = parsed.description.trim(); + } +- if ( +- parsed.publisher && +- typeof parsed.publisher.name === "string" +- ) { ++ if (parsed.publisher && typeof parsed.publisher.name === "string") { + metadata.siteName = parsed.publisher.name.trim(); + } + if (typeof parsed.datePublished === "string") { +@@ -1467,19 +1712,21 @@ Readability.prototype = { + * + * @return Object with optional "excerpt" and "byline" properties + */ +- _getArticleMetadata: function(jsonld) { ++ _getArticleMetadata: function (jsonld) { + var metadata = {}; + var values = {}; + var metaElements = this._doc.getElementsByTagName("meta"); + + // property is a space-separated list of values +- var propertyPattern = /\s*(article|dc|dcterm|og|twitter)\s*:\s*(author|creator|description|published_time|title|site_name)\s*/gi; ++ var propertyPattern = ++ /\s*(article|dc|dcterm|og|twitter)\s*:\s*(author|creator|description|published_time|title|site_name)\s*/gi; + + // name is a single value +- var namePattern = /^\s*(?:(dc|dcterm|og|twitter|weibo:(article|webpage))\s*[\.:]\s*)?(author|creator|description|title|site_name)\s*$/i; ++ var namePattern = ++ /^\s*(?:(dc|dcterm|og|twitter|weibo:(article|webpage))\s*[\.:]\s*)?(author|creator|description|title|site_name)\s*$/i; + + // Find description tags. +- this._forEachNode(metaElements, function(element) { ++ this._forEachNode(metaElements, function (element) { + var elementName = element.getAttribute("name"); + var elementProperty = element.getAttribute("property"); + var content = element.getAttribute("content"); +@@ -1511,42 +1758,44 @@ Readability.prototype = { + }); + + // get title +- metadata.title = jsonld.title || +- values["dc:title"] || +- values["dcterm:title"] || +- values["og:title"] || +- values["weibo:article:title"] || +- values["weibo:webpage:title"] || +- values["title"] || +- values["twitter:title"]; ++ metadata.title = ++ jsonld.title || ++ values["dc:title"] || ++ values["dcterm:title"] || ++ values["og:title"] || ++ values["weibo:article:title"] || ++ values["weibo:webpage:title"] || ++ values["title"] || ++ values["twitter:title"]; + + if (!metadata.title) { + metadata.title = this._getArticleTitle(); + } + + // get author +- metadata.byline = jsonld.byline || +- values["dc:creator"] || +- values["dcterm:creator"] || +- values["author"]; ++ metadata.byline = ++ jsonld.byline || ++ values["dc:creator"] || ++ values["dcterm:creator"] || ++ values["author"]; + + // get description +- metadata.excerpt = jsonld.excerpt || +- values["dc:description"] || +- values["dcterm:description"] || +- values["og:description"] || +- values["weibo:article:description"] || +- values["weibo:webpage:description"] || +- values["description"] || +- values["twitter:description"]; ++ metadata.excerpt = ++ jsonld.excerpt || ++ values["dc:description"] || ++ values["dcterm:description"] || ++ values["og:description"] || ++ values["weibo:article:description"] || ++ values["weibo:webpage:description"] || ++ values["description"] || ++ values["twitter:description"]; + + // get site name +- metadata.siteName = jsonld.siteName || +- values["og:site_name"]; ++ metadata.siteName = jsonld.siteName || values["og:site_name"]; + + // get article published time +- metadata.publishedTime = jsonld.datePublished || +- values["article:published_time"] || null; ++ metadata.publishedTime = ++ jsonld.datePublished || values["article:published_time"] || null; + + // in many sites the meta value is escaped with HTML entities, + // so here we need to unescape it +@@ -1564,8 +1813,8 @@ Readability.prototype = { + * whether as a direct child or as its descendants. + * + * @param Element +- **/ +- _isSingleImage: function(node) { ++ **/ ++ _isSingleImage: function (node) { + if (node.tagName === "IMG") { + return true; + } +@@ -1584,12 +1833,12 @@ Readability.prototype = { + * some sites (e.g. Medium). + * + * @param Element +- **/ +- _unwrapNoscriptImages: function(doc) { ++ **/ ++ _unwrapNoscriptImages: function (doc) { + // Find img without source or attributes that might contains image, and remove it. + // This is done to prevent a placeholder img is replaced by img from noscript in next step. + var imgs = Array.from(doc.getElementsByTagName("img")); +- this._forEachNode(imgs, function(img) { ++ this._forEachNode(imgs, function (img) { + for (var i = 0; i < img.attributes.length; i++) { + var attr = img.attributes[i]; + switch (attr.name) { +@@ -1610,7 +1859,7 @@ Readability.prototype = { + + // Next find noscript and try to extract its image + var noscripts = Array.from(doc.getElementsByTagName("noscript")); +- this._forEachNode(noscripts, function(noscript) { ++ this._forEachNode(noscripts, function (noscript) { + // Parse content of noscript and make sure it only contains image + var tmp = doc.createElement("div"); + tmp.innerHTML = noscript.innerHTML; +@@ -1635,7 +1884,11 @@ Readability.prototype = { + continue; + } + +- if (attr.name === "src" || attr.name === "srcset" || /\.(jpg|jpeg|png|webp)/i.test(attr.value)) { ++ if ( ++ attr.name === "src" || ++ attr.name === "srcset" || ++ /\.(jpg|jpeg|png|webp)/i.test(attr.value) ++ ) { + if (newImg.getAttribute(attr.name) === attr.value) { + continue; + } +@@ -1658,8 +1911,8 @@ Readability.prototype = { + * Removes script tags from the document. + * + * @param Element +- **/ +- _removeScripts: function(doc) { ++ **/ ++ _removeScripts: function (doc) { + this._removeNodes(this._getAllNodesWithTag(doc, ["script", "noscript"])); + }, + +@@ -1670,25 +1923,31 @@ Readability.prototype = { + * + * @param Element + * @param string tag of child element +- **/ +- _hasSingleTagInsideElement: function(element, tag) { ++ **/ ++ _hasSingleTagInsideElement: function (element, tag) { + // There should be exactly 1 element child with given tag + if (element.children.length != 1 || element.children[0].tagName !== tag) { + return false; + } + + // And there should be no text nodes with real content +- return !this._someNode(element.childNodes, function(node) { +- return node.nodeType === this.TEXT_NODE && +- this.REGEXPS.hasContent.test(node.textContent); ++ return !this._someNode(element.childNodes, function (node) { ++ return ( ++ node.nodeType === this.TEXT_NODE && ++ this.REGEXPS.hasContent.test(node.textContent) ++ ); + }); + }, + +- _isElementWithoutContent: function(node) { +- return node.nodeType === this.ELEMENT_NODE && ++ _isElementWithoutContent: function (node) { ++ return ( ++ node.nodeType === this.ELEMENT_NODE && + node.textContent.trim().length == 0 && + (node.children.length == 0 || +- node.children.length == node.getElementsByTagName("br").length + node.getElementsByTagName("hr").length); ++ node.children.length == ++ node.getElementsByTagName("br").length + ++ node.getElementsByTagName("hr").length) ++ ); + }, + + /** +@@ -1697,25 +1956,35 @@ Readability.prototype = { + * @param Element + */ + _hasChildBlockElement: function (element) { +- return this._someNode(element.childNodes, function(node) { +- return this.DIV_TO_P_ELEMS.has(node.tagName) || +- this._hasChildBlockElement(node); ++ return this._someNode(element.childNodes, function (node) { ++ return ( ++ this.DIV_TO_P_ELEMS.has(node.tagName) || ++ this._hasChildBlockElement(node) ++ ); + }); + }, + + /*** + * Determine if a node qualifies as phrasing content. + * https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Phrasing_content +- **/ +- _isPhrasingContent: function(node) { +- return node.nodeType === this.TEXT_NODE || this.PHRASING_ELEMS.indexOf(node.tagName) !== -1 || +- ((node.tagName === "A" || node.tagName === "DEL" || node.tagName === "INS") && +- this._everyNode(node.childNodes, this._isPhrasingContent)); ++ **/ ++ _isPhrasingContent: function (node) { ++ return ( ++ node.nodeType === this.TEXT_NODE || ++ this.PHRASING_ELEMS.indexOf(node.tagName) !== -1 || ++ ((node.tagName === "A" || ++ node.tagName === "DEL" || ++ node.tagName === "INS") && ++ this._everyNode(node.childNodes, this._isPhrasingContent)) ++ ); + }, + +- _isWhitespace: function(node) { +- return (node.nodeType === this.TEXT_NODE && node.textContent.trim().length === 0) || +- (node.nodeType === this.ELEMENT_NODE && node.tagName === "BR"); ++ _isWhitespace: function (node) { ++ return ( ++ (node.nodeType === this.TEXT_NODE && ++ node.textContent.trim().length === 0) || ++ (node.nodeType === this.ELEMENT_NODE && node.tagName === "BR") ++ ); + }, + + /** +@@ -1725,9 +1994,10 @@ Readability.prototype = { + * @param Element + * @param Boolean normalizeSpaces (default: true) + * @return string +- **/ +- _getInnerText: function(e, normalizeSpaces) { +- normalizeSpaces = (typeof normalizeSpaces === "undefined") ? true : normalizeSpaces; ++ **/ ++ _getInnerText: function (e, normalizeSpaces) { ++ normalizeSpaces = ++ typeof normalizeSpaces === "undefined" ? true : normalizeSpaces; + var textContent = e.textContent.trim(); + + if (normalizeSpaces) { +@@ -1742,8 +2012,8 @@ Readability.prototype = { + * @param Element + * @param string - what to split on. Default is "," + * @return number (integer) +- **/ +- _getCharCount: function(e, s) { ++ **/ ++ _getCharCount: function (e, s) { + s = s || ","; + return this._getInnerText(e).split(s).length - 1; + }, +@@ -1754,10 +2024,9 @@ Readability.prototype = { + * + * @param Element + * @return void +- **/ +- _cleanStyles: function(e) { +- if (!e || e.tagName.toLowerCase() === "svg") +- return; ++ **/ ++ _cleanStyles: function (e) { ++ if (!e || e.tagName.toLowerCase() === "svg") return; + + // Remove `style` and deprecated presentational attributes + for (var i = 0; i < this.PRESENTATIONAL_ATTRIBUTES.length; i++) { +@@ -1782,16 +2051,15 @@ Readability.prototype = { + * + * @param Element + * @return number (float) +- **/ +- _getLinkDensity: function(element) { ++ **/ ++ _getLinkDensity: function (element) { + var textLength = this._getInnerText(element).length; +- if (textLength === 0) +- return 0; ++ if (textLength === 0) return 0; + + var linkLength = 0; + + // XXX implement _reduceNodeList? +- this._forEachNode(element.getElementsByTagName("a"), function(linkNode) { ++ this._forEachNode(element.getElementsByTagName("a"), function (linkNode) { + var href = linkNode.getAttribute("href"); + var coefficient = href && this.REGEXPS.hashUrl.test(href) ? 0.3 : 1; + linkLength += this._getInnerText(linkNode).length * coefficient; +@@ -1806,29 +2074,24 @@ Readability.prototype = { + * + * @param Element + * @return number (Integer) +- **/ +- _getClassWeight: function(e) { +- if (!this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) +- return 0; ++ **/ ++ _getClassWeight: function (e) { ++ if (!this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) return 0; + + var weight = 0; + + // Look for a special classname +- if (typeof(e.className) === "string" && e.className !== "") { +- if (this.REGEXPS.negative.test(e.className)) +- weight -= 25; ++ if (typeof e.className === "string" && e.className !== "") { ++ if (this.REGEXPS.negative.test(e.className)) weight -= 25; + +- if (this.REGEXPS.positive.test(e.className)) +- weight += 25; ++ if (this.REGEXPS.positive.test(e.className)) weight += 25; + } + + // Look for a special ID +- if (typeof(e.id) === "string" && e.id !== "") { +- if (this.REGEXPS.negative.test(e.id)) +- weight -= 25; ++ if (typeof e.id === "string" && e.id !== "") { ++ if (this.REGEXPS.negative.test(e.id)) weight -= 25; + +- if (this.REGEXPS.positive.test(e.id)) +- weight += 25; ++ if (this.REGEXPS.positive.test(e.id)) weight += 25; + } + + return weight; +@@ -1842,10 +2105,10 @@ Readability.prototype = { + * @param string tag to clean + * @return void + **/ +- _clean: function(e, tag) { ++ _clean: function (e, tag) { + var isEmbed = ["object", "embed", "iframe"].indexOf(tag) !== -1; + +- this._removeNodes(this._getAllNodesWithTag(e, [tag]), function(element) { ++ this._removeNodes(this._getAllNodesWithTag(e, [tag]), function (element) { + // Allow youtube and vimeo videos through as people usually want to see those. + if (isEmbed) { + // First, check the elements attributes to see if any of them contain youtube or vimeo +@@ -1856,7 +2119,10 @@ Readability.prototype = { + } + + // For embed with