diff --git a/manipulation.go b/manipulation.go index d9edcd5..ce7731d 100644 --- a/manipulation.go +++ b/manipulation.go @@ -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 } diff --git a/manipulation_test.go b/manipulation_test.go index fd5e020..cb5145b 100644 --- a/manipulation_test.go +++ b/manipulation_test.go @@ -759,3 +759,123 @@ func TestHtmlWithNonElementNode(t *testing.T) { }) } } + +func TestFormAncestorCache(t *testing.T) { + const htmlTemplate = `` + + `
c1
` + + `
c2
` + + `` + + const insertFragment = `
` + const wrapFragment = `
` + + 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, ` +
+
+ `) + + doc.Find(".target").AppendHtml(`
`) + + 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, ` +

existing

+

existing

+ `) + + doc.Find(".target").PrependHtml(`
`) + + 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, ` +

old

+

old

+ `) + + doc.Find(".target").SetHtml(`
`) + + 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) +}