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