Index: src/test/groovy/xml/MarkupBuilderTest.groovy
===================================================================
--- src/test/groovy/xml/MarkupBuilderTest.groovy (revision 4346)
+++ src/test/groovy/xml/MarkupBuilderTest.groovy (working copy)
@@ -15,14 +15,21 @@
* @author Pilho Kim
*/
class MarkupBuilderTest extends GroovyTestCase {
-
- StringWriter writer = new StringWriter()
- MarkupBuilder chars = new MarkupBuilder(writer)
- XmlParser parser = new XmlParser()
+ private StringWriter writer
+ private MarkupBuilder xml
+ protected void setUp() {
+ writer = new StringWriter()
+ xml = new MarkupBuilder(writer)
+ }
+
+ /**
+ * Main test method. Checks that well-formed XML is generated
+ * and that the appropriate characters are escaped with the
+ * correct entities.
+ */
void testBuilder() {
- String expectedXml =
-"""
+ String expectedXml = '''
&
"
'
@@ -30,14 +37,14 @@
chars: & < > " in middle
>
-"""
+'''
// Generate the markup.
- chars.chars {
+ xml.chars {
ampersand(a: "&", "&")
quote(attr: "\"", "\"")
apostrophe(attr: "'", "'")
- lessthan(attr: "value", "chars: & < > '")
+ lessthan(attr: "value", "chars: & < > '")
element(attr: "value 1 & 2", "chars: & < > \" in middle")
greaterthan(">")
emptyElement()
@@ -45,37 +52,99 @@
// Compare the MarkupBuilder generated XML with the 'expectedXml'
// string.
- def outputValue = writer.toString()
- if (expectedXml.indexOf("\r\n") >= 0) expectedXml = expectedXml.replaceAll("\r\n", "\n");
- if (outputValue.indexOf("\r\n") >= 0) outputValue = outputValue.replaceAll("\r\n", "\n");
- assertEquals(expectedXml, outputValue)
+ assertEquals(expectedXml, fixEOLs(writer.toString()))
}
/**
+ * Tests the builder with double quotes for attribute values.
+ */
+ void testBuilderWithDoubleQuotes() {
+ String expectedXml = '''
+ &
+ "
+ '
+ chars: & < > '
+ chars: & < > " in middle
+ >
+
+'''
+
+ // Generate the markup.
+ xml.doubleQuotes = true
+ xml.chars {
+ ampersand(a: "&", "&")
+ quote(attr: "\"", "\"")
+ apostrophe(attr: "'", "'")
+ lessthan(attr: "value", "chars: & < > '")
+ element(attr: "value 1 & 2", "chars: & < > \" in middle")
+ greaterthan(">")
+ emptyElement()
+ }
+
+ // Compare the MarkupBuilder generated XML with the 'expectedXml'
+ // string.
+ assertEquals(expectedXml, fixEOLs(writer.toString()))
+ }
+
+ /**
* Tests that MarkupBuilder escapes element content correctly, even
* when the content contains line-endings.
*/
void testEscapingMultiLineContent() {
def expectedXml =
-"""This is multi-line content with characters, such as <, that
+'''This is multi-line content with characters, such as <, that
require escaping. The other characters consist of:
* > - greater than
* & - ampersand
-"""
+'''
// Generate the markup.
- chars.element("""This is multi-line content with characters, such as <, that
+ xml.element('''This is multi-line content with characters, such as <, that
require escaping. The other characters consist of:
* > - greater than
* & - ampersand
-""")
+''')
// Compare the generated markup with the 'expectedXml' string.
- def outputValue = writer.toString()
- if (expectedXml.indexOf("\r\n") >= 0) expectedXml = expectedXml.replaceAll("\r\n", "\n");
- if (outputValue.indexOf("\r\n") >= 0) outputValue = outputValue.replaceAll("\r\n", "\n");
- assertEquals(expectedXml, outputValue)
+ assertEquals(expectedXml, fixEOLs(writer.toString()))
}
+
+ /**
+ * Checks against a regression bug whereby some empty elements were
+ * not closed.
+ */
+ void testMarkupForClosingTags() {
+ def expectedXml =
+'''
+
+
+ text
+
+
+
+ text
+
+
+
+ text
+
+'''
+
+ // Generate the XML.
+ def list = ['first', 'second', 'third']
+
+ xml.ELEM1() {
+ list.each(){ r ->
+ xml.ELEM2(id:r, type:'2') {
+ xml.ELEM3A(id:r)
+ xml.ELEM3B(type:'3', 'text')
+ }
+ }
+ }
+
+ // Check that the MarkupBuilder has generated the expected XML.
+ assertEquals(expectedXml, fixEOLs(writer.toString()))
+ }
}
Index: src/main/groovy/xml/MarkupBuilder.java
===================================================================
--- src/main/groovy/xml/MarkupBuilder.java (revision 4346)
+++ src/main/groovy/xml/MarkupBuilder.java (working copy)
@@ -66,6 +66,7 @@
private boolean nospace;
private int state;
private boolean nodeIsEmpty = true;
+ private boolean useDoubleQuotes = false;
public MarkupBuilder() {
this(new IndentPrinter());
@@ -83,6 +84,25 @@
this.out = out;
}
+ /**
+ * Returns true if attribute values are output with
+ * double quotes; false if single quotes are used.
+ * By default, single quotes are used.
+ */
+ public boolean getDoubleQuotes() {
+ return this.useDoubleQuotes;
+ }
+
+ /**
+ * Sets whether the builder outputs attribute values in double
+ * quotes or single quotes.
+ * @param useDoubleQuotes If this parameter is true,
+ * double quotes are used; otherwise, single quotes are.
+ */
+ public void setDoubleQuotes(boolean useDoubleQuotes) {
+ this.useDoubleQuotes = useDoubleQuotes;
+ }
+
protected IndentPrinter getPrinter() {
return this.out;
}
@@ -97,6 +117,7 @@
protected Object createNode(Object name, Object value) {
toState(2, name);
+ this.nodeIsEmpty = false;
out.print(">");
out.print(escapeElementContent(value.toString()));
return name;
@@ -107,16 +128,25 @@
for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
Map.Entry entry = (Map.Entry) iter.next();
out.print(" ");
+
+ // Output the attribute name,
print(entry.getKey().toString());
- out.print("='");
+
+ // Output the attribute value within quotes. Use whichever
+ // type of quotes are currently configured.
+ out.print(this.useDoubleQuotes ? "=\"" : "='");
print(escapeAttributeValue(entry.getValue().toString()));
- out.print("'");
+ out.print(this.useDoubleQuotes ? "\"" : "'");
}
- if (value != null)
- {
+
+ if (value != null) {
nodeIsEmpty = false;
out.print(">" + escapeElementContent(value.toString()) + "" + name + ">");
}
+ else {
+ nodeIsEmpty = true;
+ }
+
return name;
}
@@ -134,8 +164,8 @@
}
protected Object getName(String methodName) {
- return super.getName(methodName);
- }
+ return super.getName(methodName);
+ }
/**
* Returns a String with special XML characters escaped as entities so that
@@ -214,10 +244,10 @@
* @return A new string in which all characters that require escaping
* have been replaced with the corresponding XML entities.
*/
- private String escapeXmlValue(String value, boolean isAttrValue){
+ private String escapeXmlValue(String value, boolean isAttrValue) {
StringBuffer buffer = new StringBuffer(value);
- for (int i = 0, n = buffer.length(); i < n; i++){
- switch (buffer.charAt(i)){
+ for (int i = 0, n = buffer.length(); i < n; i++) {
+ switch (buffer.charAt(i)) {
case '&':
buffer.replace(i, i + 1, "&");
@@ -248,10 +278,27 @@
n += 3;
break;
+ case '"':
+ // The double quote is only escaped if the value is for
+ // an attribute and the builder is configured to output
+ // attribute values inside double quotes.
+ if (isAttrValue && this.useDoubleQuotes) {
+ buffer.replace(i, i + 1, """);
+
+ // We're replacing a single character by a string of
+ // length 6, so we need to update the index variable
+ // and the total length.
+ i += 5;
+ n += 5;
+ }
+ break;
+
case '\'':
// The apostrophe is only escaped if the value is for an
- // attribute, as opposed to element content.
- if (isAttrValue){
+ // attribute, as opposed to element content, and if the
+ // builder is configured to surround attribute values with
+ // single quotes.
+ if (isAttrValue && !this.useDoubleQuotes){
buffer.replace(i, i + 1, "'");
// We're replacing a single character by a string of
@@ -347,5 +394,4 @@
}
state = next;
}
-
}