-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhtml_diff.go
More file actions
144 lines (113 loc) · 3.08 KB
/
html_diff.go
File metadata and controls
144 lines (113 loc) · 3.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package testastic
import (
"html"
"sort"
"strings"
)
// formatHTMLDiffInline generates a git-style inline diff between expected and actual HTML.
// Uses the same format as JSON diff.
func formatHTMLDiffInline(expected, actual *htmlNode) string {
expHTML := renderPrettyHTML(expected, 0)
actHTML := renderPrettyHTML(actual, 0)
expLines := strings.Split(expHTML, "\n")
actLines := strings.Split(actHTML, "\n")
diff := computeDiff(expLines, actLines)
var sb strings.Builder
for _, line := range diff {
sb.WriteString(line)
sb.WriteString("\n")
}
return sb.String()
}
// renderPrettyHTML renders an htmlNode tree as formatted HTML string.
//
//nolint:gocognit,funlen // HTML rendering requires handling multiple cases and statements.
func renderPrettyHTML(node *htmlNode, indent int) string {
if node == nil {
return ""
}
var sb strings.Builder
indentStr := strings.Repeat(" ", indent)
switch node.Type {
case htmlElement:
if node.Tag == "#document" {
for i, child := range node.Children {
if i > 0 {
sb.WriteString("\n")
}
sb.WriteString(renderPrettyHTML(child, indent))
}
return sb.String()
}
sb.WriteString(indentStr)
sb.WriteString("<")
sb.WriteString(node.Tag)
// Sort attributes for consistent output.
if len(node.Attributes) > 0 {
attrs := make([]string, 0, len(node.Attributes))
for name := range node.Attributes {
attrs = append(attrs, name)
}
sort.Strings(attrs)
for _, name := range attrs {
val := node.Attributes[name]
sb.WriteString(" ")
sb.WriteString(name)
sb.WriteString("=\"")
sb.WriteString(html.EscapeString(getString(val)))
sb.WriteString("\"")
}
}
if isVoidElement(node.Tag) {
sb.WriteString(">")
return sb.String()
}
sb.WriteString(">")
// Inline text content for single-text children.
if len(node.Children) == 1 && node.Children[0].Type == htmlText {
text := getTextContent(node.Children[0])
sb.WriteString(html.EscapeString(text))
sb.WriteString("</")
sb.WriteString(node.Tag)
sb.WriteString(">")
return sb.String()
}
if len(node.Children) > 0 {
for _, child := range node.Children {
sb.WriteString("\n")
sb.WriteString(renderPrettyHTML(child, indent+1))
}
sb.WriteString("\n")
sb.WriteString(indentStr)
}
sb.WriteString("</")
sb.WriteString(node.Tag)
sb.WriteString(">")
case htmlText:
text := getTextContent(node)
if strings.TrimSpace(text) != "" {
sb.WriteString(indentStr)
sb.WriteString(html.EscapeString(strings.TrimSpace(text)))
}
case htmlComment:
sb.WriteString(indentStr)
sb.WriteString("<!-- ")
sb.WriteString(getString(node.Text))
sb.WriteString(" -->")
case htmlDoctype:
sb.WriteString("<!DOCTYPE ")
sb.WriteString(node.Tag)
sb.WriteString(">")
}
return sb.String()
}
// isVoidElement returns true if the tag is a void element (self-closing).
func isVoidElement(tag string) bool {
switch strings.ToLower(tag) {
case "area", "base", "br", "col", "embed", "hr", "img", "input",
"link", "meta", "param", "source", "track", "wbr":
return true
default:
return false
}
}