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)