382 lines
12 KiB
JavaScript
382 lines
12 KiB
JavaScript
/*
|
|
* 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",
|
|
};
|