Skip to main content

rdom_core/
form_assoc.rs

1//! Form association — which `<form>` a control belongs to, and what a
2//! submission by a given submitter would carry. Names, attributes and
3//! tree shape only; no rendering, no runtime.
4//!
5//! - [`Dom::form_owner`] — HTML §4.10.17.3 "reset the form owner": the
6//!   `form="id"` content attribute, else the nearest ancestor `<form>`.
7//! - [`Dom::form_listed_elements`] — the listed elements a form owns, in
8//!   tree order of the form's whole tree (`form.elements`, §4.10.3).
9//! - [`Dom::is_submit_button`] — §4.10.6 / §4.10.5.1.19.
10//! - [`Dom::submit_detail`] — the effective `action` / `method` /
11//!   `enctype` / `target` / no-validate state (§4.10.19.6).
12
13use crate::dom::Dom;
14use crate::event_detail::{FormEnctype, FormMethod, SubmitDetail};
15use crate::input_type::InputTypeState;
16use crate::node_id::NodeId;
17
18/// HTML §4.10.2 *listed* elements: the form-associated elements that
19/// take a `form` content attribute and appear in `form.elements`.
20fn is_listed(tag: &str) -> bool {
21    matches!(
22        tag,
23        "button" | "fieldset" | "input" | "object" | "output" | "select" | "textarea"
24    )
25}
26
27impl<Ext> Dom<Ext> {
28    /// The form owner of `id` (HTML §4.10.17.3):
29    ///
30    /// - a *listed* element (`<button>`, `<fieldset>`, `<input>`,
31    ///   `<object>`, `<output>`, `<select>`, `<textarea>`) that has a
32    ///   `form` attribute and is connected is owned by the first element
33    ///   in the document with that id **if it is a `<form>`** — and by
34    ///   nothing otherwise, even inside another form;
35    /// - anything else is owned by its nearest ancestor `<form>`.
36    ///
37    /// A form is not its own owner. Dead / non-element ids have none.
38    pub fn form_owner(&self, id: NodeId) -> Option<NodeId> {
39        let tag = self.get_node(id)?.tag_name()?;
40        let mut ancestor = self.parent_element_id(id);
41        while let Some(a) = ancestor {
42            if self.tag_is(a, "form") {
43                break;
44            }
45            ancestor = self.parent_element_id(a);
46        }
47        let connected = self.root_of(id) == Some(self.root());
48        self.owner_given_ancestor(id, tag, ancestor, connected)
49    }
50
51    /// The listed elements whose form owner is `form`, in tree order of
52    /// `form`'s whole tree — HTML `form.elements` (§4.10.3), which
53    /// includes controls outside the form that name it with `form=`.
54    /// Empty for a non-`<form>`. One pass over the tree.
55    pub fn form_listed_elements(&self, form: NodeId) -> Vec<NodeId> {
56        let mut out = Vec::new();
57        if !self.tag_is(form, "form") {
58            return out;
59        }
60        let Some(root) = self.root_of(form) else {
61            return out;
62        };
63        let connected = root == self.root();
64        // Pre-order DFS carrying each node's nearest ancestor `<form>`.
65        let mut stack: Vec<(NodeId, Option<NodeId>)> = vec![(root, None)];
66        while let Some((id, ancestor_form)) = stack.pop() {
67            let Some(node) = self.get_node(id) else {
68                continue;
69            };
70            let mut child_ancestor = ancestor_form;
71            if let Some(tag) = node.tag_name() {
72                if is_listed(tag)
73                    && self.owner_given_ancestor(id, tag, ancestor_form, connected) == Some(form)
74                {
75                    out.push(id);
76                }
77                if tag == "form" {
78                    child_ancestor = Some(id);
79                }
80            }
81            // Push children in reverse so they pop in tree order.
82            let first = stack.len();
83            let mut c = node.first_child;
84            while let Some(cid) = c {
85                stack.push((cid, child_ancestor));
86                c = self.get_node(cid).and_then(|n| n.next_sibling);
87            }
88            stack[first..].reverse();
89        }
90        out
91    }
92
93    /// Whether `id` is a *submit button* (HTML §4.10.6, §4.10.5.1.19–20):
94    /// a `<button>` whose `type` is missing, invalid or `submit`, or an
95    /// `<input type="submit">` / `<input type="image">`. `type` keywords
96    /// match ASCII case-insensitively (§2.3.3).
97    pub fn is_submit_button(&self, id: NodeId) -> bool {
98        match self.get_node(id).and_then(|n| n.tag_name()) {
99            Some("button") => !self.get_attribute(id, "type").is_some_and(|t| {
100                t.eq_ignore_ascii_case("reset") || t.eq_ignore_ascii_case("button")
101            }),
102            Some("input") => matches!(
103                self.input_type_state(id),
104                Some(InputTypeState::Submit | InputTypeState::Image)
105            ),
106            _ => false,
107        }
108    }
109
110    /// The `submit` event payload for `form` submitted by `submitter`
111    /// (HTML §4.10.21.3 with the §4.10.19.6 attributes): each of
112    /// `action` / `method` / `enctype` / `target` is the submitter's
113    /// `formaction` / `formmethod` / `formenctype` / `formtarget` when the
114    /// submitter is a submit button carrying it, else the form's
115    /// `action` / `method` / `enctype` / `target`. `no_validate` is the
116    /// submitter's `formnovalidate` or the form's `novalidate`. A
117    /// `submitter` that is not a submit button contributes no override
118    /// but is still reported.
119    pub fn submit_detail(&self, form: NodeId, submitter: Option<NodeId>) -> SubmitDetail {
120        let button = submitter.filter(|&s| self.is_submit_button(s));
121        let attr = |over: &str, own: &str| -> Option<&str> {
122            button
123                .and_then(|b| self.get_attribute(b, over))
124                .or_else(|| self.get_attribute(form, own))
125        };
126        let mut d = SubmitDetail::new(submitter);
127        d.action = attr("formaction", "action").unwrap_or("").to_string();
128        d.method = attr("formmethod", "method")
129            .map(FormMethod::from_attribute)
130            .unwrap_or_default();
131        d.enctype = attr("formenctype", "enctype")
132            .map(FormEnctype::from_attribute)
133            .unwrap_or_default();
134        d.target = attr("formtarget", "target").unwrap_or("").to_string();
135        d.no_validate = button.is_some_and(|b| self.has_attribute(b, "formnovalidate"))
136            || self.has_attribute(form, "novalidate");
137        d
138    }
139
140    /// The owner rule of [`Self::form_owner`], given the element's
141    /// nearest ancestor `<form>` and whether it is connected.
142    fn owner_given_ancestor(
143        &self,
144        id: NodeId,
145        tag: &str,
146        ancestor_form: Option<NodeId>,
147        connected: bool,
148    ) -> Option<NodeId> {
149        if is_listed(tag)
150            && connected
151            && let Some(form_id) = self.get_attribute(id, "form")
152        {
153            // `get_element_by_id` answers the first connected element in
154            // tree order; a detached hit means no element in the
155            // document carries the id.
156            return self
157                .get_element_by_id(form_id)
158                .filter(|&f| self.tag_is(f, "form") && self.root_of(f) == Some(self.root()));
159        }
160        ancestor_form
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use crate::node_id::NodeId;
167    use crate::{Dom, FormEnctype, FormMethod};
168
169    fn el(dom: &mut Dom, parent: NodeId, tag: &str, attrs: &[(&str, &str)]) -> NodeId {
170        let e = dom.create_element(tag);
171        for (k, v) in attrs {
172            dom.set_attribute(e, k, v).unwrap();
173        }
174        dom.append_child(parent, e).unwrap();
175        e
176    }
177
178    #[test]
179    fn owner_is_the_nearest_ancestor_form_without_a_form_attribute() {
180        let mut dom: Dom = Dom::new();
181        let root = dom.root();
182        let form = el(&mut dom, root, "form", &[]);
183        let div = el(&mut dom, form, "div", &[]);
184        let input = el(&mut dom, div, "input", &[]);
185        let outside = el(&mut dom, root, "input", &[]);
186        assert_eq!(dom.form_owner(input), Some(form));
187        assert_eq!(dom.form_owner(outside), None);
188        assert_eq!(dom.form_owner(form), None, "a form is not its own owner");
189    }
190
191    /// HTML §4.10.17.3: `form="id"` naming a `<form>` wins over the
192    /// ancestor, and reaches controls outside any form.
193    #[test]
194    fn form_attribute_names_the_owner() {
195        let mut dom: Dom = Dom::new();
196        let root = dom.root();
197        let a = el(&mut dom, root, "form", &[("id", "a")]);
198        let b = el(&mut dom, root, "form", &[("id", "b")]);
199        let in_a_for_b = el(&mut dom, a, "input", &[("form", "b")]);
200        let outside_for_a = el(&mut dom, root, "select", &[("form", "a")]);
201        assert_eq!(dom.form_owner(in_a_for_b), Some(b));
202        assert_eq!(dom.form_owner(outside_for_a), Some(a));
203    }
204
205    /// The `form` attribute's "otherwise" is no owner at all: an unknown
206    /// id, or an id naming a non-form, leaves even a nested control
207    /// unowned.
208    #[test]
209    fn form_attribute_that_names_no_form_means_no_owner() {
210        let mut dom: Dom = Dom::new();
211        let root = dom.root();
212        let form = el(&mut dom, root, "form", &[("id", "f")]);
213        el(&mut dom, root, "div", &[("id", "d")]);
214        let unknown = el(&mut dom, form, "input", &[("form", "nope")]);
215        let div_id = el(&mut dom, form, "button", &[("form", "d")]);
216        let empty = el(&mut dom, form, "textarea", &[("form", "")]);
217        assert_eq!(dom.form_owner(unknown), None);
218        assert_eq!(dom.form_owner(div_id), None);
219        assert_eq!(dom.form_owner(empty), None);
220    }
221
222    /// Only listed elements take `form=`; a disconnected control falls
223    /// back to its ancestor (HTML: the attribute applies when connected).
224    #[test]
225    fn form_attribute_ignored_on_non_listed_and_disconnected_elements() {
226        let mut dom: Dom = Dom::new();
227        let root = dom.root();
228        let f = el(&mut dom, root, "form", &[("id", "f")]);
229        let g = el(&mut dom, root, "form", &[("id", "g")]);
230        let label = el(&mut dom, g, "label", &[("form", "f")]);
231        assert_eq!(dom.form_owner(label), Some(g));
232
233        let detached = dom.create_element("form");
234        let input = dom.create_element("input");
235        dom.set_attribute(input, "form", "f").unwrap();
236        dom.append_child(detached, input).unwrap();
237        assert_eq!(dom.form_owner(input), Some(detached));
238        let _ = f;
239    }
240
241    /// `form.elements`: tree order of the whole document, including
242    /// controls outside the form that point at it, excluding controls
243    /// inside it that point elsewhere.
244    #[test]
245    fn listed_elements_follow_document_tree_order() {
246        let mut dom: Dom = Dom::new();
247        let root = dom.root();
248        let before = el(&mut dom, root, "input", &[("form", "f")]);
249        let form = el(&mut dom, root, "form", &[("id", "f")]);
250        let fs = el(&mut dom, form, "fieldset", &[]);
251        let inner = el(&mut dom, fs, "input", &[]);
252        el(&mut dom, form, "input", &[("form", "other")]);
253        el(&mut dom, form, "div", &[]);
254        let after = el(&mut dom, root, "button", &[("form", "f")]);
255        el(&mut dom, root, "input", &[]);
256        assert_eq!(
257            dom.form_listed_elements(form),
258            vec![before, fs, inner, after]
259        );
260    }
261
262    /// HTML §2.3.3: `type` is an enumerated attribute, matched ASCII
263    /// case-insensitively — `SUBMIT` is Submit, `Reset` is Reset.
264    #[test]
265    fn submit_button_type_matches_ascii_case_insensitively() {
266        let mut dom: Dom = Dom::new();
267        let root = dom.root();
268        let upper_submit_input = el(&mut dom, root, "input", &[("type", "SUBMIT")]);
269        let mixed_submit_input = el(&mut dom, root, "input", &[("type", "Submit")]);
270        let upper_reset = el(&mut dom, root, "button", &[("type", "RESET")]);
271        let mixed_button = el(&mut dom, root, "button", &[("type", "Button")]);
272        let upper_submit_button = el(&mut dom, root, "button", &[("type", "SUBMIT")]);
273        assert!(dom.is_submit_button(upper_submit_input));
274        assert!(dom.is_submit_button(mixed_submit_input));
275        assert!(!dom.is_submit_button(upper_reset));
276        assert!(!dom.is_submit_button(mixed_button));
277        assert!(dom.is_submit_button(upper_submit_button));
278    }
279
280    /// HTML §4.10.5.1.20: the Image Button state is a submit button,
281    /// and its `formaction` / `formmethod` overrides apply.
282    #[test]
283    fn an_image_input_is_a_submit_button() {
284        let mut dom: Dom = Dom::new();
285        let root = dom.root();
286        let form = el(&mut dom, root, "form", &[("method", "get")]);
287        let image = el(
288            &mut dom,
289            form,
290            "input",
291            &[("type", "Image"), ("formmethod", "post")],
292        );
293        assert!(dom.is_submit_button(image));
294        assert_eq!(
295            dom.submit_detail(form, Some(image)).method,
296            FormMethod::Post
297        );
298    }
299
300    #[test]
301    fn submit_detail_reads_the_form_attributes() {
302        let mut dom: Dom = Dom::new();
303        let root = dom.root();
304        let form = el(
305            &mut dom,
306            root,
307            "form",
308            &[
309                ("action", "/save"),
310                ("method", "POST"),
311                ("enctype", "multipart/form-data"),
312                ("target", "_blank"),
313                ("novalidate", ""),
314            ],
315        );
316        let plain = el(&mut dom, form, "button", &[]);
317        let d = dom.submit_detail(form, Some(plain));
318        assert_eq!(d.submitter, Some(plain));
319        assert_eq!(d.action, "/save");
320        assert_eq!(d.method, FormMethod::Post);
321        assert_eq!(d.enctype, FormEnctype::MultipartFormData);
322        assert_eq!(d.target, "_blank");
323        assert!(d.no_validate);
324
325        let bare = el(&mut dom, root, "form", &[("method", "bogus")]);
326        let d = dom.submit_detail(bare, None);
327        assert_eq!(
328            (
329                d.action.as_str(),
330                d.method,
331                d.enctype,
332                d.target.as_str(),
333                d.no_validate
334            ),
335            ("", FormMethod::Get, FormEnctype::UrlEncoded, "", false)
336        );
337    }
338
339    /// §4.10.19.6: a submit button's `form*` attributes override the
340    /// form's; `formmethod` with an invalid value is GET (no missing
341    /// value default, so presence alone overrides).
342    #[test]
343    fn submit_detail_prefers_the_submit_buttons_overrides() {
344        let mut dom: Dom = Dom::new();
345        let root = dom.root();
346        let form = el(
347            &mut dom,
348            root,
349            "form",
350            &[("action", "/a"), ("method", "post"), ("target", "t")],
351        );
352        let over = el(
353            &mut dom,
354            form,
355            "input",
356            &[
357                ("type", "submit"),
358                ("formaction", "/b"),
359                ("formmethod", "dialog"),
360                ("formenctype", "text/plain"),
361                ("formtarget", "_self"),
362                ("formnovalidate", ""),
363            ],
364        );
365        let d = dom.submit_detail(form, Some(over));
366        assert_eq!(d.action, "/b");
367        assert_eq!(d.method, FormMethod::Dialog);
368        assert_eq!(d.enctype, FormEnctype::TextPlain);
369        assert_eq!(d.target, "_self");
370        assert!(d.no_validate);
371
372        let invalid = el(&mut dom, form, "button", &[("formmethod", "bogus")]);
373        assert_eq!(
374            dom.submit_detail(form, Some(invalid)).method,
375            FormMethod::Get
376        );
377
378        // A non-submit button contributes no override.
379        let reset = el(
380            &mut dom,
381            form,
382            "button",
383            &[("type", "reset"), ("formaction", "/r")],
384        );
385        let d = dom.submit_detail(form, Some(reset));
386        assert_eq!((d.submitter, d.action.as_str()), (Some(reset), "/a"));
387    }
388}