mirror of
https://github.com/PuerkitoBio/goquery.git
synced 2026-10-21 03:49:40 +00:00
fix: account for form ancestor context in HTML fragment cache
golang.org/x/net/html.ParseFragmentWithOptions initializes its form pointer based on whether the context element or any of its ancestors is a form element. If the context has a form ancestor, nested form tags are discarded per HTML parsing rules. Previously, cachedParseHtmlWithContext keyed cached fragments strictly by context tag name. When a selection contained targets both inside and outside forms sharing the same tag name, the first visited target cached its parsed fragment, leading to incorrect parsing for subsequent targets under the other form-ancestor state. Differentiate the fragment cache key by appending a form-ancestor indicator when context or any of its ancestors is a form element. Fixes #614
This commit is contained in:
+18
-2
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
"golang.org/x/net/html/atom"
|
||||
)
|
||||
|
||||
// After applies the selector from the root document and inserts the matched elements
|
||||
@@ -677,11 +678,26 @@ func (s *Selection) eachNodeHtml(htmlStr string, isParent bool, mergeFn func(n *
|
||||
return s
|
||||
}
|
||||
|
||||
// hasFormAncestor reports whether n or any of its ancestors is an HTML form element.
|
||||
// This mirrors the logic in net/html.ParseFragmentWithOptions, which initializes its
|
||||
// form pointer from context and its ancestor chain.
|
||||
func hasFormAncestor(n *html.Node) bool {
|
||||
for ; n != nil; n = n.Parent {
|
||||
if n.Type == html.ElementNode && (n.DataAtom == atom.Form || n.Data == "form") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// cachedParseHtmlWithContext returns parseHtmlWithContext(htmlStr, context), reusing a prior
|
||||
// result when context's nodeName has already been seen. Callers pass their own
|
||||
// cache map so the cache lifetime matches the caller's loop scope.
|
||||
// result when context's nodeName has already been seen with the same form ancestor state.
|
||||
// Callers pass their own cache map so the cache lifetime matches the caller's loop scope.
|
||||
func cachedParseHtmlWithContext(cache map[string][]*html.Node, htmlStr string, context *html.Node, opts []html.ParseOption) []*html.Node {
|
||||
key := nodeName(context)
|
||||
if hasFormAncestor(context) {
|
||||
key += ":form"
|
||||
}
|
||||
if nodes, ok := cache[key]; ok {
|
||||
return nodes
|
||||
}
|
||||
|
||||
@@ -759,3 +759,123 @@ func TestHtmlWithNonElementNode(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormAncestorCache(t *testing.T) {
|
||||
const htmlTemplate = `<html><head></head><body>` +
|
||||
`<div id="wrapper-out"><div id="p-out"><div id="target-out" class="target"><span>c1</span></div></div></div>` +
|
||||
`<form id="wrapper-in"><div id="p-in"><div id="target-in" class="target"><span>c2</span></div></div></form>` +
|
||||
`</body></html>`
|
||||
|
||||
const insertFragment = `<form><input type="text"/></form>`
|
||||
const wrapFragment = `<form><div class="wrapper"><span></span></div></form>`
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
fragment string
|
||||
fn func(*Selection, string) *Selection
|
||||
}{
|
||||
{"AppendHtml", insertFragment, (*Selection).AppendHtml},
|
||||
{"PrependHtml", insertFragment, (*Selection).PrependHtml},
|
||||
{"SetHtml", insertFragment, (*Selection).SetHtml},
|
||||
{"BeforeHtml", insertFragment, (*Selection).BeforeHtml},
|
||||
{"AfterHtml", insertFragment, (*Selection).AfterHtml},
|
||||
{"ReplaceWithHtml", insertFragment, (*Selection).ReplaceWithHtml},
|
||||
{"WrapHtml", wrapFragment, (*Selection).WrapHtml},
|
||||
{"WrapInnerHtml", wrapFragment, (*Selection).WrapInnerHtml},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Control: apply to target-out only
|
||||
docCtrlOut := loadString(t, htmlTemplate)
|
||||
tc.fn(docCtrlOut.Find("#target-out"), tc.fragment)
|
||||
ctrlOutHTML, err := docCtrlOut.Find("#p-out").Html()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Control: apply to target-in only
|
||||
docCtrlIn := loadString(t, htmlTemplate)
|
||||
tc.fn(docCtrlIn.Find("#target-in"), tc.fragment)
|
||||
ctrlInHTML, err := docCtrlIn.Find("#p-in").Html()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Batch 1: outside element first [target-out, target-in]
|
||||
docOutFirst := loadString(t, htmlTemplate)
|
||||
selOutFirst := docOutFirst.Find("#target-out").AddNodes(docOutFirst.Find("#target-in").Get(0))
|
||||
tc.fn(selOutFirst, tc.fragment)
|
||||
outFirst_OutHTML, _ := docOutFirst.Find("#p-out").Html()
|
||||
outFirst_InHTML, _ := docOutFirst.Find("#p-in").Html()
|
||||
|
||||
if outFirst_OutHTML != ctrlOutHTML {
|
||||
t.Errorf("outside-first target-out: got %q, want %q", outFirst_OutHTML, ctrlOutHTML)
|
||||
}
|
||||
if outFirst_InHTML != ctrlInHTML {
|
||||
t.Errorf("outside-first target-in: got %q, want %q", outFirst_InHTML, ctrlInHTML)
|
||||
}
|
||||
|
||||
// Batch 2: inside element first [target-in, target-out]
|
||||
docInFirst := loadString(t, htmlTemplate)
|
||||
selInFirst := docInFirst.Find("#target-in").AddNodes(docInFirst.Find("#target-out").Get(0))
|
||||
tc.fn(selInFirst, tc.fragment)
|
||||
inFirst_OutHTML, _ := docInFirst.Find("#p-out").Html()
|
||||
inFirst_InHTML, _ := docInFirst.Find("#p-in").Html()
|
||||
|
||||
if inFirst_OutHTML != ctrlOutHTML {
|
||||
t.Errorf("inside-first target-out: got %q, want %q", inFirst_OutHTML, ctrlOutHTML)
|
||||
}
|
||||
if inFirst_InHTML != ctrlInHTML {
|
||||
t.Errorf("inside-first target-in: got %q, want %q", inFirst_InHTML, ctrlInHTML)
|
||||
}
|
||||
|
||||
// Empty selection control
|
||||
docEmpty := loadString(t, htmlTemplate)
|
||||
emptySel := docEmpty.Find(".nonexistent")
|
||||
tc.fn(emptySel, tc.fragment)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendHtmlFormAncestorContext(t *testing.T) {
|
||||
doc := loadString(t, `<html><body>
|
||||
<div id="out"><div class="target" id="t-out"></div></div>
|
||||
<form id="f"><div class="target" id="t-in"></div></form>
|
||||
</body></html>`)
|
||||
|
||||
doc.Find(".target").AppendHtml(`<form><input type="text" name="foo"/></form>`)
|
||||
|
||||
assertLength(t, doc.Find("#t-out form").Nodes, 1)
|
||||
assertLength(t, doc.Find("#t-out form input").Nodes, 1)
|
||||
assertLength(t, doc.Find("#t-in form").Nodes, 0)
|
||||
assertLength(t, doc.Find("#t-in input").Nodes, 1)
|
||||
}
|
||||
|
||||
func TestPrependHtmlFormAncestorContext(t *testing.T) {
|
||||
doc := loadString(t, `<html><body>
|
||||
<div id="out"><div class="target" id="t-out"><p>existing</p></div></div>
|
||||
<form id="f"><div class="target" id="t-in"><p>existing</p></div></form>
|
||||
</body></html>`)
|
||||
|
||||
doc.Find(".target").PrependHtml(`<form><input type="text" name="foo"/></form>`)
|
||||
|
||||
assertLength(t, doc.Find("#t-out form").Nodes, 1)
|
||||
assertLength(t, doc.Find("#t-out form input").Nodes, 1)
|
||||
assertLength(t, doc.Find("#t-in form").Nodes, 0)
|
||||
assertLength(t, doc.Find("#t-in input").Nodes, 1)
|
||||
}
|
||||
|
||||
func TestSetHtmlFormAncestorContext(t *testing.T) {
|
||||
doc := loadString(t, `<html><body>
|
||||
<div id="out"><div class="target" id="t-out"><p>old</p></div></div>
|
||||
<form id="f"><div class="target" id="t-in"><p>old</p></div></form>
|
||||
</body></html>`)
|
||||
|
||||
doc.Find(".target").SetHtml(`<form><input type="text" name="foo"/></form>`)
|
||||
|
||||
assertLength(t, doc.Find("#t-out form").Nodes, 1)
|
||||
assertLength(t, doc.Find("#t-out form input").Nodes, 1)
|
||||
assertLength(t, doc.Find("#t-in form").Nodes, 0)
|
||||
assertLength(t, doc.Find("#t-in input").Nodes, 1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user