From d9331eca808e568c022cdb17c7758b81cded47de Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Sat, 13 May 2023 22:38:30 +0530 Subject: [PATCH] fix(templates): use XmlBuilder from 'xml-dom' module for building XML files --- app/proguard-rules.pro | 2 +- .../itsaky/androidide/preferences/xmlPrefs.kt | 2 +- .../lsp/xml/models/XMLFormattingOptions.kt | 57 +- .../format/DOMAttributeFormatter.java | 4 +- .../providers/format/DOMCDATAFormatter.java | 2 +- .../providers/format/DOMCommentFormatter.java | 2 +- .../providers/format/DOMElementFormatter.java | 12 +- .../providers/format/DOMTextFormatter.java | 4 +- .../format/XMLFormatterDocument.java | 18 +- .../format/XMLFormatterDocumentNew.java | 4 +- .../androidide/lsp/xml/utils/XMLBuilder.java | 526 +--------------- subprojects/xml-dom/build.gradle.kts | 1 + .../dom/builder/BaseXmlFormattingOptions.kt | 51 ++ .../lemminx/dom/builder/EmptyElements.kt} | 19 +- .../lemminx/dom/builder/IndentedXmlBuilder.kt | 103 ++++ .../lemminx/dom/builder/XmlBuilder.java | 559 ++++++++++++++++++ .../androidide/xml/utils/XmlBuilder.java | 211 ------- templates-api/build.gradle.kts | 1 + .../base/AndroidModuleTemplateBuilder.kt | 16 + .../itsaky/androidide/templates/base/base.kt | 20 +- .../base/util/AndroidManifestBuilder.kt | 47 +- .../base/util/AndroidModuleResManager.kt | 24 +- .../templates/base/util/xmlBuilder.kt | 24 +- .../itsaky/androidide/templates/template.kt | 2 +- .../androidide/templates/impl/base/themes.kt | 2 +- 25 files changed, 878 insertions(+), 835 deletions(-) create mode 100644 subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/BaseXmlFormattingOptions.kt rename subprojects/{xml-utils/src/test/java/com/itsaky/androidide/xml/utils/XmlBuilderTest.kt => xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/EmptyElements.kt} (74%) create mode 100644 subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/IndentedXmlBuilder.kt create mode 100644 subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/XmlBuilder.java delete mode 100644 subprojects/xml-utils/src/main/java/com/itsaky/androidide/xml/utils/XmlBuilder.java diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 3e4907728..eb908568f 100755 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -78,7 +78,7 @@ } # Used in preferences --keep enum com.itsaky.androidide.lsp.xml.models.EmptyElements { *; } +-keep enum org.eclipse.lemminx.dom.builder.EmptyElements { *; } -keep enum com.itsaky.androidide.xml.permissions.Permission { *; } # Lots of native methods in tree-sitter diff --git a/app/src/main/java/com/itsaky/androidide/preferences/xmlPrefs.kt b/app/src/main/java/com/itsaky/androidide/preferences/xmlPrefs.kt index dfcc53a44..5714a109e 100644 --- a/app/src/main/java/com/itsaky/androidide/preferences/xmlPrefs.kt +++ b/app/src/main/java/com/itsaky/androidide/preferences/xmlPrefs.kt @@ -19,7 +19,6 @@ package com.itsaky.androidide.preferences import android.content.Context import com.itsaky.androidide.resources.R.string -import com.itsaky.androidide.lsp.xml.models.EmptyElements import com.itsaky.androidide.preferences.internal.CLOSING_BRACKET_NEW_LINE import com.itsaky.androidide.preferences.internal.EMPTY_ELEMENTS_BEHAVIOR import com.itsaky.androidide.preferences.internal.INSERT_FINAL_NEW_LINE @@ -50,6 +49,7 @@ import com.itsaky.androidide.preferences.internal.splitAttributes import com.itsaky.androidide.preferences.internal.splitAttributesIndentSize import com.itsaky.androidide.preferences.internal.trimFinalNewLine import com.itsaky.androidide.preferences.internal.trimTrailingWhitespace +import org.eclipse.lemminx.dom.builder.EmptyElements import kotlinx.parcelize.Parcelize @Parcelize diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/models/XMLFormattingOptions.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/models/XMLFormattingOptions.kt index 8b5b28a7f..6c6bef194 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/models/XMLFormattingOptions.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/models/XMLFormattingOptions.kt @@ -19,6 +19,8 @@ package com.itsaky.androidide.lsp.xml.models import com.itsaky.androidide.lsp.xml.providers.format.FormatElementCategory import com.itsaky.androidide.lsp.xml.providers.format.FormatElementCategory.PreserveSpace +import org.eclipse.lemminx.dom.builder.EmptyElements +import org.eclipse.lemminx.dom.builder.BaseXmlFormattingOptions import org.eclipse.lemminx.dom.DOMElement /** @@ -26,60 +28,47 @@ import org.eclipse.lemminx.dom.DOMElement * * @author Akash Yadav */ -open class XMLFormattingOptions { - open val trimFinalNewLine: Boolean +open class XMLFormattingOptions : BaseXmlFormattingOptions() { + + override val isTrimFinalNewLine: Boolean get() = com.itsaky.androidide.preferences.internal.trimFinalNewLine - open val insertFinalNewLine: Boolean + override val isInsertFinalNewLine: Boolean get() = com.itsaky.androidide.preferences.internal.insertFinalNewLine - open val splitAttributes: Boolean + override val isSplitAttributes: Boolean get() = com.itsaky.androidide.preferences.internal.splitAttributes - open val joinCDataLines: Boolean + override val isJoinCDataLines: Boolean get() = com.itsaky.androidide.preferences.internal.joinCDataLines - open val joinCommentLines: Boolean + override val isJoinCommentLines: Boolean get() = com.itsaky.androidide.preferences.internal.joinCommentLines - open val joinContentLines: Boolean + override val isJoinContentLines: Boolean get() = com.itsaky.androidide.preferences.internal.joinContentLines - open val spaceBeforeEmptyCloseTag: Boolean + override val isSpaceBeforeEmptyCloseTag: Boolean get() = com.itsaky.androidide.preferences.internal.spaceBeforeEmptyCloseTag - open val preserveEmptyContent: Boolean + override val isPreserveEmptyContent: Boolean get() = com.itsaky.androidide.preferences.internal.preserveEmptyContent - open val preserveAttributeLineBreaks: Boolean + override val isPreserveAttributeLineBreaks: Boolean get() = com.itsaky.androidide.preferences.internal.preserveAttributeLineBreaks - open val closingBracketNewLine: Boolean + override val isClosingBracketNewLine: Boolean get() = com.itsaky.androidide.preferences.internal.closingBracketNewLine - open val trimTrailingWhitespace: Boolean + override val isTrimTrailingWhitespace: Boolean get() = com.itsaky.androidide.preferences.internal.trimTrailingWhitespace - open val maxLineWidth: Int + override val maxLineWidth: Int get() = com.itsaky.androidide.preferences.internal.maxLineWidth - open val preservedNewLines: Int + override val preservedNewLines: Int get() = com.itsaky.androidide.preferences.internal.preservedNewLines - open val splitAttributesIndentSize: Int + override val splitAttributesIndentSize: Int get() = com.itsaky.androidide.preferences.internal.splitAttributesIndentSize - open val emptyElementsBehavior: EmptyElements - get() = EmptyElements.valueOf(com.itsaky.androidide.preferences.internal.emptyElementsBehavior) + override val emptyElementsBehavior: EmptyElements + get() = EmptyElements.valueOf( + com.itsaky.androidide.preferences.internal.emptyElementsBehavior) private val preserveSpace = - listOf( - "xsl:text", - "xsl:comment", - "xsl:processing-instruction", - "literallayout", - "programlisting", - "screen", - "synopsis", - "pre", - "xd:pre" - ) + listOf("xsl:text", "xsl:comment", "xsl:processing-instruction", + "literallayout", "programlisting", "screen", "synopsis", "pre", "xd:pre") fun getFormatElementCategory(element: DOMElement): FormatElementCategory? { return preserveSpace.find { it == element.tagName }?.let { PreserveSpace } } } - -enum class EmptyElements { - Expand, - Collapse, - Ignore -} diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMAttributeFormatter.java b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMAttributeFormatter.java index 8b7ccb86d..e6e552ea4 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMAttributeFormatter.java +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMAttributeFormatter.java @@ -144,7 +144,7 @@ public class DOMAttributeFormatter { } private boolean isSplitAttributes() { - return formatterDocument.getSharedSettings().getFormattingOptions().getSplitAttributes(); + return formatterDocument.getSharedSettings().getFormattingOptions().isSplitAttributes(); } private int getSplitAttributesIndentSize() { @@ -158,7 +158,7 @@ public class DOMAttributeFormatter { return formatterDocument .getSharedSettings() .getFormattingOptions() - .getPreserveAttributeLineBreaks(); + .isPreserveAttributeLineBreaks(); } private boolean hasLineBreak(int prevOffset, int start) { diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMCDATAFormatter.java b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMCDATAFormatter.java index e3b30a3fa..6aed9c1da 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMCDATAFormatter.java +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMCDATAFormatter.java @@ -113,7 +113,7 @@ public class DOMCDATAFormatter { } private boolean isJoinCDATALines() { - return formatterDocument.getSharedSettings().getFormattingOptions().getJoinCDataLines(); + return formatterDocument.getSharedSettings().getFormattingOptions().isJoinCDataLines(); } private int getTabSize() { diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMCommentFormatter.java b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMCommentFormatter.java index 319c9ca53..f46d64ecc 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMCommentFormatter.java +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMCommentFormatter.java @@ -109,7 +109,7 @@ public class DOMCommentFormatter { } private boolean isJoinCommentLines() { - return formatterDocument.getSharedSettings().getFormattingOptions().getJoinCommentLines(); + return formatterDocument.getSharedSettings().getFormattingOptions().isJoinCommentLines(); } private int getTabSize() { diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMElementFormatter.java b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMElementFormatter.java index e1a5e3045..7b423c8ee 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMElementFormatter.java +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMElementFormatter.java @@ -29,8 +29,8 @@ package com.itsaky.androidide.lsp.xml.providers.format; import com.itsaky.androidide.lsp.models.TextEdit; -import com.itsaky.androidide.lsp.xml.models.EmptyElements; +import org.eclipse.lemminx.dom.builder.EmptyElements; import org.eclipse.lemminx.commons.BadLocationException; import org.eclipse.lemminx.dom.DOMAttr; import org.eclipse.lemminx.dom.DOMElement; @@ -435,7 +435,7 @@ public class DOMElementFormatter { private boolean shouldFormatClosingBracketNewLine(DOMElement element) { boolean isSingleAttribute = element.getAttributeNodes() == null || element.getAttributeNodes().size() == 1; - return (formatterDocument.getSharedSettings().getFormattingOptions().getClosingBracketNewLine() + return (formatterDocument.getSharedSettings().getFormattingOptions().isClosingBracketNewLine() && isSplitAttributes() && !isSingleAttribute); } @@ -500,11 +500,11 @@ public class DOMElementFormatter { return formatterDocument .getSharedSettings() .getFormattingOptions() - .getPreserveAttributeLineBreaks(); + .isPreserveAttributeLineBreaks(); } private boolean isSplitAttributes() { - return formatterDocument.getSharedSettings().getFormattingOptions().getSplitAttributes(); + return formatterDocument.getSharedSettings().getFormattingOptions().isSplitAttributes(); } private int getSplitAttributesIndentSize() { @@ -518,7 +518,7 @@ public class DOMElementFormatter { return formatterDocument .getSharedSettings() .getFormattingOptions() - .getSpaceBeforeEmptyCloseTag(); + .isSpaceBeforeEmptyCloseTag(); } private EmptyElements getEmptyElements() { @@ -526,7 +526,7 @@ public class DOMElementFormatter { } private boolean isPreserveEmptyContent() { - return formatterDocument.getSharedSettings().getFormattingOptions().getPreserveEmptyContent(); + return formatterDocument.getSharedSettings().getFormattingOptions().isPreserveEmptyContent(); } private void formatChildren( diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMTextFormatter.java b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMTextFormatter.java index 84e68eb9e..de6c227dc 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMTextFormatter.java +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/DOMTextFormatter.java @@ -123,7 +123,7 @@ public class DOMTextFormatter { } private boolean isPreserveEmptyContent() { - return formatterDocument.getSharedSettings().getFormattingOptions().getPreserveEmptyContent(); + return formatterDocument.getSharedSettings().getFormattingOptions().isPreserveEmptyContent(); } private void replaceSpacesWithOneSpace(int spaceStart, int spaceEnd, List edits) { @@ -131,6 +131,6 @@ public class DOMTextFormatter { } private boolean isJoinContentLines() { - return formatterDocument.getSharedSettings().getFormattingOptions().getJoinContentLines(); + return formatterDocument.getSharedSettings().getFormattingOptions().isJoinContentLines(); } } diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/XMLFormatterDocument.java b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/XMLFormatterDocument.java index 1c7cae2b8..f0311abdd 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/XMLFormatterDocument.java +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/XMLFormatterDocument.java @@ -28,13 +28,13 @@ package com.itsaky.androidide.lsp.xml.providers.format; import com.itsaky.androidide.lsp.models.TextEdit; -import com.itsaky.androidide.lsp.xml.models.EmptyElements; import com.itsaky.androidide.lsp.xml.models.XMLServerSettings; import com.itsaky.androidide.lsp.xml.utils.XMLBuilder; import com.itsaky.androidide.models.Position; import com.itsaky.androidide.models.Range; import com.itsaky.androidide.preferences.internal.EditorPreferencesKt; +import org.eclipse.lemminx.dom.builder.EmptyElements; import org.eclipse.lemminx.commons.BadLocationException; import org.eclipse.lemminx.commons.TextDocument; import org.eclipse.lemminx.dom.DOMAttr; @@ -505,7 +505,7 @@ public class XMLFormatterDocument { * @throws BadLocationException */ private void formatElementStartTagCloseBracket(DOMElement element) throws BadLocationException { - if (this.sharedSettings.getFormattingOptions().getPreserveAttributeLineBreaks() + if (this.sharedSettings.getFormattingOptions().isPreserveAttributeLineBreaks() && element.hasAttributes() && !isSameLine(getLastAttribute(element).getEnd(), element.getStartTagCloseOffset())) { xmlBuilder.linefeed(); @@ -527,7 +527,7 @@ public class XMLFormatterDocument { */ private void formatElementStartTagSelfCloseBracket(DOMElement element) throws BadLocationException { - if (this.sharedSettings.getFormattingOptions().getPreserveAttributeLineBreaks() + if (this.sharedSettings.getFormattingOptions().isPreserveAttributeLineBreaks() && element.hasAttributes()) { int elementEndOffset = element.getEnd(); if (element.isStartTagClosed()) { @@ -550,8 +550,8 @@ public class XMLFormatterDocument { formatAttribute(attr, isSingleAttribute, prevOffset); prevOffset = attr.getEnd(); } - if ((this.sharedSettings.getFormattingOptions().getClosingBracketNewLine() - && this.sharedSettings.getFormattingOptions().getSplitAttributes()) + if ((this.sharedSettings.getFormattingOptions().isClosingBracketNewLine() + && this.sharedSettings.getFormattingOptions().isSplitAttributes()) && !isSingleAttribute) { xmlBuilder.linefeed(); // Indent by tag + splitAttributesIndentSize to match with attribute indent @@ -565,7 +565,7 @@ public class XMLFormatterDocument { private void formatAttribute(DOMAttr attr, boolean isSingleAttribute, int prevOffset) throws BadLocationException { - if (this.sharedSettings.getFormattingOptions().getPreserveAttributeLineBreaks() + if (this.sharedSettings.getFormattingOptions().isPreserveAttributeLineBreaks() && !isSameLine(prevOffset, attr.getStart())) { xmlBuilder.linefeed(); xmlBuilder.indent(this.indentLevel + 1); @@ -636,7 +636,7 @@ public class XMLFormatterDocument { case Expand: case Collapse: { - if (this.sharedSettings.getFormattingOptions().getPreserveEmptyContent()) { + if (this.sharedSettings.getFormattingOptions().isPreserveEmptyContent()) { // preserve content if (element.hasChildNodes()) { // The element is empty and contains somes spaces which must be preserved @@ -752,11 +752,11 @@ public class XMLFormatterDocument { // check if format range reaches the end of the document if (this.endOffset == this.textDocument.getText().length()) { - if (this.sharedSettings.getFormattingOptions().getTrimFinalNewLine()) { + if (this.sharedSettings.getFormattingOptions().isTrimFinalNewLine()) { this.xmlBuilder.trimFinalNewlines(); } - if (this.sharedSettings.getFormattingOptions().getInsertFinalNewLine() + if (this.sharedSettings.getFormattingOptions().isInsertFinalNewLine() && !this.xmlBuilder.isLastLineEmptyOrWhitespace()) { this.xmlBuilder.linefeed(); } diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/XMLFormatterDocumentNew.java b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/XMLFormatterDocumentNew.java index 8ea3b0705..0a8282051 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/XMLFormatterDocumentNew.java +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/format/XMLFormatterDocumentNew.java @@ -683,11 +683,11 @@ public class XMLFormatterDocumentNew { } private boolean isTrimFinalNewlines() { - return getFormattingSettings().getTrimFinalNewLine(); + return getFormattingSettings().isTrimFinalNewLine(); } private boolean isInsertFinalNewline() { - return getFormattingSettings().getInsertFinalNewLine(); + return getFormattingSettings().isInsertFinalNewLine(); } private String getQuotationAsString() { diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/utils/XMLBuilder.java b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/utils/XMLBuilder.java index 7ba8065da..8f6e33548 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/utils/XMLBuilder.java +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/utils/XMLBuilder.java @@ -27,535 +27,21 @@ package com.itsaky.androidide.lsp.xml.utils; -import static org.eclipse.lemminx.utils.StringUtils.normalizeSpace; - import com.itsaky.androidide.lsp.xml.models.XMLServerSettings; -import com.itsaky.androidide.preferences.internal.EditorPreferencesKt; -import org.eclipse.lemminx.dom.DOMAttr; -import org.eclipse.lemminx.dom.DOMComment; -import org.eclipse.lemminx.dom.DTDDeclNode; +import org.eclipse.lemminx.dom.builder.BaseXmlFormattingOptions; +import org.eclipse.lemminx.dom.builder.XmlBuilder; /** * XML content builder utilities. */ -public class XMLBuilder { - - private final XMLServerSettings sharedSettings; - private final String lineDelimiter; - private final StringBuilder xml; - private final String whitespacesIndent; +public class XMLBuilder extends XmlBuilder { public XMLBuilder(String whitespacesIndent, String lineDelimiter) { - this(whitespacesIndent, lineDelimiter, XMLServerSettings.INSTANCE); + this(whitespacesIndent, lineDelimiter, XMLServerSettings.INSTANCE.getFormattingOptions()); } - public XMLBuilder(String whitespacesIndent, String lineDelimiter, XMLServerSettings settings) { - this.whitespacesIndent = whitespacesIndent; - this.lineDelimiter = lineDelimiter; - this.sharedSettings = settings; - this.xml = new StringBuilder(); - } - - public XMLBuilder appendSpace() { - append(" "); - return this; - } - - public XMLBuilder startElement(String prefix, String name, boolean close) { - append("<"); - if (prefix != null && !prefix.isEmpty()) { - append(prefix); - append(":"); - } - append(name); - if (close) { - closeStartElement(); - } - return this; - } - - public XMLBuilder startElement(String name, boolean close) { - return startElement(null, name, close); - } - - public XMLBuilder endElement(String name, boolean isEndTagClosed) { - return endElement(null, name, isEndTagClosed); - } - - public XMLBuilder endElement(String name) { - return endElement(null, name, true); - } - - public XMLBuilder endElement(String prefix, String name) { - return endElement(prefix, name, true); - } - - public XMLBuilder endElement(String prefix, String name, boolean isEndTagClosed) { - append(""); - } - return this; - } - - public XMLBuilder closeStartElement() { - append(">"); - return this; - } - - public XMLBuilder selfCloseElement() { - if (isSpaceBeforeEmptyCloseTag() && !isLastLineEmptyOrWhitespace()) { - appendSpace(); - } - append("/>"); - return this; - } - - public XMLBuilder addSingleAttribute(DOMAttr attr) { - return addSingleAttribute(attr, false, true); - } - - public XMLBuilder addSingleAttribute(DOMAttr attr, boolean surroundWithQuotes, - boolean prependSpace + public XMLBuilder(String whitespacesIndent, String lineDelimiter, BaseXmlFormattingOptions settings ) { - return addSingleAttribute(attr.getName(), attr.getOriginalValue(), surroundWithQuotes, - prependSpace); - } - - public XMLBuilder addSingleAttribute(String name, String value, boolean surroundWithQuotes) { - return addSingleAttribute(name, value, surroundWithQuotes, true); - } - - /** - * Used when only one attribute is being added to a node. - * - *

