Skip to content

Commit df9472e

Browse files
anderskclaude
andcommitted
Ignore the form element pointer throughout template contents
The form element pointer is deliberately not used inside template contents, but the conditions expressing that only tested the stack of open elements. A fragment parse whose context element is a template element produces template contents without a template element on that stack, so a nested <form> was dropped there. The standard now defines "parsing template contents", which also covers that case, and uses it for the <form> start and end tags and for form association (whatwg/html#12911). It also changed the "in table" <form> start tag to insert the element while parsing template contents, skipping only the pointer assignment, rather than dropping it, and requires there to be no fragment context element for form association. Do the same. This fixes three template.dat tests that now live in WPT (html/syntax/parsing/resources). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anders Kaseorg <andersk@mit.edu>
1 parent 7760920 commit df9472e

3 files changed

Lines changed: 63 additions & 10 deletions

File tree

‎html5ever/src/tree_builder/mod.rs‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,16 @@ where
11191119
.any(|elem| self.html_elem_named(elem, name.clone()))
11201120
}
11211121

1122+
/// <https://html.spec.whatwg.org/multipage/#parsing-template-contents>
1123+
fn parsing_template_contents(&self) -> bool {
1124+
self.in_html_elem_named(local_name!("template"))
1125+
|| self
1126+
.context_elem
1127+
.borrow()
1128+
.as_ref()
1129+
.is_some_and(|elem| self.html_elem_named(elem, local_name!("template")))
1130+
}
1131+
11221132
fn current_node_named(&self, name: LocalName) -> bool {
11231133
self.html_elem_named(&self.current_node(), name)
11241134
}
@@ -1385,7 +1395,8 @@ where
13851395
let qname = QualName::new(None, ns, name);
13861396
let form_is_associatable = form_associatable(qname.expanded())
13871397
&& self.form_elem.borrow().is_some()
1388-
&& !self.in_html_elem_named(local_name!("template"))
1398+
&& !self.parsing_template_contents()
1399+
&& !self.is_fragment()
13891400
&& !(listed(qname.expanded())
13901401
&& attrs
13911402
.iter()

‎html5ever/src/tree_builder/rules.rs‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -541,14 +541,12 @@ where
541541
},
542542

543543
Token::Tag(tag @ tag!(<form>)) => {
544-
if self.form_elem.borrow().is_some()
545-
&& !self.in_html_elem_named(local_name!("template"))
546-
{
544+
if self.form_elem.borrow().is_some() && !self.parsing_template_contents() {
547545
self.sink.parse_error(Borrowed("nested forms"));
548546
} else {
549547
self.close_p_element_in_button_scope();
550548
let elem = self.insert_element_for(tag);
551-
if !self.in_html_elem_named(local_name!("template")) {
549+
if !self.parsing_template_contents() {
552550
*self.form_elem.borrow_mut() = Some(elem);
553551
}
554552
}
@@ -630,7 +628,7 @@ where
630628
},
631629

632630
Token::Tag(tag!(</form>)) => {
633-
if !self.in_html_elem_named(local_name!("template")) {
631+
if !self.parsing_template_contents() {
634632
let Some(node) = self.form_elem.take() else {
635633
self.sink
636634
.parse_error(Borrowed("Null form element pointer on </form>"));
@@ -1116,10 +1114,12 @@ where
11161114

11171115
Token::Tag(tag @ tag!(<form>)) => {
11181116
self.unexpected(&tag);
1119-
if !self.in_html_elem_named(local_name!("template"))
1120-
&& self.form_elem.borrow().is_none()
1121-
{
1122-
*self.form_elem.borrow_mut() = Some(self.insert_and_pop_element_for(tag));
1117+
let parsing_template_contents = self.parsing_template_contents();
1118+
if self.form_elem.borrow().is_none() || parsing_template_contents {
1119+
let elem = self.insert_and_pop_element_for(tag);
1120+
if !parsing_template_contents {
1121+
*self.form_elem.borrow_mut() = Some(elem);
1122+
}
11231123
}
11241124
ProcessResult::Done
11251125
},

‎rcdom/tests/html-driver.rs‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,48 @@ use html5ever::serialize;
33
use html5ever::tendril::TendrilSink;
44
use markup5ever_rcdom::{RcDom, SerializableHandle};
55

6+
#[test]
7+
fn form_element_pointer_ignored_in_template_contents() {
8+
fn serialize_children(node: markup5ever_rcdom::Handle) -> String {
9+
let mut serialized = Vec::new();
10+
let node: SerializableHandle = node.into();
11+
serialize::serialize(&mut serialized, &node, Default::default()).unwrap();
12+
String::from_utf8(serialized).unwrap()
13+
}
14+
15+
// A form start tag in a table in template contents inserts a form
16+
let dom = driver::parse_document(RcDom::default(), Default::default())
17+
.one("<template><table><form></table></template>");
18+
let html = dom.document.children.borrow()[0].clone();
19+
let head = html.children.borrow()[0].clone();
20+
let template = head.children.borrow()[0].clone();
21+
let markup5ever_rcdom::NodeData::Element {
22+
ref template_contents,
23+
..
24+
} = template.data
25+
else {
26+
panic!("expected a template element");
27+
};
28+
let contents = template_contents.borrow().clone().unwrap();
29+
assert_eq!(serialize_children(contents), "<table><form></form></table>");
30+
31+
// When fragment parsing with a template context, nested forms are not dropped
32+
let dom = driver::parse_fragment(
33+
RcDom::default(),
34+
Default::default(),
35+
html5ever::QualName::new(
36+
None,
37+
html5ever::ns!(html),
38+
html5ever::local_name!("template"),
39+
),
40+
vec![],
41+
true,
42+
)
43+
.one("<form><form>");
44+
let html = dom.document.children.borrow()[0].clone();
45+
assert_eq!(serialize_children(html), "<form><form></form></form>");
46+
}
47+
648
#[test]
749
fn from_utf8() {
850
let dom = driver::parse_document(RcDom::default(), Default::default())

0 commit comments

Comments
 (0)