Skip to content

Commit 3603825

Browse files
committed
Merge PR #120
2 parents bb12302 + 6ad6686 commit 3603825

File tree

2 files changed

+136
-53
lines changed

2 files changed

+136
-53
lines changed

node.go

+56-51
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package xmlquery
22

33
import (
4+
"bufio"
45
"encoding/xml"
56
"fmt"
67
"html"
8+
"io"
79
"strings"
810
)
911

@@ -153,32 +155,34 @@ type indentation struct {
153155
level int
154156
hasChild bool
155157
indent string
156-
b *strings.Builder
158+
w io.Writer
157159
}
158160

159-
func newIndentation(indent string, b *strings.Builder) *indentation {
161+
func newIndentation(indent string, w io.Writer) *indentation {
160162
if indent == "" {
161163
return nil
162164
}
163165
return &indentation{
164166
indent: indent,
165-
b: b,
167+
w: w,
166168
}
167169
}
168170

169171
func (i *indentation) NewLine() {
170172
if i == nil {
171173
return
172174
}
173-
i.b.WriteString("\n")
175+
io.WriteString(i.w, "\n")
174176
}
175177

176178
func (i *indentation) Open() {
177179
if i == nil {
178180
return
179181
}
180-
i.b.WriteString("\n")
181-
i.b.WriteString(strings.Repeat(i.indent, i.level))
182+
183+
io.WriteString(i.w, "\n")
184+
io.WriteString(i.w, strings.Repeat(i.indent, i.level))
185+
182186
i.level++
183187
i.hasChild = false
184188
}
@@ -189,119 +193,120 @@ func (i *indentation) Close() {
189193
}
190194
i.level--
191195
if i.hasChild {
192-
i.b.WriteString("\n")
193-
i.b.WriteString(strings.Repeat(i.indent, i.level))
196+
io.WriteString(i.w, "\n")
197+
io.WriteString(i.w, strings.Repeat(i.indent, i.level))
194198
}
195199
i.hasChild = true
196200
}
197201

