/* * Copyright (C) 2019 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ // This tree builder attempts to match input text to output DOM node. // This therefore doesn't do HTML5 tree construction like implicitly-closing // specific HTML parent nodes depending on being in a particular node, // it only does basic implicitly-closing. In general this tries to be a // whitespace reformatter for input text and not generate the ultimate // html tree that a browser would generate. // // When run with the XML option, all HTML specific cases are disabled. HTMLTreeBuilderFormatter = class HTMLTreeBuilderFormatter { constructor({isXML} = {}) { this._isXML = !!isXML; } // Public get dom() { return this._dom; } begin() { this._dom = []; this._stackOfOpenElements = []; } pushParserNode(parserNode) { let containerNode = this._stackOfOpenElements.lastValue; if (!containerNode) this._pushParserNodeTopLevel(parserNode); else this._pushParserNodeStack(parserNode, containerNode); } end() { for (let node of this._stackOfOpenElements) node.implicitClose = true; } // Private _pushParserNodeTopLevel(parserNode) { if (parserNode.type === HTMLParser.NodeType.OpenTag) { let node = this._buildDOMNodeFromOpenTag(parserNode); this._dom.push(node); if (!this._isEmptyNode(parserNode, node)) this._stackOfOpenElements.push(node); return; } if (parserNode.type === HTMLParser.NodeType.CloseTag) { let errorNode = this._buildErrorNodeFromCloseTag(parserNode); this._dom.push(errorNode); return; } let node = this._buildSimpleNodeFromParserNode(parserNode); this._dom.push(node); } _pushParserNodeStack(parserNode, containerNode) { if (parserNode.type === HTMLParser.NodeType.OpenTag) { let node = this._buildDOMNodeFromOpenTag(parserNode); let childrenArray = containerNode.children; if (!this._isXML) { this._implicitlyCloseHTMLNodesForOpenTag(parserNode, node); containerNode = this._stackOfOpenElements.lastValue; childrenArray = containerNode ? containerNode.children : this._dom; } childrenArray.push(node); if (!this._isEmptyNode(parserNode, node)) this._stackOfOpenElements.push(node); return; } if (parserNode.type === HTMLParser.NodeType.CloseTag) { let tagName = this._isXML ? parserNode.name : parserNode.name.toLowerCase(); let matchingOpenTagIndex = this._indexOfStackNodeMatchingTagNames([tagName]); // Found a matching tag, implicitly-close nodes. if (matchingOpenTagIndex !== -1) { let nodesToPop = this._stackOfOpenElements.length - matchingOpenTagIndex; for (let i = 0; i < nodesToPop - 1; ++i) { let implicitlyClosingNode = this._stackOfOpenElements.pop(); implicitlyClosingNode.implicitClose = true; } let implicitlyClosingNode = this._stackOfOpenElements.pop(); if (parserNode.pos) { implicitlyClosingNode.closeTagPos = parserNode.pos; implicitlyClosingNode.closeTagName = parserNode.name; } return; } // Did not find a matching tag to close. // Treat this as an error text node. let errorNode = this._buildErrorNodeFromCloseTag(parserNode); containerNode.children.push(errorNode); return; } let node = this._buildSimpleNodeFromParserNode(parserNode); containerNode.children.push(node); } _implicitlyCloseHTMLNodesForOpenTag(parserNode, node) { if (parserNode.closed) return; switch (node.lowercaseName) { // closes . case "body": this._implicitlyCloseTagNamesInsideParentTagNames(["head"]); break; // Inside