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

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: 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
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..997259b79c2a2bf60291e106e60d6e6b73dee5ed 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,475 @@
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);
+ var document = this._model.document();
+ for (var i = 0; i < document.children.length; ++i)
+ this._walk(document.children[i]);
+ this._formatTokensTill(document, text.length);
+ },
+
+ /**
+ * @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)
+ return;
+ 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)
+ {
+ // Avoid leading new line.
+ if (element.parent === this._model.document() && this._model.document().children[0] === element)
+ return;
+ if (!element.children.length)
+ return;
+ this._builder.addNewLine();
+ },
+
+ /**
+ * @param {!WebInspector.HTMLModel.Element} element
+ */
+ _afterOpenTag: function(element)
+ {
+ if (!element.children.length)
+ return;
+ this._builder.increaseNestingLevel();
+ this._builder.addNewLine();
+ },
+
+ /**
+ * @param {!WebInspector.HTMLModel.Element} element
+ */
+ _beforeCloseTag: function(element)
+ {
+ if (!element.children.length)
+ 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")) {
+ this._builder.addToken(token.value.trim(), token.startOffset);
+ this._builder.addNewLine();
+ return;
+ }
+ if (token.type.has("meta")) {
+ this._builder.addToken(token.value.trim(), token.startOffset);
+ this._builder.addNewLine();
+ return;
dgozman 2016/04/19 01:55:03 else if ()
lushnikov 2016/04/19 23:11:40 Kinda Done. We both liked what we came up with abo
+ }
+ if (token.type.has("attribute"))
+ this._builder.addSoftSpace();
+ if (element === this._model.document()) {
+ this._builder.addToken(token.value, token.startOffset);
dgozman 2016/04/19 01:55:03 This is a default.
lushnikov 2016/04/19 23:11:40 Done.
+ return;
+ }
+ if (token.startOffset < element.openTag.endOffset) {
+ this._builder.addToken(token.value, token.startOffset);
dgozman 2016/04/19 01:55:03 ditto
lushnikov 2016/04/19 23:11:40 Done.
+ return;
+ }
+ if (element.closeTag && token.startOffset >= element.closeTag.startOffset) {
+ this._builder.addToken(token.value, token.startOffset);
dgozman 2016/04/19 01:55:03 ditto
lushnikov 2016/04/19 23:11:40 Done.
+ return;
+ }
+ if (element.name === "style") {
+ this._builder.addNewLine();
+ this._cssFormatter.format(this._text, this._lineEndings, token.startOffset, token.endOffset);
+ return;
+ }
+ if (element.name === "script") {
+ this._builder.addNewLine();
+ 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);
+ return;
+ }
+ this._builder.addToken(token.value, token.startOffset);
+ this._builder.addNewLine();
+ return;
+ }
+ 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.States.Initial;
+ this._document = new WebInspector.HTMLModel.Element("document");
+ this._stack = [this._document];
+
+ this._autoClosingTags = new Multimap();
+ for (var key in WebInspector.HTMLModel.AutoClosingTags) {
+ var values = WebInspector.HTMLModel.AutoClosingTags[key];
+ for (var value of values)
+ this._autoClosingTags.set(key, value);
+ }
+
+ 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": ["body"],
dgozman 2016/04/19 01:55:03 Maybe make a set here and use as-is? "head": new S
lushnikov 2016/04/19 23:11:40 Done.
+ "li": ["li"],
+ "dt": ["dt", "dd"],
+ "dd": ["dt", "dd"],
+ "p": ["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": ["rb", "rt", "rtc", "rp"],
+ "rt": ["rb", "rt", "rtc", "rp"],
+ "rtc": ["rb", "rtc", "rp"],
+ "rp": ["rb", "rt", "rtc", "rp"],
+ "optgroup": ["optgroup"],
+ "option": ["option", "optgroup"],
+ "colgroup": ["colgroup"],
+ "thead": ["tbody", "tfoot"],
+ "tbody": ["tbody", "tfoot"],
+ "tfoot": ["tbody"],
+ "tr": ["tr"],
+ "td": ["td", "th"],
+ "th": ["td", "th"],
+};
+
+/** @enum {string} */
+WebInspector.HTMLModel.States = {
dgozman 2016/04/19 01:55:03 nit: State
lushnikov 2016/04/19 23:11:40 Done.
+ 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));
+ }
/**
+ * @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);
- if (tokenType["bracket"] && (tokenValue === "<" || tokenValue === "</")) {
- accumulatedTokenValue = tokenValue;
- return;
+ var element = this._stack.peekLast();
+ if (element && (element.name === "script" || element.name === "style") && element.openTag.endOffset === lastOffset)
+ return WebInspector.AbortTokenization;
+ }
+ },
+
+ /**
+ * @param {!WebInspector.HTMLModel.Token} token
+ */
+ _updateDOM: function(token)
+ {
+ var S = WebInspector.HTMLModel.States;
+ 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 (this._autoClosingTags.hasValue(topElement.name, 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;
}

Powered by Google App Engine
This is Rietveld 408576698