diff --git a/README.md b/README.md index c14e5a5..5a60f67 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,71 @@ # android-xml-formatter -Formats xml files according to Android Studio's default formatting rules. By default, also re-orders the attributes to specify the `android:id`, `android:layout_width` and `android:layout_height` first. This can be turned off with a command line option. +[![GitHub check runs](https://img.shields.io/github/check-runs/ByteHamster/android-xml-formatter/main)](https://github.com/ByteHamster/android-xml-formatter/actions/workflows/checks.yml?query=branch%3Amain) +[![License: GPL v3](https://img.shields.io/github/license/ByteHamster/android-xml-formatter)](https://www.gnu.org/licenses/gpl-3.0) +[![GitHub Release](https://img.shields.io/github/v/release/ByteHamster/android-xml-formatter)](https://github.com/ByteHamster/android-xml-formatter/releases) -To view available command line options, execute `java -jar android-xml-formatter.jar --help`. +Formats XML files according to Android Studio's default formatting rules. By default, also re-orders the attributes to specify the `android:id`, `android:layout_width` and `android:layout_height` first. This can be turned off with a command line option. -Can be used as a style check on a CI server by executing and then printing the diff. +## Requirements + +- Java 8 or higher +- Maven 3.6 or higher + +## Building + +- `mvn clean package` to create an executable JAR file with all dependencies + at `target/android-xml-formatter-1.0-SNAPSHOT-jar-with-dependencies.jar`. +- `mvn verify` runs all tests and code format checks. + +## Usage + +To view available command line options: + +```bash +java -jar target/android-xml-formatter-1.0-SNAPSHOT-jar-with-dependencies.jar --help +``` + +| Option | Description | +|-----------------------------|----------------------------------------------------------------------------| +| `--indention ` | Set indentation spaces (default: 4) | +| `--attribute-indention ` | Set attribute indentation spaces (default: 4) | +| `--attribute-order ` | Comma-separated attribute order (default: `id,layout_width,layout_height`) | +| `--attribute-sort` | Sort attributes alphabetically | +| `--namespace-order ` | Comma-separated namespace order (default: `android`) | +| `--namespace-sort` | Sort namespaces alphabetically | + +### Example + +Format an XML file: + +```bash +java -jar target/android-xml-formatter-1.0-SNAPSHOT-jar-with-dependencies.jar path/to/layout.xml +``` + +Format multiple files: + +```bash +java -jar target/android-xml-formatter-1.0-SNAPSHOT-jar-with-dependencies.jar file1.xml file2.xml file3.xml +``` + +## Contributing + +This project uses [Spotless Maven Plugin](https://github.com/diffplug/spotless/tree/main/plugin-maven) +with Eclipse JDT formatter for Java code formatting, configured to match the +[AOSP (Android Open Source Project) code style](https://source.android.com/docs/setup/contribute/code-style). + +To verify that all Java code follows the formatting rules, run `mvn spotless:check`. +To automatically format all Java code, run `mvn spotless:apply`. + +## CI Integration + +This project can be used as a style check on a CI server by executing the formatter and then printing the diff: + +```bash +java -jar android-xml-formatter.jar *.xml +git diff --exit-code +``` + +## License + +See [LICENSE](LICENSE) file for details. diff --git a/pom.xml b/pom.xml index 1f124ef..2b712b9 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,32 @@ + + com.diffplug.spotless + spotless-maven-plugin + 2.43.0 + + + + + ${project.basedir}/settings/eclipse-aosp-style.xml + + + + android,androidx,com.android,dalvik,libcore,com,org,java,javax, + + + + + + + + check + + verify + + + maven-assembly-plugin 3.1.1 @@ -59,4 +85,4 @@ - \ No newline at end of file + diff --git a/settings/eclipse-aosp-style.xml b/settings/eclipse-aosp-style.xml new file mode 100644 index 0000000..29426f7 --- /dev/null +++ b/settings/eclipse-aosp-style.xml @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/bytehamster/androidxmlformatter/AndroidXmlOutputter.java b/src/main/java/com/bytehamster/androidxmlformatter/AndroidXmlOutputter.java index 8ffba59..1d2ad14 100644 --- a/src/main/java/com/bytehamster/androidxmlformatter/AndroidXmlOutputter.java +++ b/src/main/java/com/bytehamster/androidxmlformatter/AndroidXmlOutputter.java @@ -1,11 +1,5 @@ package com.bytehamster.androidxmlformatter; -import java.io.IOException; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - import org.apache.commons.lang3.StringUtils; import org.jdom.Attribute; import org.jdom.CDATA; @@ -19,6 +13,12 @@ import org.jdom.output.Format; import org.jdom.output.XMLOutputter; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + public class AndroidXmlOutputter extends XMLOutputter { final String[] namespaceOrder; final String[] attributeNameOrder; @@ -27,8 +27,8 @@ public class AndroidXmlOutputter extends XMLOutputter { final boolean alphabeticalNamespaces; public AndroidXmlOutputter(int indention, int attributeIndention, - String[] namespaceOrder, String[] attributeNameOrder, - boolean alphabeticalAttributes, boolean alphabeticalNamespaces) { + String[] namespaceOrder, String[] attributeNameOrder, + boolean alphabeticalAttributes, boolean alphabeticalNamespaces) { this.attributeIndention = attributeIndention; this.namespaceOrder = namespaceOrder; this.attributeNameOrder = attributeNameOrder; @@ -51,7 +51,8 @@ static private int elementDepth(Element element) { return result; } - private void printNamespace(Writer out, Namespace ns, XMLOutputter.NamespaceStack namespaces) throws IOException { + private void printNamespace(Writer out, Namespace ns, XMLOutputter.NamespaceStack namespaces) + throws IOException { String prefix = ns.getPrefix(); String uri = ns.getURI(); if (!uri.equals(namespaces.getURI(prefix))) { @@ -67,7 +68,9 @@ private void printNamespace(Writer out, Namespace ns, XMLOutputter.NamespaceStac namespaces.push(ns); } } - private void printElementNamespace(Writer out, Element element, XMLOutputter.NamespaceStack namespaces) throws IOException { + + private void printElementNamespace(Writer out, Element element, + XMLOutputter.NamespaceStack namespaces) throws IOException { Namespace ns = element.getNamespace(); if (ns != Namespace.XML_NAMESPACE) { if (ns != Namespace.NO_NAMESPACE || namespaces.getURI("") != null) { @@ -77,11 +80,12 @@ private void printElementNamespace(Writer out, Element element, XMLOutputter.Nam } } - private void printAdditionalNamespaces(Writer out, Element element, XMLOutputter.NamespaceStack namespaces) throws IOException { + private void printAdditionalNamespaces(Writer out, Element element, + XMLOutputter.NamespaceStack namespaces) throws IOException { List list = element.getAdditionalNamespaces(); if (list != null) { - for(int i = 0; i < list.size(); ++i) { - Namespace additional = (Namespace)list.get(i); + for (int i = 0; i < list.size(); ++i) { + Namespace additional = (Namespace) list.get(i); if (attributeIndention > 0) { newline(out); indent(out, elementDepth(element) - 1); @@ -105,7 +109,7 @@ private int skipTrailingWhite(List content, int start) { if (this.currentFormat.getTextMode() == Format.TextMode.TRIM_FULL_WHITE || this.currentFormat.getTextMode() == Format.TextMode.NORMALIZE || this.currentFormat.getTextMode() == Format.TextMode.TRIM) { - while(index >= 0 && this.isAllWhitespace(content.get(index - 1))) { + while (index >= 0 && this.isAllWhitespace(content.get(index - 1))) { --index; } } @@ -116,7 +120,7 @@ private int skipTrailingWhite(List content, int start) { private boolean isAllWhitespace(Object obj) { String str = null; if (obj instanceof String) { - str = (String)obj; + str = (String) obj; } else { if (!(obj instanceof Text)) { if (obj instanceof EntityRef) { @@ -126,10 +130,10 @@ private boolean isAllWhitespace(Object obj) { return false; } - str = ((Text)obj).getText(); + str = ((Text) obj).getText(); } - for(int i = 0; i < str.length(); ++i) { + for (int i = 0; i < str.length(); ++i) { if (!Verifier.isXMLWhitespace(str.charAt(i))) { return false; } @@ -146,7 +150,7 @@ private int skipLeadingWhite(List content, int start) { int index = start; int size = content.size(); if (true) { - while(index < size) { + while (index < size) { if (!this.isAllWhitespace(content.get(index))) { return index; } @@ -166,7 +170,7 @@ private static int nextNonText(List content, int start) { int index = start; int size; - for(size = content.size(); index < size; ++index) { + for (size = content.size(); index < size; ++index) { Object node = content.get(index); if (!(node instanceof Text) && !(node instanceof EntityRef)) { return index; @@ -176,11 +180,12 @@ private static int nextNonText(List content, int start) { return size; } - private void printContentRange(Writer out, List content, int start, int end, int level, XMLOutputter.NamespaceStack namespaces) throws IOException { + private void printContentRange(Writer out, List content, int start, int end, int level, + XMLOutputter.NamespaceStack namespaces) throws IOException { int index = start; - while(true) { - while(index < end) { + while (true) { + while (index < end) { boolean firstNode = index == start; Object next = content.get(index); if (!(next instanceof Text) && !(next instanceof EntityRef)) { @@ -190,11 +195,11 @@ private void printContentRange(Writer out, List content, int start, int end, int this.indent(out, level); if (next instanceof Comment) { - this.printComment(out, (Comment)next); + this.printComment(out, (Comment) next); } else if (next instanceof Element) { - this.printElement(out, (Element)next, level, namespaces); + this.printElement(out, (Element) next, level, namespaces); } else if (next instanceof ProcessingInstruction) { - this.printProcessingInstruction(out, (ProcessingInstruction)next); + this.printProcessingInstruction(out, (ProcessingInstruction) next); } ++index; @@ -233,30 +238,32 @@ private void printTextRange(Writer out, List content, int start, int end) throws if (start < size) { end = this.skipTrailingWhite(content, end); - for(int i = start; i < end; ++i) { + for (int i = start; i < end; ++i) { Object node = content.get(i); String next; if (node instanceof Text) { - next = ((Text)node).getText(); + next = ((Text) node).getText(); } else { if (!(node instanceof EntityRef)) { - throw new IllegalStateException("Should see only CDATA, Text, or EntityRef"); + throw new IllegalStateException( + "Should see only CDATA, Text, or EntityRef"); } - next = "&" + ((EntityRef)node).getValue() + ";"; + next = "&" + ((EntityRef) node).getValue() + ";"; } if (next != null && !"".equals(next)) { - if (previous != null && (this.currentFormat.getTextMode() == Format.TextMode.NORMALIZE - || this.currentFormat.getTextMode() == Format.TextMode.TRIM) + if (previous != null + && (this.currentFormat.getTextMode() == Format.TextMode.NORMALIZE + || this.currentFormat.getTextMode() == Format.TextMode.TRIM) && (this.endsWithWhite(previous) || this.startsWithWhite(next))) { out.write(" "); } if (node instanceof CDATA) { - this.printCDATA(out, (CDATA)node); + this.printCDATA(out, (CDATA) node); } else if (node instanceof EntityRef) { - this.printEntityRef(out, (EntityRef)node); + this.printEntityRef(out, (EntityRef) node); } else { this.printString(out, next); } @@ -273,11 +280,13 @@ private boolean startsWithWhite(String str) { } private boolean endsWithWhite(String str) { - return str != null && str.length() > 0 && Verifier.isXMLWhitespace(str.charAt(str.length() - 1)); + return str != null && str.length() > 0 + && Verifier.isXMLWhitespace(str.charAt(str.length() - 1)); } @Override - protected void printElement(Writer out, Element element, int level, NamespaceStack namespaces) throws IOException { + protected void printElement(Writer out, Element element, int level, NamespaceStack namespaces) + throws IOException { List attributes = element.getAttributes(); List content = element.getContent(); String space = null; @@ -322,7 +331,7 @@ protected void printElement(Writer out, Element element, int level, NamespaceSta out.write(">"); } - while(namespaces.size() > previouslyDeclaredNamespaces) { + while (namespaces.size() > previouslyDeclaredNamespaces) { namespaces.pop(); } @@ -335,13 +344,14 @@ private void newline(Writer out) throws IOException { } private void indent(Writer out, int level) throws IOException { - for(int i = 0; i < level; ++i) { + for (int i = 0; i < level; ++i) { out.write(getFormat().getIndent()); } } @Override - protected void printAttributes(Writer writer, List attribs, Element parent, NamespaceStack ns) throws IOException { + protected void printAttributes(Writer writer, List attribs, Element parent, NamespaceStack ns) + throws IOException { List attributes = new ArrayList<>(); for (Object attribObj : attribs) { attributes.add((Attribute) attribObj); @@ -413,4 +423,4 @@ private void printQualifiedName(Writer out, Element e) throws IOException { } } -} \ No newline at end of file +} diff --git a/src/main/java/com/bytehamster/androidxmlformatter/Main.java b/src/main/java/com/bytehamster/androidxmlformatter/Main.java index 398ea2b..9d4cbad 100644 --- a/src/main/java/com/bytehamster/androidxmlformatter/Main.java +++ b/src/main/java/com/bytehamster/androidxmlformatter/Main.java @@ -63,13 +63,15 @@ public static void main(String[] args) throws Exception { Integer.parseInt(cmd.getOptionValue("indention", "4")), Integer.parseInt(cmd.getOptionValue("attribute-indention", "4")), cmd.getOptionValue("namespace-order", "android").split(","), - cmd.getOptionValue("attribute-order", "id,layout_width,layout_height").split(","), + cmd.getOptionValue("attribute-order", "id,layout_width,layout_height") + .split(","), cmd.hasOption("attribute-sort"), cmd.hasOption("namespace-sort")); ByteArrayOutputStream stream = new ByteArrayOutputStream(); outputter.output(doc, stream); byte[] content = stream.toByteArray(); - new FileOutputStream(filename).write(content, 0, content.length - 2); // Strip double line break + new FileOutputStream(filename).write(content, 0, content.length - 2); // Strip double + // line break } } }