Skip to content

Commit

Permalink
Token WriteTo and indentation modifications
Browse files Browse the repository at this point in the history
Rename etree.XMLWriter to etree.Writer.

Add unit tests for new WriteTo feature.

Add Element.IndentWithSettings to make it possible to re-indent
an element before calling its WriteTo function.

Rename IndentSettings property SuppressTrailingNewline to
SuppressTrailingWhitespace. This is more accurate terminology.
  • Loading branch information
beevik committed May 11, 2023
1 parent a291eec commit ecaabe8
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 104 deletions.
218 changes: 119 additions & 99 deletions etree.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,6 @@ type WriteSettings struct {
UseCRLF bool
}

// XMLWriter is a Writer that also has convenience methods for writing
// strings an single bytes.
type XMLWriter interface {
io.StringWriter
io.ByteWriter
io.Writer
}

// newWriteSettings creates a default WriteSettings record.
func newWriteSettings() WriteSettings {
return WriteSettings{
Expand Down Expand Up @@ -152,29 +144,58 @@ type IndentSettings struct {
// false.
PreserveLeafWhitespace bool

// SuppressTrailingNewline suppresses the generation of a trailing newline
// character at the end of the indented document. Default: false.
SuppressTrailingNewline bool
// SuppressTrailingWhitespace suppresses the generation of a trailing
// whitespace characters (such as newlines) at the end of the indented
// document. Default: false.
SuppressTrailingWhitespace bool
}

// NewIndentSettings creates a default IndentSettings record.
func NewIndentSettings() IndentSettings {
return IndentSettings{
Spaces: 4,
UseTabs: false,
UseCRLF: false,
PreserveLeafWhitespace: false,
SuppressTrailingNewline: false,
func NewIndentSettings() *IndentSettings {
return &IndentSettings{
Spaces: 4,
UseTabs: false,
UseCRLF: false,
PreserveLeafWhitespace: false,
SuppressTrailingWhitespace: false,
}
}

type indentFunc func(depth int) string

func getIndentFunc(s *IndentSettings) indentFunc {
if s.UseTabs {
if s.UseCRLF {
return func(depth int) string { return indentCRLF(depth, indentTabs) }
} else {
return func(depth int) string { return indentLF(depth, indentTabs) }
}
} else {
if s.Spaces < 0 {
return func(depth int) string { return "" }
} else if s.UseCRLF {
return func(depth int) string { return indentCRLF(depth*s.Spaces, indentSpaces) }
} else {
return func(depth int) string { return indentLF(depth*s.Spaces, indentSpaces) }
}
}
}

// Writer is the interface that wraps the Write* methods called by each token
// type's WriteTo function.
type Writer interface {
io.StringWriter
io.ByteWriter
io.Writer
}

// A Token is an interface type used to represent XML elements, character
// data, CDATA sections, XML comments, XML directives, and XML processing
// instructions.
type Token interface {
Parent() *Element
Index() int
WriteTo(w XMLWriter, s *WriteSettings)
WriteTo(w Writer, s *WriteSettings)
dup(parent *Element) Token
setParent(parent *Element)
setIndex(index int)
Expand Down Expand Up @@ -390,8 +411,6 @@ func (d *Document) WriteToString() (s string, err error) {
return string(b), nil
}

type indentFunc func(depth int) string

// Indent modifies the document's element tree by inserting character data
// tokens containing newlines and spaces for indentation. The amount of
// indentation per depth level is given by the 'spaces' parameter. Other than
Expand All @@ -415,37 +434,17 @@ func (d *Document) IndentTabs() {
// IndentWithSettings modifies the document's element tree by inserting
// character data tokens containing newlines and indentation. The behavior
// of the indentation algorithm is configured by the indent settings.
func (d *Document) IndentWithSettings(s IndentSettings) {
func (d *Document) IndentWithSettings(s *IndentSettings) {
// WriteSettings.UseCRLF is deprecated. Until removed from the package, it
// overrides IndentSettings.UseCRLF when true.
if d.WriteSettings.UseCRLF {
s.UseCRLF = true
}

var indent indentFunc
if s.UseTabs {
if s.UseCRLF {
indent = func(depth int) string { return indentCRLF(depth, indentTabs) }
} else {
indent = func(depth int) string { return indentLF(depth, indentTabs) }
}
} else {
if s.Spaces < 0 {
indent = func(depth int) string { return "" }
} else if s.UseCRLF {
indent = func(depth int) string { return indentCRLF(depth*s.Spaces, indentSpaces) }
} else {
indent = func(depth int) string { return indentLF(depth*s.Spaces, indentSpaces) }
}
}
d.Element.indent(0, getIndentFunc(s), s)

d.Element.indent(0, indent, &s)

if s.SuppressTrailingNewline && len(d.Element.Child) > 0 {
n := len(d.Element.Child) - 1
if cd, ok := d.Element.Child[n].(*CharData); ok && (cd.flags&whitespaceFlag) != 0 {
d.Element.Child = d.Element.Child[:n]
}
if s.SuppressTrailingWhitespace {
d.Element.stripTrailingWhitespace()
}
}

Expand Down Expand Up @@ -1047,6 +1046,16 @@ func (e *Element) GetRelativePath(source *Element) string {
return strings.Join(parts, "/")
}

// IndentWithSettings modifies the element and its child tree by inserting
// character data tokens containing newlines and indentation. The behavior of
// the indentation algorithm is configured by the indent settings. Because
// this function indents the element as if it were at the root of a document,
// it is most useful when called just before writing the element as an XML
// fragment using WriteTo.
func (e *Element) IndentWithSettings(s *IndentSettings) {
e.indent(1, getIndentFunc(s), s)
}

// indent recursively inserts proper indentation between an XML element's
// child tokens.
func (e *Element) indent(depth int, indent indentFunc, s *IndentSettings) {
Expand Down Expand Up @@ -1123,6 +1132,17 @@ func (e *Element) stripIndent(s *IndentSettings) {
e.Child = newChild
}

// stripTrailingWhitespace removes any trailing whitespace CharData tokens
// from the element's children.
func (e *Element) stripTrailingWhitespace() {
for i := len(e.Child) - 1; i >= 0; i-- {
if cd, ok := e.Child[i].(*CharData); !ok || !cd.IsWhitespace() {
e.Child = e.Child[:i+1]
return
}
}
}

// dup duplicates the element.
func (e *Element) dup(parent *Element) Token {
ne := &Element{
Expand Down Expand Up @@ -1153,18 +1173,8 @@ func (e *Element) Index() int {
return e.index
}

// setParent replaces this element token's parent.
func (e *Element) setParent(parent *Element) {
e.parent = parent
}

// setIndex sets this element token's index within its parent's Child slice.
func (e *Element) setIndex(index int) {
e.index = index
}

// WriteTo serializes the element to the writer w.
func (e *Element) WriteTo(w XMLWriter, s *WriteSettings) {
func (e *Element) WriteTo(w Writer, s *WriteSettings) {
w.WriteByte('<')
w.WriteString(e.FullTag())
for _, a := range e.Attr {
Expand All @@ -1190,6 +1200,16 @@ func (e *Element) WriteTo(w XMLWriter, s *WriteSettings) {
}
}

// setParent replaces this element token's parent.
func (e *Element) setParent(parent *Element) {
e.parent = parent
}

// setIndex sets this element token's index within its parent's Child slice.
func (e *Element) setIndex(index int) {
e.index = index
}

// addChild adds a child token to the element e.
func (e *Element) addChild(t Token) {
t.setParent(e)
Expand Down Expand Up @@ -1292,7 +1312,7 @@ func (a *Attr) NamespaceURI() string {
}

// WriteTo serializes the attribute to the writer.
func (a *Attr) WriteTo(w XMLWriter, s *WriteSettings) {
func (a *Attr) WriteTo(w Writer, s *WriteSettings) {
w.WriteString(a.FullKey())
if s.AttrSingleQuote {
w.WriteString(`='`)
Expand Down Expand Up @@ -1407,6 +1427,23 @@ func (c *CharData) Index() int {
return c.index
}

// WriteTo serializes character data to the writer.
func (c *CharData) WriteTo(w Writer, s *WriteSettings) {
if c.IsCData() {
w.WriteString(`<![CDATA[`)
w.WriteString(c.Data)
w.WriteString(`]]>`)
} else {
var m escapeMode
if s.CanonicalText {
m = escapeCanonicalText
} else {
m = escapeNormal
}
escapeString(w, c.Data, m)
}
}

// dup duplicates the character data.
func (c *CharData) dup(parent *Element) Token {
return &CharData{
Expand All @@ -1428,23 +1465,6 @@ func (c *CharData) setIndex(index int) {
c.index = index
}

// WriteTo serializes character data to the writer.
func (c *CharData) WriteTo(w XMLWriter, s *WriteSettings) {
if c.IsCData() {
w.WriteString(`<![CDATA[`)
w.WriteString(c.Data)
w.WriteString(`]]>`)
} else {
var m escapeMode
if s.CanonicalText {
m = escapeCanonicalText
} else {
m = escapeNormal
}
escapeString(w, c.Data, m)
}
}

// NewComment creates an unparented comment token.
func NewComment(comment string) *Comment {
return newComment(comment, nil)
Expand Down Expand Up @@ -1490,6 +1510,13 @@ func (c *Comment) Index() int {
return c.index
}

// WriteTo serialies the comment to the writer.
func (c *Comment) WriteTo(w Writer, s *WriteSettings) {
w.WriteString("<!--")
w.WriteString(c.Data)
w.WriteString("-->")
}

// setParent replaces the comment token's parent.
func (c *Comment) setParent(parent *Element) {
c.parent = parent
Expand All @@ -1501,13 +1528,6 @@ func (c *Comment) setIndex(index int) {
c.index = index
}

// WriteTo serialies the comment to the writer.
func (c *Comment) WriteTo(w XMLWriter, s *WriteSettings) {
w.WriteString("<!--")
w.WriteString(c.Data)
w.WriteString("-->")
}

// NewDirective creates an unparented XML directive token.
func NewDirective(data string) *Directive {
return newDirective(data, nil)
Expand Down Expand Up @@ -1555,6 +1575,13 @@ func (d *Directive) Index() int {
return d.index
}

// WriteTo serializes the XML directive to the writer.
func (d *Directive) WriteTo(w Writer, s *WriteSettings) {
w.WriteString("<!")
w.WriteString(d.Data)
w.WriteString(">")
}

// setParent replaces the directive token's parent.
func (d *Directive) setParent(parent *Element) {
d.parent = parent
Expand All @@ -1566,13 +1593,6 @@ func (d *Directive) setIndex(index int) {
d.index = index
}

// WriteTo serializes the XML directive to the writer.
func (d *Directive) WriteTo(w XMLWriter, s *WriteSettings) {
w.WriteString("<!")
w.WriteString(d.Data)
w.WriteString(">")
}

// NewProcInst creates an unparented XML processing instruction.
func NewProcInst(target, inst string) *ProcInst {
return newProcInst(target, inst, nil)
Expand Down Expand Up @@ -1623,6 +1643,17 @@ func (p *ProcInst) Index() int {
return p.index
}

// WriteTo serializes the processing instruction to the writer.
func (p *ProcInst) WriteTo(w Writer, s *WriteSettings) {
w.WriteString("<?")
w.WriteString(p.Target)
if p.Inst != "" {
w.WriteByte(' ')
w.WriteString(p.Inst)
}
w.WriteString("?>")
}

// setParent replaces the processing instruction token's parent.
func (p *ProcInst) setParent(parent *Element) {
p.parent = parent
Expand All @@ -1633,14 +1664,3 @@ func (p *ProcInst) setParent(parent *Element) {
func (p *ProcInst) setIndex(index int) {
p.index = index
}

// WriteTo serializes the processing instruction to the writer.
func (p *ProcInst) WriteTo(w XMLWriter, s *WriteSettings) {
w.WriteString("<?")
w.WriteString(p.Target)
if p.Inst != "" {
w.WriteByte(' ')
w.WriteString(p.Inst)
}
w.WriteString("?>")
}
Loading

0 comments on commit ecaabe8

Please sign in to comment.