blob: 873ab766e412dbe26a742069902d2c388f7270ba [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2001, 2008 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 java.util.List;
import java.util.Vector;
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.utils.StringUtils;
import org.eclipse.wst.xml.core.internal.Logger;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.w3c.dom.Node;
public class TextNodeFormatter extends NodeFormatter {
static private final String CR = "\r"; //$NON-NLS-1$
static private final String DELIMITERS = " \t\n\r\f"; //$NON-NLS-1$
static private final String EMPTY_STRING = ""; //$NON-NLS-1$
static private final String FF = "\f"; //$NON-NLS-1$
static private final String LF = "\n"; //$NON-NLS-1$
static private final String SPACE = " "; //$NON-NLS-1$
static private final String TAB = "\t"; //$NON-NLS-1$
private String compressSpaces(String string, IStructuredFormatContraints formatContraints) {
/*
* Note that the StructuredTextEditor supports mixed new line
* characters (CR, LF, CRLF) in one file. We have to handle that when
* we try to preserve blank lines.
*/
String[] stringArray = null;
boolean clearAllBlankLines = formatContraints.getClearAllBlankLines();
if (clearAllBlankLines)
stringArray = StringUtils.asArray(string);
else
stringArray = StringUtils.asArray(string, DELIMITERS, true);
StringBuffer compressedString = new StringBuffer();
if (stringArray.length > 0) {
boolean cr = false, lf = false, cr2 = false, nonSpace = true;
if (stringArray[0].compareTo(CR) == 0)
cr = true;
else if (stringArray[0].compareTo(LF) == 0)
lf = true;
else if ((stringArray[0].compareTo(SPACE) != 0) && (stringArray[0].compareTo(TAB) != 0) && (stringArray[0].compareTo(FF) != 0)) {
compressedString.append(stringArray[0]);
nonSpace = true;
}
for (int i = 1; i < stringArray.length; i++) {
if (stringArray[i].compareTo(CR) == 0) {
if (cr && lf) {
if (nonSpace) {
compressedString.append(CR + LF);
nonSpace = false;
}
compressedString.append(stringArray[i]);
cr2 = true;
}
else if (cr) {
if (nonSpace) {
compressedString.append(CR);
nonSpace = false;
}
compressedString.append(stringArray[i]);
cr2 = true;
}
else
cr = true;
}
else if (stringArray[i].compareTo(LF) == 0) {
if (cr && lf && cr2) {
compressedString.append(stringArray[i]);
}
else if (lf) {
if (nonSpace) {
compressedString.append(LF);
nonSpace = false;
}
compressedString.append(stringArray[i]);
}
else
lf = true;
}
else if ((stringArray[i].compareTo(SPACE) != 0) && (stringArray[i].compareTo(TAB) != 0) && (stringArray[i].compareTo(FF) != 0)) {
if (compressedString.length() > 0)
compressedString.append(SPACE);
compressedString.append(stringArray[i]);
cr = false;
lf = false;
cr2 = false;
nonSpace = true;
}
}
}
return compressedString.toString();
}
protected void formatNode(IDOMNode node, IStructuredFormatContraints formatContraints) {
// [111674] If inside xml:space="preserve" element, we bail
if (formatContraints.getInPreserveSpaceElement())
return;
if (node != null) {
IStructuredDocument doc = node.getStructuredDocument();
int lineWidth = getFormatPreferences().getLineWidth();
int currentAvailableLineWidth = computeAvailableLineWidth(doc, node.getStartOffset(), lineWidth);
String nodeText = getNodeText(node);
String compressedText = compressSpaces(nodeText, formatContraints);
IDOMNode parentNode = (IDOMNode) node.getParentNode();
if (((enoughSpace(parentNode, currentAvailableLineWidth, compressedText)) && (noSiblingsAndNoFollowingComment(node)) && !firstStructuredDocumentRegionContainsLineDelimiters(parentNode)) || node.getStartOffset() == 0) {
handleNoReflow(node, doc, compressedText, parentNode);
}
else {
// not enough space, need to reflow text
String nodeIndentation = formatContraints.getCurrentIndent();
currentAvailableLineWidth = lineWidth - getIndentationLength(nodeIndentation);
List vector = reflowText(compressedText, currentAvailableLineWidth);
int vectorSize = vector.size();
StringBuffer reflowedTextBuffer = new StringBuffer();
String lineDelimiter = getLineDelimiter(doc, node.getStartOffset());
// handle first line specially to check for allowWhitespace
if (vectorSize > 0) {
// determines whether or not to allow whitespace if there
// is an entity or cdata before it
boolean allowWhitespace = true;
// [206072] StringIndexOutOfBoundsException
if (nodeText.length() == 0 || !Character.isWhitespace(nodeText.charAt(0))) {
Node previousSibling = node.getPreviousSibling();
if (previousSibling != null && (previousSibling.getNodeType() == Node.ENTITY_REFERENCE_NODE || previousSibling.getNodeType() == Node.CDATA_SECTION_NODE))
allowWhitespace = false;
}
String theString = (String) vector.get(0);
if (allowWhitespace) {
reflowedTextBuffer.append(lineDelimiter);
if (theString.trim().length() > 0)
reflowedTextBuffer.append(nodeIndentation).append(theString);
}
else {
reflowedTextBuffer.append(theString);
}
}
// do the rest of the lines
for (int i = 1; i < vectorSize; i++) {
String theString = (String) vector.get(i);
if (theString.trim().length() > 0)
reflowedTextBuffer.append(lineDelimiter).append(nodeIndentation).append(theString);
else
reflowedTextBuffer.append(lineDelimiter);
}
String reflowedText = reflowedTextBuffer.toString();
if (node.getNextSibling() == null) {
if (isEndTagMissing(parentNode)) {
// don't add indentation to end if parent end tag is
// missing
}
else {
// add parent's indentation to end
nodeIndentation = getNodeIndent(parentNode);
if (!reflowedText.endsWith(lineDelimiter + nodeIndentation)) {
reflowedText = StringUtils.appendIfNotEndWith(reflowedText, lineDelimiter);
reflowedText = StringUtils.appendIfNotEndWith(reflowedText, nodeIndentation);
}
}
}
else {
if (!reflowedText.endsWith(lineDelimiter + nodeIndentation)) {
// not already ended with the expected indentation
Node nextSibling = node.getNextSibling();
if (nextSibling.getNodeType() == Node.COMMENT_NODE) {
// add indentation to end if
// currentTextEndsWithLineDelimiter
// or followed by multiLineComment
int indexOfLastLineDelimiter = StringUtils.indexOfLastLineDelimiter(nodeText);
boolean currentTextEndsWithLineDelimiter = indexOfLastLineDelimiter != -1;
if (currentTextEndsWithLineDelimiter) {
// no more non blank character after the last
// line delimiter
currentTextEndsWithLineDelimiter = StringUtils.indexOfNonblank(nodeText, indexOfLastLineDelimiter) == -1;
}
String nodeValue = nextSibling.getNodeValue();
boolean multiLineComment = StringUtils.containsLineDelimiter(nodeValue);
if (currentTextEndsWithLineDelimiter || multiLineComment) {
reflowedText = StringUtils.appendIfNotEndWith(reflowedText, lineDelimiter);
reflowedText = StringUtils.appendIfNotEndWith(reflowedText, nodeIndentation);
}
}
else if (nextSibling.getNodeType() == Node.ENTITY_REFERENCE_NODE || nextSibling.getNodeType() == Node.CDATA_SECTION_NODE) {
int textLength = nodeText.length();
if (textLength > 0 && Character.isWhitespace(nodeText.charAt(textLength - 1))) {
reflowedText = StringUtils.appendIfNotEndWith(reflowedText, lineDelimiter);
reflowedText = StringUtils.appendIfNotEndWith(reflowedText, nodeIndentation);
}
}
else {
// not a comment, just add add indentation to end
reflowedText = StringUtils.appendIfNotEndWith(reflowedText, lineDelimiter);
reflowedText = StringUtils.appendIfNotEndWith(reflowedText, nodeIndentation);
}
}
}
replaceNodeValue(node, reflowedText);
}
}
}
/**
* Keeps text inline with its parent (no reflow necessary)
*
* @param node
* @param doc
* @param compressedText
* @param parentNode
*/
private void handleNoReflow(IDOMNode node, IStructuredDocument doc, String compressedText, IDOMNode parentNode) {
String nodeIndentation;
// enough space and text has no line delimiters and (node has no
// siblings or followed by inline comment) and
// parentFirstStructuredDocumentRegionContainsLineDelimiters
if (isEndTagMissing(parentNode)) {
parentNode = (IDOMNode) parentNode.getParentNode();
while (isEndTagMissing(parentNode))
parentNode = (IDOMNode) parentNode.getParentNode();
// add parent's indentation to end
nodeIndentation = getNodeIndent(parentNode);
String lineDelimiter = getLineDelimiter(doc, node.getStartOffset());
if (!compressedText.endsWith(lineDelimiter + nodeIndentation)) {
compressedText = StringUtils.appendIfNotEndWith(compressedText, lineDelimiter);
compressedText = StringUtils.appendIfNotEndWith(compressedText, nodeIndentation);
}
}
if ((parentNode != null) && (parentNode.getNodeType() == Node.DOCUMENT_NODE) && (node.getNodeValue().length() > 0) && (node.getNodeValue().trim().length() == 0) && ((node.getPreviousSibling() == null) || (node.getNextSibling() == null)))
// delete spaces at the beginning or end of the document
compressedText = EMPTY_STRING;
replaceNodeValue(node, compressedText);
}
private boolean noSiblingsAndNoFollowingComment(IDOMNode node) {
IDOMNode nextSibling = (IDOMNode) node.getNextSibling();
return !nodeHasSiblings(node) || (noLineDelimiter(node) && isComment(nextSibling) && noLineDelimiter(nextSibling));
}
private boolean isComment(IDOMNode node) {
boolean result = false;
if (node != null) {
result = node.getNodeType() == Node.COMMENT_NODE;
}
return result;
}
private boolean noLineDelimiter(IDOMNode node) {
boolean result = false;
if (node != null) {
result = !StringUtils.containsLineDelimiter(node.getNodeValue());
}
return result;
}
/**
* Calculates if there is enough space on the current line for
* compressedText (and for its parent end tag)
*
* @param parentNode
* @param currentAvailableLineWidth
* @param compressedText
* @return
*/
private boolean enoughSpace(IDOMNode parentNode, int currentAvailableLineWidth, String compressedText) {
int parentEndTagLength = parentNode.getNodeName().length() + 3;
return compressedText.length() <= (currentAvailableLineWidth - parentEndTagLength) && !StringUtils.containsLineDelimiter(compressedText);
}
protected Vector reflowText(String text, int availableWidth) {
String[] stringArray = null;
boolean clearAllBlankLines = getFormatPreferences().getClearAllBlankLines();
if (clearAllBlankLines)
stringArray = StringUtils.asArray(text);
else
stringArray = StringUtils.asArray(text, DELIMITERS, true);
Vector output = new Vector();
if ((stringArray != null) && (stringArray.length > 0)) {
StringBuffer buffer = new StringBuffer();
if (stringArray[0].compareTo(CR) != 0)
buffer.append(stringArray[0]);
int bufferLength = stringArray[0].toString().length();
boolean cr = stringArray[0].compareTo(CR) == 0;
for (int i = 1; i < stringArray.length; i++) {
String eachString = stringArray[i];
if ((eachString.compareTo(SPACE) != 0) && (eachString.compareTo(TAB) != 0) && (eachString.compareTo(FF) != 0)) {
if ((bufferLength + 1 + eachString.length() > availableWidth) || (eachString.compareTo(CR) == 0) || (eachString.compareTo(LF) == 0)) {
if ((eachString.compareTo(LF) == 0) && cr) {
// do nothing
}
else {
output.add(buffer.toString());
buffer = new StringBuffer();
bufferLength = 0;
}
cr = eachString.compareTo(CR) == 0;
}
else if (buffer.toString().trim().length() > 0) {
buffer.append(SPACE);
bufferLength++;
}
if ((eachString.compareTo(CR) != 0) && (eachString.compareTo(LF) != 0)) {
buffer.append(eachString);
bufferLength = bufferLength + eachString.length();
}
}
}
output.add(buffer.toString());
}
else
output.add(text);
return output;
}
private String getLineDelimiter(IStructuredDocument doc, int nodeOffset) {
int line = doc.getLineOfOffset(nodeOffset);
String lineDelimiter = doc.getLineDelimiter();
try {
if (line > 0) {
lineDelimiter = doc.getLineDelimiter(line - 1);
}
}
catch (BadLocationException e) {
// log for now, unless we find reason not to
Logger.log(Logger.INFO, e.getMessage());
}
// BUG115716: if cannot get line delimiter from current line, just
// use default line delimiter
if (lineDelimiter == null)
lineDelimiter = doc.getLineDelimiter();
return lineDelimiter;
}
private int computeAvailableLineWidth(IStructuredDocument doc, int nodeOffset, int lineWidth) {
// compute current available line width
int currentAvailableLineWidth = 0;
try {
int lineOffset = doc.getLineInformationOfOffset(nodeOffset).getOffset();
String text = doc.get(lineOffset, nodeOffset - lineOffset);
int usedWidth = getIndentationLength(text);
currentAvailableLineWidth = lineWidth - usedWidth;
}
catch (BadLocationException e) {
// log for now, unless we find reason not to
Logger.log(Logger.INFO, e.getMessage());
}
return currentAvailableLineWidth;
}
}