198-
func outputXML(b *strings.Builder, n *Node, preserveSpaces bool, config *outputConfiguration, indent *indentation) {
202+
func outputXML(w io.Writer, n *Node, preserveSpaces bool, config *outputConfiguration, indent *indentation) {
199203
preserveSpaces = calculatePreserveSpaces(n, preserveSpaces)
200204
switch n.Type {
201205
case TextNode:
202-
b.WriteString(html.EscapeString(n.sanitizedData(preserveSpaces)))
206+
io.WriteString(w, html.EscapeString(n.sanitizedData(preserveSpaces)))
203207
return
204208
case CharDataNode:
205-
b.WriteString("<![CDATA[")
206-
b.WriteString(n.Data)
207-
b.WriteString("]]>")
209+
io.WriteString(w, "<![CDATA[")
210+
io.WriteString(w, n.Data)
211+
io.WriteString(w, "]]>")
208212
return
209213
case CommentNode:
210214
if !config.skipComments {
211-
b.WriteString("<!--")
212-
b.WriteString(n.Data)
213-
b.WriteString("-->")
215+
io.WriteString(w, "<!--")
216+
io.WriteString(w, n.Data)
217+
io.WriteString(w, "-->")
214218
}
215219
return
216220
case NotationNode:
217221
indent.NewLine()
218-
fmt.Fprintf(b, "<!%s>", n.Data)
222+
fmt.Fprintf(w, "<!%s>", n.Data)
219223
return
220224
case DeclarationNode:
221-
b.WriteString("<?" + n.Data)
225+
io.WriteString(w, "<?" + n.Data)
222226
default:
223227
indent.Open()
224228
if n.Prefix == "" {
225-
b.WriteString("<" + n.Data)
229+
io.WriteString(w, "<" + n.Data)
226230
} else {
227-
fmt.Fprintf(b, "<%s:%s", n.Prefix, n.Data)
231+
fmt.Fprintf(w, "<%s:%s", n.Prefix, n.Data)
228232
}
229233
}
230234

231235
for _, attr := range n.Attr {
232236
if attr.Name.Space != "" {
233-
fmt.Fprintf(b, ` %s:%s=`, attr.Name.Space, attr.Name.Local)
237+
fmt.Fprintf(w, ` %s:%s=`, attr.Name.Space, attr.Name.Local)
234238
} else {
235-
fmt.Fprintf(b, ` %s=`, attr.Name.Local)
239+
fmt.Fprintf(w, ` %s=`, attr.Name.Local)
236240
}
237-
b.WriteByte('"')
238-
b.WriteString(html.EscapeString(attr.Value))
239-
b.WriteByte('"')
241+
242+
fmt.Fprintf(w, `"%v"`, html.EscapeString(attr.Value))
240243
}
241244
if n.Type == DeclarationNode {
242-
b.WriteString("?>")
245+
io.WriteString(w, "?>")
243246
} else {
244247
if n.FirstChild != nil || !config.emptyElementTagSupport {
245-
b.WriteString(">")
248+
io.WriteString(w, ">")
246249
} else {
247-
b.WriteString("/>")
250+
io.WriteString(w, "/>")
248251
indent.Close()
249252
return
250253
}
251254
}
252255
for child := n.FirstChild; child != nil; child = child.NextSibling {
253-
outputXML(b, child, preserveSpaces, config, indent)
256+
outputXML(w, child, preserveSpaces, config, indent)
254257
}
255258
if n.Type != DeclarationNode {
256259
indent.Close()
257260
if n.Prefix == "" {
258-
fmt.Fprintf(b, "</%s>", n.Data)
261+
fmt.Fprintf(w, "</%s>", n.Data)
259262
} else {
260-
fmt.Fprintf(b, "</%s:%s>", n.Prefix, n.Data)
263+
fmt.Fprintf(w, "</%s:%s>", n.Prefix, n.Data)
261264
}
262265
}
263266
}
264267

265268
// OutputXML returns the text that including tags name.
266269
func (n *Node) OutputXML(self bool) string {
267-
268-
config := &outputConfiguration{
269-
printSelf: true,
270-
emptyElementTagSupport: false,
270+
if self {
271+
return n.OutputXMLWithOptions(WithOutputSelf())
271272
}
272-
preserveSpaces := calculatePreserveSpaces(n, false)
273-
var b strings.Builder
274-
if self && n.Type != DocumentNode {
275-
outputXML(&b, n, preserveSpaces, config, newIndentation(config.useIndentation, &b))
276-
} else {
277-
for n := n.FirstChild; n != nil; n = n.NextSibling {
278-
outputXML(&b, n, preserveSpaces, config, newIndentation(config.useIndentation, &b))
279-
}
280-
}
281-
282-
return b.String()
273+
return n.OutputXMLWithOptions()
283274
}
284275

285276
// OutputXMLWithOptions returns the text that including tags name.
286277
func (n *Node) OutputXMLWithOptions(opts ...OutputOption) string {
278+
var b strings.Builder
279+
n.WriteWithOptions(&b, opts...)
280+
return b.String()
281+
}
287282

283+
// Write writes xml to given writer.
284+
func (n *Node) Write(writer io.Writer, self bool) {
285+
if self {
286+
n.WriteWithOptions(writer, WithOutputSelf())
287+
}
288+
n.WriteWithOptions(writer)
289+
}
290+
291+
// WriteWithOptions writes xml with given options to given writer.
292+
func (n *Node) WriteWithOptions(writer io.Writer, opts ...OutputOption) {
288293
config := &outputConfiguration{}
289294
// Set the options
290295
for _, opt := range opts {
291296
opt(config)
292297
}
293298
pastPreserveSpaces := config.preserveSpaces
294299
preserveSpaces := calculatePreserveSpaces(n, pastPreserveSpaces)
295-
var b strings.Builder
300+
b := bufio.NewWriter(writer)
301+
defer b.Flush()
302+
296303
if config.printSelf && n.Type != DocumentNode {
297-
outputXML(&b, n, preserveSpaces, config, newIndentation(config.useIndentation, &b))
304+
outputXML(b, n, preserveSpaces, config, newIndentation(config.useIndentation, b))
298305
} else {
299306
for n := n.FirstChild; n != nil; n = n.NextSibling {
300-
outputXML(&b, n, preserveSpaces, config, newIndentation(config.useIndentation, &b))
307+
outputXML(b, n, preserveSpaces, config, newIndentation(config.useIndentation, b))
301308
}
302309
}
303-
304-
return b.String()
305310
}
306311

307312
// AddAttr adds a new attribute specified by 'key' and 'val' to a node 'n'.

node_test.go

+80-2
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,7 @@ func TestSelectElement(t *testing.T) {
343343
t.Fatalf("n is nil")
344344
}
345345

346-
var ns []*Node
347-
ns = aaa.SelectElements("CCC")
346+
ns := aaa.SelectElements("CCC")
348347
if len(ns) != 2 {
349348
t.Fatalf("len(ns)!=2")
350349
}
@@ -365,6 +364,23 @@ func TestEscapeOutputValue(t *testing.T) {
365364

366365
}
367366

367+
func TestEscapeValueWrite(t *testing.T) {
368+
data := `<AAA>&lt;*&gt;</AAA>`
369+
370+
root, err := Parse(strings.NewReader(data))
371+
if err != nil {
372+
t.Error(err)
373+
}
374+
375+
var b strings.Builder
376+
root.Write(&b, true)
377+
escapedInnerText := b.String()
378+
if !strings.Contains(escapedInnerText, "&lt;*&gt;") {
379+
t.Fatal("Inner Text has not been escaped")
380+
}
381+
382+
}
383+
368384
func TestUnnecessaryEscapeOutputValue(t *testing.T) {
369385
data := `<?xml version="1.0" encoding="utf-8"?>
370386
<class_list xml:space="preserve">
@@ -391,6 +407,34 @@ func TestUnnecessaryEscapeOutputValue(t *testing.T) {
391407

392408
}
393409

410+
func TestUnnecessaryEscapeValueWrite(t *testing.T) {
411+
data := `<?xml version="1.0" encoding="utf-8"?>
412+
<class_list xml:space="preserve">
413+
<student>
414+
<name> Robert </name>
415+
<grade>A+</grade>
416+
417+
</student>
418+
</class_list>`
419+
420+
root, err := Parse(strings.NewReader(data))
421+
if err != nil {
422+
t.Error(err)
423+
}
424+
425+
var b strings.Builder
426+
root.Write(&b, true)
427+
escapedInnerText := b.String()
428+
if strings.Contains(escapedInnerText, "&#x9") {
429+
t.Fatal("\\n has been escaped unnecessarily")
430+
}
431+
432+
if strings.Contains(escapedInnerText, "&#xA") {
433+
t.Fatal("\\t has been escaped unnecessarily")
434+
}
435+
436+
}
437+
394438
func TestHtmlUnescapeStringOriginString(t *testing.T) {
395439
// has escape html character and \t
396440
data := `<?xml version="1.0" encoding="utf-8"?>
@@ -412,6 +456,29 @@ func TestHtmlUnescapeStringOriginString(t *testing.T) {
412456

413457
}
414458

459+
func TestHtmlUnescapeStringOriginStringWrite(t *testing.T) {
460+
// has escape html character and \t
461+
data := `<?xml version="1.0" encoding="utf-8"?>
462+
<example xml:space="preserve"><word>&amp;#48; </word></example>`
463+
464+
root, err := Parse(strings.NewReader(data))
465+
if err != nil {
466+
t.Error(err)
467+
}
468+
469+
var b strings.Builder
470+
root.Write(&b, false)
471+
escapedInnerText := b.String()
472+
unescapeString := html.UnescapeString(escapedInnerText)
473+
if strings.Contains(unescapeString, "&amp;") {
474+
t.Fatal("&amp; need unescape")
475+
}
476+
if !strings.Contains(escapedInnerText, "&amp;#48;\t\t") {
477+
t.Fatal("Inner Text should keep plain text")
478+
}
479+
480+
}
481+
415482
func TestOutputXMLWithNamespacePrefix(t *testing.T) {
416483
s := `<?xml version="1.0" encoding="UTF-8"?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body></S:Body></S:Envelope>`
417484
doc, _ := Parse(strings.NewReader(s))
@@ -420,6 +487,17 @@ func TestOutputXMLWithNamespacePrefix(t *testing.T) {
420487
}
421488
}
422489

490+
func TestWriteWithNamespacePrefix(t *testing.T) {
491+
s := `<?xml version="1.0" encoding="UTF-8"?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body></S:Body></S:Envelope>`
492+
doc, _ := Parse(strings.NewReader(s))
493+
var b strings.Builder
494+
doc.Write(&b, false)
495+
if s != b.String() {
496+
t.Fatal("xml document missing some characters")
497+
}
498+
}
499+
500+
423501
func TestQueryWithPrefix(t *testing.T) {
424502
s := `<?xml version="1.0" encoding="UTF-8"?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body test="1"><ns2:Fault xmlns:ns2="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns3="http://www.w3.org/2003/05/soap-envelope"><faultcode>ns2:Client</faultcode><faultstring>This is a client fault</faultstring></ns2:Fault></S:Body></S:Envelope>`
425503
doc, _ := Parse(strings.NewReader(s))

0 commit comments

Comments
 (0)