/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththisfile,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/** *ThisisarelativelylightweightDOMParserthatissafetouseinaweb *worker.ThisisfarfromacompleteDOMimplementation;however,itshould *containtheminimalsetoffunctionalitynecessaryforReadability.js. * *AsidefromnotimplementingthefullDOMAPI,thereareotherquirkstobe *awareofwhenusingtheJSDOMParser: * *1)ProperlyformedHTML/XMLmustbeused.Thismeansyoushouldbeextra *carefulwhenusingthisparseronanythingreceiveddirectlyfroman *XMLHttpRequest.ProvidingaserializedstringfromanXMLSerializer, *however,shouldbesafe(sincethebrowser'sXMLSerializershould *generatevalidHTML/XML).Therefore,ifparsingadocumentfromanXHR, *therecommendedapproachistodotheXHRinthemainthread,use *XMLSerializer.serializeToString()ontheresponseXML,andpassthe *resultingstringtotheworker. * *2)LiveNodeListsarenotsupported.DOMmethodsandpropertiessuchas *getElementsByTagName()andchildNodesreturnstandardarrays.Ifyou *wanttheseliststobeupdatedwhennodesareremovedoraddedtothe *document,youmusttakecaretomanuallyupdatethemyourself.
*/
(function (global) { // XML only defines these and the numeric ones:
function encodeTextContentHTML(s) { return s.replace(/[&<>]/g, function (x) { return reverseEntityTable[x];
});
}
function encodeHTML(s) { return s.replace(/[&<>'"]/g, function (x) { return reverseEntityTable[x];
});
}
function decodeHTML(str) { return str
.replace(/&(quot|amp|apos|lt|gt);/g, function (match, tag) { return entityTable[tag];
})
.replace(/(?:x([0-9a-f]+)|([0-9]+));/gi, function (match, hex, numStr) { var num = parseInt(hex || numStr, hex ? 16 : 10);
// these character references are replaced by a conforming HTML parser if (num == 0 || num > 0x10ffff || (num >= 0xd800 && num <= 0xdfff)) {
num = 0xfffd;
}
function getElementsByTagName(tag) {
tag = tag.toUpperCase(); var elems = []; var allTags = tag === "*"; function getElems(node) { var length = node.children.length; for (var i = 0; i < length; i++) { var child = node.children[i]; if (allTags || child.tagName === tag) {
elems.push(child);
}
getElems(child);
}
}
getElems(this);
elems._isLiveNodeList = true; return elems;
}
remove() {
let parent = this.parentNode; if (!parent) { // We were already detached so there's nothing to do. returnthis;
} var childNodes = parent.childNodes; var childIndex = childNodes.indexOf(this); if (childIndex === -1) { thrownew Error("removeChild: node not found");
} this.parentNode = null; var prev = this.previousSibling; var next = this.nextSibling; if (prev) {
prev.nextSibling = next;
} if (next) {
next.previousSibling = prev;
}
childNodes.splice(childIndex, 1);
if (this.nodeType === Node.ELEMENT_NODE) {
prev = this.previousElementSibling;
next = this.nextElementSibling; if (prev) {
prev.nextElementSibling = next;
} if (next) {
next.previousElementSibling = prev;
}
parent.children.splice(parent.children.indexOf(this), 1);
}
replaceChild(newNode, oldNode) { var childNodes = this.childNodes; var childIndex = childNodes.indexOf(oldNode); if (childIndex === -1) { thrownew Error("replaceChild: node not found");
} else { // This will take care of updating the new node if it was somewhere else before: if (newNode.parentNode) {
newNode.remove();
}
childNodes[childIndex] = newNode;
// update the new node's sibling properties, and its new siblings' sibling properties
newNode.nextSibling = oldNode.nextSibling;
newNode.previousSibling = oldNode.previousSibling; if (newNode.nextSibling) {
newNode.nextSibling.previousSibling = newNode;
} if (newNode.previousSibling) {
newNode.previousSibling.nextSibling = newNode;
}
newNode.parentNode = this;
// Now deal with elements before we clear out those values for the old node, // because it can help us take shortcuts here: if (newNode.nodeType === Node.ELEMENT_NODE) { if (oldNode.nodeType === Node.ELEMENT_NODE) { // Both were elements, which makes this easier, we just swap things out:
newNode.previousElementSibling = oldNode.previousElementSibling;
newNode.nextElementSibling = oldNode.nextElementSibling; if (newNode.previousElementSibling) {
newNode.previousElementSibling.nextElementSibling = newNode;
} if (newNode.nextElementSibling) {
newNode.nextElementSibling.previousElementSibling = newNode;
} this.children[this.children.indexOf(oldNode)] = newNode;
} else { // Hard way:
newNode.previousElementSibling = (function () { for (var i = childIndex - 1; i >= 0; i--) { if (childNodes[i].nodeType === Node.ELEMENT_NODE) { return childNodes[i];
}
} returnnull;
})(); if (newNode.previousElementSibling) {
newNode.nextElementSibling =
newNode.previousElementSibling.nextElementSibling;
} else {
newNode.nextElementSibling = (function () { for (var i = childIndex + 1; i < childNodes.length; i++) { if (childNodes[i].nodeType === Node.ELEMENT_NODE) { return childNodes[i];
}
} returnnull;
})();
} if (newNode.previousElementSibling) {
newNode.previousElementSibling.nextElementSibling = newNode;
} if (newNode.nextElementSibling) {
newNode.nextElementSibling.previousElementSibling = newNode;
}
if (newNode.nextElementSibling) { this.children.splice( this.children.indexOf(newNode.nextElementSibling), 0,
newNode
);
} else { this.children.push(newNode);
}
}
} elseif (oldNode.nodeType === Node.ELEMENT_NODE) { // new node is not an element node. // if the old one was, update its element siblings: if (oldNode.previousElementSibling) {
oldNode.previousElementSibling.nextElementSibling =
oldNode.nextElementSibling;
} if (oldNode.nextElementSibling) {
oldNode.nextElementSibling.previousElementSibling =
oldNode.previousElementSibling;
} this.children.splice(this.children.indexOf(oldNode), 1);
// If the old node wasn't an element, neither the new nor the old node was an element, // and the children array and its members shouldn't need any updating.
}
getElementById(id) { function getElem(node) { var length = node.children.length; if (node.id === id) { return node;
} for (var i = 0; i < length; i++) { var el = getElem(node.children[i]); if (el) { return el;
}
} returnnull;
} return getElem(this);
},
createElement(tag) { var node = new Element(tag); return node;
},
createTextNode(text) { var node = new Text();
node.textContent = text; return node;
},
get baseURI() { if (!this.hasOwnProperty("_baseURI")) { this._baseURI = this.documentURI; var baseElements = this.getElementsByTagName("base"); var href = baseElements[0] && baseElements[0].getAttribute("href"); if (href) { try { this._baseURI = new URL(href, this._baseURI).href;
} catch (ex) { /* Just fall back to documentURI */
}
}
} returnthis._baseURI;
},
};
var Element = function (tag) { // We use this to find the closing tag. this._matchingTag = tag; // We're explicitly a non-namespace aware parser, we just pretend it's all HTML. var lastColonIndex = tag.lastIndexOf(":"); if (lastColonIndex != -1) {
tag = tag.substring(lastColonIndex + 1);
} this.attributes = []; this.childNodes = []; this.children = []; this.nextElementSibling = this.previousElementSibling = null; this.localName = tag.toLowerCase(); this.tagName = tag.toUpperCase(); this.style = new Style(this);
};
Element.prototype = {
__proto__: Node.prototype,
nodeType: Node.ELEMENT_NODE,
getElementsByTagName,
get className() { returnthis.getAttribute("class") || "";
},
set className(str) { this.setAttribute("class", str);
},
get id() { returnthis.getAttribute("id") || "";
},
set id(str) { this.setAttribute("id", str);
},
get href() { returnthis.getAttribute("href") || "";
},
set href(str) { this.setAttribute("href", str);
},
get src() { returnthis.getAttribute("src") || "";
},
set src(str) { this.setAttribute("src", str);
},
get srcset() { returnthis.getAttribute("srcset") || "";
},
set srcset(str) { this.setAttribute("srcset", str);
},
get nodeName() { returnthis.tagName;
},
get innerHTML() { function getHTML(node) { var i = 0; for (i = 0; i < node.childNodes.length; i++) { var child = node.childNodes[i]; if (child.localName) {
arr.push("<" + child.localName);
// serialize attribute list for (var j = 0; j < child.attributes.length; j++) { var attr = child.attributes[j]; // the attribute value will be HTML escaped. var val = attr.getEncodedValue(); var quote = !val.includes('"') ? '"' : "'";
arr.push(" " + attr.name + "=" + quote + val + quote);
}
if (child.localName in voidElems && !child.childNodes.length) { // if this is a self-closing element, end it here
arr.push("/>");
} else { // otherwise, add its children
arr.push(">");
getHTML(child);
arr.push("</" + child.localName + ">");
}
} else { // This is a text node, so asking for innerHTML won't recurse.
arr.push(child.innerHTML);
}
}
}
// Using Array.join() avoids the overhead from lazy string concatenation. var arr = [];
getHTML(this); return arr.join("");
},
set innerHTML(html) { var parser = new JSDOMParser(); var node = parser.parse(html); var i; for (i = this.childNodes.length; --i >= 0; ) { this.childNodes[i].parentNode = null;
} this.childNodes = node.childNodes; this.children = node.children; for (i = this.childNodes.length; --i >= 0; ) { this.childNodes[i].parentNode = this;
}
},
set textContent(text) { // clear parentNodes for existing children for (var i = this.childNodes.length; --i >= 0; ) { this.childNodes[i].parentNode = null;
}
var node = new Text(); this.childNodes = [node]; this.children = [];
node.textContent = text;
node.parentNode = this;
},
get textContent() { function getText(node) { var nodes = node.childNodes; for (var i = 0; i < nodes.length; i++) { var child = nodes[i]; if (child.nodeType === 3) {
text.push(child.textContent);
} else {
getText(child);
}
}
}
var Style = function (node) { this.node = node;
};
// getStyle() and setStyle() use the style attribute string directly. This // won't be very efficient if there are a lot of style manipulations, but // it's the easiest way to make sure the style attribute string and the JS // style property stay in sync. Readability.js doesn't do many style // manipulations, so this should be okay.
Style.prototype = {
getStyle(styleName) { var attr = this.node.getAttribute("style"); if (!attr) { return undefined;
}
var styles = attr.split(";"); for (var i = 0; i < styles.length; i++) { var style = styles[i].split(":"); var name = style[0].trim(); if (name === styleName) { return style[1].trim();
}
}
return undefined;
},
setStyle(styleName, styleValue) { var value = this.node.getAttribute("style") || ""; var index = 0; do { var next = value.indexOf(";", index) + 1; var length = next - index - 1; var style =
length > 0 ? value.substr(index, length) : value.substr(index); if (style.substr(0, style.indexOf(":")).trim() === styleName) {
value =
value.substr(0, index).trim() +
(next ? " " + value.substr(next).trim() : ""); break;
}
index = next;
} while (index);
// For each item in styleMap, define a getter and setter on the style // property. for (var jsName in styleMap) {
(function (cssName) {
Style.prototype.__defineGetter__(jsName, function () { returnthis.getStyle(cssName);
});
Style.prototype.__defineSetter__(jsName, function (value) { this.setStyle(cssName, value);
});
})(styleMap[jsName]);
}
var JSDOMParser = function () { this.currentChar = 0;
// In makeElementNode() we build up many strings one char at a time. Using // += for this results in lots of short-lived intermediate strings. It's // better to build an array of single-char strings and then join() them // together at the end. And reusing a single array (i.e. |this.strBuf|) // over and over for this purpose uses less memory than using a new array // for each string. this.strBuf = [];
// Similarly, we reuse this array to return the two arguments from // makeElementNode(), which saves us from having to allocate a new array // every time. this.retPair = [];
this.errorState = "";
};
JSDOMParser.prototype = {
error(m) { if (typeof console !== "undefined") { // eslint-disable-next-line no-console
console.log("JSDOMParser error: " + m + "\n");
} elseif (typeof dump !== "undefined") { /* global dump */
dump("JSDOMParser error: " + m + "\n");
} this.errorState += m + "\n";
},
/** *Calledafteraquotecharacterisread.Thisfindsthenextquote *characterandreturnsthetextstringinbetween.
*/
readString(quote) { var str; var n = this.html.indexOf(quote, this.currentChar); if (n === -1) { this.currentChar = this.html.length;
str = null;
} else {
str = this.html.substring(this.currentChar, n); this.currentChar = n + 1;
}
return str;
},
/** *Calledwhenparsinganode.Thisfindsthenextname/valueattribute *pairandaddstheresulttotheattributeslist.
*/
readAttribute(node) { var name = "";
var n = this.html.indexOf("=", this.currentChar); if (n === -1) { this.currentChar = this.html.length;
} else { // Read until a '=' character is hit; this will be the attribute key
name = this.html.substring(this.currentChar, n); this.currentChar = n + 1;
}
if (!name) { return;
}
// After a '=', we should see a '"' for the attribute value var c = this.nextChar(); if (c !== '"' && c !== "'") { this.error("Error reading attribute " + name + ", expecting '\"'"); return;
}
// Read the attribute value (and consume the matching quote) var value = this.readString(c);
/** *ParsesandreturnsanElementnode.Thisiscalledaftera'<'hasbeen *read. * *@returnsanarray;thefirstindexofthearrayistheparsednode; *thesecondindexisabooleanindicatingwhetherthisisavoid *Element
*/
makeElementNode(retPair) { var c = this.nextChar();
// Read the Element tag name var strBuf = this.strBuf;
strBuf.length = 0; while (!whitespace.includes(c) && c !== ">" && c !== "/") { if (c === undefined) { returnfalse;
}
strBuf.push(c);
c = this.nextChar();
} var tag = strBuf.join("");
if (!tag) { returnfalse;
}
var node = new Element(tag);
// Read Element attributes while (c !== "/" && c !== ">") { if (c === undefined) { returnfalse;
} while (whitespace.includes(this.html[this.currentChar++])) { // Advance cursor to first non-whitespace char.
} this.currentChar--;
c = this.nextChar(); if (c !== "/" && c !== ">") {
--this.currentChar; this.readAttribute(node);
}
}
// If this is a self-closing tag, read '/>' var closed = false; if (c === "/") {
closed = true;
c = this.nextChar(); if (c !== ">") { this.error("expected '>' to close " + tag); returnfalse;
}
}
/** *Searchestheinputuntilastringisfoundanddiscardsallinputupto *andincludingthematchedstring.
*/
discardTo(str) { var index = this.html.indexOf(str, this.currentChar) + str.length; if (index === -1) { this.currentChar = this.html.length;
} this.currentChar = index;
},
/** *Readschildnodesforthegivennode.
*/
readChildren(node) { var child; while ((child = this.readNode())) { // Don't keep Comment nodes if (child.nodeType !== 8) {
node.appendChild(child);
}
}
},
discardNextComment() { if (this.match("--")) { this.discardTo("-->");
} else { var c = this.nextChar(); while (c !== ">") { if (c === undefined) { returnnull;
} if (c === '"' || c === "'") { this.readString(c);
}
c = this.nextChar();
}
} returnnew Comment();
},
/** *Readsthenextchildnodefromtheinput.Ifwe'rereadingaclosing *tag,orifwe'vereachedtheendofinput,returnnull. * *@returnsthenode
*/
readNode() { var c = this.nextChar();
if (c === undefined) { returnnull;
}
// Read any text as Text node var textNode; if (c !== "<") {
--this.currentChar;
textNode = new Text(); var n = this.html.indexOf("<", this.currentChar); // We're not expecting XSS type exploitation inside JSDOMParser, // we just have to implement innerHTML stuff... /* eslint-disable no-unsanitized/property */ if (n === -1) {
textNode.innerHTML = this.html.substring( this.currentChar, this.html.length
); this.currentChar = this.html.length;
} else {
textNode.innerHTML = this.html.substring(this.currentChar, n); this.currentChar = n;
} /* eslint-enable no-unsanitized/property */ return textNode;
}
if (this.match("![CDATA[")) { var endChar = this.html.indexOf("]]>", this.currentChar); if (endChar === -1) { this.error("unclosed CDATA section"); returnnull;
}
textNode = new Text();
textNode.textContent = this.html.substring(this.currentChar, endChar); this.currentChar = endChar + "]]>".length; return textNode;
}
c = this.peekNext();
// Read Comment node. Normally, Comment nodes know their inner // textContent, but we don't really care about Comment nodes (we throw // them away in readChildren()). So just returning an empty Comment node // here is sufficient. if (c === "!" || c === "?") { // We're still before the ! or ? that is starting this comment: this.currentChar++; returnthis.discardNextComment();
}
// If we're reading a closing tag, return null. This means we've reached // the end of this set of child nodes. if (c === "/") {
--this.currentChar; returnnull;
}
// Otherwise, we're looking at an Element node var result = this.makeElementNode(this.retPair); if (!result) { returnnull;
}
var node = this.retPair[0]; var closed = this.retPair[1]; var localName = node.localName;
// If this isn't a void Element, read its child nodes if (!closed) { this.readChildren(node); var closingTag = "</" + node._matchingTag + ">"; if (!this.match(closingTag)) { this.error( "expected '" +
closingTag + "' and got " + this.html.substr(this.currentChar, closingTag.length)
); returnnull;
}
}
// Only use the first title, because SVG might have other // title elements which we don't care about (medium.com // does this, at least). if (localName === "title" && !this.doc.title) { this.doc.title = node.textContent.trim();
} elseif (localName === "head") { this.doc.head = node;
} elseif (localName === "body") { this.doc.body = node;
} elseif (localName === "html") { this.doc.documentElement = node;
}
return node;
},
/** *ParsesanHTMLstringandreturnsaJSimplementationoftheDocument.
*/
parse(html, url) { this.html = html; var doc = (this.doc = new Document(url)); this.readChildren(doc);
// If this is an HTML document, remove root-level children except for the // <html> node if (doc.documentElement) { for (var i = doc.childNodes.length; --i >= 0; ) { var child = doc.childNodes[i]; if (child !== doc.documentElement) {
child.remove();
}
}
}
return doc;
},
};
// Attach the standard DOM types to the global scope
global.Node = Node;
global.Comment = Comment;
global.Document = Document;
global.Element = Element;
global.Text = Text;
// Attach JSDOMParser to the global scope
global.JSDOMParser = JSDOMParser;
})(this);
if (typeof module === "object") { /* eslint-disable-next-line no-redeclare */ /* global module */
module.exports = this.JSDOMParser;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.