diff --git a/marshal.go b/marshal.go
index edf03e3..6c3c36a 100644
--- a/marshal.go
+++ b/marshal.go
@@ -140,6 +140,21 @@ func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return b.Bytes(), nil
}
+// MarshalSelfClosing works like [Marshal], but any empty xml tags are
+// rendered as self-closing tags.
+func MarshalSelfClosing(v any) ([]byte, error) {
+ var b bytes.Buffer
+ enc := NewEncoder(&b)
+ enc.SelfClosing(true)
+ if err := enc.Encode(v); err != nil {
+ return nil, err
+ }
+ if err := enc.Close(); err != nil {
+ return nil, err
+ }
+ return b.Bytes(), nil
+}
+
// An Encoder writes XML data to an output stream.
type Encoder struct {
p printer
@@ -160,6 +175,12 @@ func (enc *Encoder) Indent(prefix, indent string) {
enc.p.indent = indent
}
+// SelfClosing sets the encoder to generate XML in which each element
+// with no content will be rendered as self-closing.
+func (enc *Encoder) SelfClosing(selfclosing bool) {
+ enc.p.selfclosing = selfclosing
+}
+
// Encode writes the XML encoding of v to the stream.
//
// See the documentation for [Marshal] for details about the conversion
@@ -351,17 +372,19 @@ func joinPrefixed(prefix, name string) string {
}
type printer struct {
- w *bufio.Writer
- encoder *Encoder
- seq int
- indent string
- prefix string
- depth int
- indentedIn bool
- putNewline bool
- elements []element
- closed bool
- err error
+ w *bufio.Writer
+ encoder *Encoder
+ seq int
+ indent string
+ prefix string
+ depth int
+ indentedIn bool
+ putNewline bool
+ elements []element
+ closed bool
+ err error
+ selfclose *StartElement
+ selfclosing bool
}
// getPrefix finds the prefix to use for the given namespace URI, but does not create it.
@@ -858,7 +881,11 @@ func (p *printer) writeStart(start *StartElement) error {
p.EscapeString(attr.Value)
p.WriteByte('"')
}
- p.WriteByte('>')
+ if p.selfclosing {
+ p.selfclose = start
+ } else {
+ p.WriteByte('>')
+ }
return nil
}
@@ -883,6 +910,21 @@ func (p *printer) writeEnd(name Name) error {
if name.Space != e.xmlns {
return fmt.Errorf("xml: end namespace %q does not match start namespace %q", name.Space, e.xmlns)
}
+
+ if p.selfclose != nil && p.selfclose.Name == name {
+ p.selfclose = nil
+
+ p.WriteByte('/')
+ p.WriteByte('>')
+
+ // Pop elements stack
+ p.elements = p.elements[:len(p.elements)-1]
+
+ p.writeIndentDecrement()
+
+ return nil
+ }
+
p.writeIndent(-1)
p.WriteByte('<')
p.WriteByte('/')
@@ -1109,6 +1151,11 @@ func (p *printer) Write(b []byte) (n int, err error) {
p.err = errors.New("use of closed Encoder")
}
if p.err == nil {
+ if p.selfclose != nil && len(b) > 0 {
+ p.selfclose = nil
+ p.w.WriteByte('>')
+ }
+
n, p.err = p.w.Write(b)
}
return n, p.err
@@ -1120,6 +1167,11 @@ func (p *printer) WriteString(s string) (n int, err error) {
p.err = errors.New("use of closed Encoder")
}
if p.err == nil {
+ if p.selfclose != nil && len(s) > 0 {
+ p.selfclose = nil
+ p.w.WriteByte('>')
+ }
+
n, p.err = p.w.WriteString(s)
}
return n, p.err
@@ -1131,6 +1183,11 @@ func (p *printer) WriteByte(c byte) error {
p.err = errors.New("use of closed Encoder")
}
if p.err == nil {
+ if p.selfclose != nil {
+ p.selfclose = nil
+ p.w.WriteByte('>')
+ }
+
p.err = p.w.WriteByte(c)
}
return p.err
@@ -1159,17 +1216,30 @@ func (p *printer) cachedWriteError() error {
return err
}
+func (p *printer) writeIndentDecrement() bool {
+ p.depth--
+ if p.indentedIn {
+ p.indentedIn = false
+ return true
+ }
+ p.indentedIn = false
+
+ return false
+}
+
+func (p *printer) writeIndentIncrement() {
+ p.depth++
+ p.indentedIn = true
+}
+
func (p *printer) writeIndent(depthDelta int) {
if len(p.prefix) == 0 && len(p.indent) == 0 {
return
}
if depthDelta < 0 {
- p.depth--
- if p.indentedIn {
- p.indentedIn = false
+ if p.writeIndentDecrement() {
return
}
- p.indentedIn = false
}
if p.putNewline {
p.WriteByte('\n')
@@ -1185,8 +1255,7 @@ func (p *printer) writeIndent(depthDelta int) {
}
}
if depthDelta > 0 {
- p.depth++
- p.indentedIn = true
+ p.writeIndentIncrement()
}
}
diff --git a/marshal_test.go b/marshal_test.go
index 28dfa1c..e0a4393 100644
--- a/marshal_test.go
+++ b/marshal_test.go
@@ -578,6 +578,7 @@ var marshalTests = []struct {
MarshalError string
UnmarshalOnly bool
UnmarshalError string
+ Selfclosing bool
}{
// Test nil marshals to nothing
{Value: nil, ExpectXML: ``, MarshalOnly: true},
@@ -750,6 +751,25 @@ var marshalTests = []struct {
``,
MarshalOnly: true,
},
+ {
+ Value: &NestedItems{Items: []string{"abc", ""}, Item1: []string{}},
+ ExpectXML: `` +
+ `` +
+ `- abc
` +
+ ` ` +
+ `` +
+ ``,
+ MarshalOnly: true,
+ Selfclosing: true,
+ },
+ {
+ Value: &NestedItems{Items: []string{}, Item1: []string{}},
+ ExpectXML: `` +
+ `` +
+ ``,
+ MarshalOnly: true,
+ Selfclosing: true,
+ },
{
Value: &NestedItems{Items: nil, Item1: []string{"A"}},
ExpectXML: `` +
@@ -1284,6 +1304,16 @@ var marshalTests = []struct {
ExpectXML: ``,
Value: &Strings{},
},
+ {
+ ExpectXML: ``,
+ Value: &Strings{},
+ Selfclosing: true,
+ },
+ {
+ ExpectXML: `abc`,
+ Value: &Strings{X: []string{"abc"}},
+ Selfclosing: true,
+ },
// Custom marshalers.
{
ExpectXML: `hello world`,
@@ -1759,7 +1789,17 @@ func TestMarshal(t *testing.T) {
}
t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
- data, err := Marshal(test.Value)
+ var (
+ data []byte
+ err error
+ )
+
+ if test.Selfclosing {
+ data, err = MarshalSelfClosing(test.Value)
+ } else {
+ data, err = Marshal(test.Value)
+ }
+
if err != nil {
if test.MarshalError == "" {
t.Errorf("marshal(%#v): %s", test.Value, err)