/*
* Copyright (c) 2013, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.engine.html.ast;
import com.google.dart.engine.html.ast.visitor.XmlVisitor;
import com.google.dart.engine.html.scanner.Token;
import com.google.dart.engine.html.scanner.TokenType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Instances of {@code XmlTagNode} represent XML or HTML elements such as {@code <p>} and
* {@code <body foo="bar"> ... </body>}.
*
* @coverage dart.engine.html
*/
public class XmlTagNode extends XmlNode {
/**
* Constant representing empty list of attributes.
*/
public static final List<XmlAttributeNode> NO_ATTRIBUTES = Collections.unmodifiableList(new ArrayList<XmlAttributeNode>());
/**
* Constant representing empty list of tag nodes.
*/
public static final List<XmlTagNode> NO_TAG_NODES = Collections.unmodifiableList(new ArrayList<XmlTagNode>());
/**
* The starting {@link TokenType#LT} token (not {@code null}).
*/
private final Token nodeStart;
/**
* The {@link TokenType#TAG} token after the starting '<' (not {@code null}).
*/
private final Token tag;
/**
* The attributes contained by the receiver (not {@code null}, contains no {@code null}s).
*/
private List<XmlAttributeNode> attributes;
/**
* The {@link TokenType#GT} or {@link TokenType#SLASH_GT} token after the attributes (not
* {@code null}). The token may be the same token as {@link #nodeEnd} if there are no child
* {@link #tagNodes}.
*/
private final Token attributeEnd;
/**
* The tag nodes contained in the receiver (not {@code null}, contains no {@code null}s).
*/
private final List<XmlTagNode> tagNodes;
/**
* The token (not {@code null}) after the content, which may be
* <ul>
* <li>(1) {@link TokenType#LT_SLASH} for nodes with open and close tags, or</li>
* <li>(2) the {@link TokenType#LT} nodeStart of the next sibling node if this node is self
* closing or the attributeEnd is {@link TokenType#SLASH_GT}, or</li>
* <li>(3) {@link TokenType#EOF} if the node does not have a closing tag and is the last node in
* the stream {@link TokenType#LT_SLASH} token after the content, or {@code null} if there is no
* content and the attributes ended with {@link TokenType#SLASH_GT}.</li>
* </ul>
*/
private final Token contentEnd;
/**
* The closing {@link TokenType#TAG} after the child elements or {@code null} if there is no
* content and the attributes ended with {@link TokenType#SLASH_GT}
*/
private final Token closingTag;
/**
* The ending {@link TokenType#GT} or {@link TokenType#SLASH_GT} token (not {@code null}).
*/
private final Token nodeEnd;
/**
* The expressions that are embedded in the tag's content.
*/
private XmlExpression[] expressions = XmlExpression.EMPTY_ARRAY;
/**
* Construct a new instance representing an XML or HTML element
*
* @param nodeStart the starting {@link TokenType#LT} token (not {@code null})
* @param tag the {@link TokenType#TAG} token after the starting '<' (not {@code null}).
* @param attributes the attributes associated with this element or {@link #NO_ATTRIBUTES} (not
* {@code null}, contains no {@code null}s)
* @param attributeEnd The {@link TokenType#GT} or {@link TokenType#SLASH_GT} token after the
* attributes (not {@code null}). The token may be the same token as {@link #nodeEnd} if
* there are no child {@link #tagNodes}.
* @param tagNodes child tag nodes of the receiver or {@link #NO_TAG_NODES} (not {@code null},
* contains no {@code null}s)
* @param contentEnd the token (not {@code null}) after the content, which may be
* <ul>
* <li>(1) {@link TokenType#LT_SLASH} for nodes with open and close tags, or</li>
* <li>(2) the {@link TokenType#LT} nodeStart of the next sibling node if this node is
* self closing or the attributeEnd is {@link TokenType#SLASH_GT}, or</li>
* <li>(3) {@link TokenType#EOF} if the node does not have a closing tag and is the last
* node in the stream {@link TokenType#LT_SLASH} token after the content, or {@code null}
* if there is no content and the attributes ended with {@link TokenType#SLASH_GT}.</li>
* </ul>
* @param closingTag the closing {@link TokenType#TAG} after the child elements or {@code null} if
* there is no content and the attributes ended with {@link TokenType#SLASH_GT}
* @param nodeEnd the ending {@link TokenType#GT} or {@link TokenType#SLASH_GT} token (not
* {@code null})
*/
public XmlTagNode(Token nodeStart, Token tag, List<XmlAttributeNode> attributes,
Token attributeEnd, List<XmlTagNode> tagNodes, Token contentEnd, Token closingTag,
Token nodeEnd) {
this.nodeStart = nodeStart;
this.tag = tag;
this.attributes = becomeParentOfAll(attributes, NO_ATTRIBUTES);
this.attributeEnd = attributeEnd;
this.tagNodes = becomeParentOfAll(tagNodes, NO_TAG_NODES);
this.contentEnd = contentEnd;
this.closingTag = closingTag;
this.nodeEnd = nodeEnd;
}
@Override
public <R> R accept(XmlVisitor<R> visitor) {
return visitor.visitXmlTagNode(this);
}
/**
* Answer the attribute with the specified name.
*
* @param name the attribute name
* @return the attribute or {@code null} if no matching attribute is found
*/
public XmlAttributeNode getAttribute(String name) {
for (XmlAttributeNode attribute : attributes) {
if (attribute.getName().equals(name)) {
return attribute;
}
}
return null;
}
/**
* The {@link TokenType#GT} or {@link TokenType#SLASH_GT} token after the attributes (not
* {@code null}). The token may be the same token as {@link #nodeEnd} if there are no child
* {@link #tagNodes}.
*
* @return the token (not {@code null})
*/
public Token getAttributeEnd() {
return attributeEnd;
}
/**
* Answer the receiver's attributes. Callers should not manipulate the returned list to edit the
* AST structure.
*
* @return the attributes (not {@code null}, contains no {@code null}s)
*/
public List<XmlAttributeNode> getAttributes() {
return attributes;
}
/**
* Find the attribute with the given name (see {@link #getAttribute(String)} and answer the lexeme
* for the attribute's value token without the leading and trailing quotes (see
* {@link XmlAttributeNode#getText()}).
*
* @param name the attribute name
* @return the attribute text or {@code null} if no matching attribute is found
*/
public String getAttributeText(String name) {
XmlAttributeNode attribute = getAttribute(name);
return attribute != null ? attribute.getText() : null;
}
@Override
public Token getBeginToken() {
return nodeStart;
}
/**
* The the closing {@link TokenType#TAG} after the child elements or {@code null} if there is no
* content and the attributes ended with {@link TokenType#SLASH_GT}
*
* @return the closing tag or {@code null}
*/
public Token getClosingTag() {
return closingTag;
}
/**
* Answer a string representing the content contained in the receiver. This includes the textual
* representation of any child tag nodes ({@link #getTagNodes()}). Whitespace between '<',
* '</', and '>', '/>' is discarded, but all other whitespace is preserved.
*
* @return the content (not {@code null})
*/
public String getContent() {
Token token = attributeEnd.getNext();
if (token == contentEnd) {
return "";
}
//TODO (danrubel): handle CDATA and replace HTML character encodings with the actual characters
String content = token.getLexeme();
token = token.getNext();
if (token == contentEnd) {
return content;
}
StringBuilder buffer = new StringBuilder(content);
while (token != contentEnd) {
buffer.append(token.getLexeme());
token = token.getNext();
}
return buffer.toString();
}
/**
* Answer the token (not {@code null}) after the content, which may be
* <ul>
* <li>(1) {@link TokenType#LT_SLASH} for nodes with open and close tags, or</li>
* <li>(2) the {@link TokenType#LT} nodeStart of the next sibling node if this node is self
* closing or the attributeEnd is {@link TokenType#SLASH_GT}, or</li>
* <li>(3) {@link TokenType#EOF} if the node does not have a closing tag and is the last node in
* the stream {@link TokenType#LT_SLASH} token after the content, or {@code null} if there is no
* content and the attributes ended with {@link TokenType#SLASH_GT}.</li>
* </ul>
*
* @return the token (not {@code null})
*/
public Token getContentEnd() {
return contentEnd;
}
@Override
public Token getEndToken() {
if (nodeEnd != null) {
return nodeEnd;
}
if (closingTag != null) {
return closingTag;
}
if (contentEnd != null) {
return contentEnd;
}
if (!tagNodes.isEmpty()) {
return tagNodes.get(tagNodes.size() - 1).getEndToken();
}
if (attributeEnd != null) {
return attributeEnd;
}
if (!attributes.isEmpty()) {
return attributes.get(attributes.size() - 1).getEndToken();
}
return tag;
}
/**
* Return the expressions that are embedded in the tag's content.
*
* @return the expressions that are embedded in the tag's content
*/
public XmlExpression[] getExpressions() {
return expressions;
}
/**
* Answer the ending {@link TokenType#GT} or {@link TokenType#SLASH_GT} token.
*
* @return the token (not {@code null})
*/
public Token getNodeEnd() {
return nodeEnd;
}
/**
* Answer the starting {@link TokenType#LT} token.
*
* @return the token (not {@code null})
*/
public Token getNodeStart() {
return nodeStart;
}
/**
* Answer the tag name after the starting '<'.
*
* @return the tag name (not {@code null})
*/
public String getTag() {
return tag.getLexeme();
}
/**
* Answer the tag nodes contained in the receiver. Callers should not manipulate the returned list
* to edit the AST structure.
*
* @return the children (not {@code null}, contains no {@code null}s)
*/
public List<XmlTagNode> getTagNodes() {
return tagNodes;
}
/**
* Answer the {@link TokenType#TAG} token after the starting '<'.
*
* @return the token (not {@code null})
*/
public Token getTagToken() {
return tag;
}
/**
* Set the expressions that are embedded in the tag's content.
*
* @param expressions expressions that are embedded in the tag's content
*/
public void setExpressions(XmlExpression[] expressions) {
this.expressions = expressions;
}
@Override
public void visitChildren(XmlVisitor<?> visitor) {
for (XmlAttributeNode node : attributes) {
node.accept(visitor);
}
for (XmlTagNode node : tagNodes) {
node.accept(visitor);
}
}
}