use crate::ir::{IRChapter, NodeId, Role};
pub fn optimize(chapter: &mut IRChapter) {
vacuum(chapter);
hoist_nested_inlines(chapter);
merge_adjacent_spans(chapter);
fuse_lists(chapter);
wrap_mixed_content(chapter);
prune_empty(chapter);
}
fn vacuum(chapter: &mut IRChapter) {
if chapter.node_count() > 0 {
vacuum_children(chapter, NodeId::ROOT);
}
}
fn vacuum_children(chapter: &mut IRChapter, parent_id: NodeId) {
let mut child_opt = chapter.node(parent_id).and_then(|n| n.first_child);
while let Some(child_id) = child_opt {
vacuum_children(chapter, child_id);
child_opt = chapter.node(child_id).and_then(|n| n.next_sibling);
}
let parent_role = chapter.node(parent_id).map(|n| n.role);
if !is_structural_container(parent_role) {
return;
}
let mut prev_opt: Option<NodeId> = None;
let mut cursor_opt = chapter.node(parent_id).and_then(|n| n.first_child);
while let Some(current_id) = cursor_opt {
let next_opt = chapter.node(current_id).and_then(|n| n.next_sibling);
if should_vacuum(chapter, current_id) {
if let Some(prev_id) = prev_opt {
if let Some(prev_node) = chapter.node_mut(prev_id) {
prev_node.next_sibling = next_opt;
}
} else {
if let Some(parent_node) = chapter.node_mut(parent_id) {
parent_node.first_child = next_opt;
}
}
} else {
prev_opt = Some(current_id);
}
cursor_opt = next_opt;
}
}
fn should_vacuum(chapter: &IRChapter, node_id: NodeId) -> bool {
let node = match chapter.node(node_id) {
Some(n) => n,
None => return false,
};
if node.role != Role::Text {
return false;
}
if node.first_child.is_some() {
return false;
}
if chapter.semantics.id(node_id).is_some() {
return false;
}
if node.text.is_empty() {
return true; }
let text = chapter.text(node.text);
text.trim().is_empty()
}
fn is_structural_container(role: Option<Role>) -> bool {
matches!(
role,
Some(
Role::Root
| Role::Container | Role::Figure
| Role::Sidebar
| Role::Footnote
| Role::Table
| Role::TableRow
| Role::OrderedList
| Role::UnorderedList
| Role::DefinitionList
)
)
}
fn hoist_nested_inlines(chapter: &mut IRChapter) {
if chapter.node_count() == 0 {
return;
}
let mut changed = true;
let mut attempts = 0;
while changed && attempts < 5 {
changed = hoist_pass(chapter, NodeId::ROOT);
attempts += 1;
}
}
fn hoist_pass(chapter: &mut IRChapter, parent_id: NodeId) -> bool {
let mut changed = false;
let mut child_opt = chapter.node(parent_id).and_then(|n| n.first_child);
while let Some(child_id) = child_opt {
if hoist_pass(chapter, child_id) {
changed = true;
}
child_opt = chapter.node(child_id).and_then(|n| n.next_sibling);
}
let mut prev_opt: Option<NodeId> = None;
let mut cursor_opt = chapter.node(parent_id).and_then(|n| n.first_child);
while let Some(current_id) = cursor_opt {
let next_opt = chapter.node(current_id).and_then(|n| n.next_sibling);
if is_redundant_wrapper(chapter, current_id) {
let child_id = chapter
.node(current_id)
.and_then(|n| n.first_child)
.unwrap();
if let Some(child_node) = chapter.node_mut(child_id) {
child_node.parent = Some(parent_id);
child_node.next_sibling = next_opt;
}
if let Some(prev_id) = prev_opt {
if let Some(prev_node) = chapter.node_mut(prev_id) {
prev_node.next_sibling = Some(child_id);
}
} else if let Some(parent_node) = chapter.node_mut(parent_id) {
parent_node.first_child = Some(child_id);
}
if let Some(wrapper_node) = chapter.node_mut(current_id) {
wrapper_node.first_child = None;
wrapper_node.next_sibling = None;
}
prev_opt = Some(child_id);
cursor_opt = next_opt;
changed = true;
} else {
prev_opt = Some(current_id);
cursor_opt = next_opt;
}
}
changed
}
fn is_redundant_wrapper(chapter: &IRChapter, node_id: NodeId) -> bool {
let node = match chapter.node(node_id) {
Some(n) => n,
None => return false,
};
if !matches!(node.role, Role::Container | Role::Inline) {
return false;
}
let first_child = match node.first_child {
Some(id) => id,
None => return false,
};
if chapter
.node(first_child)
.and_then(|n| n.next_sibling)
.is_some()
{
return false;
}
if has_semantic_attrs(chapter, node_id) {
return false;
}
if !node.text.is_empty() {
return false;
}
true
}
fn merge_adjacent_spans(chapter: &mut IRChapter) {
if chapter.node_count() > 0 {
merge_children(chapter, NodeId::ROOT);
}
}
fn merge_children(chapter: &mut IRChapter, parent_id: NodeId) {
let mut child_opt = chapter.node(parent_id).and_then(|n| n.first_child);
while let Some(child_id) = child_opt {
merge_children(chapter, child_id);
child_opt = chapter.node(child_id).and_then(|n| n.next_sibling);
}
let mut cursor_opt = chapter.node(parent_id).and_then(|n| n.first_child);
while let Some(current_id) = cursor_opt {
let next_opt = chapter.node(current_id).and_then(|n| n.next_sibling);
if let Some(next_id) = next_opt
&& can_merge_spans(chapter, current_id, next_id)
{
let next_len = chapter.node(next_id).map(|n| n.text.len).unwrap_or(0);
if let Some(current_node) = chapter.node_mut(current_id) {
current_node.text.len += next_len;
}
let new_next = chapter.node(next_id).and_then(|n| n.next_sibling);
if let Some(current_node) = chapter.node_mut(current_id) {
current_node.next_sibling = new_next;
}
continue;
}
cursor_opt = next_opt;
}
}
fn can_merge_spans(chapter: &IRChapter, left_id: NodeId, right_id: NodeId) -> bool {
let (left, right) = match (chapter.node(left_id), chapter.node(right_id)) {
(Some(l), Some(r)) => (l, r),
_ => return false,
};
if left.role != Role::Text || right.role != Role::Text {
return false;
}
if left.text.is_empty() || right.text.is_empty() {
return false;
}
if left.style != right.style {
return false;
}
if has_semantic_attrs(chapter, left_id) || has_semantic_attrs(chapter, right_id) {
return false;
}
if left.text.end() != right.text.start {
return false;
}
true
}
fn fuse_lists(chapter: &mut IRChapter) {
if chapter.node_count() > 0 {
fuse_list_children(chapter, NodeId::ROOT);
}
}
fn fuse_list_children(chapter: &mut IRChapter, parent_id: NodeId) {
let mut child_opt = chapter.node(parent_id).and_then(|n| n.first_child);
while let Some(child_id) = child_opt {
fuse_list_children(chapter, child_id);
child_opt = chapter.node(child_id).and_then(|n| n.next_sibling);
}
let mut cursor_opt = chapter.node(parent_id).and_then(|n| n.first_child);
while let Some(current_id) = cursor_opt {
let next_opt = chapter.node(current_id).and_then(|n| n.next_sibling);
if let Some(next_id) = next_opt
&& can_fuse_lists(chapter, current_id, next_id)
{
fuse_list_pair(chapter, current_id, next_id);
continue;
}
cursor_opt = next_opt;
}
}
fn can_fuse_lists(chapter: &IRChapter, left_id: NodeId, right_id: NodeId) -> bool {
let (left, right) = match (chapter.node(left_id), chapter.node(right_id)) {
(Some(l), Some(r)) => (l, r),
_ => return false,
};
matches!(
(left.role, right.role),
(Role::OrderedList, Role::OrderedList) | (Role::UnorderedList, Role::UnorderedList)
)
}
fn fuse_list_pair(chapter: &mut IRChapter, left_id: NodeId, right_id: NodeId) {
let right_first = chapter.node(right_id).and_then(|n| n.first_child);
if right_first.is_none() {
let right_next = chapter.node(right_id).and_then(|n| n.next_sibling);
if let Some(left_node) = chapter.node_mut(left_id) {
left_node.next_sibling = right_next;
}
return;
}
let mut child_opt = right_first;
while let Some(child_id) = child_opt {
let next_child = chapter.node(child_id).and_then(|n| n.next_sibling);
if let Some(child_node) = chapter.node_mut(child_id) {
child_node.parent = Some(left_id);
}
child_opt = next_child;
}
let mut left_last = chapter.node(left_id).and_then(|n| n.first_child);
if let Some(mut current) = left_last {
while let Some(next) = chapter.node(current).and_then(|n| n.next_sibling) {
current = next;
}
left_last = Some(current);
}
if let Some(last_id) = left_last {
if let Some(last_node) = chapter.node_mut(last_id) {
last_node.next_sibling = right_first;
}
} else {
if let Some(left_node) = chapter.node_mut(left_id) {
left_node.first_child = right_first;
}
}
let right_next = chapter.node(right_id).and_then(|n| n.next_sibling);
if let Some(left_node) = chapter.node_mut(left_id) {
left_node.next_sibling = right_next;
}
}
fn wrap_mixed_content(chapter: &mut IRChapter) {
if chapter.node_count() > 0 {
wrap_mixed_children(chapter, NodeId::ROOT);
}
}
fn wrap_mixed_children(chapter: &mut IRChapter, parent_id: NodeId) {
let mut child_opt = chapter.node(parent_id).and_then(|n| n.first_child);
while let Some(child_id) = child_opt {
wrap_mixed_children(chapter, child_id);
child_opt = chapter.node(child_id).and_then(|n| n.next_sibling);
}
let parent_role = chapter.node(parent_id).map(|n| n.role);
if !is_block_container(parent_role) {
return;
}
let (has_inline, has_block) = analyze_children(chapter, parent_id);
if !has_inline || !has_block {
return; }
wrap_inline_runs(chapter, parent_id);
}
fn is_block_container(role: Option<Role>) -> bool {
matches!(
role,
Some(
Role::Root
| Role::Container
| Role::BlockQuote
| Role::Figure
| Role::Sidebar
| Role::Footnote
| Role::ListItem
| Role::TableCell
)
)
}
fn is_inline_role(role: Role) -> bool {
matches!(
role,
Role::Text | Role::Inline | Role::Link | Role::Image | Role::Break
)
}
fn analyze_children(chapter: &IRChapter, parent_id: NodeId) -> (bool, bool) {
let mut has_inline = false;
let mut has_block = false;
let mut child_opt = chapter.node(parent_id).and_then(|n| n.first_child);
while let Some(child_id) = child_opt {
if let Some(child) = chapter.node(child_id) {
if is_inline_role(child.role) {
has_inline = true;
} else {
has_block = true;
}
}
child_opt = chapter.node(child_id).and_then(|n| n.next_sibling);
}
(has_inline, has_block)
}
fn wrap_inline_runs(chapter: &mut IRChapter, parent_id: NodeId) {
let mut children_info: Vec<(NodeId, bool)> = Vec::new();
let mut child_opt = chapter.node(parent_id).and_then(|n| n.first_child);
while let Some(child_id) = child_opt {
let is_inline = chapter
.node(child_id)
.map(|n| is_inline_role(n.role))
.unwrap_or(false);
children_info.push((child_id, is_inline));
child_opt = chapter.node(child_id).and_then(|n| n.next_sibling);
}
let mut runs: Vec<(usize, usize)> = Vec::new(); let mut run_start: Option<usize> = None;
for (idx, &(_, is_inline)) in children_info.iter().enumerate() {
if is_inline {
if run_start.is_none() {
run_start = Some(idx);
}
} else if let Some(start) = run_start {
runs.push((start, idx - 1));
run_start = None;
}
}
if let Some(start) = run_start {
runs.push((start, children_info.len() - 1));
}
for (start_idx, end_idx) in runs.into_iter().rev() {
wrap_run(chapter, parent_id, &children_info, start_idx, end_idx);
}
}
fn wrap_run(
chapter: &mut IRChapter,
parent_id: NodeId,
children_info: &[(NodeId, bool)],
start_idx: usize,
end_idx: usize,
) {
use crate::ir::Node;
let wrapper_id = chapter.alloc_node(Node::new(Role::Container));
if let Some(wrapper) = chapter.node_mut(wrapper_id) {
wrapper.parent = Some(parent_id);
}
let last_inline_id = children_info[end_idx].0;
let after_run = chapter.node(last_inline_id).and_then(|n| n.next_sibling);
let mut prev_in_wrapper: Option<NodeId> = None;
for (child_id, _) in &children_info[start_idx..=end_idx] {
let child_id = *child_id;
if let Some(child) = chapter.node_mut(child_id) {
child.parent = Some(wrapper_id);
}
if let Some(prev_id) = prev_in_wrapper {
if let Some(prev) = chapter.node_mut(prev_id) {
prev.next_sibling = Some(child_id);
}
} else {
if let Some(wrapper) = chapter.node_mut(wrapper_id) {
wrapper.first_child = Some(child_id);
}
}
prev_in_wrapper = Some(child_id);
}
if let Some(last) = chapter.node_mut(last_inline_id) {
last.next_sibling = None;
}
if let Some(wrapper) = chapter.node_mut(wrapper_id) {
wrapper.next_sibling = after_run;
}
if start_idx == 0 {
if let Some(parent) = chapter.node_mut(parent_id) {
parent.first_child = Some(wrapper_id);
}
} else {
let prev_sibling_id = children_info[start_idx - 1].0;
if let Some(prev) = chapter.node_mut(prev_sibling_id) {
prev.next_sibling = Some(wrapper_id);
}
}
}
fn prune_empty(chapter: &mut IRChapter) {
if chapter.node_count() > 0 {
prune_children(chapter, NodeId::ROOT);
}
}
fn prune_children(chapter: &mut IRChapter, parent_id: NodeId) {
let mut child_opt = chapter.node(parent_id).and_then(|n| n.first_child);
while let Some(child_id) = child_opt {
prune_children(chapter, child_id);
child_opt = chapter.node(child_id).and_then(|n| n.next_sibling);
}
let mut prev_opt: Option<NodeId> = None;
let mut cursor_opt = chapter.node(parent_id).and_then(|n| n.first_child);
while let Some(current_id) = cursor_opt {
let next_opt = chapter.node(current_id).and_then(|n| n.next_sibling);
if should_prune(chapter, current_id) {
if let Some(prev_id) = prev_opt {
if let Some(prev_node) = chapter.node_mut(prev_id) {
prev_node.next_sibling = next_opt;
}
} else if let Some(parent_node) = chapter.node_mut(parent_id) {
parent_node.first_child = next_opt;
}
} else {
prev_opt = Some(current_id);
}
cursor_opt = next_opt;
}
}
fn should_prune(chapter: &IRChapter, node_id: NodeId) -> bool {
let node = match chapter.node(node_id) {
Some(n) => n,
None => return false,
};
if !is_prunable_role(node.role) {
return false;
}
if node.first_child.is_some() {
return false;
}
if !node.text.is_empty() {
return false;
}
if chapter.semantics.id(node_id).is_some() {
return false;
}
if chapter.semantics.src(node_id).is_some() {
return false;
}
true
}
fn is_prunable_role(role: Role) -> bool {
matches!(
role,
Role::Container
| Role::Inline
| Role::Figure
| Role::Sidebar
| Role::Footnote
| Role::BlockQuote
| Role::OrderedList
| Role::UnorderedList
| Role::DefinitionList
| Role::Table
| Role::TableRow
)
}
fn has_semantic_attrs(chapter: &IRChapter, node_id: NodeId) -> bool {
let s = &chapter.semantics;
s.href(node_id).is_some()
|| s.src(node_id).is_some()
|| s.alt(node_id).is_some()
|| s.id(node_id).is_some()
|| s.title(node_id).is_some()
|| s.lang(node_id).is_some()
|| s.epub_type(node_id).is_some()
|| s.aria_role(node_id).is_some()
|| s.datetime(node_id).is_some()
|| s.language(node_id).is_some()
|| s.list_start(node_id).is_some()
|| s.row_span(node_id).is_some()
|| s.col_span(node_id).is_some()
|| s.is_header_cell(node_id)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::Node;
#[test]
fn test_vacuum_removes_whitespace_in_structural_container() {
let mut chapter = IRChapter::new();
let list = chapter.alloc_node(Node::new(Role::UnorderedList));
chapter.append_child(NodeId::ROOT, list);
let ws1 = chapter.append_text("\n ");
let ws1_node = chapter.alloc_node(Node::text(ws1));
chapter.append_child(list, ws1_node);
let item = chapter.alloc_node(Node::new(Role::ListItem));
chapter.append_child(list, item);
let ws2 = chapter.append_text("\n ");
let ws2_node = chapter.alloc_node(Node::text(ws2));
chapter.append_child(list, ws2_node);
assert_eq!(chapter.children(list).count(), 3);
vacuum(&mut chapter);
let children: Vec<_> = chapter.children(list).collect();
assert_eq!(children.len(), 1);
assert_eq!(chapter.node(children[0]).unwrap().role, Role::ListItem);
}
#[test]
fn test_vacuum_preserves_whitespace_in_inline() {
let mut chapter = IRChapter::new();
let inline = chapter.alloc_node(Node::new(Role::Inline));
chapter.append_child(NodeId::ROOT, inline);
let ws = chapter.append_text(" ");
let ws_node = chapter.alloc_node(Node::text(ws));
chapter.append_child(inline, ws_node);
let text = chapter.append_text("word");
let text_node = chapter.alloc_node(Node::text(text));
chapter.append_child(inline, text_node);
vacuum(&mut chapter);
assert_eq!(chapter.children(inline).count(), 2);
}
#[test]
fn test_vacuum_preserves_whitespace_in_paragraph() {
let mut chapter = IRChapter::new();
let para = chapter.alloc_node(Node::new(Role::Paragraph));
chapter.append_child(NodeId::ROOT, para);
let span1 = chapter.alloc_node(Node::new(Role::Inline));
chapter.append_child(para, span1);
let text1 = chapter.append_text("Hello");
let text1_node = chapter.alloc_node(Node::text(text1));
chapter.append_child(span1, text1_node);
let ws = chapter.append_text(" ");
let ws_node = chapter.alloc_node(Node::text(ws));
chapter.append_child(para, ws_node);
let span2 = chapter.alloc_node(Node::new(Role::Inline));
chapter.append_child(para, span2);
let text2 = chapter.append_text("World");
let text2_node = chapter.alloc_node(Node::text(text2));
chapter.append_child(span2, text2_node);
assert_eq!(chapter.children(para).count(), 3);
vacuum(&mut chapter);
assert_eq!(chapter.children(para).count(), 3);
}
#[test]
fn test_vacuum_preserves_node_with_id() {
let mut chapter = IRChapter::new();
let list = chapter.alloc_node(Node::new(Role::UnorderedList));
chapter.append_child(NodeId::ROOT, list);
let ws = chapter.append_text(" ");
let ws_node = chapter.alloc_node(Node::text(ws));
chapter.append_child(list, ws_node);
chapter.semantics.set_id(ws_node, "anchor".to_string());
let item = chapter.alloc_node(Node::new(Role::ListItem));
chapter.append_child(list, item);
vacuum(&mut chapter);
assert_eq!(chapter.children(list).count(), 2);
}
#[test]
fn test_hoist_dissolves_single_child_wrapper() {
let mut chapter = IRChapter::new();
let container = chapter.alloc_node(Node::new(Role::Container));
chapter.append_child(NodeId::ROOT, container);
let inline = chapter.alloc_node(Node::new(Role::Inline));
chapter.append_child(container, inline);
let text_range = chapter.append_text("Hello");
let text_node = chapter.alloc_node(Node::text(text_range));
chapter.append_child(inline, text_node);
assert_eq!(chapter.children(NodeId::ROOT).count(), 1);
let root_child = chapter.children(NodeId::ROOT).next().unwrap();
assert_eq!(chapter.node(root_child).unwrap().role, Role::Container);
hoist_nested_inlines(&mut chapter);
let root_children: Vec<_> = chapter.children(NodeId::ROOT).collect();
assert_eq!(root_children.len(), 1);
assert_eq!(chapter.node(root_children[0]).unwrap().role, Role::Text);
}
#[test]
fn test_hoist_enables_span_merge() {
let mut chapter = IRChapter::new();
let c1 = chapter.alloc_node(Node::new(Role::Container));
chapter.append_child(NodeId::ROOT, c1);
let i1 = chapter.alloc_node(Node::new(Role::Inline));
chapter.append_child(c1, i1);
let t1 = chapter.append_text("T");
let t1_node = chapter.alloc_node(Node::text(t1));
chapter.append_child(i1, t1_node);
let c2 = chapter.alloc_node(Node::new(Role::Container));
chapter.append_child(NodeId::ROOT, c2);
let i2 = chapter.alloc_node(Node::new(Role::Inline));
chapter.append_child(c2, i2);
let t2 = chapter.append_text("HE");
let t2_node = chapter.alloc_node(Node::text(t2));
chapter.append_child(i2, t2_node);
assert_eq!(chapter.children(NodeId::ROOT).count(), 2);
optimize(&mut chapter);
let mut found_the = false;
for id in chapter.iter_dfs() {
let node = chapter.node(id).unwrap();
if node.role == Role::Text && !node.text.is_empty() {
let text = chapter.text(node.text);
if text == "THE" {
found_the = true;
}
}
}
assert!(found_the, "Expected merged text 'THE' not found");
}
#[test]
fn test_hoist_preserves_multi_child_container() {
let mut chapter = IRChapter::new();
let container = chapter.alloc_node(Node::new(Role::Container));
chapter.append_child(NodeId::ROOT, container);
let i1 = chapter.alloc_node(Node::new(Role::Inline));
chapter.append_child(container, i1);
let t1 = chapter.append_text("A");
let t1_node = chapter.alloc_node(Node::text(t1));
chapter.append_child(i1, t1_node);
let i2 = chapter.alloc_node(Node::new(Role::Inline));
chapter.append_child(container, i2);
let t2 = chapter.append_text("B");
let t2_node = chapter.alloc_node(Node::text(t2));
chapter.append_child(i2, t2_node);
hoist_nested_inlines(&mut chapter);
let root_children: Vec<_> = chapter.children(NodeId::ROOT).collect();
assert_eq!(root_children.len(), 1);
assert_eq!(
chapter.node(root_children[0]).unwrap().role,
Role::Container
);
}
#[test]
fn test_hoist_preserves_container_with_id() {
let mut chapter = IRChapter::new();
let container = chapter.alloc_node(Node::new(Role::Container));
chapter.append_child(NodeId::ROOT, container);
chapter.semantics.set_id(container, "anchor".to_string());
let inline = chapter.alloc_node(Node::new(Role::Inline));
chapter.append_child(container, inline);
let text_range = chapter.append_text("Hello");
let text_node = chapter.alloc_node(Node::text(text_range));
chapter.append_child(inline, text_node);
hoist_nested_inlines(&mut chapter);
let root_children: Vec<_> = chapter.children(NodeId::ROOT).collect();
assert_eq!(root_children.len(), 1);
assert_eq!(
chapter.node(root_children[0]).unwrap().role,
Role::Container
);
}
#[test]
fn test_merge_adjacent_text_nodes() {
let mut chapter = IRChapter::new();
let para = chapter.alloc_node(Node::new(Role::Paragraph));
chapter.append_child(NodeId::ROOT, para);
let range1 = chapter.append_text("T");
let node1 = chapter.alloc_node(Node::text(range1));
chapter.append_child(para, node1);
let range2 = chapter.append_text("HE");
let node2 = chapter.alloc_node(Node::text(range2));
chapter.append_child(para, node2);
let range3 = chapter.append_text(" ");
let node3 = chapter.alloc_node(Node::text(range3));
chapter.append_child(para, node3);
assert_eq!(chapter.children(para).count(), 3);
optimize(&mut chapter);
let children: Vec<_> = chapter.children(para).collect();
assert_eq!(children.len(), 1);
assert_eq!(
chapter.text(chapter.node(children[0]).unwrap().text),
"THE "
);
}
#[test]
fn test_no_merge_different_styles() {
let mut chapter = IRChapter::new();
let para = chapter.alloc_node(Node::new(Role::Paragraph));
chapter.append_child(NodeId::ROOT, para);
let range1 = chapter.append_text("Hello");
let mut node1 = Node::text(range1);
let bold = chapter.styles.intern(crate::ir::ComputedStyle {
font_weight: crate::ir::FontWeight::BOLD,
..Default::default()
});
node1.style = bold;
let id1 = chapter.alloc_node(node1);
chapter.append_child(para, id1);
let range2 = chapter.append_text(" World");
let node2 = Node::text(range2);
let id2 = chapter.alloc_node(node2);
chapter.append_child(para, id2);
optimize(&mut chapter);
assert_eq!(chapter.children(para).count(), 2);
}
#[test]
fn test_fuse_adjacent_unordered_lists() {
let mut chapter = IRChapter::new();
let ul1 = chapter.alloc_node(Node::new(Role::UnorderedList));
chapter.append_child(NodeId::ROOT, ul1);
let li1 = chapter.alloc_node(Node::new(Role::ListItem));
chapter.append_child(ul1, li1);
let ul2 = chapter.alloc_node(Node::new(Role::UnorderedList));
chapter.append_child(NodeId::ROOT, ul2);
let li2 = chapter.alloc_node(Node::new(Role::ListItem));
chapter.append_child(ul2, li2);
assert_eq!(chapter.children(NodeId::ROOT).count(), 2);
fuse_lists(&mut chapter);
let root_children: Vec<_> = chapter.children(NodeId::ROOT).collect();
assert_eq!(root_children.len(), 1);
let list_children: Vec<_> = chapter.children(root_children[0]).collect();
assert_eq!(list_children.len(), 2);
}
#[test]
fn test_no_fuse_different_list_types() {
let mut chapter = IRChapter::new();
let ul = chapter.alloc_node(Node::new(Role::UnorderedList));
chapter.append_child(NodeId::ROOT, ul);
let ol = chapter.alloc_node(Node::new(Role::OrderedList));
chapter.append_child(NodeId::ROOT, ol);
fuse_lists(&mut chapter);
assert_eq!(chapter.children(NodeId::ROOT).count(), 2);
}
#[test]
fn test_prune_empty_container() {
let mut chapter = IRChapter::new();
let container = chapter.alloc_node(Node::new(Role::Container));
chapter.append_child(NodeId::ROOT, container);
prune_empty(&mut chapter);
assert_eq!(chapter.children(NodeId::ROOT).count(), 0);
}
#[test]
fn test_prune_cascades() {
let mut chapter = IRChapter::new();
let container = chapter.alloc_node(Node::new(Role::Container));
chapter.append_child(NodeId::ROOT, container);
let inline = chapter.alloc_node(Node::new(Role::Inline));
chapter.append_child(container, inline);
assert_eq!(chapter.children(container).count(), 1);
prune_empty(&mut chapter);
assert_eq!(chapter.children(NodeId::ROOT).count(), 0);
}
#[test]
fn test_prune_preserves_id() {
let mut chapter = IRChapter::new();
let container = chapter.alloc_node(Node::new(Role::Container));
chapter.append_child(NodeId::ROOT, container);
chapter.semantics.set_id(container, "anchor".to_string());
prune_empty(&mut chapter);
assert_eq!(chapter.children(NodeId::ROOT).count(), 1);
}
#[test]
fn test_prune_preserves_content() {
let mut chapter = IRChapter::new();
let container = chapter.alloc_node(Node::new(Role::Container));
chapter.append_child(NodeId::ROOT, container);
let text_range = chapter.append_text("Content");
let text = chapter.alloc_node(Node::text(text_range));
chapter.append_child(container, text);
prune_empty(&mut chapter);
assert_eq!(chapter.children(NodeId::ROOT).count(), 1);
}
#[test]
fn test_full_pipeline() {
let mut chapter = IRChapter::new();
let container = chapter.alloc_node(Node::new(Role::Container));
chapter.append_child(NodeId::ROOT, container);
let ul1 = chapter.alloc_node(Node::new(Role::UnorderedList));
chapter.append_child(container, ul1);
let li1 = chapter.alloc_node(Node::new(Role::ListItem));
chapter.append_child(ul1, li1);
let ul2 = chapter.alloc_node(Node::new(Role::UnorderedList));
chapter.append_child(container, ul2);
let li2 = chapter.alloc_node(Node::new(Role::ListItem));
chapter.append_child(ul2, li2);
assert_eq!(chapter.children(container).count(), 2);
optimize(&mut chapter);
let children: Vec<_> = chapter.children(container).collect();
assert_eq!(children.len(), 1);
let list_items: Vec<_> = chapter.children(children[0]).collect();
assert_eq!(list_items.len(), 2);
}
#[test]
fn test_wrap_mixed_content_in_blockquote() {
let mut chapter = IRChapter::new();
let bq = chapter.alloc_node(Node::new(Role::BlockQuote));
chapter.append_child(NodeId::ROOT, bq);
let para = chapter.alloc_node(Node::new(Role::Paragraph));
chapter.append_child(bq, para);
let verse_range = chapter.append_text("Some verse...");
let verse = chapter.alloc_node(Node::text(verse_range));
chapter.append_child(para, verse);
let cite = chapter.alloc_node(Node::new(Role::Inline));
chapter.append_child(bq, cite);
let author_range = chapter.append_text("— Author");
let author = chapter.alloc_node(Node::text(author_range));
chapter.append_child(cite, author);
assert_eq!(chapter.children(bq).count(), 2);
wrap_mixed_content(&mut chapter);
let children: Vec<_> = chapter.children(bq).collect();
assert_eq!(children.len(), 2);
assert_eq!(chapter.node(children[0]).unwrap().role, Role::Paragraph);
assert_eq!(chapter.node(children[1]).unwrap().role, Role::Container);
let wrapper_children: Vec<_> = chapter.children(children[1]).collect();
assert_eq!(wrapper_children.len(), 1);
assert_eq!(
chapter.node(wrapper_children[0]).unwrap().role,
Role::Inline
);
}
#[test]
fn test_no_wrap_when_only_block_children() {
let mut chapter = IRChapter::new();
let container = chapter.alloc_node(Node::new(Role::Container));
chapter.append_child(NodeId::ROOT, container);
let p1 = chapter.alloc_node(Node::new(Role::Paragraph));
chapter.append_child(container, p1);
let p2 = chapter.alloc_node(Node::new(Role::Paragraph));
chapter.append_child(container, p2);
assert_eq!(chapter.children(container).count(), 2);
wrap_mixed_content(&mut chapter);
let children: Vec<_> = chapter.children(container).collect();
assert_eq!(children.len(), 2);
assert_eq!(chapter.node(children[0]).unwrap().role, Role::Paragraph);
assert_eq!(chapter.node(children[1]).unwrap().role, Role::Paragraph);
}
#[test]
fn test_no_wrap_when_only_inline_children() {
let mut chapter = IRChapter::new();
let para = chapter.alloc_node(Node::new(Role::Paragraph));
chapter.append_child(NodeId::ROOT, para);
let t1_range = chapter.append_text("Hello ");
let t1 = chapter.alloc_node(Node::text(t1_range));
chapter.append_child(para, t1);
let span = chapter.alloc_node(Node::new(Role::Inline));
chapter.append_child(para, span);
let t2_range = chapter.append_text(" World");
let t2 = chapter.alloc_node(Node::text(t2_range));
chapter.append_child(para, t2);
assert_eq!(chapter.children(para).count(), 3);
wrap_mixed_content(&mut chapter);
let children: Vec<_> = chapter.children(para).collect();
assert_eq!(children.len(), 3);
}
}