Skip to content
Draft
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
33 changes: 27 additions & 6 deletions marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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()
Expand All @@ -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")
}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
}
Expand Down
38 changes: 38 additions & 0 deletions marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down Expand Up @@ -1698,11 +1706,25 @@ var marshalTests = []struct {
UnmarshalOnly: true,
},

// Test self-closing tags
{
ExpectXML: `<closer/>`,
Value: &Closer{},
},
{
ExpectXML: `<closer beverage="coffee"/>`,
Value: &Closer{Beverage: "coffee"},
},

// Test namespace prefixes
{
ExpectXML: `<epp xmlns="urn:ietf:params:xml:ns:epp-1.0"></epp>`,
Value: &EPP{},
},
{
ExpectXML: `<epp xmlns="urn:ietf:params:xml:ns:epp-1.0"><hello/></epp>`,
Value: &EPP{Hello: &Hello{}},
},
{
ExpectXML: `<epp xmlns="urn:ietf:params:xml:ns:epp-1.0"><command></command></epp>`,
Value: &EPP{Command: &Command{}},
Expand Down Expand Up @@ -2065,6 +2087,22 @@ var encodeTokenTests = []struct {
StartElement{Name{"space", "local"}, nil},
},
want: `<local xmlns="space">`,
}, {
desc: "self-closing element with namespace",
toks: []Token{
SelfClosingElement{Name{"space", "local"}, nil},
},
want: `<local xmlns="space"/>`,
}, {
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: `<outer><a/><b/><c/></outer>`,
}, {
desc: "start element with no name",
toks: []Token{
Expand Down
7 changes: 7 additions & 0 deletions typeinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ const (

fOmitEmpty

fSelfClosing

fMode = fElement | fAttr | fCDATA | fCharData | fInnerXML | fComment | fAny

xmlName = "XMLName"
Expand Down Expand Up @@ -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
}
}

Expand All @@ -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"))
Expand Down
12 changes: 12 additions & 0 deletions xml.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions xml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 := `<RootElement>
<Child1/>
<Child2/>
</RootElement>`
if writer.String() != expected {
t.Errorf("output mismatch:\nhave: %#v\nwant: %#v", writer.String(), expected)
}
}
Loading