Skip to content

Instantly share code, notes, and snippets.

@kepano
Last active November 11, 2024 15:40
Show Gist options
  • Save kepano/90c05f162c37cf730abb8ff027987ca3 to your computer and use it in GitHub Desktop.
Save kepano/90c05f162c37cf730abb8ff027987ca3 to your computer and use it in GitHub Desktop.
Obsidian Web Clipper Bookmarklet to save articles and pages from the web (for Safari, Chrome, Firefox, and mobile browsers)

By @kepano

Visit my website to see a demo and find more information about this project.

Now available as a browser extension

Obsidian Web Clipper is now available as a browser extension, which makes it more customizable and solves issues related to running third-party scripts on web pages.

I originally created Obsidian Web Clipper as a bookmarklet in 2021. The bookmarklet is still available below for archival purposes, or if you prefer not to install browser extensions.

Bookmarklet

Installation

Create a new bookmark in your browser, then copy/paste the minified code below into the URL field.

You can customize the output using the optional variables at the top, and the template at the bottom. The default template is designed for use with the Dataview plugin. If you make changes I recommend using Bookmarklet Maker to minify and URI encode the bookmarklet.

Usage

By default, clicking the bookmarklet creates a new Obsidian file from the main body of the article (similar to Readability view). Alternatively you can choose to create a file from a selection, by either selecting all (CMD+A), or just a portion of the page.

Any images in the content will be embedded as external references. If you want to download images locally you can use the Local Images plugin which allows you to download images for a note.

Troubleshooting

This bookmarklet may not work on all websites. If you run into issues, you can also try the MarkDownload browser extension which provides similar functionality. You can troubleshoot issues by opening the Developer Console in your browser and checking if any errors appear when you click the bookmarklet. The most common error is that a website or the browser itself is blocking third party code execution. Unfortunately there is no good solve for that yet.

javascript: Promise.all([import('https://unpkg.com/turndown@6.0.0?module'), import('https://unpkg.com/@tehshrike/readability@0.2.0'), ]).then(async ([{
default: Turndown
}, {
default: Readability
}]) => {
/* Optional vault name */
const vault = "";
/* Optional folder name such as "Clippings/" */
const folder = "Clippings/";
/* Optional tags */
let tags = "clippings";
/* Parse the site's meta keywords content into tags, if present */
if (document.querySelector('meta[name="keywords" i]')) {
var keywords = document.querySelector('meta[name="keywords" i]').getAttribute('content').split(',');
keywords.forEach(function(keyword) {
let tag = ' ' + keyword.split(' ').join('');
tags += tag;
});
}
function getSelectionHtml() {
var html = "";
if (typeof window.getSelection != "undefined") {
var sel = window.getSelection();
if (sel.rangeCount) {
var container = document.createElement("div");
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
container.appendChild(sel.getRangeAt(i).cloneContents());
}
html = container.innerHTML;
}
} else if (typeof document.selection != "undefined") {
if (document.selection.type == "Text") {
html = document.selection.createRange().htmlText;
}
}
return html;
}
const selection = getSelectionHtml();
const {
title,
byline,
content
} = new Readability(document.cloneNode(true)).parse();
function getFileName(fileName) {
var userAgent = window.navigator.userAgent,
platform = window.navigator.platform,
windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];
if (windowsPlatforms.indexOf(platform) !== -1) {
fileName = fileName.replace(':', '').replace(/[/\\?%*|"<>]/g, '-');
} else {
fileName = fileName.replace(':', '').replace(/\//g, '-').replace(/\\/g, '-');
}
return fileName;
}
const fileName = getFileName(title);
if (selection) {
var markdownify = selection;
} else {
var markdownify = content;
}
if (vault) {
var vaultName = '&vault=' + encodeURIComponent(`${vault}`);
} else {
var vaultName = '';
}
const markdownBody = new Turndown({
headingStyle: 'atx',
hr: '---',
bulletListMarker: '-',
codeBlockStyle: 'fenced',
emDelimiter: '*',
}).turndown(markdownify);
var date = new Date();
function convertDate(date) {
var yyyy = date.getFullYear().toString();
var mm = (date.getMonth()+1).toString();
var dd = date.getDate().toString();
var mmChars = mm.split('');
var ddChars = dd.split('');
return yyyy + '-' + (mmChars[1]?mm:"0"+mmChars[0]) + '-' + (ddChars[1]?dd:"0"+ddChars[0]);
}
const today = convertDate(date);
// Utility function to get meta content by name or property
function getMetaContent(attr, value) {
var element = document.querySelector(`meta[${attr}='${value}']`);
return element ? element.getAttribute("content").trim() : "";
}
// Fetch byline, meta author, property author, or site name
var author = byline || getMetaContent("name", "author") || getMetaContent("property", "author") || getMetaContent("property", "og:site_name");
// Check if there's an author and add brackets
var authorBrackets = author ? `"[[${author}]]"` : "";
/* Try to get published date */
var timeElement = document.querySelector("time");
var publishedDate = timeElement ? timeElement.getAttribute("datetime") : "";
if (publishedDate && publishedDate.trim() !== "") {
var date = new Date(publishedDate);
var year = date.getFullYear();
var month = date.getMonth() + 1; // Months are 0-based in JavaScript
var day = date.getDate();
// Pad month and day with leading zeros if necessary
month = month < 10 ? '0' + month : month;
day = day < 10 ? '0' + day : day;
var published = year + '-' + month + '-' + day;
} else {
var published = ''
}
/* YAML front matter as tags render cleaner with special chars */
const fileContent =
'---\n'
+ 'category: "[[Clippings]]"\n'
+ 'author: ' + authorBrackets + '\n'
+ 'title: "' + title + '"\n'
+ 'source: ' + document.URL + '\n'
+ 'clipped: ' + today + '\n'
+ 'published: ' + published + '\n'
+ 'topics: \n'
+ 'tags: [' + tags + ']\n'
+ '---\n\n'
+ markdownBody ;
document.location.href = "obsidian://new?"
+ "file=" + encodeURIComponent(folder + fileName)
+ "&content=" + encodeURIComponent(fileContent)
+ vaultName ;
})
javascript:(function()%7Bjavascript%3A%20Promise.all(%5Bimport('https%3A%2F%2Funpkg.com%2Fturndown%406.0.0%3Fmodule')%2C%20import('https%3A%2F%2Funpkg.com%2F%40tehshrike%2Freadability%400.2.0')%2C%20%5D).then(async%20(%5B%7B%0A%20%20%20%20default%3A%20Turndown%0A%7D%2C%20%7B%0A%20%20%20%20default%3A%20Readability%0A%7D%5D)%20%3D%3E%20%7B%0A%0A%20%20%2F*%20Optional%20vault%20name%20*%2F%0A%20%20const%20vault%20%3D%20%22%22%3B%0A%0A%20%20%2F*%20Optional%20folder%20name%20such%20as%20%22Clippings%2F%22%20*%2F%0A%20%20const%20folder%20%3D%20%22Clippings%2F%22%3B%0A%0A%20%20%2F*%20Optional%20tags%20%20*%2F%0A%20%20let%20tags%20%3D%20%22clippings%22%3B%0A%0A%20%20%2F*%20Parse%20the%20site's%20meta%20keywords%20content%20into%20tags%2C%20if%20present%20*%2F%0A%20%20if%20(document.querySelector('meta%5Bname%3D%22keywords%22%20i%5D'))%20%7B%0A%20%20%20%20%20%20var%20keywords%20%3D%20document.querySelector('meta%5Bname%3D%22keywords%22%20i%5D').getAttribute('content').split('%2C')%3B%0A%0A%20%20%20%20%20%20keywords.forEach(function(keyword)%20%7B%0A%20%20%20%20%20%20%20%20%20%20let%20tag%20%3D%20'%20'%20%2B%20keyword.split('%20').join('')%3B%0A%20%20%20%20%20%20%20%20%20%20tags%20%2B%3D%20tag%3B%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%7D%0A%0A%20%20function%20getSelectionHtml()%20%7B%0A%20%20%20%20var%20html%20%3D%20%22%22%3B%0A%20%20%20%20if%20(typeof%20window.getSelection%20!%3D%20%22undefined%22)%20%7B%0A%20%20%20%20%20%20%20%20var%20sel%20%3D%20window.getSelection()%3B%0A%20%20%20%20%20%20%20%20if%20(sel.rangeCount)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20var%20container%20%3D%20document.createElement(%22div%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20(var%20i%20%3D%200%2C%20len%20%3D%20sel.rangeCount%3B%20i%20%3C%20len%3B%20%2B%2Bi)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20container.appendChild(sel.getRangeAt(i).cloneContents())%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20html%20%3D%20container.innerHTML%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%20else%20if%20(typeof%20document.selection%20!%3D%20%22undefined%22)%20%7B%0A%20%20%20%20%20%20%20%20if%20(document.selection.type%20%3D%3D%20%22Text%22)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20html%20%3D%20document.selection.createRange().htmlText%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20return%20html%3B%0A%20%20%7D%0A%0A%20%20const%20selection%20%3D%20getSelectionHtml()%3B%0A%0A%20%20const%20%7B%0A%20%20%20%20%20%20title%2C%0A%20%20%20%20%20%20byline%2C%0A%20%20%20%20%20%20content%0A%20%20%7D%20%3D%20new%20Readability(document.cloneNode(true)).parse()%3B%0A%0A%20%20function%20getFileName(fileName)%20%7B%0A%20%20%20%20var%20userAgent%20%3D%20window.navigator.userAgent%2C%0A%20%20%20%20%20%20%20%20platform%20%3D%20window.navigator.platform%2C%0A%20%20%20%20%20%20%20%20windowsPlatforms%20%3D%20%5B'Win32'%2C%20'Win64'%2C%20'Windows'%2C%20'WinCE'%5D%3B%0A%0A%20%20%20%20if%20(windowsPlatforms.indexOf(platform)%20!%3D%3D%20-1)%20%7B%0A%20%20%20%20%20%20fileName%20%3D%20fileName.replace('%3A'%2C%20'').replace(%2F%5B%2F%5C%5C%3F%25*%7C%22%3C%3E%5D%2Fg%2C%20'-')%3B%0A%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20fileName%20%3D%20fileName.replace('%3A'%2C%20'').replace(%2F%5C%2F%2Fg%2C%20'-').replace(%2F%5C%5C%2Fg%2C%20'-')%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20fileName%3B%0A%20%20%7D%0A%20%20const%20fileName%20%3D%20getFileName(title)%3B%0A%0A%20%20if%20(selection)%20%7B%0A%20%20%20%20%20%20var%20markdownify%20%3D%20selection%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20var%20markdownify%20%3D%20content%3B%0A%20%20%7D%0A%0A%20%20if%20(vault)%20%7B%0A%20%20%20%20%20%20var%20vaultName%20%3D%20'%26vault%3D'%20%2B%20encodeURIComponent(%60%24%7Bvault%7D%60)%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20var%20vaultName%20%3D%20''%3B%0A%20%20%7D%0A%0A%20%20const%20markdownBody%20%3D%20new%20Turndown(%7B%0A%20%20%20%20%20%20headingStyle%3A%20'atx'%2C%0A%20%20%20%20%20%20hr%3A%20'---'%2C%0A%20%20%20%20%20%20bulletListMarker%3A%20'-'%2C%0A%20%20%20%20%20%20codeBlockStyle%3A%20'fenced'%2C%0A%20%20%20%20%20%20emDelimiter%3A%20'*'%2C%0A%20%20%7D).turndown(markdownify)%3B%0A%0A%20%20var%20date%20%3D%20new%20Date()%3B%0A%0A%20%20function%20convertDate(date)%20%7B%0A%20%20%20%20var%20yyyy%20%3D%20date.getFullYear().toString()%3B%0A%20%20%20%20var%20mm%20%3D%20(date.getMonth()%2B1).toString()%3B%0A%20%20%20%20var%20dd%20%20%3D%20date.getDate().toString()%3B%0A%20%20%20%20var%20mmChars%20%3D%20mm.split('')%3B%0A%20%20%20%20var%20ddChars%20%3D%20dd.split('')%3B%0A%20%20%20%20return%20yyyy%20%2B%20'-'%20%2B%20(mmChars%5B1%5D%3Fmm%3A%220%22%2BmmChars%5B0%5D)%20%2B%20'-'%20%2B%20(ddChars%5B1%5D%3Fdd%3A%220%22%2BddChars%5B0%5D)%3B%0A%20%20%7D%0A%0A%20%20const%20today%20%3D%20convertDate(date)%3B%0A%0A%20%20%2F%2F%20Utility%20function%20to%20get%20meta%20content%20by%20name%20or%20property%0A%20%20function%20getMetaContent(attr%2C%20value)%20%7B%0A%20%20%20%20%20%20var%20element%20%3D%20document.querySelector(%60meta%5B%24%7Battr%7D%3D'%24%7Bvalue%7D'%5D%60)%3B%0A%20%20%20%20%20%20return%20element%20%3F%20element.getAttribute(%22content%22).trim()%20%3A%20%22%22%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20Fetch%20byline%2C%20meta%20author%2C%20property%20author%2C%20or%20site%20name%0A%20%20var%20author%20%3D%20byline%20%7C%7C%20getMetaContent(%22name%22%2C%20%22author%22)%20%7C%7C%20getMetaContent(%22property%22%2C%20%22author%22)%20%7C%7C%20getMetaContent(%22property%22%2C%20%22og%3Asite_name%22)%3B%0A%0A%20%20%2F%2F%20Check%20if%20there's%20an%20author%20and%20add%20brackets%0A%20%20var%20authorBrackets%20%3D%20author%20%3F%20%60%22%5B%5B%24%7Bauthor%7D%5D%5D%22%60%20%3A%20%22%22%3B%0A%0A%0A%20%20%2F*%20Try%20to%20get%20published%20date%20*%2F%0A%20%20var%20timeElement%20%3D%20document.querySelector(%22time%22)%3B%0A%20%20var%20publishedDate%20%3D%20timeElement%20%3F%20timeElement.getAttribute(%22datetime%22)%20%3A%20%22%22%3B%0A%0A%20%20if%20(publishedDate%20%26%26%20publishedDate.trim()%20!%3D%3D%20%22%22)%20%7B%0A%20%20%20%20%20%20var%20date%20%3D%20new%20Date(publishedDate)%3B%0A%20%20%20%20%20%20var%20year%20%3D%20date.getFullYear()%3B%0A%20%20%20%20%20%20var%20month%20%3D%20date.getMonth()%20%2B%201%3B%20%2F%2F%20Months%20are%200-based%20in%20JavaScript%0A%20%20%20%20%20%20var%20day%20%3D%20date.getDate()%3B%0A%0A%20%20%20%20%20%20%2F%2F%20Pad%20month%20and%20day%20with%20leading%20zeros%20if%20necessary%0A%20%20%20%20%20%20month%20%3D%20month%20%3C%2010%20%3F%20'0'%20%2B%20month%20%3A%20month%3B%0A%20%20%20%20%20%20day%20%3D%20day%20%3C%2010%20%3F%20'0'%20%2B%20day%20%3A%20day%3B%0A%0A%20%20%20%20%20%20var%20published%20%3D%20year%20%2B%20'-'%20%2B%20month%20%2B%20'-'%20%2B%20day%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20var%20published%20%3D%20''%0A%20%20%7D%0A%0A%20%20%2F*%20YAML%20front%20matter%20as%20tags%20render%20cleaner%20with%20special%20chars%20%20*%2F%0A%20%20const%20fileContent%20%3D%20%0A%20%20%20%20%20%20'---%5Cn'%0A%20%20%20%20%20%20%2B%20'category%3A%20%22%5B%5BClippings%5D%5D%22%5Cn'%0A%20%20%20%20%20%20%2B%20'author%3A%20'%20%2B%20authorBrackets%20%2B%20'%5Cn'%0A%20%20%20%20%20%20%2B%20'title%3A%20%22'%20%2B%20title%20%2B%20'%22%5Cn'%0A%20%20%20%20%20%20%2B%20'source%3A%20'%20%2B%20document.URL%20%2B%20'%5Cn'%0A%20%20%20%20%20%20%2B%20'clipped%3A%20'%20%2B%20today%20%2B%20'%5Cn'%0A%20%20%20%20%20%20%2B%20'published%3A%20'%20%2B%20published%20%2B%20'%5Cn'%20%0A%20%20%20%20%20%20%2B%20'topics%3A%20%5Cn'%0A%20%20%20%20%20%20%2B%20'tags%3A%20%5B'%20%2B%20tags%20%2B%20'%5D%5Cn'%0A%20%20%20%20%20%20%2B%20'---%5Cn%5Cn'%0A%20%20%20%20%20%20%2B%20markdownBody%20%3B%0A%0A%20%20%20document.location.href%20%3D%20%22obsidian%3A%2F%2Fnew%3F%22%0A%20%20%20%20%2B%20%22file%3D%22%20%2B%20encodeURIComponent(folder%20%2B%20fileName)%0A%20%20%20%20%2B%20%22%26content%3D%22%20%2B%20encodeURIComponent(fileContent)%0A%20%20%20%20%2B%20vaultName%20%3B%0A%0A%7D)%7D)()%3B
@Maahantuoja
Copy link

Hi, I'm using the opera browser, but I can't get this to work properly. If I capture only some part of the page content then everything goes ok, but if I try to capture the whole page then it just closes the tab, but doesn't save anything.

I greatly appreciate it if someone could help me with this problem.

@bramhooimeijer
Copy link

Hi!

A small bug in the gist:

      fileName = fileName.replace(':', '').replace(/\//g, '-').replace(/\\/g, '-');

Here you only replace the first occurence of :, leaving the rest as a possible issue.

      fileName = fileName.replace(/:/g, '').replace(/\//g, '-').replace(/\\/g, '-');

Should fix that.

@Jerwins
Copy link

Jerwins commented Jan 30, 2024

On my Windows 10 with Google Chrome I had to add this to the registry and then it works.

Got the tip via this thread and used the Obsidian Clipper Maker

obsidan-allow.reg

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome]

[HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome\AutoOpenAllowedForURLs]
"1"="obsidian://"

Thanks, this worked.

@KernelBypass
Copy link

KernelBypass commented Jan 30, 2024

With content security policy enforcement in Safari this needs to embed the packages instead of importing them. My gist over here fixes it:

https://gist.github.com/philips/8f4e93cb2be2428344ece63aecca7941

https://github.blog/2013-04-19-content-security-policy/ https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP

[Error] Refused to load https://unpkg.com/turndown@6.0.0?module because it does not appear in the script-src directive of the Content Security Policy. [Error] Refused to load https://unpkg.com/@tehshrike/readability@0.2.0 because it does not appear in the script-src directive of the Content Security Policy.

Kepano's OG script works fine for me in MacOS Safari, but yours doesn't seem to fit into the bookmark's address field even when minified.

  1. Am I doing something incorrectly? I copy your gist -> https://caiorss.github.io/bookmarklet-maker/ -> insert into a bookmark. Pretty straightforward, but yours won't paste fully.
  2. What is the Safari content security policy enforcement issue contingent upon? Because I do not seem to have that issue.

Both work great in Firefox though.

@Kmfernan5
Copy link

@dbdennis
Copy link

dbdennis commented Feb 9, 2024

On my Windows 10 with Google Chrome I had to add this to the registry and then it works.
Got the tip via this thread and used the Obsidian Clipper Maker
obsidan-allow.reg

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome]

[HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome\AutoOpenAllowedForURLs]
"1"="obsidian://"

Thanks, this worked.

Thank you so much for this excellent clipper!! It is working wonderfully with chrome and two macbooks. It isn't working on chrome and a Windows 10 setup. I did these steps and there are no errors in the Developers Console when I run the original "Clip in Obsidian" or a modified one from the Maker (specifying a Vault and Clippings folder). Again, both work perfectly on Macs. But even though it is clean on the chrome console on the Windows machine (helping a friend), Obsidian doesn't open and the page isn't copied. (If I just do `obsidian://' in the chrome url bar, it does open, so I don't think it is a security setting.)

Any suggestions highly appreciated!

@jmiralva
Copy link

Hi, thanks for awesome codes! I am an Arc Browser user and bookmarklet doesn't work for me. I turn it an extension (dev-mode) by using this page, want to share everyone. Bookmarklet2Extension:https://sandbox.self.li/bookmarklet-to-extension/

@yagmurx does this still work for you? i'm trying to use it now but can't get it to work. any tips? thanks!

@aREversez
Copy link

It works well with FireFox, but doesn't work with my Chrome. I don't know why..

@aREversez
Copy link

On my Windows 10 with Google Chrome I had to add this to the registry and then it works.

Got the tip via this thread and used the Obsidian Clipper Maker

obsidan-allow.reg

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome]

[HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome\AutoOpenAllowedForURLs]
"1"="obsidian://"

I tried but it still didn't work. Do you know why?

@saifsmailbox98
Copy link

saifsmailbox98 commented Jun 21, 2024

Simplified version that only saves the following: title, url, local timestamp (e.g., 2024-06-21T06:12:25)

javascript:(function(){
    const now = new Date();
    const pad = (num) => String(num).padStart(2, '0');
    const formattedDate = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;

    const title = document.title;
    const url = document.URL;

    const folder = "00 - 09 System/04 Bookmarks/04.01 Obsidian Web Clipper/Raw Notes/";
    const fileName = `${title}`;

    const fileContent = 
        '---\n'
        + `url: ${url}\n`
        + `date: ${formattedDate}\n`
        + '---\n\n';

    document.location.href = "obsidian://new?"
        + "file=" + encodeURIComponent(folder + fileName)
        + "&content=" + encodeURIComponent(fileContent);
})();

@ericraymond
Copy link

ericraymond commented Aug 11, 2024

Here's a version that includes the latest gist with the two modules inlined ... which allows it to work on pretty much all websites (including github) that block third party modules via a CSP policy.

I realize this is substantially bigger, but it would be nice to update the version in this gist or provide this as a second version that stays in sync. My guess is many people give up on the current version in various ways and might not find this comment.

javascript:(function()%7B%2F*%20Optional%20vault%20name%20*%2F%0Aconst%20vault%20%3D%20''%3B%0A%0A%2F*%20Optional%20folder%20name%20such%20as%20%22Clippings%2F%22%20*%2F%0Aconst%20folder%20%3D%20'Clippings%2F'%3B%0A%0A%2F*%20Optional%20tags%20%20*%2F%0Alet%20tags%20%3D%20'clippings'%3B%0A%0A%0A%2F%2F%2F%20Readability%3A%20https%3A%2F%2Funpkg.com%2F%40tehshrike%2Freadability%400.2.0%0A%2F*%0A%20*%20Copyright%20(c)%202010%20Arc90%20Inc%0A%20*%0A%20*%20Licensed%20under%20the%20Apache%20License%2C%20Version%202.0%20(the%20%22License%22)%3B%0A%20*%20you%20may%20not%20use%20this%20file%20except%20in%20compliance%20with%20the%20License.%0A%20*%20You%20may%20obtain%20a%20copy%20of%20the%20License%20at%0A%20*%0A%20*%20%20%20%20%20http%3A%2F%2Fwww.apache.org%2Flicenses%2FLICENSE-2.0%0A%20*%0A%20*%20Unless%20required%20by%20applicable%20law%20or%20agreed%20to%20in%20writing%2C%20software%0A%20*%20distributed%20under%20the%20License%20is%20distributed%20on%20an%20%22AS%20IS%22%20BASIS%2C%0A%20*%20WITHOUT%20WARRANTIES%20OR%20CONDITIONS%20OF%20ANY%20KIND%2C%20either%20express%20or%20implied.%0A%20*%20See%20the%20License%20for%20the%20specific%20language%20governing%20permissions%20and%0A%20*%20limitations%20under%20the%20License.%0A%20*%2F%0A%0A%2F*%0A%20*%20This%20code%20is%20heavily%20based%20on%20Arc90's%20readability.js%20(1.7.1)%20script%0A%20*%20available%20at%3A%20http%3A%2F%2Fcode.google.com%2Fp%2Farc90labs-readability%0A%20*%2F%0A%0A%2F**%0A%20*%20Public%20constructor.%0A%20*%20%40param%20%7BHTMLDocument%7D%20doc%20%20%20%20%20The%20document%20to%20parse.%0A%20*%20%40param%20%7BObject%7D%20%20%20%20%20%20%20options%20The%20options%20object.%0A%20*%2F%0Afunction%20Readability(doc%2C%20options)%20%7B%0A%20%20%2F%2F%20In%20some%20older%20versions%2C%20people%20passed%20a%20URI%20as%20the%20first%20argument.%20Cope%3A%0A%20%20if%20(options%20%26%26%20options.documentElement)%20%7B%0A%20%20%20%20doc%20%3D%20options%3B%0A%20%20%20%20options%20%3D%20arguments%5B2%5D%3B%0A%20%20%7D%20else%20if%20(!doc%20%7C%7C%20!doc.documentElement)%20%7B%0A%20%20%20%20throw%20new%20Error(%0A%20%20%20%20%20%20'First%20argument%20to%20Readability%20constructor%20should%20be%20a%20document%20object.'%2C%0A%20%20%20%20)%3B%0A%20%20%7D%0A%20%20options%20%3D%20options%20%7C%7C%20%7B%7D%3B%0A%0A%20%20this._doc%20%3D%20doc%3B%0A%20%20this._docJSDOMParser%20%3D%20this._doc.firstChild.__JSDOMParser__%3B%0A%20%20this._articleTitle%20%3D%20null%3B%0A%20%20this._articleByline%20%3D%20null%3B%0A%20%20this._articleDir%20%3D%20null%3B%0A%20%20this._articleSiteName%20%3D%20null%3B%0A%20%20this._attempts%20%3D%20%5B%5D%3B%0A%0A%20%20%2F%2F%20Configurable%20options%0A%20%20this._debug%20%3D%20!!options.debug%3B%0A%20%20this._maxElemsToParse%20%3D%0A%20%20%20%20options.maxElemsToParse%20%7C%7C%20this.DEFAULT_MAX_ELEMS_TO_PARSE%3B%0A%20%20this._nbTopCandidates%20%3D%0A%20%20%20%20options.nbTopCandidates%20%7C%7C%20this.DEFAULT_N_TOP_CANDIDATES%3B%0A%20%20this._charThreshold%20%3D%20options.charThreshold%20%7C%7C%20this.DEFAULT_CHAR_THRESHOLD%3B%0A%20%20this._classesToPreserve%20%3D%20this.CLASSES_TO_PRESERVE.concat(%0A%20%20%20%20options.classesToPreserve%20%7C%7C%20%5B%5D%2C%0A%20%20)%3B%0A%20%20this._keepClasses%20%3D%20!!options.keepClasses%3B%0A%0A%20%20%2F%2F%20Start%20with%20all%20flags%20set%0A%20%20this._flags%20%3D%0A%20%20%20%20this.FLAG_STRIP_UNLIKELYS%20%7C%0A%20%20%20%20this.FLAG_WEIGHT_CLASSES%20%7C%0A%20%20%20%20this.FLAG_CLEAN_CONDITIONALLY%3B%0A%0A%20%20let%20logEl%3B%0A%0A%20%20%2F%2F%20Control%20whether%20log%20messages%20are%20sent%20to%20the%20console%0A%20%20if%20(this._debug)%20%7B%0A%20%20%20%20logEl%20%3D%20function%20(e)%20%7B%0A%20%20%20%20%20%20const%20rv%20%3D%20e.nodeName%20%2B%20'%20'%3B%0A%20%20%20%20%20%20if%20(e.nodeType%20%3D%3D%20e.TEXT_NODE)%20%7B%0A%20%20%20%20%20%20%20%20return%20rv%20%2B%20'(%22'%20%2B%20e.textContent%20%2B%20'%22)'%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20const%20classDesc%20%3D%20e.className%20%26%26%20'.'%20%2B%20e.className.replace(%2F%20%2Fg%2C%20'.')%3B%0A%20%20%20%20%20%20let%20elDesc%20%3D%20''%3B%0A%20%20%20%20%20%20if%20(e.id)%20%7B%0A%20%20%20%20%20%20%20%20elDesc%20%3D%20'(%23'%20%2B%20e.id%20%2B%20classDesc%20%2B%20')'%3B%0A%20%20%20%20%20%20%7D%20else%20if%20(classDesc)%20%7B%0A%20%20%20%20%20%20%20%20elDesc%20%3D%20'('%20%2B%20classDesc%20%2B%20')'%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20return%20rv%20%2B%20elDesc%3B%0A%20%20%20%20%7D%3B%0A%20%20%20%20this.log%20%3D%20function%20()%20%7B%0A%20%20%20%20%20%20if%20(typeof%20dump%20!%3D%3D%20'undefined')%20%7B%0A%20%20%20%20%20%20%20%20const%20msg%20%3D%20Array.prototype.map%0A%20%20%20%20%20%20%20%20%20%20.call(arguments%2C%20(x)%20%3D%3E%20(x%20%26%26%20x.nodeName%20%3F%20logEl(x)%20%3A%20x))%0A%20%20%20%20%20%20%20%20%20%20.join('%20')%3B%0A%20%20%20%20%20%20%20%20dump('Reader%3A%20(Readability)%20'%20%2B%20msg%20%2B%20'%5Cn')%3B%0A%20%20%20%20%20%20%7D%20else%20if%20(typeof%20console%20!%3D%3D%20'undefined')%20%7B%0A%20%20%20%20%20%20%20%20const%20args%20%3D%20%5B'Reader%3A%20(Readability)%20'%5D.concat(arguments)%3B%0A%20%20%20%20%20%20%20%20console.log.apply(console%2C%20args)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20this.log%20%3D%20function%20()%20%7B%7D%3B%0A%20%20%7D%0A%7D%0A%0AReadability.prototype%20%3D%20%7B%0A%20%20FLAG_STRIP_UNLIKELYS%3A%200x1%2C%0A%20%20FLAG_WEIGHT_CLASSES%3A%200x2%2C%0A%20%20FLAG_CLEAN_CONDITIONALLY%3A%200x4%2C%0A%0A%20%20%2F%2F%20https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FAPI%2FNode%2FnodeType%0A%20%20ELEMENT_NODE%3A%201%2C%0A%20%20TEXT_NODE%3A%203%2C%0A%0A%20%20%2F%2F%20Max%20number%20of%20nodes%20supported%20by%20this%20parser.%20Default%3A%200%20(no%20limit)%0A%20%20DEFAULT_MAX_ELEMS_TO_PARSE%3A%200%2C%0A%0A%20%20%2F%2F%20The%20number%20of%20top%20candidates%20to%20consider%20when%20analysing%20how%0A%20%20%2F%2F%20tight%20the%20competition%20is%20among%20candidates.%0A%20%20DEFAULT_N_TOP_CANDIDATES%3A%205%2C%0A%0A%20%20%2F%2F%20Element%20tags%20to%20score%20by%20default.%0A%20%20DEFAULT_TAGS_TO_SCORE%3A%20'section%2Ch2%2Ch3%2Ch4%2Ch5%2Ch6%2Cp%2Ctd%2Cpre'%0A%20%20%20%20.toUpperCase()%0A%20%20%20%20.split('%2C')%2C%0A%0A%20%20%2F%2F%20The%20default%20number%20of%20chars%20an%20article%20must%20have%20in%20order%20to%20return%20a%20result%0A%20%20DEFAULT_CHAR_THRESHOLD%3A%20500%2C%0A%0A%20%20%2F%2F%20All%20of%20the%20regular%20expressions%20in%20use%20within%20readability.%0A%20%20%2F%2F%20Defined%20up%20here%20so%20we%20don't%20instantiate%20them%20repeatedly%20in%20loops.%0A%20%20REGEXPS%3A%20%7B%0A%20%20%20%20%2F%2F%20NOTE%3A%20These%20two%20regular%20expressions%20are%20duplicated%20in%0A%20%20%20%20%2F%2F%20Readability-readerable.js.%20Please%20keep%20both%20copies%20in%20sync.%0A%20%20%20%20unlikelyCandidates%3A%0A%20%20%20%20%20%20%2F-ad-%7Cai2html%7Cbanner%7Cbreadcrumbs%7Ccombx%7Ccomment%7Ccommunity%7Ccover-wrap%7Cdisqus%7Cextra%7Cfooter%7Cgdpr%7Cheader%7Clegends%7Cmenu%7Crelated%7Cremark%7Creplies%7Crss%7Cshoutbox%7Csidebar%7Cskyscraper%7Csocial%7Csponsor%7Csupplemental%7Cad-break%7Cagegate%7Cpagination%7Cpager%7Cpopup%7Cyom-remote%2Fi%2C%0A%20%20%20%20okMaybeItsACandidate%3A%20%2Fand%7Carticle%7Cbody%7Ccolumn%7Ccontent%7Cmain%7Cshadow%2Fi%2C%0A%0A%20%20%20%20positive%3A%0A%20%20%20%20%20%20%2Farticle%7Cbody%7Ccontent%7Centry%7Chentry%7Ch-entry%7Cmain%7Cpage%7Cpagination%7Cpost%7Ctext%7Cblog%7Cstory%2Fi%2C%0A%20%20%20%20negative%3A%0A%20%20%20%20%20%20%2Fhidden%7C%5Ehid%24%7C%20hid%24%7C%20hid%20%7C%5Ehid%20%7Cbanner%7Ccombx%7Ccomment%7Ccom-%7Ccontact%7Cfoot%7Cfooter%7Cfootnote%7Cgdpr%7Cmasthead%7Cmedia%7Cmeta%7Coutbrain%7Cpromo%7Crelated%7Cscroll%7Cshare%7Cshoutbox%7Csidebar%7Cskyscraper%7Csponsor%7Cshopping%7Ctags%7Ctool%7Cwidget%2Fi%2C%0A%20%20%20%20extraneous%3A%0A%20%20%20%20%20%20%2Fprint%7Carchive%7Ccomment%7Cdiscuss%7Ce%5B%5C-%5D%3Fmail%7Cshare%7Creply%7Call%7Clogin%7Csign%7Csingle%7Cutility%2Fi%2C%0A%20%20%20%20byline%3A%20%2Fbyline%7Cauthor%7Cdateline%7Cwrittenby%7Cp-author%2Fi%2C%0A%20%20%20%20replaceFonts%3A%20%2F%3C(%5C%2F%3F)font%5B%5E%3E%5D*%3E%2Fgi%2C%0A%20%20%20%20normalize%3A%20%2F%5Cs%7B2%2C%7D%2Fg%2C%0A%20%20%20%20videos%3A%0A%20%20%20%20%20%20%2F%5C%2F%5C%2F(www%5C.)%3F((dailymotion%7Cyoutube%7Cyoutube-nocookie%7Cplayer%5C.vimeo%7Cv%5C.qq)%5C.com%7C(archive%7Cupload%5C.wikimedia)%5C.org%7Cplayer%5C.twitch%5C.tv)%2Fi%2C%0A%20%20%20%20shareElements%3A%20%2F(%5Cb%7C_)(share%7Csharedaddy)(%5Cb%7C_)%2Fi%2C%0A%20%20%20%20nextLink%3A%20%2F(next%7Cweiter%7Ccontinue%7C%3E(%5B%5E%5C%7C%5D%7C%24)%7C%C2%BB(%5B%5E%5C%7C%5D%7C%24))%2Fi%2C%0A%20%20%20%20prevLink%3A%20%2F(prev%7Cearl%7Cold%7Cnew%7C%3C%7C%C2%AB)%2Fi%2C%0A%20%20%20%20whitespace%3A%20%2F%5E%5Cs*%24%2F%2C%0A%20%20%20%20hasContent%3A%20%2F%5CS%24%2F%2C%0A%20%20%20%20srcsetUrl%3A%20%2F(%5CS%2B)(%5Cs%2B%5B%5Cd.%5D%2B%5Bxw%5D)%3F(%5Cs*(%3F%3A%2C%7C%24))%2Fg%2C%0A%20%20%20%20b64DataUrl%3A%20%2F%5Edata%3A%5Cs*(%5B%5E%5Cs%3B%2C%5D%2B)%5Cs*%3B%5Cs*base64%5Cs*%2C%2Fi%2C%0A%20%20%7D%2C%0A%0A%20%20DIV_TO_P_ELEMS%3A%20%5B%0A%20%20%20%20'A'%2C%0A%20%20%20%20'BLOCKQUOTE'%2C%0A%20%20%20%20'DL'%2C%0A%20%20%20%20'DIV'%2C%0A%20%20%20%20'IMG'%2C%0A%20%20%20%20'OL'%2C%0A%20%20%20%20'P'%2C%0A%20%20%20%20'PRE'%2C%0A%20%20%20%20'TABLE'%2C%0A%20%20%20%20'UL'%2C%0A%20%20%20%20'SELECT'%2C%0A%20%20%5D%2C%0A%0A%20%20ALTER_TO_DIV_EXCEPTIONS%3A%20%5B'DIV'%2C%20'ARTICLE'%2C%20'SECTION'%2C%20'P'%5D%2C%0A%0A%20%20PRESENTATIONAL_ATTRIBUTES%3A%20%5B%0A%20%20%20%20'align'%2C%0A%20%20%20%20'background'%2C%0A%20%20%20%20'bgcolor'%2C%0A%20%20%20%20'border'%2C%0A%20%20%20%20'cellpadding'%2C%0A%20%20%20%20'cellspacing'%2C%0A%20%20%20%20'frame'%2C%0A%20%20%20%20'hspace'%2C%0A%20%20%20%20'rules'%2C%0A%20%20%20%20'style'%2C%0A%20%20%20%20'valign'%2C%0A%20%20%20%20'vspace'%2C%0A%20%20%5D%2C%0A%0A%20%20DEPRECATED_SIZE_ATTRIBUTE_ELEMS%3A%20%5B'TABLE'%2C%20'TH'%2C%20'TD'%2C%20'HR'%2C%20'PRE'%5D%2C%0A%0A%20%20%2F%2F%20The%20commented%20out%20elements%20qualify%20as%20phrasing%20content%20but%20tend%20to%20be%0A%20%20%2F%2F%20removed%20by%20readability%20when%20put%20into%20paragraphs%2C%20so%20we%20ignore%20them%20here.%0A%20%20PHRASING_ELEMS%3A%20%5B%0A%20%20%20%20%2F%2F%20%22CANVAS%22%2C%20%22IFRAME%22%2C%20%22SVG%22%2C%20%22VIDEO%22%2C%0A%20%20%20%20'ABBR'%2C%0A%20%20%20%20'AUDIO'%2C%0A%20%20%20%20'B'%2C%0A%20%20%20%20'BDO'%2C%0A%20%20%20%20'BR'%2C%0A%20%20%20%20'BUTTON'%2C%0A%20%20%20%20'CITE'%2C%0A%20%20%20%20'CODE'%2C%0A%20%20%20%20'DATA'%2C%0A%20%20%20%20'DATALIST'%2C%0A%20%20%20%20'DFN'%2C%0A%20%20%20%20'EM'%2C%0A%20%20%20%20'EMBED'%2C%0A%20%20%20%20'I'%2C%0A%20%20%20%20'IMG'%2C%0A%20%20%20%20'INPUT'%2C%0A%20%20%20%20'KBD'%2C%0A%20%20%20%20'LABEL'%2C%0A%20%20%20%20'MARK'%2C%0A%20%20%20%20'MATH'%2C%0A%20%20%20%20'METER'%2C%0A%20%20%20%20'NOSCRIPT'%2C%0A%20%20%20%20'OBJECT'%2C%0A%20%20%20%20'OUTPUT'%2C%0A%20%20%20%20'PROGRESS'%2C%0A%20%20%20%20'Q'%2C%0A%20%20%20%20'RUBY'%2C%0A%20%20%20%20'SAMP'%2C%0A%20%20%20%20'SCRIPT'%2C%0A%20%20%20%20'SELECT'%2C%0A%20%20%20%20'SMALL'%2C%0A%20%20%20%20'SPAN'%2C%0A%20%20%20%20'STRONG'%2C%0A%20%20%20%20'SUB'%2C%0A%20%20%20%20'SUP'%2C%0A%20%20%20%20'TEXTAREA'%2C%0A%20%20%20%20'TIME'%2C%0A%20%20%20%20'VAR'%2C%0A%20%20%20%20'WBR'%2C%0A%20%20%5D%2C%0A%0A%20%20%2F%2F%20These%20are%20the%20classes%20that%20readability%20sets%20itself.%0A%20%20CLASSES_TO_PRESERVE%3A%20%5B'page'%5D%2C%0A%0A%20%20%2F%2F%20These%20are%20the%20list%20of%20HTML%20entities%20that%20need%20to%20be%20escaped.%0A%20%20HTML_ESCAPE_MAP%3A%20%7B%0A%20%20%20%20lt%3A%20'%3C'%2C%0A%20%20%20%20gt%3A%20'%3E'%2C%0A%20%20%20%20amp%3A%20'%26'%2C%0A%20%20%20%20quot%3A%20'%22'%2C%0A%20%20%20%20apos%3A%20%22'%22%2C%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Run%20any%20post-process%20modifications%20to%20article%20content%20as%20necessary.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%20%40return%20void%0A%20%20%20**%2F%0A%20%20_postProcessContent(articleContent)%20%7B%0A%20%20%20%20%2F%2F%20Readability%20cannot%20open%20relative%20uris%20so%20we%20convert%20them%20to%20absolute%20uris.%0A%20%20%20%20this._fixRelativeUris(articleContent)%3B%0A%0A%20%20%20%20if%20(!this._keepClasses)%20%7B%0A%20%20%20%20%20%20%2F%2F%20Remove%20classes.%0A%20%20%20%20%20%20this._cleanClasses(articleContent)%3B%0A%20%20%20%20%7D%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Iterates%20over%20a%20NodeList%2C%20calls%20%60filterFn%60%20for%20each%20node%20and%20removes%20node%0A%20%20%20*%20if%20function%20returned%20%60true%60.%0A%20%20%20*%0A%20%20%20*%20If%20function%20is%20not%20passed%2C%20removes%20all%20the%20nodes%20in%20node%20list.%0A%20%20%20*%0A%20%20%20*%20%40param%20NodeList%20nodeList%20The%20nodes%20to%20operate%20on%0A%20%20%20*%20%40param%20Function%20filterFn%20the%20function%20to%20use%20as%20a%20filter%0A%20%20%20*%20%40return%20void%0A%20%20%20*%2F%0A%20%20_removeNodes(nodeList%2C%20filterFn)%20%7B%0A%20%20%20%20%2F%2F%20Avoid%20ever%20operating%20on%20live%20node%20lists.%0A%20%20%20%20if%20(this._docJSDOMParser%20%26%26%20nodeList._isLiveNodeList)%20%7B%0A%20%20%20%20%20%20throw%20new%20Error('Do%20not%20pass%20live%20node%20lists%20to%20_removeNodes')%3B%0A%20%20%20%20%7D%0A%20%20%20%20for%20(let%20i%20%3D%20nodeList.length%20-%201%3B%20i%20%3E%3D%200%3B%20i--)%20%7B%0A%20%20%20%20%20%20const%20node%20%3D%20nodeList%5Bi%5D%3B%0A%20%20%20%20%20%20const%20parentNode%20%3D%20node.parentNode%3B%0A%20%20%20%20%20%20if%20(parentNode)%20%7B%0A%20%20%20%20%20%20%20%20if%20(!filterFn%20%7C%7C%20filterFn.call(this%2C%20node%2C%20i%2C%20nodeList))%20%7B%0A%20%20%20%20%20%20%20%20%20%20parentNode.removeChild(node)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Iterates%20over%20a%20NodeList%2C%20and%20calls%20_setNodeTag%20for%20each%20node.%0A%20%20%20*%0A%20%20%20*%20%40param%20NodeList%20nodeList%20The%20nodes%20to%20operate%20on%0A%20%20%20*%20%40param%20String%20newTagName%20the%20new%20tag%20name%20to%20use%0A%20%20%20*%20%40return%20void%0A%20%20%20*%2F%0A%20%20_replaceNodeTags(nodeList%2C%20newTagName)%20%7B%0A%20%20%20%20%2F%2F%20Avoid%20ever%20operating%20on%20live%20node%20lists.%0A%20%20%20%20if%20(this._docJSDOMParser%20%26%26%20nodeList._isLiveNodeList)%20%7B%0A%20%20%20%20%20%20throw%20new%20Error('Do%20not%20pass%20live%20node%20lists%20to%20_replaceNodeTags')%3B%0A%20%20%20%20%7D%0A%20%20%20%20for%20(let%20i%20%3D%20nodeList.length%20-%201%3B%20i%20%3E%3D%200%3B%20i--)%20%7B%0A%20%20%20%20%20%20const%20node%20%3D%20nodeList%5Bi%5D%3B%0A%20%20%20%20%20%20this._setNodeTag(node%2C%20newTagName)%3B%0A%20%20%20%20%7D%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Iterate%20over%20a%20NodeList%2C%20which%20doesn't%20natively%20fully%20implement%20the%20Array%0A%20%20%20*%20interface.%0A%20%20%20*%0A%20%20%20*%20For%20convenience%2C%20the%20current%20object%20context%20is%20applied%20to%20the%20provided%0A%20%20%20*%20iterate%20function.%0A%20%20%20*%0A%20%20%20*%20%40param%20%20NodeList%20nodeList%20The%20NodeList.%0A%20%20%20*%20%40param%20%20Function%20fn%20%20%20%20%20%20%20The%20iterate%20function.%0A%20%20%20*%20%40return%20void%0A%20%20%20*%2F%0A%20%20_forEachNode(nodeList%2C%20fn)%20%7B%0A%20%20%20%20Array.prototype.forEach.call(nodeList%2C%20fn%2C%20this)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Iterate%20over%20a%20NodeList%2C%20return%20true%20if%20any%20of%20the%20provided%20iterate%0A%20%20%20*%20function%20calls%20returns%20true%2C%20false%20otherwise.%0A%20%20%20*%0A%20%20%20*%20For%20convenience%2C%20the%20current%20object%20context%20is%20applied%20to%20the%0A%20%20%20*%20provided%20iterate%20function.%0A%20%20%20*%0A%20%20%20*%20%40param%20%20NodeList%20nodeList%20The%20NodeList.%0A%20%20%20*%20%40param%20%20Function%20fn%20%20%20%20%20%20%20The%20iterate%20function.%0A%20%20%20*%20%40return%20Boolean%0A%20%20%20*%2F%0A%20%20_someNode(nodeList%2C%20fn)%20%7B%0A%20%20%20%20return%20Array.prototype.some.call(nodeList%2C%20fn%2C%20this)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Iterate%20over%20a%20NodeList%2C%20return%20true%20if%20all%20of%20the%20provided%20iterate%0A%20%20%20*%20function%20calls%20return%20true%2C%20false%20otherwise.%0A%20%20%20*%0A%20%20%20*%20For%20convenience%2C%20the%20current%20object%20context%20is%20applied%20to%20the%0A%20%20%20*%20provided%20iterate%20function.%0A%20%20%20*%0A%20%20%20*%20%40param%20%20NodeList%20nodeList%20The%20NodeList.%0A%20%20%20*%20%40param%20%20Function%20fn%20%20%20%20%20%20%20The%20iterate%20function.%0A%20%20%20*%20%40return%20Boolean%0A%20%20%20*%2F%0A%20%20_everyNode(nodeList%2C%20fn)%20%7B%0A%20%20%20%20return%20Array.prototype.every.call(nodeList%2C%20fn%2C%20this)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Concat%20all%20nodelists%20passed%20as%20arguments.%0A%20%20%20*%0A%20%20%20*%20%40return%20...NodeList%0A%20%20%20*%20%40return%20Array%0A%20%20%20*%2F%0A%20%20_concatNodeLists()%20%7B%0A%20%20%20%20const%20slice%20%3D%20Array.prototype.slice%3B%0A%20%20%20%20const%20args%20%3D%20slice.call(arguments)%3B%0A%20%20%20%20const%20nodeLists%20%3D%20args.map((list)%20%3D%3E%20slice.call(list))%3B%0A%20%20%20%20return%20Array.prototype.concat.apply(%5B%5D%2C%20nodeLists)%3B%0A%20%20%7D%2C%0A%0A%20%20_getAllNodesWithTag(node%2C%20tagNames)%20%7B%0A%20%20%20%20if%20(node.querySelectorAll)%20%7B%0A%20%20%20%20%20%20return%20node.querySelectorAll(tagNames.join('%2C'))%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20%5B%5D.concat.apply(%0A%20%20%20%20%20%20%5B%5D%2C%0A%20%20%20%20%20%20tagNames.map((tag)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20collection%20%3D%20node.getElementsByTagName(tag)%3B%0A%20%20%20%20%20%20%20%20return%20Array.isArray(collection)%20%3F%20collection%20%3A%20Array.from(collection)%3B%0A%20%20%20%20%20%20%7D)%2C%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Removes%20the%20class%3D%22%22%20attribute%20from%20every%20element%20in%20the%20given%0A%20%20%20*%20subtree%2C%20except%20those%20that%20match%20CLASSES_TO_PRESERVE%20and%0A%20%20%20*%20the%20classesToPreserve%20array%20from%20the%20options%20object.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%20%40return%20void%0A%20%20%20*%2F%0A%20%20_cleanClasses(node)%20%7B%0A%20%20%20%20const%20classesToPreserve%20%3D%20this._classesToPreserve%3B%0A%20%20%20%20const%20className%20%3D%20(node.getAttribute('class')%20%7C%7C%20'')%0A%20%20%20%20%20%20.split(%2F%5Cs%2B%2F)%0A%20%20%20%20%20%20.filter((cls)%20%3D%3E%20classesToPreserve.indexOf(cls)%20!%3D%20-1)%0A%20%20%20%20%20%20.join('%20')%3B%0A%0A%20%20%20%20if%20(className)%20%7B%0A%20%20%20%20%20%20node.setAttribute('class'%2C%20className)%3B%0A%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20node.removeAttribute('class')%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20for%20(node%20%3D%20node.firstElementChild%3B%20node%3B%20node%20%3D%20node.nextElementSibling)%20%7B%0A%20%20%20%20%20%20this._cleanClasses(node)%3B%0A%20%20%20%20%7D%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Converts%20each%20%3Ca%3E%20and%20%3Cimg%3E%20uri%20in%20the%20given%20element%20to%20an%20absolute%20URI%2C%0A%20%20%20*%20ignoring%20%23ref%20URIs.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%20%40return%20void%0A%20%20%20*%2F%0A%20%20_fixRelativeUris(articleContent)%20%7B%0A%20%20%20%20const%20baseURI%20%3D%20this._doc.baseURI%3B%0A%20%20%20%20const%20documentURI%20%3D%20this._doc.documentURI%3B%0A%20%20%20%20function%20toAbsoluteURI(uri)%20%7B%0A%20%20%20%20%20%20%2F%2F%20Leave%20hash%20links%20alone%20if%20the%20base%20URI%20matches%20the%20document%20URI%3A%0A%20%20%20%20%20%20if%20(baseURI%20%3D%3D%20documentURI%20%26%26%20uri.charAt(0)%20%3D%3D%20'%23')%20%7B%0A%20%20%20%20%20%20%20%20return%20uri%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%2F%2F%20Otherwise%2C%20resolve%20against%20base%20URI%3A%0A%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20return%20new%20URL(uri%2C%20baseURI).href%3B%0A%20%20%20%20%20%20%7D%20catch%20(ex)%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20Something%20went%20wrong%2C%20just%20return%20the%20original%3A%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20return%20uri%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20const%20links%20%3D%20this._getAllNodesWithTag(articleContent%2C%20%5B'a'%5D)%3B%0A%20%20%20%20this._forEachNode(links%2C%20function%20(link)%20%7B%0A%20%20%20%20%20%20const%20href%20%3D%20link.getAttribute('href')%3B%0A%20%20%20%20%20%20if%20(href)%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20Remove%20links%20with%20javascript%3A%20URIs%2C%20since%0A%20%20%20%20%20%20%20%20%2F%2F%20they%20won't%20work%20after%20scripts%20have%20been%20removed%20from%20the%20page.%0A%20%20%20%20%20%20%20%20if%20(href.indexOf('javascript%3A')%20%3D%3D%3D%200)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20if%20the%20link%20only%20contains%20simple%20text%20content%2C%20it%20can%20be%20converted%20to%20a%20text%20node%0A%20%20%20%20%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20link.childNodes.length%20%3D%3D%3D%201%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20link.childNodes%5B0%5D.nodeType%20%3D%3D%3D%20this.TEXT_NODE%0A%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20text%20%3D%20this._doc.createTextNode(link.textContent)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20link.parentNode.replaceChild(text%2C%20link)%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20if%20the%20link%20has%20multiple%20children%2C%20they%20should%20all%20be%20preserved%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20container%20%3D%20this._doc.createElement('span')%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20while%20(link.childNodes.length%20%3E%200)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20container.appendChild(link.childNodes%5B0%5D)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20link.parentNode.replaceChild(container%2C%20link)%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20%20%20link.setAttribute('href'%2C%20toAbsoluteURI(href))%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20const%20medias%20%3D%20this._getAllNodesWithTag(articleContent%2C%20%5B%0A%20%20%20%20%20%20'img'%2C%0A%20%20%20%20%20%20'picture'%2C%0A%20%20%20%20%20%20'figure'%2C%0A%20%20%20%20%20%20'video'%2C%0A%20%20%20%20%20%20'audio'%2C%0A%20%20%20%20%20%20'source'%2C%0A%20%20%20%20%5D)%3B%0A%0A%20%20%20%20this._forEachNode(medias%2C%20function%20(media)%20%7B%0A%20%20%20%20%20%20const%20src%20%3D%20media.getAttribute('src')%3B%0A%20%20%20%20%20%20const%20poster%20%3D%20media.getAttribute('poster')%3B%0A%20%20%20%20%20%20const%20srcset%20%3D%20media.getAttribute('srcset')%3B%0A%0A%20%20%20%20%20%20if%20(src)%20%7B%0A%20%20%20%20%20%20%20%20media.setAttribute('src'%2C%20toAbsoluteURI(src))%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20if%20(poster)%20%7B%0A%20%20%20%20%20%20%20%20media.setAttribute('poster'%2C%20toAbsoluteURI(poster))%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20if%20(srcset)%20%7B%0A%20%20%20%20%20%20%20%20const%20newSrcset%20%3D%20srcset.replace(%0A%20%20%20%20%20%20%20%20%20%20this.REGEXPS.srcsetUrl%2C%0A%20%20%20%20%20%20%20%20%20%20(_%2C%20p1%2C%20p2%2C%20p3)%20%3D%3E%20toAbsoluteURI(p1)%20%2B%20(p2%20%7C%7C%20'')%20%2B%20p3%2C%0A%20%20%20%20%20%20%20%20)%3B%0A%0A%20%20%20%20%20%20%20%20media.setAttribute('srcset'%2C%20newSrcset)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Get%20the%20article%20title%20as%20an%20H1.%0A%20%20%20*%0A%20%20%20*%20%40return%20void%0A%20%20%20**%2F%0A%20%20_getArticleTitle()%20%7B%0A%20%20%20%20const%20doc%20%3D%20this._doc%3B%0A%20%20%20%20let%20curTitle%20%3D%20''%3B%0A%20%20%20%20let%20origTitle%20%3D%20''%3B%0A%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20curTitle%20%3D%20origTitle%20%3D%20doc.title.trim()%3B%0A%0A%20%20%20%20%20%20%2F%2F%20If%20they%20had%20an%20element%20with%20id%20%22title%22%20in%20their%20HTML%0A%20%20%20%20%20%20if%20(typeof%20curTitle%20!%3D%3D%20'string')%20%7B%0A%20%20%20%20%20%20%20%20curTitle%20%3D%20origTitle%20%3D%20this._getInnerText(%0A%20%20%20%20%20%20%20%20%20%20doc.getElementsByTagName('title')%5B0%5D%2C%0A%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%2F*%20ignore%20exceptions%20setting%20the%20title.%20*%2F%0A%20%20%20%20%7D%0A%0A%20%20%20%20let%20titleHadHierarchicalSeparators%20%3D%20false%3B%0A%20%20%20%20function%20wordCount(str)%20%7B%0A%20%20%20%20%20%20return%20str.split(%2F%5Cs%2B%2F).length%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20If%20there's%20a%20separator%20in%20the%20title%2C%20first%20remove%20the%20final%20part%0A%20%20%20%20if%20(%2F%20%5B%5C%7C%5C-%5C%5C%5C%2F%3E%C2%BB%5D%20%2F.test(curTitle))%20%7B%0A%20%20%20%20%20%20titleHadHierarchicalSeparators%20%3D%20%2F%20%5B%5C%5C%5C%2F%3E%C2%BB%5D%20%2F.test(curTitle)%3B%0A%20%20%20%20%20%20curTitle%20%3D%20origTitle.replace(%2F(.*)%5B%5C%7C%5C-%5C%5C%5C%2F%3E%C2%BB%5D%20.*%2Fgi%2C%20'%241')%3B%0A%0A%20%20%20%20%20%20%2F%2F%20If%20the%20resulting%20title%20is%20too%20short%20(3%20words%20or%20fewer)%2C%20remove%0A%20%20%20%20%20%20%2F%2F%20the%20first%20part%20instead%3A%0A%20%20%20%20%20%20if%20(wordCount(curTitle)%20%3C%203)%20%7B%0A%20%20%20%20%20%20%20%20curTitle%20%3D%20origTitle.replace(%2F%5B%5E%5C%7C%5C-%5C%5C%5C%2F%3E%C2%BB%5D*%5B%5C%7C%5C-%5C%5C%5C%2F%3E%C2%BB%5D(.*)%2Fgi%2C%20'%241')%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%20else%20if%20(curTitle.indexOf('%3A%20')%20!%3D%3D%20-1)%20%7B%0A%20%20%20%20%20%20%2F%2F%20Check%20if%20we%20have%20an%20heading%20containing%20this%20exact%20string%2C%20so%20we%0A%20%20%20%20%20%20%2F%2F%20could%20assume%20it's%20the%20full%20title.%0A%20%20%20%20%20%20const%20headings%20%3D%20this._concatNodeLists(%0A%20%20%20%20%20%20%20%20doc.getElementsByTagName('h1')%2C%0A%20%20%20%20%20%20%20%20doc.getElementsByTagName('h2')%2C%0A%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20const%20trimmedTitle%20%3D%20curTitle.trim()%3B%0A%20%20%20%20%20%20const%20match%20%3D%20this._someNode(%0A%20%20%20%20%20%20%20%20headings%2C%0A%20%20%20%20%20%20%20%20(heading)%20%3D%3E%20heading.textContent.trim()%20%3D%3D%3D%20trimmedTitle%2C%0A%20%20%20%20%20%20)%3B%0A%0A%20%20%20%20%20%20%2F%2F%20If%20we%20don't%2C%20let's%20extract%20the%20title%20out%20of%20the%20original%20title%20string.%0A%20%20%20%20%20%20if%20(!match)%20%7B%0A%20%20%20%20%20%20%20%20curTitle%20%3D%20origTitle.substring(origTitle.lastIndexOf('%3A')%20%2B%201)%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20If%20the%20title%20is%20now%20too%20short%2C%20try%20the%20first%20colon%20instead%3A%0A%20%20%20%20%20%20%20%20if%20(wordCount(curTitle)%20%3C%203)%20%7B%0A%20%20%20%20%20%20%20%20%20%20curTitle%20%3D%20origTitle.substring(origTitle.indexOf('%3A')%20%2B%201)%3B%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20But%20if%20we%20have%20too%20many%20words%20before%20the%20colon%20there's%20something%20weird%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20with%20the%20titles%20and%20the%20H%20tags%20so%20let's%20just%20use%20the%20original%20title%20instead%0A%20%20%20%20%20%20%20%20%7D%20else%20if%20(wordCount(origTitle.substr(0%2C%20origTitle.indexOf('%3A')))%20%3E%205)%20%7B%0A%20%20%20%20%20%20%20%20%20%20curTitle%20%3D%20origTitle%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%20else%20if%20(curTitle.length%20%3E%20150%20%7C%7C%20curTitle.length%20%3C%2015)%20%7B%0A%20%20%20%20%20%20const%20hOnes%20%3D%20doc.getElementsByTagName('h1')%3B%0A%0A%20%20%20%20%20%20if%20(hOnes.length%20%3D%3D%3D%201)%20%7B%0A%20%20%20%20%20%20%20%20curTitle%20%3D%20this._getInnerText(hOnes%5B0%5D)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20curTitle%20%3D%20curTitle.trim().replace(this.REGEXPS.normalize%2C%20'%20')%3B%0A%20%20%20%20%2F%2F%20If%20we%20now%20have%204%20words%20or%20fewer%20as%20our%20title%2C%20and%20either%20no%0A%20%20%20%20%2F%2F%20'hierarchical'%20separators%20(%5C%2C%20%2F%2C%20%3E%20or%20%C2%BB)%20were%20found%20in%20the%20original%0A%20%20%20%20%2F%2F%20title%20or%20we%20decreased%20the%20number%20of%20words%20by%20more%20than%201%20word%2C%20use%0A%20%20%20%20%2F%2F%20the%20original%20title.%0A%20%20%20%20const%20curTitleWordCount%20%3D%20wordCount(curTitle)%3B%0A%20%20%20%20if%20(%0A%20%20%20%20%20%20curTitleWordCount%20%3C%3D%204%20%26%26%0A%20%20%20%20%20%20(!titleHadHierarchicalSeparators%20%7C%7C%0A%20%20%20%20%20%20%20%20curTitleWordCount%20!%3D%0A%20%20%20%20%20%20%20%20wordCount(origTitle.replace(%2F%5B%5C%7C%5C-%5C%5C%5C%2F%3E%C2%BB%5D%2B%2Fg%2C%20''))%20-%201)%0A%20%20%20%20)%20%7B%0A%20%20%20%20%20%20curTitle%20%3D%20origTitle%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20return%20curTitle%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Prepare%20the%20HTML%20document%20for%20readability%20to%20scrape%20it.%0A%20%20%20*%20This%20includes%20things%20like%20stripping%20javascript%2C%20CSS%2C%20and%20handling%20terrible%20markup.%0A%20%20%20*%0A%20%20%20*%20%40return%20void%0A%20%20%20**%2F%0A%20%20_prepDocument()%20%7B%0A%20%20%20%20const%20doc%20%3D%20this._doc%3B%0A%0A%20%20%20%20%2F%2F%20Remove%20all%20style%20tags%20in%20head%0A%20%20%20%20this._removeNodes(this._getAllNodesWithTag(doc%2C%20%5B'style'%5D))%3B%0A%0A%20%20%20%20if%20(doc.body)%20%7B%0A%20%20%20%20%20%20this._replaceBrs(doc.body)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20this._replaceNodeTags(this._getAllNodesWithTag(doc%2C%20%5B'font'%5D)%2C%20'SPAN')%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Finds%20the%20next%20element%2C%20starting%20from%20the%20given%20node%2C%20and%20ignoring%0A%20%20%20*%20whitespace%20in%20between.%20If%20the%20given%20node%20is%20an%20element%2C%20the%20same%20node%20is%0A%20%20%20*%20returned.%0A%20%20%20*%2F%0A%20%20_nextElement(node)%20%7B%0A%20%20%20%20let%20next%20%3D%20node%3B%0A%20%20%20%20while%20(%0A%20%20%20%20%20%20next%20%26%26%0A%20%20%20%20%20%20next.nodeType%20!%3D%20this.ELEMENT_NODE%20%26%26%0A%20%20%20%20%20%20this.REGEXPS.whitespace.test(next.textContent)%0A%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20next%20%3D%20next.nextSibling%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20next%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Replaces%202%20or%20more%20successive%20%3Cbr%3E%20elements%20with%20a%20single%20%3Cp%3E.%0A%20%20%20*%20Whitespace%20between%20%3Cbr%3E%20elements%20are%20ignored.%20For%20example%3A%0A%20%20%20*%20%20%20%3Cdiv%3Efoo%3Cbr%3Ebar%3Cbr%3E%20%3Cbr%3E%3Cbr%3Eabc%3C%2Fdiv%3E%0A%20%20%20*%20will%20become%3A%0A%20%20%20*%20%20%20%3Cdiv%3Efoo%3Cbr%3Ebar%3Cp%3Eabc%3C%2Fp%3E%3C%2Fdiv%3E%0A%20%20%20*%2F%0A%20%20_replaceBrs(elem)%20%7B%0A%20%20%20%20this._forEachNode(this._getAllNodesWithTag(elem%2C%20%5B'br'%5D)%2C%20function%20(br)%20%7B%0A%20%20%20%20%20%20let%20next%20%3D%20br.nextSibling%3B%0A%0A%20%20%20%20%20%20%2F%2F%20Whether%202%20or%20more%20%3Cbr%3E%20elements%20have%20been%20found%20and%20replaced%20with%20a%0A%20%20%20%20%20%20%2F%2F%20%3Cp%3E%20block.%0A%20%20%20%20%20%20let%20replaced%20%3D%20false%3B%0A%0A%20%20%20%20%20%20%2F%2F%20If%20we%20find%20a%20%3Cbr%3E%20chain%2C%20remove%20the%20%3Cbr%3Es%20until%20we%20hit%20another%20element%0A%20%20%20%20%20%20%2F%2F%20or%20non-whitespace.%20This%20leaves%20behind%20the%20first%20%3Cbr%3E%20in%20the%20chain%0A%20%20%20%20%20%20%2F%2F%20(which%20will%20be%20replaced%20with%20a%20%3Cp%3E%20later).%0A%20%20%20%20%20%20while%20((next%20%3D%20this._nextElement(next))%20%26%26%20next.tagName%20%3D%3D%20'BR')%20%7B%0A%20%20%20%20%20%20%20%20replaced%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20const%20brSibling%20%3D%20next.nextSibling%3B%0A%20%20%20%20%20%20%20%20next.parentNode.removeChild(next)%3B%0A%20%20%20%20%20%20%20%20next%20%3D%20brSibling%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%2F%2F%20If%20we%20removed%20a%20%3Cbr%3E%20chain%2C%20replace%20the%20remaining%20%3Cbr%3E%20with%20a%20%3Cp%3E.%20Add%0A%20%20%20%20%20%20%2F%2F%20all%20sibling%20nodes%20as%20children%20of%20the%20%3Cp%3E%20until%20we%20hit%20another%20%3Cbr%3E%0A%20%20%20%20%20%20%2F%2F%20chain.%0A%20%20%20%20%20%20if%20(replaced)%20%7B%0A%20%20%20%20%20%20%20%20const%20p%20%3D%20this._doc.createElement('p')%3B%0A%20%20%20%20%20%20%20%20br.parentNode.replaceChild(p%2C%20br)%3B%0A%0A%20%20%20%20%20%20%20%20next%20%3D%20p.nextSibling%3B%0A%20%20%20%20%20%20%20%20while%20(next)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20If%20we've%20hit%20another%20%3Cbr%3E%3Cbr%3E%2C%20we're%20done%20adding%20children%20to%20this%20%3Cp%3E.%0A%20%20%20%20%20%20%20%20%20%20if%20(next.tagName%20%3D%3D%20'BR')%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20nextElem%20%3D%20this._nextElement(next.nextSibling)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(nextElem%20%26%26%20nextElem.tagName%20%3D%3D%20'BR')%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20if%20(!this._isPhrasingContent(next))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20Otherwise%2C%20make%20this%20node%20a%20child%20of%20the%20new%20%3Cp%3E.%0A%20%20%20%20%20%20%20%20%20%20const%20sibling%20%3D%20next.nextSibling%3B%0A%20%20%20%20%20%20%20%20%20%20p.appendChild(next)%3B%0A%20%20%20%20%20%20%20%20%20%20next%20%3D%20sibling%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20while%20(p.lastChild%20%26%26%20this._isWhitespace(p.lastChild))%20%7B%0A%20%20%20%20%20%20%20%20%20%20p.removeChild(p.lastChild)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20if%20(p.parentNode.tagName%20%3D%3D%3D%20'P')%20%7B%0A%20%20%20%20%20%20%20%20%20%20this._setNodeTag(p.parentNode%2C%20'DIV')%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D)%3B%0A%20%20%7D%2C%0A%0A%20%20_setNodeTag(node%2C%20tag)%20%7B%0A%20%20%20%20this.log('_setNodeTag'%2C%20node%2C%20tag)%3B%0A%20%20%20%20if%20(this._docJSDOMParser)%20%7B%0A%20%20%20%20%20%20node.localName%20%3D%20tag.toLowerCase()%3B%0A%20%20%20%20%20%20node.tagName%20%3D%20tag.toUpperCase()%3B%0A%20%20%20%20%20%20return%20node%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20const%20replacement%20%3D%20node.ownerDocument.createElement(tag)%3B%0A%20%20%20%20while%20(node.firstChild)%20%7B%0A%20%20%20%20%20%20replacement.appendChild(node.firstChild)%3B%0A%20%20%20%20%7D%0A%20%20%20%20node.parentNode.replaceChild(replacement%2C%20node)%3B%0A%20%20%20%20if%20(node.readability)%20%7B%0A%20%20%20%20%20%20replacement.readability%20%3D%20node.readability%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20node.attributes.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20replacement.setAttribute(%0A%20%20%20%20%20%20%20%20%20%20node.attributes%5Bi%5D.name%2C%0A%20%20%20%20%20%20%20%20%20%20node.attributes%5Bi%5D.value%2C%0A%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%7D%20catch%20(ex)%20%7B%0A%20%20%20%20%20%20%20%20%2F*%20it's%20possible%20for%20setAttribute()%20to%20throw%20if%20the%20attribute%20name%0A%20%20%20%20%20%20%20%20%20*%20isn't%20a%20valid%20XML%20Name.%20Such%20attributes%20can%20however%20be%20parsed%20from%0A%20%20%20%20%20%20%20%20%20*%20source%20in%20HTML%20docs%2C%20see%20https%3A%2F%2Fgithub.com%2Fwhatwg%2Fhtml%2Fissues%2F4275%2C%0A%20%20%20%20%20%20%20%20%20*%20so%20we%20can%20hit%20them%20here%20and%20then%20throw.%20We%20don't%20care%20about%20such%0A%20%20%20%20%20%20%20%20%20*%20attributes%20so%20we%20ignore%20them.%0A%20%20%20%20%20%20%20%20%20*%2F%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20return%20replacement%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Prepare%20the%20article%20node%20for%20display.%20Clean%20out%20any%20inline%20styles%2C%0A%20%20%20*%20iframes%2C%20forms%2C%20strip%20extraneous%20%3Cp%3E%20tags%2C%20etc.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%20%40return%20void%0A%20%20%20**%2F%0A%20%20_prepArticle(articleContent)%20%7B%0A%20%20%20%20this._cleanStyles(articleContent)%3B%0A%0A%20%20%20%20%2F%2F%20Check%20for%20data%20tables%20before%20we%20continue%2C%20to%20avoid%20removing%20items%20in%0A%20%20%20%20%2F%2F%20those%20tables%2C%20which%20will%20often%20be%20isolated%20even%20though%20they're%0A%20%20%20%20%2F%2F%20visually%20linked%20to%20other%20content-ful%20elements%20(text%2C%20images%2C%20etc.).%0A%20%20%20%20this._markDataTables(articleContent)%3B%0A%0A%20%20%20%20this._fixLazyImages(articleContent)%3B%0A%0A%20%20%20%20%2F%2F%20Clean%20out%20junk%20from%20the%20article%20content%0A%20%20%20%20this._cleanConditionally(articleContent%2C%20'form')%3B%0A%20%20%20%20this._cleanConditionally(articleContent%2C%20'fieldset')%3B%0A%20%20%20%20this._clean(articleContent%2C%20'object')%3B%0A%20%20%20%20this._clean(articleContent%2C%20'embed')%3B%0A%20%20%20%20this._clean(articleContent%2C%20'h1')%3B%0A%20%20%20%20this._clean(articleContent%2C%20'footer')%3B%0A%20%20%20%20this._clean(articleContent%2C%20'link')%3B%0A%20%20%20%20this._clean(articleContent%2C%20'aside')%3B%0A%0A%20%20%20%20%2F%2F%20Clean%20out%20elements%20with%20little%20content%20that%20have%20%22share%22%20in%20their%20id%2Fclass%20combinations%20from%20final%20top%20candidates%2C%0A%20%20%20%20%2F%2F%20which%20means%20we%20don't%20remove%20the%20top%20candidates%20even%20they%20have%20%22share%22.%0A%0A%20%20%20%20const%20shareElementThreshold%20%3D%20this.DEFAULT_CHAR_THRESHOLD%3B%0A%0A%20%20%20%20this._forEachNode(articleContent.children%2C%20function%20(topCandidate)%20%7B%0A%20%20%20%20%20%20this._cleanMatchedNodes(topCandidate%2C%20function%20(node%2C%20matchString)%20%7B%0A%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20this.REGEXPS.shareElements.test(matchString)%20%26%26%0A%20%20%20%20%20%20%20%20%20%20node.textContent.length%20%3C%20shareElementThreshold%0A%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20%2F%2F%20If%20there%20is%20only%20one%20h2%20and%20its%20text%20content%20substantially%20equals%20article%20title%2C%0A%20%20%20%20%2F%2F%20they%20are%20probably%20using%20it%20as%20a%20header%20and%20not%20a%20subheader%2C%0A%20%20%20%20%2F%2F%20so%20remove%20it%20since%20we%20already%20extract%20the%20title%20separately.%0A%20%20%20%20const%20h2%20%3D%20articleContent.getElementsByTagName('h2')%3B%0A%20%20%20%20if%20(h2.length%20%3D%3D%3D%201)%20%7B%0A%20%20%20%20%20%20const%20lengthSimilarRate%20%3D%0A%20%20%20%20%20%20%20%20(h2%5B0%5D.textContent.length%20-%20this._articleTitle.length)%20%2F%0A%20%20%20%20%20%20%20%20this._articleTitle.length%3B%0A%20%20%20%20%20%20if%20(Math.abs(lengthSimilarRate)%20%3C%200.5)%20%7B%0A%20%20%20%20%20%20%20%20let%20titlesMatch%20%3D%20false%3B%0A%20%20%20%20%20%20%20%20if%20(lengthSimilarRate%20%3E%200)%20%7B%0A%20%20%20%20%20%20%20%20%20%20titlesMatch%20%3D%20h2%5B0%5D.textContent.includes(this._articleTitle)%3B%0A%20%20%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20%20%20titlesMatch%20%3D%20this._articleTitle.includes(h2%5B0%5D.textContent)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20if%20(titlesMatch)%20%7B%0A%20%20%20%20%20%20%20%20%20%20this._clean(articleContent%2C%20'h2')%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20this._clean(articleContent%2C%20'iframe')%3B%0A%20%20%20%20this._clean(articleContent%2C%20'input')%3B%0A%20%20%20%20this._clean(articleContent%2C%20'textarea')%3B%0A%20%20%20%20this._clean(articleContent%2C%20'select')%3B%0A%20%20%20%20this._clean(articleContent%2C%20'button')%3B%0A%20%20%20%20this._cleanHeaders(articleContent)%3B%0A%0A%20%20%20%20%2F%2F%20Do%20these%20last%20as%20the%20previous%20stuff%20may%20have%20removed%20junk%0A%20%20%20%20%2F%2F%20that%20will%20affect%20these%0A%20%20%20%20this._cleanConditionally(articleContent%2C%20'table')%3B%0A%20%20%20%20this._cleanConditionally(articleContent%2C%20'ul')%3B%0A%20%20%20%20this._cleanConditionally(articleContent%2C%20'div')%3B%0A%0A%20%20%20%20%2F%2F%20Remove%20extra%20paragraphs%0A%20%20%20%20this._removeNodes(%0A%20%20%20%20%20%20this._getAllNodesWithTag(articleContent%2C%20%5B'p'%5D)%2C%0A%20%20%20%20%20%20function%20(paragraph)%20%7B%0A%20%20%20%20%20%20%20%20const%20imgCount%20%3D%20paragraph.getElementsByTagName('img').length%3B%0A%20%20%20%20%20%20%20%20const%20embedCount%20%3D%20paragraph.getElementsByTagName('embed').length%3B%0A%20%20%20%20%20%20%20%20const%20objectCount%20%3D%20paragraph.getElementsByTagName('object').length%3B%0A%20%20%20%20%20%20%20%20%2F%2F%20At%20this%20point%2C%20nasty%20iframes%20have%20been%20removed%2C%20only%20remain%20embedded%20video%20ones.%0A%20%20%20%20%20%20%20%20const%20iframeCount%20%3D%20paragraph.getElementsByTagName('iframe').length%3B%0A%20%20%20%20%20%20%20%20const%20totalCount%20%3D%20imgCount%20%2B%20embedCount%20%2B%20objectCount%20%2B%20iframeCount%3B%0A%0A%20%20%20%20%20%20%20%20return%20totalCount%20%3D%3D%3D%200%20%26%26%20!this._getInnerText(paragraph%2C%20false)%3B%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20)%3B%0A%0A%20%20%20%20this._forEachNode(%0A%20%20%20%20%20%20this._getAllNodesWithTag(articleContent%2C%20%5B'br'%5D)%2C%0A%20%20%20%20%20%20function%20(br)%20%7B%0A%20%20%20%20%20%20%20%20const%20next%20%3D%20this._nextElement(br.nextSibling)%3B%0A%20%20%20%20%20%20%20%20if%20(next%20%26%26%20next.tagName%20%3D%3D%20'P')%20%7B%0A%20%20%20%20%20%20%20%20%20%20br.parentNode.removeChild(br)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20)%3B%0A%0A%20%20%20%20%2F%2F%20Remove%20single-cell%20tables%0A%20%20%20%20this._forEachNode(%0A%20%20%20%20%20%20this._getAllNodesWithTag(articleContent%2C%20%5B'table'%5D)%2C%0A%20%20%20%20%20%20function%20(table)%20%7B%0A%20%20%20%20%20%20%20%20const%20tbody%20%3D%20this._hasSingleTagInsideElement(table%2C%20'TBODY')%0A%20%20%20%20%20%20%20%20%20%20%3F%20table.firstElementChild%0A%20%20%20%20%20%20%20%20%20%20%3A%20table%3B%0A%20%20%20%20%20%20%20%20if%20(this._hasSingleTagInsideElement(tbody%2C%20'TR'))%20%7B%0A%20%20%20%20%20%20%20%20%20%20const%20row%20%3D%20tbody.firstElementChild%3B%0A%20%20%20%20%20%20%20%20%20%20if%20(this._hasSingleTagInsideElement(row%2C%20'TD'))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20let%20cell%20%3D%20row.firstElementChild%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20cell%20%3D%20this._setNodeTag(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20cell%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20this._everyNode(cell.childNodes%2C%20this._isPhrasingContent)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3F%20'P'%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3A%20'DIV'%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20table.parentNode.replaceChild(cell%2C%20table)%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Initialize%20a%20node%20with%20the%20readability%20object.%20Also%20checks%20the%0A%20%20%20*%20className%2Fid%20for%20special%20names%20to%20add%20to%20its%20score.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%20%40return%20void%0A%20%20%20**%2F%0A%20%20_initializeNode(node)%20%7B%0A%20%20%20%20node.readability%20%3D%20%7BcontentScore%3A%200%7D%3B%0A%0A%20%20%20%20switch%20(node.tagName)%20%7B%0A%20%20%20%20%20%20case%20'DIV'%3A%0A%20%20%20%20%20%20%20%20node.readability.contentScore%20%2B%3D%205%3B%0A%20%20%20%20%20%20%20%20break%3B%0A%0A%20%20%20%20%20%20case%20'PRE'%3A%0A%20%20%20%20%20%20case%20'TD'%3A%0A%20%20%20%20%20%20case%20'BLOCKQUOTE'%3A%0A%20%20%20%20%20%20%20%20node.readability.contentScore%20%2B%3D%203%3B%0A%20%20%20%20%20%20%20%20break%3B%0A%0A%20%20%20%20%20%20case%20'ADDRESS'%3A%0A%20%20%20%20%20%20case%20'OL'%3A%0A%20%20%20%20%20%20case%20'UL'%3A%0A%20%20%20%20%20%20case%20'DL'%3A%0A%20%20%20%20%20%20case%20'DD'%3A%0A%20%20%20%20%20%20case%20'DT'%3A%0A%20%20%20%20%20%20case%20'LI'%3A%0A%20%20%20%20%20%20case%20'FORM'%3A%0A%20%20%20%20%20%20%20%20node.readability.contentScore%20-%3D%203%3B%0A%20%20%20%20%20%20%20%20break%3B%0A%0A%20%20%20%20%20%20case%20'H1'%3A%0A%20%20%20%20%20%20case%20'H2'%3A%0A%20%20%20%20%20%20case%20'H3'%3A%0A%20%20%20%20%20%20case%20'H4'%3A%0A%20%20%20%20%20%20case%20'H5'%3A%0A%20%20%20%20%20%20case%20'H6'%3A%0A%20%20%20%20%20%20case%20'TH'%3A%0A%20%20%20%20%20%20%20%20node.readability.contentScore%20-%3D%205%3B%0A%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20node.readability.contentScore%20%2B%3D%20this._getClassWeight(node)%3B%0A%20%20%7D%2C%0A%0A%20%20_removeAndGetNext(node)%20%7B%0A%20%20%20%20const%20nextNode%20%3D%20this._getNextNode(node%2C%20true)%3B%0A%20%20%20%20node.parentNode.removeChild(node)%3B%0A%20%20%20%20return%20nextNode%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Traverse%20the%20DOM%20from%20node%20to%20node%2C%20starting%20at%20the%20node%20passed%20in.%0A%20%20%20*%20Pass%20true%20for%20the%20second%20parameter%20to%20indicate%20this%20node%20itself%0A%20%20%20*%20(and%20its%20kids)%20are%20going%20away%2C%20and%20we%20want%20the%20next%20node%20over.%0A%20%20%20*%0A%20%20%20*%20Calling%20this%20in%20a%20loop%20will%20traverse%20the%20DOM%20depth-first.%0A%20%20%20*%2F%0A%20%20_getNextNode(node%2C%20ignoreSelfAndKids)%20%7B%0A%20%20%20%20%2F%2F%20First%20check%20for%20kids%20if%20those%20aren't%20being%20ignored%0A%20%20%20%20if%20(!ignoreSelfAndKids%20%26%26%20node.firstElementChild)%20%7B%0A%20%20%20%20%20%20return%20node.firstElementChild%3B%0A%20%20%20%20%7D%0A%20%20%20%20%2F%2F%20Then%20for%20siblings...%0A%20%20%20%20if%20(node.nextElementSibling)%20%7B%0A%20%20%20%20%20%20return%20node.nextElementSibling%3B%0A%20%20%20%20%7D%0A%20%20%20%20%2F%2F%20And%20finally%2C%20move%20up%20the%20parent%20chain%20*and*%20find%20a%20sibling%0A%20%20%20%20%2F%2F%20(because%20this%20is%20depth-first%20traversal%2C%20we%20will%20have%20already%0A%20%20%20%20%2F%2F%20seen%20the%20parent%20nodes%20themselves).%0A%20%20%20%20do%20%7B%0A%20%20%20%20%20%20node%20%3D%20node.parentNode%3B%0A%20%20%20%20%7D%20while%20(node%20%26%26%20!node.nextElementSibling)%3B%0A%20%20%20%20return%20node%20%26%26%20node.nextElementSibling%3B%0A%20%20%7D%2C%0A%0A%20%20_checkByline(node%2C%20matchString)%20%7B%0A%20%20%20%20if%20(this._articleByline)%20%7B%0A%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if%20(node.getAttribute%20!%3D%3D%20undefined)%20%7B%0A%20%20%20%20%20%20var%20rel%20%3D%20node.getAttribute('rel')%3B%0A%20%20%20%20%20%20var%20itemprop%20%3D%20node.getAttribute('itemprop')%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if%20(%0A%20%20%20%20%20%20(rel%20%3D%3D%3D%20'author'%20%7C%7C%0A%20%20%20%20%20%20%20%20(itemprop%20%26%26%20itemprop.indexOf('author')%20!%3D%3D%20-1)%20%7C%7C%0A%20%20%20%20%20%20%20%20this.REGEXPS.byline.test(matchString))%20%26%26%0A%20%20%20%20%20%20this._isValidByline(node.textContent)%0A%20%20%20%20)%20%7B%0A%20%20%20%20%20%20this._articleByline%20%3D%20node.textContent.trim()%3B%0A%20%20%20%20%20%20return%20true%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20return%20false%3B%0A%20%20%7D%2C%0A%0A%20%20_getNodeAncestors(node%2C%20maxDepth)%20%7B%0A%20%20%20%20maxDepth%20%3D%20maxDepth%20%7C%7C%200%3B%0A%20%20%20%20let%20i%20%3D%200%2C%0A%20%20%20%20%20%20ancestors%20%3D%20%5B%5D%3B%0A%20%20%20%20while%20(node.parentNode)%20%7B%0A%20%20%20%20%20%20ancestors.push(node.parentNode)%3B%0A%20%20%20%20%20%20if%20(maxDepth%20%26%26%20%2B%2Bi%20%3D%3D%3D%20maxDepth)%20%7B%0A%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20node%20%3D%20node.parentNode%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20ancestors%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%20*%0A%20%20%20*%20grabArticle%20-%20Using%20a%20variety%20of%20metrics%20(content%20score%2C%20classname%2C%20element%20types)%2C%20find%20the%20content%20that%20is%0A%20%20%20*%20%20%20%20%20%20%20%20%20most%20likely%20to%20be%20the%20stuff%20a%20user%20wants%20to%20read.%20Then%20return%20it%20wrapped%20up%20in%20a%20div.%0A%20%20%20*%0A%20%20%20*%20%40param%20page%20a%20document%20to%20run%20upon.%20Needs%20to%20be%20a%20full%20document%2C%20complete%20with%20body.%0A%20%20%20*%20%40return%20Element%0A%20%20%20**%2F%0A%20%20_grabArticle(page)%20%7B%0A%20%20%20%20this.log('****%20grabArticle%20****')%3B%0A%20%20%20%20const%20doc%20%3D%20this._doc%3B%0A%20%20%20%20const%20isPaging%20%3D%20page%20!%3D%3D%20null%3B%0A%20%20%20%20page%20%3D%20page%20%3F%20page%20%3A%20this._doc.body%3B%0A%0A%20%20%20%20%2F%2F%20We%20can't%20grab%20an%20article%20if%20we%20don't%20have%20a%20page!%0A%20%20%20%20if%20(!page)%20%7B%0A%20%20%20%20%20%20this.log('No%20body%20found%20in%20document.%20Abort.')%3B%0A%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20const%20pageCacheHtml%20%3D%20page.innerHTML%3B%0A%0A%20%20%20%20while%20(true)%20%7B%0A%20%20%20%20%20%20const%20stripUnlikelyCandidates%20%3D%20this._flagIsActive(%0A%20%20%20%20%20%20%20%20this.FLAG_STRIP_UNLIKELYS%2C%0A%20%20%20%20%20%20)%3B%0A%0A%20%20%20%20%20%20%2F%2F%20First%2C%20node%20prepping.%20Trash%20nodes%20that%20look%20cruddy%20(like%20ones%20with%20the%0A%20%20%20%20%20%20%2F%2F%20class%20name%20%22comment%22%2C%20etc)%2C%20and%20turn%20divs%20into%20P%20tags%20where%20they%20have%20been%0A%20%20%20%20%20%20%2F%2F%20used%20inappropriately%20(as%20in%2C%20where%20they%20contain%20no%20other%20block%20level%20elements.)%0A%20%20%20%20%20%20const%20elementsToScore%20%3D%20%5B%5D%3B%0A%20%20%20%20%20%20let%20node%20%3D%20this._doc.documentElement%3B%0A%0A%20%20%20%20%20%20while%20(node)%20%7B%0A%20%20%20%20%20%20%20%20const%20matchString%20%3D%20node.className%20%2B%20'%20'%20%2B%20node.id%3B%0A%0A%20%20%20%20%20%20%20%20if%20(!this._isProbablyVisible(node))%20%7B%0A%20%20%20%20%20%20%20%20%20%20this.log('Removing%20hidden%20node%20-%20'%20%2B%20matchString)%3B%0A%20%20%20%20%20%20%20%20%20%20node%20%3D%20this._removeAndGetNext(node)%3B%0A%20%20%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Check%20to%20see%20if%20this%20node%20is%20a%20byline%2C%20and%20remove%20it%20if%20it%20is.%0A%20%20%20%20%20%20%20%20if%20(this._checkByline(node%2C%20matchString))%20%7B%0A%20%20%20%20%20%20%20%20%20%20node%20%3D%20this._removeAndGetNext(node)%3B%0A%20%20%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Remove%20unlikely%20candidates%0A%20%20%20%20%20%20%20%20if%20(stripUnlikelyCandidates)%20%7B%0A%20%20%20%20%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20this.REGEXPS.unlikelyCandidates.test(matchString)%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20!this.REGEXPS.okMaybeItsACandidate.test(matchString)%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20!this._hasAncestorTag(node%2C%20'table')%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20node.tagName%20!%3D%3D%20'BODY'%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20node.tagName%20!%3D%3D%20'A'%0A%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20this.log('Removing%20unlikely%20candidate%20-%20'%20%2B%20matchString)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20node%20%3D%20this._removeAndGetNext(node)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20if%20(node.getAttribute('role')%20%3D%3D%20'complementary')%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20this.log('Removing%20complementary%20content%20-%20'%20%2B%20matchString)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20node%20%3D%20this._removeAndGetNext(node)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Remove%20DIV%2C%20SECTION%2C%20and%20HEADER%20nodes%20without%20any%20content(e.g.%20text%2C%20image%2C%20video%2C%20or%20iframe).%0A%20%20%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20%20%20(node.tagName%20%3D%3D%3D%20'DIV'%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20%20%20node.tagName%20%3D%3D%3D%20'SECTION'%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20%20%20node.tagName%20%3D%3D%3D%20'HEADER'%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20%20%20node.tagName%20%3D%3D%3D%20'H1'%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20%20%20node.tagName%20%3D%3D%3D%20'H2'%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20%20%20node.tagName%20%3D%3D%3D%20'H3'%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20%20%20node.tagName%20%3D%3D%3D%20'H4'%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20%20%20node.tagName%20%3D%3D%3D%20'H5'%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20%20%20node.tagName%20%3D%3D%3D%20'H6')%20%26%26%0A%20%20%20%20%20%20%20%20%20%20this._isElementWithoutContent(node)%0A%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20node%20%3D%20this._removeAndGetNext(node)%3B%0A%20%20%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20if%20(this.DEFAULT_TAGS_TO_SCORE.indexOf(node.tagName)%20!%3D%3D%20-1)%20%7B%0A%20%20%20%20%20%20%20%20%20%20elementsToScore.push(node)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Turn%20all%20divs%20that%20don't%20have%20children%20block%20level%20elements%20into%20p's%0A%20%20%20%20%20%20%20%20if%20(node.tagName%20%3D%3D%3D%20'DIV')%20%7B%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20Put%20phrasing%20content%20into%20paragraphs.%0A%20%20%20%20%20%20%20%20%20%20let%20p%20%3D%20null%3B%0A%20%20%20%20%20%20%20%20%20%20let%20childNode%20%3D%20node.firstChild%3B%0A%20%20%20%20%20%20%20%20%20%20while%20(childNode)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20nextSibling%20%3D%20childNode.nextSibling%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(this._isPhrasingContent(childNode))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20(p%20!%3D%3D%20null)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20p.appendChild(childNode)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%20else%20if%20(!this._isWhitespace(childNode))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20p%20%3D%20doc.createElement('p')%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20node.replaceChild(p%2C%20childNode)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20p.appendChild(childNode)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20else%20if%20(p%20!%3D%3D%20null)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20while%20(p.lastChild%20%26%26%20this._isWhitespace(p.lastChild))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20p.removeChild(p.lastChild)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20p%20%3D%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20childNode%20%3D%20nextSibling%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20Sites%20like%20http%3A%2F%2Fmobile.slate.com%20encloses%20each%20paragraph%20with%20a%20DIV%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20element.%20DIVs%20with%20only%20a%20P%20element%20inside%20and%20no%20text%20content%20can%20be%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20safely%20converted%20into%20plain%20P%20elements%20to%20avoid%20confusing%20the%20scoring%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20algorithm%20with%20DIVs%20with%20are%2C%20in%20practice%2C%20paragraphs.%0A%20%20%20%20%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20this._hasSingleTagInsideElement(node%2C%20'P')%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20this._getLinkDensity(node)%20%3C%200.25%0A%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20newNode%20%3D%20node.children%5B0%5D%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20node.parentNode.replaceChild(newNode%2C%20node)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20node%20%3D%20newNode%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20elementsToScore.push(node)%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%20else%20if%20(!this._hasChildBlockElement(node))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20node%20%3D%20this._setNodeTag(node%2C%20'P')%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20elementsToScore.push(node)%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20node%20%3D%20this._getNextNode(node)%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%2F**%0A%20%20%20%20%20%20%20*%20Loop%20through%20all%20paragraphs%2C%20and%20assign%20a%20score%20to%20them%20based%20on%20how%20content-y%20they%20look.%0A%20%20%20%20%20%20%20*%20Then%20add%20their%20score%20to%20their%20parent%20node.%0A%20%20%20%20%20%20%20*%0A%20%20%20%20%20%20%20*%20A%20score%20is%20determined%20by%20things%20like%20number%20of%20commas%2C%20class%20names%2C%20etc.%20Maybe%20eventually%20link%20density.%0A%20%20%20%20%20%20%20**%2F%0A%20%20%20%20%20%20var%20candidates%20%3D%20%5B%5D%3B%0A%20%20%20%20%20%20this._forEachNode(elementsToScore%2C%20function%20(elementToScore)%20%7B%0A%20%20%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20%20%20!elementToScore.parentNode%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20typeof%20elementToScore.parentNode.tagName%20%3D%3D%3D%20'undefined'%0A%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20If%20this%20paragraph%20is%20less%20than%2025%20characters%2C%20don't%20even%20count%20it.%0A%20%20%20%20%20%20%20%20const%20innerText%20%3D%20this._getInnerText(elementToScore)%3B%0A%20%20%20%20%20%20%20%20if%20(innerText.length%20%3C%2025)%20%7B%0A%20%20%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Exclude%20nodes%20with%20no%20ancestor.%0A%20%20%20%20%20%20%20%20const%20ancestors%20%3D%20this._getNodeAncestors(elementToScore%2C%203)%3B%0A%20%20%20%20%20%20%20%20if%20(ancestors.length%20%3D%3D%3D%200)%20%7B%0A%20%20%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20let%20contentScore%20%3D%200%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Add%20a%20point%20for%20the%20paragraph%20itself%20as%20a%20base.%0A%20%20%20%20%20%20%20%20contentScore%20%2B%3D%201%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Add%20points%20for%20any%20commas%20within%20this%20paragraph.%0A%20%20%20%20%20%20%20%20contentScore%20%2B%3D%20innerText.split('%2C').length%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20For%20every%20100%20characters%20in%20this%20paragraph%2C%20add%20another%20point.%20Up%20to%203%20points.%0A%20%20%20%20%20%20%20%20contentScore%20%2B%3D%20Math.min(Math.floor(innerText.length%20%2F%20100)%2C%203)%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Initialize%20and%20score%20ancestors.%0A%20%20%20%20%20%20%20%20this._forEachNode(ancestors%2C%20function%20(ancestor%2C%20level)%20%7B%0A%20%20%20%20%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20!ancestor.tagName%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20%20%20!ancestor.parentNode%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20%20%20typeof%20ancestor.parentNode.tagName%20%3D%3D%3D%20'undefined'%0A%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20if%20(typeof%20ancestor.readability%20%3D%3D%3D%20'undefined')%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20this._initializeNode(ancestor)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20candidates.push(ancestor)%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20Node%20score%20divider%3A%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20-%20parent%3A%20%20%20%20%20%20%20%20%20%20%20%20%201%20(no%20division)%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20-%20grandparent%3A%20%20%20%20%20%20%20%202%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20-%20great%20grandparent%2B%3A%20ancestor%20level%20*%203%0A%20%20%20%20%20%20%20%20%20%20if%20(level%20%3D%3D%3D%200)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20var%20scoreDivider%20%3D%201%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%20else%20if%20(level%20%3D%3D%3D%201)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20scoreDivider%20%3D%202%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20scoreDivider%20%3D%20level%20*%203%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20ancestor.readability.contentScore%20%2B%3D%20contentScore%20%2F%20scoreDivider%3B%0A%20%20%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20%7D)%3B%0A%0A%20%20%20%20%20%20%2F%2F%20After%20we've%20calculated%20scores%2C%20loop%20through%20all%20of%20the%20possible%0A%20%20%20%20%20%20%2F%2F%20candidate%20nodes%20we%20found%20and%20find%20the%20one%20with%20the%20highest%20score.%0A%20%20%20%20%20%20const%20topCandidates%20%3D%20%5B%5D%3B%0A%20%20%20%20%20%20for%20(let%20c%20%3D%200%2C%20cl%20%3D%20candidates.length%3B%20c%20%3C%20cl%3B%20c%20%2B%3D%201)%20%7B%0A%20%20%20%20%20%20%20%20const%20candidate%20%3D%20candidates%5Bc%5D%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Scale%20the%20final%20candidates%20score%20based%20on%20link%20density.%20Good%20content%0A%20%20%20%20%20%20%20%20%2F%2F%20should%20have%20a%20relatively%20small%20link%20density%20(5%25%20or%20less)%20and%20be%20mostly%0A%20%20%20%20%20%20%20%20%2F%2F%20unaffected%20by%20this%20operation.%0A%20%20%20%20%20%20%20%20const%20candidateScore%20%3D%0A%20%20%20%20%20%20%20%20%20%20candidate.readability.contentScore%20*%0A%20%20%20%20%20%20%20%20%20%20(1%20-%20this._getLinkDensity(candidate))%3B%0A%20%20%20%20%20%20%20%20candidate.readability.contentScore%20%3D%20candidateScore%3B%0A%0A%20%20%20%20%20%20%20%20this.log('Candidate%3A'%2C%20candidate%2C%20'with%20score%20'%20%2B%20candidateScore)%3B%0A%0A%20%20%20%20%20%20%20%20for%20(let%20t%20%3D%200%3B%20t%20%3C%20this._nbTopCandidates%3B%20t%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20const%20aTopCandidate%20%3D%20topCandidates%5Bt%5D%3B%0A%0A%20%20%20%20%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20!aTopCandidate%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20%20%20candidateScore%20%3E%20aTopCandidate.readability.contentScore%0A%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20topCandidates.splice(t%2C%200%2C%20candidate)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(topCandidates.length%20%3E%20this._nbTopCandidates)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20topCandidates.pop()%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20let%20topCandidate%20%3D%20topCandidates%5B0%5D%20%7C%7C%20null%3B%0A%20%20%20%20%20%20let%20neededToCreateTopCandidate%20%3D%20false%3B%0A%20%20%20%20%20%20var%20parentOfTopCandidate%3B%0A%0A%20%20%20%20%20%20%2F%2F%20If%20we%20still%20have%20no%20top%20candidate%2C%20just%20use%20the%20body%20as%20a%20last%20resort.%0A%20%20%20%20%20%20%2F%2F%20We%20also%20have%20to%20copy%20the%20body%20node%20so%20it%20is%20something%20we%20can%20modify.%0A%20%20%20%20%20%20if%20(topCandidate%20%3D%3D%3D%20null%20%7C%7C%20topCandidate.tagName%20%3D%3D%3D%20'BODY')%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20Move%20all%20of%20the%20page's%20children%20into%20topCandidate%0A%20%20%20%20%20%20%20%20topCandidate%20%3D%20doc.createElement('DIV')%3B%0A%20%20%20%20%20%20%20%20neededToCreateTopCandidate%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20%2F%2F%20Move%20everything%20(not%20just%20elements%2C%20also%20text%20nodes%20etc.)%20into%20the%20container%0A%20%20%20%20%20%20%20%20%2F%2F%20so%20we%20even%20include%20text%20directly%20in%20the%20body%3A%0A%20%20%20%20%20%20%20%20const%20kids%20%3D%20page.childNodes%3B%0A%20%20%20%20%20%20%20%20while%20(kids.length)%20%7B%0A%20%20%20%20%20%20%20%20%20%20this.log('Moving%20child%20out%3A'%2C%20kids%5B0%5D)%3B%0A%20%20%20%20%20%20%20%20%20%20topCandidate.appendChild(kids%5B0%5D)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20page.appendChild(topCandidate)%3B%0A%0A%20%20%20%20%20%20%20%20this._initializeNode(topCandidate)%3B%0A%20%20%20%20%20%20%7D%20else%20if%20(topCandidate)%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20Find%20a%20better%20top%20candidate%20node%20if%20it%20contains%20(at%20least%20three)%20nodes%20which%20belong%20to%20%60topCandidates%60%20array%0A%20%20%20%20%20%20%20%20%2F%2F%20and%20whose%20scores%20are%20quite%20closed%20with%20current%20%60topCandidate%60%20node.%0A%20%20%20%20%20%20%20%20const%20alternativeCandidateAncestors%20%3D%20%5B%5D%3B%0A%20%20%20%20%20%20%20%20for%20(let%20i%20%3D%201%3B%20i%20%3C%20topCandidates.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20topCandidates%5Bi%5D.readability.contentScore%20%2F%0A%20%20%20%20%20%20%20%20%20%20%20%20topCandidate.readability.contentScore%20%3E%3D%0A%20%20%20%20%20%20%20%20%20%20%20%200.75%0A%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20alternativeCandidateAncestors.push(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20this._getNodeAncestors(topCandidates%5Bi%5D)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20const%20MINIMUM_TOPCANDIDATES%20%3D%203%3B%0A%20%20%20%20%20%20%20%20if%20(alternativeCandidateAncestors.length%20%3E%3D%20MINIMUM_TOPCANDIDATES)%20%7B%0A%20%20%20%20%20%20%20%20%20%20parentOfTopCandidate%20%3D%20topCandidate.parentNode%3B%0A%20%20%20%20%20%20%20%20%20%20while%20(parentOfTopCandidate.tagName%20!%3D%3D%20'BODY')%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20let%20listsContainingThisAncestor%20%3D%200%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20let%20ancestorIndex%20%3D%200%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20ancestorIndex%20%3C%20alternativeCandidateAncestors.length%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20listsContainingThisAncestor%20%3C%20MINIMUM_TOPCANDIDATES%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20ancestorIndex%2B%2B%0A%20%20%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20listsContainingThisAncestor%20%2B%3D%20Number(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alternativeCandidateAncestors%5BancestorIndex%5D.includes(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20parentOfTopCandidate%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(listsContainingThisAncestor%20%3E%3D%20MINIMUM_TOPCANDIDATES)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20topCandidate%20%3D%20parentOfTopCandidate%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20parentOfTopCandidate%20%3D%20parentOfTopCandidate.parentNode%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20if%20(!topCandidate.readability)%20%7B%0A%20%20%20%20%20%20%20%20%20%20this._initializeNode(topCandidate)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Because%20of%20our%20bonus%20system%2C%20parents%20of%20candidates%20might%20have%20scores%0A%20%20%20%20%20%20%20%20%2F%2F%20themselves.%20They%20get%20half%20of%20the%20node.%20There%20won't%20be%20nodes%20with%20higher%0A%20%20%20%20%20%20%20%20%2F%2F%20scores%20than%20our%20topCandidate%2C%20but%20if%20we%20see%20the%20score%20going%20*up*%20in%20the%20first%0A%20%20%20%20%20%20%20%20%2F%2F%20few%20steps%20up%20the%20tree%2C%20that's%20a%20decent%20sign%20that%20there%20might%20be%20more%20content%0A%20%20%20%20%20%20%20%20%2F%2F%20lurking%20in%20other%20places%20that%20we%20want%20to%20unify%20in.%20The%20sibling%20stuff%0A%20%20%20%20%20%20%20%20%2F%2F%20below%20does%20some%20of%20that%20-%20but%20only%20if%20we've%20looked%20high%20enough%20up%20the%20DOM%0A%20%20%20%20%20%20%20%20%2F%2F%20tree.%0A%20%20%20%20%20%20%20%20parentOfTopCandidate%20%3D%20topCandidate.parentNode%3B%0A%20%20%20%20%20%20%20%20let%20lastScore%20%3D%20topCandidate.readability.contentScore%3B%0A%20%20%20%20%20%20%20%20%2F%2F%20The%20scores%20shouldn't%20get%20too%20low.%0A%20%20%20%20%20%20%20%20const%20scoreThreshold%20%3D%20lastScore%20%2F%203%3B%0A%20%20%20%20%20%20%20%20while%20(parentOfTopCandidate.tagName%20!%3D%3D%20'BODY')%20%7B%0A%20%20%20%20%20%20%20%20%20%20if%20(!parentOfTopCandidate.readability)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20parentOfTopCandidate%20%3D%20parentOfTopCandidate.parentNode%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20const%20parentScore%20%3D%20parentOfTopCandidate.readability.contentScore%3B%0A%20%20%20%20%20%20%20%20%20%20if%20(parentScore%20%3C%20scoreThreshold)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20if%20(parentScore%20%3E%20lastScore)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Alright!%20We%20found%20a%20better%20parent%20to%20use.%0A%20%20%20%20%20%20%20%20%20%20%20%20topCandidate%20%3D%20parentOfTopCandidate%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20lastScore%20%3D%20parentOfTopCandidate.readability.contentScore%3B%0A%20%20%20%20%20%20%20%20%20%20parentOfTopCandidate%20%3D%20parentOfTopCandidate.parentNode%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20If%20the%20top%20candidate%20is%20the%20only%20child%2C%20use%20parent%20instead.%20This%20will%20help%20sibling%0A%20%20%20%20%20%20%20%20%2F%2F%20joining%20logic%20when%20adjacent%20content%20is%20actually%20located%20in%20parent's%20sibling%20node.%0A%20%20%20%20%20%20%20%20parentOfTopCandidate%20%3D%20topCandidate.parentNode%3B%0A%20%20%20%20%20%20%20%20while%20(%0A%20%20%20%20%20%20%20%20%20%20parentOfTopCandidate.tagName%20!%3D%20'BODY'%20%26%26%0A%20%20%20%20%20%20%20%20%20%20parentOfTopCandidate.children.length%20%3D%3D%201%0A%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20topCandidate%20%3D%20parentOfTopCandidate%3B%0A%20%20%20%20%20%20%20%20%20%20parentOfTopCandidate%20%3D%20topCandidate.parentNode%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20if%20(!topCandidate.readability)%20%7B%0A%20%20%20%20%20%20%20%20%20%20this._initializeNode(topCandidate)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%2F%2F%20Now%20that%20we%20have%20the%20top%20candidate%2C%20look%20through%20its%20siblings%20for%20content%0A%20%20%20%20%20%20%2F%2F%20that%20might%20also%20be%20related.%20Things%20like%20preambles%2C%20content%20split%20by%20ads%0A%20%20%20%20%20%20%2F%2F%20that%20we%20removed%2C%20etc.%0A%20%20%20%20%20%20let%20articleContent%20%3D%20doc.createElement('DIV')%3B%0A%20%20%20%20%20%20if%20(isPaging)%20%7B%0A%20%20%20%20%20%20%20%20articleContent.id%20%3D%20'readability-content'%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20const%20siblingScoreThreshold%20%3D%20Math.max(%0A%20%20%20%20%20%20%20%2010%2C%0A%20%20%20%20%20%20%20%20topCandidate.readability.contentScore%20*%200.2%2C%0A%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%2F%2F%20Keep%20potential%20top%20candidate's%20parent%20node%20to%20try%20to%20get%20text%20direction%20of%20it%20later.%0A%20%20%20%20%20%20parentOfTopCandidate%20%3D%20topCandidate.parentNode%3B%0A%20%20%20%20%20%20const%20siblings%20%3D%20parentOfTopCandidate.children%3B%0A%0A%20%20%20%20%20%20for%20(let%20s%20%3D%200%2C%20sl%20%3D%20siblings.length%3B%20s%20%3C%20sl%3B%20s%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20let%20sibling%20%3D%20siblings%5Bs%5D%3B%0A%20%20%20%20%20%20%20%20let%20append%20%3D%20false%3B%0A%0A%20%20%20%20%20%20%20%20this.log(%0A%20%20%20%20%20%20%20%20%20%20'Looking%20at%20sibling%20node%3A'%2C%0A%20%20%20%20%20%20%20%20%20%20sibling%2C%0A%20%20%20%20%20%20%20%20%20%20sibling.readability%0A%20%20%20%20%20%20%20%20%20%20%20%20%3F%20'with%20score%20'%20%2B%20sibling.readability.contentScore%0A%20%20%20%20%20%20%20%20%20%20%20%20%3A%20''%2C%0A%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%20%20this.log(%0A%20%20%20%20%20%20%20%20%20%20'Sibling%20has%20score'%2C%0A%20%20%20%20%20%20%20%20%20%20sibling.readability%20%3F%20sibling.readability.contentScore%20%3A%20'Unknown'%2C%0A%20%20%20%20%20%20%20%20)%3B%0A%0A%20%20%20%20%20%20%20%20if%20(sibling%20%3D%3D%3D%20topCandidate)%20%7B%0A%20%20%20%20%20%20%20%20%20%20append%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20%20%20let%20contentBonus%20%3D%200%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20Give%20a%20bonus%20if%20sibling%20nodes%20and%20top%20candidates%20have%20the%20example%20same%20classname%0A%20%20%20%20%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20sibling.className%20%3D%3D%3D%20topCandidate.className%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20topCandidate.className%20!%3D%3D%20''%0A%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20contentBonus%20%2B%3D%20topCandidate.readability.contentScore%20*%200.2%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20sibling.readability%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20sibling.readability.contentScore%20%2B%20contentBonus%20%3E%3D%0A%20%20%20%20%20%20%20%20%20%20%20%20siblingScoreThreshold%0A%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20append%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%20else%20if%20(sibling.nodeName%20%3D%3D%3D%20'P')%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20linkDensity%20%3D%20this._getLinkDensity(sibling)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20nodeContent%20%3D%20this._getInnerText(sibling)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20nodeLength%20%3D%20nodeContent.length%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(nodeLength%20%3E%2080%20%26%26%20linkDensity%20%3C%200.25)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20append%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20else%20if%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20nodeLength%20%3C%2080%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20nodeLength%20%3E%200%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20linkDensity%20%3D%3D%3D%200%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20nodeContent.search(%2F%5C.(%20%7C%24)%2F)%20!%3D%3D%20-1%0A%20%20%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20append%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20if%20(append)%20%7B%0A%20%20%20%20%20%20%20%20%20%20this.log('Appending%20node%3A'%2C%20sibling)%3B%0A%0A%20%20%20%20%20%20%20%20%20%20if%20(this.ALTER_TO_DIV_EXCEPTIONS.indexOf(sibling.nodeName)%20%3D%3D%3D%20-1)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20We%20have%20a%20node%20that%20isn't%20a%20common%20block%20level%20element%2C%20like%20a%20form%20or%20td%20tag.%0A%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Turn%20it%20into%20a%20div%20so%20it%20doesn't%20get%20filtered%20out%20later%20by%20accident.%0A%20%20%20%20%20%20%20%20%20%20%20%20this.log('Altering%20sibling%3A'%2C%20sibling%2C%20'to%20div.')%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20sibling%20%3D%20this._setNodeTag(sibling%2C%20'DIV')%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20articleContent.appendChild(sibling)%3B%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20siblings%20is%20a%20reference%20to%20the%20children%20array%2C%20and%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20sibling%20is%20removed%20from%20the%20array%20when%20we%20call%20appendChild().%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20As%20a%20result%2C%20we%20must%20revisit%20this%20index%20since%20the%20nodes%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20have%20been%20shifted.%0A%20%20%20%20%20%20%20%20%20%20s%20-%3D%201%3B%0A%20%20%20%20%20%20%20%20%20%20sl%20-%3D%201%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20if%20(this._debug)%20%7B%0A%20%20%20%20%20%20%20%20this.log('Article%20content%20pre-prep%3A%20'%20%2B%20articleContent.innerHTML)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%2F%2F%20So%20we%20have%20all%20of%20the%20content%20that%20we%20need.%20Now%20we%20clean%20it%20up%20for%20presentation.%0A%20%20%20%20%20%20this._prepArticle(articleContent)%3B%0A%20%20%20%20%20%20if%20(this._debug)%20%7B%0A%20%20%20%20%20%20%20%20this.log('Article%20content%20post-prep%3A%20'%20%2B%20articleContent.innerHTML)%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20if%20(neededToCreateTopCandidate)%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20We%20already%20created%20a%20fake%20div%20thing%2C%20and%20there%20wouldn't%20have%20been%20any%20siblings%20left%0A%20%20%20%20%20%20%20%20%2F%2F%20for%20the%20previous%20loop%2C%20so%20there's%20no%20point%20trying%20to%20create%20a%20new%20div%2C%20and%20then%0A%20%20%20%20%20%20%20%20%2F%2F%20move%20all%20the%20children%20over.%20Just%20assign%20IDs%20and%20class%20names%20here.%20No%20need%20to%20append%0A%20%20%20%20%20%20%20%20%2F%2F%20because%20that%20already%20happened%20anyway.%0A%20%20%20%20%20%20%20%20topCandidate.id%20%3D%20'readability-page-1'%3B%0A%20%20%20%20%20%20%20%20topCandidate.className%20%3D%20'page'%3B%0A%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20const%20div%20%3D%20doc.createElement('DIV')%3B%0A%20%20%20%20%20%20%20%20div.id%20%3D%20'readability-page-1'%3B%0A%20%20%20%20%20%20%20%20div.className%20%3D%20'page'%3B%0A%20%20%20%20%20%20%20%20const%20children%20%3D%20articleContent.childNodes%3B%0A%20%20%20%20%20%20%20%20while%20(children.length)%20%7B%0A%20%20%20%20%20%20%20%20%20%20div.appendChild(children%5B0%5D)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20articleContent.appendChild(div)%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20if%20(this._debug)%20%7B%0A%20%20%20%20%20%20%20%20this.log('Article%20content%20after%20paging%3A%20'%20%2B%20articleContent.innerHTML)%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20let%20parseSuccessful%20%3D%20true%3B%0A%0A%20%20%20%20%20%20%2F%2F%20Now%20that%20we've%20gone%20through%20the%20full%20algorithm%2C%20check%20to%20see%20if%0A%20%20%20%20%20%20%2F%2F%20we%20got%20any%20meaningful%20content.%20If%20we%20didn't%2C%20we%20may%20need%20to%20re-run%0A%20%20%20%20%20%20%2F%2F%20grabArticle%20with%20different%20flags%20set.%20This%20gives%20us%20a%20higher%20likelihood%20of%0A%20%20%20%20%20%20%2F%2F%20finding%20the%20content%2C%20and%20the%20sieve%20approach%20gives%20us%20a%20higher%20likelihood%20of%0A%20%20%20%20%20%20%2F%2F%20finding%20the%20-right-%20content.%0A%20%20%20%20%20%20const%20textLength%20%3D%20this._getInnerText(articleContent%2C%20true).length%3B%0A%20%20%20%20%20%20if%20(textLength%20%3C%20this._charThreshold)%20%7B%0A%20%20%20%20%20%20%20%20parseSuccessful%20%3D%20false%3B%0A%20%20%20%20%20%20%20%20page.innerHTML%20%3D%20pageCacheHtml%3B%0A%0A%20%20%20%20%20%20%20%20if%20(this._flagIsActive(this.FLAG_STRIP_UNLIKELYS))%20%7B%0A%20%20%20%20%20%20%20%20%20%20this._removeFlag(this.FLAG_STRIP_UNLIKELYS)%3B%0A%20%20%20%20%20%20%20%20%20%20this._attempts.push(%7BarticleContent%2C%20textLength%7D)%3B%0A%20%20%20%20%20%20%20%20%7D%20else%20if%20(this._flagIsActive(this.FLAG_WEIGHT_CLASSES))%20%7B%0A%20%20%20%20%20%20%20%20%20%20this._removeFlag(this.FLAG_WEIGHT_CLASSES)%3B%0A%20%20%20%20%20%20%20%20%20%20this._attempts.push(%7BarticleContent%2C%20textLength%7D)%3B%0A%20%20%20%20%20%20%20%20%7D%20else%20if%20(this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY))%20%7B%0A%20%20%20%20%20%20%20%20%20%20this._removeFlag(this.FLAG_CLEAN_CONDITIONALLY)%3B%0A%20%20%20%20%20%20%20%20%20%20this._attempts.push(%7BarticleContent%2C%20textLength%7D)%3B%0A%20%20%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20%20%20this._attempts.push(%7BarticleContent%2C%20textLength%7D)%3B%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20No%20luck%20after%20removing%20flags%2C%20just%20return%20the%20longest%20text%20we%20found%20during%20the%20different%20loops%0A%20%20%20%20%20%20%20%20%20%20this._attempts.sort((a%2C%20b)%20%3D%3E%20b.textLength%20-%20a.textLength)%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20But%20first%20check%20if%20we%20actually%20have%20something%0A%20%20%20%20%20%20%20%20%20%20if%20(!this._attempts%5B0%5D.textLength)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20articleContent%20%3D%20this._attempts%5B0%5D.articleContent%3B%0A%20%20%20%20%20%20%20%20%20%20parseSuccessful%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20if%20(parseSuccessful)%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20Find%20out%20text%20direction%20from%20ancestors%20of%20final%20top%20candidate.%0A%20%20%20%20%20%20%20%20const%20ancestors%20%3D%20%5BparentOfTopCandidate%2C%20topCandidate%5D.concat(%0A%20%20%20%20%20%20%20%20%20%20this._getNodeAncestors(parentOfTopCandidate)%2C%0A%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%20%20this._someNode(ancestors%2C%20function%20(ancestor)%20%7B%0A%20%20%20%20%20%20%20%20%20%20if%20(!ancestor.tagName)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20const%20articleDir%20%3D%20ancestor.getAttribute('dir')%3B%0A%20%20%20%20%20%20%20%20%20%20if%20(articleDir)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20this._articleDir%20%3D%20articleDir%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20true%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20%20%20return%20articleContent%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Check%20whether%20the%20input%20string%20could%20be%20a%20byline.%0A%20%20%20*%20This%20verifies%20that%20the%20input%20is%20a%20string%2C%20and%20that%20the%20length%0A%20%20%20*%20is%20less%20than%20100%20chars.%0A%20%20%20*%0A%20%20%20*%20%40param%20possibleByline%20%7Bstring%7D%20-%20a%20string%20to%20check%20whether%20its%20a%20byline.%0A%20%20%20*%20%40return%20Boolean%20-%20whether%20the%20input%20string%20is%20a%20byline.%0A%20%20%20*%2F%0A%20%20_isValidByline(byline)%20%7B%0A%20%20%20%20if%20(typeof%20byline%20%3D%3D%3D%20'string'%20%7C%7C%20byline%20instanceof%20String)%20%7B%0A%20%20%20%20%20%20byline%20%3D%20byline.trim()%3B%0A%20%20%20%20%20%20return%20byline.length%20%3E%200%20%26%26%20byline.length%20%3C%20100%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20false%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Converts%20some%20of%20the%20common%20HTML%20entities%20in%20string%20to%20their%20corresponding%20characters.%0A%20%20%20*%0A%20%20%20*%20%40param%20str%20%7Bstring%7D%20-%20a%20string%20to%20unescape.%0A%20%20%20*%20%40return%20string%20without%20HTML%20entity.%0A%20%20%20*%2F%0A%20%20_unescapeHtmlEntities(str)%20%7B%0A%20%20%20%20if%20(!str)%20%7B%0A%20%20%20%20%20%20return%20str%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20const%20htmlEscapeMap%20%3D%20this.HTML_ESCAPE_MAP%3B%0A%20%20%20%20return%20str%0A%20%20%20%20%20%20.replace(%2F%26(quot%7Camp%7Capos%7Clt%7Cgt)%3B%2Fg%2C%20(_%2C%20tag)%20%3D%3E%20htmlEscapeMap%5Btag%5D)%0A%20%20%20%20%20%20.replace(%2F%26%23(%3F%3Ax(%5B0-9a-z%5D%7B1%2C4%7D)%7C(%5B0-9%5D%7B1%2C4%7D))%3B%2Fgi%2C%20(_%2C%20hex%2C%20numStr)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20num%20%3D%20parseInt(hex%20%7C%7C%20numStr%2C%20hex%20%3F%2016%20%3A%2010)%3B%0A%20%20%20%20%20%20%20%20return%20String.fromCharCode(num)%3B%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Attempts%20to%20get%20excerpt%20and%20byline%20metadata%20for%20the%20article.%0A%20%20%20*%0A%20%20%20*%20%40return%20Object%20with%20optional%20%22excerpt%22%20and%20%22byline%22%20properties%0A%20%20%20*%2F%0A%20%20_getArticleMetadata()%20%7B%0A%20%20%20%20const%20metadata%20%3D%20%7B%7D%3B%0A%20%20%20%20const%20values%20%3D%20%7B%7D%3B%0A%20%20%20%20const%20metaElements%20%3D%20this._doc.getElementsByTagName('meta')%3B%0A%0A%20%20%20%20%2F%2F%20property%20is%20a%20space-separated%20list%20of%20values%0A%20%20%20%20const%20propertyPattern%20%3D%0A%20%20%20%20%20%20%2F%5Cs*(dc%7Cdcterm%7Cog%7Ctwitter)%5Cs*%3A%5Cs*(author%7Ccreator%7Cdescription%7Ctitle%7Csite_name)%5Cs*%2Fgi%3B%0A%0A%20%20%20%20%2F%2F%20name%20is%20a%20single%20value%0A%20%20%20%20const%20namePattern%20%3D%0A%20%20%20%20%20%20%2F%5E%5Cs*(%3F%3A(dc%7Cdcterm%7Cog%7Ctwitter%7Cweibo%3A(article%7Cwebpage))%5Cs*%5B%5C.%3A%5D%5Cs*)%3F(author%7Ccreator%7Cdescription%7Ctitle%7Csite_name)%5Cs*%24%2Fi%3B%0A%0A%20%20%20%20%2F%2F%20Find%20description%20tags.%0A%20%20%20%20this._forEachNode(metaElements%2C%20(element)%20%3D%3E%20%7B%0A%20%20%20%20%20%20const%20elementName%20%3D%20element.getAttribute('name')%3B%0A%20%20%20%20%20%20const%20elementProperty%20%3D%20element.getAttribute('property')%3B%0A%20%20%20%20%20%20const%20content%20%3D%20element.getAttribute('content')%3B%0A%20%20%20%20%20%20if%20(!content)%20%7B%0A%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20let%20matches%20%3D%20null%3B%0A%20%20%20%20%20%20let%20name%20%3D%20null%3B%0A%0A%20%20%20%20%20%20if%20(elementProperty)%20%7B%0A%20%20%20%20%20%20%20%20matches%20%3D%20elementProperty.match(propertyPattern)%3B%0A%20%20%20%20%20%20%20%20if%20(matches)%20%7B%0A%20%20%20%20%20%20%20%20%20%20for%20(let%20i%20%3D%20matches.length%20-%201%3B%20i%20%3E%3D%200%3B%20i--)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20Convert%20to%20lowercase%2C%20and%20remove%20any%20whitespace%0A%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20so%20we%20can%20match%20below.%0A%20%20%20%20%20%20%20%20%20%20%20%20name%20%3D%20matches%5Bi%5D.toLowerCase().replace(%2F%5Cs%2Fg%2C%20'')%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20multiple%20authors%0A%20%20%20%20%20%20%20%20%20%20%20%20values%5Bname%5D%20%3D%20content.trim()%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20if%20(!matches%20%26%26%20elementName%20%26%26%20namePattern.test(elementName))%20%7B%0A%20%20%20%20%20%20%20%20name%20%3D%20elementName%3B%0A%20%20%20%20%20%20%20%20if%20(content)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20Convert%20to%20lowercase%2C%20remove%20any%20whitespace%2C%20and%20convert%20dots%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20to%20colons%20so%20we%20can%20match%20below.%0A%20%20%20%20%20%20%20%20%20%20name%20%3D%20name.toLowerCase().replace(%2F%5Cs%2Fg%2C%20'').replace(%2F%5C.%2Fg%2C%20'%3A')%3B%0A%20%20%20%20%20%20%20%20%20%20values%5Bname%5D%20%3D%20content.trim()%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20%2F%2F%20get%20title%0A%20%20%20%20metadata.title%20%3D%0A%20%20%20%20%20%20values%5B'dc%3Atitle'%5D%20%7C%7C%0A%20%20%20%20%20%20values%5B'dcterm%3Atitle'%5D%20%7C%7C%0A%20%20%20%20%20%20values%5B'og%3Atitle'%5D%20%7C%7C%0A%20%20%20%20%20%20values%5B'weibo%3Aarticle%3Atitle'%5D%20%7C%7C%0A%20%20%20%20%20%20values%5B'weibo%3Awebpage%3Atitle'%5D%20%7C%7C%0A%20%20%20%20%20%20values.title%20%7C%7C%0A%20%20%20%20%20%20values%5B'twitter%3Atitle'%5D%3B%0A%0A%20%20%20%20if%20(!metadata.title)%20%7B%0A%20%20%20%20%20%20metadata.title%20%3D%20this._getArticleTitle()%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20get%20author%0A%20%20%20%20metadata.byline%20%3D%0A%20%20%20%20%20%20values%5B'dc%3Acreator'%5D%20%7C%7C%20values%5B'dcterm%3Acreator'%5D%20%7C%7C%20values.author%3B%0A%0A%20%20%20%20%2F%2F%20get%20description%0A%20%20%20%20metadata.excerpt%20%3D%0A%20%20%20%20%20%20values%5B'dc%3Adescription'%5D%20%7C%7C%0A%20%20%20%20%20%20values%5B'dcterm%3Adescription'%5D%20%7C%7C%0A%20%20%20%20%20%20values%5B'og%3Adescription'%5D%20%7C%7C%0A%20%20%20%20%20%20values%5B'weibo%3Aarticle%3Adescription'%5D%20%7C%7C%0A%20%20%20%20%20%20values%5B'weibo%3Awebpage%3Adescription'%5D%20%7C%7C%0A%20%20%20%20%20%20values.description%20%7C%7C%0A%20%20%20%20%20%20values%5B'twitter%3Adescription'%5D%3B%0A%0A%20%20%20%20%2F%2F%20get%20site%20name%0A%20%20%20%20metadata.siteName%20%3D%20values%5B'og%3Asite_name'%5D%3B%0A%0A%20%20%20%20%2F%2F%20in%20many%20sites%20the%20meta%20value%20is%20escaped%20with%20HTML%20entities%2C%0A%20%20%20%20%2F%2F%20so%20here%20we%20need%20to%20unescape%20it%0A%20%20%20%20metadata.title%20%3D%20this._unescapeHtmlEntities(metadata.title)%3B%0A%20%20%20%20metadata.byline%20%3D%20this._unescapeHtmlEntities(metadata.byline)%3B%0A%20%20%20%20metadata.excerpt%20%3D%20this._unescapeHtmlEntities(metadata.excerpt)%3B%0A%20%20%20%20metadata.siteName%20%3D%20this._unescapeHtmlEntities(metadata.siteName)%3B%0A%0A%20%20%20%20return%20metadata%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Check%20if%20node%20is%20image%2C%20or%20if%20node%20contains%20exactly%20only%20one%20image%0A%20%20%20*%20whether%20as%20a%20direct%20child%20or%20as%20its%20descendants.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20**%2F%0A%20%20_isSingleImage(node)%20%7B%0A%20%20%20%20if%20(node.tagName%20%3D%3D%3D%20'IMG')%20%7B%0A%20%20%20%20%20%20return%20true%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if%20(node.children.length%20!%3D%3D%201%20%7C%7C%20node.textContent.trim()%20!%3D%3D%20'')%20%7B%0A%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20return%20this._isSingleImage(node.children%5B0%5D)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Find%20all%20%3Cnoscript%3E%20that%20are%20located%20after%20%3Cimg%3E%20nodes%2C%20and%20which%20contain%20only%20one%0A%20%20%20*%20%3Cimg%3E%20element.%20Replace%20the%20first%20image%20with%20the%20image%20from%20inside%20the%20%3Cnoscript%3E%20tag%2C%0A%20%20%20*%20and%20remove%20the%20%3Cnoscript%3E%20tag.%20This%20improves%20the%20quality%20of%20the%20images%20we%20use%20on%0A%20%20%20*%20some%20sites%20(e.g.%20Medium).%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20**%2F%0A%20%20_unwrapNoscriptImages(doc)%20%7B%0A%20%20%20%20%2F%2F%20Find%20img%20without%20source%20or%20attributes%20that%20might%20contains%20image%2C%20and%20remove%20it.%0A%20%20%20%20%2F%2F%20This%20is%20done%20to%20prevent%20a%20placeholder%20img%20is%20replaced%20by%20img%20from%20noscript%20in%20next%20step.%0A%20%20%20%20const%20imgs%20%3D%20Array.from(doc.getElementsByTagName('img'))%3B%0A%20%20%20%20this._forEachNode(imgs%2C%20(img)%20%3D%3E%20%7B%0A%20%20%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20img.attributes.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20const%20attr%20%3D%20img.attributes%5Bi%5D%3B%0A%20%20%20%20%20%20%20%20switch%20(attr.name)%20%7B%0A%20%20%20%20%20%20%20%20%20%20case%20'src'%3A%0A%20%20%20%20%20%20%20%20%20%20case%20'srcset'%3A%0A%20%20%20%20%20%20%20%20%20%20case%20'data-src'%3A%0A%20%20%20%20%20%20%20%20%20%20case%20'data-srcset'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20if%20(%2F%5C.(jpg%7Cjpeg%7Cpng%7Cwebp)%2Fi.test(attr.value))%20%7B%0A%20%20%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20img.parentNode.removeChild(img)%3B%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20%2F%2F%20Next%20find%20noscript%20and%20try%20to%20extract%20its%20image%0A%20%20%20%20const%20noscripts%20%3D%20Array.from(doc.getElementsByTagName('noscript'))%3B%0A%20%20%20%20this._forEachNode(noscripts%2C%20function%20(noscript)%20%7B%0A%20%20%20%20%20%20%2F%2F%20Parse%20content%20of%20noscript%20and%20make%20sure%20it%20only%20contains%20image%0A%20%20%20%20%20%20const%20tmp%20%3D%20doc.createElement('div')%3B%0A%20%20%20%20%20%20tmp.innerHTML%20%3D%20noscript.innerHTML%3B%0A%20%20%20%20%20%20if%20(!this._isSingleImage(tmp))%20%7B%0A%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%2F%2F%20If%20noscript%20has%20previous%20sibling%20and%20it%20only%20contains%20image%2C%0A%20%20%20%20%20%20%2F%2F%20replace%20it%20with%20noscript%20content.%20However%20we%20also%20keep%20old%0A%20%20%20%20%20%20%2F%2F%20attributes%20that%20might%20contains%20image.%0A%20%20%20%20%20%20const%20prevElement%20%3D%20noscript.previousElementSibling%3B%0A%20%20%20%20%20%20if%20(prevElement%20%26%26%20this._isSingleImage(prevElement))%20%7B%0A%20%20%20%20%20%20%20%20let%20prevImg%20%3D%20prevElement%3B%0A%20%20%20%20%20%20%20%20if%20(prevImg.tagName%20!%3D%3D%20'IMG')%20%7B%0A%20%20%20%20%20%20%20%20%20%20prevImg%20%3D%20prevElement.getElementsByTagName('img')%5B0%5D%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20const%20newImg%20%3D%20tmp.getElementsByTagName('img')%5B0%5D%3B%0A%20%20%20%20%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20prevImg.attributes.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20const%20attr%20%3D%20prevImg.attributes%5Bi%5D%3B%0A%20%20%20%20%20%20%20%20%20%20if%20(attr.value%20%3D%3D%3D%20'')%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20attr.name%20%3D%3D%3D%20'src'%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20%20%20attr.name%20%3D%3D%3D%20'srcset'%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20%20%20%2F%5C.(jpg%7Cjpeg%7Cpng%7Cwebp)%2Fi.test(attr.value)%0A%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(newImg.getAttribute(attr.name)%20%3D%3D%3D%20attr.value)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20let%20attrName%20%3D%20attr.name%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(newImg.hasAttribute(attrName))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20attrName%20%3D%20'data-old-'%20%2B%20attrName%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20newImg.setAttribute(attrName%2C%20attr.value)%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20noscript.parentNode.replaceChild(tmp.firstElementChild%2C%20prevElement)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Removes%20script%20tags%20from%20the%20document.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20**%2F%0A%20%20_removeScripts(doc)%20%7B%0A%20%20%20%20this._removeNodes(%0A%20%20%20%20%20%20this._getAllNodesWithTag(doc%2C%20%5B'script'%5D)%2C%0A%20%20%20%20%20%20(scriptNode)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20scriptNode.nodeValue%20%3D%20''%3B%0A%20%20%20%20%20%20%20%20scriptNode.removeAttribute('src')%3B%0A%20%20%20%20%20%20%20%20return%20true%3B%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20)%3B%0A%20%20%20%20this._removeNodes(this._getAllNodesWithTag(doc%2C%20%5B'noscript'%5D))%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Check%20if%20this%20node%20has%20only%20whitespace%20and%20a%20single%20element%20with%20given%20tag%0A%20%20%20*%20Returns%20false%20if%20the%20DIV%20node%20contains%20non-empty%20text%20nodes%0A%20%20%20*%20or%20if%20it%20contains%20no%20element%20with%20given%20tag%20or%20more%20than%201%20element.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%20%40param%20string%20tag%20of%20child%20element%0A%20%20%20**%2F%0A%20%20_hasSingleTagInsideElement(element%2C%20tag)%20%7B%0A%20%20%20%20%2F%2F%20There%20should%20be%20exactly%201%20element%20child%20with%20given%20tag%0A%20%20%20%20if%20(element.children.length%20!%3D%201%20%7C%7C%20element.children%5B0%5D.tagName%20!%3D%3D%20tag)%20%7B%0A%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20And%20there%20should%20be%20no%20text%20nodes%20with%20real%20content%0A%20%20%20%20return%20!this._someNode(element.childNodes%2C%20function%20(node)%20%7B%0A%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20node.nodeType%20%3D%3D%3D%20this.TEXT_NODE%20%26%26%0A%20%20%20%20%20%20%20%20this.REGEXPS.hasContent.test(node.textContent)%0A%20%20%20%20%20%20)%3B%0A%20%20%20%20%7D)%3B%0A%20%20%7D%2C%0A%0A%20%20_isElementWithoutContent(node)%20%7B%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20node.nodeType%20%3D%3D%3D%20this.ELEMENT_NODE%20%26%26%0A%20%20%20%20%20%20node.textContent.trim().length%20%3D%3D%200%20%26%26%0A%20%20%20%20%20%20(node.children.length%20%3D%3D%200%20%7C%7C%0A%20%20%20%20%20%20%20%20node.children.length%20%3D%3D%0A%20%20%20%20%20%20%20%20node.getElementsByTagName('br').length%20%2B%0A%20%20%20%20%20%20%20%20node.getElementsByTagName('hr').length)%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Determine%20whether%20element%20has%20any%20children%20block%20level%20elements.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%2F%0A%20%20_hasChildBlockElement(element)%20%7B%0A%20%20%20%20return%20this._someNode(element.childNodes%2C%20function%20(node)%20%7B%0A%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20this.DIV_TO_P_ELEMS.indexOf(node.tagName)%20!%3D%3D%20-1%20%7C%7C%0A%20%20%20%20%20%20%20%20this._hasChildBlockElement(node)%0A%20%20%20%20%20%20)%3B%0A%20%20%20%20%7D)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%20*%0A%20%20%20*%20Determine%20if%20a%20node%20qualifies%20as%20phrasing%20content.%0A%20%20%20*%20https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FGuide%2FHTML%2FContent_categories%23Phrasing_content%0A%20%20%20**%2F%0A%20%20_isPhrasingContent(node)%20%7B%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20node.nodeType%20%3D%3D%3D%20this.TEXT_NODE%20%7C%7C%0A%20%20%20%20%20%20this.PHRASING_ELEMS.indexOf(node.tagName)%20!%3D%3D%20-1%20%7C%7C%0A%20%20%20%20%20%20((node.tagName%20%3D%3D%3D%20'A'%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20node.tagName%20%3D%3D%3D%20'DEL'%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20node.tagName%20%3D%3D%3D%20'INS')%20%26%26%0A%20%20%20%20%20%20%20%20this._everyNode(node.childNodes%2C%20this._isPhrasingContent))%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%0A%20%20_isWhitespace(node)%20%7B%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20(node.nodeType%20%3D%3D%3D%20this.TEXT_NODE%20%26%26%0A%20%20%20%20%20%20%20%20node.textContent.trim().length%20%3D%3D%3D%200)%20%7C%7C%0A%20%20%20%20%20%20(node.nodeType%20%3D%3D%3D%20this.ELEMENT_NODE%20%26%26%20node.tagName%20%3D%3D%3D%20'BR')%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Get%20the%20inner%20text%20of%20a%20node%20-%20cross%20browser%20compatibly.%0A%20%20%20*%20This%20also%20strips%20out%20any%20excess%20whitespace%20to%20be%20found.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%20%40param%20Boolean%20normalizeSpaces%20(default%3A%20true)%0A%20%20%20*%20%40return%20string%0A%20%20%20**%2F%0A%20%20_getInnerText(e%2C%20normalizeSpaces)%20%7B%0A%20%20%20%20normalizeSpaces%20%3D%0A%20%20%20%20%20%20typeof%20normalizeSpaces%20%3D%3D%3D%20'undefined'%20%3F%20true%20%3A%20normalizeSpaces%3B%0A%20%20%20%20const%20textContent%20%3D%20e.textContent.trim()%3B%0A%0A%20%20%20%20if%20(normalizeSpaces)%20%7B%0A%20%20%20%20%20%20return%20textContent.replace(this.REGEXPS.normalize%2C%20'%20')%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20textContent%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Get%20the%20number%20of%20times%20a%20string%20s%20appears%20in%20the%20node%20e.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%20%40param%20string%20-%20what%20to%20split%20on.%20Default%20is%20%22%2C%22%0A%20%20%20*%20%40return%20number%20(integer)%0A%20%20%20**%2F%0A%20%20_getCharCount(e%2C%20s)%20%7B%0A%20%20%20%20s%20%3D%20s%20%7C%7C%20'%2C'%3B%0A%20%20%20%20return%20this._getInnerText(e).split(s).length%20-%201%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Remove%20the%20style%20attribute%20on%20every%20e%20and%20under.%0A%20%20%20*%20TODO%3A%20Test%20if%20getElementsByTagName(*)%20is%20faster.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%20%40return%20void%0A%20%20%20**%2F%0A%20%20_cleanStyles(e)%20%7B%0A%20%20%20%20if%20(!e%20%7C%7C%20e.tagName.toLowerCase()%20%3D%3D%3D%20'svg')%20%7B%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20Remove%20%60style%60%20and%20deprecated%20presentational%20attributes%0A%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20this.PRESENTATIONAL_ATTRIBUTES.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20e.removeAttribute(this.PRESENTATIONAL_ATTRIBUTES%5Bi%5D)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if%20(this.DEPRECATED_SIZE_ATTRIBUTE_ELEMS.indexOf(e.tagName)%20!%3D%3D%20-1)%20%7B%0A%20%20%20%20%20%20e.removeAttribute('width')%3B%0A%20%20%20%20%20%20e.removeAttribute('height')%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20let%20cur%20%3D%20e.firstElementChild%3B%0A%20%20%20%20while%20(cur%20!%3D%3D%20null)%20%7B%0A%20%20%20%20%20%20this._cleanStyles(cur)%3B%0A%20%20%20%20%20%20cur%20%3D%20cur.nextElementSibling%3B%0A%20%20%20%20%7D%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Get%20the%20density%20of%20links%20as%20a%20percentage%20of%20the%20content%0A%20%20%20*%20This%20is%20the%20amount%20of%20text%20that%20is%20inside%20a%20link%20divided%20by%20the%20total%20text%20in%20the%20node.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%20%40return%20number%20(float)%0A%20%20%20**%2F%0A%20%20_getLinkDensity(element)%20%7B%0A%20%20%20%20const%20textLength%20%3D%20this._getInnerText(element).length%3B%0A%20%20%20%20if%20(textLength%20%3D%3D%3D%200)%20%7B%0A%20%20%20%20%20%20return%200%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20let%20linkLength%20%3D%200%3B%0A%0A%20%20%20%20%2F%2F%20XXX%20implement%20_reduceNodeList%3F%0A%20%20%20%20this._forEachNode(element.getElementsByTagName('a')%2C%20function%20(linkNode)%20%7B%0A%20%20%20%20%20%20linkLength%20%2B%3D%20this._getInnerText(linkNode).length%3B%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20return%20linkLength%20%2F%20textLength%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Get%20an%20elements%20class%2Fid%20weight.%20Uses%20regular%20expressions%20to%20tell%20if%20this%0A%20%20%20*%20element%20looks%20good%20or%20bad.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%20%40return%20number%20(Integer)%0A%20%20%20**%2F%0A%20%20_getClassWeight(e)%20%7B%0A%20%20%20%20if%20(!this._flagIsActive(this.FLAG_WEIGHT_CLASSES))%20%7B%0A%20%20%20%20%20%20return%200%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20let%20weight%20%3D%200%3B%0A%0A%20%20%20%20%2F%2F%20Look%20for%20a%20special%20classname%0A%20%20%20%20if%20(typeof%20e.className%20%3D%3D%3D%20'string'%20%26%26%20e.className%20!%3D%3D%20'')%20%7B%0A%20%20%20%20%20%20if%20(this.REGEXPS.negative.test(e.className))%20%7B%0A%20%20%20%20%20%20%20%20weight%20-%3D%2025%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20if%20(this.REGEXPS.positive.test(e.className))%20%7B%0A%20%20%20%20%20%20%20%20weight%20%2B%3D%2025%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20Look%20for%20a%20special%20ID%0A%20%20%20%20if%20(typeof%20e.id%20%3D%3D%3D%20'string'%20%26%26%20e.id%20!%3D%3D%20'')%20%7B%0A%20%20%20%20%20%20if%20(this.REGEXPS.negative.test(e.id))%20%7B%0A%20%20%20%20%20%20%20%20weight%20-%3D%2025%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20if%20(this.REGEXPS.positive.test(e.id))%20%7B%0A%20%20%20%20%20%20%20%20weight%20%2B%3D%2025%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20return%20weight%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Clean%20a%20node%20of%20all%20elements%20of%20type%20%22tag%22.%0A%20%20%20*%20(Unless%20it's%20a%20youtube%2Fvimeo%20video.%20People%20love%20movies.)%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%20%40param%20string%20tag%20to%20clean%0A%20%20%20*%20%40return%20void%0A%20%20%20**%2F%0A%20%20_clean(e%2C%20tag)%20%7B%0A%20%20%20%20const%20isEmbed%20%3D%20%5B'object'%2C%20'embed'%2C%20'iframe'%5D.indexOf(tag)%20!%3D%3D%20-1%3B%0A%0A%20%20%20%20this._removeNodes(this._getAllNodesWithTag(e%2C%20%5Btag%5D)%2C%20function%20(element)%20%7B%0A%20%20%20%20%20%20%2F%2F%20Allow%20youtube%20and%20vimeo%20videos%20through%20as%20people%20usually%20want%20to%20see%20those.%0A%20%20%20%20%20%20if%20(isEmbed)%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20First%2C%20check%20the%20elements%20attributes%20to%20see%20if%20any%20of%20them%20contain%20youtube%20or%20vimeo%0A%20%20%20%20%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20element.attributes.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20if%20(this.REGEXPS.videos.test(element.attributes%5Bi%5D.value))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20For%20embed%20with%20%3Cobject%3E%20tag%2C%20check%20inner%20HTML%20as%20well.%0A%20%20%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20%20%20element.tagName%20%3D%3D%3D%20'object'%20%26%26%0A%20%20%20%20%20%20%20%20%20%20this.REGEXPS.videos.test(element.innerHTML)%0A%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20return%20true%3B%0A%20%20%20%20%7D)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Check%20if%20a%20given%20node%20has%20one%20of%20its%20ancestor%20tag%20name%20matching%20the%0A%20%20%20*%20provided%20one.%0A%20%20%20*%20%40param%20%20HTMLElement%20node%0A%20%20%20*%20%40param%20%20String%20%20%20%20%20%20tagName%0A%20%20%20*%20%40param%20%20Number%20%20%20%20%20%20maxDepth%0A%20%20%20*%20%40param%20%20Function%20%20%20%20filterFn%20a%20filter%20to%20invoke%20to%20determine%20whether%20this%20node%20'counts'%0A%20%20%20*%20%40return%20Boolean%0A%20%20%20*%2F%0A%20%20_hasAncestorTag(node%2C%20tagName%2C%20maxDepth%2C%20filterFn)%20%7B%0A%20%20%20%20maxDepth%20%3D%20maxDepth%20%7C%7C%203%3B%0A%20%20%20%20tagName%20%3D%20tagName.toUpperCase()%3B%0A%20%20%20%20let%20depth%20%3D%200%3B%0A%20%20%20%20while%20(node.parentNode)%20%7B%0A%20%20%20%20%20%20if%20(maxDepth%20%3E%200%20%26%26%20depth%20%3E%20maxDepth)%20%7B%0A%20%20%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20node.parentNode.tagName%20%3D%3D%3D%20tagName%20%26%26%0A%20%20%20%20%20%20%20%20(!filterFn%20%7C%7C%20filterFn(node.parentNode))%0A%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20return%20true%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20node%20%3D%20node.parentNode%3B%0A%20%20%20%20%20%20depth%2B%2B%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20false%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Return%20an%20object%20indicating%20how%20many%20rows%20and%20columns%20this%20table%20has.%0A%20%20%20*%2F%0A%20%20_getRowAndColumnCount(table)%20%7B%0A%20%20%20%20let%20rows%20%3D%200%3B%0A%20%20%20%20let%20columns%20%3D%200%3B%0A%20%20%20%20const%20trs%20%3D%20table.getElementsByTagName('tr')%3B%0A%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20trs.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20let%20rowspan%20%3D%20trs%5Bi%5D.getAttribute('rowspan')%20%7C%7C%200%3B%0A%20%20%20%20%20%20if%20(rowspan)%20%7B%0A%20%20%20%20%20%20%20%20rowspan%20%3D%20parseInt(rowspan%2C%2010)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20rows%20%2B%3D%20rowspan%20%7C%7C%201%3B%0A%0A%20%20%20%20%20%20%2F%2F%20Now%20look%20for%20column-related%20info%0A%20%20%20%20%20%20let%20columnsInThisRow%20%3D%200%3B%0A%20%20%20%20%20%20const%20cells%20%3D%20trs%5Bi%5D.getElementsByTagName('td')%3B%0A%20%20%20%20%20%20for%20(let%20j%20%3D%200%3B%20j%20%3C%20cells.length%3B%20j%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20let%20colspan%20%3D%20cells%5Bj%5D.getAttribute('colspan')%20%7C%7C%200%3B%0A%20%20%20%20%20%20%20%20if%20(colspan)%20%7B%0A%20%20%20%20%20%20%20%20%20%20colspan%20%3D%20parseInt(colspan%2C%2010)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20columnsInThisRow%20%2B%3D%20colspan%20%7C%7C%201%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20columns%20%3D%20Math.max(columns%2C%20columnsInThisRow)%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20%7Brows%2C%20columns%7D%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Look%20for%20'data'%20(as%20opposed%20to%20'layout')%20tables%2C%20for%20which%20we%20use%0A%20%20%20*%20similar%20checks%20as%0A%20%20%20*%20https%3A%2F%2Fdxr.mozilla.org%2Fmozilla-central%2Frev%2F71224049c0b52ab190564d3ea0eab089a159a4cf%2Faccessible%2Fhtml%2FHTMLTableAccessible.cpp%23920%0A%20%20%20*%2F%0A%20%20_markDataTables(root)%20%7B%0A%20%20%20%20const%20tables%20%3D%20root.getElementsByTagName('table')%3B%0A%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20tables.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20var%20table%20%3D%20tables%5Bi%5D%3B%0A%20%20%20%20%20%20const%20role%20%3D%20table.getAttribute('role')%3B%0A%20%20%20%20%20%20if%20(role%20%3D%3D%20'presentation')%20%7B%0A%20%20%20%20%20%20%20%20table._readabilityDataTable%20%3D%20false%3B%0A%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20const%20datatable%20%3D%20table.getAttribute('datatable')%3B%0A%20%20%20%20%20%20if%20(datatable%20%3D%3D%20'0')%20%7B%0A%20%20%20%20%20%20%20%20table._readabilityDataTable%20%3D%20false%3B%0A%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20const%20summary%20%3D%20table.getAttribute('summary')%3B%0A%20%20%20%20%20%20if%20(summary)%20%7B%0A%20%20%20%20%20%20%20%20table._readabilityDataTable%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20const%20caption%20%3D%20table.getElementsByTagName('caption')%5B0%5D%3B%0A%20%20%20%20%20%20if%20(caption%20%26%26%20caption.childNodes.length%20%3E%200)%20%7B%0A%20%20%20%20%20%20%20%20table._readabilityDataTable%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%2F%2F%20If%20the%20table%20has%20a%20descendant%20with%20any%20of%20these%20tags%2C%20consider%20a%20data%20table%3A%0A%20%20%20%20%20%20const%20dataTableDescendants%20%3D%20%5B'col'%2C%20'colgroup'%2C%20'tfoot'%2C%20'thead'%2C%20'th'%5D%3B%0A%20%20%20%20%20%20const%20descendantExists%20%3D%20function%20(tag)%20%7B%0A%20%20%20%20%20%20%20%20return%20!!table.getElementsByTagName(tag)%5B0%5D%3B%0A%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20if%20(dataTableDescendants.some(descendantExists))%20%7B%0A%20%20%20%20%20%20%20%20this.log('Data%20table%20because%20found%20data-y%20descendant')%3B%0A%20%20%20%20%20%20%20%20table._readabilityDataTable%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%2F%2F%20Nested%20tables%20indicate%20a%20layout%20table%3A%0A%20%20%20%20%20%20if%20(table.getElementsByTagName('table')%5B0%5D)%20%7B%0A%20%20%20%20%20%20%20%20table._readabilityDataTable%20%3D%20false%3B%0A%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20const%20sizeInfo%20%3D%20this._getRowAndColumnCount(table)%3B%0A%20%20%20%20%20%20if%20(sizeInfo.rows%20%3E%3D%2010%20%7C%7C%20sizeInfo.columns%20%3E%204)%20%7B%0A%20%20%20%20%20%20%20%20table._readabilityDataTable%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%2F%2F%20Now%20just%20go%20by%20size%20entirely%3A%0A%20%20%20%20%20%20table._readabilityDataTable%20%3D%20sizeInfo.rows%20*%20sizeInfo.columns%20%3E%2010%3B%0A%20%20%20%20%7D%0A%20%20%7D%2C%0A%0A%20%20%2F*%20convert%20images%20and%20figures%20that%20have%20properties%20like%20data-src%20into%20images%20that%20can%20be%20loaded%20without%20JS%20*%2F%0A%20%20_fixLazyImages(root)%20%7B%0A%20%20%20%20this._forEachNode(%0A%20%20%20%20%20%20this._getAllNodesWithTag(root%2C%20%5B'img'%2C%20'picture'%2C%20'figure'%5D)%2C%0A%20%20%20%20%20%20function%20(elem)%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20In%20some%20sites%20(e.g.%20Kotaku)%2C%20they%20put%201px%20square%20image%20as%20base64%20data%20uri%20in%20the%20src%20attribute.%0A%20%20%20%20%20%20%20%20%2F%2F%20So%2C%20here%20we%20check%20if%20the%20data%20uri%20is%20too%20short%2C%20just%20might%20as%20well%20remove%20it.%0A%20%20%20%20%20%20%20%20if%20(elem.src%20%26%26%20this.REGEXPS.b64DataUrl.test(elem.src))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20Make%20sure%20it's%20not%20SVG%2C%20because%20SVG%20can%20have%20a%20meaningful%20image%20in%20under%20133%20bytes.%0A%20%20%20%20%20%20%20%20%20%20const%20parts%20%3D%20this.REGEXPS.b64DataUrl.exec(elem.src)%3B%0A%20%20%20%20%20%20%20%20%20%20if%20(parts%5B1%5D%20%3D%3D%3D%20'image%2Fsvg%2Bxml')%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20Make%20sure%20this%20element%20has%20other%20attributes%20which%20contains%20image.%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20If%20it%20doesn't%2C%20then%20this%20src%20is%20important%20and%20shouldn't%20be%20removed.%0A%20%20%20%20%20%20%20%20%20%20let%20srcCouldBeRemoved%20%3D%20false%3B%0A%20%20%20%20%20%20%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20elem.attributes.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20var%20attr%20%3D%20elem.attributes%5Bi%5D%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(attr.name%20%3D%3D%3D%20'src')%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(%2F%5C.(jpg%7Cjpeg%7Cpng%7Cwebp)%2Fi.test(attr.value))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20srcCouldBeRemoved%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20Here%20we%20assume%20if%20image%20is%20less%20than%20100%20bytes%20(or%20133B%20after%20encoded%20to%20base64)%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20it%20will%20be%20too%20small%2C%20therefore%20it%20might%20be%20placeholder%20image.%0A%20%20%20%20%20%20%20%20%20%20if%20(srcCouldBeRemoved)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20b64starts%20%3D%20elem.src.search(%2Fbase64%5Cs*%2Fi)%20%2B%207%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20b64length%20%3D%20elem.src.length%20-%20b64starts%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(b64length%20%3C%20133)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20elem.removeAttribute('src')%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20also%20check%20for%20%22null%22%20to%20work%20around%20https%3A%2F%2Fgithub.com%2Fjsdom%2Fjsdom%2Fissues%2F2580%0A%20%20%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20%20%20(elem.src%20%7C%7C%20(elem.srcset%20%26%26%20elem.srcset%20!%3D%20'null'))%20%26%26%0A%20%20%20%20%20%20%20%20%20%20elem.className.toLowerCase().indexOf('lazy')%20%3D%3D%3D%20-1%0A%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20for%20(let%20j%20%3D%200%3B%20j%20%3C%20elem.attributes.length%3B%20j%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20attr%20%3D%20elem.attributes%5Bj%5D%3B%0A%20%20%20%20%20%20%20%20%20%20if%20(attr.name%20%3D%3D%3D%20'src'%20%7C%7C%20attr.name%20%3D%3D%3D%20'srcset')%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20let%20copyTo%20%3D%20null%3B%0A%20%20%20%20%20%20%20%20%20%20if%20(%2F%5C.(jpg%7Cjpeg%7Cpng%7Cwebp)%5Cs%2B%5Cd%2F.test(attr.value))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20copyTo%20%3D%20'srcset'%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%20else%20if%20(%2F%5E%5Cs*%5CS%2B%5C.(jpg%7Cjpeg%7Cpng%7Cwebp)%5CS*%5Cs*%24%2F.test(attr.value))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20copyTo%20%3D%20'src'%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20if%20(copyTo)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20if%20this%20is%20an%20img%20or%20picture%2C%20set%20the%20attribute%20directly%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(elem.tagName%20%3D%3D%3D%20'IMG'%20%7C%7C%20elem.tagName%20%3D%3D%3D%20'PICTURE')%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20elem.setAttribute(copyTo%2C%20attr.value)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20else%20if%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20elem.tagName%20%3D%3D%3D%20'FIGURE'%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20!this._getAllNodesWithTag(elem%2C%20%5B'img'%2C%20'picture'%5D).length%0A%20%20%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20if%20the%20item%20is%20a%20%3Cfigure%3E%20that%20does%20not%20contain%20an%20image%20or%20picture%2C%20create%20one%20and%20place%20it%20inside%20the%20figure%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20see%20the%20nytimes-3%20testcase%20for%20an%20example%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20const%20img%20%3D%20this._doc.createElement('img')%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20img.setAttribute(copyTo%2C%20attr.value)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20elem.appendChild(img)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Clean%20an%20element%20of%20all%20tags%20of%20type%20%22tag%22%20if%20they%20look%20fishy.%0A%20%20%20*%20%22Fishy%22%20is%20an%20algorithm%20based%20on%20content%20length%2C%20classnames%2C%20link%20density%2C%20number%20of%20images%20%26%20embeds%2C%20etc.%0A%20%20%20*%0A%20%20%20*%20%40return%20void%0A%20%20%20**%2F%0A%20%20_cleanConditionally(e%2C%20tag)%20%7B%0A%20%20%20%20if%20(!this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY))%20%7B%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20const%20isList%20%3D%20tag%20%3D%3D%3D%20'ul'%20%7C%7C%20tag%20%3D%3D%3D%20'ol'%3B%0A%0A%20%20%20%20%2F%2F%20Gather%20counts%20for%20other%20typical%20elements%20embedded%20within.%0A%20%20%20%20%2F%2F%20Traverse%20backwards%20so%20we%20can%20remove%20nodes%20at%20the%20same%20time%0A%20%20%20%20%2F%2F%20without%20effecting%20the%20traversal.%0A%20%20%20%20%2F%2F%0A%20%20%20%20%2F%2F%20TODO%3A%20Consider%20taking%20into%20account%20original%20contentScore%20here.%0A%20%20%20%20this._removeNodes(this._getAllNodesWithTag(e%2C%20%5Btag%5D)%2C%20function%20(node)%20%7B%0A%20%20%20%20%20%20%2F%2F%20First%20check%20if%20this%20node%20IS%20data%20table%2C%20in%20which%20case%20don't%20remove%20it.%0A%20%20%20%20%20%20const%20isDataTable%20%3D%20function%20(t)%20%7B%0A%20%20%20%20%20%20%20%20return%20t._readabilityDataTable%3B%0A%20%20%20%20%20%20%7D%3B%0A%0A%20%20%20%20%20%20if%20(tag%20%3D%3D%3D%20'table'%20%26%26%20isDataTable(node))%20%7B%0A%20%20%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%2F%2F%20Next%20check%20if%20we're%20inside%20a%20data%20table%2C%20in%20which%20case%20don't%20remove%20it%20as%20well.%0A%20%20%20%20%20%20if%20(this._hasAncestorTag(node%2C%20'table'%2C%20-1%2C%20isDataTable))%20%7B%0A%20%20%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20const%20weight%20%3D%20this._getClassWeight(node)%3B%0A%20%20%20%20%20%20const%20contentScore%20%3D%200%3B%0A%0A%20%20%20%20%20%20this.log('Cleaning%20Conditionally'%2C%20node)%3B%0A%0A%20%20%20%20%20%20if%20(weight%20%2B%20contentScore%20%3C%200)%20%7B%0A%20%20%20%20%20%20%20%20return%20true%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20if%20(this._getCharCount(node%2C%20'%2C')%20%3C%2010)%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20If%20there%20are%20not%20very%20many%20commas%2C%20and%20the%20number%20of%0A%20%20%20%20%20%20%20%20%2F%2F%20non-paragraph%20elements%20is%20more%20than%20paragraphs%20or%20other%0A%20%20%20%20%20%20%20%20%2F%2F%20ominous%20signs%2C%20remove%20the%20element.%0A%20%20%20%20%20%20%20%20const%20p%20%3D%20node.getElementsByTagName('p').length%3B%0A%20%20%20%20%20%20%20%20const%20img%20%3D%20node.getElementsByTagName('img').length%3B%0A%20%20%20%20%20%20%20%20const%20li%20%3D%20node.getElementsByTagName('li').length%20-%20100%3B%0A%20%20%20%20%20%20%20%20const%20input%20%3D%20node.getElementsByTagName('input').length%3B%0A%0A%20%20%20%20%20%20%20%20let%20embedCount%20%3D%200%3B%0A%20%20%20%20%20%20%20%20const%20embeds%20%3D%20this._getAllNodesWithTag(node%2C%20%5B%0A%20%20%20%20%20%20%20%20%20%20'object'%2C%0A%20%20%20%20%20%20%20%20%20%20'embed'%2C%0A%20%20%20%20%20%20%20%20%20%20'iframe'%2C%0A%20%20%20%20%20%20%20%20%5D)%3B%0A%0A%20%20%20%20%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20embeds.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20If%20this%20embed%20has%20attribute%20that%20matches%20video%20regex%2C%20don't%20delete%20it.%0A%20%20%20%20%20%20%20%20%20%20for%20(let%20j%20%3D%200%3B%20j%20%3C%20embeds%5Bi%5D.attributes.length%3B%20j%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(this.REGEXPS.videos.test(embeds%5Bi%5D.attributes%5Bj%5D.value))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20For%20embed%20with%20%3Cobject%3E%20tag%2C%20check%20inner%20HTML%20as%20well.%0A%20%20%20%20%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20embeds%5Bi%5D.tagName%20%3D%3D%3D%20'object'%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20this.REGEXPS.videos.test(embeds%5Bi%5D.innerHTML)%0A%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20embedCount%2B%2B%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20const%20linkDensity%20%3D%20this._getLinkDensity(node)%3B%0A%20%20%20%20%20%20%20%20const%20contentLength%20%3D%20this._getInnerText(node).length%3B%0A%0A%20%20%20%20%20%20%20%20const%20haveToRemove%20%3D%0A%20%20%20%20%20%20%20%20%20%20(img%20%3E%201%20%26%26%20p%20%2F%20img%20%3C%200.5%20%26%26%20!this._hasAncestorTag(node%2C%20'figure'))%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20(!isList%20%26%26%20li%20%3E%20p)%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20input%20%3E%20Math.floor(p%20%2F%203)%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20(!isList%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20contentLength%20%3C%2025%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20(img%20%3D%3D%3D%200%20%7C%7C%20img%20%3E%202)%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20!this._hasAncestorTag(node%2C%20'figure'))%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20(!isList%20%26%26%20weight%20%3C%2025%20%26%26%20linkDensity%20%3E%200.2)%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20(weight%20%3E%3D%2025%20%26%26%20linkDensity%20%3E%200.5)%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20(embedCount%20%3D%3D%3D%201%20%26%26%20contentLength%20%3C%2075)%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20embedCount%20%3E%201%3B%0A%20%20%20%20%20%20%20%20return%20haveToRemove%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%7D)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Clean%20out%20elements%20that%20match%20the%20specified%20conditions%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%20%40param%20Function%20determines%20whether%20a%20node%20should%20be%20removed%0A%20%20%20*%20%40return%20void%0A%20%20%20**%2F%0A%20%20_cleanMatchedNodes(e%2C%20filter)%20%7B%0A%20%20%20%20const%20endOfSearchMarkerNode%20%3D%20this._getNextNode(e%2C%20true)%3B%0A%20%20%20%20let%20next%20%3D%20this._getNextNode(e)%3B%0A%20%20%20%20while%20(next%20%26%26%20next%20!%3D%20endOfSearchMarkerNode)%20%7B%0A%20%20%20%20%20%20if%20(filter.call(this%2C%20next%2C%20next.className%20%2B%20'%20'%20%2B%20next.id))%20%7B%0A%20%20%20%20%20%20%20%20next%20%3D%20this._removeAndGetNext(next)%3B%0A%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20next%20%3D%20this._getNextNode(next)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Clean%20out%20spurious%20headers%20from%20an%20Element.%20Checks%20things%20like%20classnames%20and%20link%20density.%0A%20%20%20*%0A%20%20%20*%20%40param%20Element%0A%20%20%20*%20%40return%20void%0A%20%20%20**%2F%0A%20%20_cleanHeaders(e)%20%7B%0A%20%20%20%20this._removeNodes(%0A%20%20%20%20%20%20this._getAllNodesWithTag(e%2C%20%5B'h1'%2C%20'h2'%5D)%2C%0A%20%20%20%20%20%20function%20(header)%20%7B%0A%20%20%20%20%20%20%20%20return%20this._getClassWeight(header)%20%3C%200%3B%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%0A%20%20_flagIsActive(flag)%20%7B%0A%20%20%20%20return%20(this._flags%20%26%20flag)%20%3E%200%3B%0A%20%20%7D%2C%0A%0A%20%20_removeFlag(flag)%20%7B%0A%20%20%20%20this._flags%20%3D%20this._flags%20%26%20~flag%3B%0A%20%20%7D%2C%0A%0A%20%20_isProbablyVisible(node)%20%7B%0A%20%20%20%20%2F%2F%20Have%20to%20null-check%20node.style%20and%20node.className.indexOf%20to%20deal%20with%20SVG%20and%20MathML%20nodes.%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20(!node.style%20%7C%7C%20node.style.display%20!%3D%20'none')%20%26%26%0A%20%20%20%20%20%20!node.hasAttribute('hidden')%20%26%26%0A%20%20%20%20%20%20%2F%2F%20check%20for%20%22fallback-image%22%20so%20that%20wikimedia%20math%20images%20are%20displayed%0A%20%20%20%20%20%20(!node.hasAttribute('aria-hidden')%20%7C%7C%0A%20%20%20%20%20%20%20%20node.getAttribute('aria-hidden')%20!%3D%20'true'%20%7C%7C%0A%20%20%20%20%20%20%20%20(node.className%20%26%26%0A%20%20%20%20%20%20%20%20%20%20node.className.indexOf%20%26%26%0A%20%20%20%20%20%20%20%20%20%20node.className.indexOf('fallback-image')%20!%3D%3D%20-1))%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Runs%20readability.%0A%20%20%20*%0A%20%20%20*%20Workflow%3A%0A%20%20%20*%20%201.%20Prep%20the%20document%20by%20removing%20script%20tags%2C%20css%2C%20etc.%0A%20%20%20*%20%202.%20Build%20readability's%20DOM%20tree.%0A%20%20%20*%20%203.%20Grab%20the%20article%20content%20from%20the%20current%20dom%20tree.%0A%20%20%20*%20%204.%20Replace%20the%20current%20DOM%20tree%20with%20the%20new%20one.%0A%20%20%20*%20%205.%20Read%20peacefully.%0A%20%20%20*%0A%20%20%20*%20%40return%20void%0A%20%20%20**%2F%0A%20%20parse()%20%7B%0A%20%20%20%20%2F%2F%20Avoid%20parsing%20too%20large%20documents%2C%20as%20per%20configuration%20option%0A%20%20%20%20if%20(this._maxElemsToParse%20%3E%200)%20%7B%0A%20%20%20%20%20%20const%20numTags%20%3D%20this._doc.getElementsByTagName('*').length%3B%0A%20%20%20%20%20%20if%20(numTags%20%3E%20this._maxElemsToParse)%20%7B%0A%20%20%20%20%20%20%20%20throw%20new%20Error(%0A%20%20%20%20%20%20%20%20%20%20'Aborting%20parsing%20document%3B%20'%20%2B%20numTags%20%2B%20'%20elements%20found'%2C%0A%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20Unwrap%20image%20from%20noscript%0A%20%20%20%20this._unwrapNoscriptImages(this._doc)%3B%0A%0A%20%20%20%20%2F%2F%20Remove%20script%20tags%20from%20the%20document.%0A%20%20%20%20this._removeScripts(this._doc)%3B%0A%0A%20%20%20%20this._prepDocument()%3B%0A%0A%20%20%20%20const%20metadata%20%3D%20this._getArticleMetadata()%3B%0A%20%20%20%20this._articleTitle%20%3D%20metadata.title%3B%0A%0A%20%20%20%20const%20articleContent%20%3D%20this._grabArticle()%3B%0A%20%20%20%20if%20(!articleContent)%20%7B%0A%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20this.log('Grabbed%3A%20'%20%2B%20articleContent.innerHTML)%3B%0A%0A%20%20%20%20this._postProcessContent(articleContent)%3B%0A%0A%20%20%20%20%2F%2F%20If%20we%20haven't%20found%20an%20excerpt%20in%20the%20article's%20metadata%2C%20use%20the%20article's%0A%20%20%20%20%2F%2F%20first%20paragraph%20as%20the%20excerpt.%20This%20is%20used%20for%20displaying%20a%20preview%20of%0A%20%20%20%20%2F%2F%20the%20article's%20content.%0A%20%20%20%20if%20(!metadata.excerpt)%20%7B%0A%20%20%20%20%20%20const%20paragraphs%20%3D%20articleContent.getElementsByTagName('p')%3B%0A%20%20%20%20%20%20if%20(paragraphs.length%20%3E%200)%20%7B%0A%20%20%20%20%20%20%20%20metadata.excerpt%20%3D%20paragraphs%5B0%5D.textContent.trim()%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20const%20textContent%20%3D%20articleContent.textContent%3B%0A%20%20%20%20return%20%7B%0A%20%20%20%20%20%20title%3A%20this._articleTitle%2C%0A%20%20%20%20%20%20byline%3A%20metadata.byline%20%7C%7C%20this._articleByline%2C%0A%20%20%20%20%20%20dir%3A%20this._articleDir%2C%0A%20%20%20%20%20%20content%3A%20articleContent.innerHTML%2C%0A%20%20%20%20%20%20textContent%2C%0A%20%20%20%20%20%20length%3A%20textContent.length%2C%0A%20%20%20%20%20%20excerpt%3A%20metadata.excerpt%2C%0A%20%20%20%20%20%20siteName%3A%20metadata.siteName%20%7C%7C%20this._articleSiteName%2C%0A%20%20%20%20%7D%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0A%2F%2F%2F%20TURNDOWN%3A%20https%3A%2F%2Funpkg.com%2Fturndown%406.0.0%3Fmodule%0A%0Afunction%20extend(destination)%20%7B%0A%20%20for%20(let%20i%20%3D%201%3B%20i%20%3C%20arguments.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20const%20source%20%3D%20arguments%5Bi%5D%3B%0A%20%20%20%20for%20(const%20key%20in%20source)%20%7B%0A%20%20%20%20%20%20if%20(source.hasOwnProperty(key))%20destination%5Bkey%5D%20%3D%20source%5Bkey%5D%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%20%20return%20destination%3B%0A%7D%0A%0Afunction%20repeat(character%2C%20count)%20%7B%0A%20%20return%20Array(count%20%2B%201).join(character)%3B%0A%7D%0A%0Aconst%20blockElements%20%3D%20%5B%0A%20%20'address'%2C%0A%20%20'article'%2C%0A%20%20'aside'%2C%0A%20%20'audio'%2C%0A%20%20'blockquote'%2C%0A%20%20'body'%2C%0A%20%20'canvas'%2C%0A%20%20'center'%2C%0A%20%20'dd'%2C%0A%20%20'dir'%2C%0A%20%20'div'%2C%0A%20%20'dl'%2C%0A%20%20'dt'%2C%0A%20%20'fieldset'%2C%0A%20%20'figcaption'%2C%0A%20%20'figure'%2C%0A%20%20'footer'%2C%0A%20%20'form'%2C%0A%20%20'frameset'%2C%0A%20%20'h1'%2C%0A%20%20'h2'%2C%0A%20%20'h3'%2C%0A%20%20'h4'%2C%0A%20%20'h5'%2C%0A%20%20'h6'%2C%0A%20%20'header'%2C%0A%20%20'hgroup'%2C%0A%20%20'hr'%2C%0A%20%20'html'%2C%0A%20%20'isindex'%2C%0A%20%20'li'%2C%0A%20%20'main'%2C%0A%20%20'menu'%2C%0A%20%20'nav'%2C%0A%20%20'noframes'%2C%0A%20%20'noscript'%2C%0A%20%20'ol'%2C%0A%20%20'output'%2C%0A%20%20'p'%2C%0A%20%20'pre'%2C%0A%20%20'section'%2C%0A%20%20'table'%2C%0A%20%20'tbody'%2C%0A%20%20'td'%2C%0A%20%20'tfoot'%2C%0A%20%20'th'%2C%0A%20%20'thead'%2C%0A%20%20'tr'%2C%0A%20%20'ul'%2C%0A%5D%3B%0A%0Afunction%20isBlock(node)%20%7B%0A%20%20return%20blockElements.indexOf(node.nodeName.toLowerCase())%20!%3D%3D%20-1%3B%0A%7D%0A%0Aconst%20voidElements%20%3D%20%5B%0A%20%20'area'%2C%0A%20%20'base'%2C%0A%20%20'br'%2C%0A%20%20'col'%2C%0A%20%20'command'%2C%0A%20%20'embed'%2C%0A%20%20'hr'%2C%0A%20%20'img'%2C%0A%20%20'input'%2C%0A%20%20'keygen'%2C%0A%20%20'link'%2C%0A%20%20'meta'%2C%0A%20%20'param'%2C%0A%20%20'source'%2C%0A%20%20'track'%2C%0A%20%20'wbr'%2C%0A%5D%3B%0A%0Afunction%20isVoid(node)%20%7B%0A%20%20return%20voidElements.indexOf(node.nodeName.toLowerCase())%20!%3D%3D%20-1%3B%0A%7D%0A%0Aconst%20voidSelector%20%3D%20voidElements.join()%3B%0Afunction%20hasVoid(node)%20%7B%0A%20%20return%20node.querySelector%20%26%26%20node.querySelector(voidSelector)%3B%0A%7D%0A%0Aconst%20rules%20%3D%20%7B%7D%3B%0A%0Arules.paragraph%20%3D%20%7B%0A%20%20filter%3A%20'p'%2C%0A%0A%20%20replacement%3A%20function%20(content)%20%7B%0A%20%20%20%20return%20'%5Cn%5Cn'%20%2B%20content%20%2B%20'%5Cn%5Cn'%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0Arules.lineBreak%20%3D%20%7B%0A%20%20filter%3A%20'br'%2C%0A%0A%20%20replacement%3A%20function%20(content%2C%20node%2C%20options)%20%7B%0A%20%20%20%20return%20options.br%20%2B%20'%5Cn'%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0Arules.heading%20%3D%20%7B%0A%20%20filter%3A%20%5B'h1'%2C%20'h2'%2C%20'h3'%2C%20'h4'%2C%20'h5'%2C%20'h6'%5D%2C%0A%0A%20%20replacement%3A%20function%20(content%2C%20node%2C%20options)%20%7B%0A%20%20%20%20const%20hLevel%20%3D%20Number(node.nodeName.charAt(1))%3B%0A%0A%20%20%20%20if%20(options.headingStyle%20%3D%3D%3D%20'setext'%20%26%26%20hLevel%20%3C%203)%20%7B%0A%20%20%20%20%20%20const%20underline%20%3D%20repeat(hLevel%20%3D%3D%3D%201%20%3F%20'%3D'%20%3A%20'-'%2C%20content.length)%3B%0A%20%20%20%20%20%20return%20'%5Cn%5Cn'%20%2B%20content%20%2B%20'%5Cn'%20%2B%20underline%20%2B%20'%5Cn%5Cn'%3B%0A%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20return%20'%5Cn%5Cn'%20%2B%20repeat('%23'%2C%20hLevel)%20%2B%20'%20'%20%2B%20content%20%2B%20'%5Cn%5Cn'%3B%0A%20%20%20%20%7D%0A%20%20%7D%2C%0A%7D%3B%0A%0Arules.blockquote%20%3D%20%7B%0A%20%20filter%3A%20'blockquote'%2C%0A%0A%20%20replacement%3A%20function%20(content)%20%7B%0A%20%20%20%20content%20%3D%20content.replace(%2F%5E%5Cn%2B%7C%5Cn%2B%24%2Fg%2C%20'')%3B%0A%20%20%20%20content%20%3D%20content.replace(%2F%5E%2Fgm%2C%20'%3E%20')%3B%0A%20%20%20%20return%20'%5Cn%5Cn'%20%2B%20content%20%2B%20'%5Cn%5Cn'%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0Arules.list%20%3D%20%7B%0A%20%20filter%3A%20%5B'ul'%2C%20'ol'%5D%2C%0A%0A%20%20replacement%3A%20function%20(content%2C%20node)%20%7B%0A%20%20%20%20const%20parent%20%3D%20node.parentNode%3B%0A%20%20%20%20if%20(parent.nodeName%20%3D%3D%3D%20'LI'%20%26%26%20parent.lastElementChild%20%3D%3D%3D%20node)%20%7B%0A%20%20%20%20%20%20return%20'%5Cn'%20%2B%20content%3B%0A%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20return%20'%5Cn%5Cn'%20%2B%20content%20%2B%20'%5Cn%5Cn'%3B%0A%20%20%20%20%7D%0A%20%20%7D%2C%0A%7D%3B%0A%0Arules.listItem%20%3D%20%7B%0A%20%20filter%3A%20'li'%2C%0A%0A%20%20replacement%3A%20function%20(content%2C%20node%2C%20options)%20%7B%0A%20%20%20%20content%20%3D%20content%0A%20%20%20%20%20%20.replace(%2F%5E%5Cn%2B%2F%2C%20'')%20%2F%2F%20remove%20leading%20newlines%0A%20%20%20%20%20%20.replace(%2F%5Cn%2B%24%2F%2C%20'%5Cn')%20%2F%2F%20replace%20trailing%20newlines%20with%20just%20a%20single%20one%0A%20%20%20%20%20%20.replace(%2F%5Cn%2Fgm%2C%20'%5Cn%20%20%20%20')%3B%20%2F%2F%20indent%0A%20%20%20%20let%20prefix%20%3D%20options.bulletListMarker%20%2B%20'%20%20%20'%3B%0A%20%20%20%20const%20parent%20%3D%20node.parentNode%3B%0A%20%20%20%20if%20(parent.nodeName%20%3D%3D%3D%20'OL')%20%7B%0A%20%20%20%20%20%20const%20start%20%3D%20parent.getAttribute('start')%3B%0A%20%20%20%20%20%20const%20index%20%3D%20Array.prototype.indexOf.call(parent.children%2C%20node)%3B%0A%20%20%20%20%20%20prefix%20%3D%20(start%20%3F%20Number(start)%20%2B%20index%20%3A%20index%20%2B%201)%20%2B%20'.%20%20'%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20prefix%20%2B%20content%20%2B%20(node.nextSibling%20%26%26%20!%2F%5Cn%24%2F.test(content)%20%3F%20'%5Cn'%20%3A%20'')%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0Arules.indentedCodeBlock%20%3D%20%7B%0A%20%20filter%3A%20function%20(node%2C%20options)%20%7B%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20options.codeBlockStyle%20%3D%3D%3D%20'indented'%20%26%26%0A%20%20%20%20%20%20node.nodeName%20%3D%3D%3D%20'PRE'%20%26%26%0A%20%20%20%20%20%20node.firstChild%20%26%26%0A%20%20%20%20%20%20node.firstChild.nodeName%20%3D%3D%3D%20'CODE'%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%0A%20%20replacement%3A%20function%20(content%2C%20node%2C%20options)%20%7B%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20'%5Cn%5Cn%20%20%20%20'%20%2B%20node.firstChild.textContent.replace(%2F%5Cn%2Fg%2C%20'%5Cn%20%20%20%20')%20%2B%20'%5Cn%5Cn'%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0Arules.fencedCodeBlock%20%3D%20%7B%0A%20%20filter%3A%20function%20(node%2C%20options)%20%7B%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20options.codeBlockStyle%20%3D%3D%3D%20'fenced'%20%26%26%0A%20%20%20%20%20%20node.nodeName%20%3D%3D%3D%20'PRE'%20%26%26%0A%20%20%20%20%20%20node.firstChild%20%26%26%0A%20%20%20%20%20%20node.firstChild.nodeName%20%3D%3D%3D%20'CODE'%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%0A%20%20replacement%3A%20function%20(content%2C%20node%2C%20options)%20%7B%0A%20%20%20%20const%20className%20%3D%20node.firstChild.className%20%7C%7C%20''%3B%0A%20%20%20%20const%20language%20%3D%20(className.match(%2Flanguage-(%5CS%2B)%2F)%20%7C%7C%20%5Bnull%2C%20''%5D)%5B1%5D%3B%0A%20%20%20%20const%20code%20%3D%20node.firstChild.textContent%3B%0A%0A%20%20%20%20const%20fenceChar%20%3D%20options.fence.charAt(0)%3B%0A%20%20%20%20let%20fenceSize%20%3D%203%3B%0A%20%20%20%20const%20fenceInCodeRegex%20%3D%20new%20RegExp('%5E'%20%2B%20fenceChar%20%2B%20'%7B3%2C%7D'%2C%20'gm')%3B%0A%0A%20%20%20%20let%20match%3B%0A%20%20%20%20while%20((match%20%3D%20fenceInCodeRegex.exec(code)))%20%7B%0A%20%20%20%20%20%20if%20(match%5B0%5D.length%20%3E%3D%20fenceSize)%20%7B%0A%20%20%20%20%20%20%20%20fenceSize%20%3D%20match%5B0%5D.length%20%2B%201%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20const%20fence%20%3D%20repeat(fenceChar%2C%20fenceSize)%3B%0A%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20'%5Cn%5Cn'%20%2B%0A%20%20%20%20%20%20fence%20%2B%0A%20%20%20%20%20%20language%20%2B%0A%20%20%20%20%20%20'%5Cn'%20%2B%0A%20%20%20%20%20%20code.replace(%2F%5Cn%24%2F%2C%20'')%20%2B%0A%20%20%20%20%20%20'%5Cn'%20%2B%0A%20%20%20%20%20%20fence%20%2B%0A%20%20%20%20%20%20'%5Cn%5Cn'%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0Arules.horizontalRule%20%3D%20%7B%0A%20%20filter%3A%20'hr'%2C%0A%0A%20%20replacement%3A%20function%20(content%2C%20node%2C%20options)%20%7B%0A%20%20%20%20return%20'%5Cn%5Cn'%20%2B%20options.hr%20%2B%20'%5Cn%5Cn'%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0Arules.inlineLink%20%3D%20%7B%0A%20%20filter%3A%20function%20(node%2C%20options)%20%7B%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20options.linkStyle%20%3D%3D%3D%20'inlined'%20%26%26%0A%20%20%20%20%20%20node.nodeName%20%3D%3D%3D%20'A'%20%26%26%0A%20%20%20%20%20%20node.getAttribute('href')%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%0A%20%20replacement%3A%20function%20(content%2C%20node)%20%7B%0A%20%20%20%20const%20href%20%3D%20node.getAttribute('href')%3B%0A%20%20%20%20const%20title%20%3D%20node.title%20%3F%20'%20%22'%20%2B%20node.title%20%2B%20'%22'%20%3A%20''%3B%0A%20%20%20%20return%20'%5B'%20%2B%20content%20%2B%20'%5D('%20%2B%20href%20%2B%20title%20%2B%20')'%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0Arules.referenceLink%20%3D%20%7B%0A%20%20filter%3A%20function%20(node%2C%20options)%20%7B%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20options.linkStyle%20%3D%3D%3D%20'referenced'%20%26%26%0A%20%20%20%20%20%20node.nodeName%20%3D%3D%3D%20'A'%20%26%26%0A%20%20%20%20%20%20node.getAttribute('href')%0A%20%20%20%20)%3B%0A%20%20%7D%2C%0A%0A%20%20replacement%3A%20function%20(content%2C%20node%2C%20options)%20%7B%0A%20%20%20%20const%20href%20%3D%20node.getAttribute('href')%3B%0A%20%20%20%20const%20title%20%3D%20node.title%20%3F%20'%20%22'%20%2B%20node.title%20%2B%20'%22'%20%3A%20''%3B%0A%20%20%20%20let%20replacement%3B%0A%20%20%20%20let%20reference%3B%0A%0A%20%20%20%20switch%20(options.linkReferenceStyle)%20%7B%0A%20%20%20%20%20%20case%20'collapsed'%3A%0A%20%20%20%20%20%20%20%20replacement%20%3D%20'%5B'%20%2B%20content%20%2B%20'%5D%5B%5D'%3B%0A%20%20%20%20%20%20%20%20reference%20%3D%20'%5B'%20%2B%20content%20%2B%20'%5D%3A%20'%20%2B%20href%20%2B%20title%3B%0A%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20case%20'shortcut'%3A%0A%20%20%20%20%20%20%20%20replacement%20%3D%20'%5B'%20%2B%20content%20%2B%20'%5D'%3B%0A%20%20%20%20%20%20%20%20reference%20%3D%20'%5B'%20%2B%20content%20%2B%20'%5D%3A%20'%20%2B%20href%20%2B%20title%3B%0A%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20default%3A%0A%20%20%20%20%20%20%20%20var%20id%20%3D%20this.references.length%20%2B%201%3B%0A%20%20%20%20%20%20%20%20replacement%20%3D%20'%5B'%20%2B%20content%20%2B%20'%5D%5B'%20%2B%20id%20%2B%20'%5D'%3B%0A%20%20%20%20%20%20%20%20reference%20%3D%20'%5B'%20%2B%20id%20%2B%20'%5D%3A%20'%20%2B%20href%20%2B%20title%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20this.references.push(reference)%3B%0A%20%20%20%20return%20replacement%3B%0A%20%20%7D%2C%0A%0A%20%20references%3A%20%5B%5D%2C%0A%0A%20%20append%3A%20function%20(options)%20%7B%0A%20%20%20%20let%20references%20%3D%20''%3B%0A%20%20%20%20if%20(this.references.length)%20%7B%0A%20%20%20%20%20%20references%20%3D%20'%5Cn%5Cn'%20%2B%20this.references.join('%5Cn')%20%2B%20'%5Cn%5Cn'%3B%0A%20%20%20%20%20%20this.references%20%3D%20%5B%5D%3B%20%2F%2F%20Reset%20references%0A%20%20%20%20%7D%0A%20%20%20%20return%20references%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0Arules.emphasis%20%3D%20%7B%0A%20%20filter%3A%20%5B'em'%2C%20'i'%5D%2C%0A%0A%20%20replacement%3A%20function%20(content%2C%20node%2C%20options)%20%7B%0A%20%20%20%20if%20(!content.trim())%20return%20''%3B%0A%20%20%20%20return%20options.emDelimiter%20%2B%20content%20%2B%20options.emDelimiter%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0Arules.strong%20%3D%20%7B%0A%20%20filter%3A%20%5B'strong'%2C%20'b'%5D%2C%0A%0A%20%20replacement%3A%20function%20(content%2C%20node%2C%20options)%20%7B%0A%20%20%20%20if%20(!content.trim())%20return%20''%3B%0A%20%20%20%20return%20options.strongDelimiter%20%2B%20content%20%2B%20options.strongDelimiter%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0Arules.code%20%3D%20%7B%0A%20%20filter%3A%20function%20(node)%20%7B%0A%20%20%20%20const%20hasSiblings%20%3D%20node.previousSibling%20%7C%7C%20node.nextSibling%3B%0A%20%20%20%20const%20isCodeBlock%20%3D%20node.parentNode.nodeName%20%3D%3D%3D%20'PRE'%20%26%26%20!hasSiblings%3B%0A%0A%20%20%20%20return%20node.nodeName%20%3D%3D%3D%20'CODE'%20%26%26%20!isCodeBlock%3B%0A%20%20%7D%2C%0A%0A%20%20replacement%3A%20function%20(content)%20%7B%0A%20%20%20%20if%20(!content.trim())%20return%20''%3B%0A%0A%20%20%20%20let%20delimiter%20%3D%20'%60'%3B%0A%20%20%20%20let%20leadingSpace%20%3D%20''%3B%0A%20%20%20%20let%20trailingSpace%20%3D%20''%3B%0A%20%20%20%20const%20matches%20%3D%20content.match(%2F%60%2B%2Fgm)%3B%0A%20%20%20%20if%20(matches)%20%7B%0A%20%20%20%20%20%20if%20(%2F%5E%60%2F.test(content))%20leadingSpace%20%3D%20'%20'%3B%0A%20%20%20%20%20%20if%20(%2F%60%24%2F.test(content))%20trailingSpace%20%3D%20'%20'%3B%0A%20%20%20%20%20%20while%20(matches.indexOf(delimiter)%20!%3D%3D%20-1)%20delimiter%20%3D%20delimiter%20%2B%20'%60'%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20return%20delimiter%20%2B%20leadingSpace%20%2B%20content%20%2B%20trailingSpace%20%2B%20delimiter%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0Arules.image%20%3D%20%7B%0A%20%20filter%3A%20'img'%2C%0A%0A%20%20replacement%3A%20function%20(content%2C%20node)%20%7B%0A%20%20%20%20const%20alt%20%3D%20node.alt%20%7C%7C%20''%3B%0A%20%20%20%20const%20src%20%3D%20node.getAttribute('src')%20%7C%7C%20''%3B%0A%20%20%20%20const%20title%20%3D%20node.title%20%7C%7C%20''%3B%0A%20%20%20%20const%20titlePart%20%3D%20title%20%3F%20'%20%22'%20%2B%20title%20%2B%20'%22'%20%3A%20''%3B%0A%20%20%20%20return%20src%20%3F%20'!%5B'%20%2B%20alt%20%2B%20'%5D'%20%2B%20'('%20%2B%20src%20%2B%20titlePart%20%2B%20')'%20%3A%20''%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0A%2F**%0A%20*%20Manages%20a%20collection%20of%20rules%20used%20to%20convert%20HTML%20to%20Markdown%0A%20*%2F%0A%0Afunction%20Rules(options)%20%7B%0A%20%20this.options%20%3D%20options%3B%0A%20%20this._keep%20%3D%20%5B%5D%3B%0A%20%20this._remove%20%3D%20%5B%5D%3B%0A%0A%20%20this.blankRule%20%3D%20%7B%0A%20%20%20%20replacement%3A%20options.blankReplacement%2C%0A%20%20%7D%3B%0A%0A%20%20this.keepReplacement%20%3D%20options.keepReplacement%3B%0A%0A%20%20this.defaultRule%20%3D%20%7B%0A%20%20%20%20replacement%3A%20options.defaultReplacement%2C%0A%20%20%7D%3B%0A%0A%20%20this.array%20%3D%20%5B%5D%3B%0A%20%20for%20(const%20key%20in%20options.rules)%20this.array.push(options.rules%5Bkey%5D)%3B%0A%7D%0A%0ARules.prototype%20%3D%20%7B%0A%20%20add%3A%20function%20(key%2C%20rule)%20%7B%0A%20%20%20%20this.array.unshift(rule)%3B%0A%20%20%7D%2C%0A%0A%20%20keep%3A%20function%20(filter)%20%7B%0A%20%20%20%20this._keep.unshift(%7B%0A%20%20%20%20%20%20filter%3A%20filter%2C%0A%20%20%20%20%20%20replacement%3A%20this.keepReplacement%2C%0A%20%20%20%20%7D)%3B%0A%20%20%7D%2C%0A%0A%20%20remove%3A%20function%20(filter)%20%7B%0A%20%20%20%20this._remove.unshift(%7B%0A%20%20%20%20%20%20filter%3A%20filter%2C%0A%20%20%20%20%20%20replacement%3A%20function%20()%20%7B%0A%20%20%20%20%20%20%20%20return%20''%3B%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%7D)%3B%0A%20%20%7D%2C%0A%0A%20%20forNode%3A%20function%20(node)%20%7B%0A%20%20%20%20if%20(node.isBlank)%20return%20this.blankRule%3B%0A%20%20%20%20let%20rule%3B%0A%0A%20%20%20%20if%20((rule%20%3D%20findRule(this.array%2C%20node%2C%20this.options)))%20return%20rule%3B%0A%20%20%20%20if%20((rule%20%3D%20findRule(this._keep%2C%20node%2C%20this.options)))%20return%20rule%3B%0A%20%20%20%20if%20((rule%20%3D%20findRule(this._remove%2C%20node%2C%20this.options)))%20return%20rule%3B%0A%0A%20%20%20%20return%20this.defaultRule%3B%0A%20%20%7D%2C%0A%0A%20%20forEach%3A%20function%20(fn)%20%7B%0A%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20this.array.length%3B%20i%2B%2B)%20fn(this.array%5Bi%5D%2C%20i)%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0Afunction%20findRule(rules%2C%20node%2C%20options)%20%7B%0A%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20rules.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20const%20rule%20%3D%20rules%5Bi%5D%3B%0A%20%20%20%20if%20(filterValue(rule%2C%20node%2C%20options))%20return%20rule%3B%0A%20%20%7D%0A%20%20return%20void%200%3B%0A%7D%0A%0Afunction%20filterValue(rule%2C%20node%2C%20options)%20%7B%0A%20%20const%20filter%20%3D%20rule.filter%3B%0A%20%20if%20(typeof%20filter%20%3D%3D%3D%20'string')%20%7B%0A%20%20%20%20if%20(filter%20%3D%3D%3D%20node.nodeName.toLowerCase())%20return%20true%3B%0A%20%20%7D%20else%20if%20(Array.isArray(filter))%20%7B%0A%20%20%20%20if%20(filter.indexOf(node.nodeName.toLowerCase())%20%3E%20-1)%20return%20true%3B%0A%20%20%7D%20else%20if%20(typeof%20filter%20%3D%3D%3D%20'function')%20%7B%0A%20%20%20%20if%20(filter.call(rule%2C%20node%2C%20options))%20return%20true%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20throw%20new%20TypeError('%60filter%60%20needs%20to%20be%20a%20string%2C%20array%2C%20or%20function')%3B%0A%20%20%7D%0A%7D%0A%0A%2F**%0A%20*%20The%20collapseWhitespace%20function%20is%20adapted%20from%20collapse-whitespace%0A%20*%20by%20Luc%20Thevenard.%0A%20*%0A%20*%20The%20MIT%20License%20(MIT)%0A%20*%0A%20*%20Copyright%20(c)%202014%20Luc%20Thevenard%20%3Clucthevenard%40gmail.com%3E%0A%20*%0A%20*%20Permission%20is%20hereby%20granted%2C%20free%20of%20charge%2C%20to%20any%20person%20obtaining%20a%20copy%0A%20*%20of%20this%20software%20and%20associated%20documentation%20files%20(the%20%22Software%22)%2C%20to%20deal%0A%20*%20in%20the%20Software%20without%20restriction%2C%20including%20without%20limitation%20the%20rights%0A%20*%20to%20use%2C%20copy%2C%20modify%2C%20merge%2C%20publish%2C%20distribute%2C%20sublicense%2C%20and%2For%20sell%0A%20*%20copies%20of%20the%20Software%2C%20and%20to%20permit%20persons%20to%20whom%20the%20Software%20is%0A%20*%20furnished%20to%20do%20so%2C%20subject%20to%20the%20following%20conditions%3A%0A%20*%0A%20*%20The%20above%20copyright%20notice%20and%20this%20permission%20notice%20shall%20be%20included%20in%0A%20*%20all%20copies%20or%20substantial%20portions%20of%20the%20Software.%0A%20*%0A%20*%20THE%20SOFTWARE%20IS%20PROVIDED%20%22AS%20IS%22%2C%20WITHOUT%20WARRANTY%20OF%20ANY%20KIND%2C%20EXPRESS%20OR%0A%20*%20IMPLIED%2C%20INCLUDING%20BUT%20NOT%20LIMITED%20TO%20THE%20WARRANTIES%20OF%20MERCHANTABILITY%2C%0A%20*%20FITNESS%20FOR%20A%20PARTICULAR%20PURPOSE%20AND%20NONINFRINGEMENT.%20IN%20NO%20EVENT%20SHALL%20THE%0A%20*%20AUTHORS%20OR%20COPYRIGHT%20HOLDERS%20BE%20LIABLE%20FOR%20ANY%20CLAIM%2C%20DAMAGES%20OR%20OTHER%0A%20*%20LIABILITY%2C%20WHETHER%20IN%20AN%20ACTION%20OF%20CONTRACT%2C%20TORT%20OR%20OTHERWISE%2C%20ARISING%20FROM%2C%0A%20*%20OUT%20OF%20OR%20IN%20CONNECTION%20WITH%20THE%20SOFTWARE%20OR%20THE%20USE%20OR%20OTHER%20DEALINGS%20IN%0A%20*%20THE%20SOFTWARE.%0A%20*%2F%0A%0A%2F**%0A%20*%20collapseWhitespace(options)%20removes%20extraneous%20whitespace%20from%20an%20the%20given%20element.%0A%20*%0A%20*%20%40param%20%7BObject%7D%20options%0A%20*%2F%0Afunction%20collapseWhitespace(options)%20%7B%0A%20%20const%20element%20%3D%20options.element%3B%0A%20%20const%20isBlock%20%3D%20options.isBlock%3B%0A%20%20const%20isVoid%20%3D%20options.isVoid%3B%0A%20%20const%20isPre%20%3D%0A%20%20%20%20options.isPre%20%7C%7C%0A%20%20%20%20function%20(node)%20%7B%0A%20%20%20%20%20%20return%20node.nodeName%20%3D%3D%3D%20'PRE'%3B%0A%20%20%20%20%7D%3B%0A%0A%20%20if%20(!element.firstChild%20%7C%7C%20isPre(element))%20return%3B%0A%0A%20%20let%20prevText%20%3D%20null%3B%0A%20%20let%20prevVoid%20%3D%20false%3B%0A%0A%20%20let%20prev%20%3D%20null%3B%0A%20%20let%20node%20%3D%20next(prev%2C%20element%2C%20isPre)%3B%0A%0A%20%20while%20(node%20!%3D%3D%20element)%20%7B%0A%20%20%20%20if%20(node.nodeType%20%3D%3D%3D%203%20%7C%7C%20node.nodeType%20%3D%3D%3D%204)%20%7B%0A%20%20%20%20%20%20%2F%2F%20Node.TEXT_NODE%20or%20Node.CDATA_SECTION_NODE%0A%20%20%20%20%20%20let%20text%20%3D%20node.data.replace(%2F%5B%20%5Cr%5Cn%5Ct%5D%2B%2Fg%2C%20'%20')%3B%0A%0A%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20(!prevText%20%7C%7C%20%2F%20%24%2F.test(prevText.data))%20%26%26%0A%20%20%20%20%20%20%20%20!prevVoid%20%26%26%0A%20%20%20%20%20%20%20%20text%5B0%5D%20%3D%3D%3D%20'%20'%0A%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20text%20%3D%20text.substr(1)%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%2F%2F%20%60text%60%20might%20be%20empty%20at%20this%20point.%0A%20%20%20%20%20%20if%20(!text)%20%7B%0A%20%20%20%20%20%20%20%20node%20%3D%20remove(node)%3B%0A%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20node.data%20%3D%20text%3B%0A%0A%20%20%20%20%20%20prevText%20%3D%20node%3B%0A%20%20%20%20%7D%20else%20if%20(node.nodeType%20%3D%3D%3D%201)%20%7B%0A%20%20%20%20%20%20%2F%2F%20Node.ELEMENT_NODE%0A%20%20%20%20%20%20if%20(isBlock(node)%20%7C%7C%20node.nodeName%20%3D%3D%3D%20'BR')%20%7B%0A%20%20%20%20%20%20%20%20if%20(prevText)%20%7B%0A%20%20%20%20%20%20%20%20%20%20prevText.data%20%3D%20prevText.data.replace(%2F%20%24%2F%2C%20'')%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20prevText%20%3D%20null%3B%0A%20%20%20%20%20%20%20%20prevVoid%20%3D%20false%3B%0A%20%20%20%20%20%20%7D%20else%20if%20(isVoid(node))%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20Avoid%20trimming%20space%20around%20non-block%2C%20non-BR%20void%20elements.%0A%20%20%20%20%20%20%20%20prevText%20%3D%20null%3B%0A%20%20%20%20%20%20%20%20prevVoid%20%3D%20true%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20node%20%3D%20remove(node)%3B%0A%20%20%20%20%20%20continue%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20const%20nextNode%20%3D%20next(prev%2C%20node%2C%20isPre)%3B%0A%20%20%20%20prev%20%3D%20node%3B%0A%20%20%20%20node%20%3D%20nextNode%3B%0A%20%20%7D%0A%0A%20%20if%20(prevText)%20%7B%0A%20%20%20%20prevText.data%20%3D%20prevText.data.replace(%2F%20%24%2F%2C%20'')%3B%0A%20%20%20%20if%20(!prevText.data)%20%7B%0A%20%20%20%20%20%20remove(prevText)%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A%0A%2F**%0A%20*%20remove(node)%20removes%20the%20given%20node%20from%20the%20DOM%20and%20returns%20the%0A%20*%20next%20node%20in%20the%20sequence.%0A%20*%0A%20*%20%40param%20%7BNode%7D%20node%0A%20*%20%40return%20%7BNode%7D%20node%0A%20*%2F%0Afunction%20remove(node)%20%7B%0A%20%20const%20next%20%3D%20node.nextSibling%20%7C%7C%20node.parentNode%3B%0A%0A%20%20node.parentNode.removeChild(node)%3B%0A%0A%20%20return%20next%3B%0A%7D%0A%0A%2F**%0A%20*%20next(prev%2C%20current%2C%20isPre)%20returns%20the%20next%20node%20in%20the%20sequence%2C%20given%20the%0A%20*%20current%20and%20previous%20nodes.%0A%20*%0A%20*%20%40param%20%7BNode%7D%20prev%0A%20*%20%40param%20%7BNode%7D%20current%0A%20*%20%40param%20%7BFunction%7D%20isPre%0A%20*%20%40return%20%7BNode%7D%0A%20*%2F%0Afunction%20next(prev%2C%20current%2C%20isPre)%20%7B%0A%20%20if%20((prev%20%26%26%20prev.parentNode%20%3D%3D%3D%20current)%20%7C%7C%20isPre(current))%20%7B%0A%20%20%20%20return%20current.nextSibling%20%7C%7C%20current.parentNode%3B%0A%20%20%7D%0A%0A%20%20return%20current.firstChild%20%7C%7C%20current.nextSibling%20%7C%7C%20current.parentNode%3B%0A%7D%0A%0A%2F*%0A%20*%20Set%20up%20window%20for%20Node.js%0A%20*%2F%0A%0Aconst%20root%20%3D%20typeof%20window%20!%3D%3D%20'undefined'%20%3F%20window%20%3A%20%7B%7D%3B%0A%0A%2F*%0A%20*%20Parsing%20HTML%20strings%0A%20*%2F%0A%0Afunction%20canParseHTMLNatively()%20%7B%0A%20%20const%20Parser%20%3D%20root.DOMParser%3B%0A%20%20let%20canParse%20%3D%20false%3B%0A%0A%20%20%2F%2F%20Adapted%20from%20https%3A%2F%2Fgist.github.com%2F1129031%0A%20%20%2F%2F%20Firefox%2FOpera%2FIE%20throw%20errors%20on%20unsupported%20types%0A%20%20try%20%7B%0A%20%20%20%20%2F%2F%20WebKit%20returns%20null%20on%20unsupported%20types%0A%20%20%20%20if%20(new%20Parser().parseFromString(''%2C%20'text%2Fhtml'))%20%7B%0A%20%20%20%20%20%20canParse%20%3D%20true%3B%0A%20%20%20%20%7D%0A%20%20%7D%20catch%20(e)%20%7B%7D%0A%0A%20%20return%20canParse%3B%0A%7D%0A%0Afunction%20createHTMLParser()%20%7B%0A%20%20const%20Parser%20%3D%20function%20()%20%7B%7D%3B%0A%0A%20%20%7B%0A%20%20%20%20const%20JSDOM%20%3D%20require('jsdom').JSDOM%3B%0A%20%20%20%20Parser.prototype.parseFromString%20%3D%20function%20(string)%20%7B%0A%20%20%20%20%20%20return%20new%20JSDOM(string).window.document%3B%0A%20%20%20%20%7D%3B%0A%20%20%7D%0A%20%20return%20Parser%3B%0A%7D%0A%0Aconst%20HTMLParser%20%3D%20canParseHTMLNatively()%20%3F%20root.DOMParser%20%3A%20createHTMLParser()%3B%0A%0Afunction%20RootNode(input)%20%7B%0A%20%20let%20root%3B%0A%20%20if%20(typeof%20input%20%3D%3D%3D%20'string')%20%7B%0A%20%20%20%20const%20doc%20%3D%20htmlParser().parseFromString(%0A%20%20%20%20%20%20%2F%2F%20DOM%20parsers%20arrange%20elements%20in%20the%20%3Chead%3E%20and%20%3Cbody%3E.%0A%20%20%20%20%20%20%2F%2F%20Wrapping%20in%20a%20custom%20element%20ensures%20elements%20are%20reliably%20arranged%20in%0A%20%20%20%20%20%20%2F%2F%20a%20single%20element.%0A%20%20%20%20%20%20'%3Cx-turndown%20id%3D%22turndown-root%22%3E'%20%2B%20input%20%2B%20'%3C%2Fx-turndown%3E'%2C%0A%20%20%20%20%20%20'text%2Fhtml'%2C%0A%20%20%20%20)%3B%0A%0A%20%20%20%20root%20%3D%20doc.getElementById('turndown-root')%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20root%20%3D%20input.cloneNode(true)%3B%0A%20%20%7D%0A%20%20collapseWhitespace(%7B%0A%20%20%20%20element%3A%20root%2C%0A%20%20%20%20isBlock%3A%20isBlock%2C%0A%20%20%20%20isVoid%3A%20isVoid%2C%0A%20%20%7D)%3B%0A%0A%20%20return%20root%3B%0A%7D%0A%0Alet%20_htmlParser%3B%0Afunction%20htmlParser()%20%7B%0A%20%20_htmlParser%20%3D%20_htmlParser%20%7C%7C%20new%20HTMLParser()%3B%0A%20%20return%20_htmlParser%3B%0A%7D%0A%0Afunction%20Node(node)%20%7B%0A%20%20node.isBlock%20%3D%20isBlock(node)%3B%0A%20%20node.isCode%20%3D%0A%20%20%20%20node.nodeName.toLowerCase()%20%3D%3D%3D%20'code'%20%7C%7C%20node.parentNode.isCode%3B%0A%20%20node.isBlank%20%3D%20isBlank(node)%3B%0A%20%20node.flankingWhitespace%20%3D%20flankingWhitespace(node)%3B%0A%20%20return%20node%3B%0A%7D%0A%0Afunction%20isBlank(node)%20%7B%0A%20%20return%20(%0A%20%20%20%20%5B'A'%2C%20'TH'%2C%20'TD'%2C%20'IFRAME'%2C%20'SCRIPT'%2C%20'AUDIO'%2C%20'VIDEO'%5D.indexOf(%0A%20%20%20%20%20%20node.nodeName%2C%0A%20%20%20%20)%20%3D%3D%3D%20-1%20%26%26%0A%20%20%20%20%2F%5E%5Cs*%24%2Fi.test(node.textContent)%20%26%26%0A%20%20%20%20!isVoid(node)%20%26%26%0A%20%20%20%20!hasVoid(node)%0A%20%20)%3B%0A%7D%0A%0Afunction%20flankingWhitespace(node)%20%7B%0A%20%20let%20leading%20%3D%20''%3B%0A%20%20let%20trailing%20%3D%20''%3B%0A%0A%20%20if%20(!node.isBlock)%20%7B%0A%20%20%20%20const%20hasLeading%20%3D%20%2F%5E%5Cs%2F.test(node.textContent)%3B%0A%20%20%20%20const%20hasTrailing%20%3D%20%2F%5Cs%24%2F.test(node.textContent)%3B%0A%20%20%20%20const%20blankWithSpaces%20%3D%20node.isBlank%20%26%26%20hasLeading%20%26%26%20hasTrailing%3B%0A%0A%20%20%20%20if%20(hasLeading%20%26%26%20!isFlankedByWhitespace('left'%2C%20node))%20%7B%0A%20%20%20%20%20%20leading%20%3D%20'%20'%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if%20(%0A%20%20%20%20%20%20!blankWithSpaces%20%26%26%0A%20%20%20%20%20%20hasTrailing%20%26%26%0A%20%20%20%20%20%20!isFlankedByWhitespace('right'%2C%20node)%0A%20%20%20%20)%20%7B%0A%20%20%20%20%20%20trailing%20%3D%20'%20'%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%0A%20%20return%20%7Bleading%3A%20leading%2C%20trailing%3A%20trailing%7D%3B%0A%7D%0A%0Afunction%20isFlankedByWhitespace(side%2C%20node)%20%7B%0A%20%20let%20sibling%3B%0A%20%20let%20regExp%3B%0A%20%20let%20isFlanked%3B%0A%0A%20%20if%20(side%20%3D%3D%3D%20'left')%20%7B%0A%20%20%20%20sibling%20%3D%20node.previousSibling%3B%0A%20%20%20%20regExp%20%3D%20%2F%20%24%2F%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20sibling%20%3D%20node.nextSibling%3B%0A%20%20%20%20regExp%20%3D%20%2F%5E%20%2F%3B%0A%20%20%7D%0A%0A%20%20if%20(sibling)%20%7B%0A%20%20%20%20if%20(sibling.nodeType%20%3D%3D%3D%203)%20%7B%0A%20%20%20%20%20%20isFlanked%20%3D%20regExp.test(sibling.nodeValue)%3B%0A%20%20%20%20%7D%20else%20if%20(sibling.nodeType%20%3D%3D%3D%201%20%26%26%20!isBlock(sibling))%20%7B%0A%20%20%20%20%20%20isFlanked%20%3D%20regExp.test(sibling.textContent)%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%20%20return%20isFlanked%3B%0A%7D%0A%0Aconst%20reduce%20%3D%20Array.prototype.reduce%3B%0Aconst%20leadingNewLinesRegExp%20%3D%20%2F%5E%5Cn*%2F%3B%0Aconst%20trailingNewLinesRegExp%20%3D%20%2F%5Cn*%24%2F%3B%0Aconst%20escapes%20%3D%20%5B%0A%20%20%5B%2F%5C%5C%2Fg%2C%20'%5C%5C%5C%5C'%5D%2C%0A%20%20%5B%2F%5C*%2Fg%2C%20'%5C%5C*'%5D%2C%0A%20%20%5B%2F%5E-%2Fg%2C%20'%5C%5C-'%5D%2C%0A%20%20%5B%2F%5E%5C%2B%20%2Fg%2C%20'%5C%5C%2B%20'%5D%2C%0A%20%20%5B%2F%5E(%3D%2B)%2Fg%2C%20'%5C%5C%241'%5D%2C%0A%20%20%5B%2F%5E(%23%7B1%2C6%7D)%20%2Fg%2C%20'%5C%5C%241%20'%5D%2C%0A%20%20%5B%2F%60%2Fg%2C%20'%5C%5C%60'%5D%2C%0A%20%20%5B%2F%5E~~~%2Fg%2C%20'%5C%5C~~~'%5D%2C%0A%20%20%5B%2F%5C%5B%2Fg%2C%20'%5C%5C%5B'%5D%2C%0A%20%20%5B%2F%5C%5D%2Fg%2C%20'%5C%5C%5D'%5D%2C%0A%20%20%5B%2F%5E%3E%2Fg%2C%20'%5C%5C%3E'%5D%2C%0A%20%20%5B%2F_%2Fg%2C%20'%5C%5C_'%5D%2C%0A%20%20%5B%2F%5E(%5Cd%2B)%5C.%20%2Fg%2C%20'%241%5C%5C.%20'%5D%2C%0A%5D%3B%0A%0Afunction%20TurndownService(options)%20%7B%0A%20%20if%20(!(this%20instanceof%20TurndownService))%20return%20new%20TurndownService(options)%3B%0A%0A%20%20const%20defaults%20%3D%20%7B%0A%20%20%20%20rules%3A%20rules%2C%0A%20%20%20%20headingStyle%3A%20'setext'%2C%0A%20%20%20%20hr%3A%20'*%20*%20*'%2C%0A%20%20%20%20bulletListMarker%3A%20'*'%2C%0A%20%20%20%20codeBlockStyle%3A%20'indented'%2C%0A%20%20%20%20fence%3A%20'%60%60%60'%2C%0A%20%20%20%20emDelimiter%3A%20'_'%2C%0A%20%20%20%20strongDelimiter%3A%20'**'%2C%0A%20%20%20%20linkStyle%3A%20'inlined'%2C%0A%20%20%20%20linkReferenceStyle%3A%20'full'%2C%0A%20%20%20%20br%3A%20'%20%20'%2C%0A%20%20%20%20blankReplacement%3A%20function%20(content%2C%20node)%20%7B%0A%20%20%20%20%20%20return%20node.isBlock%20%3F%20'%5Cn%5Cn'%20%3A%20''%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20keepReplacement%3A%20function%20(content%2C%20node)%20%7B%0A%20%20%20%20%20%20return%20node.isBlock%20%3F%20'%5Cn%5Cn'%20%2B%20node.outerHTML%20%2B%20'%5Cn%5Cn'%20%3A%20node.outerHTML%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20defaultReplacement%3A%20function%20(content%2C%20node)%20%7B%0A%20%20%20%20%20%20return%20node.isBlock%20%3F%20'%5Cn%5Cn'%20%2B%20content%20%2B%20'%5Cn%5Cn'%20%3A%20content%3B%0A%20%20%20%20%7D%2C%0A%20%20%7D%3B%0A%0A%20%20this.options%20%3D%20extend(%7B%7D%2C%20defaults%2C%20options)%3B%0A%20%20this.rules%20%3D%20new%20Rules(this.options)%3B%0A%7D%0A%0ATurndownService.prototype%20%3D%20%7B%0A%20%20%2F**%0A%20%20%20*%20The%20entry%20point%20for%20converting%20a%20string%20or%20DOM%20node%20to%20Markdown%0A%20%20%20*%20%40public%0A%20%20%20*%20%40param%20%7BString%7CHTMLElement%7D%20input%20The%20string%20or%20DOM%20node%20to%20convert%0A%20%20%20*%20%40returns%20A%20Markdown%20representation%20of%20the%20input%0A%20%20%20*%20%40type%20String%0A%20%20%20*%2F%0A%0A%20%20turndown%3A%20function%20(input)%20%7B%0A%20%20%20%20if%20(!canConvert(input))%20%7B%0A%20%20%20%20%20%20throw%20new%20TypeError(%0A%20%20%20%20%20%20%20%20input%20%2B%20'%20is%20not%20a%20string%2C%20or%20an%20element%2Fdocument%2Ffragment%20node.'%2C%0A%20%20%20%20%20%20)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if%20(input%20%3D%3D%3D%20'')%20return%20''%3B%0A%0A%20%20%20%20const%20output%20%3D%20process.call(this%2C%20new%20RootNode(input))%3B%0A%20%20%20%20return%20postProcess.call(this%2C%20output)%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Add%20one%20or%20more%20plugins%0A%20%20%20*%20%40public%0A%20%20%20*%20%40param%20%7BFunction%7CArray%7D%20plugin%20The%20plugin%20or%20array%20of%20plugins%20to%20add%0A%20%20%20*%20%40returns%20The%20Turndown%20instance%20for%20chaining%0A%20%20%20*%20%40type%20Object%0A%20%20%20*%2F%0A%0A%20%20use%3A%20function%20(plugin)%20%7B%0A%20%20%20%20if%20(Array.isArray(plugin))%20%7B%0A%20%20%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20plugin.length%3B%20i%2B%2B)%20this.use(plugin%5Bi%5D)%3B%0A%20%20%20%20%7D%20else%20if%20(typeof%20plugin%20%3D%3D%3D%20'function')%20%7B%0A%20%20%20%20%20%20plugin(this)%3B%0A%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20throw%20new%20TypeError('plugin%20must%20be%20a%20Function%20or%20an%20Array%20of%20Functions')%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20this%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Adds%20a%20rule%0A%20%20%20*%20%40public%0A%20%20%20*%20%40param%20%7BString%7D%20key%20The%20unique%20key%20of%20the%20rule%0A%20%20%20*%20%40param%20%7BObject%7D%20rule%20The%20rule%0A%20%20%20*%20%40returns%20The%20Turndown%20instance%20for%20chaining%0A%20%20%20*%20%40type%20Object%0A%20%20%20*%2F%0A%0A%20%20addRule%3A%20function%20(key%2C%20rule)%20%7B%0A%20%20%20%20this.rules.add(key%2C%20rule)%3B%0A%20%20%20%20return%20this%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Keep%20a%20node%20(as%20HTML)%20that%20matches%20the%20filter%0A%20%20%20*%20%40public%0A%20%20%20*%20%40param%20%7BString%7CArray%7CFunction%7D%20filter%20The%20unique%20key%20of%20the%20rule%0A%20%20%20*%20%40returns%20The%20Turndown%20instance%20for%20chaining%0A%20%20%20*%20%40type%20Object%0A%20%20%20*%2F%0A%0A%20%20keep%3A%20function%20(filter)%20%7B%0A%20%20%20%20this.rules.keep(filter)%3B%0A%20%20%20%20return%20this%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Remove%20a%20node%20that%20matches%20the%20filter%0A%20%20%20*%20%40public%0A%20%20%20*%20%40param%20%7BString%7CArray%7CFunction%7D%20filter%20The%20unique%20key%20of%20the%20rule%0A%20%20%20*%20%40returns%20The%20Turndown%20instance%20for%20chaining%0A%20%20%20*%20%40type%20Object%0A%20%20%20*%2F%0A%0A%20%20remove%3A%20function%20(filter)%20%7B%0A%20%20%20%20this.rules.remove(filter)%3B%0A%20%20%20%20return%20this%3B%0A%20%20%7D%2C%0A%0A%20%20%2F**%0A%20%20%20*%20Escapes%20Markdown%20syntax%0A%20%20%20*%20%40public%0A%20%20%20*%20%40param%20%7BString%7D%20string%20The%20string%20to%20escape%0A%20%20%20*%20%40returns%20A%20string%20with%20Markdown%20syntax%20escaped%0A%20%20%20*%20%40type%20String%0A%20%20%20*%2F%0A%0A%20%20escape%3A%20function%20(string)%20%7B%0A%20%20%20%20return%20escapes.reduce(function%20(accumulator%2C%20escape)%20%7B%0A%20%20%20%20%20%20return%20accumulator.replace(escape%5B0%5D%2C%20escape%5B1%5D)%3B%0A%20%20%20%20%7D%2C%20string)%3B%0A%20%20%7D%2C%0A%7D%3B%0A%0A%2F**%0A%20*%20Reduces%20a%20DOM%20node%20down%20to%20its%20Markdown%20string%20equivalent%0A%20*%20%40private%0A%20*%20%40param%20%7BHTMLElement%7D%20parentNode%20The%20node%20to%20convert%0A%20*%20%40returns%20A%20Markdown%20representation%20of%20the%20node%0A%20*%20%40type%20String%0A%20*%2F%0A%0Afunction%20process(parentNode)%20%7B%0A%20%20const%20self%20%3D%20this%3B%0A%20%20return%20reduce.call(%0A%20%20%20%20parentNode.childNodes%2C%0A%20%20%20%20function%20(output%2C%20node)%20%7B%0A%20%20%20%20%20%20node%20%3D%20new%20Node(node)%3B%0A%0A%20%20%20%20%20%20let%20replacement%20%3D%20''%3B%0A%20%20%20%20%20%20if%20(node.nodeType%20%3D%3D%3D%203)%20%7B%0A%20%20%20%20%20%20%20%20replacement%20%3D%20node.isCode%0A%20%20%20%20%20%20%20%20%20%20%3F%20node.nodeValue%0A%20%20%20%20%20%20%20%20%20%20%3A%20self.escape(node.nodeValue)%3B%0A%20%20%20%20%20%20%7D%20else%20if%20(node.nodeType%20%3D%3D%3D%201)%20%7B%0A%20%20%20%20%20%20%20%20replacement%20%3D%20replacementForNode.call(self%2C%20node)%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20return%20join(output%2C%20replacement)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20''%2C%0A%20%20)%3B%0A%7D%0A%0A%2F**%0A%20*%20Appends%20strings%20as%20each%20rule%20requires%20and%20trims%20the%20output%0A%20*%20%40private%0A%20*%20%40param%20%7BString%7D%20output%20The%20conversion%20output%0A%20*%20%40returns%20A%20trimmed%20version%20of%20the%20ouput%0A%20*%20%40type%20String%0A%20*%2F%0A%0Afunction%20postProcess(output)%20%7B%0A%20%20const%20self%20%3D%20this%3B%0A%20%20this.rules.forEach(function%20(rule)%20%7B%0A%20%20%20%20if%20(typeof%20rule.append%20%3D%3D%3D%20'function')%20%7B%0A%20%20%20%20%20%20output%20%3D%20join(output%2C%20rule.append(self.options))%3B%0A%20%20%20%20%7D%0A%20%20%7D)%3B%0A%0A%20%20return%20output.replace(%2F%5E%5B%5Ct%5Cr%5Cn%5D%2B%2F%2C%20'').replace(%2F%5B%5Ct%5Cr%5Cn%5Cs%5D%2B%24%2F%2C%20'')%3B%0A%7D%0A%0A%2F**%0A%20*%20Converts%20an%20element%20node%20to%20its%20Markdown%20equivalent%0A%20*%20%40private%0A%20*%20%40param%20%7BHTMLElement%7D%20node%20The%20node%20to%20convert%0A%20*%20%40returns%20A%20Markdown%20representation%20of%20the%20node%0A%20*%20%40type%20String%0A%20*%2F%0A%0Afunction%20replacementForNode(node)%20%7B%0A%20%20const%20rule%20%3D%20this.rules.forNode(node)%3B%0A%20%20let%20content%20%3D%20process.call(this%2C%20node)%3B%0A%20%20const%20whitespace%20%3D%20node.flankingWhitespace%3B%0A%20%20if%20(whitespace.leading%20%7C%7C%20whitespace.trailing)%20content%20%3D%20content.trim()%3B%0A%20%20return%20(%0A%20%20%20%20whitespace.leading%20%2B%0A%20%20%20%20rule.replacement(content%2C%20node%2C%20this.options)%20%2B%0A%20%20%20%20whitespace.trailing%0A%20%20)%3B%0A%7D%0A%0A%2F**%0A%20*%20Determines%20the%20new%20lines%20between%20the%20current%20output%20and%20the%20replacement%0A%20*%20%40private%0A%20*%20%40param%20%7BString%7D%20output%20The%20current%20conversion%20output%0A%20*%20%40param%20%7BString%7D%20replacement%20The%20string%20to%20append%20to%20the%20output%0A%20*%20%40returns%20The%20whitespace%20to%20separate%20the%20current%20output%20and%20the%20replacement%0A%20*%20%40type%20String%0A%20*%2F%0A%0Afunction%20separatingNewlines(output%2C%20replacement)%20%7B%0A%20%20const%20newlines%20%3D%20%5B%0A%20%20%20%20output.match(trailingNewLinesRegExp)%5B0%5D%2C%0A%20%20%20%20replacement.match(leadingNewLinesRegExp)%5B0%5D%2C%0A%20%20%5D.sort()%3B%0A%20%20const%20maxNewlines%20%3D%20newlines%5Bnewlines.length%20-%201%5D%3B%0A%20%20return%20maxNewlines.length%20%3C%202%20%3F%20maxNewlines%20%3A%20'%5Cn%5Cn'%3B%0A%7D%0A%0Afunction%20join(string1%2C%20string2)%20%7B%0A%20%20const%20separator%20%3D%20separatingNewlines(string1%2C%20string2)%3B%0A%0A%20%20%2F%2F%20Remove%20trailing%2Fleading%20newlines%20and%20replace%20with%20separator%0A%20%20string1%20%3D%20string1.replace(trailingNewLinesRegExp%2C%20'')%3B%0A%20%20string2%20%3D%20string2.replace(leadingNewLinesRegExp%2C%20'')%3B%0A%0A%20%20return%20string1%20%2B%20separator%20%2B%20string2%3B%0A%7D%0A%0A%2F**%0A%20*%20Determines%20whether%20an%20input%20can%20be%20converted%0A%20*%20%40private%0A%20*%20%40param%20%7BString%7CHTMLElement%7D%20input%20Describe%20this%20parameter%0A%20*%20%40returns%20Describe%20what%20it%20returns%0A%20*%20%40type%20String%7CObject%7CArray%7CBoolean%7CNumber%0A%20*%2F%0A%0Afunction%20canConvert(input)%20%7B%0A%20%20return%20(%0A%20%20%20%20input%20!%3D%20null%20%26%26%0A%20%20%20%20(typeof%20input%20%3D%3D%3D%20'string'%20%7C%7C%0A%20%20%20%20%20%20(input.nodeType%20%26%26%0A%20%20%20%20%20%20%20%20(input.nodeType%20%3D%3D%3D%201%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20input.nodeType%20%3D%3D%3D%209%20%7C%7C%0A%20%20%20%20%20%20%20%20%20%20input.nodeType%20%3D%3D%3D%2011)))%0A%20%20)%3B%0A%7D%0A%0A%2F%2F%2F%20CLIPPER%20ITSELF%0A%0A%2F*%20Parse%20the%20site's%20meta%20keywords%20content%20into%20tags%2C%20if%20present%20*%2F%0Aif%20(document.querySelector('meta%5Bname%3D%22keywords%22%20i%5D'))%20%7B%0A%20%20const%20keywords%20%3D%20document%0A%20%20%20%20.querySelector('meta%5Bname%3D%22keywords%22%20i%5D')%0A%20%20%20%20.getAttribute('content')%0A%20%20%20%20.split('%2C')%3B%0A%0A%20%20keywords.forEach(function%20(keyword)%20%7B%0A%20%20%20%20const%20tag%20%3D%20'%20'%20%2B%20keyword.split('%20').join('')%3B%0A%20%20%20%20tags%20%2B%3D%20tag%3B%0A%20%20%7D)%3B%0A%7D%0A%0Afunction%20getSelectionHtml()%20%7B%0A%20%20let%20html%20%3D%20''%3B%0A%20%20if%20(typeof%20window.getSelection%20!%3D%3D%20'undefined')%20%7B%0A%20%20%20%20const%20sel%20%3D%20window.getSelection()%3B%0A%20%20%20%20if%20(sel.rangeCount)%20%7B%0A%20%20%20%20%20%20const%20container%20%3D%20document.createElement('div')%3B%0A%20%20%20%20%20%20for%20(let%20i%20%3D%200%2C%20len%20%3D%20sel.rangeCount%3B%20i%20%3C%20len%3B%20%2B%2Bi)%20%7B%0A%20%20%20%20%20%20%20%20container.appendChild(sel.getRangeAt(i).cloneContents())%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20html%20%3D%20container.innerHTML%3B%0A%20%20%20%20%7D%0A%20%20%7D%20else%20if%20(typeof%20document.selection%20!%3D%3D%20'undefined')%20%7B%0A%20%20%20%20if%20(document.selection.type%20%3D%3D%20'Text')%20%7B%0A%20%20%20%20%20%20html%20%3D%20document.selection.createRange().htmlText%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%20%20return%20html%3B%0A%7D%0A%0Aconst%20selection%20%3D%20getSelectionHtml()%3B%0A%0Aconst%20%7Btitle%2C%20byline%2C%20content%7D%20%3D%20new%20Readability(%0A%20%20document.cloneNode(true)%2C%0A).parse()%3B%0A%0Afunction%20getFileName(fileName)%20%7B%0A%20%20const%20userAgent%20%3D%20window.navigator.userAgent%2C%0A%20%20%20%20platform%20%3D%20window.navigator.platform%2C%0A%20%20%20%20windowsPlatforms%20%3D%20%5B'Win32'%2C%20'Win64'%2C%20'Windows'%2C%20'WinCE'%5D%3B%0A%0A%20%20if%20(windowsPlatforms.indexOf(platform)%20!%3D%3D%20-1)%20%7B%0A%20%20%20%20fileName%20%3D%20fileName.replace('%3A'%2C%20'').replace(%2F%5B%2F%5C%5C%3F%25*%7C%22%3C%3E%5D%2Fg%2C%20'-')%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20fileName%20%3D%20fileName%0A%20%20%20%20%20%20.replace('%3A'%2C%20'')%0A%20%20%20%20%20%20.replace(%2F%5C%2F%2Fg%2C%20'-')%0A%20%20%20%20%20%20.replace(%2F%5C%5C%2Fg%2C%20'-')%3B%0A%20%20%7D%0A%20%20return%20fileName%3B%0A%7D%0Aconst%20fileName%20%3D%20getFileName(title)%3B%0A%0Aif%20(selection)%20%7B%0A%20%20var%20markdownify%20%3D%20selection%3B%0A%7D%20else%20%7B%0A%20%20var%20markdownify%20%3D%20content%3B%0A%7D%0A%0Aif%20(vault)%20%7B%0A%20%20var%20vaultName%20%3D%20'%26vault%3D'%20%2B%20encodeURIComponent(%60%24%7Bvault%7D%60)%3B%0A%7D%20else%20%7B%0A%20%20var%20vaultName%20%3D%20''%3B%0A%7D%0A%0Aconst%20markdownBody%20%3D%20new%20TurndownService(%7B%0A%20%20headingStyle%3A%20'atx'%2C%0A%20%20hr%3A%20'---'%2C%0A%20%20bulletListMarker%3A%20'-'%2C%0A%20%20codeBlockStyle%3A%20'fenced'%2C%0A%20%20emDelimiter%3A%20'*'%2C%0A%7D).turndown(markdownify)%3B%0A%0Avar%20date%20%3D%20new%20Date()%3B%0A%0Afunction%20convertDate(date)%20%7B%0A%20%20const%20yyyy%20%3D%20date.getFullYear().toString()%3B%0A%20%20const%20mm%20%3D%20(date.getMonth()%20%2B%201).toString()%3B%0A%20%20const%20dd%20%3D%20date.getDate().toString()%3B%0A%20%20const%20mmChars%20%3D%20mm.split('')%3B%0A%20%20const%20ddChars%20%3D%20dd.split('')%3B%0A%20%20return%20(%0A%20%20%20%20yyyy%20%2B%0A%20%20%20%20'-'%20%2B%0A%20%20%20%20(mmChars%5B1%5D%20%3F%20mm%20%3A%20'0'%20%2B%20mmChars%5B0%5D)%20%2B%0A%20%20%20%20'-'%20%2B%0A%20%20%20%20(ddChars%5B1%5D%20%3F%20dd%20%3A%20'0'%20%2B%20ddChars%5B0%5D)%0A%20%20)%3B%0A%7D%0A%0Aconst%20today%20%3D%20convertDate(date)%3B%0A%0A%2F%2F%20Utility%20function%20to%20get%20meta%20content%20by%20name%20or%20property%0Afunction%20getMetaContent(attr%2C%20value)%20%7B%0A%20%20const%20element%20%3D%20document.querySelector(%60meta%5B%24%7Battr%7D%3D'%24%7Bvalue%7D'%5D%60)%3B%0A%20%20return%20element%20%3F%20element.getAttribute('content').trim()%20%3A%20''%3B%0A%7D%0A%0A%2F%2F%20Fetch%20byline%2C%20meta%20author%2C%20property%20author%2C%20or%20site%20name%0Aconst%20author%20%3D%0A%20%20byline%20%7C%7C%0A%20%20getMetaContent('name'%2C%20'author')%20%7C%7C%0A%20%20getMetaContent('property'%2C%20'author')%20%7C%7C%0A%20%20getMetaContent('property'%2C%20'og%3Asite_name')%3B%0A%0A%2F%2F%20Check%20if%20there's%20an%20author%20and%20add%20brackets%0Aconst%20authorBrackets%20%3D%20author%20%3F%20%60%22%5B%5B%24%7Bauthor%7D%5D%5D%22%60%20%3A%20''%3B%0A%0A%2F*%20Try%20to%20get%20published%20date%20*%2F%0Aconst%20timeElement%20%3D%20document.querySelector('time')%3B%0Aconst%20publishedDate%20%3D%20timeElement%20%3F%20timeElement.getAttribute('datetime')%20%3A%20''%3B%0A%0Aif%20(publishedDate%20%26%26%20publishedDate.trim()%20!%3D%3D%20'')%20%7B%0A%20%20var%20date%20%3D%20new%20Date(publishedDate)%3B%0A%20%20const%20year%20%3D%20date.getFullYear()%3B%0A%20%20let%20month%20%3D%20date.getMonth()%20%2B%201%3B%20%2F%2F%20Months%20are%200-based%20in%20JavaScript%0A%20%20let%20day%20%3D%20date.getDate()%3B%0A%0A%20%20%2F%2F%20Pad%20month%20and%20day%20with%20leading%20zeros%20if%20necessary%0A%20%20month%20%3D%20month%20%3C%2010%20%3F%20'0'%20%2B%20month%20%3A%20month%3B%0A%20%20day%20%3D%20day%20%3C%2010%20%3F%20'0'%20%2B%20day%20%3A%20day%3B%0A%0A%20%20var%20published%20%3D%20year%20%2B%20'-'%20%2B%20month%20%2B%20'-'%20%2B%20day%3B%0A%7D%20else%20%7B%0A%20%20var%20published%20%3D%20''%3B%0A%7D%0A%0A%2F*%20YAML%20front%20matter%20as%20tags%20render%20cleaner%20with%20special%20chars%20%20*%2F%0Aconst%20fileContent%20%3D%0A%20%20'---%5Cn'%20%2B%0A%20%20'category%3A%20%22%5B%5BClippings%5D%5D%22%5Cn'%20%2B%0A%20%20'author%3A%20'%20%2B%0A%20%20authorBrackets%20%2B%0A%20%20'%5Cn'%20%2B%0A%20%20'title%3A%20%22'%20%2B%0A%20%20title%20%2B%0A%20%20'%22%5Cn'%20%2B%0A%20%20'source%3A%20'%20%2B%0A%20%20document.URL%20%2B%0A%20%20'%5Cn'%20%2B%0A%20%20'clipped%3A%20'%20%2B%0A%20%20today%20%2B%0A%20%20'%5Cn'%20%2B%0A%20%20'published%3A%20'%20%2B%0A%20%20published%20%2B%0A%20%20'%5Cn'%20%2B%0A%20%20'topics%3A%20%5Cn'%20%2B%0A%20%20'tags%3A%20%5B'%20%2B%0A%20%20tags%20%2B%0A%20%20'%5D%5Cn'%20%2B%0A%20%20'---%5Cn%5Cn'%20%2B%0A%20%20markdownBody%3B%0A%0Adocument.location.href%20%3D%0A%20%20'obsidian%3A%2F%2Fnew%3F'%20%2B%0A%20%20'file%3D'%20%2B%0A%20%20encodeURIComponent(folder%20%2B%20fileName)%20%2B%0A%20%20'%26content%3D'%20%2B%0A%20%20encodeURIComponent(fileContent)%20%2B%0A%20%20vaultName%3B%7D)()%3B

@ericraymond
Copy link

Here's the "source" that you can edit and paste into Bookmarklet Maker is below. The only trick is to change new Turndown( to new TurndownService(

/* Optional vault name */
const vault = '';

/* Optional folder name such as "Clippings/" */
const folder = 'Clippings/';

/* Optional tags  */
let tags = 'clippings';


/// Readability: https://unpkg.com/@tehshrike/readability@0.2.0
/*
 * Copyright (c) 2010 Arc90 Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * This code is heavily based on Arc90's readability.js (1.7.1) script
 * available at: http://code.google.com/p/arc90labs-readability
 */

/**
 * Public constructor.
 * @param {HTMLDocument} doc     The document to parse.
 * @param {Object}       options The options object.
 */
function Readability(doc, options) {
  // In some older versions, people passed a URI as the first argument. Cope:
  if (options && options.documentElement) {
    doc = options;
    options = arguments[2];
  } else if (!doc || !doc.documentElement) {
    throw new Error(
      'First argument to Readability constructor should be a document object.',
    );
  }
  options = options || {};

  this._doc = doc;
  this._docJSDOMParser = this._doc.firstChild.__JSDOMParser__;
  this._articleTitle = null;
  this._articleByline = null;
  this._articleDir = null;
  this._articleSiteName = null;
  this._attempts = [];

  // 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._charThreshold = options.charThreshold || this.DEFAULT_CHAR_THRESHOLD;
  this._classesToPreserve = this.CLASSES_TO_PRESERVE.concat(
    options.classesToPreserve || [],
  );
  this._keepClasses = !!options.keepClasses;

  // Start with all flags set
  this._flags =
    this.FLAG_STRIP_UNLIKELYS |
    this.FLAG_WEIGHT_CLASSES |
    this.FLAG_CLEAN_CONDITIONALLY;

  let logEl;

  // Control whether log messages are sent to the console
  if (this._debug) {
    logEl = function (e) {
      const rv = e.nodeName + ' ';
      if (e.nodeType == e.TEXT_NODE) {
        return rv + '("' + e.textContent + '")';
      }
      const classDesc = e.className && '.' + e.className.replace(/ /g, '.');
      let elDesc = '';
      if (e.id) {
        elDesc = '(#' + e.id + classDesc + ')';
      } else if (classDesc) {
        elDesc = '(' + classDesc + ')';
      }
      return rv + elDesc;
    };
    this.log = function () {
      if (typeof dump !== 'undefined') {
        const msg = Array.prototype.map
          .call(arguments, (x) => (x && x.nodeName ? logEl(x) : x))
          .join(' ');
        dump('Reader: (Readability) ' + msg + '\n');
      } else if (typeof console !== 'undefined') {
        const args = ['Reader: (Readability) '].concat(arguments);
        console.log.apply(console, args);
      }
    };
  } else {
    this.log = function () {};
  }
}

Readability.prototype = {
  FLAG_STRIP_UNLIKELYS: 0x1,
  FLAG_WEIGHT_CLASSES: 0x2,
  FLAG_CLEAN_CONDITIONALLY: 0x4,

  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
  ELEMENT_NODE: 1,
  TEXT_NODE: 3,

  // Max number of nodes supported by this parser. Default: 0 (no limit)
  DEFAULT_MAX_ELEMS_TO_PARSE: 0,

  // The number of top candidates to consider when analysing how
  // tight the competition is among candidates.
  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(','),

  // The default number of chars an article must have in order to return a result
  DEFAULT_CHAR_THRESHOLD: 500,

  // All of the regular expressions in use within readability.
  // Defined up here so we don't instantiate them repeatedly in loops.
  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,
    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:
      /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,
    shareElements: /(\b|_)(share|sharedaddy)(\b|_)/i,
    nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
    prevLink: /(prev|earl|old|new|<|«)/i,
    whitespace: /^\s*$/,
    hasContent: /\S$/,
    srcsetUrl: /(\S+)(\s+[\d.]+[xw])?(\s*(?:,|$))/g,
    b64DataUrl: /^data:\s*([^\s;,]+)\s*;\s*base64\s*,/i,
  },

  DIV_TO_P_ELEMS: [
    'A',
    'BLOCKQUOTE',
    'DL',
    'DIV',
    'IMG',
    'OL',
    'P',
    'PRE',
    'TABLE',
    'UL',
    'SELECT',
  ],

  ALTER_TO_DIV_EXCEPTIONS: ['DIV', 'ARTICLE', 'SECTION', 'P'],

  PRESENTATIONAL_ATTRIBUTES: [
    'align',
    'background',
    'bgcolor',
    'border',
    'cellpadding',
    'cellspacing',
    'frame',
    'hspace',
    'rules',
    'style',
    'valign',
    'vspace',
  ],

  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',
  ],

  // These are the classes that readability sets itself.
  CLASSES_TO_PRESERVE: ['page'],

  // These are the list of HTML entities that need to be escaped.
  HTML_ESCAPE_MAP: {
    lt: '<',
    gt: '>',
    amp: '&',
    quot: '"',
    apos: "'",
  },

  /**
   * Run any post-process modifications to article content as necessary.
   *
   * @param Element
   * @return void
   **/
  _postProcessContent(articleContent) {
    // Readability cannot open relative uris so we convert them to absolute uris.
    this._fixRelativeUris(articleContent);

    if (!this._keepClasses) {
      // Remove classes.
      this._cleanClasses(articleContent);
    }
  },

  /**
   * Iterates over a NodeList, calls `filterFn` for each node and removes node
   * if function returned `true`.
   *
   * If function is not passed, removes all the nodes in node list.
   *
   * @param NodeList nodeList The nodes to operate on
   * @param Function filterFn the function to use as a filter
   * @return void
   */
  _removeNodes(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');
    }
    for (let i = nodeList.length - 1; i >= 0; i--) {
      const node = nodeList[i];
      const parentNode = node.parentNode;
      if (parentNode) {
        if (!filterFn || filterFn.call(this, node, i, nodeList)) {
          parentNode.removeChild(node);
        }
      }
    }
  },

  /**
   * Iterates over a NodeList, and calls _setNodeTag for each node.
   *
   * @param NodeList nodeList The nodes to operate on
   * @param String newTagName the new tag name to use
   * @return void
   */
  _replaceNodeTags(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');
    }
    for (let i = nodeList.length - 1; i >= 0; i--) {
      const node = nodeList[i];
      this._setNodeTag(node, newTagName);
    }
  },

  /**
   * Iterate over a NodeList, which doesn't natively fully implement the Array
   * interface.
   *
   * For convenience, the current object context is applied to the provided
   * iterate function.
   *
   * @param  NodeList nodeList The NodeList.
   * @param  Function fn       The iterate function.
   * @return void
   */
  _forEachNode(nodeList, fn) {
    Array.prototype.forEach.call(nodeList, fn, this);
  },

  /**
   * Iterate over a NodeList, return true if any of the provided iterate
   * function calls returns true, false otherwise.
   *
   * For convenience, the current object context is applied to the
   * provided iterate function.
   *
   * @param  NodeList nodeList The NodeList.
   * @param  Function fn       The iterate function.
   * @return Boolean
   */
  _someNode(nodeList, fn) {
    return Array.prototype.some.call(nodeList, fn, this);
  },

  /**
   * Iterate over a NodeList, return true if all of the provided iterate
   * function calls return true, false otherwise.
   *
   * For convenience, the current object context is applied to the
   * provided iterate function.
   *
   * @param  NodeList nodeList The NodeList.
   * @param  Function fn       The iterate function.
   * @return Boolean
   */
  _everyNode(nodeList, fn) {
    return Array.prototype.every.call(nodeList, fn, this);
  },

  /**
   * Concat all nodelists passed as arguments.
   *
   * @return ...NodeList
   * @return Array
   */
  _concatNodeLists() {
    const slice = Array.prototype.slice;
    const args = slice.call(arguments);
    const nodeLists = args.map((list) => slice.call(list));
    return Array.prototype.concat.apply([], nodeLists);
  },

  _getAllNodesWithTag(node, tagNames) {
    if (node.querySelectorAll) {
      return node.querySelectorAll(tagNames.join(','));
    }
    return [].concat.apply(
      [],
      tagNames.map((tag) => {
        const collection = node.getElementsByTagName(tag);
        return Array.isArray(collection) ? collection : Array.from(collection);
      }),
    );
  },

  /**
   * Removes the class="" attribute from every element in the given
   * subtree, except those that match CLASSES_TO_PRESERVE and
   * the classesToPreserve array from the options object.
   *
   * @param Element
   * @return void
   */
  _cleanClasses(node) {
    const classesToPreserve = this._classesToPreserve;
    const className = (node.getAttribute('class') || '')
      .split(/\s+/)
      .filter((cls) => classesToPreserve.indexOf(cls) != -1)
      .join(' ');

    if (className) {
      node.setAttribute('class', className);
    } else {
      node.removeAttribute('class');
    }

    for (node = node.firstElementChild; node; node = node.nextElementSibling) {
      this._cleanClasses(node);
    }
  },

  /**
   * Converts each <a> and <img> uri in the given element to an absolute URI,
   * ignoring #ref URIs.
   *
   * @param Element
   * @return void
   */
  _fixRelativeUris(articleContent) {
    const baseURI = this._doc.baseURI;
    const documentURI = this._doc.documentURI;
    function toAbsoluteURI(uri) {
      // Leave hash links alone if the base URI matches the document URI:
      if (baseURI == documentURI && uri.charAt(0) == '#') {
        return uri;
      }

      // Otherwise, resolve against base URI:
      try {
        return new URL(uri, baseURI).href;
      } catch (ex) {
        // Something went wrong, just return the original:
      }
      return uri;
    }

    const links = this._getAllNodesWithTag(articleContent, ['a']);
    this._forEachNode(links, function (link) {
      const 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
          ) {
            const text = this._doc.createTextNode(link.textContent);
            link.parentNode.replaceChild(text, link);
          } else {
            // if the link has multiple children, they should all be preserved
            const container = this._doc.createElement('span');
            while (link.childNodes.length > 0) {
              container.appendChild(link.childNodes[0]);
            }
            link.parentNode.replaceChild(container, link);
          }
        } else {
          link.setAttribute('href', toAbsoluteURI(href));
        }
      }
    });

    const medias = this._getAllNodesWithTag(articleContent, [
      'img',
      'picture',
      'figure',
      'video',
      'audio',
      'source',
    ]);

    this._forEachNode(medias, function (media) {
      const src = media.getAttribute('src');
      const poster = media.getAttribute('poster');
      const srcset = media.getAttribute('srcset');

      if (src) {
        media.setAttribute('src', toAbsoluteURI(src));
      }

      if (poster) {
        media.setAttribute('poster', toAbsoluteURI(poster));
      }

      if (srcset) {
        const newSrcset = srcset.replace(
          this.REGEXPS.srcsetUrl,
          (_, p1, p2, p3) => toAbsoluteURI(p1) + (p2 || '') + p3,
        );

        media.setAttribute('srcset', newSrcset);
      }
    });
  },

  /**
   * Get the article title as an H1.
   *
   * @return void
   **/
  _getArticleTitle() {
    const doc = this._doc;
    let curTitle = '';
    let origTitle = '';

    try {
      curTitle = origTitle = doc.title.trim();

      // 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. */
    }

    let titleHadHierarchicalSeparators = false;
    function wordCount(str) {
      return str.split(/\s+/).length;
    }

    // If there's a separator in the title, first remove the final part
    if (/ [\|\-\\\/>»] /.test(curTitle)) {
      titleHadHierarchicalSeparators = / [\\\/>»] /.test(curTitle);
      curTitle = origTitle.replace(/(.*)[\|\-\\\/>»] .*/gi, '$1');

      // If the resulting title is too short (3 words or fewer), remove
      // the first part instead:
      if (wordCount(curTitle) < 3) {
        curTitle = origTitle.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi, '$1');
      }
    } else if (curTitle.indexOf(': ') !== -1) {
      // Check if we have an heading containing this exact string, so we
      // could assume it's the full title.
      const headings = this._concatNodeLists(
        doc.getElementsByTagName('h1'),
        doc.getElementsByTagName('h2'),
      );
      const trimmedTitle = curTitle.trim();
      const match = this._someNode(
        headings,
        (heading) => heading.textContent.trim() === trimmedTitle,
      );

      // If we don't, let's extract the title out of the original title string.
      if (!match) {
        curTitle = origTitle.substring(origTitle.lastIndexOf(':') + 1);

        // If the title is now too short, try the first colon instead:
        if (wordCount(curTitle) < 3) {
          curTitle = origTitle.substring(origTitle.indexOf(':') + 1);
          // But if we have too many words before the colon there's something weird
          // with the titles and the H tags so let's just use the original title instead
        } else if (wordCount(origTitle.substr(0, origTitle.indexOf(':'))) > 5) {
          curTitle = origTitle;
        }
      }
    } else if (curTitle.length > 150 || curTitle.length < 15) {
      const hOnes = doc.getElementsByTagName('h1');

      if (hOnes.length === 1) {
        curTitle = this._getInnerText(hOnes[0]);
      }
    }

    curTitle = curTitle.trim().replace(this.REGEXPS.normalize, ' ');
    // If we now have 4 words or fewer as our title, and either no
    // 'hierarchical' separators (\, /, > or ») were found in the original
    // title or we decreased the number of words by more than 1 word, use
    // the original title.
    const curTitleWordCount = wordCount(curTitle);
    if (
      curTitleWordCount <= 4 &&
      (!titleHadHierarchicalSeparators ||
        curTitleWordCount !=
        wordCount(origTitle.replace(/[\|\-\\\/>»]+/g, '')) - 1)
    ) {
      curTitle = origTitle;
    }

    return curTitle;
  },

  /**
   * Prepare the HTML document for readability to scrape it.
   * This includes things like stripping javascript, CSS, and handling terrible markup.
   *
   * @return void
   **/
  _prepDocument() {
    const doc = this._doc;

    // Remove all style tags in head
    this._removeNodes(this._getAllNodesWithTag(doc, ['style']));

    if (doc.body) {
      this._replaceBrs(doc.body);
    }

    this._replaceNodeTags(this._getAllNodesWithTag(doc, ['font']), 'SPAN');
  },

  /**
   * Finds the next element, starting from the given node, and ignoring
   * whitespace in between. If the given node is an element, the same node is
   * returned.
   */
  _nextElement(node) {
    let next = node;
    while (
      next &&
      next.nodeType != this.ELEMENT_NODE &&
      this.REGEXPS.whitespace.test(next.textContent)
      ) {
      next = next.nextSibling;
    }
    return next;
  },

  /**
   * Replaces 2 or more successive <br> elements with a single <p>.
   * Whitespace between <br> elements are ignored. For example:
   *   <div>foo<br>bar<br> <br><br>abc</div>
   * will become:
   *   <div>foo<br>bar<p>abc</p></div>
   */
  _replaceBrs(elem) {
    this._forEachNode(this._getAllNodesWithTag(elem, ['br']), function (br) {
      let next = br.nextSibling;

      // Whether 2 or more <br> elements have been found and replaced with a
      // <p> block.
      let replaced = false;

      // If we find a <br> chain, remove the <br>s until we hit another element
      // or non-whitespace. This leaves behind the first <br> in the chain
      // (which will be replaced with a <p> later).
      while ((next = this._nextElement(next)) && next.tagName == 'BR') {
        replaced = true;
        const brSibling = next.nextSibling;
        next.parentNode.removeChild(next);
        next = brSibling;
      }

      // If we removed a <br> chain, replace the remaining <br> with a <p>. Add
      // all sibling nodes as children of the <p> until we hit another <br>
      // chain.
      if (replaced) {
        const p = this._doc.createElement('p');
        br.parentNode.replaceChild(p, br);

        next = p.nextSibling;
        while (next) {
          // If we've hit another <br><br>, we're done adding children to this <p>.
          if (next.tagName == 'BR') {
            const nextElem = this._nextElement(next.nextSibling);
            if (nextElem && nextElem.tagName == 'BR') {
              break;
            }
          }

          if (!this._isPhrasingContent(next)) {
            break;
          }

          // Otherwise, make this node a child of the new <p>.
          const sibling = next.nextSibling;
          p.appendChild(next);
          next = sibling;
        }

        while (p.lastChild && this._isWhitespace(p.lastChild)) {
          p.removeChild(p.lastChild);
        }

        if (p.parentNode.tagName === 'P') {
          this._setNodeTag(p.parentNode, 'DIV');
        }
      }
    });
  },

  _setNodeTag(node, tag) {
    this.log('_setNodeTag', node, tag);
    if (this._docJSDOMParser) {
      node.localName = tag.toLowerCase();
      node.tagName = tag.toUpperCase();
      return node;
    }

    const replacement = node.ownerDocument.createElement(tag);
    while (node.firstChild) {
      replacement.appendChild(node.firstChild);
    }
    node.parentNode.replaceChild(replacement, node);
    if (node.readability) {
      replacement.readability = node.readability;
    }

    for (let i = 0; i < node.attributes.length; i++) {
      try {
        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
         * source in HTML docs, see https://github.com/whatwg/html/issues/4275,
         * so we can hit them here and then throw. We don't care about such
         * attributes so we ignore them.
         */
      }
    }
    return replacement;
  },

  /**
   * Prepare the article node for display. Clean out any inline styles,
   * iframes, forms, strip extraneous <p> tags, etc.
   *
   * @param Element
   * @return void
   **/
  _prepArticle(articleContent) {
    this._cleanStyles(articleContent);

    // Check for data tables before we continue, to avoid removing items in
    // those tables, which will often be isolated even though they're
    // visually linked to other content-ful elements (text, images, etc.).
    this._markDataTables(articleContent);

    this._fixLazyImages(articleContent);

    // Clean out junk from the article content
    this._cleanConditionally(articleContent, 'form');
    this._cleanConditionally(articleContent, 'fieldset');
    this._clean(articleContent, 'object');
    this._clean(articleContent, 'embed');
    this._clean(articleContent, 'h1');
    this._clean(articleContent, 'footer');
    this._clean(articleContent, 'link');
    this._clean(articleContent, 'aside');

    // Clean out elements with little content that have "share" in their id/class combinations from final top candidates,
    // which means we don't remove the top candidates even they have "share".

    const shareElementThreshold = this.DEFAULT_CHAR_THRESHOLD;

    this._forEachNode(articleContent.children, function (topCandidate) {
      this._cleanMatchedNodes(topCandidate, function (node, matchString) {
        return (
          this.REGEXPS.shareElements.test(matchString) &&
          node.textContent.length < shareElementThreshold
        );
      });
    });

    // 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.
    const h2 = articleContent.getElementsByTagName('h2');
    if (h2.length === 1) {
      const lengthSimilarRate =
        (h2[0].textContent.length - this._articleTitle.length) /
        this._articleTitle.length;
      if (Math.abs(lengthSimilarRate) < 0.5) {
        let 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, 'input');
    this._clean(articleContent, 'textarea');
    this._clean(articleContent, 'select');
    this._clean(articleContent, 'button');
    this._cleanHeaders(articleContent);

    // Do these last as the previous stuff may have removed junk
    // that will affect these
    this._cleanConditionally(articleContent, 'table');
    this._cleanConditionally(articleContent, 'ul');
    this._cleanConditionally(articleContent, 'div');

    // Remove extra paragraphs
    this._removeNodes(
      this._getAllNodesWithTag(articleContent, ['p']),
      function (paragraph) {
        const imgCount = paragraph.getElementsByTagName('img').length;
        const embedCount = paragraph.getElementsByTagName('embed').length;
        const objectCount = paragraph.getElementsByTagName('object').length;
        // At this point, nasty iframes have been removed, only remain embedded video ones.
        const iframeCount = paragraph.getElementsByTagName('iframe').length;
        const totalCount = imgCount + embedCount + objectCount + iframeCount;

        return totalCount === 0 && !this._getInnerText(paragraph, false);
      },
    );

    this._forEachNode(
      this._getAllNodesWithTag(articleContent, ['br']),
      function (br) {
        const next = this._nextElement(br.nextSibling);
        if (next && next.tagName == 'P') {
          br.parentNode.removeChild(br);
        }
      },
    );

    // Remove single-cell tables
    this._forEachNode(
      this._getAllNodesWithTag(articleContent, ['table']),
      function (table) {
        const tbody = this._hasSingleTagInsideElement(table, 'TBODY')
          ? table.firstElementChild
          : table;
        if (this._hasSingleTagInsideElement(tbody, 'TR')) {
          const row = tbody.firstElementChild;
          if (this._hasSingleTagInsideElement(row, 'TD')) {
            let cell = row.firstElementChild;
            cell = this._setNodeTag(
              cell,
              this._everyNode(cell.childNodes, this._isPhrasingContent)
                ? 'P'
                : 'DIV',
            );
            table.parentNode.replaceChild(cell, table);
          }
        }
      },
    );
  },

  /**
   * Initialize a node with the readability object. Also checks the
   * className/id for special names to add to its score.
   *
   * @param Element
   * @return void
   **/
  _initializeNode(node) {
    node.readability = {contentScore: 0};

    switch (node.tagName) {
      case 'DIV':
        node.readability.contentScore += 5;
        break;

      case 'PRE':
      case 'TD':
      case 'BLOCKQUOTE':
        node.readability.contentScore += 3;
        break;

      case 'ADDRESS':
      case 'OL':
      case 'UL':
      case 'DL':
      case 'DD':
      case 'DT':
      case 'LI':
      case 'FORM':
        node.readability.contentScore -= 3;
        break;

      case 'H1':
      case 'H2':
      case 'H3':
      case 'H4':
      case 'H5':
      case 'H6':
      case 'TH':
        node.readability.contentScore -= 5;
        break;
    }

    node.readability.contentScore += this._getClassWeight(node);
  },

  _removeAndGetNext(node) {
    const nextNode = this._getNextNode(node, true);
    node.parentNode.removeChild(node);
    return nextNode;
  },

  /**
   * Traverse the DOM from node to node, starting at the node passed in.
   * Pass true for the second parameter to indicate this node itself
   * (and its kids) are going away, and we want the next node over.
   *
   * Calling this in a loop will traverse the DOM depth-first.
   */
  _getNextNode(node, ignoreSelfAndKids) {
    // First check for kids if those aren't being ignored
    if (!ignoreSelfAndKids && node.firstElementChild) {
      return node.firstElementChild;
    }
    // Then for siblings...
    if (node.nextElementSibling) {
      return node.nextElementSibling;
    }
    // And finally, move up the parent chain *and* find a sibling
    // (because this is depth-first traversal, we will have already
    // seen the parent nodes themselves).
    do {
      node = node.parentNode;
    } while (node && !node.nextElementSibling);
    return node && node.nextElementSibling;
  },

  _checkByline(node, matchString) {
    if (this._articleByline) {
      return false;
    }

    if (node.getAttribute !== undefined) {
      var rel = node.getAttribute('rel');
      var itemprop = node.getAttribute('itemprop');
    }

    if (
      (rel === 'author' ||
        (itemprop && itemprop.indexOf('author') !== -1) ||
        this.REGEXPS.byline.test(matchString)) &&
      this._isValidByline(node.textContent)
    ) {
      this._articleByline = node.textContent.trim();
      return true;
    }

    return false;
  },

  _getNodeAncestors(node, maxDepth) {
    maxDepth = maxDepth || 0;
    let i = 0,
      ancestors = [];
    while (node.parentNode) {
      ancestors.push(node.parentNode);
      if (maxDepth && ++i === maxDepth) {
        break;
      }
      node = node.parentNode;
    }
    return ancestors;
  },

  /** *
   * grabArticle - Using a variety of metrics (content score, classname, element types), find the content that is
   *         most likely to be the stuff a user wants to read. Then return it wrapped up in a div.
   *
   * @param page a document to run upon. Needs to be a full document, complete with body.
   * @return Element
   **/
  _grabArticle(page) {
    this.log('**** grabArticle ****');
    const doc = this._doc;
    const isPaging = page !== null;
    page = page ? page : this._doc.body;

    // We can't grab an article if we don't have a page!
    if (!page) {
      this.log('No body found in document. Abort.');
      return null;
    }

    const pageCacheHtml = page.innerHTML;

    while (true) {
      const 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
      // used inappropriately (as in, where they contain no other block level elements.)
      const elementsToScore = [];
      let node = this._doc.documentElement;

      while (node) {
        const matchString = node.className + ' ' + node.id;

        if (!this._isProbablyVisible(node)) {
          this.log('Removing hidden node - ' + matchString);
          node = this._removeAndGetNext(node);
          continue;
        }

        // Check to see if this node is a byline, and remove it if it is.
        if (this._checkByline(node, matchString)) {
          node = this._removeAndGetNext(node);
          continue;
        }

        // Remove unlikely candidates
        if (stripUnlikelyCandidates) {
          if (
            this.REGEXPS.unlikelyCandidates.test(matchString) &&
            !this.REGEXPS.okMaybeItsACandidate.test(matchString) &&
            !this._hasAncestorTag(node, 'table') &&
            node.tagName !== 'BODY' &&
            node.tagName !== 'A'
          ) {
            this.log('Removing unlikely candidate - ' + matchString);
            node = this._removeAndGetNext(node);
            continue;
          }

          if (node.getAttribute('role') == 'complementary') {
            this.log('Removing complementary content - ' + 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)
        ) {
          node = this._removeAndGetNext(node);
          continue;
        }

        if (this.DEFAULT_TAGS_TO_SCORE.indexOf(node.tagName) !== -1) {
          elementsToScore.push(node);
        }

        // Turn all divs that don't have children block level elements into p's
        if (node.tagName === 'DIV') {
          // Put phrasing content into paragraphs.
          let p = null;
          let childNode = node.firstChild;
          while (childNode) {
            const nextSibling = childNode.nextSibling;
            if (this._isPhrasingContent(childNode)) {
              if (p !== null) {
                p.appendChild(childNode);
              } else if (!this._isWhitespace(childNode)) {
                p = doc.createElement('p');
                node.replaceChild(p, childNode);
                p.appendChild(childNode);
              }
            } else if (p !== null) {
              while (p.lastChild && this._isWhitespace(p.lastChild)) {
                p.removeChild(p.lastChild);
              }
              p = null;
            }
            childNode = nextSibling;
          }

          // Sites like http://mobile.slate.com encloses each paragraph with a DIV
          // 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
          ) {
            const newNode = node.children[0];
            node.parentNode.replaceChild(newNode, node);
            node = newNode;
            elementsToScore.push(node);
          } else if (!this._hasChildBlockElement(node)) {
            node = this._setNodeTag(node, 'P');
            elementsToScore.push(node);
          }
        }
        node = this._getNextNode(node);
      }

      /**
       * Loop through all paragraphs, and assign a score to them based on how content-y they look.
       * 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'
        ) {
          return;
        }

        // If this paragraph is less than 25 characters, don't even count it.
        const innerText = this._getInnerText(elementToScore);
        if (innerText.length < 25) {
          return;
        }

        // Exclude nodes with no ancestor.
        const ancestors = this._getNodeAncestors(elementToScore, 3);
        if (ancestors.length === 0) {
          return;
        }

        let contentScore = 0;

        // Add a point for the paragraph itself as a base.
        contentScore += 1;

        // Add points for any commas within this paragraph.
        contentScore += innerText.split(',').length;

        // For every 100 characters in this paragraph, add another point. Up to 3 points.
        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'
          ) {
            return;
          }

          if (typeof ancestor.readability === 'undefined') {
            this._initializeNode(ancestor);
            candidates.push(ancestor);
          }

          // Node score divider:
          // - 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;
          }
          ancestor.readability.contentScore += contentScore / scoreDivider;
        });
      });

      // After we've calculated scores, loop through all of the possible
      // candidate nodes we found and find the one with the highest score.
      const topCandidates = [];
      for (let c = 0, cl = candidates.length; c < cl; c += 1) {
        const candidate = candidates[c];

        // 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.
        const candidateScore =
          candidate.readability.contentScore *
          (1 - this._getLinkDensity(candidate));
        candidate.readability.contentScore = candidateScore;

        this.log('Candidate:', candidate, 'with score ' + candidateScore);

        for (let t = 0; t < this._nbTopCandidates; t++) {
          const aTopCandidate = topCandidates[t];

          if (
            !aTopCandidate ||
            candidateScore > aTopCandidate.readability.contentScore
          ) {
            topCandidates.splice(t, 0, candidate);
            if (topCandidates.length > this._nbTopCandidates) {
              topCandidates.pop();
            }
            break;
          }
        }
      }

      let topCandidate = topCandidates[0] || null;
      let neededToCreateTopCandidate = false;
      var parentOfTopCandidate;

      // If we still have no top candidate, just use the body as a last resort.
      // We also have to copy the body node so it is something we can modify.
      if (topCandidate === null || topCandidate.tagName === 'BODY') {
        // Move all of the page's children into topCandidate
        topCandidate = doc.createElement('DIV');
        neededToCreateTopCandidate = true;
        // Move everything (not just elements, also text nodes etc.) into the container
        // so we even include text directly in the body:
        const kids = page.childNodes;
        while (kids.length) {
          this.log('Moving child out:', kids[0]);
          topCandidate.appendChild(kids[0]);
        }

        page.appendChild(topCandidate);

        this._initializeNode(topCandidate);
      } else if (topCandidate) {
        // Find a better top candidate node if it contains (at least three) nodes which belong to `topCandidates` array
        // and whose scores are quite closed with current `topCandidate` node.
        const alternativeCandidateAncestors = [];
        for (let i = 1; i < topCandidates.length; i++) {
          if (
            topCandidates[i].readability.contentScore /
            topCandidate.readability.contentScore >=
            0.75
          ) {
            alternativeCandidateAncestors.push(
              this._getNodeAncestors(topCandidates[i]),
            );
          }
        }
        const MINIMUM_TOPCANDIDATES = 3;
        if (alternativeCandidateAncestors.length >= MINIMUM_TOPCANDIDATES) {
          parentOfTopCandidate = topCandidate.parentNode;
          while (parentOfTopCandidate.tagName !== 'BODY') {
            let listsContainingThisAncestor = 0;
            for (
              let ancestorIndex = 0;
              ancestorIndex < alternativeCandidateAncestors.length &&
              listsContainingThisAncestor < MINIMUM_TOPCANDIDATES;
              ancestorIndex++
            ) {
              listsContainingThisAncestor += Number(
                alternativeCandidateAncestors[ancestorIndex].includes(
                  parentOfTopCandidate,
                ),
              );
            }
            if (listsContainingThisAncestor >= MINIMUM_TOPCANDIDATES) {
              topCandidate = parentOfTopCandidate;
              break;
            }
            parentOfTopCandidate = parentOfTopCandidate.parentNode;
          }
        }
        if (!topCandidate.readability) {
          this._initializeNode(topCandidate);
        }

        // Because of our bonus system, parents of candidates might have scores
        // themselves. They get half of the node. There won't be nodes with higher
        // scores than our topCandidate, but if we see the score going *up* in the first
        // few steps up the tree, that's a decent sign that there might be more content
        // lurking in other places that we want to unify in. The sibling stuff
        // below does some of that - but only if we've looked high enough up the DOM
        // tree.
        parentOfTopCandidate = topCandidate.parentNode;
        let lastScore = topCandidate.readability.contentScore;
        // The scores shouldn't get too low.
        const scoreThreshold = lastScore / 3;
        while (parentOfTopCandidate.tagName !== 'BODY') {
          if (!parentOfTopCandidate.readability) {
            parentOfTopCandidate = parentOfTopCandidate.parentNode;
            continue;
          }
          const parentScore = parentOfTopCandidate.readability.contentScore;
          if (parentScore < scoreThreshold) {
            break;
          }
          if (parentScore > lastScore) {
            // Alright! We found a better parent to use.
            topCandidate = parentOfTopCandidate;
            break;
          }
          lastScore = parentOfTopCandidate.readability.contentScore;
          parentOfTopCandidate = parentOfTopCandidate.parentNode;
        }

        // 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
          ) {
          topCandidate = parentOfTopCandidate;
          parentOfTopCandidate = topCandidate.parentNode;
        }
        if (!topCandidate.readability) {
          this._initializeNode(topCandidate);
        }
      }

      // Now that we have the top candidate, look through its siblings for content
      // that might also be related. Things like preambles, content split by ads
      // that we removed, etc.
      let articleContent = doc.createElement('DIV');
      if (isPaging) {
        articleContent.id = 'readability-content';
      }

      const 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;
      const siblings = parentOfTopCandidate.children;

      for (let s = 0, sl = siblings.length; s < sl; s++) {
        let sibling = siblings[s];
        let 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',
        );

        if (sibling === topCandidate) {
          append = true;
        } else {
          let contentBonus = 0;

          // Give a bonus if sibling nodes and top candidates have the example same classname
          if (
            sibling.className === topCandidate.className &&
            topCandidate.className !== ''
          ) {
            contentBonus += topCandidate.readability.contentScore * 0.2;
          }

          if (
            sibling.readability &&
            sibling.readability.contentScore + contentBonus >=
            siblingScoreThreshold
          ) {
            append = true;
          } else if (sibling.nodeName === 'P') {
            const linkDensity = this._getLinkDensity(sibling);
            const nodeContent = this._getInnerText(sibling);
            const nodeLength = nodeContent.length;

            if (nodeLength > 80 && linkDensity < 0.25) {
              append = true;
            } else if (
              nodeLength < 80 &&
              nodeLength > 0 &&
              linkDensity === 0 &&
              nodeContent.search(/\.( |$)/) !== -1
            ) {
              append = true;
            }
          }
        }

        if (append) {
          this.log('Appending node:', sibling);

          if (this.ALTER_TO_DIV_EXCEPTIONS.indexOf(sibling.nodeName) === -1) {
            // We have a node that isn't a common block level element, like a form or td tag.
            // Turn it into a div so it doesn't get filtered out later by accident.
            this.log('Altering sibling:', sibling, 'to div.');

            sibling = this._setNodeTag(sibling, 'DIV');
          }

          articleContent.appendChild(sibling);
          // siblings is a reference to the children array, and
          // sibling is removed from the array when we call appendChild().
          // As a result, we must revisit this index since the nodes
          // have been shifted.
          s -= 1;
          sl -= 1;
        }
      }

      if (this._debug) {
        this.log('Article content pre-prep: ' + articleContent.innerHTML);
      }
      // So we have all of the content that we need. Now we clean it up for presentation.
      this._prepArticle(articleContent);
      if (this._debug) {
        this.log('Article content post-prep: ' + articleContent.innerHTML);
      }

      if (neededToCreateTopCandidate) {
        // We already created a fake div thing, and there wouldn't have been any siblings left
        // for the previous loop, so there's no point trying to create a new div, and then
        // move all the children over. Just assign IDs and class names here. No need to append
        // because that already happened anyway.
        topCandidate.id = 'readability-page-1';
        topCandidate.className = 'page';
      } else {
        const div = doc.createElement('DIV');
        div.id = 'readability-page-1';
        div.className = 'page';
        const children = articleContent.childNodes;
        while (children.length) {
          div.appendChild(children[0]);
        }
        articleContent.appendChild(div);
      }

      if (this._debug) {
        this.log('Article content after paging: ' + articleContent.innerHTML);
      }

      let parseSuccessful = true;

      // Now that we've gone through the full algorithm, check to see if
      // we got any meaningful content. If we didn't, we may need to re-run
      // grabArticle with different flags set. This gives us a higher likelihood of
      // finding the content, and the sieve approach gives us a higher likelihood of
      // finding the -right- content.
      const textLength = this._getInnerText(articleContent, true).length;
      if (textLength < this._charThreshold) {
        parseSuccessful = false;
        page.innerHTML = pageCacheHtml;

        if (this._flagIsActive(this.FLAG_STRIP_UNLIKELYS)) {
          this._removeFlag(this.FLAG_STRIP_UNLIKELYS);
          this._attempts.push({articleContent, textLength});
        } else if (this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) {
          this._removeFlag(this.FLAG_WEIGHT_CLASSES);
          this._attempts.push({articleContent, textLength});
        } else if (this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)) {
          this._removeFlag(this.FLAG_CLEAN_CONDITIONALLY);
          this._attempts.push({articleContent, textLength});
        } else {
          this._attempts.push({articleContent, textLength});
          // No luck after removing flags, just return the longest text we found during the different loops
          this._attempts.sort((a, b) => b.textLength - a.textLength);

          // But first check if we actually have something
          if (!this._attempts[0].textLength) {
            return null;
          }

          articleContent = this._attempts[0].articleContent;
          parseSuccessful = true;
        }
      }

      if (parseSuccessful) {
        // Find out text direction from ancestors of final top candidate.
        const ancestors = [parentOfTopCandidate, topCandidate].concat(
          this._getNodeAncestors(parentOfTopCandidate),
        );
        this._someNode(ancestors, function (ancestor) {
          if (!ancestor.tagName) {
            return false;
          }
          const articleDir = ancestor.getAttribute('dir');
          if (articleDir) {
            this._articleDir = articleDir;
            return true;
          }
          return false;
        });
        return articleContent;
      }
    }
  },

  /**
   * Check whether the input string could be a byline.
   * This verifies that the input is a string, and that the length
   * is less than 100 chars.
   *
   * @param possibleByline {string} - a string to check whether its a byline.
   * @return Boolean - whether the input string is a byline.
   */
  _isValidByline(byline) {
    if (typeof byline === 'string' || byline instanceof String) {
      byline = byline.trim();
      return byline.length > 0 && byline.length < 100;
    }
    return false;
  },

  /**
   * Converts some of the common HTML entities in string to their corresponding characters.
   *
   * @param str {string} - a string to unescape.
   * @return string without HTML entity.
   */
  _unescapeHtmlEntities(str) {
    if (!str) {
      return str;
    }

    const htmlEscapeMap = this.HTML_ESCAPE_MAP;
    return str
      .replace(/&(quot|amp|apos|lt|gt);/g, (_, tag) => htmlEscapeMap[tag])
      .replace(/&#(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi, (_, hex, numStr) => {
        const num = parseInt(hex || numStr, hex ? 16 : 10);
        return String.fromCharCode(num);
      });
  },

  /**
   * Attempts to get excerpt and byline metadata for the article.
   *
   * @return Object with optional "excerpt" and "byline" properties
   */
  _getArticleMetadata() {
    const metadata = {};
    const values = {};
    const metaElements = this._doc.getElementsByTagName('meta');

    // property is a space-separated list of values
    const propertyPattern =
      /\s*(dc|dcterm|og|twitter)\s*:\s*(author|creator|description|title|site_name)\s*/gi;

    // name is a single value
    const 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, (element) => {
      const elementName = element.getAttribute('name');
      const elementProperty = element.getAttribute('property');
      const content = element.getAttribute('content');
      if (!content) {
        return;
      }
      let matches = null;
      let name = null;

      if (elementProperty) {
        matches = elementProperty.match(propertyPattern);
        if (matches) {
          for (let 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();
          }
        }
      }
      if (!matches && elementName && namePattern.test(elementName)) {
        name = elementName;
        if (content) {
          // Convert to lowercase, remove any whitespace, and convert dots
          // to colons so we can match below.
          name = name.toLowerCase().replace(/\s/g, '').replace(/\./g, ':');
          values[name] = content.trim();
        }
      }
    });

    // get title
    metadata.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 =
      values['dc:creator'] || values['dcterm:creator'] || values.author;

    // get description
    metadata.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 = values['og:site_name'];

    // in many sites the meta value is escaped with HTML entities,
    // so here we need to unescape it
    metadata.title = this._unescapeHtmlEntities(metadata.title);
    metadata.byline = this._unescapeHtmlEntities(metadata.byline);
    metadata.excerpt = this._unescapeHtmlEntities(metadata.excerpt);
    metadata.siteName = this._unescapeHtmlEntities(metadata.siteName);

    return metadata;
  },

  /**
   * Check if node is image, or if node contains exactly only one image
   * whether as a direct child or as its descendants.
   *
   * @param Element
   **/
  _isSingleImage(node) {
    if (node.tagName === 'IMG') {
      return true;
    }

    if (node.children.length !== 1 || node.textContent.trim() !== '') {
      return false;
    }

    return this._isSingleImage(node.children[0]);
  },

  /**
   * Find all <noscript> that are located after <img> nodes, and which contain only one
   * <img> element. Replace the first image with the image from inside the <noscript> tag,
   * and remove the <noscript> tag. This improves the quality of the images we use on
   * some sites (e.g. Medium).
   *
   * @param Element
   **/
  _unwrapNoscriptImages(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.
    const imgs = Array.from(doc.getElementsByTagName('img'));
    this._forEachNode(imgs, (img) => {
      for (let i = 0; i < img.attributes.length; i++) {
        const attr = img.attributes[i];
        switch (attr.name) {
          case 'src':
          case 'srcset':
          case 'data-src':
          case 'data-srcset':
            return;
        }

        if (/\.(jpg|jpeg|png|webp)/i.test(attr.value)) {
          return;
        }
      }

      img.parentNode.removeChild(img);
    });

    // Next find noscript and try to extract its image
    const noscripts = Array.from(doc.getElementsByTagName('noscript'));
    this._forEachNode(noscripts, function (noscript) {
      // Parse content of noscript and make sure it only contains image
      const tmp = doc.createElement('div');
      tmp.innerHTML = noscript.innerHTML;
      if (!this._isSingleImage(tmp)) {
        return;
      }

      // If noscript has previous sibling and it only contains image,
      // replace it with noscript content. However we also keep old
      // attributes that might contains image.
      const prevElement = noscript.previousElementSibling;
      if (prevElement && this._isSingleImage(prevElement)) {
        let prevImg = prevElement;
        if (prevImg.tagName !== 'IMG') {
          prevImg = prevElement.getElementsByTagName('img')[0];
        }

        const newImg = tmp.getElementsByTagName('img')[0];
        for (let i = 0; i < prevImg.attributes.length; i++) {
          const attr = prevImg.attributes[i];
          if (attr.value === '') {
            continue;
          }

          if (
            attr.name === 'src' ||
            attr.name === 'srcset' ||
            /\.(jpg|jpeg|png|webp)/i.test(attr.value)
          ) {
            if (newImg.getAttribute(attr.name) === attr.value) {
              continue;
            }

            let attrName = attr.name;
            if (newImg.hasAttribute(attrName)) {
              attrName = 'data-old-' + attrName;
            }

            newImg.setAttribute(attrName, attr.value);
          }
        }

        noscript.parentNode.replaceChild(tmp.firstElementChild, prevElement);
      }
    });
  },

  /**
   * Removes script tags from the document.
   *
   * @param Element
   **/
  _removeScripts(doc) {
    this._removeNodes(
      this._getAllNodesWithTag(doc, ['script']),
      (scriptNode) => {
        scriptNode.nodeValue = '';
        scriptNode.removeAttribute('src');
        return true;
      },
    );
    this._removeNodes(this._getAllNodesWithTag(doc, ['noscript']));
  },

  /**
   * Check if this node has only whitespace and a single element with given tag
   * Returns false if the DIV node contains non-empty text nodes
   * or if it contains no element with given tag or more than 1 element.
   *
   * @param Element
   * @param string tag of child element
   **/
  _hasSingleTagInsideElement(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)
      );
    });
  },

  _isElementWithoutContent(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)
    );
  },

  /**
   * Determine whether element has any children block level elements.
   *
   * @param Element
   */
  _hasChildBlockElement(element) {
    return this._someNode(element.childNodes, function (node) {
      return (
        this.DIV_TO_P_ELEMS.indexOf(node.tagName) !== -1 ||
        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(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(node) {
    return (
      (node.nodeType === this.TEXT_NODE &&
        node.textContent.trim().length === 0) ||
      (node.nodeType === this.ELEMENT_NODE && node.tagName === 'BR')
    );
  },

  /**
   * Get the inner text of a node - cross browser compatibly.
   * This also strips out any excess whitespace to be found.
   *
   * @param Element
   * @param Boolean normalizeSpaces (default: true)
   * @return string
   **/
  _getInnerText(e, normalizeSpaces) {
    normalizeSpaces =
      typeof normalizeSpaces === 'undefined' ? true : normalizeSpaces;
    const textContent = e.textContent.trim();

    if (normalizeSpaces) {
      return textContent.replace(this.REGEXPS.normalize, ' ');
    }
    return textContent;
  },

  /**
   * Get the number of times a string s appears in the node e.
   *
   * @param Element
   * @param string - what to split on. Default is ","
   * @return number (integer)
   **/
  _getCharCount(e, s) {
    s = s || ',';
    return this._getInnerText(e).split(s).length - 1;
  },

  /**
   * Remove the style attribute on every e and under.
   * TODO: Test if getElementsByTagName(*) is faster.
   *
   * @param Element
   * @return void
   **/
  _cleanStyles(e) {
    if (!e || e.tagName.toLowerCase() === 'svg') {
      return;
    }

    // Remove `style` and deprecated presentational attributes
    for (let i = 0; i < this.PRESENTATIONAL_ATTRIBUTES.length; i++) {
      e.removeAttribute(this.PRESENTATIONAL_ATTRIBUTES[i]);
    }

    if (this.DEPRECATED_SIZE_ATTRIBUTE_ELEMS.indexOf(e.tagName) !== -1) {
      e.removeAttribute('width');
      e.removeAttribute('height');
    }

    let cur = e.firstElementChild;
    while (cur !== null) {
      this._cleanStyles(cur);
      cur = cur.nextElementSibling;
    }
  },

  /**
   * Get the density of links as a percentage of the content
   * This is the amount of text that is inside a link divided by the total text in the node.
   *
   * @param Element
   * @return number (float)
   **/
  _getLinkDensity(element) {
    const textLength = this._getInnerText(element).length;
    if (textLength === 0) {
      return 0;
    }

    let linkLength = 0;

    // XXX implement _reduceNodeList?
    this._forEachNode(element.getElementsByTagName('a'), function (linkNode) {
      linkLength += this._getInnerText(linkNode).length;
    });

    return linkLength / textLength;
  },

  /**
   * Get an elements class/id weight. Uses regular expressions to tell if this
   * element looks good or bad.
   *
   * @param Element
   * @return number (Integer)
   **/
  _getClassWeight(e) {
    if (!this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) {
      return 0;
    }

    let weight = 0;

    // Look for a special classname
    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;
      }
    }

    // Look for a special ID
    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;
      }
    }

    return weight;
  },

  /**
   * Clean a node of all elements of type "tag".
   * (Unless it's a youtube/vimeo video. People love movies.)
   *
   * @param Element
   * @param string tag to clean
   * @return void
   **/
  _clean(e, tag) {
    const isEmbed = ['object', 'embed', 'iframe'].indexOf(tag) !== -1;

    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
        for (let i = 0; i < element.attributes.length; i++) {
          if (this.REGEXPS.videos.test(element.attributes[i].value)) {
            return false;
          }
        }

        // For embed with <object> tag, check inner HTML as well.
        if (
          element.tagName === 'object' &&
          this.REGEXPS.videos.test(element.innerHTML)
        ) {
          return false;
        }
      }

      return true;
    });
  },

  /**
   * Check if a given node has one of its ancestor tag name matching the
   * provided one.
   * @param  HTMLElement node
   * @param  String      tagName
   * @param  Number      maxDepth
   * @param  Function    filterFn a filter to invoke to determine whether this node 'counts'
   * @return Boolean
   */
  _hasAncestorTag(node, tagName, maxDepth, filterFn) {
    maxDepth = maxDepth || 3;
    tagName = tagName.toUpperCase();
    let depth = 0;
    while (node.parentNode) {
      if (maxDepth > 0 && depth > maxDepth) {
        return false;
      }
      if (
        node.parentNode.tagName === tagName &&
        (!filterFn || filterFn(node.parentNode))
      ) {
        return true;
      }
      node = node.parentNode;
      depth++;
    }
    return false;
  },

  /**
   * Return an object indicating how many rows and columns this table has.
   */
  _getRowAndColumnCount(table) {
    let rows = 0;
    let columns = 0;
    const trs = table.getElementsByTagName('tr');
    for (let i = 0; i < trs.length; i++) {
      let rowspan = trs[i].getAttribute('rowspan') || 0;
      if (rowspan) {
        rowspan = parseInt(rowspan, 10);
      }
      rows += rowspan || 1;

      // Now look for column-related info
      let columnsInThisRow = 0;
      const cells = trs[i].getElementsByTagName('td');
      for (let j = 0; j < cells.length; j++) {
        let colspan = cells[j].getAttribute('colspan') || 0;
        if (colspan) {
          colspan = parseInt(colspan, 10);
        }
        columnsInThisRow += colspan || 1;
      }
      columns = Math.max(columns, columnsInThisRow);
    }
    return {rows, columns};
  },

  /**
   * Look for 'data' (as opposed to 'layout') tables, for which we use
   * similar checks as
   * https://dxr.mozilla.org/mozilla-central/rev/71224049c0b52ab190564d3ea0eab089a159a4cf/accessible/html/HTMLTableAccessible.cpp#920
   */
  _markDataTables(root) {
    const tables = root.getElementsByTagName('table');
    for (let i = 0; i < tables.length; i++) {
      var table = tables[i];
      const role = table.getAttribute('role');
      if (role == 'presentation') {
        table._readabilityDataTable = false;
        continue;
      }
      const datatable = table.getAttribute('datatable');
      if (datatable == '0') {
        table._readabilityDataTable = false;
        continue;
      }
      const summary = table.getAttribute('summary');
      if (summary) {
        table._readabilityDataTable = true;
        continue;
      }

      const caption = table.getElementsByTagName('caption')[0];
      if (caption && caption.childNodes.length > 0) {
        table._readabilityDataTable = true;
        continue;
      }

      // If the table has a descendant with any of these tags, consider a data table:
      const dataTableDescendants = ['col', 'colgroup', 'tfoot', 'thead', 'th'];
      const descendantExists = function (tag) {
        return !!table.getElementsByTagName(tag)[0];
      };
      if (dataTableDescendants.some(descendantExists)) {
        this.log('Data table because found data-y descendant');
        table._readabilityDataTable = true;
        continue;
      }

      // Nested tables indicate a layout table:
      if (table.getElementsByTagName('table')[0]) {
        table._readabilityDataTable = false;
        continue;
      }

      const sizeInfo = this._getRowAndColumnCount(table);
      if (sizeInfo.rows >= 10 || sizeInfo.columns > 4) {
        table._readabilityDataTable = true;
        continue;
      }
      // Now just go by size entirely:
      table._readabilityDataTable = sizeInfo.rows * sizeInfo.columns > 10;
    }
  },

  /* convert images and figures that have properties like data-src into images that can be loaded without JS */
  _fixLazyImages(root) {
    this._forEachNode(
      this._getAllNodesWithTag(root, ['img', 'picture', 'figure']),
      function (elem) {
        // In some sites (e.g. Kotaku), they put 1px square image as base64 data uri in the src attribute.
        // So, here we check if the data uri is too short, just might as well remove it.
        if (elem.src && this.REGEXPS.b64DataUrl.test(elem.src)) {
          // Make sure it's not SVG, because SVG can have a meaningful image in under 133 bytes.
          const parts = this.REGEXPS.b64DataUrl.exec(elem.src);
          if (parts[1] === 'image/svg+xml') {
            return;
          }

          // Make sure this element has other attributes which contains image.
          // If it doesn't, then this src is important and shouldn't be removed.
          let srcCouldBeRemoved = false;
          for (let i = 0; i < elem.attributes.length; i++) {
            var attr = elem.attributes[i];
            if (attr.name === 'src') {
              continue;
            }

            if (/\.(jpg|jpeg|png|webp)/i.test(attr.value)) {
              srcCouldBeRemoved = true;
              break;
            }
          }

          // Here we assume if image is less than 100 bytes (or 133B after encoded to base64)
          // it will be too small, therefore it might be placeholder image.
          if (srcCouldBeRemoved) {
            const b64starts = elem.src.search(/base64\s*/i) + 7;
            const b64length = elem.src.length - b64starts;
            if (b64length < 133) {
              elem.removeAttribute('src');
            }
          }
        }

        // also check for "null" to work around https://github.com/jsdom/jsdom/issues/2580
        if (
          (elem.src || (elem.srcset && elem.srcset != 'null')) &&
          elem.className.toLowerCase().indexOf('lazy') === -1
        ) {
          return;
        }

        for (let j = 0; j < elem.attributes.length; j++) {
          attr = elem.attributes[j];
          if (attr.name === 'src' || attr.name === 'srcset') {
            continue;
          }
          let copyTo = null;
          if (/\.(jpg|jpeg|png|webp)\s+\d/.test(attr.value)) {
            copyTo = 'srcset';
          } else if (/^\s*\S+\.(jpg|jpeg|png|webp)\S*\s*$/.test(attr.value)) {
            copyTo = 'src';
          }
          if (copyTo) {
            // if this is an img or picture, set the attribute directly
            if (elem.tagName === 'IMG' || elem.tagName === 'PICTURE') {
              elem.setAttribute(copyTo, attr.value);
            } else if (
              elem.tagName === 'FIGURE' &&
              !this._getAllNodesWithTag(elem, ['img', 'picture']).length
            ) {
              // if the item is a <figure> that does not contain an image or picture, create one and place it inside the figure
              // see the nytimes-3 testcase for an example
              const img = this._doc.createElement('img');
              img.setAttribute(copyTo, attr.value);
              elem.appendChild(img);
            }
          }
        }
      },
    );
  },

  /**
   * 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.
   *
   * @return void
   **/
  _cleanConditionally(e, tag) {
    if (!this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)) {
      return;
    }

    const isList = tag === 'ul' || tag === 'ol';

    // Gather counts for other typical elements embedded within.
    // Traverse backwards so we can remove nodes at the same time
    // without effecting the traversal.
    //
    // TODO: Consider taking into account original contentScore here.
    this._removeNodes(this._getAllNodesWithTag(e, [tag]), function (node) {
      // First check if this node IS data table, in which case don't remove it.
      const isDataTable = function (t) {
        return t._readabilityDataTable;
      };

      if (tag === 'table' && isDataTable(node)) {
        return false;
      }

      // Next check if we're inside a data table, in which case don't remove it as well.
      if (this._hasAncestorTag(node, 'table', -1, isDataTable)) {
        return false;
      }

      const weight = this._getClassWeight(node);
      const contentScore = 0;

      this.log('Cleaning Conditionally', node);

      if (weight + contentScore < 0) {
        return true;
      }

      if (this._getCharCount(node, ',') < 10) {
        // If there are not very many commas, and the number of
        // non-paragraph elements is more than paragraphs or other
        // ominous signs, remove the element.
        const p = node.getElementsByTagName('p').length;
        const img = node.getElementsByTagName('img').length;
        const li = node.getElementsByTagName('li').length - 100;
        const input = node.getElementsByTagName('input').length;

        let embedCount = 0;
        const embeds = this._getAllNodesWithTag(node, [
          'object',
          'embed',
          'iframe',
        ]);

        for (let i = 0; i < embeds.length; i++) {
          // If this embed has attribute that matches video regex, don't delete it.
          for (let j = 0; j < embeds[i].attributes.length; j++) {
            if (this.REGEXPS.videos.test(embeds[i].attributes[j].value)) {
              return false;
            }
          }

          // For embed with <object> tag, check inner HTML as well.
          if (
            embeds[i].tagName === 'object' &&
            this.REGEXPS.videos.test(embeds[i].innerHTML)
          ) {
            return false;
          }

          embedCount++;
        }

        const linkDensity = this._getLinkDensity(node);
        const contentLength = this._getInnerText(node).length;

        const haveToRemove =
          (img > 1 && p / img < 0.5 && !this._hasAncestorTag(node, 'figure')) ||
          (!isList && li > p) ||
          input > Math.floor(p / 3) ||
          (!isList &&
            contentLength < 25 &&
            (img === 0 || img > 2) &&
            !this._hasAncestorTag(node, 'figure')) ||
          (!isList && weight < 25 && linkDensity > 0.2) ||
          (weight >= 25 && linkDensity > 0.5) ||
          (embedCount === 1 && contentLength < 75) ||
          embedCount > 1;
        return haveToRemove;
      }
      return false;
    });
  },

  /**
   * Clean out elements that match the specified conditions
   *
   * @param Element
   * @param Function determines whether a node should be removed
   * @return void
   **/
  _cleanMatchedNodes(e, filter) {
    const endOfSearchMarkerNode = this._getNextNode(e, true);
    let next = this._getNextNode(e);
    while (next && next != endOfSearchMarkerNode) {
      if (filter.call(this, next, next.className + ' ' + next.id)) {
        next = this._removeAndGetNext(next);
      } else {
        next = this._getNextNode(next);
      }
    }
  },

  /**
   * Clean out spurious headers from an Element. Checks things like classnames and link density.
   *
   * @param Element
   * @return void
   **/
  _cleanHeaders(e) {
    this._removeNodes(
      this._getAllNodesWithTag(e, ['h1', 'h2']),
      function (header) {
        return this._getClassWeight(header) < 0;
      },
    );
  },

  _flagIsActive(flag) {
    return (this._flags & flag) > 0;
  },

  _removeFlag(flag) {
    this._flags = this._flags & ~flag;
  },

  _isProbablyVisible(node) {
    // Have to null-check node.style and node.className.indexOf to deal with SVG and MathML nodes.
    return (
      (!node.style || node.style.display != 'none') &&
      !node.hasAttribute('hidden') &&
      // check for "fallback-image" so that wikimedia math images are displayed
      (!node.hasAttribute('aria-hidden') ||
        node.getAttribute('aria-hidden') != 'true' ||
        (node.className &&
          node.className.indexOf &&
          node.className.indexOf('fallback-image') !== -1))
    );
  },

  /**
   * Runs readability.
   *
   * Workflow:
   *  1. Prep the document by removing script tags, css, etc.
   *  2. Build readability's DOM tree.
   *  3. Grab the article content from the current dom tree.
   *  4. Replace the current DOM tree with the new one.
   *  5. Read peacefully.
   *
   * @return void
   **/
  parse() {
    // Avoid parsing too large documents, as per configuration option
    if (this._maxElemsToParse > 0) {
      const numTags = this._doc.getElementsByTagName('*').length;
      if (numTags > this._maxElemsToParse) {
        throw new Error(
          'Aborting parsing document; ' + numTags + ' elements found',
        );
      }
    }

    // Unwrap image from noscript
    this._unwrapNoscriptImages(this._doc);

    // Remove script tags from the document.
    this._removeScripts(this._doc);

    this._prepDocument();

    const metadata = this._getArticleMetadata();
    this._articleTitle = metadata.title;

    const articleContent = this._grabArticle();
    if (!articleContent) {
      return null;
    }

    this.log('Grabbed: ' + articleContent.innerHTML);

    this._postProcessContent(articleContent);

    // If we haven't found an excerpt in the article's metadata, use the article's
    // first paragraph as the excerpt. This is used for displaying a preview of
    // the article's content.
    if (!metadata.excerpt) {
      const paragraphs = articleContent.getElementsByTagName('p');
      if (paragraphs.length > 0) {
        metadata.excerpt = paragraphs[0].textContent.trim();
      }
    }

    const textContent = articleContent.textContent;
    return {
      title: this._articleTitle,
      byline: metadata.byline || this._articleByline,
      dir: this._articleDir,
      content: articleContent.innerHTML,
      textContent,
      length: textContent.length,
      excerpt: metadata.excerpt,
      siteName: metadata.siteName || this._articleSiteName,
    };
  },
};

/// TURNDOWN: https://unpkg.com/turndown@6.0.0?module

function extend(destination) {
  for (let i = 1; i < arguments.length; i++) {
    const source = arguments[i];
    for (const key in source) {
      if (source.hasOwnProperty(key)) destination[key] = source[key];
    }
  }
  return destination;
}

function repeat(character, count) {
  return Array(count + 1).join(character);
}

const blockElements = [
  'address',
  'article',
  'aside',
  'audio',
  'blockquote',
  'body',
  'canvas',
  'center',
  'dd',
  'dir',
  'div',
  'dl',
  'dt',
  'fieldset',
  'figcaption',
  'figure',
  'footer',
  'form',
  'frameset',
  'h1',
  'h2',
  'h3',
  'h4',
  'h5',
  'h6',
  'header',
  'hgroup',
  'hr',
  'html',
  'isindex',
  'li',
  'main',
  'menu',
  'nav',
  'noframes',
  'noscript',
  'ol',
  'output',
  'p',
  'pre',
  'section',
  'table',
  'tbody',
  'td',
  'tfoot',
  'th',
  'thead',
  'tr',
  'ul',
];

function isBlock(node) {
  return blockElements.indexOf(node.nodeName.toLowerCase()) !== -1;
}

const voidElements = [
  'area',
  'base',
  'br',
  'col',
  'command',
  'embed',
  'hr',
  'img',
  'input',
  'keygen',
  'link',
  'meta',
  'param',
  'source',
  'track',
  'wbr',
];

function isVoid(node) {
  return voidElements.indexOf(node.nodeName.toLowerCase()) !== -1;
}

const voidSelector = voidElements.join();
function hasVoid(node) {
  return node.querySelector && node.querySelector(voidSelector);
}

const rules = {};

rules.paragraph = {
  filter: 'p',

  replacement: function (content) {
    return '\n\n' + content + '\n\n';
  },
};

rules.lineBreak = {
  filter: 'br',

  replacement: function (content, node, options) {
    return options.br + '\n';
  },
};

rules.heading = {
  filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],

  replacement: function (content, node, options) {
    const hLevel = Number(node.nodeName.charAt(1));

    if (options.headingStyle === 'setext' && hLevel < 3) {
      const underline = repeat(hLevel === 1 ? '=' : '-', content.length);
      return '\n\n' + content + '\n' + underline + '\n\n';
    } else {
      return '\n\n' + repeat('#', hLevel) + ' ' + content + '\n\n';
    }
  },
};

rules.blockquote = {
  filter: 'blockquote',

  replacement: function (content) {
    content = content.replace(/^\n+|\n+$/g, '');
    content = content.replace(/^/gm, '> ');
    return '\n\n' + content + '\n\n';
  },
};

rules.list = {
  filter: ['ul', 'ol'],

  replacement: function (content, node) {
    const parent = node.parentNode;
    if (parent.nodeName === 'LI' && parent.lastElementChild === node) {
      return '\n' + content;
    } else {
      return '\n\n' + content + '\n\n';
    }
  },
};

rules.listItem = {
  filter: 'li',

  replacement: function (content, node, options) {
    content = content
      .replace(/^\n+/, '') // remove leading newlines
      .replace(/\n+$/, '\n') // replace trailing newlines with just a single one
      .replace(/\n/gm, '\n    '); // indent
    let prefix = options.bulletListMarker + '   ';
    const parent = node.parentNode;
    if (parent.nodeName === 'OL') {
      const start = parent.getAttribute('start');
      const index = Array.prototype.indexOf.call(parent.children, node);
      prefix = (start ? Number(start) + index : index + 1) + '.  ';
    }
    return (
      prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '')
    );
  },
};

rules.indentedCodeBlock = {
  filter: function (node, options) {
    return (
      options.codeBlockStyle === 'indented' &&
      node.nodeName === 'PRE' &&
      node.firstChild &&
      node.firstChild.nodeName === 'CODE'
    );
  },

  replacement: function (content, node, options) {
    return (
      '\n\n    ' + node.firstChild.textContent.replace(/\n/g, '\n    ') + '\n\n'
    );
  },
};

rules.fencedCodeBlock = {
  filter: function (node, options) {
    return (
      options.codeBlockStyle === 'fenced' &&
      node.nodeName === 'PRE' &&
      node.firstChild &&
      node.firstChild.nodeName === 'CODE'
    );
  },

  replacement: function (content, node, options) {
    const className = node.firstChild.className || '';
    const language = (className.match(/language-(\S+)/) || [null, ''])[1];
    const code = node.firstChild.textContent;

    const fenceChar = options.fence.charAt(0);
    let fenceSize = 3;
    const fenceInCodeRegex = new RegExp('^' + fenceChar + '{3,}', 'gm');

    let match;
    while ((match = fenceInCodeRegex.exec(code))) {
      if (match[0].length >= fenceSize) {
        fenceSize = match[0].length + 1;
      }
    }

    const fence = repeat(fenceChar, fenceSize);

    return (
      '\n\n' +
      fence +
      language +
      '\n' +
      code.replace(/\n$/, '') +
      '\n' +
      fence +
      '\n\n'
    );
  },
};

rules.horizontalRule = {
  filter: 'hr',

  replacement: function (content, node, options) {
    return '\n\n' + options.hr + '\n\n';
  },
};

rules.inlineLink = {
  filter: function (node, options) {
    return (
      options.linkStyle === 'inlined' &&
      node.nodeName === 'A' &&
      node.getAttribute('href')
    );
  },

  replacement: function (content, node) {
    const href = node.getAttribute('href');
    const title = node.title ? ' "' + node.title + '"' : '';
    return '[' + content + '](' + href + title + ')';
  },
};

rules.referenceLink = {
  filter: function (node, options) {
    return (
      options.linkStyle === 'referenced' &&
      node.nodeName === 'A' &&
      node.getAttribute('href')
    );
  },

  replacement: function (content, node, options) {
    const href = node.getAttribute('href');
    const title = node.title ? ' "' + node.title + '"' : '';
    let replacement;
    let reference;

    switch (options.linkReferenceStyle) {
      case 'collapsed':
        replacement = '[' + content + '][]';
        reference = '[' + content + ']: ' + href + title;
        break;
      case 'shortcut':
        replacement = '[' + content + ']';
        reference = '[' + content + ']: ' + href + title;
        break;
      default:
        var id = this.references.length + 1;
        replacement = '[' + content + '][' + id + ']';
        reference = '[' + id + ']: ' + href + title;
    }

    this.references.push(reference);
    return replacement;
  },

  references: [],

  append: function (options) {
    let references = '';
    if (this.references.length) {
      references = '\n\n' + this.references.join('\n') + '\n\n';
      this.references = []; // Reset references
    }
    return references;
  },
};

rules.emphasis = {
  filter: ['em', 'i'],

  replacement: function (content, node, options) {
    if (!content.trim()) return '';
    return options.emDelimiter + content + options.emDelimiter;
  },
};

rules.strong = {
  filter: ['strong', 'b'],

  replacement: function (content, node, options) {
    if (!content.trim()) return '';
    return options.strongDelimiter + content + options.strongDelimiter;
  },
};

rules.code = {
  filter: function (node) {
    const hasSiblings = node.previousSibling || node.nextSibling;
    const isCodeBlock = node.parentNode.nodeName === 'PRE' && !hasSiblings;

    return node.nodeName === 'CODE' && !isCodeBlock;
  },

  replacement: function (content) {
    if (!content.trim()) return '';

    let delimiter = '`';
    let leadingSpace = '';
    let trailingSpace = '';
    const matches = content.match(/`+/gm);
    if (matches) {
      if (/^`/.test(content)) leadingSpace = ' ';
      if (/`$/.test(content)) trailingSpace = ' ';
      while (matches.indexOf(delimiter) !== -1) delimiter = delimiter + '`';
    }

    return delimiter + leadingSpace + content + trailingSpace + delimiter;
  },
};

rules.image = {
  filter: 'img',

  replacement: function (content, node) {
    const alt = node.alt || '';
    const src = node.getAttribute('src') || '';
    const title = node.title || '';
    const titlePart = title ? ' "' + title + '"' : '';
    return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : '';
  },
};

/**
 * Manages a collection of rules used to convert HTML to Markdown
 */

function Rules(options) {
  this.options = options;
  this._keep = [];
  this._remove = [];

  this.blankRule = {
    replacement: options.blankReplacement,
  };

  this.keepReplacement = options.keepReplacement;

  this.defaultRule = {
    replacement: options.defaultReplacement,
  };

  this.array = [];
  for (const key in options.rules) this.array.push(options.rules[key]);
}

Rules.prototype = {
  add: function (key, rule) {
    this.array.unshift(rule);
  },

  keep: function (filter) {
    this._keep.unshift({
      filter: filter,
      replacement: this.keepReplacement,
    });
  },

  remove: function (filter) {
    this._remove.unshift({
      filter: filter,
      replacement: function () {
        return '';
      },
    });
  },

  forNode: function (node) {
    if (node.isBlank) return this.blankRule;
    let rule;

    if ((rule = findRule(this.array, node, this.options))) return rule;
    if ((rule = findRule(this._keep, node, this.options))) return rule;
    if ((rule = findRule(this._remove, node, this.options))) return rule;

    return this.defaultRule;
  },

  forEach: function (fn) {
    for (let i = 0; i < this.array.length; i++) fn(this.array[i], i);
  },
};

function findRule(rules, node, options) {
  for (let i = 0; i < rules.length; i++) {
    const rule = rules[i];
    if (filterValue(rule, node, options)) return rule;
  }
  return void 0;
}

function filterValue(rule, node, options) {
  const filter = rule.filter;
  if (typeof filter === 'string') {
    if (filter === node.nodeName.toLowerCase()) return true;
  } else if (Array.isArray(filter)) {
    if (filter.indexOf(node.nodeName.toLowerCase()) > -1) return true;
  } else if (typeof filter === 'function') {
    if (filter.call(rule, node, options)) return true;
  } else {
    throw new TypeError('`filter` needs to be a string, array, or function');
  }
}

/**
 * The collapseWhitespace function is adapted from collapse-whitespace
 * by Luc Thevenard.
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Luc Thevenard <lucthevenard@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * collapseWhitespace(options) removes extraneous whitespace from an the given element.
 *
 * @param {Object} options
 */
function collapseWhitespace(options) {
  const element = options.element;
  const isBlock = options.isBlock;
  const isVoid = options.isVoid;
  const isPre =
    options.isPre ||
    function (node) {
      return node.nodeName === 'PRE';
    };

  if (!element.firstChild || isPre(element)) return;

  let prevText = null;
  let prevVoid = false;

  let prev = null;
  let node = next(prev, element, isPre);

  while (node !== element) {
    if (node.nodeType === 3 || node.nodeType === 4) {
      // Node.TEXT_NODE or Node.CDATA_SECTION_NODE
      let text = node.data.replace(/[ \r\n\t]+/g, ' ');

      if (
        (!prevText || / $/.test(prevText.data)) &&
        !prevVoid &&
        text[0] === ' '
      ) {
        text = text.substr(1);
      }

      // `text` might be empty at this point.
      if (!text) {
        node = remove(node);
        continue;
      }

      node.data = text;

      prevText = node;
    } else if (node.nodeType === 1) {
      // Node.ELEMENT_NODE
      if (isBlock(node) || node.nodeName === 'BR') {
        if (prevText) {
          prevText.data = prevText.data.replace(/ $/, '');
        }

        prevText = null;
        prevVoid = false;
      } else if (isVoid(node)) {
        // Avoid trimming space around non-block, non-BR void elements.
        prevText = null;
        prevVoid = true;
      }
    } else {
      node = remove(node);
      continue;
    }

    const nextNode = next(prev, node, isPre);
    prev = node;
    node = nextNode;
  }

  if (prevText) {
    prevText.data = prevText.data.replace(/ $/, '');
    if (!prevText.data) {
      remove(prevText);
    }
  }
}

/**
 * remove(node) removes the given node from the DOM and returns the
 * next node in the sequence.
 *
 * @param {Node} node
 * @return {Node} node
 */
function remove(node) {
  const next = node.nextSibling || node.parentNode;

  node.parentNode.removeChild(node);

  return next;
}

/**
 * next(prev, current, isPre) returns the next node in the sequence, given the
 * current and previous nodes.
 *
 * @param {Node} prev
 * @param {Node} current
 * @param {Function} isPre
 * @return {Node}
 */
function next(prev, current, isPre) {
  if ((prev && prev.parentNode === current) || isPre(current)) {
    return current.nextSibling || current.parentNode;
  }

  return current.firstChild || current.nextSibling || current.parentNode;
}

/*
 * Set up window for Node.js
 */

const root = typeof window !== 'undefined' ? window : {};

/*
 * Parsing HTML strings
 */

function canParseHTMLNatively() {
  const Parser = root.DOMParser;
  let canParse = false;

  // Adapted from https://gist.github.com/1129031
  // Firefox/Opera/IE throw errors on unsupported types
  try {
    // WebKit returns null on unsupported types
    if (new Parser().parseFromString('', 'text/html')) {
      canParse = true;
    }
  } catch (e) {}

  return canParse;
}

function createHTMLParser() {
  const Parser = function () {};

  {
    const JSDOM = require('jsdom').JSDOM;
    Parser.prototype.parseFromString = function (string) {
      return new JSDOM(string).window.document;
    };
  }
  return Parser;
}

const HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser();

function RootNode(input) {
  let root;
  if (typeof input === 'string') {
    const doc = htmlParser().parseFromString(
      // DOM parsers arrange elements in the <head> and <body>.
      // Wrapping in a custom element ensures elements are reliably arranged in
      // a single element.
      '<x-turndown id="turndown-root">' + input + '</x-turndown>',
      'text/html',
    );

    root = doc.getElementById('turndown-root');
  } else {
    root = input.cloneNode(true);
  }
  collapseWhitespace({
    element: root,
    isBlock: isBlock,
    isVoid: isVoid,
  });

  return root;
}

let _htmlParser;
function htmlParser() {
  _htmlParser = _htmlParser || new HTMLParser();
  return _htmlParser;
}

function Node(node) {
  node.isBlock = isBlock(node);
  node.isCode =
    node.nodeName.toLowerCase() === 'code' || node.parentNode.isCode;
  node.isBlank = isBlank(node);
  node.flankingWhitespace = flankingWhitespace(node);
  return node;
}

function isBlank(node) {
  return (
    ['A', 'TH', 'TD', 'IFRAME', 'SCRIPT', 'AUDIO', 'VIDEO'].indexOf(
      node.nodeName,
    ) === -1 &&
    /^\s*$/i.test(node.textContent) &&
    !isVoid(node) &&
    !hasVoid(node)
  );
}

function flankingWhitespace(node) {
  let leading = '';
  let trailing = '';

  if (!node.isBlock) {
    const hasLeading = /^\s/.test(node.textContent);
    const hasTrailing = /\s$/.test(node.textContent);
    const blankWithSpaces = node.isBlank && hasLeading && hasTrailing;

    if (hasLeading && !isFlankedByWhitespace('left', node)) {
      leading = ' ';
    }

    if (
      !blankWithSpaces &&
      hasTrailing &&
      !isFlankedByWhitespace('right', node)
    ) {
      trailing = ' ';
    }
  }

  return {leading: leading, trailing: trailing};
}

function isFlankedByWhitespace(side, node) {
  let sibling;
  let regExp;
  let isFlanked;

  if (side === 'left') {
    sibling = node.previousSibling;
    regExp = / $/;
  } else {
    sibling = node.nextSibling;
    regExp = /^ /;
  }

  if (sibling) {
    if (sibling.nodeType === 3) {
      isFlanked = regExp.test(sibling.nodeValue);
    } else if (sibling.nodeType === 1 && !isBlock(sibling)) {
      isFlanked = regExp.test(sibling.textContent);
    }
  }
  return isFlanked;
}

const reduce = Array.prototype.reduce;
const leadingNewLinesRegExp = /^\n*/;
const trailingNewLinesRegExp = /\n*$/;
const escapes = [
  [/\\/g, '\\\\'],
  [/\*/g, '\\*'],
  [/^-/g, '\\-'],
  [/^\+ /g, '\\+ '],
  [/^(=+)/g, '\\$1'],
  [/^(#{1,6}) /g, '\\$1 '],
  [/`/g, '\\`'],
  [/^~~~/g, '\\~~~'],
  [/\[/g, '\\['],
  [/\]/g, '\\]'],
  [/^>/g, '\\>'],
  [/_/g, '\\_'],
  [/^(\d+)\. /g, '$1\\. '],
];

function TurndownService(options) {
  if (!(this instanceof TurndownService)) return new TurndownService(options);

  const defaults = {
    rules: rules,
    headingStyle: 'setext',
    hr: '* * *',
    bulletListMarker: '*',
    codeBlockStyle: 'indented',
    fence: '```',
    emDelimiter: '_',
    strongDelimiter: '**',
    linkStyle: 'inlined',
    linkReferenceStyle: 'full',
    br: '  ',
    blankReplacement: function (content, node) {
      return node.isBlock ? '\n\n' : '';
    },
    keepReplacement: function (content, node) {
      return node.isBlock ? '\n\n' + node.outerHTML + '\n\n' : node.outerHTML;
    },
    defaultReplacement: function (content, node) {
      return node.isBlock ? '\n\n' + content + '\n\n' : content;
    },
  };

  this.options = extend({}, defaults, options);
  this.rules = new Rules(this.options);
}

TurndownService.prototype = {
  /**
   * The entry point for converting a string or DOM node to Markdown
   * @public
   * @param {String|HTMLElement} input The string or DOM node to convert
   * @returns A Markdown representation of the input
   * @type String
   */

  turndown: function (input) {
    if (!canConvert(input)) {
      throw new TypeError(
        input + ' is not a string, or an element/document/fragment node.',
      );
    }

    if (input === '') return '';

    const output = process.call(this, new RootNode(input));
    return postProcess.call(this, output);
  },

  /**
   * Add one or more plugins
   * @public
   * @param {Function|Array} plugin The plugin or array of plugins to add
   * @returns The Turndown instance for chaining
   * @type Object
   */

  use: function (plugin) {
    if (Array.isArray(plugin)) {
      for (let i = 0; i < plugin.length; i++) this.use(plugin[i]);
    } else if (typeof plugin === 'function') {
      plugin(this);
    } else {
      throw new TypeError('plugin must be a Function or an Array of Functions');
    }
    return this;
  },

  /**
   * Adds a rule
   * @public
   * @param {String} key The unique key of the rule
   * @param {Object} rule The rule
   * @returns The Turndown instance for chaining
   * @type Object
   */

  addRule: function (key, rule) {
    this.rules.add(key, rule);
    return this;
  },

  /**
   * Keep a node (as HTML) that matches the filter
   * @public
   * @param {String|Array|Function} filter The unique key of the rule
   * @returns The Turndown instance for chaining
   * @type Object
   */

  keep: function (filter) {
    this.rules.keep(filter);
    return this;
  },

  /**
   * Remove a node that matches the filter
   * @public
   * @param {String|Array|Function} filter The unique key of the rule
   * @returns The Turndown instance for chaining
   * @type Object
   */

  remove: function (filter) {
    this.rules.remove(filter);
    return this;
  },

  /**
   * Escapes Markdown syntax
   * @public
   * @param {String} string The string to escape
   * @returns A string with Markdown syntax escaped
   * @type String
   */

  escape: function (string) {
    return escapes.reduce(function (accumulator, escape) {
      return accumulator.replace(escape[0], escape[1]);
    }, string);
  },
};

/**
 * Reduces a DOM node down to its Markdown string equivalent
 * @private
 * @param {HTMLElement} parentNode The node to convert
 * @returns A Markdown representation of the node
 * @type String
 */

function process(parentNode) {
  const self = this;
  return reduce.call(
    parentNode.childNodes,
    function (output, node) {
      node = new Node(node);

      let replacement = '';
      if (node.nodeType === 3) {
        replacement = node.isCode
          ? node.nodeValue
          : self.escape(node.nodeValue);
      } else if (node.nodeType === 1) {
        replacement = replacementForNode.call(self, node);
      }

      return join(output, replacement);
    },
    '',
  );
}

/**
 * Appends strings as each rule requires and trims the output
 * @private
 * @param {String} output The conversion output
 * @returns A trimmed version of the ouput
 * @type String
 */

function postProcess(output) {
  const self = this;
  this.rules.forEach(function (rule) {
    if (typeof rule.append === 'function') {
      output = join(output, rule.append(self.options));
    }
  });

  return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '');
}

/**
 * Converts an element node to its Markdown equivalent
 * @private
 * @param {HTMLElement} node The node to convert
 * @returns A Markdown representation of the node
 * @type String
 */

function replacementForNode(node) {
  const rule = this.rules.forNode(node);
  let content = process.call(this, node);
  const whitespace = node.flankingWhitespace;
  if (whitespace.leading || whitespace.trailing) content = content.trim();
  return (
    whitespace.leading +
    rule.replacement(content, node, this.options) +
    whitespace.trailing
  );
}

/**
 * Determines the new lines between the current output and the replacement
 * @private
 * @param {String} output The current conversion output
 * @param {String} replacement The string to append to the output
 * @returns The whitespace to separate the current output and the replacement
 * @type String
 */

function separatingNewlines(output, replacement) {
  const newlines = [
    output.match(trailingNewLinesRegExp)[0],
    replacement.match(leadingNewLinesRegExp)[0],
  ].sort();
  const maxNewlines = newlines[newlines.length - 1];
  return maxNewlines.length < 2 ? maxNewlines : '\n\n';
}

function join(string1, string2) {
  const separator = separatingNewlines(string1, string2);

  // Remove trailing/leading newlines and replace with separator
  string1 = string1.replace(trailingNewLinesRegExp, '');
  string2 = string2.replace(leadingNewLinesRegExp, '');

  return string1 + separator + string2;
}

/**
 * Determines whether an input can be converted
 * @private
 * @param {String|HTMLElement} input Describe this parameter
 * @returns Describe what it returns
 * @type String|Object|Array|Boolean|Number
 */

function canConvert(input) {
  return (
    input != null &&
    (typeof input === 'string' ||
      (input.nodeType &&
        (input.nodeType === 1 ||
          input.nodeType === 9 ||
          input.nodeType === 11)))
  );
}

/// CLIPPER ITSELF

/* Parse the site's meta keywords content into tags, if present */
if (document.querySelector('meta[name="keywords" i]')) {
  const keywords = document
    .querySelector('meta[name="keywords" i]')
    .getAttribute('content')
    .split(',');

  keywords.forEach(function (keyword) {
    const tag = ' ' + keyword.split(' ').join('');
    tags += tag;
  });
}

function getSelectionHtml() {
  let html = '';
  if (typeof window.getSelection !== 'undefined') {
    const sel = window.getSelection();
    if (sel.rangeCount) {
      const container = document.createElement('div');
      for (let i = 0, len = sel.rangeCount; i < len; ++i) {
        container.appendChild(sel.getRangeAt(i).cloneContents());
      }
      html = container.innerHTML;
    }
  } else if (typeof document.selection !== 'undefined') {
    if (document.selection.type == 'Text') {
      html = document.selection.createRange().htmlText;
    }
  }
  return html;
}

const selection = getSelectionHtml();

const {title, byline, content} = new Readability(
  document.cloneNode(true),
).parse();

function getFileName(fileName) {
  const userAgent = window.navigator.userAgent,
    platform = window.navigator.platform,
    windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];

  if (windowsPlatforms.indexOf(platform) !== -1) {
    fileName = fileName.replace(':', '').replace(/[/\\?%*|"<>]/g, '-');
  } else {
    fileName = fileName
      .replace(':', '')
      .replace(/\//g, '-')
      .replace(/\\/g, '-');
  }
  return fileName;
}
const fileName = getFileName(title);

if (selection) {
  var markdownify = selection;
} else {
  var markdownify = content;
}

if (vault) {
  var vaultName = '&vault=' + encodeURIComponent(`${vault}`);
} else {
  var vaultName = '';
}

const markdownBody = new TurndownService({
  headingStyle: 'atx',
  hr: '---',
  bulletListMarker: '-',
  codeBlockStyle: 'fenced',
  emDelimiter: '*',
}).turndown(markdownify);

var date = new Date();

function convertDate(date) {
  const yyyy = date.getFullYear().toString();
  const mm = (date.getMonth() + 1).toString();
  const dd = date.getDate().toString();
  const mmChars = mm.split('');
  const ddChars = dd.split('');
  return (
    yyyy +
    '-' +
    (mmChars[1] ? mm : '0' + mmChars[0]) +
    '-' +
    (ddChars[1] ? dd : '0' + ddChars[0])
  );
}

const today = convertDate(date);

// Utility function to get meta content by name or property
function getMetaContent(attr, value) {
  const element = document.querySelector(`meta[${attr}='${value}']`);
  return element ? element.getAttribute('content').trim() : '';
}

// Fetch byline, meta author, property author, or site name
const author =
  byline ||
  getMetaContent('name', 'author') ||
  getMetaContent('property', 'author') ||
  getMetaContent('property', 'og:site_name');

// Check if there's an author and add brackets
const authorBrackets = author ? `"[[${author}]]"` : '';

/* Try to get published date */
const timeElement = document.querySelector('time');
const publishedDate = timeElement ? timeElement.getAttribute('datetime') : '';

if (publishedDate && publishedDate.trim() !== '') {
  var date = new Date(publishedDate);
  const year = date.getFullYear();
  let month = date.getMonth() + 1; // Months are 0-based in JavaScript
  let day = date.getDate();

  // Pad month and day with leading zeros if necessary
  month = month < 10 ? '0' + month : month;
  day = day < 10 ? '0' + day : day;

  var published = year + '-' + month + '-' + day;
} else {
  var published = '';
}

/* YAML front matter as tags render cleaner with special chars  */
const fileContent =
  '---\n' +
  'category: "[[Clippings]]"\n' +
  'author: ' +
  authorBrackets +
  '\n' +
  'title: "' +
  title +
  '"\n' +
  'source: ' +
  document.URL +
  '\n' +
  'clipped: ' +
  today +
  '\n' +
  'published: ' +
  published +
  '\n' +
  'topics: \n' +
  'tags: [' +
  tags +
  ']\n' +
  '---\n\n' +
  markdownBody;

document.location.href =
  'obsidian://new?' +
  'file=' +
  encodeURIComponent(folder + fileName) +
  '&content=' +
  encodeURIComponent(fileContent) +
  vaultName;

@jostyee
Copy link

jostyee commented Aug 11, 2024

@ericraymond your version is too long that neither firefox nor safari could completely paste it to the bookmarklet address bar.

@ericraymond
Copy link

ericraymond commented Aug 11, 2024

@ericraymond your version is too long that neither firefox nor safari could completely paste it to the bookmarklet address bar.

I was able to use it in Chrome.

Try pasting the source code from https://gist.github.com/kepano/90c05f162c37cf730abb8ff027987ca3?permalink_comment_id=5150379#gistcomment-5150379 into https://caiorss.github.io/bookmarklet-maker/ and maybe you can generate a new bookmarklet that you can drag and drop?

@kvdogan
Copy link

kvdogan commented Aug 12, 2024

@ericraymond I could not make this script work without selecting text to clip and not in github repo in any case, Other than github it just does not work without text selection. Is this ok by design? No full page clipping, is that correct or it just does not work for me. I am pretty much interested in a solution that allows us clip pages incl. github repos and full page clip without using advanced uri plugin as a dependency, my code above can not handle github, but full page clip with advanced uri plugin dependency works just fine.

@jostyee
Copy link

jostyee commented Aug 13, 2024

@ericraymond your version is too long that neither firefox nor safari could completely paste it to the bookmarklet address bar.

I was able to use it in Chrome.

Try pasting the source code from https://gist.github.com/kepano/90c05f162c37cf730abb8ff027987ca3?permalink_comment_id=5150379#gistcomment-5150379 into https://caiorss.github.io/bookmarklet-maker/ and maybe you can generate a new bookmarklet that you can drag and drop?

works for Safari but Firefox still rejects to add it :-(

@strangeZombies
Copy link

strangeZombies commented Oct 10, 2024

@jostyee I used to utilize tools like Tampermonkey for this purpose. UMark2Md is a sample script. The script is not fully refined yet, so I am sharing it through a semi-public link.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment