mobile/android/chrome/content/JSDOMParser.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/chrome/content/JSDOMParser.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,912 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +/**
     1.9 + * This is a relatively lightweight DOMParser that is safe to use in a web
    1.10 + * worker. This is far from a complete DOM implementation; however, it should
    1.11 + * contain the minimal set of functionality necessary for Readability.js.
    1.12 + *
    1.13 + * Aside from not implementing the full DOM API, there are other quirks to be
    1.14 + * aware of when using the JSDOMParser:
    1.15 + *
    1.16 + *   1) Properly formed HTML/XML must be used. This means you should be extra
    1.17 + *      careful when using this parser on anything received directly from an
    1.18 + *      XMLHttpRequest. Providing a serialized string from an XMLSerializer,
    1.19 + *      however, should be safe (since the browser's XMLSerializer should
    1.20 + *      generate valid HTML/XML). Therefore, if parsing a document from an XHR,
    1.21 + *      the recommended approach is to do the XHR in the main thread, use
    1.22 + *      XMLSerializer.serializeToString() on the responseXML, and pass the
    1.23 + *      resulting string to the worker.
    1.24 + *
    1.25 + *   2) Live NodeLists are not supported. DOM methods and properties such as
    1.26 + *      getElementsByTagName() and childNodes return standard arrays. If you
    1.27 + *      want these lists to be updated when nodes are removed or added to the
    1.28 + *      document, you must take care to manually update them yourself.
    1.29 + */
    1.30 +(function (global) {
    1.31 +
    1.32 +  function error(m) {
    1.33 +    dump("JSDOMParser error: " + m);
    1.34 +  }
    1.35 +
    1.36 +  // When a style is set in JS, map it to the corresponding CSS attribute
    1.37 +  let styleMap = {
    1.38 +    "alignmentBaseline": "alignment-baseline",
    1.39 +    "background": "background",
    1.40 +    "backgroundAttachment": "background-attachment",
    1.41 +    "backgroundClip": "background-clip",
    1.42 +    "backgroundColor": "background-color",
    1.43 +    "backgroundImage": "background-image",
    1.44 +    "backgroundOrigin": "background-origin",
    1.45 +    "backgroundPosition": "background-position",
    1.46 +    "backgroundPositionX": "background-position-x",
    1.47 +    "backgroundPositionY": "background-position-y",
    1.48 +    "backgroundRepeat": "background-repeat",
    1.49 +    "backgroundRepeatX": "background-repeat-x",
    1.50 +    "backgroundRepeatY": "background-repeat-y",
    1.51 +    "backgroundSize": "background-size",
    1.52 +    "baselineShift": "baseline-shift",
    1.53 +    "border": "border",
    1.54 +    "borderBottom": "border-bottom",
    1.55 +    "borderBottomColor": "border-bottom-color",
    1.56 +    "borderBottomLeftRadius": "border-bottom-left-radius",
    1.57 +    "borderBottomRightRadius": "border-bottom-right-radius",
    1.58 +    "borderBottomStyle": "border-bottom-style",
    1.59 +    "borderBottomWidth": "border-bottom-width",
    1.60 +    "borderCollapse": "border-collapse",
    1.61 +    "borderColor": "border-color",
    1.62 +    "borderImage": "border-image",
    1.63 +    "borderImageOutset": "border-image-outset",
    1.64 +    "borderImageRepeat": "border-image-repeat",
    1.65 +    "borderImageSlice": "border-image-slice",
    1.66 +    "borderImageSource": "border-image-source",
    1.67 +    "borderImageWidth": "border-image-width",
    1.68 +    "borderLeft": "border-left",
    1.69 +    "borderLeftColor": "border-left-color",
    1.70 +    "borderLeftStyle": "border-left-style",
    1.71 +    "borderLeftWidth": "border-left-width",
    1.72 +    "borderRadius": "border-radius",
    1.73 +    "borderRight": "border-right",
    1.74 +    "borderRightColor": "border-right-color",
    1.75 +    "borderRightStyle": "border-right-style",
    1.76 +    "borderRightWidth": "border-right-width",
    1.77 +    "borderSpacing": "border-spacing",
    1.78 +    "borderStyle": "border-style",
    1.79 +    "borderTop": "border-top",
    1.80 +    "borderTopColor": "border-top-color",
    1.81 +    "borderTopLeftRadius": "border-top-left-radius",
    1.82 +    "borderTopRightRadius": "border-top-right-radius",
    1.83 +    "borderTopStyle": "border-top-style",
    1.84 +    "borderTopWidth": "border-top-width",
    1.85 +    "borderWidth": "border-width",
    1.86 +    "bottom": "bottom",
    1.87 +    "boxShadow": "box-shadow",
    1.88 +    "boxSizing": "box-sizing",
    1.89 +    "captionSide": "caption-side",
    1.90 +    "clear": "clear",
    1.91 +    "clip": "clip",
    1.92 +    "clipPath": "clip-path",
    1.93 +    "clipRule": "clip-rule",
    1.94 +    "color": "color",
    1.95 +    "colorInterpolation": "color-interpolation",
    1.96 +    "colorInterpolationFilters": "color-interpolation-filters",
    1.97 +    "colorProfile": "color-profile",
    1.98 +    "colorRendering": "color-rendering",
    1.99 +    "content": "content",
   1.100 +    "counterIncrement": "counter-increment",
   1.101 +    "counterReset": "counter-reset",
   1.102 +    "cursor": "cursor",
   1.103 +    "direction": "direction",
   1.104 +    "display": "display",
   1.105 +    "dominantBaseline": "dominant-baseline",
   1.106 +    "emptyCells": "empty-cells",
   1.107 +    "enableBackground": "enable-background",
   1.108 +    "fill": "fill",
   1.109 +    "fillOpacity": "fill-opacity",
   1.110 +    "fillRule": "fill-rule",
   1.111 +    "filter": "filter",
   1.112 +    "cssFloat": "float",
   1.113 +    "floodColor": "flood-color",
   1.114 +    "floodOpacity": "flood-opacity",
   1.115 +    "font": "font",
   1.116 +    "fontFamily": "font-family",
   1.117 +    "fontSize": "font-size",
   1.118 +    "fontStretch": "font-stretch",
   1.119 +    "fontStyle": "font-style",
   1.120 +    "fontVariant": "font-variant",
   1.121 +    "fontWeight": "font-weight",
   1.122 +    "glyphOrientationHorizontal": "glyph-orientation-horizontal",
   1.123 +    "glyphOrientationVertical": "glyph-orientation-vertical",
   1.124 +    "height": "height",
   1.125 +    "imageRendering": "image-rendering",
   1.126 +    "kerning": "kerning",
   1.127 +    "left": "left",
   1.128 +    "letterSpacing": "letter-spacing",
   1.129 +    "lightingColor": "lighting-color",
   1.130 +    "lineHeight": "line-height",
   1.131 +    "listStyle": "list-style",
   1.132 +    "listStyleImage": "list-style-image",
   1.133 +    "listStylePosition": "list-style-position",
   1.134 +    "listStyleType": "list-style-type",
   1.135 +    "margin": "margin",
   1.136 +    "marginBottom": "margin-bottom",
   1.137 +    "marginLeft": "margin-left",
   1.138 +    "marginRight": "margin-right",
   1.139 +    "marginTop": "margin-top",
   1.140 +    "marker": "marker",
   1.141 +    "markerEnd": "marker-end",
   1.142 +    "markerMid": "marker-mid",
   1.143 +    "markerStart": "marker-start",
   1.144 +    "mask": "mask",
   1.145 +    "maxHeight": "max-height",
   1.146 +    "maxWidth": "max-width",
   1.147 +    "minHeight": "min-height",
   1.148 +    "minWidth": "min-width",
   1.149 +    "opacity": "opacity",
   1.150 +    "orphans": "orphans",
   1.151 +    "outline": "outline",
   1.152 +    "outlineColor": "outline-color",
   1.153 +    "outlineOffset": "outline-offset",
   1.154 +    "outlineStyle": "outline-style",
   1.155 +    "outlineWidth": "outline-width",
   1.156 +    "overflow": "overflow",
   1.157 +    "overflowX": "overflow-x",
   1.158 +    "overflowY": "overflow-y",
   1.159 +    "padding": "padding",
   1.160 +    "paddingBottom": "padding-bottom",
   1.161 +    "paddingLeft": "padding-left",
   1.162 +    "paddingRight": "padding-right",
   1.163 +    "paddingTop": "padding-top",
   1.164 +    "page": "page",
   1.165 +    "pageBreakAfter": "page-break-after",
   1.166 +    "pageBreakBefore": "page-break-before",
   1.167 +    "pageBreakInside": "page-break-inside",
   1.168 +    "pointerEvents": "pointer-events",
   1.169 +    "position": "position",
   1.170 +    "quotes": "quotes",
   1.171 +    "resize": "resize",
   1.172 +    "right": "right",
   1.173 +    "shapeRendering": "shape-rendering",
   1.174 +    "size": "size",
   1.175 +    "speak": "speak",
   1.176 +    "src": "src",
   1.177 +    "stopColor": "stop-color",
   1.178 +    "stopOpacity": "stop-opacity",
   1.179 +    "stroke": "stroke",
   1.180 +    "strokeDasharray": "stroke-dasharray",
   1.181 +    "strokeDashoffset": "stroke-dashoffset",
   1.182 +    "strokeLinecap": "stroke-linecap",
   1.183 +    "strokeLinejoin": "stroke-linejoin",
   1.184 +    "strokeMiterlimit": "stroke-miterlimit",
   1.185 +    "strokeOpacity": "stroke-opacity",
   1.186 +    "strokeWidth": "stroke-width",
   1.187 +    "tableLayout": "table-layout",
   1.188 +    "textAlign": "text-align",
   1.189 +    "textAnchor": "text-anchor",
   1.190 +    "textDecoration": "text-decoration",
   1.191 +    "textIndent": "text-indent",
   1.192 +    "textLineThrough": "text-line-through",
   1.193 +    "textLineThroughColor": "text-line-through-color",
   1.194 +    "textLineThroughMode": "text-line-through-mode",
   1.195 +    "textLineThroughStyle": "text-line-through-style",
   1.196 +    "textLineThroughWidth": "text-line-through-width",
   1.197 +    "textOverflow": "text-overflow",
   1.198 +    "textOverline": "text-overline",
   1.199 +    "textOverlineColor": "text-overline-color",
   1.200 +    "textOverlineMode": "text-overline-mode",
   1.201 +    "textOverlineStyle": "text-overline-style",
   1.202 +    "textOverlineWidth": "text-overline-width",
   1.203 +    "textRendering": "text-rendering",
   1.204 +    "textShadow": "text-shadow",
   1.205 +    "textTransform": "text-transform",
   1.206 +    "textUnderline": "text-underline",
   1.207 +    "textUnderlineColor": "text-underline-color",
   1.208 +    "textUnderlineMode": "text-underline-mode",
   1.209 +    "textUnderlineStyle": "text-underline-style",
   1.210 +    "textUnderlineWidth": "text-underline-width",
   1.211 +    "top": "top",
   1.212 +    "unicodeBidi": "unicode-bidi",
   1.213 +    "unicodeRange": "unicode-range",
   1.214 +    "vectorEffect": "vector-effect",
   1.215 +    "verticalAlign": "vertical-align",
   1.216 +    "visibility": "visibility",
   1.217 +    "whiteSpace": "white-space",
   1.218 +    "widows": "widows",
   1.219 +    "width": "width",
   1.220 +    "wordBreak": "word-break",
   1.221 +    "wordSpacing": "word-spacing",
   1.222 +    "wordWrap": "word-wrap",
   1.223 +    "writingMode": "writing-mode",
   1.224 +    "zIndex": "z-index",
   1.225 +    "zoom": "zoom",
   1.226 +  };
   1.227 +
   1.228 +  // Elements that can be self-closing
   1.229 +  let voidElems = {
   1.230 +    "area": true,
   1.231 +    "base": true,
   1.232 +    "br": true,
   1.233 +    "col": true,
   1.234 +    "command": true,
   1.235 +    "embed": true,
   1.236 +    "hr": true,
   1.237 +    "img": true,
   1.238 +    "input": true,
   1.239 +    "link": true,
   1.240 +    "meta": true,
   1.241 +    "param": true,
   1.242 +    "source": true,
   1.243 +  };
   1.244 +
   1.245 +  // See http://www.w3schools.com/dom/dom_nodetype.asp
   1.246 +  let nodeTypes = {
   1.247 +    ELEMENT_NODE: 1,
   1.248 +    ATTRIBUTE_NODE: 2,
   1.249 +    TEXT_NODE: 3,
   1.250 +    CDATA_SECTION_NODE: 4,
   1.251 +    ENTITY_REFERENCE_NODE: 5,
   1.252 +    ENTITY_NODE: 6,
   1.253 +    PROCESSING_INSTRUCTION_NODE: 7,
   1.254 +    COMMENT_NODE: 8,
   1.255 +    DOCUMENT_NODE: 9,
   1.256 +    DOCUMENT_TYPE_NODE: 10,
   1.257 +    DOCUMENT_FRAGMENT_NODE: 11,
   1.258 +    NOTATION_NODE: 12
   1.259 +  };
   1.260 +
   1.261 +  function getElementsByTagName(tag) {
   1.262 +    tag = tag.toUpperCase();
   1.263 +    let elems = [];
   1.264 +    let allTags = (tag === "*");
   1.265 +    function getElems(node) {
   1.266 +      let length = node.childNodes.length;
   1.267 +      for (let i = 0; i < length; i++) {
   1.268 +        let child = node.childNodes[i];
   1.269 +        if (child.nodeType !== 1)
   1.270 +          continue;
   1.271 +        if (allTags || (child.tagName === tag))
   1.272 +          elems.push(child);
   1.273 +        getElems(child);
   1.274 +      }
   1.275 +    }
   1.276 +    getElems(this);
   1.277 +    return elems;
   1.278 +  }
   1.279 +
   1.280 +  let Node = function () {};
   1.281 +
   1.282 +  Node.prototype = {
   1.283 +    attributes: null,
   1.284 +    childNodes: null,
   1.285 +    localName: null,
   1.286 +    nodeName: null,
   1.287 +    parentNode: null,
   1.288 +    textContent: null,
   1.289 +
   1.290 +    get firstChild() {
   1.291 +      return this.childNodes[0] || null;
   1.292 +    },
   1.293 +
   1.294 +    get nextSibling() {
   1.295 +      if (this.parentNode) {
   1.296 +        let childNodes = this.parentNode.childNodes;
   1.297 +        return childNodes[childNodes.indexOf(this) + 1] || null;
   1.298 +      }
   1.299 +
   1.300 +      return null;
   1.301 +    },
   1.302 +
   1.303 +    appendChild: function (child) {
   1.304 +      if (child.parentNode) {
   1.305 +        child.parentNode.removeChild(child);
   1.306 +      }
   1.307 +
   1.308 +      this.childNodes.push(child);
   1.309 +      child.parentNode = this;
   1.310 +    },
   1.311 +
   1.312 +    removeChild: function (child) {
   1.313 +      let childNodes = this.childNodes;
   1.314 +      let childIndex = childNodes.indexOf(child);
   1.315 +      if (childIndex === -1) {
   1.316 +        throw "removeChild: node not found";
   1.317 +      } else {
   1.318 +        child.parentNode = null;
   1.319 +        return childNodes.splice(childIndex, 1)[0];
   1.320 +      }
   1.321 +    },
   1.322 +
   1.323 +    replaceChild: function (newNode, oldNode) {
   1.324 +      let childNodes = this.childNodes;
   1.325 +      let childIndex = childNodes.indexOf(oldNode);
   1.326 +      if (childIndex === -1) {
   1.327 +        throw "replaceChild: node not found";
   1.328 +      } else {
   1.329 +        if (newNode.parentNode)
   1.330 +          newNode.parentNode.removeChild(newNode);
   1.331 +
   1.332 +        childNodes[childIndex] = newNode;
   1.333 +        newNode.parentNode = this;
   1.334 +        oldNode.parentNode = null;
   1.335 +        return oldNode;
   1.336 +      }
   1.337 +    }
   1.338 +  };
   1.339 +
   1.340 +  for (let i in nodeTypes) {
   1.341 +    Node[i] = Node.prototype[i] = nodeTypes[i];
   1.342 +  }
   1.343 +
   1.344 +  let Attribute = function (name, value) {
   1.345 +    this.name = name;
   1.346 +    this.value = value;
   1.347 +  };
   1.348 +
   1.349 +  let Comment = function () {
   1.350 +    this.childNodes = [];
   1.351 +  };
   1.352 +
   1.353 +  Comment.prototype = {
   1.354 +    __proto__: Node.prototype,
   1.355 +
   1.356 +    nodeName: "#comment",
   1.357 +    nodeType: Node.COMMENT_NODE
   1.358 +  };
   1.359 +
   1.360 +  let Text = function () {
   1.361 +    this.childNodes = [];
   1.362 +  };
   1.363 +
   1.364 +  Text.prototype = {
   1.365 +    __proto__: Node.prototype,
   1.366 +
   1.367 +    nodeName: "#text",
   1.368 +    nodeType: Node.TEXT_NODE,
   1.369 +    textContent: ""
   1.370 +  }
   1.371 +
   1.372 +  let Document = function () {
   1.373 +    this.styleSheets = [];
   1.374 +    this.childNodes = [];
   1.375 +  };
   1.376 +
   1.377 +  Document.prototype = {
   1.378 +    __proto__: Node.prototype,
   1.379 +
   1.380 +    nodeName: "#document",
   1.381 +    nodeType: Node.DOCUMENT_NODE,
   1.382 +    title: "",
   1.383 +
   1.384 +    getElementsByTagName: getElementsByTagName,
   1.385 +
   1.386 +    getElementById: function (id) {
   1.387 +      function getElem(node) {
   1.388 +        let length = node.childNodes.length;
   1.389 +        if (node.id === id)
   1.390 +          return node;
   1.391 +        for (let i = 0; i < length; i++) {
   1.392 +          let el = getElem(node.childNodes[i]);
   1.393 +          if (el)
   1.394 +            return el;
   1.395 +        }
   1.396 +        return null;
   1.397 +      }
   1.398 +      return getElem(this);
   1.399 +    },
   1.400 +
   1.401 +    createElement: function (tag) {
   1.402 +      let node = new Element(tag);
   1.403 +      return node;
   1.404 +    }
   1.405 +  };
   1.406 +
   1.407 +  let Element = function (tag) {
   1.408 +    this.attributes = [];
   1.409 +    this.childNodes = [];
   1.410 +    this.localName = tag.toLowerCase();
   1.411 +    this.tagName = tag.toUpperCase();
   1.412 +    this.style = new Style(this);
   1.413 +  };
   1.414 +
   1.415 +  Element.prototype = {
   1.416 +    __proto__: Node.prototype,
   1.417 +
   1.418 +    nodeType: Node.ELEMENT_NODE,
   1.419 +
   1.420 +    getElementsByTagName: getElementsByTagName,
   1.421 +
   1.422 +    get className() {
   1.423 +      return this.getAttribute("class") || "";
   1.424 +    },
   1.425 +
   1.426 +    set className(str) {
   1.427 +      this.setAttribute("class", str);
   1.428 +    },
   1.429 +
   1.430 +    get id() {
   1.431 +      return this.getAttribute("id") || "";
   1.432 +    },
   1.433 +
   1.434 +    set id(str) {
   1.435 +      this.setAttribute("id", str);
   1.436 +    },
   1.437 +
   1.438 +    get href() {
   1.439 +      return this.getAttribute("href") || "";
   1.440 +    },
   1.441 +
   1.442 +    set href(str) {
   1.443 +      this.setAttribute("href", str);
   1.444 +    },
   1.445 +
   1.446 +    get src() {
   1.447 +      return this.getAttribute("src") || "";
   1.448 +    },
   1.449 +
   1.450 +    set src(str) {
   1.451 +      this.setAttribute("src", str);
   1.452 +    },
   1.453 +
   1.454 +    get nodeName() {
   1.455 +      return this.tagName;
   1.456 +    },
   1.457 +
   1.458 +    get innerHTML() {
   1.459 +      function getHTML(node) {
   1.460 +        let i = 0;
   1.461 +        for (i = 0; i < node.childNodes.length; i++) {
   1.462 +          let child = node.childNodes[i];
   1.463 +          if (child.localName) {
   1.464 +            arr.push("<" + child.localName);
   1.465 +
   1.466 +            // serialize attribute list
   1.467 +            for (let j = 0; j < child.attributes.length; j++) {
   1.468 +              let attr = child.attributes[j];
   1.469 +              let quote = (attr.value.indexOf('"') === -1 ? '"' : "'");
   1.470 +              arr.push(" " + attr.name + '=' + quote + attr.value + quote);
   1.471 +            }
   1.472 +
   1.473 +            if (child.localName in voidElems) {
   1.474 +              // if this is a self-closing element, end it here
   1.475 +              arr.push("/>");
   1.476 +            } else {
   1.477 +              // otherwise, add its children
   1.478 +              arr.push(">");
   1.479 +              getHTML(child);
   1.480 +              arr.push("</" + child.localName + ">");
   1.481 +            }
   1.482 +          } else {
   1.483 +            arr.push(child.textContent);
   1.484 +          }
   1.485 +        }
   1.486 +      }
   1.487 +
   1.488 +      // Using Array.join() avoids the overhead from lazy string concatenation.
   1.489 +      // See http://blog.cdleary.com/2012/01/string-representation-in-spidermonkey/#ropes
   1.490 +      let arr = [];
   1.491 +      getHTML(this);
   1.492 +      return arr.join("");
   1.493 +    },
   1.494 +
   1.495 +    set innerHTML(html) {
   1.496 +      let parser = new JSDOMParser();
   1.497 +      let node = parser.parse(html);
   1.498 +      for (let i = this.childNodes.length; --i >= 0;) {
   1.499 +        this.childNodes[i].parentNode = null;
   1.500 +      }
   1.501 +      this.childNodes = node.childNodes;
   1.502 +      for (let i = this.childNodes.length; --i >= 0;) {
   1.503 +        this.childNodes[i].parentNode = this;
   1.504 +      }
   1.505 +    },
   1.506 +
   1.507 +    set textContent(text) {
   1.508 +      // clear parentNodes for existing children
   1.509 +      for (let i = this.childNodes.length; --i >= 0;) {
   1.510 +        this.childNodes[i].parentNode = null;
   1.511 +      }
   1.512 +
   1.513 +      let node = new Text();
   1.514 +      this.childNodes = [ node ];
   1.515 +      node.textContent = text;
   1.516 +      node.parentNode = this;
   1.517 +    },
   1.518 +
   1.519 +    get textContent() {
   1.520 +      function getText(node) {
   1.521 +        let nodes = node.childNodes;
   1.522 +        for (let i = 0; i < nodes.length; i++) {
   1.523 +          let child = nodes[i];
   1.524 +          if (child.nodeType === 3) {
   1.525 +            text.push(child.textContent);
   1.526 +          } else {
   1.527 +            getText(child);
   1.528 +          }
   1.529 +        }
   1.530 +      }
   1.531 +
   1.532 +      // Using Array.join() avoids the overhead from lazy string concatenation.
   1.533 +      // See http://blog.cdleary.com/2012/01/string-representation-in-spidermonkey/#ropes
   1.534 +      let text = [];
   1.535 +      getText(this);
   1.536 +      return text.join("");
   1.537 +    },
   1.538 +
   1.539 +    getAttribute: function (name) {
   1.540 +      for (let i = this.attributes.length; --i >= 0;) {
   1.541 +        let attr = this.attributes[i];
   1.542 +        if (attr.name === name)
   1.543 +          return attr.value;
   1.544 +      }
   1.545 +      return undefined;
   1.546 +    },
   1.547 +
   1.548 +    setAttribute: function (name, value) {
   1.549 +      for (let i = this.attributes.length; --i >= 0;) {
   1.550 +        let attr = this.attributes[i];
   1.551 +        if (attr.name === name) {
   1.552 +          attr.value = value;
   1.553 +          return;
   1.554 +        }
   1.555 +      }
   1.556 +      this.attributes.push(new Attribute(name, value));
   1.557 +    },
   1.558 +
   1.559 +    removeAttribute: function (name) {
   1.560 +      for (let i = this.attributes.length; --i >= 0;) {
   1.561 +        let attr = this.attributes[i];
   1.562 +        if (attr.name === name) {
   1.563 +          this.attributes.splice(i, 1);
   1.564 +          break;
   1.565 +        }
   1.566 +      }
   1.567 +    }
   1.568 +  };
   1.569 +
   1.570 +  let Style = function (node) {
   1.571 +    this.node = node;
   1.572 +  };
   1.573 +
   1.574 +  // getStyle() and setStyle() use the style attribute string directly. This
   1.575 +  // won't be very efficient if there are a lot of style manipulations, but
   1.576 +  // it's the easiest way to make sure the style attribute string and the JS
   1.577 +  // style property stay in sync. Readability.js doesn't do many style
   1.578 +  // manipulations, so this should be okay.
   1.579 +  Style.prototype = {
   1.580 +    getStyle: function (styleName) {
   1.581 +      let attr = this.node.getAttribute("style");
   1.582 +      if (!attr)
   1.583 +        return undefined;
   1.584 +
   1.585 +      let styles = attr.value.split(";");
   1.586 +      for (let i = 0; i < styles.length; i++) {
   1.587 +        let style = styles[i].split(":");
   1.588 +        let name = style[0].trim();
   1.589 +        if (name === styleName)
   1.590 +          return style[1].trim();
   1.591 +      }
   1.592 +
   1.593 +      return undefined;
   1.594 +    },
   1.595 +
   1.596 +    setStyle: function (styleName, styleValue) {
   1.597 +      let attr = this.node.getAttribute("style");
   1.598 +      let value = (attr ? attr.value : "");
   1.599 +      let index = 0;
   1.600 +      do {
   1.601 +        let next = value.indexOf(";", index) + 1;
   1.602 +        let length = next - index - 1;
   1.603 +        let style = (length > 0 ? value.substr(index, length) : value.substr(index));
   1.604 +        if (style.substr(0, style.indexOf(":")).trim() === styleName) {
   1.605 +          value = value.substr(0, index).trim() + (next ? " " + value.substr(next).trim() : "");
   1.606 +          break;
   1.607 +        }
   1.608 +        index = next;
   1.609 +      } while (index);
   1.610 +
   1.611 +      value += " " + styleName + ": " + styleValue + ";";
   1.612 +      this.node.setAttribute("style", value.trim());
   1.613 +    }
   1.614 +  };
   1.615 +
   1.616 +  // For each item in styleMap, define a getter and setter on the style
   1.617 +  // property.
   1.618 +  for (let jsName in styleMap) {
   1.619 +    (function (cssName) {
   1.620 +      Style.prototype.__defineGetter__(jsName, function () {
   1.621 +        return this.getStyle(cssName);
   1.622 +      });
   1.623 +      Style.prototype.__defineSetter__(jsName, function (value) {
   1.624 +        this.setStyle(cssName, value);
   1.625 +      });
   1.626 +    }) (styleMap[jsName]);
   1.627 +  }
   1.628 +
   1.629 +  let JSDOMParser = function () {
   1.630 +    this.currentChar = 0;
   1.631 +  };
   1.632 +
   1.633 +  JSDOMParser.prototype = {
   1.634 +    /**
   1.635 +     * Look at the next character without advancing the index.
   1.636 +     */
   1.637 +    peekNext: function () {
   1.638 +      return this.html[this.currentChar];
   1.639 +    },
   1.640 +
   1.641 +    /**
   1.642 +     * Get the next character and advance the index.
   1.643 +     */
   1.644 +    nextChar: function () {
   1.645 +      return this.html[this.currentChar++];
   1.646 +    },
   1.647 +
   1.648 +    /**
   1.649 +     * Called after a quote character is read. This finds the next quote
   1.650 +     * character and returns the text string in between.
   1.651 +     */
   1.652 +    readString: function (quote) {
   1.653 +      let str;
   1.654 +      let n = this.html.indexOf(quote, this.currentChar);
   1.655 +      if (n === -1) {
   1.656 +        this.currentChar = this.html.length;
   1.657 +        str = null;
   1.658 +      } else {
   1.659 +        str = this.html.substring(this.currentChar, n);
   1.660 +        this.currentChar = n + 1;
   1.661 +      }
   1.662 +
   1.663 +      return str;
   1.664 +    },
   1.665 +
   1.666 +    /**
   1.667 +     * Called when parsing a node. This finds the next name/value attribute
   1.668 +     * pair and adds the result to the attributes list.
   1.669 +     */
   1.670 +    readAttribute: function (node) {
   1.671 +      let name = "";
   1.672 +
   1.673 +      let n = this.html.indexOf("=", this.currentChar);
   1.674 +      if (n === -1) {
   1.675 +        this.currentChar = this.html.length;
   1.676 +      } else {
   1.677 +        // Read until a '=' character is hit; this will be the attribute key
   1.678 +        name = this.html.substring(this.currentChar, n);
   1.679 +        this.currentChar = n + 1;
   1.680 +      }
   1.681 +
   1.682 +      if (!name)
   1.683 +        return;
   1.684 +
   1.685 +      // After a '=', we should see a '"' for the attribute value
   1.686 +      let c = this.nextChar();
   1.687 +      if (c !== '"' && c !== "'") {
   1.688 +        error("expecting '\"'");
   1.689 +        return;
   1.690 +      }
   1.691 +
   1.692 +      // Read the attribute value (and consume the matching quote)
   1.693 +      let value = this.readString(c);
   1.694 +
   1.695 +      if (!value)
   1.696 +        return;
   1.697 +
   1.698 +      node.attributes.push(new Attribute(name, value));
   1.699 +
   1.700 +      return;
   1.701 +    },
   1.702 +
   1.703 +    /**
   1.704 +     * Parses and returns an Element node. This is called after a '<' has been
   1.705 +     * read.
   1.706 +     *
   1.707 +     * @returns an array; the first index of the array is the parsed node;
   1.708 +     *          the second index is a boolean indicating whether this is a void
   1.709 +     *          Element
   1.710 +     */
   1.711 +    makeElementNode: function () {
   1.712 +      let c = this.nextChar();
   1.713 +
   1.714 +      // Read the Element tag name
   1.715 +      let tag = "";
   1.716 +      while (c !== " " && c !== ">" && c !== "/") {
   1.717 +        if (c === undefined)
   1.718 +          return null;
   1.719 +        tag += c;
   1.720 +        c = this.nextChar();
   1.721 +      }
   1.722 +
   1.723 +      if (!tag)
   1.724 +        return null;
   1.725 +
   1.726 +      let node = new Element(tag);
   1.727 +
   1.728 +      // Read Element attributes
   1.729 +      while (c !== "/" && c !== ">") {
   1.730 +        if (c === undefined)
   1.731 +          return null;
   1.732 +        while (this.match(" "));
   1.733 +        c = this.nextChar();
   1.734 +        if (c !== "/" && c !== ">") {
   1.735 +          --this.currentChar;
   1.736 +          this.readAttribute(node);
   1.737 +        }
   1.738 +      }
   1.739 +
   1.740 +      // If this is a self-closing tag, read '/>'
   1.741 +      let closed = tag in voidElems;
   1.742 +      if (c === "/") {
   1.743 +        closed = true;
   1.744 +        c = this.nextChar();
   1.745 +        if (c !== ">") {
   1.746 +          error("expected '>'");
   1.747 +          return null;
   1.748 +        }
   1.749 +      }
   1.750 +
   1.751 +      return [node, closed];
   1.752 +    },
   1.753 +
   1.754 +    /**
   1.755 +     * If the current input matches this string, advance the input index;
   1.756 +     * otherwise, do nothing.
   1.757 +     *
   1.758 +     * @returns whether input matched string
   1.759 +     */
   1.760 +    match: function (str) {
   1.761 +      let strlen = str.length;
   1.762 +      if (this.html.substr(this.currentChar, strlen) === str) {
   1.763 +        this.currentChar += strlen;
   1.764 +        return true;
   1.765 +      }
   1.766 +      return false;
   1.767 +    },
   1.768 +
   1.769 +    /**
   1.770 +     * Searches the input until a string is found and discards all input up to
   1.771 +     * and including the matched string.
   1.772 +     */
   1.773 +    discardTo: function (str) {
   1.774 +      let index = this.html.indexOf(str, this.currentChar) + str.length;
   1.775 +      if (index === -1)
   1.776 +        this.currentChar = this.html.length;
   1.777 +      this.currentChar = index;
   1.778 +    },
   1.779 +
   1.780 +    /**
   1.781 +     * Reads child nodes for the given node.
   1.782 +     */
   1.783 +    readChildren: function (node) {
   1.784 +      let child;
   1.785 +      while ((child = this.readNode())) {
   1.786 +        // Don't keep Comment nodes
   1.787 +        if (child.nodeType !== 8) {
   1.788 +          node.childNodes.push(child);
   1.789 +          child.parentNode = node;
   1.790 +        }
   1.791 +      }
   1.792 +    },
   1.793 +
   1.794 +    /**
   1.795 +     * Reads the next child node from the input. If we're reading a closing
   1.796 +     * tag, or if we've reached the end of input, return null.
   1.797 +     *
   1.798 +     * @returns the node
   1.799 +     */
   1.800 +    readNode: function () {
   1.801 +      let c = this.nextChar();
   1.802 + 
   1.803 +      if (c === undefined)
   1.804 +        return null;
   1.805 +
   1.806 +      // Read any text as Text node
   1.807 +      if (c !== "<") {
   1.808 +        --this.currentChar;
   1.809 +        let node = new Text();
   1.810 +        let n = this.html.indexOf("<", this.currentChar);
   1.811 +        if (n === -1) {
   1.812 +          node.textContent = this.html.substring(this.currentChar, this.html.length);
   1.813 +          this.currentChar = this.html.length;
   1.814 +        } else {
   1.815 +          node.textContent = this.html.substring(this.currentChar, n);
   1.816 +          this.currentChar = n;
   1.817 +        }
   1.818 +        return node;
   1.819 +      }
   1.820 +
   1.821 +      c = this.peekNext();
   1.822 +
   1.823 +      // Read Comment node. Normally, Comment nodes know their inner
   1.824 +      // textContent, but we don't really care about Comment nodes (we throw
   1.825 +      // them away in readChildren()). So just returning an empty Comment node
   1.826 +      // here is sufficient.
   1.827 +      if (c === "!" || c === "?") {
   1.828 +        this.currentChar++;
   1.829 +        if (this.match("--")) {
   1.830 +          this.discardTo("-->");
   1.831 +        } else {
   1.832 +          let c = this.nextChar();
   1.833 +          while (c !== ">") {
   1.834 +            if (c === undefined)
   1.835 +              return null;
   1.836 +            if (c === '"' || c === "'")
   1.837 +              this.readString(c);
   1.838 +            c = this.nextChar();
   1.839 +          }
   1.840 +        }
   1.841 +        return new Comment();
   1.842 +      }
   1.843 +
   1.844 +      // If we're reading a closing tag, return null. This means we've reached
   1.845 +      // the end of this set of child nodes.
   1.846 +      if (c === "/") {
   1.847 +        --this.currentChar;
   1.848 +        return null;
   1.849 +      }
   1.850 +
   1.851 +      // Otherwise, we're looking at an Element node
   1.852 +      let result = this.makeElementNode();
   1.853 +      if (result === null)
   1.854 +        return null;
   1.855 +
   1.856 +      let [node, closed] = result;
   1.857 +      let localName = node.localName;
   1.858 +
   1.859 +      // If this isn't a void Element, read its child nodes
   1.860 +      if (!closed) {
   1.861 +        this.readChildren(node);
   1.862 +        let closingTag = "</" + localName + ">";
   1.863 +        if (!this.match(closingTag)) {
   1.864 +          error("expected '" + closingTag + "'");
   1.865 +          return null;
   1.866 +        }
   1.867 +      }
   1.868 +
   1.869 +      if (localName === "title") {
   1.870 +        this.doc.title = node.textContent.trim();
   1.871 +      } else if (localName === "head") {
   1.872 +        this.doc.head = node;
   1.873 +      } else if (localName === "body") {
   1.874 +        this.doc.body = node;
   1.875 +      } else if (localName === "html") {
   1.876 +        this.doc.documentElement = node;
   1.877 +      }
   1.878 +
   1.879 +      return node;
   1.880 +    },
   1.881 +
   1.882 +    /**
   1.883 +     * Parses an HTML string and returns a JS implementation of the Document.
   1.884 +     */
   1.885 +    parse: function (html) {
   1.886 +      this.html = html;
   1.887 +      let doc = this.doc = new Document();
   1.888 +      this.readChildren(doc);
   1.889 +
   1.890 +      // If this is an HTML document, remove root-level children except for the
   1.891 +      // <html> node
   1.892 +      if (doc.documentElement) {
   1.893 +        for (let i = doc.childNodes.length; --i >= 0;) {
   1.894 +          let child = doc.childNodes[i];
   1.895 +          if (child !== doc.documentElement) {
   1.896 +            doc.removeChild(child);
   1.897 +          }
   1.898 +        }
   1.899 +      }
   1.900 +
   1.901 +      return doc;
   1.902 +    }
   1.903 +  };
   1.904 +
   1.905 +  // Attach the standard DOM types to the global scope
   1.906 +  global.Node = Node;
   1.907 +  global.Comment = Comment;
   1.908 +  global.Document = Document;
   1.909 +  global.Element = Element;
   1.910 +  global.Text = Text;
   1.911 +
   1.912 +  // Attach JSDOMParser to the global scope
   1.913 +  global.JSDOMParser = JSDOMParser;
   1.914 +
   1.915 +}) (this);

mercurial