use crate::model::{Chapter, NodeId, Role};
use super::pass::walk_bottom_up;
use super::predicates::has_semantic_attrs;
pub fn fuse_lists(chapter: &mut Chapter) {
walk_bottom_up(chapter, |chapter, parent_id| {
fuse_list_siblings(chapter, parent_id);
});
}
fn fuse_list_siblings(chapter: &mut Chapter, parent_id: NodeId) {
let mut cursor_opt = chapter.node(parent_id).and_then(|n| n.first_child);
let mut cached_tail: Option<NodeId> = None;
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)
{
cached_tail = fuse_list_pair(chapter, current_id, next_id, cached_tail);
continue;
}
cached_tail = None;
cursor_opt = next_opt;
}
}
fn can_fuse_lists(chapter: &Chapter, 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 !matches!(
(left.role, right.role),
(Role::OrderedList, Role::OrderedList) | (Role::UnorderedList, Role::UnorderedList)
) {
return false;
}
if left.style != right.style {
return false;
}
if has_semantic_attrs(chapter, left_id) || has_semantic_attrs(chapter, right_id) {
return false;
}
true
}
fn fuse_list_pair(
chapter: &mut Chapter,
left_id: NodeId,
right_id: NodeId,
left_tail: Option<NodeId>,
) -> Option<NodeId> {
let right_first = chapter.node(right_id).and_then(|n| n.first_child);
let right_next = chapter.node(right_id).and_then(|n| n.next_sibling);
if right_first.is_none() {
if let Some(left_node) = chapter.node_mut(left_id) {
left_node.next_sibling = right_next;
}
return left_tail;
}
let mut right_last = None;
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);
}
right_last = Some(child_id);
child_opt = next_child;
}
let left_last = left_tail.or_else(|| {
let mut current_opt = chapter.node(left_id).and_then(|n| n.first_child);
while let Some(current) = current_opt {
let next = chapter.node(current).and_then(|n| n.next_sibling);
if next.is_none() {
return Some(current);
}
current_opt = next;
}
None
});
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;
}
}
if let Some(left_node) = chapter.node_mut(left_id) {
left_node.next_sibling = right_next;
left_node.last_child = right_last;
}
right_last
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::Node;
#[test]
fn test_fuse_adjacent_unordered_lists() {
let mut chapter = Chapter::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 = Chapter::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);
}
fn list_with_item(chapter: &mut Chapter, role: Role) -> NodeId {
let list = chapter.alloc_node(Node::new(role));
chapter.append_child(NodeId::ROOT, list);
let li = chapter.alloc_node(Node::new(Role::ListItem));
chapter.append_child(list, li);
list
}
#[test]
fn no_fuse_when_second_list_has_id() {
let mut chapter = Chapter::new();
let _ul1 = list_with_item(&mut chapter, Role::UnorderedList);
let ul2 = list_with_item(&mut chapter, Role::UnorderedList);
chapter.semantics.set_id(ul2, "target");
fuse_lists(&mut chapter);
assert_eq!(chapter.children(NodeId::ROOT).count(), 2);
assert_eq!(chapter.semantics.id(ul2), Some("target"));
}
#[test]
fn no_fuse_when_ordered_list_has_start() {
let mut chapter = Chapter::new();
let _ol1 = list_with_item(&mut chapter, Role::OrderedList);
let ol2 = list_with_item(&mut chapter, Role::OrderedList);
chapter.semantics.set_list_start(ol2, 5);
fuse_lists(&mut chapter);
assert_eq!(chapter.children(NodeId::ROOT).count(), 2);
assert_eq!(chapter.semantics.list_start(ol2), Some(5));
}
#[test]
fn no_fuse_when_styles_differ() {
use crate::style::{ComputedStyle, FontWeight};
let mut chapter = Chapter::new();
let _ul1 = list_with_item(&mut chapter, Role::UnorderedList);
let ul2 = list_with_item(&mut chapter, Role::UnorderedList);
let bold = chapter.styles.intern(ComputedStyle {
font_weight: FontWeight::BOLD,
..Default::default()
});
if let Some(node) = chapter.node_mut(ul2) {
node.style = bold;
}
fuse_lists(&mut chapter);
assert_eq!(chapter.children(NodeId::ROOT).count(), 2);
}
#[test]
fn fuses_long_run_of_single_item_lists_in_order() {
let mut chapter = Chapter::new();
let mut items = Vec::new();
for _ in 0..8 {
let ul = chapter.alloc_node(Node::new(Role::UnorderedList));
chapter.append_child(NodeId::ROOT, ul);
let li = chapter.alloc_node(Node::new(Role::ListItem));
chapter.append_child(ul, li);
items.push(li);
}
fuse_lists(&mut chapter);
let root_children: Vec<_> = chapter.children(NodeId::ROOT).collect();
assert_eq!(root_children.len(), 1);
let fused_items: Vec<_> = chapter.children(root_children[0]).collect();
assert_eq!(fused_items, items, "items preserved in document order");
for li in &fused_items {
assert_eq!(
chapter.node(*li).and_then(|n| n.parent),
Some(root_children[0])
);
}
}
}