Home | History | Annotate | Download | only in formatting
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.ide.eclipse.adt.internal.editors.formatting;
     17 
     18 import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS;
     19 import static com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors.COLOR_ELEMENT;
     20 import static com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors.DIMEN_ELEMENT;
     21 import static com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors.ITEM_TAG;
     22 import static com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors.STRING_ELEMENT;
     23 import static com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors.STYLE_ELEMENT;
     24 
     25 import com.android.ide.eclipse.adt.AdtUtils;
     26 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     27 
     28 import org.eclipse.wst.xml.core.internal.document.DocumentTypeImpl;
     29 import org.eclipse.wst.xml.core.internal.document.ElementImpl;
     30 import org.w3c.dom.Attr;
     31 import org.w3c.dom.Document;
     32 import org.w3c.dom.Element;
     33 import org.w3c.dom.NamedNodeMap;
     34 import org.w3c.dom.Node;
     35 import org.w3c.dom.NodeList;
     36 
     37 import java.util.ArrayList;
     38 import java.util.Collections;
     39 import java.util.Comparator;
     40 import java.util.List;
     41 
     42 /**
     43  * Visitor which walks over the subtree of the DOM to be formatted and pretty prints
     44  * the DOM into the given {@link StringBuilder}
     45  */
     46 @SuppressWarnings("restriction")
     47 public class XmlPrettyPrinter {
     48     private static final String COMMENT_BEGIN = "<!--"; //$NON-NLS-1$
     49     private static final String COMMENT_END = "-->";    //$NON-NLS-1$
     50 
     51     /** The style to print the XML in */
     52     private final XmlFormatStyle mStyle;
     53     /** Formatting preferences to use when formatting the XML */
     54     private final XmlFormatPreferences mPrefs;
     55     /** Start node to start formatting at */
     56     private Node mStartNode;
     57     /** Start node to stop formatting after */
     58     private Node mEndNode;
     59     /** Whether the visitor is currently in range */
     60     private boolean mInRange;
     61     /** Output builder */
     62     private StringBuilder mOut;
     63     /** String to insert for a single indentation level */
     64     private String mIndentString;
     65     /** Line separator to use */
     66     private String mLineSeparator;
     67     /** If true, we're only formatting an open tag */
     68     private boolean mOpenTagOnly;
     69     /** List of indentation to use for each given depth */
     70     private String[] mIndentationLevels;
     71 
     72     /**
     73      * Creates a new {@link XmlPrettyPrinter}
     74      *
     75      * @param prefs the preferences to format with
     76      * @param style the style to format with
     77      * @param lineSeparator the line separator to use, such as "\n" (can be null, in which
     78      *            case the system default is looked up via the line.separator property)
     79      */
     80     public XmlPrettyPrinter(XmlFormatPreferences prefs, XmlFormatStyle style,
     81             String lineSeparator) {
     82         mPrefs = prefs;
     83         mStyle = style;
     84         if (lineSeparator == null) {
     85             lineSeparator = System.getProperty("line.separator"); //$NON-NLS-1$
     86         }
     87         mLineSeparator = lineSeparator;
     88     }
     89 
     90     /**
     91      * Sets the indentation levels to use (indentation string to use for each depth,
     92      * indexed by depth
     93      *
     94      * @param indentationLevels an array of strings to use for the various indentation
     95      *            levels
     96      */
     97     public void setIndentationLevels(String[] indentationLevels) {
     98         mIndentationLevels = indentationLevels;
     99     }
    100 
    101     /**
    102      * Pretty-prints the given XML document, which must be well-formed. If it is not,
    103      * the original unformatted XML document is returned
    104      *
    105      * @param xml the XML content to format
    106      * @param prefs the preferences to format with
    107      * @param style the style to format with
    108      * @param lineSeparator the line separator to use, such as "\n" (can be null, in which
    109      *     case the system default is looked up via the line.separator property)
    110      * @return the formatted document (or if a parsing error occurred, returns the
    111      *     unformatted document)
    112      */
    113     public static String prettyPrint(String xml, XmlFormatPreferences prefs, XmlFormatStyle style,
    114             String lineSeparator) {
    115         Document document = DomUtilities.parseStructuredDocument(xml);
    116         if (document != null) {
    117             XmlPrettyPrinter printer = new XmlPrettyPrinter(prefs, style, lineSeparator);
    118             StringBuilder sb = new StringBuilder(3 * xml.length() / 2);
    119             printer.prettyPrint(-1, document, null, null, sb, false /*openTagOnly*/);
    120             return sb.toString();
    121         } else {
    122             // Parser error: just return the unformatted content
    123             return xml;
    124         }
    125     }
    126 
    127     /**
    128      * Start pretty-printing at the given node, which must either be the
    129      * startNode or contain it as a descendant.
    130      *
    131      * @param rootDepth the depth of the given node, used to determine indentation
    132      * @param root the node to start pretty printing from (which may not itself be
    133      *            included in the start to end node range but should contain it)
    134      * @param startNode the node to start formatting at
    135      * @param endNode the node to end formatting at
    136      * @param out the {@link StringBuilder} to pretty print into
    137      * @param openTagOnly if true, only format the open tag of the startNode (and nothing
    138      *     else)
    139      */
    140     public void prettyPrint(int rootDepth, Node root, Node startNode, Node endNode,
    141             StringBuilder out, boolean openTagOnly) {
    142         if (startNode == null) {
    143             startNode = root;
    144         }
    145         if (endNode == null) {
    146             endNode = root;
    147         }
    148         assert !openTagOnly || startNode == endNode;
    149 
    150         mStartNode = startNode;
    151         mOpenTagOnly = openTagOnly;
    152         mEndNode = endNode;
    153         mOut = out;
    154         mInRange = false;
    155         mIndentString = mPrefs.getOneIndentUnit();
    156 
    157         visitNode(rootDepth, root);
    158     }
    159 
    160     /** Visit the given node at the given depth */
    161     private void visitNode(int depth, Node node) {
    162         if (node == mStartNode) {
    163             mInRange = true;
    164         }
    165 
    166         if (mInRange) {
    167             visitBeforeChildren(depth, node);
    168             if (mOpenTagOnly && mStartNode == node) {
    169                 mInRange = false;
    170                 return;
    171             }
    172         }
    173 
    174         NodeList children = node.getChildNodes();
    175         for (int i = 0, n = children.getLength(); i < n; i++) {
    176             Node child = children.item(i);
    177             visitNode(depth + 1, child);
    178         }
    179 
    180         if (mInRange) {
    181             visitAfterChildren(depth, node);
    182         }
    183 
    184         if (node == mEndNode) {
    185             mInRange = false;
    186         }
    187     }
    188 
    189     private void visitBeforeChildren(int depth, Node node) {
    190         short type = node.getNodeType();
    191         switch (type) {
    192             case Node.DOCUMENT_NODE:
    193             case Node.DOCUMENT_FRAGMENT_NODE:
    194                 // Nothing to do
    195                 break;
    196 
    197             case Node.ATTRIBUTE_NODE:
    198                 // Handled as part of processing elements
    199                 break;
    200 
    201             case Node.ELEMENT_NODE: {
    202                 printOpenElementTag(depth, node);
    203                 break;
    204             }
    205 
    206             case Node.TEXT_NODE: {
    207                 printText(node);
    208                 break;
    209             }
    210 
    211             case Node.CDATA_SECTION_NODE:
    212                 printCharacterData(depth, node);
    213                 break;
    214 
    215             case Node.PROCESSING_INSTRUCTION_NODE:
    216                 printProcessingInstruction(node);
    217                 break;
    218 
    219             case Node.COMMENT_NODE: {
    220                 printComment(depth, node);
    221                 break;
    222             }
    223 
    224             case Node.DOCUMENT_TYPE_NODE:
    225                 printDocType(node);
    226                 break;
    227 
    228             case Node.ENTITY_REFERENCE_NODE:
    229             case Node.ENTITY_NODE:
    230             case Node.NOTATION_NODE:
    231                 break;
    232             default:
    233                 assert false : type;
    234         }
    235     }
    236 
    237     private void visitAfterChildren(int depth, Node node) {
    238         short type = node.getNodeType();
    239         switch (type) {
    240             case Node.ATTRIBUTE_NODE:
    241                 // Handled as part of processing elements
    242                 break;
    243             case Node.ELEMENT_NODE: {
    244                 printCloseElementTag(depth, node);
    245                 break;
    246             }
    247         }
    248     }
    249 
    250     private void printProcessingInstruction(Node node) {
    251         mOut.append("<?xml "); //$NON-NLS-1$
    252         mOut.append(node.getNodeValue().trim());
    253         mOut.append('?').append('>').append(mLineSeparator);
    254     }
    255 
    256     private void printDocType(Node node) {
    257         // In Eclipse, org.w3c.dom.DocumentType.getTextContent() returns null
    258         if (node instanceof DocumentTypeImpl) {
    259             String content = ((DocumentTypeImpl) node).getSource();
    260             mOut.append(content);
    261             mOut.append(mLineSeparator);
    262         }
    263     }
    264 
    265     private void printCharacterData(int depth, Node node) {
    266         indent(depth);
    267         mOut.append("<![CDATA["); //$NON-NLS-1$
    268         mOut.append(node.getNodeValue());
    269         mOut.append("]]>");     //$NON-NLS-1$
    270         mOut.append(mLineSeparator);
    271     }
    272 
    273     private void printText(Node node) {
    274         String text = node.getNodeValue();
    275 
    276         // Most text nodes are just whitespace for formatting (which we're replacing)
    277         // so look for actual text content and extract that part out
    278         String trimmed = text.trim();
    279         if (trimmed.length() > 0) {
    280             // TODO: Reformat the contents if it is too wide?
    281 
    282             // Note that we append the actual text content, NOT the trimmed content,
    283             // since the whitespace may be significant, e.g.
    284             // <string name="toast_sync_error">Sync error: <xliff:g id="error">%1$s</xliff:g>...
    285             DomUtilities.appendXmlTextValue(mOut, text);
    286 
    287             if (mStyle != XmlFormatStyle.RESOURCE) {
    288                 mOut.append(mLineSeparator);
    289             }
    290         }
    291     }
    292 
    293     private void printComment(int depth, Node node) {
    294         String comment = node.getNodeValue();
    295         boolean multiLine = comment.indexOf('\n') != -1;
    296         String trimmed = comment.trim();
    297 
    298         // See if this is an "end-of-the-line" comment, e.g. it is not a multi-line
    299         // comment and it appears on the same line as an opening or closing element tag;
    300         // if so, continue to place it as a suffix comment
    301         boolean isSuffixComment = false;
    302         if (!multiLine) {
    303             Node previous = node.getPreviousSibling();
    304             isSuffixComment = true;
    305             while (previous != null) {
    306                 short type = previous.getNodeType();
    307                 if (type == Node.TEXT_NODE || type == Node.COMMENT_NODE) {
    308                     if (previous.getNodeValue().indexOf('\n') != -1) {
    309                         isSuffixComment = false;
    310                         break;
    311                     }
    312                 } else {
    313                     break;
    314                 }
    315                 previous = previous.getPreviousSibling();
    316             }
    317             if (isSuffixComment) {
    318                 // Remove newline added by element open tag or element close tag
    319                 if (endsWithLineSeparator()) {
    320                     removeLastLineSeparator();
    321                 }
    322                 mOut.append(' ');
    323             }
    324         }
    325 
    326         // Put the comment on a line on its own? Only if it was separated by a blank line
    327         // in the previous version of the document. In other words, if the document
    328         // adds blank lines between comments this formatter will preserve that fact, and vice
    329         // versa for a tightly formatted document it will preserve that convention as well.
    330         if (!mPrefs.removeEmptyLines && depth > 0 && !isSuffixComment) {
    331             Node curr = node.getPreviousSibling();
    332             if (curr == null) {
    333                 mOut.append(mLineSeparator);
    334             } else if (curr.getNodeType() == Node.TEXT_NODE) {
    335                 String text = curr.getNodeValue();
    336                 // Count how many newlines we find in the trailing whitespace of the
    337                 // text node
    338                 int newLines = 0;
    339                 for (int i = text.length() - 1; i >= 0; i--) {
    340                     char c = text.charAt(i);
    341                     if (Character.isWhitespace(c)) {
    342                         if (c == '\n') {
    343                             newLines++;
    344                             if (newLines == 2) {
    345                                 break;
    346                             }
    347                         }
    348                     } else {
    349                         break;
    350                     }
    351                 }
    352                 if (newLines >= 2) {
    353                     mOut.append(mLineSeparator);
    354                 } else if (text.trim().length() == 0 && curr.getPreviousSibling() == null) {
    355                     // Comment before first child in node
    356                     mOut.append(mLineSeparator);
    357                 }
    358             }
    359         }
    360 
    361 
    362         // TODO: Reformat the comment text?
    363         if (!multiLine) {
    364             if (!isSuffixComment) {
    365                 indent(depth);
    366             }
    367             mOut.append(COMMENT_BEGIN).append(' ');
    368             mOut.append(trimmed);
    369             mOut.append(' ').append(COMMENT_END);
    370             mOut.append(mLineSeparator);
    371         } else {
    372             // Strip off blank lines at the beginning and end of the comment text.
    373             // Find last newline at the beginning of the text:
    374             int index = 0;
    375             int end = comment.length();
    376             int recentNewline = -1;
    377             while (index < end) {
    378                 char c = comment.charAt(index);
    379                 if (c == '\n') {
    380                     recentNewline = index;
    381                 }
    382                 if (!Character.isWhitespace(c)) {
    383                     break;
    384                 }
    385                 index++;
    386             }
    387 
    388             int start = recentNewline + 1;
    389 
    390             // Find last newline at the end of the text
    391             index = end - 1;
    392             recentNewline = -1;
    393             while (index > start) {
    394                 char c = comment.charAt(index);
    395                 if (c == '\n') {
    396                     recentNewline = index;
    397                 }
    398                 if (!Character.isWhitespace(c)) {
    399                     break;
    400                 }
    401                 index--;
    402             }
    403 
    404             end = recentNewline == -1 ? index + 1 : recentNewline;
    405             if (start >= end) {
    406                 // It's a blank comment like <!-- \n\n--> - just clean it up
    407                 if (!isSuffixComment) {
    408                     indent(depth);
    409                 }
    410                 mOut.append(COMMENT_BEGIN).append(' ').append(COMMENT_END);
    411                 mOut.append(mLineSeparator);
    412                 return;
    413             }
    414 
    415             trimmed = comment.substring(start, end);
    416 
    417             // When stripping out prefix and suffix blank lines we might have ended up
    418             // with a single line comment again so check and format single line comments
    419             // without newlines inside the <!-- --> delimiters
    420             multiLine = trimmed.indexOf('\n') != -1;
    421             if (multiLine) {
    422                 indent(depth);
    423                 mOut.append(COMMENT_BEGIN);
    424                 mOut.append(mLineSeparator);
    425 
    426                 // See if we need to add extra spacing to keep alignment. Consider a comment
    427                 // like this:
    428                 // <!-- Deprecated strings - Move the identifiers to this section,
    429                 //      and remove the actual text. -->
    430                 // This String will be
    431                 // " Deprecated strings - Move the identifiers to this section,\n" +
    432                 // "     and remove the actual text. -->"
    433                 // where the left side column no longer lines up.
    434                 // To fix this, we need to insert some extra whitespace into the first line
    435                 // of the string; in particular, the exact number of characters that the
    436                 // first line of the comment was indented with!
    437 
    438                 // However, if the comment started like this:
    439                 // <!--
    440                 // /** Copyright
    441                 // -->
    442                 // then obviously the align-indent is 0, so we only want to compute an
    443                 // align indent when we don't find a newline before the content
    444                 boolean startsWithNewline = false;
    445                 for (int i = 0; i < start; i++) {
    446                     if (comment.charAt(i) == '\n') {
    447                         startsWithNewline = true;
    448                         break;
    449                     }
    450                 }
    451                 if (!startsWithNewline) {
    452                     Node previous = node.getPreviousSibling();
    453                     if (previous != null && previous.getNodeType() == Node.TEXT_NODE) {
    454                         String prevText = previous.getNodeValue();
    455                         int indentation = COMMENT_BEGIN.length();
    456                         for (int i = prevText.length() - 1; i >= 0; i--) {
    457                             char c = prevText.charAt(i);
    458                             if (c == '\n') {
    459                                 break;
    460                             } else {
    461                                 indentation += (c == '\t') ? mPrefs.getTabWidth() : 1;
    462                             }
    463                         }
    464 
    465                         // See if the next line after the newline has indentation; if it doesn't,
    466                         // leave things alone. This fixes a case like this:
    467                         //     <!-- This is the
    468                         //     comment block -->
    469                         // such that it doesn't turn it into
    470                         //     <!--
    471                         //          This is the
    472                         //     comment block
    473                         //     -->
    474                         // In this case we instead want
    475                         //     <!--
    476                         //     This is the
    477                         //     comment block
    478                         //     -->
    479                         int minIndent = Integer.MAX_VALUE;
    480                         String[] lines = trimmed.split("\n"); //$NON-NLS-1$
    481                         // Skip line 0 since we know that it doesn't start with a newline
    482                         for (int i = 1; i < lines.length; i++) {
    483                             int indent = 0;
    484                             String line = lines[i];
    485                             for (int j = 0; j < line.length(); j++) {
    486                                 char c = line.charAt(j);
    487                                 if (!Character.isWhitespace(c)) {
    488                                     // Only set minIndent if there's text content on the line;
    489                                     // blank lines can exist in the comment without affecting
    490                                     // the overall minimum indentation boundary.
    491                                     if (indent < minIndent) {
    492                                         minIndent = indent;
    493                                     }
    494                                     break;
    495                                 } else {
    496                                     indent += (c == '\t') ? mPrefs.getTabWidth() : 1;
    497                                 }
    498                             }
    499                         }
    500 
    501                         if (minIndent < indentation) {
    502                             indentation = minIndent;
    503 
    504                             // Subtract any indentation that is already present on the line
    505                             String line = lines[0];
    506                             for (int j = 0; j < line.length(); j++) {
    507                                 char c = line.charAt(j);
    508                                 if (!Character.isWhitespace(c)) {
    509                                     break;
    510                                 } else {
    511                                     indentation -= (c == '\t') ? mPrefs.getTabWidth() : 1;
    512                                 }
    513                             }
    514                         }
    515 
    516                         for (int i = 0; i < indentation; i++) {
    517                             mOut.append(' ');
    518                         }
    519 
    520                         if (indentation < 0) {
    521                             boolean prefixIsSpace = true;
    522                             for (int i = 0; i < -indentation && i < trimmed.length(); i++) {
    523                                 if (!Character.isWhitespace(trimmed.charAt(i))) {
    524                                     prefixIsSpace = false;
    525                                     break;
    526                                 }
    527                             }
    528                             if (prefixIsSpace) {
    529                                 trimmed = trimmed.substring(-indentation);
    530                             }
    531                         }
    532                     }
    533                 }
    534 
    535                 mOut.append(trimmed);
    536                 mOut.append(mLineSeparator);
    537                 indent(depth);
    538                 mOut.append(COMMENT_END);
    539                 mOut.append(mLineSeparator);
    540             } else {
    541                 mOut.append(COMMENT_BEGIN).append(' ');
    542                 mOut.append(trimmed);
    543                 mOut.append(' ').append(COMMENT_END);
    544                 mOut.append(mLineSeparator);
    545             }
    546         }
    547 
    548         // Preserve whitespace after comment: See if the original document had two or
    549         // more newlines after the comment, and if so have a blank line between this
    550         // comment and the next
    551         Node next = node.getNextSibling();
    552         if (!mPrefs.removeEmptyLines && next != null && next.getNodeType() == Node.TEXT_NODE) {
    553             String text = next.getNodeValue();
    554             int newLinesBeforeText = 0;
    555             for (int i = 0, n = text.length(); i < n; i++) {
    556                 char c = text.charAt(i);
    557                 if (c == '\n') {
    558                     newLinesBeforeText++;
    559                     if (newLinesBeforeText == 2) {
    560                         // Yes
    561                         mOut.append(mLineSeparator);
    562                         break;
    563                     }
    564                 } else if (!Character.isWhitespace(c)) {
    565                     break;
    566                 }
    567             }
    568         }
    569     }
    570 
    571     private boolean endsWithLineSeparator() {
    572         int separatorLength = mLineSeparator.length();
    573         if (mOut.length() >= separatorLength) {
    574             for (int i = 0, j = mOut.length() - separatorLength; i < separatorLength; i++) {
    575                if (mOut.charAt(j) != mLineSeparator.charAt(i)) {
    576                    return false;
    577                }
    578             }
    579         }
    580 
    581         return true;
    582     }
    583 
    584     private void removeLastLineSeparator() {
    585         mOut.setLength(mOut.length() - mLineSeparator.length());
    586     }
    587 
    588     private void printOpenElementTag(int depth, Node node) {
    589         Element element = (Element) node;
    590         if (newlineBeforeElementOpen(element, depth)) {
    591             mOut.append(mLineSeparator);
    592         }
    593         if (indentBeforeElementOpen(element, depth)) {
    594             indent(depth);
    595         }
    596         mOut.append('<').append(element.getTagName());
    597 
    598         NamedNodeMap attributes = element.getAttributes();
    599         int attributeCount = attributes.getLength();
    600         if (attributeCount > 0) {
    601             // Sort the attributes
    602             List<Attr> attributeList = new ArrayList<Attr>();
    603             for (int i = 0, n = attributeCount; i < n; i++) {
    604                 attributeList.add((Attr) attributes.item(i));
    605             }
    606             Comparator<Attr> comparator = mPrefs.sortAttributes.getAttributeComparator();
    607             Collections.sort(attributeList, comparator);
    608 
    609             // Put the single attribute on the same line as the element tag?
    610             boolean singleLine = mPrefs.oneAttributeOnFirstLine && attributeCount == 1
    611                     // In resource files we always put all the attributes (which is
    612                     // usually just zero, one or two) on the same line
    613                     || mStyle == XmlFormatStyle.RESOURCE;
    614 
    615             // We also place the namespace declaration on the same line as the root element,
    616             // but this doesn't also imply singleLine handling; subsequent attributes end up
    617             // on their own lines
    618             boolean indentNextAttribute;
    619             if (singleLine || (depth == 0 && XMLNS.equals(attributeList.get(0).getPrefix()))) {
    620                 mOut.append(' ');
    621                 indentNextAttribute = false;
    622             } else {
    623                 mOut.append(mLineSeparator);
    624                 indentNextAttribute = true;
    625             }
    626 
    627             Attr last = attributeList.get(attributeCount - 1);
    628             for (Attr attribute : attributeList) {
    629                 if (indentNextAttribute) {
    630                     indent(depth + 1);
    631                 }
    632                 mOut.append(attribute.getName());
    633                 mOut.append('=').append('"');
    634                 DomUtilities.appendXmlAttributeValue(mOut, attribute.getValue());
    635                 mOut.append('"');
    636 
    637                 // Don't add a newline at the last attribute line; the > should
    638                 // immediately follow the last attribute
    639                 if (attribute != last) {
    640                     mOut.append(singleLine ? " " : mLineSeparator); //$NON-NLS-1$
    641                     indentNextAttribute = !singleLine;
    642                 }
    643             }
    644         }
    645 
    646         boolean isClosed = isEmptyTag(element);
    647 
    648         // Add a space before the > or /> ? In resource files, only do this when closing the
    649         // element
    650         if (mPrefs.spaceBeforeClose && (mStyle != XmlFormatStyle.RESOURCE || isClosed)
    651                 // in <selector> files etc still treat the <item> entries as in resource files
    652                 && !ITEM_TAG.equals(element.getTagName())) {
    653             mOut.append(' ');
    654         }
    655 
    656         if (isClosed) {
    657             mOut.append('/');
    658         }
    659 
    660         mOut.append('>');
    661 
    662         if (newlineAfterElementOpen(element, depth, isClosed)) {
    663             mOut.append(mLineSeparator);
    664         }
    665     }
    666 
    667     private void printCloseElementTag(int depth, Node node) {
    668         Element element = (Element) node;
    669         if (isEmptyTag(element)) {
    670             // Empty tag: Already handled as part of opening tag
    671             return;
    672         }
    673 
    674         // Put the closing declaration on its own line - unless it's a compact
    675         // resource file format
    676         // If the element had element children, separate the end tag from them
    677         if (newlineBeforeElementClose(element, depth)) {
    678             mOut.append(mLineSeparator);
    679         }
    680         if (indentBeforeElementClose(element, depth)) {
    681             indent(depth);
    682         }
    683         mOut.append('<').append('/');
    684         mOut.append(node.getNodeName());
    685         mOut.append('>');
    686 
    687         if (newlineAfterElementClose(element, depth)) {
    688             mOut.append(mLineSeparator);
    689         }
    690     }
    691 
    692     private boolean newlineBeforeElementOpen(Element element, int depth) {
    693         if (hasBlankLineAbove()) {
    694             return false;
    695         }
    696 
    697         if (mPrefs.removeEmptyLines || depth <= 0) {
    698             return false;
    699         }
    700 
    701         // See if this element should be separated from the previous element.
    702         // This is the case if we are not compressing whitespace (checked above),
    703         // or if we are not immediately following a comment (in which case the
    704         // newline would have been added above it), or if we are not in a formatting
    705         // style where
    706         if (mStyle == XmlFormatStyle.LAYOUT) {
    707             // In layouts we always separate elements
    708             return true;
    709         }
    710 
    711         if (mStyle == XmlFormatStyle.MANIFEST || mStyle == XmlFormatStyle.RESOURCE) {
    712             Node curr = element.getPreviousSibling();
    713 
    714             // <style> elements are traditionally separated unless it follows a comment
    715             if (STYLE_ELEMENT.equals(element.getTagName())) {
    716                 if (curr == null
    717                         || curr.getNodeType() == Node.ELEMENT_NODE
    718                         || (curr.getNodeType() == Node.TEXT_NODE
    719                                 && curr.getNodeValue().trim().length() == 0
    720                                 && (curr.getPreviousSibling() == null
    721                                 || curr.getPreviousSibling().getNodeType()
    722                                         == Node.ELEMENT_NODE))) {
    723                     return true;
    724                 }
    725             }
    726 
    727             // In all other styles, we separate elements if they have a different tag than
    728             // the previous one (but we don't insert a newline inside tags)
    729             while (curr != null) {
    730                 short nodeType = curr.getNodeType();
    731                 if (nodeType == Node.ELEMENT_NODE) {
    732                     Element sibling = (Element) curr;
    733                     if (!element.getTagName().equals(sibling.getTagName())) {
    734                         return true;
    735                     }
    736                     break;
    737                 } else if (nodeType == Node.TEXT_NODE) {
    738                     String text = curr.getNodeValue();
    739                     if (text.trim().length() > 0) {
    740                         break;
    741                     }
    742                     // If there is just whitespace, continue looking for a previous sibling
    743                 } else {
    744                     // Any other previous node type, such as a comment, means we don't
    745                     // continue looking: this element should not be separated
    746                     break;
    747                 }
    748                 curr = curr.getPreviousSibling();
    749             }
    750             if (curr == null && depth <= 1) {
    751                 // Insert new line inside tag if it's the first element inside the root tag
    752                 return true;
    753             }
    754 
    755             return false;
    756         }
    757 
    758         return false;
    759     }
    760 
    761     private boolean indentBeforeElementOpen(Element element, int depth) {
    762         if (element.getParentNode().getNodeType() == Node.ELEMENT_NODE
    763                 && keepElementAsSingleLine(depth - 1, (Element) element.getParentNode())) {
    764             return false;
    765         }
    766 
    767         return true;
    768     }
    769 
    770     private boolean indentBeforeElementClose(Element element, int depth) {
    771         char lastOutChar = mOut.charAt(mOut.length() - 1);
    772         char lastDelimiterChar = mLineSeparator.charAt(mLineSeparator.length() - 1);
    773         return lastOutChar == lastDelimiterChar;
    774     }
    775 
    776     private boolean newlineAfterElementOpen(Element element, int depth, boolean isClosed) {
    777         if (hasBlankLineAbove()) {
    778             return false;
    779         }
    780 
    781         // In resource files we keep the child content directly on the same
    782         // line as the element (unless it has children). in other files, separate them
    783         return isClosed || !keepElementAsSingleLine(depth, element);
    784     }
    785 
    786     private boolean newlineBeforeElementClose(Element element, int depth) {
    787         if (hasBlankLineAbove()) {
    788             return false;
    789         }
    790 
    791         return depth == 0 && !mPrefs.removeEmptyLines;
    792     }
    793 
    794     private boolean hasBlankLineAbove() {
    795         if (mOut.length() < 2 * mLineSeparator.length()) {
    796             return false;
    797         }
    798 
    799         return AdtUtils.endsWith(mOut, mLineSeparator) &&
    800                 AdtUtils.endsWith(mOut, mOut.length() - mLineSeparator.length(), mLineSeparator);
    801     }
    802 
    803     private boolean newlineAfterElementClose(Element element, int depth) {
    804         if (hasBlankLineAbove()) {
    805             return false;
    806         }
    807 
    808         return element.getParentNode().getNodeType() == Node.ELEMENT_NODE
    809                 && !keepElementAsSingleLine(depth - 1, (Element) element.getParentNode());
    810     }
    811 
    812     /**
    813      * TODO: Explain why we need to do per-tag decisions on whether to keep them on the
    814      * same line or not. Show that we can't just do it by depth, or by file type.
    815      * (style versus plurals example)
    816      * @param tag
    817      * @return
    818      */
    819     private boolean isSingleLineTag(Element element) {
    820         String tag = element.getTagName();
    821 
    822         return tag.equals(ITEM_TAG)
    823                 || tag.equals(STRING_ELEMENT)
    824                 || tag.equals(DIMEN_ELEMENT)
    825                 || tag.equals(COLOR_ELEMENT);
    826     }
    827 
    828     private boolean keepElementAsSingleLine(int depth, Element element) {
    829         if (depth == 0) {
    830             return false;
    831         }
    832 
    833         return isSingleLineTag(element)
    834                 || (mStyle == XmlFormatStyle.RESOURCE
    835                     && !DomUtilities.hasElementChildren(element));
    836     }
    837 
    838     private void indent(int depth) {
    839         int i = 0;
    840 
    841         if (mIndentationLevels != null) {
    842             for (int j = Math.min(depth, mIndentationLevels.length - 1); j >= 0; j--) {
    843                 String indent = mIndentationLevels[j];
    844                 if (indent != null) {
    845                     mOut.append(indent);
    846                     i = j;
    847                     break;
    848                 }
    849             }
    850         }
    851 
    852         for (; i < depth; i++) {
    853             mOut.append(mIndentString);
    854         }
    855     }
    856 
    857     private boolean isEmptyTag(Element element) {
    858         boolean isClosed = false;
    859         if (element instanceof ElementImpl) {
    860             ElementImpl elementImpl = (ElementImpl) element;
    861             if (elementImpl.isEmptyTag()) {
    862                 isClosed = true;
    863             }
    864         }
    865         return isClosed;
    866     }
    867 }