Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(208)

Unified Diff: third_party/WebKit/Source/devtools/front_end/formatter_worker/HTMLFormatter.js

Issue 1894343002: DevTools: pretty-print HTML (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: address comments Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « third_party/WebKit/Source/devtools/front_end/formatter_worker/FormatterWorker.js ('k') | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: third_party/WebKit/Source/devtools/front_end/formatter_worker/HTMLFormatter.js
diff --git a/third_party/WebKit/Source/devtools/front_end/formatter_worker/HTMLFormatter.js b/third_party/WebKit/Source/devtools/front_end/formatter_worker/HTMLFormatter.js
index 9514125d6380f3628bd902c5cc6a59dbb938da3a..0961cdb78916bd95e98a762b0dca3170c79807df 100644
--- a/third_party/WebKit/Source/devtools/front_end/formatter_worker/HTMLFormatter.js
+++ b/third_party/WebKit/Source/devtools/front_end/formatter_worker/HTMLFormatter.js
@@ -9,69 +9,460 @@
WebInspector.HTMLFormatter = function(builder)
{
this._builder = builder;
+ this._jsFormatter = new WebInspector.JavaScriptFormatter(builder);
+ this._cssFormatter = new WebInspector.CSSFormatter(builder);
+}
+
+WebInspector.HTMLFormatter.SupportedJavaScriptMimeTypes = new Set([
+ "text/javascript",
+ "text/ecmascript",
+ "application/javascript",
+ "application/ecmascript"
+]);
+
+WebInspector.HTMLFormatter.prototype = {
+ /**
+ * @param {string} text
+ * @param {!Array<number>} lineEndings
+ */
+ format: function(text, lineEndings)
+ {
+ this._text = text;
+ this._lineEndings = lineEndings;
+ this._model = new WebInspector.HTMLModel(text);
+ this._walk(this._model.document());
+ },
+
+ /**
+ * @param {!WebInspector.HTMLModel.Element} element
+ * @param {number} offset
+ */
+ _formatTokensTill: function(element, offset)
+ {
+ while (this._model.peekToken() && this._model.peekToken().startOffset < offset) {
+ var token = this._model.nextToken();
+ this._formatToken(element, token);
+ }
+ },
+
+ /**
+ * @param {!WebInspector.HTMLModel.Element} element
+ */
+ _walk: function(element)
+ {
+ if (element.parent)
+ this._formatTokensTill(element.parent, element.openTag.startOffset);
+ this._beforeOpenTag(element);
+ this._formatTokensTill(element, element.openTag.endOffset);
+ this._afterOpenTag(element);
+ for (var i = 0; i < element.children.length; ++i)
+ this._walk(element.children[i]);
+
+ this._formatTokensTill(element, element.closeTag.startOffset);
+ this._beforeCloseTag(element);
+ this._formatTokensTill(element, element.closeTag.endOffset);
+ this._afterCloseTag(element);
+ },
+
+ /**
+ * @param {!WebInspector.HTMLModel.Element} element
+ */
+ _beforeOpenTag: function(element)
+ {
+ if (!element.children.length || element === this._model.document())
+ return;
+ this._builder.addNewLine();
+ },
+
+ /**
+ * @param {!WebInspector.HTMLModel.Element} element
+ */
+ _afterOpenTag: function(element)
+ {
+ if (!element.children.length || element === this._model.document())
+ return;
+ this._builder.increaseNestingLevel();
+ this._builder.addNewLine();
+ },
+
+ /**
+ * @param {!WebInspector.HTMLModel.Element} element
+ */
+ _beforeCloseTag: function(element)
+ {
+ if (!element.children.length || element === this._model.document())
+ return;
+ this._builder.decreaseNestingLevel();
+ this._builder.addNewLine();
+ },
+
+ /**
+ * @param {!WebInspector.HTMLModel.Element} element
+ */
+ _afterCloseTag: function(element)
+ {
+ this._builder.addNewLine();
+ },
+
+ /**
+ * @param {!WebInspector.HTMLModel.Element} element
+ * @param {!WebInspector.HTMLModel.Token} token
+ */
+ _formatToken: function(element, token)
+ {
+ if (token.value.isWhitespace())
+ return;
+ if (token.type.has("comment") || token.type.has("meta")) {
+ this._builder.addNewLine();
+ this._builder.addToken(token.value.trim(), token.startOffset);
+ this._builder.addNewLine();
+ return;
+ }
+
+ var isBodyToken = element.openTag.endOffset <= token.startOffset && token.startOffset < element.closeTag.startOffset;
+ if (isBodyToken && element.name === "style") {
+ this._builder.addNewLine();
+ this._builder.increaseNestingLevel();
+ this._cssFormatter.format(this._text, this._lineEndings, token.startOffset, token.endOffset);
+ this._builder.decreaseNestingLevel();
+ return;
+ }
+ if (isBodyToken && element.name === "script") {
+ this._builder.addNewLine();
+ this._builder.increaseNestingLevel();
+ var mimeType = element.openTag.attributes.has("type") ? element.openTag.attributes.get("type").toLowerCase() : null;
+ if (!mimeType || WebInspector.HTMLFormatter.SupportedJavaScriptMimeTypes.has(mimeType)) {
+ this._jsFormatter.format(this._text, this._lineEndings, token.startOffset, token.endOffset);
+ } else {
+ this._builder.addToken(token.value, token.startOffset);
+ this._builder.addNewLine();
+ }
+ this._builder.decreaseNestingLevel();
+ return;
+ }
+
+ if (!isBodyToken && token.type.has("attribute"))
+ this._builder.addSoftSpace();
+
+ this._builder.addToken(token.value, token.startOffset);
+ }
}
/**
* @constructor
- * @param {string} tagName
- * @param {number} offset
+ * @param {string} text
*/
-WebInspector.HTMLFormatter.Result = function(tagName, offset)
+WebInspector.HTMLModel = function(text)
{
- this.tagName = tagName;
- this.offset = offset;
+ this._state = WebInspector.HTMLModel.ParseState.Initial;
+ this._document = new WebInspector.HTMLModel.Element("document");
+ this._document.openTag = new WebInspector.HTMLModel.Tag("document", 0, 0, new Map(), true, false);
+ this._document.closeTag = new WebInspector.HTMLModel.Tag("document", text.length, text.length, new Map(), false, false);
+
+ this._stack = [this._document];
+
+ this._tokens = [];
+ this._tokenIndex = 0;
+ this._build(text);
}
-WebInspector.HTMLFormatter.prototype = {
+WebInspector.HTMLModel.SelfClosingTags = new Set([
+ "area",
+ "base",
+ "br",
+ "col",
+ "command",
+ "embed",
+ "hr",
+ "img",
+ "input",
+ "keygen",
+ "link",
+ "meta",
+ "param",
+ "source",
+ "track",
+ "wbr"
+]);
+
+// @see https://www.w3.org/TR/html/syntax.html 8.1.2.4 Optional tags
+WebInspector.HTMLModel.AutoClosingTags = {
+ "head": new Set(["body"]),
+ "li": new Set(["li"]),
+ "dt": new Set(["dt", "dd"]),
+ "dd": new Set(["dt", "dd"]),
+ "p": new Set(["address", "article", "aside", "blockquote", "div", "dl", "fieldset", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "main", "nav", "ol", "p", "pre", "section", "table", "ul"]),
+ "rb": new Set(["rb", "rt", "rtc", "rp"]),
+ "rt": new Set(["rb", "rt", "rtc", "rp"]),
+ "rtc": new Set(["rb", "rtc", "rp"]),
+ "rp": new Set(["rb", "rt", "rtc", "rp"]),
+ "optgroup": new Set(["optgroup"]),
+ "option": new Set(["option", "optgroup"]),
+ "colgroup": new Set(["colgroup"]),
+ "thead": new Set(["tbody", "tfoot"]),
+ "tbody": new Set(["tbody", "tfoot"]),
+ "tfoot": new Set(["tbody"]),
+ "tr": new Set(["tr"]),
+ "td": new Set(["td", "th"]),
+ "th": new Set(["td", "th"]),
+};
+
+/** @enum {string} */
+WebInspector.HTMLModel.ParseState = {
+ Initial: "Initial",
+ Tag: "Tag",
+ AttributeName: "AttributeName",
+ AttributeValue: "AttributeValue"
+}
+
+WebInspector.HTMLModel.prototype = {
/**
* @param {string} text
- * @param {!Array<number>} lineEndings
- * @param {number} fromOffset
- * @return {!WebInspector.HTMLFormatter.Result}
*/
- format: function(text, lineEndings, fromOffset)
+ _build: function(text)
{
- var content = text.substring(fromOffset);
- var tagName = "";
- var accumulatedTokenValue = "";
- var lastOffset = fromOffset;
+ var tokenizer = WebInspector.createTokenizer("text/html");
+ var lastOffset = 0;
+ var lowerCaseText = text.toLowerCase();
+
+ while (true) {
+ tokenizer(text.substring(lastOffset), processToken.bind(this, lastOffset));
+ if (lastOffset >= text.length)
+ break;
+ var element = this._stack.peekLast();
+ lastOffset = lowerCaseText.indexOf("</" + element.name, lastOffset);
+ if (lastOffset === -1)
+ lastOffset = text.length;
+ var tokenStart = element.openTag.endOffset;
+ var tokenEnd = lastOffset;
+ var tokenValue = text.substring(tokenStart, tokenEnd);
+ this._tokens.push(new WebInspector.HTMLModel.Token(tokenValue, new Set(), tokenStart, tokenEnd));
+ }
+
+ while (this._stack.length > 1) {
+ var element = this._stack.peekLast();
+ this._popElement(new WebInspector.HTMLModel.Tag(element.name, text.length, text.length, new Map(), false, false));
+ }
/**
+ * @param {number} baseOffset
* @param {string} tokenValue
* @param {?string} type
* @param {number} tokenStart
* @param {number} tokenEnd
* @return {(!Object|undefined)}
- * @this {WebInspector.HTMLFormatter}
+ * @this {WebInspector.HTMLModel}
*/
- function processToken(tokenValue, type, tokenStart, tokenEnd)
+ function processToken(baseOffset, tokenValue, type, tokenStart, tokenEnd)
{
- tokenStart += fromOffset;
- tokenEnd += fromOffset;
+ tokenStart += baseOffset;
+ tokenEnd += baseOffset;
lastOffset = tokenEnd;
- this._builder.addToken(tokenValue, tokenStart);
- if (!type)
- return;
- var tokenType = type.split(" ").keySet();
- if (!tokenType["tag"])
- return;
+ var tokenType = type ? new Set(type.split(" ")) : new Set();
+ var token = new WebInspector.HTMLModel.Token(tokenValue, tokenType, tokenStart, tokenEnd);
+ this._tokens.push(token);
+ this._updateDOM(token);
+
+ var element = this._stack.peekLast();
+ if (element && (element.name === "script" || element.name === "style") && element.openTag.endOffset === lastOffset)
+ return WebInspector.AbortTokenization;
+ }
+ },
- if (tokenType["bracket"] && (tokenValue === "<" || tokenValue === "</")) {
- accumulatedTokenValue = tokenValue;
- return;
+ /**
+ * @param {!WebInspector.HTMLModel.Token} token
+ */
+ _updateDOM: function(token)
+ {
+ var S = WebInspector.HTMLModel.ParseState;
+ var value = token.value;
+ var type = token.type;
+ switch (this._state) {
+ case S.Initial:
+ if (type.has("bracket") && (value === "<" || value === "</")) {
+ this._onStartTag(token);
+ this._state = S.Tag;
+ }
+ return;
+ case S.Tag:
+ if (type.has("tag") && !type.has("bracket")) {
+ this._tagName = value.trim().toLowerCase();
+ } else if (type.has("attribute")) {
+ this._attributeName = value.trim().toLowerCase();
+ this._attributes.set(this._attributeName, "");
+ this._state = S.AttributeName;
+ } else if (type.has("bracket") && (value === ">" || value === "/>")) {
+ this._onEndTag(token);
+ this._state = S.Initial;
+ }
+ return;
+ case S.AttributeName:
+ if (!type.size && value === "=") {
+ this._state = S.AttributeValue;
+ } else if (type.has("bracket") && (value === ">" || value === "/>")) {
+ this._onEndTag(token);
+ this._state = S.Initial;
}
+ return;
+ case S.AttributeValue:
+ if (type.has("string")) {
+ this._attributes.set(this._attributeName, value);
+ this._state = S.Tag;
+ } else if (type.has("bracket") && (value === ">" || value === "/>")) {
+ this._onEndTag(token);
+ this._state = S.Initial;
+ }
+ return;
+ }
+ },
- if (tagName && tokenValue === ">")
- return WebInspector.AbortTokenization;
+ /**
+ * @param {!WebInspector.HTMLModel.Token} token
+ */
+ _onStartTag: function(token)
+ {
+ this._tagName = "";
+ this._tagStartOffset = token.startOffset;
+ this._tagEndOffset = null;
+ this._attributes = new Map();
+ this._attributeName = "";
+ this._isOpenTag = token.value === "<";
+ },
+
+ /**
+ * @param {!WebInspector.HTMLModel.Token} token
+ */
+ _onEndTag: function(token)
+ {
+ this._tagEndOffset = token.endOffset;
+ var selfClosingTag = token.value === "/>" || WebInspector.HTMLModel.SelfClosingTags.has(this._tagName);
+ var tag = new WebInspector.HTMLModel.Tag(this._tagName, this._tagStartOffset, this._tagEndOffset, this._attributes, this._isOpenTag, selfClosingTag);
+ this._onTagComplete(tag);
+ },
- accumulatedTokenValue = accumulatedTokenValue + tokenValue.toLowerCase();
- if (accumulatedTokenValue === "<script" || accumulatedTokenValue === "<style")
- tagName = accumulatedTokenValue.substring(1);
- accumulatedTokenValue = "";
+ /**
+ * @param {!WebInspector.HTMLModel.Tag} tag
+ */
+ _onTagComplete: function(tag)
+ {
+ if (tag.isOpenTag) {
+ var topElement = this._stack.peekLast();
+ if (topElement !== this._document && topElement.openTag.selfClosingTag)
+ this._popElement(autocloseTag(topElement, topElement.openTag.endOffset));
+ else if ((topElement.name in WebInspector.HTMLModel.AutoClosingTags) && WebInspector.HTMLModel.AutoClosingTags[topElement.name].has(tag.name))
+ this._popElement(autocloseTag(topElement, tag.startOffset));
+ this._pushElement(tag);
+ return;
}
- var tokenizer = WebInspector.createTokenizer("text/html");
- tokenizer(content, processToken.bind(this));
- return new WebInspector.HTMLFormatter.Result(tagName, lastOffset);
+
+ while (this._stack.length > 1 && this._stack.peekLast().name !== tag.name)
+ this._popElement(autocloseTag(this._stack.peekLast(), tag.startOffset));
+ if (this._stack.length === 1)
+ return;
+ this._popElement(tag);
+
+ /**
+ * @param {!WebInspector.HTMLModel.Element} element
+ * @param {number} offset
+ * @return {!WebInspector.HTMLModel.Tag}
+ */
+ function autocloseTag(element, offset)
+ {
+ return new WebInspector.HTMLModel.Tag(element.name, offset, offset, new Map(), false, false);
+ }
+ },
+
+ /**
+ * @param {!WebInspector.HTMLModel.Tag} closeTag
+ */
+ _popElement: function(closeTag)
+ {
+ var element = this._stack.pop();
+ element.closeTag = closeTag;
+ },
+
+ /**
+ * @param {!WebInspector.HTMLModel.Tag} openTag
+ */
+ _pushElement: function(openTag)
+ {
+ var topElement = this._stack.peekLast();
+ var newElement = new WebInspector.HTMLModel.Element(openTag.name);
+ newElement.parent = topElement;
+ topElement.children.push(newElement);
+ newElement.openTag = openTag;
+ this._stack.push(newElement);
+ },
+
+ /**
+ * @return {?WebInspector.HTMLModel.Token}
+ */
+ peekToken: function()
+ {
+ return this._tokenIndex < this._tokens.length ? this._tokens[this._tokenIndex] : null;
+ },
+
+ /**
+ * @return {?WebInspector.HTMLModel.Token}
+ */
+ nextToken: function()
+ {
+ return this._tokens[this._tokenIndex++];
},
+
+ /**
+ * @return {!WebInspector.HTMLModel.Element}
+ */
+ document: function()
+ {
+ return this._document;
+ }
+}
+
+/**
+ * @constructor
+ * @param {string} value
+ * @param {!Set<string>} type
+ * @param {number} startOffset
+ * @param {number} endOffset
+ */
+WebInspector.HTMLModel.Token = function(value, type, startOffset, endOffset)
+{
+ this.value = value;
+ this.type = type;
+ this.startOffset = startOffset;
+ this.endOffset = endOffset;
+}
+
+/**
+ * @constructor
+ * @param {string} name
+ * @param {number} startOffset
+ * @param {number} endOffset
+ * @param {!Map<string, string>} attributes
+ * @param {boolean} isOpenTag
+ * @param {boolean} selfClosingTag
+ */
+WebInspector.HTMLModel.Tag = function(name, startOffset, endOffset, attributes, isOpenTag, selfClosingTag)
+{
+ this.name = name;
+ this.startOffset = startOffset;
+ this.endOffset = endOffset;
+ this.attributes = attributes;
+ this.isOpenTag = isOpenTag;
+ this.selfClosingTag = selfClosingTag;
+}
+
+/**
+ * @constructor
+ * @param {string} name
+ */
+WebInspector.HTMLModel.Element = function(name)
+{
+ this.name = name;
+ this.children = [];
+ this.parent = null;
+ this.openTag = null;
+ this.closeTag = null;
}
« no previous file with comments | « third_party/WebKit/Source/devtools/front_end/formatter_worker/FormatterWorker.js ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698