Chromium Code Reviews| 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; |
| } |