Skip to main content

serval_scripted_dom/
forms.rs

1//! Form-control value extraction for the verso flip's form layer.
2//!
3//! serval has no separate dirty-value store (`set_attribute` is the only value
4//! path), so a control's value *is* its `value` attribute — `<textarea>` is the
5//! one exception, whose value is its text content. Best-effort, per the flip's
6//! form layer: it degrades, never blocks.
7
8use layout_dom_api::NodeKind;
9
10use crate::{NodeId, ScriptedDom};
11
12impl ScriptedDom {
13    /// Form-control values under `root` (inclusive), keyed by the control's `name`,
14    /// falling back to `id`. Covers `<input>`/`<select>` (the `value` attribute) and
15    /// `<textarea>` (text content). Controls with neither a name nor an id are
16    /// skipped — nothing identifies them in a freshly loaded page.
17    pub fn form_values(&self, root: NodeId) -> Vec<(String, String)> {
18        let mut out = Vec::new();
19        self.collect_form_values(root, &mut out);
20        out
21    }
22
23    fn collect_form_values(&self, id: NodeId, out: &mut Vec<(String, String)>) {
24        let node = self.node(id);
25        if node.kind == NodeKind::Element {
26            if let Some(name) = &node.name {
27                let tag = name.local.as_ref();
28                if matches!(tag, "input" | "select" | "textarea") {
29                    let attr = |local: &str| {
30                        node.attrs
31                            .iter()
32                            .find(|(n, _)| n.local.as_ref() == local)
33                            .map(|(_, v)| v.as_str())
34                    };
35                    if let Some(key) = attr("name").or_else(|| attr("id")) {
36                        let value = if tag == "textarea" {
37                            self.text_content(id)
38                        } else {
39                            attr("value").unwrap_or("").to_owned()
40                        };
41                        out.push((key.to_owned(), value));
42                    }
43                }
44            }
45        }
46        for &child in &node.children {
47            self.collect_form_values(child, out);
48        }
49    }
50
51    /// Concatenated descendant text — a `<textarea>`'s value.
52    fn text_content(&self, id: NodeId) -> String {
53        let mut out = String::new();
54        self.append_text_content(id, &mut out);
55        out
56    }
57
58    fn append_text_content(&self, id: NodeId, out: &mut String) {
59        let node = self.node(id);
60        if node.kind == NodeKind::Text {
61            if let Some(text) = &node.text {
62                out.push_str(text);
63            }
64        }
65        for &child in &node.children {
66            self.append_text_content(child, out);
67        }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use html5ever::{local_name, ns};
75    use layout_dom_api::{LayoutDomMut, QualName};
76
77    fn tag(local: markup5ever::LocalName) -> QualName {
78        QualName::new(None, ns!(html), local)
79    }
80    fn attr(local: markup5ever::LocalName) -> QualName {
81        QualName::new(None, ns!(), local)
82    }
83
84    #[test]
85    fn collects_input_value_keyed_by_name() {
86        let mut dom = ScriptedDom::new();
87        let form = dom.create_element(tag(local_name!("form")));
88        let input = dom.create_element(tag(local_name!("input")));
89        dom.set_attribute(input, attr(local_name!("name")), "email");
90        dom.set_attribute(input, attr(local_name!("value")), "a@b.com");
91        dom.append_child(form, input);
92        assert_eq!(
93            dom.form_values(form),
94            vec![("email".to_owned(), "a@b.com".to_owned())]
95        );
96    }
97
98    #[test]
99    fn textarea_value_is_its_text_content() {
100        let mut dom = ScriptedDom::new();
101        let ta = dom.create_element(tag(local_name!("textarea")));
102        dom.set_attribute(ta, attr(local_name!("id")), "note");
103        let text = dom.create_text("hello");
104        dom.append_child(ta, text);
105        assert_eq!(
106            dom.form_values(ta),
107            vec![("note".to_owned(), "hello".to_owned())]
108        );
109    }
110
111    #[test]
112    fn unnamed_controls_are_skipped() {
113        let mut dom = ScriptedDom::new();
114        let input = dom.create_element(tag(local_name!("input")));
115        dom.set_attribute(input, attr(local_name!("value")), "x");
116        assert!(dom.form_values(input).is_empty());
117    }
118}