fix(templates): use XmlBuilder from 'xml-dom' module for building XML files

This commit is contained in:
Akash Yadav
2023-05-13 22:38:30 +05:30
parent 80bd4ce3ee
commit d9331eca80
25 changed files with 878 additions and 835 deletions
+1 -1
View File
@@ -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
@@ -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
@@ -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
}
@@ -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) {
@@ -113,7 +113,7 @@ public class DOMCDATAFormatter {
}
private boolean isJoinCDATALines() {
return formatterDocument.getSharedSettings().getFormattingOptions().getJoinCDataLines();
return formatterDocument.getSharedSettings().getFormattingOptions().isJoinCDataLines();
}
private int getTabSize() {
@@ -109,7 +109,7 @@ public class DOMCommentFormatter {
}
private boolean isJoinCommentLines() {
return formatterDocument.getSharedSettings().getFormattingOptions().getJoinCommentLines();
return formatterDocument.getSharedSettings().getFormattingOptions().isJoinCommentLines();
}
private int getTabSize() {
@@ -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(
@@ -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<TextEdit> edits) {
@@ -131,6 +131,6 @@ public class DOMTextFormatter {
}
private boolean isJoinContentLines() {
return formatterDocument.getSharedSettings().getFormattingOptions().getJoinContentLines();
return formatterDocument.getSharedSettings().getFormattingOptions().isJoinContentLines();
}
}
@@ -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();
}
@@ -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() {
@@ -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("</");
if (prefix != null && !prefix.isEmpty()) {
append(prefix);
append(":");
}
if (name != null) {
append(name);
}
if (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.
*
* <p>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
*
* <p>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.
*
* <p>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}.
*
* <p>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 <code>text</code> added
*
* @param text the text to add
* @return this XMLBuilder with <code>text</code> added
*/
public XMLBuilder addContent(String text) {
return addContent(text, false, false, null);
}
/**
* Returns this XMLBuilder with <code>text</code> added depending on <code>isWhitespaceContent
* </code>, <code>hasSiblings</code> and <code>delimiter</code>
*
* @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 <code>text</code> added depending on <code>isWhitespaceContent
* </code>, <code>hasSiblings</code> and <code>delimiter</code>
*/
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("<?");
append(tagName);
return this;
}
public XMLBuilder addContentPI(String content) {
appendSpace();
append(content);
return this;
}
public XMLBuilder endPrologOrPI() {
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 <code>str</code> with the trailing spaces from each line removed
*
* @param str the String
* @return <code>str</code> 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("<![CDATA[");
return this;
}
public XMLBuilder addContentCDATA(String content) {
if (isJoinCDATALines()) {
content = normalizeSpace(content);
}
append(content);
return this;
}
public XMLBuilder endCDATA() {
append("]]>");
return this;
}
public XMLBuilder startComment(DOMComment comment) {
if (comment.isCommentSameLineEndTag()) {
appendSpace();
}
append("<!--");
return this;
}
public XMLBuilder addContentComment(String content) {
if (isJoinCommentLines()) {
appendSpace();
append(normalizeSpace(content));
appendSpace();
} else {
append(content);
}
return this;
}
public XMLBuilder addDeclTagStart(DTDDeclNode tag) {
append("<!" + tag.getDeclType());
return this;
}
public XMLBuilder addDeclTagStart(String declTagName) {
append("<!" + declTagName);
return this;
}
public XMLBuilder startDoctype() {
append("<!DOCTYPE");
return this;
}
public XMLBuilder startDTDElementDecl() {
append("<!ELEMENT");
return this;
}
public XMLBuilder startDTDAttlistDecl() {
append("<!ATTLIST");
return this;
}
public XMLBuilder addParameter(String parameter) {
return addUnindentedParameter(" " + replaceQuotesIfNeeded(parameter));
}
public XMLBuilder addUnindentedParameter(String parameter) {
append(replaceQuotesIfNeeded(parameter));
return this;
}
public XMLBuilder startDoctypeInternalSubset() {
append(" [");
return this;
}
public XMLBuilder startUnindentedDoctypeInternalSubset() {
append("[");
return this;
}
public XMLBuilder endDoctypeInternalSubset() {
append("]");
return this;
}
public XMLBuilder endComment() {
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);
}
}
+1
View File
@@ -18,6 +18,7 @@
@Suppress("JavaPluginLanguageLevel")
plugins {
id("java-library")
id("org.jetbrains.kotlin.jvm")
}
dependencies {
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
@@ -15,20 +15,15 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
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
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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("</");
if (prefix != null && !prefix.isEmpty()) {
append(prefix);
append(":");
}
if (name != null) {
append(name);
}
if (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.
*
* <p>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
*
* <p>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.
*
* <p>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}.
*
* <p>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 <code>text</code> added
*
* @param text the text to add
* @return this XmlBuilder with <code>text</code> added
*/
public XmlBuilder addContent(String text) {
return addContent(text, false, false, null);
}
/**
* Returns this XmlBuilder with <code>text</code> added depending on <code>isWhitespaceContent
* </code>, <code>hasSiblings</code> and <code>delimiter</code>
*
* @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 <code>text</code> added depending on <code>isWhitespaceContent
* </code>, <code>hasSiblings</code> and <code>delimiter</code>
*/
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("<?");
append(tagName);
return this;
}
public XmlBuilder addContentPI(String content) {
appendSpace();
append(content);
return this;
}
public XmlBuilder endPrologOrPI() {
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 <code>str</code> with the trailing spaces from each line removed
*
* @param str the String
* @return <code>str</code> 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("<![CDATA[");
return this;
}
public XmlBuilder addContentCDATA(String content) {
if (isJoinCDATALines()) {
content = normalizeSpace(content);
}
append(content);
return this;
}
public XmlBuilder endCDATA() {
append("]]>");
return this;
}
public XmlBuilder startComment(DOMComment comment) {
if (comment.isCommentSameLineEndTag()) {
appendSpace();
}
append("<!--");
return this;
}
public XmlBuilder addContentComment(String content) {
if (isJoinCommentLines()) {
appendSpace();
append(normalizeSpace(content));
appendSpace();
} else {
append(content);
}
return this;
}
public XmlBuilder addDeclTagStart(DTDDeclNode tag) {
append("<!" + tag.getDeclType());
return this;
}
public XmlBuilder addDeclTagStart(String declTagName) {
append("<!" + declTagName);
return this;
}
public XmlBuilder startDoctype() {
append("<!DOCTYPE");
return this;
}
public XmlBuilder startDTDElementDecl() {
append("<!ELEMENT");
return this;
}
public XmlBuilder startDTDAttlistDecl() {
append("<!ATTLIST");
return this;
}
public XmlBuilder addParameter(String parameter) {
return addUnindentedParameter(" " + replaceQuotesIfNeeded(parameter));
}
public XmlBuilder addUnindentedParameter(String parameter) {
append(replaceQuotesIfNeeded(parameter));
return this;
}
public XmlBuilder startDoctypeInternalSubset() {
append(" [");
return this;
}
public XmlBuilder startUnindentedDoctypeInternalSubset() {
append("[");
return this;
}
public XmlBuilder endDoctypeInternalSubset() {
append("]");
return this;
}
public XmlBuilder endComment() {
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);
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
/*
* 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("</").append(name).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();
}
}
+1
View File
@@ -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)
@@ -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() {
@@ -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!!)
}
@@ -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 {
@@ -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<String, String>()
/**
* 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)
}
@@ -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()
}
/**
@@ -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) {
@@ -22,7 +22,7 @@ internal fun simpleMaterial3Theme(themeName: String) : String {
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.${themeName}" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- Customize your theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>