use std::collections::{BTreeMap, BTreeSet};
use std::fmt::{self, Write as _};
use std::str::FromStr;
use crate::codes::{Op, Role};
use crate::error::{ParseError, Result};
use crate::tokenize::{quote_label, quote_value, split_indent, strip_quotes, Tokenizer};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Ref(pub u32);
impl Ref {
pub const ROOT: Ref = Ref(0);
#[must_use]
pub const fn is_root(self) -> bool {
self.0 == 0
}
}
impl fmt::Display for Ref {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for Ref {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self> {
s.parse::<u32>()
.map(Ref)
.map_err(|_| ParseError::InvalidRef { raw: s.to_string() })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Node {
pub r: Ref,
pub role: Role,
pub label: String,
pub ops: BTreeSet<Op>,
pub attrs: BTreeMap<String, String>,
pub children: Vec<Node>,
}
impl Node {
#[must_use]
pub fn leaf(r: Ref, role: Role, label: impl Into<String>) -> Self {
Self {
r,
role,
label: label.into(),
ops: BTreeSet::new(),
attrs: BTreeMap::new(),
children: Vec::new(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Tree {
pub roots: Vec<Node>,
}
impl Tree {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_root(root: Node) -> Self {
Self { roots: vec![root] }
}
#[must_use]
pub fn iter(&self) -> TreeIter<'_> {
TreeIter::new(self)
}
#[must_use]
pub fn encode(&self) -> String {
let mut out = String::new();
for root in &self.roots {
encode_node(root, 0, &mut out);
}
out
}
pub fn parse(input: &str) -> Result<Self> {
let mut entries: Vec<(usize, NodeShell)> = Vec::new();
for (idx, raw_line) in input.lines().enumerate() {
if raw_line.trim().is_empty() {
continue;
}
let (depth, body) = split_indent(raw_line);
let shell = parse_node_line(body, idx + 1)?;
entries.push((depth, shell));
}
Ok(assemble(&entries))
}
}
#[derive(Debug)]
struct NodeShell {
r: Ref,
role: Role,
label: String,
ops: BTreeSet<Op>,
attrs: BTreeMap<String, String>,
}
fn assemble(entries: &[(usize, NodeShell)]) -> Tree {
let mut roots: Vec<Node> = Vec::new();
let mut stack: Vec<(usize, Node)> = Vec::new();
for (depth, shell) in entries {
while let Some(&(top_depth, _)) = stack.last() {
if top_depth >= *depth {
let (_, popped) = stack.pop().expect("non-empty");
if let Some(parent) = stack.last_mut() {
parent.1.children.push(popped);
} else {
roots.push(popped);
}
} else {
break;
}
}
let node = Node {
r: shell.r,
role: shell.role,
label: shell.label.clone(),
ops: shell.ops.clone(),
attrs: shell.attrs.clone(),
children: Vec::new(),
};
stack.push((*depth, node));
}
while let Some((_, popped)) = stack.pop() {
if let Some(parent) = stack.last_mut() {
parent.1.children.push(popped);
} else {
roots.push(popped);
}
}
Tree { roots }
}
fn encode_node(node: &Node, depth: usize, out: &mut String) {
for _ in 0..depth {
out.push_str(" ");
}
write!(out, "{} {} {}", node.r, node.role, quote_label(&node.label))
.expect("write to String never fails");
if !node.ops.is_empty() {
out.push(' ');
let mut first = true;
for op in &node.ops {
if !first {
out.push(',');
}
out.push_str(op.as_str());
first = false;
}
}
for (k, v) in &node.attrs {
out.push(' ');
out.push_str(k);
out.push('=');
out.push_str("e_value(v));
}
out.push('\n');
for child in &node.children {
encode_node(child, depth + 1, out);
}
}
fn parse_node_line(line: &str, line_no: usize) -> Result<NodeShell> {
let mut tokens = Tokenizer::new(line);
let r_tok = tokens.next().ok_or(ParseError::MalformedLine {
line: line_no,
message: "missing ref",
})?;
let role_tok = tokens.next().ok_or(ParseError::MalformedLine {
line: line_no,
message: "missing role",
})?;
let label_tok = tokens.next().ok_or(ParseError::MalformedLine {
line: line_no,
message: "missing label",
})?;
let r: Ref = r_tok.parse()?;
let role: Role = role_tok.parse()?;
let label = strip_quotes(label_tok).to_string();
let mut ops = BTreeSet::new();
let mut attrs = BTreeMap::new();
for tok in tokens {
if let Some((k, v)) = tok.split_once('=') {
if k.is_empty() {
return Err(ParseError::InvalidAttribute { raw: tok.into() });
}
attrs.insert(k.to_string(), strip_quotes(v).to_string());
} else {
for piece in tok.split(',') {
if piece.is_empty() {
return Err(ParseError::MalformedLine {
line: line_no,
message: "empty op in cluster",
});
}
ops.insert(piece.parse::<Op>()?);
}
}
}
Ok(NodeShell {
r,
role,
label,
ops,
attrs,
})
}
impl<'a> IntoIterator for &'a Tree {
type Item = &'a Node;
type IntoIter = TreeIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
pub struct TreeIter<'a> {
stack: Vec<&'a Node>,
}
impl<'a> TreeIter<'a> {
fn new(tree: &'a Tree) -> Self {
let mut stack: Vec<&'a Node> = Vec::new();
for root in tree.roots.iter().rev() {
stack.push(root);
}
Self { stack }
}
}
impl<'a> Iterator for TreeIter<'a> {
type Item = &'a Node;
fn next(&mut self) -> Option<&'a Node> {
let node = self.stack.pop()?;
for child in node.children.iter().rev() {
self.stack.push(child);
}
Some(node)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codes::Op;
fn sample_tree() -> Tree {
let mut p = Node::leaf(Ref(3), Role::P, "This domain is for examples.");
p.attrs.insert("lang".into(), "en".into());
let mut lnk = Node::leaf(Ref(4), Role::Lnk, "More information");
lnk.ops.insert(Op::Click);
lnk.attrs
.insert("href".into(), "https://www.iana.org/".into());
let mut hd = Node::leaf(Ref(2), Role::Hd, "Example Domain");
hd.attrs.insert("level".into(), "1".into());
let doc = Node {
r: Ref(1),
role: Role::Doc,
label: "Example Domain".into(),
ops: BTreeSet::new(),
attrs: BTreeMap::new(),
children: vec![hd, p, lnk],
};
Tree::from_root(doc)
}
#[test]
fn encode_sample_matches_spec_shape() {
let encoded = sample_tree().encode();
let expected = "\
1 doc \"Example Domain\"
2 hd \"Example Domain\" level=1
3 p \"This domain is for examples.\" lang=en
4 lnk \"More information\" click href=https://www.iana.org/
";
assert_eq!(encoded, expected);
}
#[test]
fn round_trip_sample() {
let t = sample_tree();
let s = t.encode();
let parsed = Tree::parse(&s).expect("parses");
assert_eq!(parsed, t);
}
#[test]
fn empty_tree_round_trips() {
let empty = Tree::new();
assert_eq!(empty.encode(), "");
assert_eq!(Tree::parse("").unwrap(), empty);
}
#[test]
fn parse_handles_blank_lines() {
let s = "\n1 doc empty\n\n";
let parsed = Tree::parse(s).unwrap();
assert_eq!(parsed.roots.len(), 1);
assert_eq!(parsed.roots[0].label, "empty");
}
#[test]
fn parse_rejects_unknown_role() {
let err = Tree::parse("1 zzz foo").unwrap_err();
matches!(err, ParseError::UnknownCode(_));
}
#[test]
fn parse_rejects_bad_ref() {
let err = Tree::parse("xx doc foo").unwrap_err();
matches!(err, ParseError::InvalidRef { .. });
}
#[test]
fn ops_cluster_round_trips() {
let mut node = Node::leaf(Ref(1), Role::Btn, "Submit");
node.ops.insert(Op::Click);
node.ops.insert(Op::Focus);
node.ops.insert(Op::Hover);
let tree = Tree::from_root(node);
let s = tree.encode();
assert!(s.contains("click,hover,focus"));
let back = Tree::parse(&s).unwrap();
assert_eq!(back, tree);
}
#[test]
fn attribute_with_whitespace_quoted() {
let mut node = Node::leaf(Ref(1), Role::Tf, "");
node.attrs
.insert("placeholder".into(), "Your name here".into());
let s = Tree::from_root(node.clone()).encode();
assert!(s.contains("placeholder=\"Your name here\""));
let back = Tree::parse(&s).unwrap();
assert_eq!(back, Tree::from_root(node));
}
#[test]
fn iter_pre_order() {
let t = sample_tree();
let refs: Vec<u32> = t.iter().map(|n| n.r.0).collect();
assert_eq!(refs, vec![1, 2, 3, 4]);
}
}