mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-28 18:18:55 +01:00 
			
		
		
		
	 9ba49bdc5f
			
		
	
	
		9ba49bdc5f
		
	
	
	
	
		
			
			git-subtree-dir: apps/web-clipper git-subtree-mainline: b0e519071893ce9434dd3bb3a568b3e41332c718 git-subtree-split: 786d249a6effe0262859d05e6cb1737e5fc8bdd8
		
			
				
	
	
		
			1197 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1197 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*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 = {
 | |
|     "<": "<",
 | |
|     ">": ">",
 | |
|     "&": "&",
 | |
|     '"': """,
 | |
|     "'": "'",
 | |
|   };
 | |
| 
 | |
|   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;
 | |
| }
 |