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