Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions Readability.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ Readability.prototype = {
whitespace: /^\s*$/,
hasContent: /\S$/,
hashUrl: /^#.+/,
httpUrl: /https?:\/\/[^\s<>"')\]]+/g,
srcsetUrl: /(\S+)(\s+[\d.]+[xw])?(\s*(?:,|$))/g,
b64DataUrl: /^data:\s*([^\s;,]+)\s*;\s*base64\s*,/i,
// Commas as used in Latin, Sindhi, Chinese and various other scripts.
Expand Down Expand Up @@ -1307,10 +1308,12 @@ 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.
// unaffected by this operation. Plain-text URLs are counted here
// so a block that is mostly URLs (e.g. an endnote/citation dump)
// can't outscore genuine article prose.
var candidateScore =
candidate.readability.contentScore *
(1 - this._getLinkDensity(candidate));
(1 - this._getLinkDensity(candidate, true));
candidate.readability.contentScore = candidateScore;

this.log("Candidate:", candidate, "with score " + candidateScore);
Expand Down Expand Up @@ -2140,7 +2143,7 @@ Readability.prototype = {
* @param Element
* @return number (float)
**/
_getLinkDensity(element) {
_getLinkDensity(element, includePlainTextUrls) {
var textLength = this._getInnerText(element).length;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's maybe cache the return of getInnerText which may not be cheap for large nodes, given we now potentially reuse it further down?

if (textLength === 0) {
return 0;
Expand All @@ -2155,6 +2158,17 @@ Readability.prototype = {
linkLength += this._getInnerText(linkNode).length * coefficient;
});

// Optionally count plain-text URLs (e.g. citation sections) as links.
// Used when scoring a candidates so actual article content is
// given precedence over a list of links.
if (includePlainTextUrls) {
var innerText = this._getInnerText(element);
var urls = innerText.match(this.REGEXPS.httpUrl) || [];
urls.forEach(function (url) {
linkLength += url.length;
});
}

return linkLength / textLength;
},

Expand Down
10 changes: 10 additions & 0 deletions test/test-pages/plain-text-links/expected-metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"title": "Russian Offensive Campaign Assessment, June 27, 2026",
"byline": "Fiona Noonan",
"dir": null,
"lang": "en-US",
"excerpt": "Ukraine conducted an FP-5 Flamingo cruise missile strike on the Russian Titan-Barrikady ballistic missile development and production plant.",
"siteName": "Institute for the Study of War",
"publishedTime": "2026-06-27T22:52:28+00:00",
"readerable": true
}
Loading