use crate::model::{Chapter, Node, NodeId, Role};
use super::pass::walk_bottom_up;
use super::predicates::{is_block_container, is_inline_role};
pub fn wrap_mixed_content(chapter: &mut Chapter) {
walk_bottom_up(chapter, |chapter, parent_id| {
wrap_mixed_children(chapter, parent_id);
});
}
fn wrap_mixed_children(chapter: &mut Chapter, parent_id: NodeId) {
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 analyze_children(chapter: &Chapter, 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 Chapter, 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 Chapter,
parent_id: NodeId,
children_info: &[(NodeId, bool)],
start_idx: usize,
end_idx: usize,
) {
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);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wraps_mixed_content_in_blockquote() {
let mut chapter = Chapter::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 = Chapter::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 = Chapter::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);
}
}