blob: fb8694a388d6e731ca09a76d1679e07d62248d59 [file] [log] [blame]
/*
* 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.
*/
HTMLFormatter = class HTMLFormatter
{
constructor(sourceText, sourceType, builder, indentString = " ")
{
console.assert(typeof sourceText === "string");
console.assert(Object.values(HTMLFormatter.SourceType).includes(sourceType));
this._sourceType = sourceType;
this._success = false;
let dom = (function() {
try {
let options = {
isXML: sourceType === HTMLFormatter.SourceType.XML,
};
let parser = new HTMLParser;
let treeBuilder = new HTMLTreeBuilderFormatter(options);
parser.parseDocument(sourceText, treeBuilder, options);
return treeBuilder.dom;
} catch (e) {
console.error("Unexpected HTMLFormatter Error", e);
return null;
}
})();
if (!dom)
return;
this._sourceText = sourceText;
this._builder = builder;
if (!this._builder) {
this._builder = new FormatterContentBuilder(indentString);
this._builder.setOriginalLineEndings(this._sourceText.lineEndings());
}
this._walkArray(dom, null);
this._builder.appendNewline();
this._builder.appendMapping(this._sourceText.length);
this._success = true;
}
// Public
get success() { return this._success; }
get formattedText()
{
if (!this._success)
return null;
return this._builder.formattedContent;
}
get sourceMapData()
{
if (!this._success)
return null;
return this._builder.sourceMapData;
}
// Private
_walk(node, parent)
{
if (!node)
return;
this._before(node, parent);
this._walkArray(node.children, node);
this._after(node, parent);
}
_walkArray(children, parent)
{
if (!children)
return;
this._previousSiblingNode = null;
for (let child of children) {
this._walk(child, parent);
this._previousSiblingNode = child;
}
}
_shouldHaveNoChildren(node)
{
switch (this._sourceType) {
case HTMLFormatter.SourceType.HTML:
return HTMLTreeBuilderFormatter.TagNamesWithoutChildren.has(node.lowercaseName);
case HTMLFormatter.SourceType.XML:
return false;
}
console.assert(false, "Unknown source type", this._sourceType);
return false;
}
_shouldHaveInlineContent(node)
{
if (node.__shouldHaveNoChildren)
return true;
let children = node.children;
if (!children)
return true;
if (!children.length)
return true;
if (children.length === 1 && node.children[0].type === HTMLTreeBuilderFormatter.NodeType.Text)
return true;
return false;
}
_hasMultipleNewLines(text)
{
let firstIndex = text.indexOf("\n");
if (firstIndex === -1)
return false;
let secondIndex = text.indexOf("\n", firstIndex + 1);
if (secondIndex === -1)
return false;
return true;
}
_buildAttributeString(attr)
{
this._builder.appendSpace();
let {name, value, quote, namePos, valuePos} = attr;
if (value !== undefined) {
let q;
switch (quote) {
case HTMLParser.AttrQuoteType.None:
q = ``;
break;
case HTMLParser.AttrQuoteType.Single:
q = `'`;
break;
case HTMLParser.AttrQuoteType.Double:
q = `"`;
break;
default:
console.assert(false, "Unexpected quote type", quote);
q = ``;
break;
}
this._builder.appendToken(name, namePos);
this._builder.appendNonToken("=");
if (q)
this._builder.appendStringWithPossibleNewlines(q + value + q, valuePos);
else
this._builder.appendToken(value, valuePos);
return;
}
console.assert(quote === HTMLParser.AttrQuoteType.None);
this._builder.appendToken(name, namePos);
}
_before(node, parent)
{
if (node.type === HTMLTreeBuilderFormatter.NodeType.Node) {
node.__shouldHaveNoChildren = this._shouldHaveNoChildren(node);
node.__inlineContent = this._shouldHaveInlineContent(node);
if (this._previousSiblingNode && this._previousSiblingNode.type === HTMLTreeBuilderFormatter.NodeType.Text)
this._builder.appendNewline();
this._builder.appendToken("<" + node.name, node.pos);
if (node.attributes) {
for (let attr of node.attributes)
this._buildAttributeString(attr);
}
if (node.selfClose)
this._builder.appendNonToken("/");
this._builder.appendNonToken(">");
if (node.selfClose || node.__shouldHaveNoChildren)
this._builder.appendNewline();
if (!node.__inlineContent) {
if (node.lowercaseName !== "html" || this._sourceType === HTMLFormatter.SourceType.XML)
this._builder.indent();
this._builder.appendNewline();
}
return;
}
if (node.type === HTMLTreeBuilderFormatter.NodeType.Text) {
// <script> and <style> inline content.
if (parent && parent.type === HTMLTreeBuilderFormatter.NodeType.Node) {
switch (parent.lowercaseName) {
case "script":
if (this._formatScript(node.data, parent, node))
return;
break;
case "style":
if (this._formatStyle(node.data, parent, node))
return;
break;
}
}
// Whitespace only text nodes.
let textString = node.data;
if (/^\s*$/.test(textString)) {
// Collapse multiple blank lines to a single blank line.
if (this._hasMultipleNewLines(textString))
this._builder.appendNewline(true);
return;
}
this._builder.appendStringWithPossibleNewlines(textString, node.pos);
return;
}
if (node.type === HTMLTreeBuilderFormatter.NodeType.Comment) {
let openerString = node.opener ? node.opener : "<!--";
let commentString = openerString + node.data;
this._builder.appendStringWithPossibleNewlines(commentString, node.pos);
return;
}
if (node.type === HTMLTreeBuilderFormatter.NodeType.Doctype) {
let doctypeString = "<" + node.raw + node.data;
this._builder.appendStringWithPossibleNewlines(doctypeString, node.pos);
return;
}
if (node.type === HTMLTreeBuilderFormatter.NodeType.CData) {
let cdataString = "<![CDATA[" + node.data;
this._builder.appendStringWithPossibleNewlines(cdataString, node.pos);
return;
}
if (node.type === HTMLTreeBuilderFormatter.NodeType.Error) {
let rawText = node.raw;
this._builder.appendStringWithPossibleNewlines(rawText, node.pos);
this._builder.appendNewline();
return;
}
console.assert(false, "Unhandled node type", node.type, node);
}
_after(node, parent)
{
if (node.type === HTMLTreeBuilderFormatter.NodeType.Node) {
if (node.selfClose)
return;
if (node.__shouldHaveNoChildren)
return;
if (!node.__inlineContent) {
if (node.lowercaseName !== "html" || this._sourceType === HTMLFormatter.SourceType.XML)
this._builder.dedent();
this._builder.appendNewline();
}
if (!node.implicitClose) {
console.assert(node.closeTagName);
console.assert(node.closeTagPos);
this._builder.appendToken("</" + node.closeTagName + ">", node.closeTagPos);
}
this._builder.appendNewline();
return;
}
if (node.type === HTMLTreeBuilderFormatter.NodeType.Text)
return;
if (node.type === HTMLTreeBuilderFormatter.NodeType.Comment) {
let closingCommentString = node.opener ? ">" : "-->";
this._builder.appendToken(closingCommentString, node.closePos);
this._builder.appendNewline();
return;
}
if (node.type === HTMLTreeBuilderFormatter.NodeType.Doctype) {
let closingDoctypeString = ">";
this._builder.appendToken(closingDoctypeString, node.closePos);
this._builder.appendNewline();
return;
}
if (node.type === HTMLTreeBuilderFormatter.NodeType.CData) {
let closingCDataString = "]]>";
this._builder.appendToken(closingCDataString, node.closePos);
return;
}
if (node.type === HTMLTreeBuilderFormatter.NodeType.Error)
return;
console.assert(false, "Unhandled node type", node.type, node);
}
_formatWithNestedFormatter(sourceText, parentNode, textNode, formatterCallback)
{
this._builder.appendNewline();
let originalIndentLevel = this._builder.indentLevel;
this._builder.originalOffset = textNode.pos;
let formatter = formatterCallback();
if (!formatter.success) {
this._builder.removeLastNewline();
this._builder.originalOffset = 0;
return false;
}
this._builder.appendMapping(sourceText.length);
this._builder.indentToLevel(originalIndentLevel);
this._builder.originalOffset = 0;
return true;
}
_formatScript(sourceText, scriptNode, textNode)
{
// <script type="module">.
let isModule = false;
if (scriptNode.attributes) {
for (let {name, value} of scriptNode.attributes) {
if (name === "type") {
if (value && value.toLowerCase() === "module")
isModule = true;
break;
}
}
}
return this._formatWithNestedFormatter(sourceText, scriptNode, textNode, () => {
let sourceType = isModule ? JSFormatter.SourceType.Module : JSFormatter.SourceType.Script;
return new JSFormatter(sourceText, sourceType, this._builder);
});
}
_formatStyle(sourceText, styleNode, textNode)
{
return this._formatWithNestedFormatter(sourceText, styleNode, textNode, () => {
return new CSSFormatter(sourceText, this._builder);
});
}
};
HTMLFormatter.SourceType = {
HTML: "html",
XML: "xml",
};