|
@@ -60,31 +60,32 @@ function Readability(doc, options) {
|
|
|
this.FLAG_WEIGHT_CLASSES |
|
|
this.FLAG_WEIGHT_CLASSES |
|
|
|
this.FLAG_CLEAN_CONDITIONALLY;
|
|
this.FLAG_CLEAN_CONDITIONALLY;
|
|
|
|
|
|
|
|
- var logEl;
|
|
|
|
|
|
|
|
|
|
// Control whether log messages are sent to the console
|
|
// Control whether log messages are sent to the console
|
|
|
if (this._debug) {
|
|
if (this._debug) {
|
|
|
- logEl = function(e) {
|
|
|
|
|
- var rv = e.nodeName + " ";
|
|
|
|
|
- if (e.nodeType == e.TEXT_NODE) {
|
|
|
|
|
- return rv + '("' + e.textContent + '")';
|
|
|
|
|
|
|
+ let logNode = function(node) {
|
|
|
|
|
+ if (node.nodeType == node.TEXT_NODE) {
|
|
|
|
|
+ return `${node.nodeName} ("${node.textContent}")`;
|
|
|
}
|
|
}
|
|
|
- var classDesc = e.className && ("." + e.className.replace(/ /g, "."));
|
|
|
|
|
- var elDesc = "";
|
|
|
|
|
- if (e.id)
|
|
|
|
|
- elDesc = "(#" + e.id + classDesc + ")";
|
|
|
|
|
- else if (classDesc)
|
|
|
|
|
- elDesc = "(" + classDesc + ")";
|
|
|
|
|
- return rv + elDesc;
|
|
|
|
|
|
|
+ let attrPairs = Array.from(node.attributes || [], function(attr) {
|
|
|
|
|
+ return `${attr.name}="${attr.value}"`;
|
|
|
|
|
+ }).join(" ");
|
|
|
|
|
+ return `<${node.localName} ${attrPairs}>`;
|
|
|
};
|
|
};
|
|
|
this.log = function () {
|
|
this.log = function () {
|
|
|
if (typeof dump !== "undefined") {
|
|
if (typeof dump !== "undefined") {
|
|
|
var msg = Array.prototype.map.call(arguments, function(x) {
|
|
var msg = Array.prototype.map.call(arguments, function(x) {
|
|
|
- return (x && x.nodeName) ? logEl(x) : x;
|
|
|
|
|
|
|
+ return (x && x.nodeName) ? logNode(x) : x;
|
|
|
}).join(" ");
|
|
}).join(" ");
|
|
|
dump("Reader: (Readability) " + msg + "\n");
|
|
dump("Reader: (Readability) " + msg + "\n");
|
|
|
} else if (typeof console !== "undefined") {
|
|
} else if (typeof console !== "undefined") {
|
|
|
- var args = ["Reader: (Readability) "].concat(arguments);
|
|
|
|
|
|
|
+ let args = Array.from(arguments, arg => {
|
|
|
|
|
+ if (arg && arg.nodeType == this.ELEMENT_NODE) {
|
|
|
|
|
+ return logNode(arg);
|
|
|
|
|
+ }
|
|
|
|
|
+ return arg;
|
|
|
|
|
+ });
|
|
|
|
|
+ args.unshift("Reader: (Readability)");
|
|
|
console.log.apply(console, args);
|
|
console.log.apply(console, args);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
@@ -124,7 +125,7 @@ Readability.prototype = {
|
|
|
okMaybeItsACandidate: /and|article|body|column|content|main|shadow/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,
|
|
positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,
|
|
|
- negative: /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,
|
|
|
|
|
|
|
+ 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,
|
|
extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,
|
|
|
byline: /byline|author|dateline|writtenby|p-author/i,
|
|
byline: /byline|author|dateline|writtenby|p-author/i,
|
|
|
replaceFonts: /<(\/?)font[^>]*>/gi,
|
|
replaceFonts: /<(\/?)font[^>]*>/gi,
|
|
@@ -133,8 +134,10 @@ Readability.prototype = {
|
|
|
shareElements: /(\b|_)(share|sharedaddy)(\b|_)/i,
|
|
shareElements: /(\b|_)(share|sharedaddy)(\b|_)/i,
|
|
|
nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
|
|
nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
|
|
|
prevLink: /(prev|earl|old|new|<|«)/i,
|
|
prevLink: /(prev|earl|old|new|<|«)/i,
|
|
|
|
|
+ tokenize: /\W+/g,
|
|
|
whitespace: /^\s*$/,
|
|
whitespace: /^\s*$/,
|
|
|
hasContent: /\S$/,
|
|
hasContent: /\S$/,
|
|
|
|
|
+ hashUrl: /^#.+/,
|
|
|
srcsetUrl: /(\S+)(\s+[\d.]+[xw])?(\s*(?:,|$))/g,
|
|
srcsetUrl: /(\S+)(\s+[\d.]+[xw])?(\s*(?:,|$))/g,
|
|
|
b64DataUrl: /^data:\s*([^\s;,]+)\s*;\s*base64\s*,/i,
|
|
b64DataUrl: /^data:\s*([^\s;,]+)\s*;\s*base64\s*,/i,
|
|
|
// See: https://schema.org/Article
|
|
// See: https://schema.org/Article
|
|
@@ -143,7 +146,7 @@ Readability.prototype = {
|
|
|
|
|
|
|
|
UNLIKELY_ROLES: [ "menu", "menubar", "complementary", "navigation", "alert", "alertdialog", "dialog" ],
|
|
UNLIKELY_ROLES: [ "menu", "menubar", "complementary", "navigation", "alert", "alertdialog", "dialog" ],
|
|
|
|
|
|
|
|
- DIV_TO_P_ELEMS: [ "A", "BLOCKQUOTE", "DL", "DIV", "IMG", "OL", "P", "PRE", "TABLE", "UL", "SELECT" ],
|
|
|
|
|
|
|
+ DIV_TO_P_ELEMS: new Set([ "BLOCKQUOTE", "DL", "DIV", "IMG", "OL", "P", "PRE", "TABLE", "UL" ]),
|
|
|
|
|
|
|
|
ALTER_TO_DIV_EXCEPTIONS: ["DIV", "ARTICLE", "SECTION", "P"],
|
|
ALTER_TO_DIV_EXCEPTIONS: ["DIV", "ARTICLE", "SECTION", "P"],
|
|
|
|
|
|
|
@@ -230,8 +233,7 @@ Readability.prototype = {
|
|
|
if (this._docJSDOMParser && nodeList._isLiveNodeList) {
|
|
if (this._docJSDOMParser && nodeList._isLiveNodeList) {
|
|
|
throw new Error("Do not pass live node lists to _replaceNodeTags");
|
|
throw new Error("Do not pass live node lists to _replaceNodeTags");
|
|
|
}
|
|
}
|
|
|
- for (var i = nodeList.length - 1; i >= 0; i--) {
|
|
|
|
|
- var node = nodeList[i];
|
|
|
|
|
|
|
+ for (const node of nodeList) {
|
|
|
this._setNodeTag(node, newTagName);
|
|
this._setNodeTag(node, newTagName);
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
@@ -452,7 +454,7 @@ Readability.prototype = {
|
|
|
/**
|
|
/**
|
|
|
* Get the article title as an H1.
|
|
* Get the article title as an H1.
|
|
|
*
|
|
*
|
|
|
- * @return void
|
|
|
|
|
|
|
+ * @return string
|
|
|
**/
|
|
**/
|
|
|
_getArticleTitle: function() {
|
|
_getArticleTitle: function() {
|
|
|
var doc = this._doc;
|
|
var doc = this._doc;
|
|
@@ -548,11 +550,11 @@ Readability.prototype = {
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Finds the next element, starting from the given node, and ignoring
|
|
|
|
|
|
|
+ * Finds the next node, starting from the given node, and ignoring
|
|
|
* whitespace in between. If the given node is an element, the same node is
|
|
* whitespace in between. If the given node is an element, the same node is
|
|
|
* returned.
|
|
* returned.
|
|
|
*/
|
|
*/
|
|
|
- _nextElement: function (node) {
|
|
|
|
|
|
|
+ _nextNode: function (node) {
|
|
|
var next = node;
|
|
var next = node;
|
|
|
while (next
|
|
while (next
|
|
|
&& (next.nodeType != this.ELEMENT_NODE)
|
|
&& (next.nodeType != this.ELEMENT_NODE)
|
|
@@ -577,10 +579,10 @@ Readability.prototype = {
|
|
|
// <p> block.
|
|
// <p> block.
|
|
|
var replaced = false;
|
|
var replaced = false;
|
|
|
|
|
|
|
|
- // If we find a <br> chain, remove the <br>s until we hit another element
|
|
|
|
|
|
|
+ // If we find a <br> chain, remove the <br>s until we hit another node
|
|
|
// or non-whitespace. This leaves behind the first <br> in the chain
|
|
// or non-whitespace. This leaves behind the first <br> in the chain
|
|
|
// (which will be replaced with a <p> later).
|
|
// (which will be replaced with a <p> later).
|
|
|
- while ((next = this._nextElement(next)) && (next.tagName == "BR")) {
|
|
|
|
|
|
|
+ while ((next = this._nextNode(next)) && (next.tagName == "BR")) {
|
|
|
replaced = true;
|
|
replaced = true;
|
|
|
var brSibling = next.nextSibling;
|
|
var brSibling = next.nextSibling;
|
|
|
next.parentNode.removeChild(next);
|
|
next.parentNode.removeChild(next);
|
|
@@ -598,7 +600,7 @@ Readability.prototype = {
|
|
|
while (next) {
|
|
while (next) {
|
|
|
// If we've hit another <br><br>, we're done adding children to this <p>.
|
|
// If we've hit another <br><br>, we're done adding children to this <p>.
|
|
|
if (next.tagName == "BR") {
|
|
if (next.tagName == "BR") {
|
|
|
- var nextElem = this._nextElement(next.nextSibling);
|
|
|
|
|
|
|
+ var nextElem = this._nextNode(next.nextSibling);
|
|
|
if (nextElem && nextElem.tagName == "BR")
|
|
if (nextElem && nextElem.tagName == "BR")
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
@@ -675,7 +677,6 @@ Readability.prototype = {
|
|
|
this._cleanConditionally(articleContent, "fieldset");
|
|
this._cleanConditionally(articleContent, "fieldset");
|
|
|
this._clean(articleContent, "object");
|
|
this._clean(articleContent, "object");
|
|
|
this._clean(articleContent, "embed");
|
|
this._clean(articleContent, "embed");
|
|
|
- this._clean(articleContent, "h1");
|
|
|
|
|
this._clean(articleContent, "footer");
|
|
this._clean(articleContent, "footer");
|
|
|
this._clean(articleContent, "link");
|
|
this._clean(articleContent, "link");
|
|
|
this._clean(articleContent, "aside");
|
|
this._clean(articleContent, "aside");
|
|
@@ -691,25 +692,6 @@ Readability.prototype = {
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // If there is only one h2 and its text content substantially equals article title,
|
|
|
|
|
- // they are probably using it as a header and not a subheader,
|
|
|
|
|
- // so remove it since we already extract the title separately.
|
|
|
|
|
- var h2 = articleContent.getElementsByTagName("h2");
|
|
|
|
|
- if (h2.length === 1) {
|
|
|
|
|
- var lengthSimilarRate = (h2[0].textContent.length - this._articleTitle.length) / this._articleTitle.length;
|
|
|
|
|
- if (Math.abs(lengthSimilarRate) < 0.5) {
|
|
|
|
|
- var titlesMatch = false;
|
|
|
|
|
- if (lengthSimilarRate > 0) {
|
|
|
|
|
- titlesMatch = h2[0].textContent.includes(this._articleTitle);
|
|
|
|
|
- } else {
|
|
|
|
|
- titlesMatch = this._articleTitle.includes(h2[0].textContent);
|
|
|
|
|
- }
|
|
|
|
|
- if (titlesMatch) {
|
|
|
|
|
- this._clean(articleContent, "h2");
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
this._clean(articleContent, "iframe");
|
|
this._clean(articleContent, "iframe");
|
|
|
this._clean(articleContent, "input");
|
|
this._clean(articleContent, "input");
|
|
|
this._clean(articleContent, "textarea");
|
|
this._clean(articleContent, "textarea");
|
|
@@ -723,6 +705,9 @@ Readability.prototype = {
|
|
|
this._cleanConditionally(articleContent, "ul");
|
|
this._cleanConditionally(articleContent, "ul");
|
|
|
this._cleanConditionally(articleContent, "div");
|
|
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");
|
|
|
|
|
+
|
|
|
// Remove extra paragraphs
|
|
// Remove extra paragraphs
|
|
|
this._removeNodes(this._getAllNodesWithTag(articleContent, ["p"]), function (paragraph) {
|
|
this._removeNodes(this._getAllNodesWithTag(articleContent, ["p"]), function (paragraph) {
|
|
|
var imgCount = paragraph.getElementsByTagName("img").length;
|
|
var imgCount = paragraph.getElementsByTagName("img").length;
|
|
@@ -736,7 +721,7 @@ Readability.prototype = {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
this._forEachNode(this._getAllNodesWithTag(articleContent, ["br"]), function(br) {
|
|
this._forEachNode(this._getAllNodesWithTag(articleContent, ["br"]), function(br) {
|
|
|
- var next = this._nextElement(br.nextSibling);
|
|
|
|
|
|
|
+ var next = this._nextNode(br.nextSibling);
|
|
|
if (next && next.tagName == "P")
|
|
if (next && next.tagName == "P")
|
|
|
br.parentNode.removeChild(br);
|
|
br.parentNode.removeChild(br);
|
|
|
});
|
|
});
|
|
@@ -832,6 +817,21 @@ Readability.prototype = {
|
|
|
return node && node.nextElementSibling;
|
|
return node && node.nextElementSibling;
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ // compares second text to first one
|
|
|
|
|
+ // 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);
|
|
|
|
|
+ if (!tokensA.length || !tokensB.length) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ 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) {
|
|
if (this._articleByline) {
|
|
|
return false;
|
|
return false;
|
|
@@ -872,7 +872,7 @@ Readability.prototype = {
|
|
|
_grabArticle: function (page) {
|
|
_grabArticle: function (page) {
|
|
|
this.log("**** grabArticle ****");
|
|
this.log("**** grabArticle ****");
|
|
|
var doc = this._doc;
|
|
var doc = this._doc;
|
|
|
- var isPaging = (page !== null ? true: false);
|
|
|
|
|
|
|
+ var isPaging = page !== null;
|
|
|
page = page ? page : this._doc.body;
|
|
page = page ? page : this._doc.body;
|
|
|
|
|
|
|
|
// We can't grab an article if we don't have a page!
|
|
// We can't grab an article if we don't have a page!
|
|
@@ -884,6 +884,7 @@ Readability.prototype = {
|
|
|
var pageCacheHtml = page.innerHTML;
|
|
var pageCacheHtml = page.innerHTML;
|
|
|
|
|
|
|
|
while (true) {
|
|
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
|
|
// First, node prepping. Trash nodes that look cruddy (like ones with the
|
|
@@ -892,6 +893,8 @@ Readability.prototype = {
|
|
|
var elementsToScore = [];
|
|
var elementsToScore = [];
|
|
|
var node = this._doc.documentElement;
|
|
var node = this._doc.documentElement;
|
|
|
|
|
|
|
|
|
|
+ let shouldRemoveTitleHeader = true;
|
|
|
|
|
+
|
|
|
while (node) {
|
|
while (node) {
|
|
|
var matchString = node.className + " " + node.id;
|
|
var matchString = node.className + " " + node.id;
|
|
|
|
|
|
|
@@ -907,11 +910,19 @@ Readability.prototype = {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (shouldRemoveTitleHeader && this._headerDuplicatesTitle(node)) {
|
|
|
|
|
+ this.log("Removing header: ", node.textContent.trim(), this._articleTitle.trim());
|
|
|
|
|
+ shouldRemoveTitleHeader = false;
|
|
|
|
|
+ node = this._removeAndGetNext(node);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Remove unlikely candidates
|
|
// Remove unlikely candidates
|
|
|
if (stripUnlikelyCandidates) {
|
|
if (stripUnlikelyCandidates) {
|
|
|
if (this.REGEXPS.unlikelyCandidates.test(matchString) &&
|
|
if (this.REGEXPS.unlikelyCandidates.test(matchString) &&
|
|
|
!this.REGEXPS.okMaybeItsACandidate.test(matchString) &&
|
|
!this.REGEXPS.okMaybeItsACandidate.test(matchString) &&
|
|
|
!this._hasAncestorTag(node, "table") &&
|
|
!this._hasAncestorTag(node, "table") &&
|
|
|
|
|
+ !this._hasAncestorTag(node, "code") &&
|
|
|
node.tagName !== "BODY" &&
|
|
node.tagName !== "BODY" &&
|
|
|
node.tagName !== "A") {
|
|
node.tagName !== "A") {
|
|
|
this.log("Removing unlikely candidate - " + matchString);
|
|
this.log("Removing unlikely candidate - " + matchString);
|
|
@@ -1446,13 +1457,11 @@ Readability.prototype = {
|
|
|
if (elementProperty) {
|
|
if (elementProperty) {
|
|
|
matches = elementProperty.match(propertyPattern);
|
|
matches = elementProperty.match(propertyPattern);
|
|
|
if (matches) {
|
|
if (matches) {
|
|
|
- for (var i = matches.length - 1; i >= 0; i--) {
|
|
|
|
|
- // Convert to lowercase, and remove any whitespace
|
|
|
|
|
- // so we can match below.
|
|
|
|
|
- name = matches[i].toLowerCase().replace(/\s/g, "");
|
|
|
|
|
- // multiple authors
|
|
|
|
|
- values[name] = content.trim();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Convert to lowercase, and remove any whitespace
|
|
|
|
|
+ // so we can match below.
|
|
|
|
|
+ name = matches[0].toLowerCase().replace(/\s/g, "");
|
|
|
|
|
+ // multiple authors
|
|
|
|
|
+ values[name] = content.trim();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
if (!matches && elementName && namePattern.test(elementName)) {
|
|
if (!matches && elementName && namePattern.test(elementName)) {
|
|
@@ -1654,7 +1663,7 @@ Readability.prototype = {
|
|
|
*/
|
|
*/
|
|
|
_hasChildBlockElement: function (element) {
|
|
_hasChildBlockElement: function (element) {
|
|
|
return this._someNode(element.childNodes, function(node) {
|
|
return this._someNode(element.childNodes, function(node) {
|
|
|
- return this.DIV_TO_P_ELEMS.indexOf(node.tagName) !== -1 ||
|
|
|
|
|
|
|
+ return this.DIV_TO_P_ELEMS.has(node.tagName) ||
|
|
|
this._hasChildBlockElement(node);
|
|
this._hasChildBlockElement(node);
|
|
|
});
|
|
});
|
|
|
},
|
|
},
|
|
@@ -1748,7 +1757,9 @@ Readability.prototype = {
|
|
|
|
|
|
|
|
// XXX implement _reduceNodeList?
|
|
// XXX implement _reduceNodeList?
|
|
|
this._forEachNode(element.getElementsByTagName("a"), function(linkNode) {
|
|
this._forEachNode(element.getElementsByTagName("a"), function(linkNode) {
|
|
|
- linkLength += this._getInnerText(linkNode).length;
|
|
|
|
|
|
|
+ var href = linkNode.getAttribute("href");
|
|
|
|
|
+ var coefficient = href && this.REGEXPS.hashUrl.test(href) ? 0.3 : 1;
|
|
|
|
|
+ linkLength += this._getInnerText(linkNode).length * coefficient;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
return linkLength / textLength;
|
|
return linkLength / textLength;
|
|
@@ -2000,6 +2011,17 @@ Readability.prototype = {
|
|
|
});
|
|
});
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ _getTextDensity: function(e, tags) {
|
|
|
|
|
+ var textLength = this._getInnerText(e, true).length;
|
|
|
|
|
+ if (textLength === 0) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ var childrenLength = 0;
|
|
|
|
|
+ var children = this._getAllNodesWithTag(e, tags);
|
|
|
|
|
+ this._forEachNode(children, (child) => childrenLength += this._getInnerText(child, true).length);
|
|
|
|
|
+ return childrenLength / textLength;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Clean an element of all tags of type "tag" if they look fishy.
|
|
* Clean an element of all tags of type "tag" if they look fishy.
|
|
|
* "Fishy" is an algorithm based on content length, classnames, link density, number of images & embeds, etc.
|
|
* "Fishy" is an algorithm based on content length, classnames, link density, number of images & embeds, etc.
|
|
@@ -2010,8 +2032,6 @@ Readability.prototype = {
|
|
|
if (!this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY))
|
|
if (!this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY))
|
|
|
return;
|
|
return;
|
|
|
|
|
|
|
|
- var isList = tag === "ul" || tag === "ol";
|
|
|
|
|
-
|
|
|
|
|
// Gather counts for other typical elements embedded within.
|
|
// Gather counts for other typical elements embedded within.
|
|
|
// Traverse backwards so we can remove nodes at the same time
|
|
// Traverse backwards so we can remove nodes at the same time
|
|
|
// without effecting the traversal.
|
|
// without effecting the traversal.
|
|
@@ -2023,6 +2043,14 @@ Readability.prototype = {
|
|
|
return t._readabilityDataTable;
|
|
return t._readabilityDataTable;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ var isList = tag === "ul" || tag === "ol";
|
|
|
|
|
+ if (!isList) {
|
|
|
|
|
+ var listLength = 0;
|
|
|
|
|
+ var listNodes = this._getAllNodesWithTag(node, ["ul", "ol"]);
|
|
|
|
|
+ this._forEachNode(listNodes, (list) => listLength += this._getInnerText(list).length);
|
|
|
|
|
+ isList = listLength / this._getInnerText(node).length > 0.9;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (tag === "table" && isDataTable(node)) {
|
|
if (tag === "table" && isDataTable(node)) {
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
@@ -2032,11 +2060,16 @@ Readability.prototype = {
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (this._hasAncestorTag(node, "code")) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
var weight = this._getClassWeight(node);
|
|
var weight = this._getClassWeight(node);
|
|
|
- var contentScore = 0;
|
|
|
|
|
|
|
|
|
|
this.log("Cleaning Conditionally", node);
|
|
this.log("Cleaning Conditionally", node);
|
|
|
|
|
|
|
|
|
|
+ var contentScore = 0;
|
|
|
|
|
+
|
|
|
if (weight + contentScore < 0) {
|
|
if (weight + contentScore < 0) {
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
@@ -2049,6 +2082,7 @@ Readability.prototype = {
|
|
|
var img = node.getElementsByTagName("img").length;
|
|
var img = node.getElementsByTagName("img").length;
|
|
|
var li = node.getElementsByTagName("li").length - 100;
|
|
var li = node.getElementsByTagName("li").length - 100;
|
|
|
var input = node.getElementsByTagName("input").length;
|
|
var input = node.getElementsByTagName("input").length;
|
|
|
|
|
+ var headingDensity = this._getTextDensity(node, ["h1", "h2", "h3", "h4", "h5", "h6"]);
|
|
|
|
|
|
|
|
var embedCount = 0;
|
|
var embedCount = 0;
|
|
|
var embeds = this._getAllNodesWithTag(node, ["object", "embed", "iframe"]);
|
|
var embeds = this._getAllNodesWithTag(node, ["object", "embed", "iframe"]);
|
|
@@ -2076,7 +2110,7 @@ Readability.prototype = {
|
|
|
(img > 1 && p / img < 0.5 && !this._hasAncestorTag(node, "figure")) ||
|
|
(img > 1 && p / img < 0.5 && !this._hasAncestorTag(node, "figure")) ||
|
|
|
(!isList && li > p) ||
|
|
(!isList && li > p) ||
|
|
|
(input > Math.floor(p/3)) ||
|
|
(input > Math.floor(p/3)) ||
|
|
|
- (!isList && contentLength < 25 && (img === 0 || img > 2) && !this._hasAncestorTag(node, "figure")) ||
|
|
|
|
|
|
|
+ (!isList && headingDensity < 0.9 && contentLength < 25 && (img === 0 || img > 2) && !this._hasAncestorTag(node, "figure")) ||
|
|
|
(!isList && weight < 25 && linkDensity > 0.2) ||
|
|
(!isList && weight < 25 && linkDensity > 0.2) ||
|
|
|
(weight >= 25 && linkDensity > 0.5) ||
|
|
(weight >= 25 && linkDensity > 0.5) ||
|
|
|
((embedCount === 1 && contentLength < 75) || embedCount > 1);
|
|
((embedCount === 1 && contentLength < 75) || embedCount > 1);
|
|
@@ -2106,17 +2140,38 @@ Readability.prototype = {
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Clean out spurious headers from an Element. Checks things like classnames and link density.
|
|
|
|
|
|
|
+ * Clean out spurious headers from an Element.
|
|
|
*
|
|
*
|
|
|
* @param Element
|
|
* @param Element
|
|
|
* @return void
|
|
* @return void
|
|
|
**/
|
|
**/
|
|
|
_cleanHeaders: function(e) {
|
|
_cleanHeaders: function(e) {
|
|
|
- this._removeNodes(this._getAllNodesWithTag(e, ["h1", "h2"]), function (header) {
|
|
|
|
|
- return this._getClassWeight(header) < 0;
|
|
|
|
|
|
|
+ let headingNodes = this._getAllNodesWithTag(e, ["h1", "h2"]);
|
|
|
|
|
+ this._removeNodes(headingNodes, function(node) {
|
|
|
|
|
+ let shouldRemove = this._getClassWeight(node) < 0;
|
|
|
|
|
+ if (shouldRemove) {
|
|
|
|
|
+ this.log("Removing header with low class weight:", node);
|
|
|
|
|
+ }
|
|
|
|
|
+ return shouldRemove;
|
|
|
});
|
|
});
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Check if this node is an H1 or H2 element whose content is mostly
|
|
|
|
|
+ * the same as the article title.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param Element the node to check.
|
|
|
|
|
+ * @return boolean indicating whether this is a title-like header.
|
|
|
|
|
+ */
|
|
|
|
|
+ _headerDuplicatesTitle: function(node) {
|
|
|
|
|
+ if (node.tagName != "H1" && node.tagName != "H2") {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ var heading = this._getInnerText(node, false);
|
|
|
|
|
+ this.log("Evaluating similarity of header:", heading, this._articleTitle);
|
|
|
|
|
+ return this._textSimilarity(this._articleTitle, heading) > 0.75;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
_flagIsActive: function(flag) {
|
|
_flagIsActive: function(flag) {
|
|
|
return (this._flags & flag) > 0;
|
|
return (this._flags & flag) > 0;
|
|
|
},
|
|
},
|