blob: beed8f4d4971970e793fd823b8deaea19aeccbbc [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2001, 2007 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* Jens Lukowski/Innoopract - initial renaming/restructuring
* Jesper Steen Møller - xml:space='preserve' support
*
*******************************************************************************/
package org.eclipse.wst.xml.core.internal.provisional.format;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.wst.sse.core.internal.format.IStructuredFormatContraints;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
import org.eclipse.wst.sse.core.utils.StringUtils;
import org.eclipse.wst.xml.core.internal.Logger;
import org.eclipse.wst.xml.core.internal.contentmodel.CMAttributeDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMNamedNodeMap;
import org.eclipse.wst.xml.core.internal.document.AttrImpl;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.eclipse.wst.xml.core.internal.provisional.document.ISourceGenerator;
import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
import org.eclipse.wst.xml.core.internal.ssemodelquery.ModelQueryAdapter;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
public class ElementNodeFormatter extends DocumentNodeFormatter {
static private final char DOUBLE_QUOTE = '"';//$NON-NLS-1$
static private final String DOUBLE_QUOTES = "\"\"";//$NON-NLS-1$
static private final char EQUAL_CHAR = '='; // equal sign$NON-NLS-1$
static private final String PRESERVE = "preserve";//$NON-NLS-1$
static private final String PRESERVE_QUOTED = "\"preserve\"";//$NON-NLS-1$
static private final char SINGLE_QUOTE = '\'';//$NON-NLS-1$
static private final String XML_SPACE = "xml:space";//$NON-NLS-1$
static private final char SPACE_CHAR = ' '; //$NON-NLS-1$
static private final String XSL_NAMESPACE = "http://www.w3.org/1999/XSL/Transform"; //$NON-NLS-1$
static private final String XSL_ATTRIBUTE = "attribute"; //$NON-NLS-1$
static private final String XSL_TEXT = "text"; //$NON-NLS-1$
protected void formatEndTag(IDOMNode node, IStructuredFormatContraints formatContraints) {
if (!isEndTagMissing(node)) {
// end tag exists
IStructuredDocument structuredDocument = node.getModel().getStructuredDocument();
String lineDelimiter = structuredDocument.getLineDelimiter();
String nodeIndentation = getNodeIndent(node);
IDOMNode lastChild = (IDOMNode) node.getLastChild();
if (lastChild != null && lastChild.getNodeType() != Node.TEXT_NODE) {
if (isEndTagMissing(lastChild)) {
// find deepest child
IDOMNode deepestChild = (IDOMNode) lastChild.getLastChild();
while (deepestChild != null && deepestChild.getLastChild() != null && isEndTagMissing(deepestChild)) {
lastChild = deepestChild;
deepestChild = (IDOMNode) deepestChild.getLastChild();
}
if (deepestChild != null) {
if (deepestChild.getNodeType() == Node.TEXT_NODE) {
// Special indentation handling if lastChild's end
// tag is missing and deepestChild is a text node.
String nodeText = deepestChild.getNodeValue();
if (!nodeText.endsWith(lineDelimiter + nodeIndentation)) {
nodeText = StringUtils.appendIfNotEndWith(nodeText, lineDelimiter);
nodeText = StringUtils.appendIfNotEndWith(nodeText, nodeIndentation);
}
replaceNodeValue(deepestChild, nodeText);
}
else
insertAfterNode(lastChild, lineDelimiter + nodeIndentation);
}
}
else
// indent end tag
insertAfterNode(lastChild, lineDelimiter + nodeIndentation);
}
else if (lastChild == null && firstStructuredDocumentRegionContainsLineDelimiters(node)) {
// BUG174243 do not indent end tag if node has empty content
// (otherwise new text node would be introduced)
ModelQueryAdapter adapter = (ModelQueryAdapter) ((IDOMDocument) node.getOwnerDocument()).getAdapterFor(ModelQueryAdapter.class);
CMElementDeclaration elementDeclaration = (CMElementDeclaration) adapter.getModelQuery().getCMNode(node);
if ((elementDeclaration == null) || (elementDeclaration.getContentType() != CMElementDeclaration.EMPTY)) {
// indent end tag
replace(structuredDocument, node.getFirstStructuredDocumentRegion().getEndOffset(), 0, lineDelimiter + nodeIndentation);
}
}
// format end tag name
IStructuredDocumentRegion endTagStructuredDocumentRegion = node.getLastStructuredDocumentRegion();
if (endTagStructuredDocumentRegion.getRegions().size() >= 3) {
ITextRegion endTagNameRegion = endTagStructuredDocumentRegion.getRegions().get(1);
removeRegionSpaces(node, endTagStructuredDocumentRegion, endTagNameRegion);
}
}
}
protected void formatNode(IDOMNode node, IStructuredFormatContraints formatContraints) {
if (node != null) {
// format indentation before node
formatIndentationBeforeNode(node, formatContraints);
// format start tag
IDOMNode newNode = node;
int startTagStartOffset = node.getStartOffset();
IDOMModel structuredModel = node.getModel();
boolean currentlyInXmlSpacePreserve = formatContraints.getInPreserveSpaceElement();
formatStartTag(node, formatContraints);
// save new node
newNode = (IDOMNode) structuredModel.getIndexedRegion(startTagStartOffset);
IStructuredDocumentRegion flatNode = newNode.getFirstStructuredDocumentRegion();
if (flatNode != null) {
ITextRegionList regions = flatNode.getRegions();
ITextRegion lastRegion = regions.get(regions.size() - 1);
// format children and end tag if not empty start tag
if (lastRegion.getType() != DOMRegionContext.XML_EMPTY_TAG_CLOSE) {
// format children
formatChildren(newNode, formatContraints);
// save new node
newNode = (IDOMNode) structuredModel.getIndexedRegion(startTagStartOffset);
// format end tag
formatEndTag(newNode, formatContraints);
}
}
formatContraints.setInPreserveSpaceElement(currentlyInXmlSpacePreserve);
// only indent if not at last node
if (newNode != null && newNode.getNextSibling() != null)
// format indentation after node
formatIndentationAfterNode(newNode, formatContraints);
}
}
/**
* This method formats the start tag name, and formats the attributes if
* available.
*/
protected void formatStartTag(IDOMNode node, IStructuredFormatContraints formatContraints) {
StructuredFormatPreferencesXML preferences = (StructuredFormatPreferencesXML) getFormatPreferences();
String singleIndent = preferences.getIndent();
String lineIndent = formatContraints.getCurrentIndent();
String attrIndent = lineIndent + singleIndent;
boolean splitMultiAttrs = preferences.getSplitMultiAttrs();
boolean alignEndBracket = preferences.isAlignEndBracket();
boolean sawXmlSpace = false;
IStructuredDocumentRegion flatNode = node.getFirstStructuredDocumentRegion();
NamedNodeMap attributes = node.getAttributes();
// Note: attributes should not be null even if the node has no
// attributes. However, attributes.getLength() will be 0. But, check
// for null just in case.
if (attributes != null) {
// compute current available line width
int currentAvailableLineWidth = 0;
try {
// 1 is for "<"
int nodeNameOffset = node.getStartOffset() + 1 + node.getNodeName().length();
int lineOffset = node.getStructuredDocument().getLineInformationOfOffset(nodeNameOffset).getOffset();
String text = node.getStructuredDocument().get(lineOffset, nodeNameOffset - lineOffset);
int usedWidth = getIndentationLength(text);
currentAvailableLineWidth = preferences.getLineWidth() - usedWidth;
}
catch (BadLocationException e) {
// log for now, unless we find reason not to
Logger.log(Logger.INFO, e.getMessage());
}
StringBuffer stringBuffer = new StringBuffer();
String lineDelimiter = node.getModel().getStructuredDocument().getLineDelimiter();
int attrLength = attributes.getLength();
int lastUndefinedRegionOffset = 0;
boolean startTagSpansOver1Line = false;
for (int i = 0; i < attrLength; i++) {
AttrImpl attr = (AttrImpl) attributes.item(i);
ITextRegion nameRegion = attr.getNameRegion();
ITextRegion equalRegion = attr.getEqualRegion();
ITextRegion valueRegion = attr.getValueRegion();
// append undefined regions
String undefinedRegion = getUndefinedRegions(node, lastUndefinedRegionOffset, attr.getStartOffset() - lastUndefinedRegionOffset);
stringBuffer.append(undefinedRegion);
lastUndefinedRegionOffset = attr.getStartOffset();
// check for xml:space attribute
if (flatNode.getText(nameRegion).compareTo(XML_SPACE) == 0) {
if (valueRegion == null) {
// [111674] If nothing has been written yet, treat as
// preserve, but only as hint
formatContraints.setInPreserveSpaceElement(true);
// Note we don't set 'sawXmlSpace', so that default or
// fixed DTD/XSD values may override.
}
else {
ISourceGenerator generator = node.getModel().getGenerator();
String newAttrValue = generator.generateAttrValue(attr);
// There is a problem in
// StructuredDocumentRegionUtil.getAttrValue(ITextRegion)
// when the region is instanceof ContextRegion.
// Workaround for now.
if (flatNode.getText(valueRegion).length() == 1) {
char firstChar = flatNode.getText(valueRegion).charAt(0);
if ((firstChar == DOUBLE_QUOTE) || (firstChar == SINGLE_QUOTE))
newAttrValue = DOUBLE_QUOTES;
}
if (newAttrValue.compareTo(PRESERVE_QUOTED) == 0)
formatContraints.setInPreserveSpaceElement(true);
else
formatContraints.setInPreserveSpaceElement(false);
sawXmlSpace = true;
}
}
if (splitMultiAttrs && attrLength > 1) {
stringBuffer.append(lineDelimiter + attrIndent);
stringBuffer.append(flatNode.getText(nameRegion));
startTagSpansOver1Line = true;
if (valueRegion != null) {
// append undefined regions
undefinedRegion = getUndefinedRegions(node, lastUndefinedRegionOffset, flatNode.getStartOffset(equalRegion) - lastUndefinedRegionOffset);
stringBuffer.append(undefinedRegion);
lastUndefinedRegionOffset = flatNode.getStartOffset(equalRegion);
stringBuffer.append(EQUAL_CHAR);
// append undefined regions
undefinedRegion = getUndefinedRegions(node, lastUndefinedRegionOffset, flatNode.getStartOffset(valueRegion) - lastUndefinedRegionOffset);
stringBuffer.append(undefinedRegion);
lastUndefinedRegionOffset = flatNode.getStartOffset(valueRegion);
// Note: trim() should not be needed for
// valueRegion.getText(). Just a workaround for a
// problem found in valueRegion for now.
stringBuffer.append(flatNode.getText(valueRegion).trim());
}
}
else {
if (valueRegion != null) {
int textLength = 1 + flatNode.getText(nameRegion).length() + 1 + flatNode.getText(valueRegion).length();
if (i == attrLength - 1) {
if (flatNode != null) {
ITextRegionList regions = flatNode.getRegions();
ITextRegion lastRegion = regions.get(regions.size() - 1);
if (lastRegion.getType() != DOMRegionContext.XML_EMPTY_TAG_CLOSE)
// 3 is for " />"
textLength += 3;
else
// 1 is for ">"
textLength++;
}
}
if (currentAvailableLineWidth >= textLength) {
stringBuffer.append(SPACE_CHAR);
currentAvailableLineWidth--;
}
else {
stringBuffer.append(lineDelimiter + attrIndent);
startTagSpansOver1Line = true;
currentAvailableLineWidth = preferences.getLineWidth() - attrIndent.length();
}
stringBuffer.append(flatNode.getText(nameRegion));
// append undefined regions
undefinedRegion = getUndefinedRegions(node, lastUndefinedRegionOffset, flatNode.getStartOffset(equalRegion) - lastUndefinedRegionOffset);
stringBuffer.append(undefinedRegion);
lastUndefinedRegionOffset = flatNode.getStartOffset(equalRegion);
stringBuffer.append(EQUAL_CHAR);
// append undefined regions
undefinedRegion = getUndefinedRegions(node, lastUndefinedRegionOffset, flatNode.getStartOffset(valueRegion) - lastUndefinedRegionOffset);
stringBuffer.append(undefinedRegion);
lastUndefinedRegionOffset = flatNode.getStartOffset(valueRegion);
// Note: trim() should not be needed for
// valueRegion.getText(). Just a workaround for a
// problem found in valueRegion for now.
stringBuffer.append(flatNode.getText(valueRegion).trim());
currentAvailableLineWidth -= flatNode.getText(nameRegion).length();
currentAvailableLineWidth--;
currentAvailableLineWidth -= flatNode.getText(valueRegion).trim().length();
}
else {
if (currentAvailableLineWidth >= 1 + flatNode.getText(nameRegion).length()) {
stringBuffer.append(SPACE_CHAR);
currentAvailableLineWidth--;
}
else {
stringBuffer.append(lineDelimiter + attrIndent);
startTagSpansOver1Line = true;
currentAvailableLineWidth = preferences.getLineWidth() - attrIndent.length();
}
stringBuffer.append(flatNode.getText(nameRegion));
currentAvailableLineWidth -= flatNode.getText(nameRegion).length();
}
}
}
// append undefined regions
String undefinedRegion = getUndefinedRegions(node, lastUndefinedRegionOffset, node.getEndOffset() - lastUndefinedRegionOffset);
stringBuffer.append(undefinedRegion);
IDOMModel structuredModel = node.getModel();
IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();
// 1 is for "<"
int offset = node.getStartOffset() + 1 + node.getNodeName().length();
// 1 is for "<"
int length = node.getFirstStructuredDocumentRegion().getTextLength() - 1 - node.getNodeName().length();
if (flatNode != null) {
ITextRegionList regions = flatNode.getRegions();
ITextRegion firstRegion = regions.get(0);
ITextRegion lastRegion = regions.get(regions.size() - 1);
if (firstRegion.getType() == DOMRegionContext.XML_END_TAG_OPEN)
// skip formatting for end tags in this format: </tagName>
return;
else {
if (lastRegion.getType() == DOMRegionContext.XML_TAG_CLOSE || lastRegion.getType() == DOMRegionContext.XML_EMPTY_TAG_CLOSE)
length = length - lastRegion.getLength();
if (lastRegion.getType() == DOMRegionContext.XML_EMPTY_TAG_CLOSE) {
// leave space before XML_EMPTY_TAG_CLOSE: <tagName />
// unless already going to move end bracket
if (!startTagSpansOver1Line || !alignEndBracket)
stringBuffer.append(SPACE_CHAR);
}
}
}
if (startTagSpansOver1Line && alignEndBracket) {
stringBuffer.append(lineDelimiter).append(lineIndent);
}
replace(structuredDocument, offset, length, stringBuffer.toString());
// BUG108074 & BUG84688 - preserve whitespace in xsl:text &
// xsl:attribute
String nodeNamespaceURI = node.getNamespaceURI();
if (XSL_NAMESPACE.equals(nodeNamespaceURI)) {
String nodeName = ((Element) node).getLocalName();
if (XSL_ATTRIBUTE.equals(nodeName) || XSL_TEXT.equals(nodeName)) {
sawXmlSpace = true;
formatContraints.setInPreserveSpaceElement(true);
}
}
// If we didn't see a xml:space attribute above, we'll look for
// one in the DTD.
// We do not check for a conflict between a DTD's 'fixed' value
// and the attribute value found in the instance document, we
// leave that to the validator.
if (!sawXmlSpace) {
ModelQueryAdapter adapter = (ModelQueryAdapter) ((IDOMDocument) node.getOwnerDocument()).getAdapterFor(ModelQueryAdapter.class);
CMElementDeclaration elementDeclaration = (CMElementDeclaration) adapter.getModelQuery().getCMNode(node);
if (elementDeclaration != null) {
int contentType = elementDeclaration.getContentType();
if (preferences.isPreservePCDATAContent() && contentType == CMElementDeclaration.PCDATA) {
formatContraints.setInPreserveSpaceElement(true);
}
else {
CMNamedNodeMap cmAttributes = elementDeclaration.getAttributes();
// Check implied values from the DTD way.
CMAttributeDeclaration attributeDeclaration = (CMAttributeDeclaration) cmAttributes.getNamedItem(XML_SPACE);
if (attributeDeclaration != null) {
// CMAttributeDeclaration found, check it out.
String defaultValue = attributeDeclaration.getAttrType().getImpliedValue();
// xml:space="preserve" means preserve space,
// everything else means back to default.
if (PRESERVE.compareTo(defaultValue) == 0)
formatContraints.setInPreserveSpaceElement(true);
else
formatContraints.setInPreserveSpaceElement(false);
}
}
}
}
}
}
protected String getUndefinedRegions(IDOMNode node, int startOffset, int length) {
String result = new String();
IStructuredDocumentRegion flatNode = node.getFirstStructuredDocumentRegion();
ITextRegionList regions = flatNode.getRegions();
for (int i = 0; i < regions.size(); i++) {
ITextRegion region = regions.get(i);
String regionType = region.getType();
int regionStartOffset = flatNode.getStartOffset(region);
if (regionType.compareTo(DOMRegionContext.UNDEFINED) == 0 && regionStartOffset >= startOffset && regionStartOffset < startOffset + length)
result = result + flatNode.getFullText(region);
}
if (result.length() > 0)
return SPACE_CHAR + result.trim();
else
return result;
}
}