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("" + n.Data)
+ io.WriteString(w, "" + n.Data)
default:
indent.Open()
if n.Prefix == "" {
- b.WriteString("<" + n.Data)
+ io.WriteString(w, "<" + n.Data)
} else {
- fmt.Fprintf(b, "<%s:%s", n.Prefix, n.Data)
+ fmt.Fprintf(w, "<%s:%s", n.Prefix, n.Data)
}
}
for _, attr := range n.Attr {
if attr.Name.Space != "" {
- fmt.Fprintf(b, ` %s:%s=`, attr.Name.Space, attr.Name.Local)
+ fmt.Fprintf(w, ` %s:%s=`, attr.Name.Space, attr.Name.Local)
} else {
- fmt.Fprintf(b, ` %s=`, attr.Name.Local)
+ fmt.Fprintf(w, ` %s=`, attr.Name.Local)
}
- b.WriteByte('"')
- b.WriteString(html.EscapeString(attr.Value))
- b.WriteByte('"')
+
+ fmt.Fprintf(w, `"%v"`, html.EscapeString(attr.Value))
}
if n.Type == 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, "%s>", n.Data)
+ fmt.Fprintf(w, "%s>", n.Data)
} else {
- fmt.Fprintf(b, "%s:%s>", n.Prefix, n.Data)
+ fmt.Fprintf(w, "%s:%s>", 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 := `
+ 0 `
+
+ 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, "0\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))