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