diff --git a/node.go b/node.go index b7a4aae..03e0ce9 100644 --- a/node.go +++ b/node.go @@ -1,9 +1,11 @@ package xmlquery import ( + "bufio" "encoding/xml" "fmt" "html" + "io" "strings" ) @@ -153,16 +155,16 @@ type indentation struct { level int hasChild bool indent string - b *strings.Builder + w io.Writer } -func newIndentation(indent string, b *strings.Builder) *indentation { +func newIndentation(indent string, w io.Writer) *indentation { if indent == "" { return nil } return &indentation{ indent: indent, - b: b, + w: w, } } @@ -170,15 +172,17 @@ func (i *indentation) NewLine() { if i == nil { return } - i.b.WriteString("\n") + io.WriteString(i.w, "\n") } func (i *indentation) Open() { if i == nil { return } - i.b.WriteString("\n") - i.b.WriteString(strings.Repeat(i.indent, i.level)) + + io.WriteString(i.w, "\n") + io.WriteString(i.w, strings.Repeat(i.indent, i.level)) + i.level++ i.hasChild = false } @@ -189,102 +193,103 @@ func (i *indentation) Close() { } i.level-- if i.hasChild { - i.b.WriteString("\n") - i.b.WriteString(strings.Repeat(i.indent, i.level)) + io.WriteString(i.w, "\n") + io.WriteString(i.w, strings.Repeat(i.indent, i.level)) } i.hasChild = true } -func outputXML(b *strings.Builder, n *Node, preserveSpaces bool, config *outputConfiguration, indent *indentation) { +func outputXML(w io.Writer, n *Node, preserveSpaces bool, config *outputConfiguration, indent *indentation) { preserveSpaces = calculatePreserveSpaces(n, preserveSpaces) switch n.Type { case TextNode: - b.WriteString(html.EscapeString(n.sanitizedData(preserveSpaces))) + io.WriteString(w, html.EscapeString(n.sanitizedData(preserveSpaces))) return case CharDataNode: - b.WriteString("") + io.WriteString(w, "") return case CommentNode: if !config.skipComments { - b.WriteString("") + io.WriteString(w, "") } return case NotationNode: indent.NewLine() - fmt.Fprintf(b, "", n.Data) + fmt.Fprintf(w, "", n.Data) return case DeclarationNode: - b.WriteString("") + io.WriteString(w, "?>") } else { if n.FirstChild != nil || !config.emptyElementTagSupport { - b.WriteString(">") + io.WriteString(w, ">") } else { - b.WriteString("/>") + io.WriteString(w, "/>") indent.Close() return } } for child := n.FirstChild; child != nil; child = child.NextSibling { - outputXML(b, child, preserveSpaces, config, indent) + outputXML(w, child, preserveSpaces, config, indent) } if n.Type != DeclarationNode { indent.Close() if n.Prefix == "" { - fmt.Fprintf(b, "", n.Data) + fmt.Fprintf(w, "", n.Data) } else { - fmt.Fprintf(b, "", n.Prefix, n.Data) + fmt.Fprintf(w, "", n.Prefix, n.Data) } } } // OutputXML returns the text that including tags name. func (n *Node) OutputXML(self bool) string { - - config := &outputConfiguration{ - printSelf: true, - emptyElementTagSupport: false, + if self { + return n.OutputXMLWithOptions(WithOutputSelf()) } - preserveSpaces := calculatePreserveSpaces(n, false) - var b strings.Builder - if self && n.Type != DocumentNode { - outputXML(&b, n, preserveSpaces, config, newIndentation(config.useIndentation, &b)) - } else { - for n := n.FirstChild; n != nil; n = n.NextSibling { - outputXML(&b, n, preserveSpaces, config, newIndentation(config.useIndentation, &b)) - } - } - - return b.String() + return n.OutputXMLWithOptions() } // OutputXMLWithOptions returns the text that including tags name. func (n *Node) OutputXMLWithOptions(opts ...OutputOption) string { + var b strings.Builder + n.WriteWithOptions(&b, opts...) + return b.String() +} +// Write writes xml to given writer. +func (n *Node) Write(writer io.Writer, self bool) { + if self { + n.WriteWithOptions(writer, WithOutputSelf()) + } + n.WriteWithOptions(writer) +} + +// WriteWithOptions writes xml with given options to given writer. +func (n *Node) WriteWithOptions(writer io.Writer, opts ...OutputOption) { config := &outputConfiguration{} // Set the options for _, opt := range opts { @@ -292,16 +297,16 @@ func (n *Node) OutputXMLWithOptions(opts ...OutputOption) string { } pastPreserveSpaces := config.preserveSpaces preserveSpaces := calculatePreserveSpaces(n, pastPreserveSpaces) - var b strings.Builder + b := bufio.NewWriter(writer) + defer b.Flush() + if config.printSelf && n.Type != DocumentNode { - outputXML(&b, n, preserveSpaces, config, newIndentation(config.useIndentation, &b)) + outputXML(b, n, preserveSpaces, config, newIndentation(config.useIndentation, b)) } else { for n := n.FirstChild; n != nil; n = n.NextSibling { - outputXML(&b, n, preserveSpaces, config, newIndentation(config.useIndentation, &b)) + outputXML(b, n, preserveSpaces, config, newIndentation(config.useIndentation, b)) } } - - return b.String() } // AddAttr adds a new attribute specified by 'key' and 'val' to a node 'n'. diff --git a/node_test.go b/node_test.go index 0c571ef..1621766 100644 --- a/node_test.go +++ b/node_test.go @@ -343,8 +343,7 @@ func TestSelectElement(t *testing.T) { t.Fatalf("n is nil") } - var ns []*Node - ns = aaa.SelectElements("CCC") + ns := aaa.SelectElements("CCC") if len(ns) != 2 { t.Fatalf("len(ns)!=2") } @@ -365,6 +364,23 @@ func TestEscapeOutputValue(t *testing.T) { } +func TestEscapeValueWrite(t *testing.T) { + data := `<*>` + + root, err := Parse(strings.NewReader(data)) + if err != nil { + t.Error(err) + } + + var b strings.Builder + root.Write(&b, true) + escapedInnerText := b.String() + if !strings.Contains(escapedInnerText, "<*>") { + t.Fatal("Inner Text has not been escaped") + } + +} + func TestUnnecessaryEscapeOutputValue(t *testing.T) { data := ` @@ -391,6 +407,34 @@ func TestUnnecessaryEscapeOutputValue(t *testing.T) { } +func TestUnnecessaryEscapeValueWrite(t *testing.T) { + data := ` + + + Robert + A+ + + + ` + + root, err := Parse(strings.NewReader(data)) + if err != nil { + t.Error(err) + } + + var b strings.Builder + root.Write(&b, true) + escapedInnerText := b.String() + if strings.Contains(escapedInnerText, " ") { + t.Fatal("\\n has been escaped unnecessarily") + } + + if strings.Contains(escapedInnerText, " ") { + t.Fatal("\\t has been escaped unnecessarily") + } + +} + func TestHtmlUnescapeStringOriginString(t *testing.T) { // has escape html character and \t data := ` @@ -412,6 +456,29 @@ func TestHtmlUnescapeStringOriginString(t *testing.T) { } +func TestHtmlUnescapeStringOriginStringWrite(t *testing.T) { + // has escape html character and \t + data := ` + &#48; ` + + root, err := Parse(strings.NewReader(data)) + if err != nil { + t.Error(err) + } + + var b strings.Builder + root.Write(&b, false) + escapedInnerText := b.String() + unescapeString := html.UnescapeString(escapedInnerText) + if strings.Contains(unescapeString, "&") { + t.Fatal("& need unescape") + } + if !strings.Contains(escapedInnerText, "&#48;\t\t") { + t.Fatal("Inner Text should keep plain text") + } + +} + func TestOutputXMLWithNamespacePrefix(t *testing.T) { s := `` doc, _ := Parse(strings.NewReader(s)) @@ -420,6 +487,17 @@ func TestOutputXMLWithNamespacePrefix(t *testing.T) { } } +func TestWriteWithNamespacePrefix(t *testing.T) { + s := `` + doc, _ := Parse(strings.NewReader(s)) + var b strings.Builder + doc.Write(&b, false) + if s != b.String() { + t.Fatal("xml document missing some characters") + } +} + + func TestQueryWithPrefix(t *testing.T) { s := `ns2:ClientThis is a client fault` doc, _ := Parse(strings.NewReader(s))