Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 87 additions & 18 deletions marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand All @@ -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('/')
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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')
Expand All @@ -1185,8 +1255,7 @@ func (p *printer) writeIndent(depthDelta int) {
}
}
if depthDelta > 0 {
p.depth++
p.indentedIn = true
p.writeIndentIncrement()
}
}

Expand Down
42 changes: 41 additions & 1 deletion marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -750,6 +751,25 @@ var marshalTests = []struct {
`</result>`,
MarshalOnly: true,
},
{
Value: &NestedItems{Items: []string{"abc", ""}, Item1: []string{}},
ExpectXML: `<result>` +
`<Items>` +
`<item>abc</item>` +
`<item/>` +
`</Items>` +
`</result>`,
MarshalOnly: true,
Selfclosing: true,
},
{
Value: &NestedItems{Items: []string{}, Item1: []string{}},
ExpectXML: `<result>` +
`<Items/>` +
`</result>`,
MarshalOnly: true,
Selfclosing: true,
},
{
Value: &NestedItems{Items: nil, Item1: []string{"A"}},
ExpectXML: `<result>` +
Expand Down Expand Up @@ -1284,6 +1304,16 @@ var marshalTests = []struct {
ExpectXML: `<Strings><A></A></Strings>`,
Value: &Strings{},
},
{
ExpectXML: `<Strings><A/></Strings>`,
Value: &Strings{},
Selfclosing: true,
},
{
ExpectXML: `<Strings><A><B>abc</B></A></Strings>`,
Value: &Strings{X: []string{"abc"}},
Selfclosing: true,
},
// Custom marshalers.
{
ExpectXML: `<MyMarshalerTest>hello world</MyMarshalerTest>`,
Expand Down Expand Up @@ -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)
Expand Down