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);