use texform_core::ast::{
Argument, ArgumentKind, ArgumentSlot, ArgumentValue, Ast, ContentMode, GroupKind, Node, NodeId,
NodeKind, ParentLink, Slot,
};
fn explicit_group(ast: &mut Ast, mode: ContentMode) -> NodeId {
let id = ast.new_node(Node::Group {
children: Vec::new(),
kind: GroupKind::Explicit,
mode,
});
ast.assert_invariants();
id
}
fn content_arg(kind: ArgumentKind, child: NodeId) -> ArgumentSlot {
Some(Argument {
kind,
value: ArgumentValue::MathContent(child),
})
}
#[test]
fn test_new_ast_starts_with_math_root() {
let ast = Ast::new();
let root = ast.root();
assert_eq!(ast.kind(root), NodeKind::Root);
assert!(ast.parent(root).is_none());
match ast.node(root) {
Node::Root { children, mode } => {
assert!(children.is_empty());
assert_eq!(*mode, ContentMode::Math);
}
other => panic!("Expected root node, got {:?}", other),
}
ast.assert_invariants();
}
#[test]
#[should_panic(expected = "Cannot create detached root node")]
fn test_new_node_rejects_root_variant() {
let mut ast = Ast::new();
let _ = ast.new_node(Node::Root {
children: Vec::new(),
mode: ContentMode::Math,
});
}
#[test]
#[should_panic(expected = "Cannot replace node with root variant")]
fn test_replace_node_rejects_root_variant() {
let mut ast = Ast::new();
let root = ast.root();
let child = ast.new_node(Node::Char('x'));
ast.assert_invariants();
ast.append_child(root, child);
ast.assert_invariants();
let _ = ast.replace_node(
child,
Node::Root {
children: Vec::new(),
mode: ContentMode::Math,
},
);
}
#[test]
fn test_group_child_editing_updates_navigation_and_detached_roots() {
let mut ast = Ast::new();
let root = ast.root();
let a = ast.new_node(Node::Char('a'));
ast.assert_invariants();
let b = ast.new_node(Node::Char('b'));
ast.assert_invariants();
let c = ast.new_node(Node::Char('c'));
ast.assert_invariants();
ast.append_child(root, a);
ast.assert_invariants();
ast.append_child(root, c);
ast.assert_invariants();
ast.insert_child(root, 1, b);
ast.assert_invariants();
assert_eq!(ast.children(root), [a, b, c].as_slice());
assert_eq!(
ast.parent(a),
Some(ParentLink {
parent: root,
slot: Slot::GroupChild(0),
})
);
assert_eq!(
ast.parent(b),
Some(ParentLink {
parent: root,
slot: Slot::GroupChild(1),
})
);
assert_eq!(
ast.parent(c),
Some(ParentLink {
parent: root,
slot: Slot::GroupChild(2),
})
);
assert_eq!(ast.next_sibling(a), Some(b));
assert_eq!(ast.next_sibling(b), Some(c));
assert_eq!(ast.prev_sibling(c), Some(b));
assert_eq!(ast.prev_sibling(a), None);
let detached = ast.detach(b);
ast.assert_invariants();
assert_eq!(detached, b);
assert_eq!(ast.children(root), [a, c].as_slice());
assert_eq!(ast.parent(b), None);
assert_eq!(ast.next_sibling(a), Some(c));
assert_eq!(ast.prev_sibling(c), Some(a));
let removed = ast.remove_detached(detached);
ast.assert_invariants();
assert_eq!(removed, Node::Char('b'));
assert!(!ast.contains(b));
}
#[test]
fn test_command_arg_slots_keep_non_content_values_out_of_tree_edges() {
let mut ast = Ast::new();
let root = ast.root();
let group = explicit_group(&mut ast, ContentMode::Math);
let x = ast.new_node(Node::Char('x'));
ast.assert_invariants();
ast.append_child(group, x);
ast.assert_invariants();
let command = ast.new_node(Node::Command {
name: "probe".to_string(),
args: vec![
Some(Argument {
kind: ArgumentKind::Star,
value: ArgumentValue::Boolean(true),
}),
None,
content_arg(ArgumentKind::Mandatory, group),
Some(Argument {
kind: ArgumentKind::Optional,
value: ArgumentValue::Dimension("1em".to_string()),
}),
],
known: true,
});
ast.assert_invariants();
ast.append_child(root, command);
ast.assert_invariants();
let slots = ast.arg_slots(command);
assert_eq!(slots.len(), 4);
let star = slots[0]
.as_ref()
.unwrap_or_else(|| panic!("Expected star slot to be present"));
assert_eq!(star.kind, ArgumentKind::Star);
assert_eq!(star.value, ArgumentValue::Boolean(true));
assert!(slots[1].is_none());
let dimension = slots[3]
.as_ref()
.unwrap_or_else(|| panic!("Expected dimension slot to be present"));
assert_eq!(dimension.value, ArgumentValue::Dimension("1em".to_string()));
assert_eq!(ast.edges(command), vec![(group, Slot::Argument(2))]);
assert_eq!(
ast.parent(group),
Some(ParentLink {
parent: command,
slot: Slot::Argument(2),
})
);
assert_eq!(
ast.find(root, |node| matches!(node, Node::Char('x'))),
Some(x)
);
assert_eq!(
ast.find_all(root, |node| matches!(node, Node::Char(_))),
vec![x]
);
}
#[test]
fn test_command_arg_slots_keep_scalar_variants_opaque() {
let mut ast = Ast::new();
let root = ast.root();
let content = explicit_group(&mut ast, ContentMode::Math);
let x = ast.new_node(Node::Char('x'));
ast.append_child(content, x);
let command = ast.new_node(Node::Command {
name: "probe".to_string(),
args: vec![
Some(Argument {
kind: ArgumentKind::Optional,
value: ArgumentValue::Integer("12".to_string()),
}),
Some(Argument {
kind: ArgumentKind::Mandatory,
value: ArgumentValue::MathContent(content),
}),
],
known: true,
});
ast.append_child(root, command);
ast.assert_invariants();
assert_eq!(ast.edges(command), vec![(content, Slot::Argument(1))]);
}
#[test]
fn test_replace_node_reuses_children_and_detaches_removed_subtrees() {
let mut ast = Ast::new();
let root = ast.root();
let lhs = explicit_group(&mut ast, ContentMode::Math);
let a = ast.new_node(Node::Char('a'));
ast.assert_invariants();
ast.append_child(lhs, a);
ast.assert_invariants();
let rhs = explicit_group(&mut ast, ContentMode::Math);
let b = ast.new_node(Node::Char('b'));
ast.assert_invariants();
ast.append_child(rhs, b);
ast.assert_invariants();
let command = ast.new_node(Node::Command {
name: "pair".to_string(),
args: vec![
content_arg(ArgumentKind::Mandatory, lhs),
content_arg(ArgumentKind::Mandatory, rhs),
],
known: true,
});
ast.assert_invariants();
ast.append_child(root, command);
ast.assert_invariants();
let sub_id = ast.new_node(Node::Char('i'));
ast.assert_invariants();
let old_node = ast.replace_node(
command,
Node::Scripted {
base: lhs,
subscript: Some(sub_id),
superscript: None,
},
);
ast.assert_invariants();
match old_node {
Node::Command { name, args, .. } => {
assert_eq!(name, "pair");
assert_eq!(args.len(), 2);
}
other => panic!("Expected old command node, got {:?}", other),
}
match ast.node(command) {
Node::Scripted {
base,
subscript,
superscript,
} => {
assert_eq!(*base, lhs);
assert_eq!(*subscript, Some(sub_id));
assert_eq!(*superscript, None);
}
other => panic!("Expected scripted node, got {:?}", other),
}
assert_eq!(
ast.parent(lhs),
Some(ParentLink {
parent: command,
slot: Slot::ScriptBase,
})
);
assert_eq!(
ast.parent(sub_id),
Some(ParentLink {
parent: command,
slot: Slot::ScriptSub,
})
);
assert_eq!(ast.parent(rhs), None);
let removed_rhs = ast.remove_detached(rhs);
ast.assert_invariants();
match removed_rhs {
Node::Group { kind, mode, .. } => {
assert_eq!(kind, GroupKind::Explicit);
assert_eq!(mode, ContentMode::Math);
}
other => panic!("Expected detached group, got {:?}", other),
}
assert!(!ast.contains(rhs));
assert!(!ast.contains(b));
}
#[test]
fn test_remove_node_deletes_attached_subtree() {
let mut ast = Ast::new();
let root = ast.root();
let numerator = explicit_group(&mut ast, ContentMode::Math);
let a = ast.new_node(Node::Char('a'));
ast.assert_invariants();
ast.append_child(numerator, a);
ast.assert_invariants();
let denominator = explicit_group(&mut ast, ContentMode::Math);
let b = ast.new_node(Node::Char('b'));
ast.assert_invariants();
ast.append_child(denominator, b);
ast.assert_invariants();
let frac = ast.new_node(Node::Command {
name: "frac".to_string(),
args: vec![
content_arg(ArgumentKind::Mandatory, numerator),
content_arg(ArgumentKind::Mandatory, denominator),
],
known: true,
});
ast.assert_invariants();
ast.append_child(root, frac);
ast.assert_invariants();
ast.remove_node(frac);
ast.assert_invariants();
assert!(ast.children(root).is_empty());
assert!(!ast.contains(frac));
assert!(!ast.contains(numerator));
assert!(!ast.contains(denominator));
assert!(!ast.contains(a));
assert!(!ast.contains(b));
}
#[test]
#[should_panic(expected = "Cannot attach child that already has a parent")]
fn test_append_child_rejects_attached_child() {
let mut ast = Ast::new();
let root = ast.root();
let group = explicit_group(&mut ast, ContentMode::Math);
let child = ast.new_node(Node::Char('x'));
ast.assert_invariants();
ast.append_child(root, group);
ast.assert_invariants();
ast.append_child(group, child);
ast.assert_invariants();
ast.append_child(root, child);
}
#[test]
#[should_panic(expected = "Can only detach GroupChild nodes")]
fn test_detach_rejects_non_group_child_nodes() {
let mut ast = Ast::new();
let root = ast.root();
let arg = explicit_group(&mut ast, ContentMode::Math);
let command = ast.new_node(Node::Command {
name: "cmd".to_string(),
args: vec![content_arg(ArgumentKind::Mandatory, arg)],
known: true,
});
ast.assert_invariants();
ast.append_child(root, command);
ast.assert_invariants();
ast.detach(arg);
}
#[test]
#[should_panic(expected = "Can only remove detached roots")]
fn test_remove_detached_rejects_attached_nodes() {
let mut ast = Ast::new();
let root = ast.root();
let child = ast.new_node(Node::Char('x'));
ast.assert_invariants();
ast.append_child(root, child);
ast.assert_invariants();
ast.remove_detached(child);
}
#[test]
#[should_panic(expected = "Environment body must be a Group node")]
fn test_environment_body_must_be_group() {
let mut ast = Ast::new();
let body = ast.new_node(Node::Char('x'));
ast.assert_invariants();
let _ = ast.new_node(Node::Environment {
name: "aligned".to_string(),
args: vec![],
known: true,
body,
});
}