Skip to main content

vs_protocol/
tree.rs

1//! In-memory tree representation and full-tree wire format.
2//!
3//! See `docs/PROTOCOL.md` § "Tree representation" for the wire spec.
4//! Each node line is:
5//!
6//! ```text
7//! <ref> <role> <label> [op[,op]...] [k=v]...
8//! ```
9//!
10//! …prefixed by two spaces of indent per nesting level. The encoder
11//! emits a canonical form (attributes sorted alphabetically, ops in
12//! source order via [`std::collections::BTreeSet`]); the parser is
13//! tolerant of either ordering.
14
15use std::collections::{BTreeMap, BTreeSet};
16use std::fmt::{self, Write as _};
17use std::str::FromStr;
18
19use crate::codes::{Op, Role};
20use crate::error::{ParseError, Result};
21use crate::tokenize::{quote_label, quote_value, split_indent, strip_quotes, Tokenizer};
22
23/// A stable session-scoped element identifier. Refs are positive
24/// integers; `0` is reserved as the wire sentinel for "root level"
25/// in delta operations.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
27pub struct Ref(pub u32);
28
29impl Ref {
30    /// The wire sentinel for "root level" parents in delta `Add` / `Move`.
31    pub const ROOT: Ref = Ref(0);
32
33    /// True if this ref is the root sentinel.
34    #[must_use]
35    pub const fn is_root(self) -> bool {
36        self.0 == 0
37    }
38}
39
40impl fmt::Display for Ref {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        write!(f, "{}", self.0)
43    }
44}
45
46impl FromStr for Ref {
47    type Err = ParseError;
48    fn from_str(s: &str) -> Result<Self> {
49        s.parse::<u32>()
50            .map(Ref)
51            .map_err(|_| ParseError::InvalidRef { raw: s.to_string() })
52    }
53}
54
55/// One node in the a11y tree.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Node {
58    pub r: Ref,
59    pub role: Role,
60    pub label: String,
61    pub ops: BTreeSet<Op>,
62    pub attrs: BTreeMap<String, String>,
63    pub children: Vec<Node>,
64}
65
66impl Node {
67    /// Build a leaf node with no ops, attrs, or children.
68    #[must_use]
69    pub fn leaf(r: Ref, role: Role, label: impl Into<String>) -> Self {
70        Self {
71            r,
72            role,
73            label: label.into(),
74            ops: BTreeSet::new(),
75            attrs: BTreeMap::new(),
76            children: Vec::new(),
77        }
78    }
79}
80
81/// A full a11y tree as exchanged on the wire.
82#[derive(Debug, Clone, Default, PartialEq, Eq)]
83pub struct Tree {
84    pub roots: Vec<Node>,
85}
86
87impl Tree {
88    #[must_use]
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    #[must_use]
94    pub fn from_root(root: Node) -> Self {
95        Self { roots: vec![root] }
96    }
97
98    /// Iterate all nodes in the tree in pre-order.
99    #[must_use]
100    pub fn iter(&self) -> TreeIter<'_> {
101        TreeIter::new(self)
102    }
103
104    /// Encode the tree as a sequence of indented lines (no trailing
105    /// blank line, no envelope). The result is the body that follows
106    /// a success envelope on the wire.
107    #[must_use]
108    pub fn encode(&self) -> String {
109        let mut out = String::new();
110        for root in &self.roots {
111            encode_node(root, 0, &mut out);
112        }
113        out
114    }
115
116    /// Parse the body of a `vs_view` response. The input must contain
117    /// only tree lines — no envelope, no warnings.
118    pub fn parse(input: &str) -> Result<Self> {
119        let mut entries: Vec<(usize, NodeShell)> = Vec::new();
120        for (idx, raw_line) in input.lines().enumerate() {
121            if raw_line.trim().is_empty() {
122                continue;
123            }
124            let (depth, body) = split_indent(raw_line);
125            let shell = parse_node_line(body, idx + 1)?;
126            entries.push((depth, shell));
127        }
128        Ok(assemble(&entries))
129    }
130}
131
132#[derive(Debug)]
133struct NodeShell {
134    r: Ref,
135    role: Role,
136    label: String,
137    ops: BTreeSet<Op>,
138    attrs: BTreeMap<String, String>,
139}
140
141fn assemble(entries: &[(usize, NodeShell)]) -> Tree {
142    let mut roots: Vec<Node> = Vec::new();
143    let mut stack: Vec<(usize, Node)> = Vec::new();
144
145    for (depth, shell) in entries {
146        // Pop any node at depth >= current; attach to its parent.
147        while let Some(&(top_depth, _)) = stack.last() {
148            if top_depth >= *depth {
149                let (_, popped) = stack.pop().expect("non-empty");
150                if let Some(parent) = stack.last_mut() {
151                    parent.1.children.push(popped);
152                } else {
153                    roots.push(popped);
154                }
155            } else {
156                break;
157            }
158        }
159        let node = Node {
160            r: shell.r,
161            role: shell.role,
162            label: shell.label.clone(),
163            ops: shell.ops.clone(),
164            attrs: shell.attrs.clone(),
165            children: Vec::new(),
166        };
167        stack.push((*depth, node));
168    }
169
170    while let Some((_, popped)) = stack.pop() {
171        if let Some(parent) = stack.last_mut() {
172            parent.1.children.push(popped);
173        } else {
174            roots.push(popped);
175        }
176    }
177
178    Tree { roots }
179}
180
181fn encode_node(node: &Node, depth: usize, out: &mut String) {
182    for _ in 0..depth {
183        out.push_str("  ");
184    }
185    write!(out, "{} {} {}", node.r, node.role, quote_label(&node.label))
186        .expect("write to String never fails");
187    if !node.ops.is_empty() {
188        out.push(' ');
189        let mut first = true;
190        for op in &node.ops {
191            if !first {
192                out.push(',');
193            }
194            out.push_str(op.as_str());
195            first = false;
196        }
197    }
198    for (k, v) in &node.attrs {
199        out.push(' ');
200        out.push_str(k);
201        out.push('=');
202        out.push_str(&quote_value(v));
203    }
204    out.push('\n');
205    for child in &node.children {
206        encode_node(child, depth + 1, out);
207    }
208}
209
210fn parse_node_line(line: &str, line_no: usize) -> Result<NodeShell> {
211    let mut tokens = Tokenizer::new(line);
212    let r_tok = tokens.next().ok_or(ParseError::MalformedLine {
213        line: line_no,
214        message: "missing ref",
215    })?;
216    let role_tok = tokens.next().ok_or(ParseError::MalformedLine {
217        line: line_no,
218        message: "missing role",
219    })?;
220    let label_tok = tokens.next().ok_or(ParseError::MalformedLine {
221        line: line_no,
222        message: "missing label",
223    })?;
224
225    let r: Ref = r_tok.parse()?;
226    let role: Role = role_tok.parse()?;
227    let label = strip_quotes(label_tok).to_string();
228
229    let mut ops = BTreeSet::new();
230    let mut attrs = BTreeMap::new();
231    for tok in tokens {
232        if let Some((k, v)) = tok.split_once('=') {
233            if k.is_empty() {
234                return Err(ParseError::InvalidAttribute { raw: tok.into() });
235            }
236            attrs.insert(k.to_string(), strip_quotes(v).to_string());
237        } else {
238            for piece in tok.split(',') {
239                if piece.is_empty() {
240                    return Err(ParseError::MalformedLine {
241                        line: line_no,
242                        message: "empty op in cluster",
243                    });
244                }
245                ops.insert(piece.parse::<Op>()?);
246            }
247        }
248    }
249
250    Ok(NodeShell {
251        r,
252        role,
253        label,
254        ops,
255        attrs,
256    })
257}
258
259impl<'a> IntoIterator for &'a Tree {
260    type Item = &'a Node;
261    type IntoIter = TreeIter<'a>;
262    fn into_iter(self) -> Self::IntoIter {
263        self.iter()
264    }
265}
266
267/// Pre-order iterator over a tree.
268pub struct TreeIter<'a> {
269    stack: Vec<&'a Node>,
270}
271
272impl<'a> TreeIter<'a> {
273    fn new(tree: &'a Tree) -> Self {
274        let mut stack: Vec<&'a Node> = Vec::new();
275        // Push roots in reverse so the first root pops first.
276        for root in tree.roots.iter().rev() {
277            stack.push(root);
278        }
279        Self { stack }
280    }
281}
282
283impl<'a> Iterator for TreeIter<'a> {
284    type Item = &'a Node;
285    fn next(&mut self) -> Option<&'a Node> {
286        let node = self.stack.pop()?;
287        for child in node.children.iter().rev() {
288            self.stack.push(child);
289        }
290        Some(node)
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::codes::Op;
298
299    fn sample_tree() -> Tree {
300        let mut p = Node::leaf(Ref(3), Role::P, "This domain is for examples.");
301        p.attrs.insert("lang".into(), "en".into());
302
303        let mut lnk = Node::leaf(Ref(4), Role::Lnk, "More information");
304        lnk.ops.insert(Op::Click);
305        lnk.attrs
306            .insert("href".into(), "https://www.iana.org/".into());
307
308        let mut hd = Node::leaf(Ref(2), Role::Hd, "Example Domain");
309        hd.attrs.insert("level".into(), "1".into());
310
311        let doc = Node {
312            r: Ref(1),
313            role: Role::Doc,
314            label: "Example Domain".into(),
315            ops: BTreeSet::new(),
316            attrs: BTreeMap::new(),
317            children: vec![hd, p, lnk],
318        };
319        Tree::from_root(doc)
320    }
321
322    #[test]
323    fn encode_sample_matches_spec_shape() {
324        let encoded = sample_tree().encode();
325        let expected = "\
3261 doc \"Example Domain\"
327  2 hd \"Example Domain\" level=1
328  3 p \"This domain is for examples.\" lang=en
329  4 lnk \"More information\" click href=https://www.iana.org/
330";
331        assert_eq!(encoded, expected);
332    }
333
334    #[test]
335    fn round_trip_sample() {
336        let t = sample_tree();
337        let s = t.encode();
338        let parsed = Tree::parse(&s).expect("parses");
339        assert_eq!(parsed, t);
340    }
341
342    #[test]
343    fn empty_tree_round_trips() {
344        let empty = Tree::new();
345        assert_eq!(empty.encode(), "");
346        assert_eq!(Tree::parse("").unwrap(), empty);
347    }
348
349    #[test]
350    fn parse_handles_blank_lines() {
351        let s = "\n1 doc empty\n\n";
352        let parsed = Tree::parse(s).unwrap();
353        assert_eq!(parsed.roots.len(), 1);
354        assert_eq!(parsed.roots[0].label, "empty");
355    }
356
357    #[test]
358    fn parse_rejects_unknown_role() {
359        let err = Tree::parse("1 zzz foo").unwrap_err();
360        matches!(err, ParseError::UnknownCode(_));
361    }
362
363    #[test]
364    fn parse_rejects_bad_ref() {
365        let err = Tree::parse("xx doc foo").unwrap_err();
366        matches!(err, ParseError::InvalidRef { .. });
367    }
368
369    #[test]
370    fn ops_cluster_round_trips() {
371        let mut node = Node::leaf(Ref(1), Role::Btn, "Submit");
372        node.ops.insert(Op::Click);
373        node.ops.insert(Op::Focus);
374        node.ops.insert(Op::Hover);
375        let tree = Tree::from_root(node);
376        let s = tree.encode();
377        // BTreeSet iter order is `Click < Fill < ... < Focus < Hover` per source order.
378        assert!(s.contains("click,hover,focus"));
379        let back = Tree::parse(&s).unwrap();
380        assert_eq!(back, tree);
381    }
382
383    #[test]
384    fn attribute_with_whitespace_quoted() {
385        let mut node = Node::leaf(Ref(1), Role::Tf, "");
386        node.attrs
387            .insert("placeholder".into(), "Your name here".into());
388        let s = Tree::from_root(node.clone()).encode();
389        assert!(s.contains("placeholder=\"Your name here\""));
390        let back = Tree::parse(&s).unwrap();
391        assert_eq!(back, Tree::from_root(node));
392    }
393
394    #[test]
395    fn iter_pre_order() {
396        let t = sample_tree();
397        let refs: Vec<u32> = t.iter().map(|n| n.r.0).collect();
398        assert_eq!(refs, vec![1, 2, 3, 4]);
399    }
400}