use crate::dom::Dom;
use crate::event_detail::{FormEnctype, FormMethod, SubmitDetail};
use crate::input_type::InputTypeState;
use crate::node_id::NodeId;
fn is_listed(tag: &str) -> bool {
matches!(
tag,
"button" | "fieldset" | "input" | "object" | "output" | "select" | "textarea"
)
}
impl<Ext> Dom<Ext> {
pub fn form_owner(&self, id: NodeId) -> Option<NodeId> {
let tag = self.get_node(id)?.tag_name()?;
let mut ancestor = self.parent_element_id(id);
while let Some(a) = ancestor {
if self.tag_is(a, "form") {
break;
}
ancestor = self.parent_element_id(a);
}
let connected = self.root_of(id) == Some(self.root());
self.owner_given_ancestor(id, tag, ancestor, connected)
}
pub fn form_listed_elements(&self, form: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
if !self.tag_is(form, "form") {
return out;
}
let Some(root) = self.root_of(form) else {
return out;
};
let connected = root == self.root();
let mut stack: Vec<(NodeId, Option<NodeId>)> = vec![(root, None)];
while let Some((id, ancestor_form)) = stack.pop() {
let Some(node) = self.get_node(id) else {
continue;
};
let mut child_ancestor = ancestor_form;
if let Some(tag) = node.tag_name() {
if is_listed(tag)
&& self.owner_given_ancestor(id, tag, ancestor_form, connected) == Some(form)
{
out.push(id);
}
if tag == "form" {
child_ancestor = Some(id);
}
}
let first = stack.len();
let mut c = node.first_child;
while let Some(cid) = c {
stack.push((cid, child_ancestor));
c = self.get_node(cid).and_then(|n| n.next_sibling);
}
stack[first..].reverse();
}
out
}
pub fn is_submit_button(&self, id: NodeId) -> bool {
match self.get_node(id).and_then(|n| n.tag_name()) {
Some("button") => !self.get_attribute(id, "type").is_some_and(|t| {
t.eq_ignore_ascii_case("reset") || t.eq_ignore_ascii_case("button")
}),
Some("input") => matches!(
self.input_type_state(id),
Some(InputTypeState::Submit | InputTypeState::Image)
),
_ => false,
}
}
pub fn submit_detail(&self, form: NodeId, submitter: Option<NodeId>) -> SubmitDetail {
let button = submitter.filter(|&s| self.is_submit_button(s));
let attr = |over: &str, own: &str| -> Option<&str> {
button
.and_then(|b| self.get_attribute(b, over))
.or_else(|| self.get_attribute(form, own))
};
let mut d = SubmitDetail::new(submitter);
d.action = attr("formaction", "action").unwrap_or("").to_string();
d.method = attr("formmethod", "method")
.map(FormMethod::from_attribute)
.unwrap_or_default();
d.enctype = attr("formenctype", "enctype")
.map(FormEnctype::from_attribute)
.unwrap_or_default();
d.target = attr("formtarget", "target").unwrap_or("").to_string();
d.no_validate = button.is_some_and(|b| self.has_attribute(b, "formnovalidate"))
|| self.has_attribute(form, "novalidate");
d
}
fn owner_given_ancestor(
&self,
id: NodeId,
tag: &str,
ancestor_form: Option<NodeId>,
connected: bool,
) -> Option<NodeId> {
if is_listed(tag)
&& connected
&& let Some(form_id) = self.get_attribute(id, "form")
{
return self
.get_element_by_id(form_id)
.filter(|&f| self.tag_is(f, "form") && self.root_of(f) == Some(self.root()));
}
ancestor_form
}
}
#[cfg(test)]
mod tests {
use crate::node_id::NodeId;
use crate::{Dom, FormEnctype, FormMethod};
fn el(dom: &mut Dom, parent: NodeId, tag: &str, attrs: &[(&str, &str)]) -> NodeId {
let e = dom.create_element(tag);
for (k, v) in attrs {
dom.set_attribute(e, k, v).unwrap();
}
dom.append_child(parent, e).unwrap();
e
}
#[test]
fn owner_is_the_nearest_ancestor_form_without_a_form_attribute() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let form = el(&mut dom, root, "form", &[]);
let div = el(&mut dom, form, "div", &[]);
let input = el(&mut dom, div, "input", &[]);
let outside = el(&mut dom, root, "input", &[]);
assert_eq!(dom.form_owner(input), Some(form));
assert_eq!(dom.form_owner(outside), None);
assert_eq!(dom.form_owner(form), None, "a form is not its own owner");
}
#[test]
fn form_attribute_names_the_owner() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let a = el(&mut dom, root, "form", &[("id", "a")]);
let b = el(&mut dom, root, "form", &[("id", "b")]);
let in_a_for_b = el(&mut dom, a, "input", &[("form", "b")]);
let outside_for_a = el(&mut dom, root, "select", &[("form", "a")]);
assert_eq!(dom.form_owner(in_a_for_b), Some(b));
assert_eq!(dom.form_owner(outside_for_a), Some(a));
}
#[test]
fn form_attribute_that_names_no_form_means_no_owner() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let form = el(&mut dom, root, "form", &[("id", "f")]);
el(&mut dom, root, "div", &[("id", "d")]);
let unknown = el(&mut dom, form, "input", &[("form", "nope")]);
let div_id = el(&mut dom, form, "button", &[("form", "d")]);
let empty = el(&mut dom, form, "textarea", &[("form", "")]);
assert_eq!(dom.form_owner(unknown), None);
assert_eq!(dom.form_owner(div_id), None);
assert_eq!(dom.form_owner(empty), None);
}
#[test]
fn form_attribute_ignored_on_non_listed_and_disconnected_elements() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let f = el(&mut dom, root, "form", &[("id", "f")]);
let g = el(&mut dom, root, "form", &[("id", "g")]);
let label = el(&mut dom, g, "label", &[("form", "f")]);
assert_eq!(dom.form_owner(label), Some(g));
let detached = dom.create_element("form");
let input = dom.create_element("input");
dom.set_attribute(input, "form", "f").unwrap();
dom.append_child(detached, input).unwrap();
assert_eq!(dom.form_owner(input), Some(detached));
let _ = f;
}
#[test]
fn listed_elements_follow_document_tree_order() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let before = el(&mut dom, root, "input", &[("form", "f")]);
let form = el(&mut dom, root, "form", &[("id", "f")]);
let fs = el(&mut dom, form, "fieldset", &[]);
let inner = el(&mut dom, fs, "input", &[]);
el(&mut dom, form, "input", &[("form", "other")]);
el(&mut dom, form, "div", &[]);
let after = el(&mut dom, root, "button", &[("form", "f")]);
el(&mut dom, root, "input", &[]);
assert_eq!(
dom.form_listed_elements(form),
vec![before, fs, inner, after]
);
}
#[test]
fn submit_button_type_matches_ascii_case_insensitively() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let upper_submit_input = el(&mut dom, root, "input", &[("type", "SUBMIT")]);
let mixed_submit_input = el(&mut dom, root, "input", &[("type", "Submit")]);
let upper_reset = el(&mut dom, root, "button", &[("type", "RESET")]);
let mixed_button = el(&mut dom, root, "button", &[("type", "Button")]);
let upper_submit_button = el(&mut dom, root, "button", &[("type", "SUBMIT")]);
assert!(dom.is_submit_button(upper_submit_input));
assert!(dom.is_submit_button(mixed_submit_input));
assert!(!dom.is_submit_button(upper_reset));
assert!(!dom.is_submit_button(mixed_button));
assert!(dom.is_submit_button(upper_submit_button));
}
#[test]
fn an_image_input_is_a_submit_button() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let form = el(&mut dom, root, "form", &[("method", "get")]);
let image = el(
&mut dom,
form,
"input",
&[("type", "Image"), ("formmethod", "post")],
);
assert!(dom.is_submit_button(image));
assert_eq!(
dom.submit_detail(form, Some(image)).method,
FormMethod::Post
);
}
#[test]
fn submit_detail_reads_the_form_attributes() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let form = el(
&mut dom,
root,
"form",
&[
("action", "/save"),
("method", "POST"),
("enctype", "multipart/form-data"),
("target", "_blank"),
("novalidate", ""),
],
);
let plain = el(&mut dom, form, "button", &[]);
let d = dom.submit_detail(form, Some(plain));
assert_eq!(d.submitter, Some(plain));
assert_eq!(d.action, "/save");
assert_eq!(d.method, FormMethod::Post);
assert_eq!(d.enctype, FormEnctype::MultipartFormData);
assert_eq!(d.target, "_blank");
assert!(d.no_validate);
let bare = el(&mut dom, root, "form", &[("method", "bogus")]);
let d = dom.submit_detail(bare, None);
assert_eq!(
(
d.action.as_str(),
d.method,
d.enctype,
d.target.as_str(),
d.no_validate
),
("", FormMethod::Get, FormEnctype::UrlEncoded, "", false)
);
}
#[test]
fn submit_detail_prefers_the_submit_buttons_overrides() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let form = el(
&mut dom,
root,
"form",
&[("action", "/a"), ("method", "post"), ("target", "t")],
);
let over = el(
&mut dom,
form,
"input",
&[
("type", "submit"),
("formaction", "/b"),
("formmethod", "dialog"),
("formenctype", "text/plain"),
("formtarget", "_self"),
("formnovalidate", ""),
],
);
let d = dom.submit_detail(form, Some(over));
assert_eq!(d.action, "/b");
assert_eq!(d.method, FormMethod::Dialog);
assert_eq!(d.enctype, FormEnctype::TextPlain);
assert_eq!(d.target, "_self");
assert!(d.no_validate);
let invalid = el(&mut dom, form, "button", &[("formmethod", "bogus")]);
assert_eq!(
dom.submit_detail(form, Some(invalid)).method,
FormMethod::Get
);
let reset = el(
&mut dom,
form,
"button",
&[("type", "reset"), ("formaction", "/r")],
);
let d = dom.submit_detail(form, Some(reset));
assert_eq!((d.submitter, d.action.as_str()), (Some(reset), "/a"));
}
}