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 ">" 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 > 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