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) + } +}