// v0.4.1 - https://github.com/mozilla/readability/commit/28843b6de84447dd6cef04058fda336938e628dc

/*eslint-env es6:false*/
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * This is a relatively lightweight DOMParser that is safe to use in a web
 * worker. This is far from a complete DOM implementation; however, it should
 * contain the minimal set of functionality necessary for Readability.js.
 *
 * Aside from not implementing the full DOM API, there are other quirks to be
 * aware of when using the JSDOMParser:
 *
 *   1) Properly formed HTML/XML must be used. This means you should be extra
 *      careful when using this parser on anything received directly from an
 *      XMLHttpRequest. Providing a serialized string from an XMLSerializer,
 *      however, should be safe (since the browser's XMLSerializer should
 *      generate valid HTML/XML). Therefore, if parsing a document from an XHR,
 *      the recommended approach is to do the XHR in the main thread, use
 *      XMLSerializer.serializeToString() on the responseXML, and pass the
 *      resulting string to the worker.
 *
 *   2) Live NodeLists are not supported. DOM methods and properties such as
 *      getElementsByTagName() and childNodes return standard arrays. If you
 *      want these lists to be updated when nodes are removed or added to the
 *      document, you must take care to manually update them yourself.
 */
 (function (global) {

	// XML only defines these and the numeric ones:
  
	var entityTable = {
	  "lt": "<",
	  "gt": ">",
	  "amp": "&",
	  "quot": '"',
	  "apos": "'",
	};
  
	var reverseEntityTable = {
	  "<": "&lt;",
	  ">": "&gt;",
	  "&": "&amp;",
	  '"': "&quot;",
	  "'": "&apos;",
	};
  
	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-z]{1,4})|([0-9]{1,4}));/gi, function(match, hex, numStr) {
		var num = parseInt(hex || numStr, hex ? 16 : 10); // read num
		return String.fromCharCode(num);
	  });
	}
  
	// When a style is set in JS, map it to the corresponding CSS attribute
	var styleMap = {
	  "alignmentBaseline": "alignment-baseline",
	  "background": "background",
	  "backgroundAttachment": "background-attachment",
	  "backgroundClip": "background-clip",
	  "backgroundColor": "background-color",
	  "backgroundImage": "background-image",
	  "backgroundOrigin": "background-origin",
	  "backgroundPosition": "background-position",
	  "backgroundPositionX": "background-position-x",
	  "backgroundPositionY": "background-position-y",
	  "backgroundRepeat": "background-repeat",
	  "backgroundRepeatX": "background-repeat-x",
	  "backgroundRepeatY": "background-repeat-y",
	  "backgroundSize": "background-size",
	  "baselineShift": "baseline-shift",
	  "border": "border",
	  "borderBottom": "border-bottom",
	  "borderBottomColor": "border-bottom-color",
	  "borderBottomLeftRadius": "border-bottom-left-radius",
	  "borderBottomRightRadius": "border-bottom-right-radius",
	  "borderBottomStyle": "border-bottom-style",
	  "borderBottomWidth": "border-bottom-width",
	  "borderCollapse": "border-collapse",
	  "borderColor": "border-color",
	  "borderImage": "border-image",
	  "borderImageOutset": "border-image-outset",
	  "borderImageRepeat": "border-image-repeat",
	  "borderImageSlice": "border-image-slice",
	  "borderImageSource": "border-image-source",
	  "borderImageWidth": "border-image-width",
	  "borderLeft": "border-left",
	  "borderLeftColor": "border-left-color",
	  "borderLeftStyle": "border-left-style",
	  "borderLeftWidth": "border-left-width",
	  "borderRadius": "border-radius",
	  "borderRight": "border-right",
	  "borderRightColor": "border-right-color",
	  "borderRightStyle": "border-right-style",
	  "borderRightWidth": "border-right-width",
	  "borderSpacing": "border-spacing",
	  "borderStyle": "border-style",
	  "borderTop": "border-top",
	  "borderTopColor": "border-top-color",
	  "borderTopLeftRadius": "border-top-left-radius",
	  "borderTopRightRadius": "border-top-right-radius",
	  "borderTopStyle": "border-top-style",
	  "borderTopWidth": "border-top-width",
	  "borderWidth": "border-width",
	  "bottom": "bottom",
	  "boxShadow": "box-shadow",
	  "boxSizing": "box-sizing",
	  "captionSide": "caption-side",
	  "clear": "clear",
	  "clip": "clip",
	  "clipPath": "clip-path",
	  "clipRule": "clip-rule",
	  "color": "color",
	  "colorInterpolation": "color-interpolation",
	  "colorInterpolationFilters": "color-interpolation-filters",
	  "colorProfile": "color-profile",
	  "colorRendering": "color-rendering",
	  "content": "content",
	  "counterIncrement": "counter-increment",
	  "counterReset": "counter-reset",
	  "cursor": "cursor",
	  "direction": "direction",
	  "display": "display",
	  "dominantBaseline": "dominant-baseline",
	  "emptyCells": "empty-cells",
	  "enableBackground": "enable-background",
	  "fill": "fill",
	  "fillOpacity": "fill-opacity",
	  "fillRule": "fill-rule",
	  "filter": "filter",
	  "cssFloat": "float",
	  "floodColor": "flood-color",
	  "floodOpacity": "flood-opacity",
	  "font": "font",
	  "fontFamily": "font-family",
	  "fontSize": "font-size",
	  "fontStretch": "font-stretch",
	  "fontStyle": "font-style",
	  "fontVariant": "font-variant",
	  "fontWeight": "font-weight",
	  "glyphOrientationHorizontal": "glyph-orientation-horizontal",
	  "glyphOrientationVertical": "glyph-orientation-vertical",
	  "height": "height",
	  "imageRendering": "image-rendering",
	  "kerning": "kerning",
	  "left": "left",
	  "letterSpacing": "letter-spacing",
	  "lightingColor": "lighting-color",
	  "lineHeight": "line-height",
	  "listStyle": "list-style",
	  "listStyleImage": "list-style-image",
	  "listStylePosition": "list-style-position",
	  "listStyleType": "list-style-type",
	  "margin": "margin",
	  "marginBottom": "margin-bottom",
	  "marginLeft": "margin-left",
	  "marginRight": "margin-right",
	  "marginTop": "margin-top",
	  "marker": "marker",
	  "markerEnd": "marker-end",
	  "markerMid": "marker-mid",
	  "markerStart": "marker-start",
	  "mask": "mask",
	  "maxHeight": "max-height",
	  "maxWidth": "max-width",
	  "minHeight": "min-height",
	  "minWidth": "min-width",
	  "opacity": "opacity",
	  "orphans": "orphans",
	  "outline": "outline",
	  "outlineColor": "outline-color",
	  "outlineOffset": "outline-offset",
	  "outlineStyle": "outline-style",
	  "outlineWidth": "outline-width",
	  "overflow": "overflow",
	  "overflowX": "overflow-x",
	  "overflowY": "overflow-y",
	  "padding": "padding",
	  "paddingBottom": "padding-bottom",
	  "paddingLeft": "padding-left",
	  "paddingRight": "padding-right",
	  "paddingTop": "padding-top",
	  "page": "page",
	  "pageBreakAfter": "page-break-after",
	  "pageBreakBefore": "page-break-before",
	  "pageBreakInside": "page-break-inside",
	  "pointerEvents": "pointer-events",
	  "position": "position",
	  "quotes": "quotes",
	  "resize": "resize",
	  "right": "right",
	  "shapeRendering": "shape-rendering",
	  "size": "size",
	  "speak": "speak",
	  "src": "src",
	  "stopColor": "stop-color",
	  "stopOpacity": "stop-opacity",
	  "stroke": "stroke",
	  "strokeDasharray": "stroke-dasharray",
	  "strokeDashoffset": "stroke-dashoffset",
	  "strokeLinecap": "stroke-linecap",
	  "strokeLinejoin": "stroke-linejoin",
	  "strokeMiterlimit": "stroke-miterlimit",
	  "strokeOpacity": "stroke-opacity",
	  "strokeWidth": "stroke-width",
	  "tableLayout": "table-layout",
	  "textAlign": "text-align",
	  "textAnchor": "text-anchor",
	  "textDecoration": "text-decoration",
	  "textIndent": "text-indent",
	  "textLineThrough": "text-line-through",
	  "textLineThroughColor": "text-line-through-color",
	  "textLineThroughMode": "text-line-through-mode",
	  "textLineThroughStyle": "text-line-through-style",
	  "textLineThroughWidth": "text-line-through-width",
	  "textOverflow": "text-overflow",
	  "textOverline": "text-overline",
	  "textOverlineColor": "text-overline-color",
	  "textOverlineMode": "text-overline-mode",
	  "textOverlineStyle": "text-overline-style",
	  "textOverlineWidth": "text-overline-width",
	  "textRendering": "text-rendering",
	  "textShadow": "text-shadow",
	  "textTransform": "text-transform",
	  "textUnderline": "text-underline",
	  "textUnderlineColor": "text-underline-color",
	  "textUnderlineMode": "text-underline-mode",
	  "textUnderlineStyle": "text-underline-style",
	  "textUnderlineWidth": "text-underline-width",
	  "top": "top",
	  "unicodeBidi": "unicode-bidi",
	  "unicodeRange": "unicode-range",
	  "vectorEffect": "vector-effect",
	  "verticalAlign": "vertical-align",
	  "visibility": "visibility",
	  "whiteSpace": "white-space",
	  "widows": "widows",
	  "width": "width",
	  "wordBreak": "word-break",
	  "wordSpacing": "word-spacing",
	  "wordWrap": "word-wrap",
	  "writingMode": "writing-mode",
	  "zIndex": "z-index",
	  "zoom": "zoom",
	};
  
	// Elements that can be self-closing
	var voidElems = {
	  "area": true,
	  "base": true,
	  "br": true,
	  "col": true,
	  "command": true,
	  "embed": true,
	  "hr": true,
	  "img": true,
	  "input": true,
	  "link": true,
	  "meta": true,
	  "param": true,
	  "source": true,
	  "wbr": true
	};
  
	var whitespace = [" ", "\t", "\n", "\r"];
  
	// See https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
	var nodeTypes = {
	  ELEMENT_NODE: 1,
	  ATTRIBUTE_NODE: 2,
	  TEXT_NODE: 3,
	  CDATA_SECTION_NODE: 4,
	  ENTITY_REFERENCE_NODE: 5,
	  ENTITY_NODE: 6,
	  PROCESSING_INSTRUCTION_NODE: 7,
	  COMMENT_NODE: 8,
	  DOCUMENT_NODE: 9,
	  DOCUMENT_TYPE_NODE: 10,
	  DOCUMENT_FRAGMENT_NODE: 11,
	  NOTATION_NODE: 12
	};
  
	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;
	}
  
	var Node = function () {};
  
	Node.prototype = {
	  attributes: null,
	  childNodes: null,
	  localName: null,
	  nodeName: null,
	  parentNode: null,
	  textContent: null,
	  nextSibling: null,
	  previousSibling: null,
  
	  get firstChild() {
		return this.childNodes[0] || null;
	  },
  
	  get firstElementChild() {
		return this.children[0] || null;
	  },
  
	  get lastChild() {
		return this.childNodes[this.childNodes.length - 1] || null;
	  },
  
	  get lastElementChild() {
		return this.children[this.children.length - 1] || null;
	  },
  
	  appendChild: function (child) {
		if (child.parentNode) {
		  child.parentNode.removeChild(child);
		}
  
		var last = this.lastChild;
		if (last)
		  last.nextSibling = child;
		child.previousSibling = last;
  
		if (child.nodeType === Node.ELEMENT_NODE) {
		  child.previousElementSibling = this.children[this.children.length - 1] || null;
		  this.children.push(child);
		  child.previousElementSibling && (child.previousElementSibling.nextElementSibling = child);
		}
		this.childNodes.push(child);
		child.parentNode = this;
	  },
  
	  removeChild: function (child) {
		var childNodes = this.childNodes;
		var childIndex = childNodes.indexOf(child);
		if (childIndex === -1) {
		  throw "removeChild: node not found";
		} else {
		  child.parentNode = null;
		  var prev = child.previousSibling;
		  var next = child.nextSibling;
		  if (prev)
			prev.nextSibling = next;
		  if (next)
			next.previousSibling = prev;
  
		  if (child.nodeType === Node.ELEMENT_NODE) {
			prev = child.previousElementSibling;
			next = child.nextElementSibling;
			if (prev)
			  prev.nextElementSibling = next;
			if (next)
			  next.previousElementSibling = prev;
			this.children.splice(this.children.indexOf(child), 1);
		  }
  
		  child.previousSibling = child.nextSibling = null;
		  child.previousElementSibling = child.nextElementSibling = null;
  
		  return childNodes.splice(childIndex, 1)[0];
		}
	  },
  
	  replaceChild: function (newNode, oldNode) {
		var childNodes = this.childNodes;
		var childIndex = childNodes.indexOf(oldNode);
		if (childIndex === -1) {
		  throw "replaceChild: node not found";
		} else {
		  // This will take care of updating the new node if it was somewhere else before:
		  if (newNode.parentNode)
			newNode.parentNode.removeChild(newNode);
  
		  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];
				}
				return null;
			  })();
			  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];
				  }
				  return null;
				})();
			  }
			  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);
			}
		  } else if (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.
		  }
  
  
		  oldNode.parentNode = null;
		  oldNode.previousSibling = null;
		  oldNode.nextSibling = null;
		  if (oldNode.nodeType === Node.ELEMENT_NODE) {
			oldNode.previousElementSibling = null;
			oldNode.nextElementSibling = null;
		  }
		  return oldNode;
		}
	  },
  
	  __JSDOMParser__: true,
	};
  
	for (var nodeType in nodeTypes) {
	  Node[nodeType] = Node.prototype[nodeType] = nodeTypes[nodeType];
	}
  
	var Attribute = function (name, value) {
	  this.name = name;
	  this._value = value;
	};
  
	Attribute.prototype = {
	  get value() {
		return this._value;
	  },
	  setValue: function(newValue) {
		this._value = newValue;
	  },
	  getEncodedValue: function() {
		return encodeHTML(this._value);
	  },
	};
  
	var Comment = function () {
	  this.childNodes = [];
	};
  
	Comment.prototype = {
	  __proto__: Node.prototype,
  
	  nodeName: "#comment",
	  nodeType: Node.COMMENT_NODE
	};
  
	var Text = function () {
	  this.childNodes = [];
	};
  
	Text.prototype = {
	  __proto__: Node.prototype,
  
	  nodeName: "#text",
	  nodeType: Node.TEXT_NODE,
	  get textContent() {
		if (typeof this._textContent === "undefined") {
		  this._textContent = decodeHTML(this._innerHTML || "");
		}
		return this._textContent;
	  },
	  get innerHTML() {
		if (typeof this._innerHTML === "undefined") {
		  this._innerHTML = encodeTextContentHTML(this._textContent || "");
		}
		return this._innerHTML;
	  },
  
	  set innerHTML(newHTML) {
		this._innerHTML = newHTML;
		delete this._textContent;
	  },
	  set textContent(newText) {
		this._textContent = newText;
		delete this._innerHTML;
	  },
	};
  
	var Document = function (url) {
	  this.documentURI = url;
	  this.styleSheets = [];
	  this.childNodes = [];
	  this.children = [];
	};
  
	Document.prototype = {
	  __proto__: Node.prototype,
  
	  nodeName: "#document",
	  nodeType: Node.DOCUMENT_NODE,
	  title: "",
  
	  getElementsByTagName: getElementsByTagName,
  
	  getElementById: function (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;
		  }
		  return null;
		}
		return getElem(this);
	  },
  
	  createElement: function (tag) {
		var node = new Element(tag);
		return node;
	  },
  
	  createTextNode: function (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 */}
		  }
		}
		return this._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: getElementsByTagName,
  
	  get className() {
		return this.getAttribute("class") || "";
	  },
  
	  set className(str) {
		this.setAttribute("class", str);
	  },
  
	  get id() {
		return this.getAttribute("id") || "";
	  },
  
	  set id(str) {
		this.setAttribute("id", str);
	  },
  
	  get href() {
		return this.getAttribute("href") || "";
	  },
  
	  set href(str) {
		this.setAttribute("href", str);
	  },
  
	  get src() {
		return this.getAttribute("src") || "";
	  },
  
	  set src(str) {
		this.setAttribute("src", str);
	  },
  
	  get srcset() {
		return this.getAttribute("srcset") || "";
	  },
  
	  set srcset(str) {
		this.setAttribute("srcset", str);
	  },
  
	  get nodeName() {
		return this.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.indexOf('"') === -1 ? '"' : "'");
				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);
			}
		  }
		}
  
		// Using Array.join() avoids the overhead from lazy string concatenation.
		// See http://blog.cdleary.com/2012/01/string-representation-in-spidermonkey/#ropes
		var text = [];
		getText(this);
		return text.join("");
	  },
  
	  getAttribute: function (name) {
		for (var i = this.attributes.length; --i >= 0;) {
		  var attr = this.attributes[i];
		  if (attr.name === name) {
			return attr.value;
		  }
		}
		return undefined;
	  },
  
	  setAttribute: function (name, value) {
		for (var i = this.attributes.length; --i >= 0;) {
		  var attr = this.attributes[i];
		  if (attr.name === name) {
			attr.setValue(value);
			return;
		  }
		}
		this.attributes.push(new Attribute(name, value));
	  },
  
	  removeAttribute: function (name) {
		for (var i = this.attributes.length; --i >= 0;) {
		  var attr = this.attributes[i];
		  if (attr.name === name) {
			this.attributes.splice(i, 1);
			break;
		  }
		}
	  },
  
	  hasAttribute: function (name) {
		return this.attributes.some(function (attr) {
		  return attr.name == name;
		});
	  },
	};
  
	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: function (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: function (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);
  
		value += " " + styleName + ": " + styleValue + ";";
		this.node.setAttribute("style", value.trim());
	  }
	};
  
	// 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 () {
		  return this.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: function(m) {
		if (typeof dump !== "undefined") {
		  dump("JSDOMParser error: " + m + "\n");
		} else if (typeof console !== "undefined") {
		  console.log("JSDOMParser error: " + m + "\n");
		}
		this.errorState += m + "\n";
	  },
  
	  /**
	   * Look at the next character without advancing the index.
	   */
	  peekNext: function () {
		return this.html[this.currentChar];
	  },
  
	  /**
	   * Get the next character and advance the index.
	   */
	  nextChar: function () {
		return this.html[this.currentChar++];
	  },
  
	  /**
	   * Called after a quote character is read. This finds the next quote
	   * character and returns the text string in between.
	   */
	  readString: function (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;
	  },
  
	  /**
	   * Called when parsing a node. This finds the next name/value attribute
	   * pair and adds the result to the attributes list.
	   */
	  readAttribute: function (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);
  
		node.attributes.push(new Attribute(name, decodeHTML(value)));
  
		return;
	  },
  
	  /**
	   * Parses and returns an Element node. This is called after a '<' has been
	   * read.
	   *
	   * @returns an array; the first index of the array is the parsed node;
	   *          the second index is a boolean indicating whether this is a void
	   *          Element
	   */
	  makeElementNode: function (retPair) {
		var c = this.nextChar();
  
		// Read the Element tag name
		var strBuf = this.strBuf;
		strBuf.length = 0;
		while (whitespace.indexOf(c) == -1 && c !== ">" && c !== "/") {
		  if (c === undefined)
			return false;
		  strBuf.push(c);
		  c = this.nextChar();
		}
		var tag = strBuf.join("");
  
		if (!tag)
		  return false;
  
		var node = new Element(tag);
  
		// Read Element attributes
		while (c !== "/" && c !== ">") {
		  if (c === undefined)
			return false;
		  while (whitespace.indexOf(this.html[this.currentChar++]) != -1) {
			// 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);
			return false;
		  }
		}
  
		retPair[0] = node;
		retPair[1] = closed;
		return true;
	  },
  
	  /**
	   * If the current input matches this string, advance the input index;
	   * otherwise, do nothing.
	   *
	   * @returns whether input matched string
	   */
	  match: function (str) {
		var strlen = str.length;
		if (this.html.substr(this.currentChar, strlen).toLowerCase() === str.toLowerCase()) {
		  this.currentChar += strlen;
		  return true;
		}
		return false;
	  },
  
	  /**
	   * Searches the input until a string is found and discards all input up to
	   * and including the matched string.
	   */
	  discardTo: function (str) {
		var index = this.html.indexOf(str, this.currentChar) + str.length;
		if (index === -1)
		  this.currentChar = this.html.length;
		this.currentChar = index;
	  },
  
	  /**
	   * Reads child nodes for the given node.
	   */
	  readChildren: function (node) {
		var child;
		while ((child = this.readNode())) {
		  // Don't keep Comment nodes
		  if (child.nodeType !== 8) {
			node.appendChild(child);
		  }
		}
	  },
  
	  discardNextComment: function() {
		if (this.match("--")) {
		  this.discardTo("-->");
		} else {
		  var c = this.nextChar();
		  while (c !== ">") {
			if (c === undefined)
			  return null;
			if (c === '"' || c === "'")
			  this.readString(c);
			c = this.nextChar();
		  }
		}
		return new Comment();
	  },
  
  
	  /**
	   * Reads the next child node from the input. If we're reading a closing
	   * tag, or if we've reached the end of input, return null.
	   *
	   * @returns the node
	   */
	  readNode: function () {
		var c = this.nextChar();
  
		if (c === undefined)
		  return null;
  
		// Read any text as Text node
		var textNode;
		if (c !== "<") {
		  --this.currentChar;
		  textNode = new Text();
		  var n = this.html.indexOf("<", this.currentChar);
		  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;
		  }
		  return textNode;
		}
  
		if (this.match("![CDATA[")) {
		  var endChar = this.html.indexOf("]]>", this.currentChar);
		  if (endChar === -1) {
			this.error("unclosed CDATA section");
			return null;
		  }
		  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++;
		  return this.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;
		  return null;
		}
  
		// Otherwise, we're looking at an Element node
		var result = this.makeElementNode(this.retPair);
		if (!result)
		  return null;
  
		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));
			return null;
		  }
		}
  
		// 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();
		} else if (localName === "head") {
		  this.doc.head = node;
		} else if (localName === "body") {
		  this.doc.body = node;
		} else if (localName === "html") {
		  this.doc.documentElement = node;
		}
  
		return node;
	  },
  
	  /**
	   * Parses an HTML string and returns a JS implementation of the Document.
	   */
	  parse: function (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) {
			  doc.removeChild(child);
			}
		  }
		}
  
		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") {
	module.exports = this.JSDOMParser;
  }