It will not perform any linefeeds and only basic indentation. - * - * @param name attribute name - * @param value attribute value - * @param surroundWithQuotes true if quotes should be added around originalValue - * @return this XML Builder - */ - private XMLBuilder addSingleAttribute(String name, String value, boolean surroundWithQuotes, - boolean prependSpace - ) { - if (prependSpace) { - appendSpace(); - } - addAttributeContents(name, true, value, surroundWithQuotes); - return this; - } - - /** - * Add prolog attribute - * - *

It will not perform any linefeeds and only basic indentation. - * - * @param attr attribute - * @return this XML Builder - */ - public XMLBuilder addPrologAttribute(DOMAttr attr) { - appendSpace(); - addAttributeContents(attr.getName(), attr.hasDelimiter(), attr.getOriginalValue(), false); - return this; - } - - /** - * Used when you are knowingly adding multiple attributes. - * - *

Does linefeeds and indentation. - * - * @param name - * @param value - * @param level - * @return - */ - public XMLBuilder addAttribute(String name, String value, int level, boolean surroundWithQuotes) { - if (isSplitAttributes()) { - linefeed(); - indent(level + sharedSettings.getFormattingOptions().getSplitAttributesIndentSize()); - } else { - appendSpace(); - } - - addAttributeContents(name, true, value, surroundWithQuotes); - return this; - } - - public XMLBuilder addAttribute(DOMAttr attr, int level) { - return addAttribute(attr, level, false); - } - - private XMLBuilder addAttribute(DOMAttr attr, int level, boolean surroundWithQuotes) { - if (isSplitAttributes()) { - linefeed(); - indent(level + sharedSettings.getFormattingOptions().getSplitAttributesIndentSize()); - } else { - appendSpace(); - } - - addAttributeContents(attr.getName(), attr.hasDelimiter(), attr.getOriginalValue(), - surroundWithQuotes); - return this; - } - - /** - * Builds the attribute {name, '=', and value}. - * - *

Never puts quotes around unquoted values unless indicated to by 'surroundWithQuotes' - * - * @param name name of the attribute - * @param equalsSign true if equals sign exists, false otherwise - * @param originalValue value of the attribute - * @param surroundWithQuotes true if quotes should be added around originalValue, false otherwise - */ - private void addAttributeContents(String name, boolean equalsSign, String originalValue, - boolean surroundWithQuotes - ) { - if (name != null) { - append(name); - } - if (equalsSign) { - append("="); - } - if (originalValue != null) { - char preferredQuote = getQuotationAsChar(); - Character quote = null; - String valueWithoutQuote = originalValue; - if (StringUtils.isQuoted(originalValue)) { - if (originalValue.charAt(0) != preferredQuote) { - quote = preferredQuote; - } else { - quote = originalValue.charAt(0); - } - valueWithoutQuote = StringUtils.convertToQuotelessValue(originalValue); - } else if (surroundWithQuotes) { - quote = preferredQuote; - } - formatAttributeValue(valueWithoutQuote, quote); - } - } - - private void formatAttributeValue(String valueWithoutQuote, Character quote) { - if (quote != null) { - append(quote); - } - append(valueWithoutQuote); - if (quote != null) { - append(quote); - } - } - - public void append(String str) { - xml.append(str); - } - - public void append(char c) { - xml.append(c); - } - - public XMLBuilder linefeed() { - append(lineDelimiter); - if (whitespacesIndent != null) { - append(whitespacesIndent); - } - return this; - } - - /** - * Returns this XMLBuilder with text added - * - * @param text the text to add - * @return this XMLBuilder with text added - */ - public XMLBuilder addContent(String text) { - return addContent(text, false, false, null); - } - - /** - * Returns this XMLBuilder with text added depending on isWhitespaceContent - * , hasSiblings and delimiter - * - * @param text the proposed text to add - * @param isWhitespaceContent whether or not the text contains only whitespace content - * @param hasSiblings whether or not the corresponding text node has siblings - * @param delimiter line delimiter - * @return this XMLBuilder with text added depending on isWhitespaceContent - * , hasSiblings and delimiter - */ - public XMLBuilder addContent(String text, boolean isWhitespaceContent, boolean hasSiblings, - String delimiter - ) { - if (!isWhitespaceContent) { - if (isJoinContentLines()) { - text = StringUtils.normalizeSpace(text); - } else if (hasSiblings) { - text = text.trim(); - } - if (isTrimTrailingWhitespace()) { - text = trimTrailingSpacesEachLine(text); - } - append(text); - } else if (!hasSiblings && isPreserveEmptyContent()) { - append(text); - } else if (hasSiblings) { - int preservedNewLines = getPreservedNewlines(); - if (preservedNewLines > 0) { - int newLineCount = StringUtils.getNumberOfNewLines(text, isWhitespaceContent, delimiter, - preservedNewLines); - for (int i = 0; i < newLineCount - 1; - i++) { // - 1 because the node after will insert a delimiter - append(delimiter); - } - } - } - return this; - } - - public XMLBuilder indent(int level) { - for (int i = 0; i < level; i++) { - if (isInsertSpaces()) { - for (int j = 0; j < getTabSize(); j++) { - appendSpace(); - } - } else { - append("\t"); - } - } - return this; - } - - public XMLBuilder startPrologOrPI(String tagName) { - append(""); - return this; - } - - @Override - public String toString() { - return xml.toString(); - } - - /** - * Trims the trailing newlines for the current XML StringBuilder - */ - public void trimFinalNewlines() { - int i = xml.length() - 1; - while (i >= 0 && (xml.charAt(i) == '\r' || xml.charAt(i) == '\n')) { - xml.deleteCharAt(i--); - } - } - - /** - * Returns str with the trailing spaces from each line removed - * - * @param str the String - * @return str with the trailing spaces from each line removed - */ - private static String trimTrailingSpacesEachLine(String str) { - StringBuilder sb = new StringBuilder(str); - int i = str.length() - 1; - boolean removeSpaces = true; - while (i >= 0) { - char curr = sb.charAt(i); - if (curr == '\n' || curr == '\r') { - removeSpaces = true; - } else if (removeSpaces && Character.isWhitespace(curr)) { - sb.deleteCharAt(i); - } else { - removeSpaces = false; - } - i--; - } - return sb.toString(); - } - - public XMLBuilder startCDATA() { - append(""); - return this; - } - - public XMLBuilder startComment(DOMComment comment) { - if (comment.isCommentSameLineEndTag()) { - appendSpace(); - } - append(""); - return this; - } - - public XMLBuilder endDoctype() { - append(">"); - return this; - } - - public boolean isLastLineEmptyOrWhitespace() { - if (this.xml.length() == 0) { - return true; - } - int i = this.xml.length() - 1; - while (i > 0 && Character.isSpaceChar(this.xml.charAt(i))) { - i--; - } - return i > 0 && (this.xml.charAt(i) == '\r' || this.xml.charAt(i) == '\n'); - } - - private String replaceQuotesIfNeeded(String str) { - if (StringUtils.isQuoted(str)) { - String quote = getQuotationAsString(); - return quote + StringUtils.convertToQuotelessValue(str) + quote; - } - return str; - } - - private String getQuotationAsString() { - return "\""; - } - - private boolean isJoinCommentLines() { - return sharedSettings.getFormattingOptions().getJoinCommentLines(); - } - - private boolean isJoinCDATALines() { - return sharedSettings.getFormattingOptions().getJoinCDataLines(); - } - - private boolean isSplitAttributes() { - return sharedSettings.getFormattingOptions().getSplitAttributes(); - } - - private boolean isInsertSpaces() { - return EditorPreferencesKt.getUseSoftTab(); - } - - private int getTabSize() { - return EditorPreferencesKt.getTabSize(); - } - - private boolean isJoinContentLines() { - return sharedSettings.getFormattingOptions().getJoinContentLines(); - } - - private boolean isPreserveEmptyContent() { - return sharedSettings.getFormattingOptions().getPreserveEmptyContent(); - } - - private boolean isTrimTrailingWhitespace() { - return sharedSettings.getFormattingOptions().getTrimTrailingWhitespace(); - } - - private int getPreservedNewlines() { - return sharedSettings.getFormattingOptions().getPreservedNewLines(); - } - - private boolean isSpaceBeforeEmptyCloseTag() { - return sharedSettings.getFormattingOptions().getSpaceBeforeEmptyCloseTag(); - } - - private char getQuotationAsChar() { - return '"'; - } - - public int length() { - return xml.length(); - } - - public char charAt(int index) { - return xml.charAt(index); + super(whitespacesIndent, lineDelimiter, settings); } } diff --git a/subprojects/xml-dom/build.gradle.kts b/subprojects/xml-dom/build.gradle.kts index 1fbfa030e..2a66e8580 100644 --- a/subprojects/xml-dom/build.gradle.kts +++ b/subprojects/xml-dom/build.gradle.kts @@ -18,6 +18,7 @@ @Suppress("JavaPluginLanguageLevel") plugins { id("java-library") + id("org.jetbrains.kotlin.jvm") } dependencies { diff --git a/subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/BaseXmlFormattingOptions.kt b/subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/BaseXmlFormattingOptions.kt new file mode 100644 index 000000000..3bcfe469d --- /dev/null +++ b/subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/BaseXmlFormattingOptions.kt @@ -0,0 +1,51 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package org.eclipse.lemminx.dom.builder + +import org.eclipse.lemminx.dom.builder.EmptyElements.Collapse + +/** + * Formatting options for the [XmlBuilder]. + * + * @author Akash Yadav + */ +open class BaseXmlFormattingOptions { + + open val isTrimFinalNewLine: Boolean = false + open val isInsertFinalNewLine: Boolean = true + open val isSplitAttributes: Boolean = true + open val isJoinCDataLines: Boolean = false + open val isJoinCommentLines: Boolean = true + open val isJoinContentLines: Boolean = false + open val isSpaceBeforeEmptyCloseTag: Boolean = true + open val isPreserveEmptyContent: Boolean = true + open val isPreserveAttributeLineBreaks: Boolean = true + open val isClosingBracketNewLine: Boolean = false + open val isTrimTrailingWhitespace: Boolean = true + open val isUseSoftTab: Boolean = true + + open val tabSize = 4 + open val maxLineWidth: Int = 80 + open val preservedNewLines: Int = 2 + + open val splitAttributesIndentSize: Int + get() = tabSize / 2 + + open val emptyElementsBehavior: EmptyElements + get() = Collapse +} \ No newline at end of file diff --git a/subprojects/xml-utils/src/test/java/com/itsaky/androidide/xml/utils/XmlBuilderTest.kt b/subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/EmptyElements.kt similarity index 74% rename from subprojects/xml-utils/src/test/java/com/itsaky/androidide/xml/utils/XmlBuilderTest.kt rename to subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/EmptyElements.kt index 78474c8c6..37aba24b9 100644 --- a/subprojects/xml-utils/src/test/java/com/itsaky/androidide/xml/utils/XmlBuilderTest.kt +++ b/subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/EmptyElements.kt @@ -15,20 +15,15 @@ * along with AndroidIDE. If not, see . */ -package com.itsaky.androidide.xml.utils - -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner +package org.eclipse.lemminx.dom.builder /** + * Behavior for empty elements. + * * @author Akash Yadav */ -@RunWith(RobolectricTestRunner::class) -class XmlBuilderTest { - - @Test - fun `test values res build`() { - - } +enum class EmptyElements { + Expand, + Collapse, + Ignore } \ No newline at end of file diff --git a/subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/IndentedXmlBuilder.kt b/subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/IndentedXmlBuilder.kt new file mode 100644 index 000000000..bbe1c020e --- /dev/null +++ b/subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/IndentedXmlBuilder.kt @@ -0,0 +1,103 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package org.eclipse.lemminx.dom.builder + +/** + * An implementation of [XmlBuilder] which can handle indentations while building the XML content. + * + * @author Akash Yadav + */ +class IndentedXmlBuilder(private val autoIndent: Boolean = false, whitespacesIndent: String?, lineDelimiter: String?, + options: BaseXmlFormattingOptions? +) : XmlBuilder(whitespacesIndent, lineDelimiter, options) { + + constructor(autoIndent: Boolean) : this(autoIndent, "", System.lineSeparator(), BaseXmlFormattingOptions()) + + constructor() : this(false) + + private var indentLevel = 0 + + override fun startElement(prefix: String?, name: String?, close: Boolean + ): XmlBuilder { + if (autoIndent) { + lfIndent() + } + return super.startElement(prefix, name, close).also { + if (autoIndent) { + incrIndent() + } + } + } + + override fun addAttributeContents(name: String?, equalsSign: Boolean, + originalValue: String?, + surroundWithQuotes: Boolean + ) { + if (autoIndent) { + lfIndent() + } + super.addAttributeContents(name, equalsSign, originalValue, + surroundWithQuotes) + } + + override fun selfCloseElement(): XmlBuilder { + if (autoIndent) { + dcrIndent() + } + return super.selfCloseElement() + } + + override fun endElement(prefix: String?, name: String?, + isEndTagClosed: Boolean + ): XmlBuilder { + if (autoIndent) { + dcrIndent() + lfIndent() + } + return super.endElement(prefix, name, isEndTagClosed) + } + + /** + * Increment the indentation level. + */ + fun incrIndent() { + ++indentLevel + } + + /** + * Decrement the indentation level. + */ + fun dcrIndent() { + --indentLevel + } + + /** + * Appends a line feed and indents the next line. + */ + fun lfIndent() { + linefeed() + indent() + } + + /** + * Appends indentation to the indent line. + */ + fun indent() { + indent(indentLevel) + } +} \ No newline at end of file diff --git a/subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/XmlBuilder.java b/subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/XmlBuilder.java new file mode 100644 index 000000000..d2e56d6b5 --- /dev/null +++ b/subprojects/xml-dom/src/main/java/org/eclipse/lemminx/dom/builder/XmlBuilder.java @@ -0,0 +1,559 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package org.eclipse.lemminx.dom.builder; + +import static org.eclipse.lemminx.utils.StringUtils.normalizeSpace; + +import org.eclipse.lemminx.dom.DOMAttr; +import org.eclipse.lemminx.dom.DOMComment; +import org.eclipse.lemminx.dom.DTDDeclNode; +import org.eclipse.lemminx.utils.StringUtils; + +/** + * XML content builder utilities. + */ +public class XmlBuilder { + + private final BaseXmlFormattingOptions formattingOptions; + private final String lineDelimiter; + private final StringBuilder xml; + private final String whitespacesIndent; + + public XmlBuilder() { + this("", System.lineSeparator(), new BaseXmlFormattingOptions()); + } + + public XmlBuilder(String whitespacesIndent, String lineDelimiter, BaseXmlFormattingOptions options + ) { + this.whitespacesIndent = whitespacesIndent; + this.lineDelimiter = lineDelimiter; + this.formattingOptions = options; + this.xml = new StringBuilder(); + } + + public XmlBuilder appendSpace() { + append(" "); + return this; + } + + public XmlBuilder startElement(String prefix, String name, boolean close) { + append("<"); + if (prefix != null && !prefix.isEmpty()) { + append(prefix); + append(":"); + } + append(name); + if (close) { + closeStartElement(); + } + return this; + } + + public XmlBuilder startElement(String name, boolean close) { + return startElement(null, name, close); + } + + public XmlBuilder endElement(String name, boolean isEndTagClosed) { + return endElement(null, name, isEndTagClosed); + } + + public XmlBuilder endElement(String name) { + return endElement(null, name, true); + } + + public XmlBuilder endElement(String prefix, String name) { + return endElement(prefix, name, true); + } + + public XmlBuilder endElement(String prefix, String name, boolean isEndTagClosed) { + append(""); + } + return this; + } + + public XmlBuilder closeStartElement() { + append(">"); + return this; + } + + public XmlBuilder selfCloseElement() { + if (isSpaceBeforeEmptyCloseTag() && !isLastLineEmptyOrWhitespace()) { + appendSpace(); + } + append("/>"); + return this; + } + + public XmlBuilder androidAttribute(String name, String value) { + return addSingleAttribute("android:" + name, value, true); + } + + public XmlBuilder addSingleAttribute(DOMAttr attr) { + return addSingleAttribute(attr, false, true); + } + + public XmlBuilder addSingleAttribute(DOMAttr attr, boolean surroundWithQuotes, + boolean prependSpace + ) { + return addSingleAttribute(attr.getName(), attr.getOriginalValue(), surroundWithQuotes, + prependSpace); + } + + public XmlBuilder addSingleAttribute(String name, String value) { + return addSingleAttribute(name, value, true); + } + + public XmlBuilder addSingleAttribute(String name, String value, boolean surroundWithQuotes) { + return addSingleAttribute(name, value, surroundWithQuotes, true); + } + + /** + * Used when only one attribute is being added to a node. + * + *

It will not perform any linefeeds and only basic indentation. + * + * @param name attribute name + * @param value attribute value + * @param surroundWithQuotes true if quotes should be added around originalValue + * @return this XML Builder + */ + private XmlBuilder addSingleAttribute(String name, String value, boolean surroundWithQuotes, + boolean prependSpace + ) { + if (prependSpace) { + appendSpace(); + } + addAttributeContents(name, true, value, surroundWithQuotes); + return this; + } + + /** + * Add prolog attribute + * + *

It will not perform any linefeeds and only basic indentation. + * + * @param attr attribute + * @return this XML Builder + */ + public XmlBuilder addPrologAttribute(DOMAttr attr) { + appendSpace(); + addAttributeContents(attr.getName(), attr.hasDelimiter(), attr.getOriginalValue(), false); + return this; + } + + /** + * Used when you are knowingly adding multiple attributes. + * + *

Does linefeeds and indentation. + * + * @param name + * @param value + * @param level + * @return + */ + public XmlBuilder addAttribute(String name, String value, int level, boolean surroundWithQuotes) { + if (isSplitAttributes()) { + linefeed(); + indent(level + formattingOptions.getSplitAttributesIndentSize()); + } else { + appendSpace(); + } + + addAttributeContents(name, true, value, surroundWithQuotes); + return this; + } + + public XmlBuilder addAttribute(DOMAttr attr, int level) { + return addAttribute(attr, level, false); + } + + private XmlBuilder addAttribute(DOMAttr attr, int level, boolean surroundWithQuotes) { + if (isSplitAttributes()) { + linefeed(); + indent(level + formattingOptions.getSplitAttributesIndentSize()); + } else { + appendSpace(); + } + + addAttributeContents(attr.getName(), attr.hasDelimiter(), attr.getOriginalValue(), + surroundWithQuotes); + return this; + } + + /** + * Builds the attribute {name, '=', and value}. + * + *

Never puts quotes around unquoted values unless indicated to by 'surroundWithQuotes' + * + * @param name name of the attribute + * @param equalsSign true if equals sign exists, false otherwise + * @param originalValue value of the attribute + * @param surroundWithQuotes true if quotes should be added around originalValue, false otherwise + */ + protected void addAttributeContents(String name, boolean equalsSign, String originalValue, + boolean surroundWithQuotes + ) { + if (name != null) { + append(name); + } + if (equalsSign) { + append("="); + } + if (originalValue != null) { + char preferredQuote = getQuotationAsChar(); + Character quote = null; + String valueWithoutQuote = originalValue; + if (StringUtils.isQuoted(originalValue)) { + if (originalValue.charAt(0) != preferredQuote) { + quote = preferredQuote; + } else { + quote = originalValue.charAt(0); + } + valueWithoutQuote = StringUtils.convertToQuotelessValue(originalValue); + } else if (surroundWithQuotes) { + quote = preferredQuote; + } + formatAttributeValue(valueWithoutQuote, quote); + } + } + + private void formatAttributeValue(String valueWithoutQuote, Character quote) { + if (quote != null) { + append(quote); + } + append(valueWithoutQuote); + if (quote != null) { + append(quote); + } + } + + public void append(String str) { + xml.append(str); + } + + public void append(char c) { + xml.append(c); + } + + public XmlBuilder linefeed() { + append(lineDelimiter); + if (whitespacesIndent != null) { + append(whitespacesIndent); + } + return this; + } + + /** + * Returns this XmlBuilder with text added + * + * @param text the text to add + * @return this XmlBuilder with text added + */ + public XmlBuilder addContent(String text) { + return addContent(text, false, false, null); + } + + /** + * Returns this XmlBuilder with text added depending on isWhitespaceContent + * , hasSiblings and delimiter + * + * @param text the proposed text to add + * @param isWhitespaceContent whether or not the text contains only whitespace content + * @param hasSiblings whether or not the corresponding text node has siblings + * @param delimiter line delimiter + * @return this XmlBuilder with text added depending on isWhitespaceContent + * , hasSiblings and delimiter + */ + public XmlBuilder addContent(String text, boolean isWhitespaceContent, boolean hasSiblings, + String delimiter + ) { + if (!isWhitespaceContent) { + if (isJoinContentLines()) { + text = StringUtils.normalizeSpace(text); + } else if (hasSiblings) { + text = text.trim(); + } + if (isTrimTrailingWhitespace()) { + text = trimTrailingSpacesEachLine(text); + } + append(text); + } else if (!hasSiblings && isPreserveEmptyContent()) { + append(text); + } else if (hasSiblings) { + int preservedNewLines = getPreservedNewlines(); + if (preservedNewLines > 0) { + int newLineCount = StringUtils.getNumberOfNewLines(text, isWhitespaceContent, delimiter, + preservedNewLines); + for (int i = 0; i < newLineCount - 1; + i++) { // - 1 because the node after will insert a delimiter + append(delimiter); + } + } + } + return this; + } + + public XmlBuilder indent(int level) { + for (int i = 0; i < level; i++) { + if (isInsertSpaces()) { + for (int j = 0; j < getTabSize(); j++) { + appendSpace(); + } + } else { + append("\t"); + } + } + return this; + } + + public XmlBuilder startPrologOrPI(String tagName) { + append(""); + return this; + } + + @Override + public String toString() { + return xml.toString(); + } + + /** + * Trims the trailing newlines for the current XML StringBuilder + */ + public void trimFinalNewlines() { + int i = xml.length() - 1; + while (i >= 0 && (xml.charAt(i) == '\r' || xml.charAt(i) == '\n')) { + xml.deleteCharAt(i--); + } + } + + /** + * Returns str with the trailing spaces from each line removed + * + * @param str the String + * @return str with the trailing spaces from each line removed + */ + private static String trimTrailingSpacesEachLine(String str) { + StringBuilder sb = new StringBuilder(str); + int i = str.length() - 1; + boolean removeSpaces = true; + while (i >= 0) { + char curr = sb.charAt(i); + if (curr == '\n' || curr == '\r') { + removeSpaces = true; + } else if (removeSpaces && Character.isWhitespace(curr)) { + sb.deleteCharAt(i); + } else { + removeSpaces = false; + } + i--; + } + return sb.toString(); + } + + public XmlBuilder startCDATA() { + append(""); + return this; + } + + public XmlBuilder startComment(DOMComment comment) { + if (comment.isCommentSameLineEndTag()) { + appendSpace(); + } + append(""); + return this; + } + + public XmlBuilder endDoctype() { + append(">"); + return this; + } + + public boolean isLastLineEmptyOrWhitespace() { + if (this.xml.length() == 0) { + return true; + } + int i = this.xml.length() - 1; + while (i > 0 && Character.isSpaceChar(this.xml.charAt(i))) { + i--; + } + return i > 0 && (this.xml.charAt(i) == '\r' || this.xml.charAt(i) == '\n'); + } + + private String replaceQuotesIfNeeded(String str) { + if (StringUtils.isQuoted(str)) { + String quote = getQuotationAsString(); + return quote + StringUtils.convertToQuotelessValue(str) + quote; + } + return str; + } + + private String getQuotationAsString() { + return "\""; + } + + private boolean isJoinCommentLines() { + return formattingOptions.isJoinCommentLines(); + } + + private boolean isJoinCDATALines() { + return formattingOptions.isJoinCDataLines(); + } + + private boolean isSplitAttributes() { + return formattingOptions.isSplitAttributes(); + } + + private boolean isInsertSpaces() { + return formattingOptions.isUseSoftTab(); + } + + private int getTabSize() { + return formattingOptions.getTabSize(); + } + + private boolean isJoinContentLines() { + return formattingOptions.isJoinContentLines(); + } + + private boolean isPreserveEmptyContent() { + return formattingOptions.isPreserveEmptyContent(); + } + + private boolean isTrimTrailingWhitespace() { + return formattingOptions.isTrimTrailingWhitespace(); + } + + private int getPreservedNewlines() { + return formattingOptions.getPreservedNewLines(); + } + + private boolean isSpaceBeforeEmptyCloseTag() { + return formattingOptions.isSpaceBeforeEmptyCloseTag(); + } + + private char getQuotationAsChar() { + return '"'; + } + + public int length() { + return xml.length(); + } + + public char charAt(int index) { + return xml.charAt(index); + } +} diff --git a/subprojects/xml-utils/src/main/java/com/itsaky/androidide/xml/utils/XmlBuilder.java b/subprojects/xml-utils/src/main/java/com/itsaky/androidide/xml/utils/XmlBuilder.java deleted file mode 100644 index c0b009240..000000000 --- a/subprojects/xml-utils/src/main/java/com/itsaky/androidide/xml/utils/XmlBuilder.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.itsaky.androidide.xml.utils; - -import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; -import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; -import static com.android.SdkConstants.PreferenceTags.PREFERENCE_CATEGORY; -import static com.android.SdkConstants.VALUE_MATCH_PARENT; -import static com.android.SdkConstants.VALUE_WRAP_CONTENT; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; - -/** - * Builds XML strings. Arguments are not validated or escaped. This class is designed to replace - * hand writing XML snippets in string literals. - */ -public final class XmlBuilder { - private enum Construct { - NULL, - START_TAG, - ATTRIBUTE, - CHARACTER_DATA, - END_TAG - } - - private final StringBuilder stringBuilder = new StringBuilder(); - - private Construct lastAppendedConstruct = Construct.NULL; - private int indentationLevel; - - @NonNull - public XmlBuilder startTag(@NonNull String name) { - if (!lastAppendedConstruct.equals(Construct.END_TAG)) { - int length = stringBuilder.length(); - if (length > 0) { - stringBuilder.replace(length - 1, length, ">\n"); - } - } - - if (indentationLevel != 0) { - stringBuilder.append('\n'); - } - - indent(); - - stringBuilder.append('<').append(name).append('\n'); - - indentationLevel++; - lastAppendedConstruct = Construct.START_TAG; - - return this; - } - - @NonNull - public XmlBuilder androidAttribute(@NonNull String name, boolean value) { - return androidAttribute(name, Boolean.toString(value)); - } - - @NonNull - public XmlBuilder androidAttribute(@NonNull String name, int value) { - return androidAttribute(name, Integer.toString(value)); - } - - @NonNull - public XmlBuilder androidAttribute(@NonNull String name, @NonNull String value) { - return attribute(SdkConstants.ANDROID_NS_NAME, name, value); - } - - @NonNull - public XmlBuilder attribute(@NonNull String name, @NonNull String value) { - return attribute("", name, value); - } - - @NonNull - public XmlBuilder attribute( - @NonNull String namespacePrefix, @NonNull String name, @NonNull String value) { - indent(); - - if (!namespacePrefix.isEmpty()) { - stringBuilder.append(namespacePrefix).append(':'); - } - - stringBuilder.append(name).append("=\"").append(value).append("\"\n"); - - lastAppendedConstruct = Construct.ATTRIBUTE; - return this; - } - - @NonNull - public XmlBuilder wrapContent() { - return withSize(VALUE_WRAP_CONTENT, VALUE_WRAP_CONTENT); - } - - @NonNull - public XmlBuilder matchParent() { - return withSize(VALUE_MATCH_PARENT, VALUE_MATCH_PARENT); - } - - @NonNull - public XmlBuilder withSize(@NonNull String width, @NonNull String height) { - androidAttribute(ATTR_LAYOUT_WIDTH, width); - androidAttribute(ATTR_LAYOUT_HEIGHT, height); - return this; - } - - @NonNull - public XmlBuilder characterData(@NonNull String data) { - if (lastAppendedConstruct.equals(Construct.START_TAG) - || lastAppendedConstruct.equals(Construct.ATTRIBUTE)) { - int length = stringBuilder.length(); - stringBuilder.replace(length - 1, length, ">\n"); - } - - indent(); - - stringBuilder.append(data).append('\n'); - - lastAppendedConstruct = Construct.CHARACTER_DATA; - return this; - } - - @NonNull - public XmlBuilder appendDirectly(String content) { - stringBuilder.append(content); - return this; - } - - @NonNull - public XmlBuilder endTag(@NonNull String name) { - return endTagImpl(name, !name.endsWith("Layout") && !name.equals(PREFERENCE_CATEGORY)); - } - - @NonNull - public XmlBuilder seperateEndTag(@NonNull String name) { - return endTagImpl(name, false); - } - - @NonNull - private XmlBuilder endTagImpl(@NonNull String name, boolean useEmptyElementTag) { - if (lastAppendedConstruct.equals(Construct.START_TAG) - || lastAppendedConstruct.equals(Construct.ATTRIBUTE)) { - int length = stringBuilder.length(); - - if (useEmptyElementTag) { - stringBuilder.deleteCharAt(length - 1); - } else { - stringBuilder.replace(length - 1, length, ">\n\n"); - } - } - - indentationLevel--; - - if ((lastAppendedConstruct.equals(Construct.START_TAG) - || lastAppendedConstruct.equals(Construct.ATTRIBUTE)) - && useEmptyElementTag) { - stringBuilder.append(" />\n"); - } else { - indent(); - - stringBuilder.append("\n"); - } - - lastAppendedConstruct = Construct.END_TAG; - return this; - } - - private void indent() { - for (int i = 0; i < indentationLevel; i++) { - stringBuilder.append(" "); - } - } - - @NonNull - @Override - public String toString() { - return stringBuilder.toString(); - } -} - diff --git a/templates-api/build.gradle.kts b/templates-api/build.gradle.kts index 80a4ab33f..cb94a094c 100644 --- a/templates-api/build.gradle.kts +++ b/templates-api/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { api(projects.common) api(projects.logger) api(projects.resources) + api(projects.subprojects.xmlDom) api(projects.subprojects.xmlUtils) api(libs.aapt2.common) diff --git a/templates-api/src/main/java/com/itsaky/androidide/templates/base/AndroidModuleTemplateBuilder.kt b/templates-api/src/main/java/com/itsaky/androidide/templates/base/AndroidModuleTemplateBuilder.kt index f38582941..f6cd32d2f 100644 --- a/templates-api/src/main/java/com/itsaky/androidide/templates/base/AndroidModuleTemplateBuilder.kt +++ b/templates-api/src/main/java/com/itsaky/androidide/templates/base/AndroidModuleTemplateBuilder.kt @@ -25,7 +25,9 @@ import com.itsaky.androidide.templates.base.modules.android.buildGradleSrc import com.itsaky.androidide.templates.base.modules.android.proguardRules import com.itsaky.androidide.templates.base.util.AndroidManifestBuilder import com.itsaky.androidide.templates.base.util.AndroidModuleResManager +import com.itsaky.androidide.templates.base.util.stringRes import com.squareup.javapoet.TypeSpec +import org.eclipse.lemminx.dom.builder.IndentedXmlBuilder import java.io.File class AndroidModuleTemplateBuilder : ModuleTemplateBuilder() { @@ -113,6 +115,20 @@ class AndroidModuleTemplateBuilder : ModuleTemplateBuilder() { manifest.apply { generate(manifestFile()) } + + res { + data.appName?.let { putStringRes(manifest.appLabelRes, it) } + + if (strings.isNotEmpty()) { + createValuesResource("strings") { + linefeed() + strings.forEach { (name, value) -> + stringRes(name, value) + } + linefeed() + } + } + } } override fun RecipeExecutor.buildGradle() { diff --git a/templates-api/src/main/java/com/itsaky/androidide/templates/base/base.kt b/templates-api/src/main/java/com/itsaky/androidide/templates/base/base.kt index 0cff0dc87..05b06b9d7 100644 --- a/templates-api/src/main/java/com/itsaky/androidide/templates/base/base.kt +++ b/templates-api/src/main/java/com/itsaky/androidide/templates/base/base.kt @@ -109,7 +109,7 @@ fun baseProject(block: ProjectTemplateConfigurator): ProjectTemplate { language = language.value!!, useKts = true) setDefaultModuleData( - ModuleTemplateData(":app", packageName.value!!, data.moduleNameToDir(":app"), + ModuleTemplateData(":app", appName = data.name, packageName.value!!, data.moduleNameToDir(":app"), type = AndroidApp, language = language.value!!, minSdk = minSdk.value!!, useKts = data.useKts)) } @@ -144,7 +144,7 @@ fun baseProject(block: ProjectTemplateConfigurator): ProjectTemplate { * * @param block The module configurator. */ -fun baseAndroidModule(block: AndroidModuleTemplateConfigurator): ModuleTemplate { +fun baseAndroidModule(isLibrary: Boolean = false, block: AndroidModuleTemplateConfigurator): ModuleTemplate { return AndroidModuleTemplateBuilder().apply { val moduleName = stringParameter { @@ -153,6 +153,12 @@ fun baseAndroidModule(block: AndroidModuleTemplateConfigurator): ModuleTemplate constraints = listOf(NONEMPTY, MODULE_NAME) } + val appName = if (isLibrary) null else stringParameter { + name = R.string.project_app_name + default = "My Application" + constraints = listOf(NONEMPTY) + } + val packageName = stringParameter { name = R.string.package_name default = "com.example.myapplication" @@ -177,11 +183,17 @@ fun baseAndroidModule(block: AndroidModuleTemplateConfigurator): ModuleTemplate displayName = Language::lang } - widgets(TextFieldWidget(moduleName), TextFieldWidget(packageName), SpinnerWidget(minSdk), + widgets(TextFieldWidget(moduleName)) + + appName?.let { + widgets(TextFieldWidget(it)) + } + + widgets(TextFieldWidget(packageName), SpinnerWidget(minSdk), SpinnerWidget(type), SpinnerWidget(language)) preRecipe = commonPreRecipe { - ModuleTemplateData(name = moduleName.value!!, packageName = packageName.value!!, + ModuleTemplateData(name = moduleName.value!!, appName = appName?.value, packageName = packageName.value!!, projectDir = requireProjectData().moduleNameToDir(moduleName.value!!), type = type.value!!, language = language.value!!, minSdk = minSdk.value!!) } diff --git a/templates-api/src/main/java/com/itsaky/androidide/templates/base/util/AndroidManifestBuilder.kt b/templates-api/src/main/java/com/itsaky/androidide/templates/base/util/AndroidManifestBuilder.kt index addd754bf..be0c1b527 100644 --- a/templates-api/src/main/java/com/itsaky/androidide/templates/base/util/AndroidManifestBuilder.kt +++ b/templates-api/src/main/java/com/itsaky/androidide/templates/base/util/AndroidManifestBuilder.kt @@ -31,7 +31,7 @@ import com.itsaky.androidide.templates.RecipeExecutor import com.itsaky.androidide.templates.base.modules.android.ManifestActivity import com.itsaky.androidide.templates.base.modules.android.ManifestIcon import com.itsaky.androidide.xml.permissions.Permission -import com.itsaky.androidide.xml.utils.XmlBuilder +import org.eclipse.lemminx.dom.builder.IndentedXmlBuilder import java.io.File /** @@ -102,29 +102,34 @@ class AndroidManifestBuilder { } private fun manifestSrc(): String { - return XmlBuilder().apply { + return IndentedXmlBuilder(autoIndent = true).apply { buildManifest() }.withXmlDecl() } - private fun XmlBuilder.buildManifest() { + private fun IndentedXmlBuilder.buildManifest() { createElement(TAG_MANIFEST) { attr(name = ANDROID_NS_NAME, value = ANDROID_URI, ns = XMLNS) + closeStartElement() permissions() application() } } - private fun XmlBuilder.permissions() { + private fun IndentedXmlBuilder.permissions() { + if (permissions.isEmpty()) { + return + } + for (permission in permissions) { - createElement(TAG_USES_PERMISSION) { + createElement(TAG_USES_PERMISSION, selfClose = true) { androidAttr("name", permission.constant) } } } - private fun XmlBuilder.application() { + private fun IndentedXmlBuilder.application() { if (isLibrary) { return } @@ -136,18 +141,25 @@ class AndroidManifestBuilder { androidAttr("label", appLabelRes) androidAttr("supportsRtl", rtl.toString()) androidAttr("theme", "@style/${themeRes}") + closeStartElement() activities() } } - private fun XmlBuilder.activities() { + private fun IndentedXmlBuilder.activities() { + if (activities.isEmpty()) { + return + } + for (activity in activities) { createElement(TAG_ACTIVITY) { androidAttr("name", activity.name(packageName)) if (activity.isLauncher || activity.isExported) { androidAttr("exported", "true") } + + closeStartElement() if (activity.isLauncher) { intentFilter() } @@ -155,26 +167,31 @@ class AndroidManifestBuilder { } } - private fun XmlBuilder.intentFilter() { - createElement(TAG_INTENT_FILTER) { + private fun IndentedXmlBuilder.intentFilter() { + createElement(TAG_INTENT_FILTER, closeStartTag = true) { // action - createElement(TAG_ACTION) { + createElement(TAG_ACTION, selfClose = true) { androidAttr("name", "android.intent.action.MAIN") } - // category - createElement(TAG_CATEGORY) { + createElement(TAG_CATEGORY, selfClose = true) { androidAttr("name", "android.intent.category.LAUNCHER") } } } - private fun XmlBuilder.androidAttr(name: String, value: String) { + private fun IndentedXmlBuilder.androidAttr(name: String, value: String) { androidAttribute(name, value) } - private fun XmlBuilder.attr(name: String, value: String, ns: String = "") { - attribute(ns, name, value) + private fun IndentedXmlBuilder.attr(name: String, value: String, + ns: String = "" + ) { + if (ns.isNotEmpty()) { + addSingleAttribute("${ns}:${name}", value) + } else { + addSingleAttribute(name, value) + } } private fun ManifestActivity.name(packageName: String?): String { diff --git a/templates-api/src/main/java/com/itsaky/androidide/templates/base/util/AndroidModuleResManager.kt b/templates-api/src/main/java/com/itsaky/androidide/templates/base/util/AndroidModuleResManager.kt index 87e305d97..40c044343 100644 --- a/templates-api/src/main/java/com/itsaky/androidide/templates/base/util/AndroidModuleResManager.kt +++ b/templates-api/src/main/java/com/itsaky/androidide/templates/base/util/AndroidModuleResManager.kt @@ -18,10 +18,12 @@ package com.itsaky.androidide.templates.base.util import com.android.aaptcompiler.ConfigDescription -import com.itsaky.androidide.xml.utils.XmlBuilder import com.itsaky.androidide.templates.SrcSet import com.itsaky.androidide.templates.SrcSet.Main import com.itsaky.androidide.templates.base.AndroidModuleTemplateBuilder +import com.itsaky.androidide.templates.base.util.AndroidModuleResManager.ResourceType.VALUES +import org.eclipse.lemminx.dom.builder.IndentedXmlBuilder +import org.eclipse.lemminx.dom.builder.XmlBuilder import java.io.File /** @@ -43,6 +45,18 @@ class AndroidModuleResManager { VALUES("values"), } + internal val strings = mutableMapOf() + + /** + * Adds a string resource entry in the `strings.xml` file. + * + * @param name The name of the string. + * @param value The value of the string. + */ + fun putStringRes(name: String, value: String) { + strings[name] = value + } + /** * Get the resource directory the given [resource type][ResourceType] and the [configuration][ConfigDescription]. * @@ -73,13 +87,15 @@ class AndroidModuleResManager { * @param config The configuration for the resource type. */ fun AndroidModuleTemplateBuilder.createValuesResource(name: String, - type: ResourceType, + type: ResourceType = VALUES, srcSet: SrcSet = Main, config: ConfigDescription = ConfigDescription(), + selfClose: Boolean = false, configure: XmlBuilder.() -> Unit ) { return createXmlResource(name, type, srcSet, config) { - createElement("resources", configure) + createElement(name = "resources", closeStartTag = true, + selfClose = selfClose, configure = configure) } } @@ -98,7 +114,7 @@ class AndroidModuleResManager { configure: XmlBuilder.() -> Unit = {} ) { val file = File(resDir(type, srcSet, config), "${name}.xml") - val builder = XmlBuilder().apply(configure) + val builder = IndentedXmlBuilder().apply(configure) executor.save(builder.withXmlDecl(), file) } diff --git a/templates-api/src/main/java/com/itsaky/androidide/templates/base/util/xmlBuilder.kt b/templates-api/src/main/java/com/itsaky/androidide/templates/base/util/xmlBuilder.kt index eb64d08ce..4a9ec2285 100644 --- a/templates-api/src/main/java/com/itsaky/androidide/templates/base/util/xmlBuilder.kt +++ b/templates-api/src/main/java/com/itsaky/androidide/templates/base/util/xmlBuilder.kt @@ -17,17 +17,22 @@ package com.itsaky.androidide.templates.base.util -import com.itsaky.androidide.xml.utils.XmlBuilder +import org.eclipse.lemminx.dom.builder.IndentedXmlBuilder +import org.eclipse.lemminx.dom.builder.XmlBuilder /** * Creates a new element in the [XmlBuilder] with the given [name]. * The [configure] function is used to configure everything inside the element and then * the element is automatically closed. */ -fun XmlBuilder.createElement(name: String, configure: XmlBuilder.() -> Unit) { - startTag(name) - apply(configure) - endTag(name) +fun XmlBuilder.createElement(name: String, closeStartTag: Boolean = false, selfClose: Boolean = false, configure: XmlBuilder.() -> Unit) { + startElement(name, closeStartTag) + configure() + if (selfClose) { + selfCloseElement() + } else { + endElement(name) + } } /** @@ -36,11 +41,14 @@ fun XmlBuilder.createElement(name: String, configure: XmlBuilder.() -> Unit) { * @param name The name of the string resource. * @param value The value of the XML resource. */ -fun XmlBuilder.addStringRes(name: String, value: String) { +fun XmlBuilder.stringRes(name: String, value: String, indent: Boolean = true) { + indent(1) createElement("string") { - attribute("name", name) - text(value) + addSingleAttribute("name", name) + closeStartElement() + append(value) } + linefeed() } /** diff --git a/templates-api/src/main/java/com/itsaky/androidide/templates/template.kt b/templates-api/src/main/java/com/itsaky/androidide/templates/template.kt index e3e501c1b..13e06b95d 100644 --- a/templates-api/src/main/java/com/itsaky/androidide/templates/template.kt +++ b/templates-api/src/main/java/com/itsaky/androidide/templates/template.kt @@ -148,7 +148,7 @@ class ProjectTemplateData(name: String, projectDir: File, val version: ProjectVe * @property type The type of module. * @property versions Version information for the module. */ -class ModuleTemplateData(name: String, val packageName: String, projectDir: File, +class ModuleTemplateData(name: String, val appName: String?, val packageName: String, projectDir: File, val type: ModuleType, language: Language, useKts: Boolean = true, minSdk: Sdk, val versions: ModuleVersionData = ModuleVersionData(minSdk) ) : BaseTemplateData(name, projectDir, language, useKts) { diff --git a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/base/themes.kt b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/base/themes.kt index b32f6de6c..8772c84a3 100644 --- a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/base/themes.kt +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/base/themes.kt @@ -22,7 +22,7 @@ internal fun simpleMaterial3Theme(themeName: String) : String {