diff --git a/marshal.go b/marshal.go
index edf03e3..c106051 100644
--- a/marshal.go
+++ b/marshal.go
@@ -211,8 +211,13 @@ func (enc *Encoder) EncodeToken(t Token) error {
p := &enc.p
switch t := t.(type) {
+ case SelfClosingElement:
+ if err := p.writeStart((*StartElement)(&t), true); err != nil {
+ return err
+ }
+ p.writeIndent(-1)
case StartElement:
- if err := p.writeStart(&t); err != nil {
+ if err := p.writeStart(&t, false); err != nil {
return err
}
case EndElement:
@@ -593,7 +598,16 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo, startTemplat
start.Attr = append(start.Attr, Attr{Name{Space: "", Local: xmlnsPrefix}, ""})
}
- if err := p.writeStart(&start); err != nil {
+ // If this is a self-closing tag, write the tag and return.
+ if finfo != nil && finfo.flags&fSelfClosing != 0 ||
+ tinfo.xmlname != nil && tinfo.xmlname.flags&fSelfClosing != 0 {
+ if err := p.writeStart(&start, true); err != nil {
+ return err
+ }
+ return p.cachedWriteError()
+ }
+
+ if err := p.writeStart(&start, false); err != nil {
return err
}
@@ -748,7 +762,7 @@ func (p *printer) marshalInterface(val Marshaler, start StartElement) error {
// marshalTextInterface marshals a TextMarshaler interface value.
func (p *printer) marshalTextInterface(val encoding.TextMarshaler, start StartElement) error {
- if err := p.writeStart(&start); err != nil {
+ if err := p.writeStart(&start, false); err != nil {
return err
}
text, err := val.MarshalText()
@@ -760,7 +774,8 @@ func (p *printer) marshalTextInterface(val encoding.TextMarshaler, start StartEl
}
// writeStart writes the given start element.
-func (p *printer) writeStart(start *StartElement) error {
+// If close is true, it is written as a self-closing element.
+func (p *printer) writeStart(start *StartElement, close bool) error {
if start.Name.Local == "" {
return fmt.Errorf("xml: start tag with no name")
}
@@ -858,7 +873,13 @@ func (p *printer) writeStart(start *StartElement) error {
p.EscapeString(attr.Value)
p.WriteByte('"')
}
- p.WriteByte('>')
+ if close {
+ p.WriteString("/>")
+ // Pop elements stack
+ p.elements = p.elements[:len(p.elements)-1]
+ } else {
+ p.WriteByte('>')
+ }
return nil
}
@@ -1217,7 +1238,7 @@ func (s *parentStack) trim(parents []string) error {
// push adds parent elements to the stack and writes open tags.
func (s *parentStack) push(parents []string) error {
for i := 0; i < len(parents); i++ {
- if err := s.p.writeStart(&StartElement{Name: Name{Local: parents[i]}}); err != nil {
+ if err := s.p.writeStart(&StartElement{Name: Name{Local: parents[i]}}, false); err != nil {
return err
}
}
diff --git a/marshal_test.go b/marshal_test.go
index 28dfa1c..506b02e 100644
--- a/marshal_test.go
+++ b/marshal_test.go
@@ -530,9 +530,17 @@ type Generic[T any] struct {
type EPP struct {
XMLName struct{} `xml:"urn:ietf:params:xml:ns:epp-1.0 epp"`
+ Hello *Hello `xml:"hello,omitempty,selfclosing"`
Command *Command `xml:"command,omitempty"`
}
+type Hello struct{}
+
+type Closer struct {
+ XMLName struct{} `xml:"closer,selfclosing"`
+ Beverage string `xml:"beverage,attr,omitempty"`
+}
+
type Command struct {
Check *Check `xml:"urn:ietf:params:xml:ns:epp-1.0 check,omitempty"`
}
@@ -1698,11 +1706,25 @@ var marshalTests = []struct {
UnmarshalOnly: true,
},
+ // Test self-closing tags
+ {
+ ExpectXML: ``,
+ Value: &Closer{},
+ },
+ {
+ ExpectXML: ``,
+ Value: &Closer{Beverage: "coffee"},
+ },
+
// Test namespace prefixes
{
ExpectXML: ``,
Value: &EPP{},
},
+ {
+ ExpectXML: ``,
+ Value: &EPP{Hello: &Hello{}},
+ },
{
ExpectXML: ``,
Value: &EPP{Command: &Command{}},
@@ -2065,6 +2087,22 @@ var encodeTokenTests = []struct {
StartElement{Name{"space", "local"}, nil},
},
want: ``,
+}, {
+ desc: "self-closing element with namespace",
+ toks: []Token{
+ SelfClosingElement{Name{"space", "local"}, nil},
+ },
+ want: ``,
+}, {
+ desc: "self-closing elements inside other elements",
+ toks: []Token{
+ StartElement{Name{"", "outer"}, nil},
+ SelfClosingElement{Name{"", "a"}, nil},
+ SelfClosingElement{Name{"", "b"}, nil},
+ SelfClosingElement{Name{"", "c"}, nil},
+ EndElement{Name{"", "outer"}},
+ },
+ want: ``,
}, {
desc: "start element with no name",
toks: []Token{
diff --git a/typeinfo.go b/typeinfo.go
index 9a98592..9d4ed4d 100644
--- a/typeinfo.go
+++ b/typeinfo.go
@@ -40,6 +40,8 @@ const (
fOmitEmpty
+ fSelfClosing
+
fMode = fElement | fAttr | fCDATA | fCharData | fInnerXML | fComment | fAny
xmlName = "XMLName"
@@ -140,6 +142,8 @@ func structFieldInfo(typ reflect.Type, f *reflect.StructField) (*fieldInfo, erro
finfo.flags |= fAny
case "omitempty":
finfo.flags |= fOmitEmpty
+ case "selfclosing":
+ finfo.flags |= fSelfClosing
}
}
@@ -162,6 +166,9 @@ func structFieldInfo(typ reflect.Type, f *reflect.StructField) (*fieldInfo, erro
if finfo.flags&fOmitEmpty != 0 && finfo.flags&(fElement|fAttr) == 0 {
valid = false
}
+ if finfo.flags&fSelfClosing != 0 && finfo.flags&fElement == 0 {
+ valid = false
+ }
if !valid {
return nil, fmt.Errorf("xml: invalid tag in field %s of type %s: %q",
f.Name, typ, f.Tag.Get("xml"))
diff --git a/xml.go b/xml.go
index 6dfafb2..2e1b720 100644
--- a/xml.go
+++ b/xml.go
@@ -73,6 +73,18 @@ func (e StartElement) End() EndElement {
return EndElement{e.Name}
}
+// A SelfClosingElement represents a self-closing XML element.
+// It is otherwise identical to StartElement.
+type SelfClosingElement StartElement
+
+// Copy creates a new copy of SelfClosingElement.
+func (e SelfClosingElement) Copy() SelfClosingElement {
+ attrs := make([]Attr, len(e.Attr))
+ copy(attrs, e.Attr)
+ e.Attr = attrs
+ return e
+}
+
// An EndElement represents an XML end element.
type EndElement struct {
Name Name
diff --git a/xml_test.go b/xml_test.go
index 8632960..ed01b4e 100644
--- a/xml_test.go
+++ b/xml_test.go
@@ -2031,3 +2031,33 @@ func TestHTMLAutoClose(t *testing.T) {
}
}
}
+
+// PR #20
+// Indentation not correct after self closing element
+func TestSelfClosingIndentation(t *testing.T) {
+ must := func(err error, msg string) {
+ if err != nil {
+ t.Errorf("%s: error = %v", msg, err)
+ }
+ }
+ // Arrange
+ writer := strings.Builder{}
+ encoder := NewEncoder(&writer)
+ encoder.Indent("", " ")
+
+ // Act
+ must(encoder.EncodeToken(StartElement{Name: Name{Local: "RootElement"}}), "start root element")
+ must(encoder.EncodeToken(SelfClosingElement{Name: Name{Local: "Child1"}}), "child1")
+ must(encoder.EncodeToken(SelfClosingElement{Name: Name{Local: "Child2"}}), "child2")
+ must(encoder.EncodeToken(EndElement{Name: Name{Local: "RootElement"}}), "end root element")
+ must(encoder.Flush(), "flush")
+
+ // Assert
+ expected := `
+
+
+`
+ if writer.String() != expected {
+ t.Errorf("output mismatch:\nhave: %#v\nwant: %#v", writer.String(), expected)
+ }
+}