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 }