/**
   * Start pretty-printing at the given node, which must either be the startNode or contain it as a
   * descendant.
   *
   * @param rootDepth the depth of the given node, used to determine indentation
   * @param root the node to start pretty printing from (which may not itself be included in the
   *     start to end node range but should contain it)
   * @param startNode the node to start formatting at
   * @param endNode the node to end formatting at
   * @param out the {@link StringBuilder} to pretty print into
   * @param openTagOnly if true, only format the open tag of the startNode (and nothing else)
   */
  public void prettyPrint(
      int rootDepth,
      Node root,
      Node startNode,
      Node endNode,
      StringBuilder out,
      boolean openTagOnly) {
    if (startNode == null) {
      startNode = root;
    }
    if (endNode == null) {
      endNode = root;
    }
    assert !openTagOnly || startNode == endNode;

    mStartNode = startNode;
    mOpenTagOnly = openTagOnly;
    mEndNode = endNode;
    mOut = out;
    mInRange = false;
    mIndentString = mPrefs.getOneIndentUnit();

    visitNode(rootDepth, root);

    if (mEndWithNewline && !endsWithLineSeparator()) {
      mOut.append(mLineSeparator);
    }
  }
 /**
  * Pretty prints the given node using default styles
  *
  * @param node the node, usually a document, to be printed
  * @param endWithNewline if true, ensure that the printed output ends with a newline
  * @return the resulting formatted string
  */
 @NonNull
 public static String prettyPrint(@NonNull Node node, boolean endWithNewline) {
   return prettyPrint(
       node,
       XmlFormatPreferences.defaults(),
       XmlFormatStyle.get(node),
       SdkUtils.getLineSeparator(),
       endWithNewline);
 }
  /** Command line driver */
  public static void main(String[] args) {
    if (args.length == 0) {
      printUsage();
    }

    List<File> files = Lists.newArrayList();

    XmlFormatPreferences prefs = XmlFormatPreferences.defaults();
    boolean stdout = false;

    for (String arg : args) {
      if (arg.startsWith("--")) {
        if ("--stdout".equals(arg)) {
          stdout = true;
        } else if ("--removeEmptyLines".equals(arg)) {
          prefs.removeEmptyLines = true;
        } else if ("--noAttributeOnFirstLine".equals(arg)) {
          prefs.oneAttributeOnFirstLine = false;
        } else if ("--noSpaceBeforeClose".equals(arg)) {
          prefs.spaceBeforeClose = false;
        } else {
          System.err.println("Unknown flag " + arg);
          printUsage();
        }
      } else {
        File file = new File(arg).getAbsoluteFile();
        if (!file.exists()) {
          System.err.println("Can't find file " + file);
          System.exit(1);
        } else {
          files.add(file);
        }
      }
    }

    for (File file : files) {
      formatFile(prefs, file, stdout);
    }

    System.exit(0);
  }
  private void printOpenElementTag(int depth, Node node) {
    Element element = (Element) node;
    if (newlineBeforeElementOpen(element, depth)) {
      mOut.append(mLineSeparator);
    }
    if (indentBeforeElementOpen(element, depth)) {
      indent(depth);
    }
    mOut.append('<').append(element.getTagName());

    NamedNodeMap attributes = element.getAttributes();
    int attributeCount = attributes.getLength();
    if (attributeCount > 0) {
      // Sort the attributes
      List<Attr> attributeList = new ArrayList<Attr>();
      for (int i = 0; i < attributeCount; i++) {
        attributeList.add((Attr) attributes.item(i));
      }
      Comparator<Attr> comparator = mPrefs.getAttributeComparator();
      if (comparator != null) {
        Collections.sort(attributeList, comparator);
      }

      // Put the single attribute on the same line as the element tag?
      boolean singleLine =
          mPrefs.oneAttributeOnFirstLine && attributeCount == 1
              // In resource files we always put all the attributes (which is
              // usually just zero, one or two) on the same line
              || mStyle == XmlFormatStyle.RESOURCE;

      // We also place the namespace declaration on the same line as the root element,
      // but this doesn't also imply singleLine handling; subsequent attributes end up
      // on their own lines
      boolean indentNextAttribute;
      if (singleLine || (depth == 0 && XMLNS.equals(attributeList.get(0).getPrefix()))) {
        mOut.append(' ');
        indentNextAttribute = false;
      } else {
        mOut.append(mLineSeparator);
        indentNextAttribute = true;
      }

      Attr last = attributeList.get(attributeCount - 1);
      for (Attr attribute : attributeList) {
        if (indentNextAttribute) {
          indent(depth + 1);
        }
        mOut.append(attribute.getName());
        mOut.append('=').append('"');
        XmlUtils.appendXmlAttributeValue(mOut, attribute.getValue());
        mOut.append('"');

        // Don't add a newline at the last attribute line; the > should
        // immediately follow the last attribute
        if (attribute != last) {
          mOut.append(singleLine ? " " : mLineSeparator); // $NON-NLS-1$
          indentNextAttribute = !singleLine;
        }
      }
    }

    boolean isClosed = isEmptyTag(element);

    // Add a space before the > or /> ? In resource files, only do this when closing the
    // element
    if (mPrefs.spaceBeforeClose
        && (mStyle != XmlFormatStyle.RESOURCE || isClosed)
        // in <selector> files etc still treat the <item> entries as in resource files
        && !TAG_ITEM.equals(element.getTagName())
        && (isClosed || element.getAttributes().getLength() > 0)) {
      mOut.append(' ');
    }

    if (isClosed) {
      mOut.append('/');
    }

    mOut.append('>');

    if (newlineAfterElementOpen(element, depth, isClosed)) {
      mOut.append(mLineSeparator);
    }
  }
  private void printComment(int depth, Node node) {
    String comment = node.getNodeValue();
    boolean multiLine = comment.indexOf('\n') != -1;
    String trimmed = comment.trim();

    // See if this is an "end-of-the-line" comment, e.g. it is not a multi-line
    // comment and it appears on the same line as an opening or closing element tag;
    // if so, continue to place it as a suffix comment
    boolean isSuffixComment = false;
    if (!multiLine) {
      Node previous = node.getPreviousSibling();
      isSuffixComment = true;
      if (previous == null && node.getParentNode().getNodeType() == Node.DOCUMENT_NODE) {
        isSuffixComment = false;
      }
      while (previous != null) {
        short type = previous.getNodeType();
        if (type == Node.COMMENT_NODE) {
          isSuffixComment = false;
          break;
        } else if (type == Node.TEXT_NODE) {
          if (previous.getNodeValue().indexOf('\n') != -1) {
            isSuffixComment = false;
            break;
          }
        } else {
          break;
        }
        previous = previous.getPreviousSibling();
      }
      if (isSuffixComment) {
        // Remove newline added by element open tag or element close tag
        if (endsWithLineSeparator()) {
          removeLastLineSeparator();
        }
        mOut.append(' ');
      }
    }

    // Put the comment on a line on its own? Only if it was separated by a blank line
    // in the previous version of the document. In other words, if the document
    // adds blank lines between comments this formatter will preserve that fact, and vice
    // versa for a tightly formatted document it will preserve that convention as well.
    if (!mPrefs.removeEmptyLines && !isSuffixComment) {
      Node curr = node.getPreviousSibling();
      if (curr == null) {
        if (mOut.length() > 0 && !endsWithLineSeparator()) {
          mOut.append(mLineSeparator);
        }
      } else if (curr.getNodeType() == Node.TEXT_NODE) {
        String text = curr.getNodeValue();
        // Count how many newlines we find in the trailing whitespace of the
        // text node
        int newLines = 0;
        for (int i = text.length() - 1; i >= 0; i--) {
          char c = text.charAt(i);
          if (Character.isWhitespace(c)) {
            if (c == '\n') {
              newLines++;
              if (newLines == 2) {
                break;
              }
            }
          } else {
            break;
          }
        }
        if (newLines >= 2) {
          mOut.append(mLineSeparator);
        } else if (text.trim().isEmpty() && curr.getPreviousSibling() == null) {
          // Comment before first child in node
          mOut.append(mLineSeparator);
        }
      }
    }

    // TODO: Reformat the comment text?
    if (!multiLine) {
      if (!isSuffixComment) {
        indent(depth);
      }
      mOut.append(XML_COMMENT_BEGIN).append(' ');
      mOut.append(trimmed);
      mOut.append(' ').append(XML_COMMENT_END);
      mOut.append(mLineSeparator);
    } else {
      // Strip off blank lines at the beginning and end of the comment text.
      // Find last newline at the beginning of the text:
      int index = 0;
      int end = comment.length();
      int recentNewline = -1;
      while (index < end) {
        char c = comment.charAt(index);
        if (c == '\n') {
          recentNewline = index;
        }
        if (!Character.isWhitespace(c)) {
          break;
        }
        index++;
      }

      int start = recentNewline + 1;

      // Find last newline at the end of the text
      index = end - 1;
      recentNewline = -1;
      while (index > start) {
        char c = comment.charAt(index);
        if (c == '\n') {
          recentNewline = index;
        }
        if (!Character.isWhitespace(c)) {
          break;
        }
        index--;
      }

      end = recentNewline == -1 ? index + 1 : recentNewline;
      if (start >= end) {
        // It's a blank comment like <!-- \n\n--> - just clean it up
        if (!isSuffixComment) {
          indent(depth);
        }
        mOut.append(XML_COMMENT_BEGIN).append(' ').append(XML_COMMENT_END);
        mOut.append(mLineSeparator);
        return;
      }

      trimmed = comment.substring(start, end);

      // When stripping out prefix and suffix blank lines we might have ended up
      // with a single line comment again so check and format single line comments
      // without newlines inside the <!-- --> delimiters
      multiLine = trimmed.indexOf('\n') != -1;
      if (multiLine) {
        indent(depth);
        mOut.append(XML_COMMENT_BEGIN);
        mOut.append(mLineSeparator);

        // See if we need to add extra spacing to keep alignment. Consider a comment
        // like this:
        // <!-- Deprecated strings - Move the identifiers to this section,
        //      and remove the actual text. -->
        // This String will be
        // " Deprecated strings - Move the identifiers to this section,\n" +
        // "     and remove the actual text. -->"
        // where the left side column no longer lines up.
        // To fix this, we need to insert some extra whitespace into the first line
        // of the string; in particular, the exact number of characters that the
        // first line of the comment was indented with!

        // However, if the comment started like this:
        // <!--
        // /** Copyright
        // -->
        // then obviously the align-indent is 0, so we only want to compute an
        // align indent when we don't find a newline before the content
        boolean startsWithNewline = false;
        for (int i = 0; i < start; i++) {
          if (comment.charAt(i) == '\n') {
            startsWithNewline = true;
            break;
          }
        }
        if (!startsWithNewline) {
          Node previous = node.getPreviousSibling();
          if (previous != null && previous.getNodeType() == Node.TEXT_NODE) {
            String prevText = previous.getNodeValue();
            int indentation = XML_COMMENT_BEGIN.length();
            for (int i = prevText.length() - 1; i >= 0; i--) {
              char c = prevText.charAt(i);
              if (c == '\n') {
                break;
              } else {
                indentation += (c == '\t') ? mPrefs.getTabWidth() : 1;
              }
            }

            // See if the next line after the newline has indentation; if it doesn't,
            // leave things alone. This fixes a case like this:
            //     <!-- This is the
            //     comment block -->
            // such that it doesn't turn it into
            //     <!--
            //          This is the
            //     comment block
            //     -->
            // In this case we instead want
            //     <!--
            //     This is the
            //     comment block
            //     -->
            int minIndent = Integer.MAX_VALUE;
            String[] lines = trimmed.split("\n"); // $NON-NLS-1$
            // Skip line 0 since we know that it doesn't start with a newline
            for (int i = 1; i < lines.length; i++) {
              int indent = 0;
              String line = lines[i];
              for (int j = 0; j < line.length(); j++) {
                char c = line.charAt(j);
                if (!Character.isWhitespace(c)) {
                  // Only set minIndent if there's text content on the line;
                  // blank lines can exist in the comment without affecting
                  // the overall minimum indentation boundary.
                  if (indent < minIndent) {
                    minIndent = indent;
                  }
                  break;
                } else {
                  indent += (c == '\t') ? mPrefs.getTabWidth() : 1;
                }
              }
            }

            if (minIndent < indentation) {
              indentation = minIndent;

              // Subtract any indentation that is already present on the line
              String line = lines[0];
              for (int j = 0; j < line.length(); j++) {
                char c = line.charAt(j);
                if (!Character.isWhitespace(c)) {
                  break;
                } else {
                  indentation -= (c == '\t') ? mPrefs.getTabWidth() : 1;
                }
              }
            }

            for (int i = 0; i < indentation; i++) {
              mOut.append(' ');
            }

            if (indentation < 0) {
              boolean prefixIsSpace = true;
              for (int i = 0; i < -indentation && i < trimmed.length(); i++) {
                if (!Character.isWhitespace(trimmed.charAt(i))) {
                  prefixIsSpace = false;
                  break;
                }
              }
              if (prefixIsSpace) {
                trimmed = trimmed.substring(-indentation);
              }
            }
          }
        }

        mOut.append(trimmed);
        mOut.append(mLineSeparator);
        indent(depth);
        mOut.append(XML_COMMENT_END);
        mOut.append(mLineSeparator);
      } else {
        mOut.append(XML_COMMENT_BEGIN).append(' ');
        mOut.append(trimmed);
        mOut.append(' ').append(XML_COMMENT_END);
        mOut.append(mLineSeparator);
      }
    }

    // Preserve whitespace after comment: See if the original document had two or
    // more newlines after the comment, and if so have a blank line between this
    // comment and the next
    Node next = node.getNextSibling();
    if (!mPrefs.removeEmptyLines && (next != null) && (next.getNodeType() == Node.TEXT_NODE)) {
      String text = next.getNodeValue();
      int newLinesBeforeText = 0;
      for (int i = 0, n = text.length(); i < n; i++) {
        char c = text.charAt(i);
        if (c == '\n') {
          newLinesBeforeText++;
          if (newLinesBeforeText == 2) {
            // Yes
            mOut.append(mLineSeparator);
            break;
          }
        } else if (!Character.isWhitespace(c)) {
          break;
        }
      }
    }
  }