/******************************************************************************* * 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; } }