use crate::wiki::{HOME_TITLE, Link, TextNode, Wiki};
use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
pub fn populate_traversal_order(wiki: &mut Wiki) {
let order = traversal_order(wiki, None)
.into_iter()
.map(str::to_owned)
.collect::<Vec<_>>();
for node in wiki.text_nodes.values_mut() {
node.traversal_index = None;
}
for (index, title) in order.into_iter().enumerate() {
wiki.text_nodes
.get_mut(&title)
.expect("Traversed titles should refer to existing nodes.")
.traversal_index = Some(index);
}
}
pub fn traversal_order<'a>(wiki: &'a Wiki, extra_title: Option<&'a str>) -> Vec<&'a str> {
let exists = |title: &str| wiki.text_nodes.contains_key(title) || extra_title == Some(title);
let link_titles = |title: &str| {
wiki.text_nodes
.get(title)
.map(text_link_titles)
.unwrap_or_default()
};
if !exists(HOME_TITLE) {
return Vec::new();
}
let mut depths = HashMap::<&str, usize>::from([(HOME_TITLE, 0)]);
let mut queued_titles = VecDeque::from([HOME_TITLE]);
while let Some(title) = queued_titles.pop_front() {
let depth = depths[title];
for target in link_titles(title) {
if exists(target) && !depths.contains_key(target) {
depths.insert(target, depth + 1);
queued_titles.push_back(target);
}
}
}
let mut order = Vec::new();
let mut visited_titles = HashSet::new();
let mut pending_titles = vec![HOME_TITLE];
while let Some(title) = pending_titles.pop() {
if !visited_titles.insert(title) {
continue;
}
order.push(title);
let child_depth = depths[title] + 1;
for target in link_titles(title).into_iter().rev() {
if depths.get(target) == Some(&child_depth) && !visited_titles.contains(target) {
pending_titles.push(target);
}
}
}
order
}
fn text_link_titles(node: &TextNode) -> BTreeSet<&str> {
node.links
.iter()
.filter_map(|link| match link {
Link::Text { title, .. } => Some(title.as_str()),
Link::Filesystem { .. } => None,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::populate_traversal_order;
use crate::{line_index::LineIndex, parser::parse, wiki::Wiki};
use std::path::Path;
fn traversal_order(source: &str) -> (Wiki, Vec<String>) {
let (mut wiki, errors) = parse(
Some(Path::new("test.mull")),
source,
&LineIndex::new(source),
);
assert!(errors.is_empty());
populate_traversal_order(&mut wiki);
let mut nodes = wiki
.text_nodes
.values()
.filter_map(|node| {
node.traversal_index
.map(|index| (index, node.title.clone()))
})
.collect::<Vec<_>>();
nodes.sort();
let titles = nodes.into_iter().map(|(_, title)| title).collect();
(wiki, titles)
}
#[test]
fn depth_first_preorder() {
let (_, titles) = traversal_order(concat!(
"# Home\nSee [A] and [B].\n",
"# A\nSee [A1] and [A2].\n",
"# A1\nSee [A1a].\n",
"# A1a\n",
"# A2\n",
"# B\nSee [B1].\n",
"# B1\n",
));
assert_eq!(titles, vec!["Home", "A", "A1", "A1a", "A2", "B", "B1"]);
}
#[test]
fn siblings_in_title_order() {
let (_, titles) = traversal_order(concat!(
"# Home\nSee [Charlie], [Alpha], [Bravo], and [Alpha] again.\n",
"# Alpha\n",
"# Bravo\n",
"# Charlie\n",
));
assert_eq!(titles, vec!["Home", "Alpha", "Bravo", "Charlie"]);
}
#[test]
fn shortest_paths_only() {
let (_, titles) = traversal_order(concat!(
"# Home\nSee [A], [M], and [Z].\n",
"# A\nSee [B].\n",
"# B\nSee [Z].\n",
"# M\n",
"# Z\n",
));
assert_eq!(titles, vec!["Home", "A", "B", "M", "Z"]);
}
#[test]
fn first_parent_at_same_depth() {
let (_, titles) = traversal_order(concat!(
"# Home\nSee [B] and [A].\n",
"# A\nSee [X].\n",
"# B\nSee [X].\n",
"# X\n",
));
assert_eq!(titles, vec!["Home", "A", "X", "B"]);
}
#[test]
fn multiple_paths_and_cycles() {
let (_, titles) = traversal_order(concat!(
"# Home\nSee [Left] and [Target].\n",
"# Left\nSee [Middle].\n",
"# Middle\nSee [Target].\n",
"# Target\nSee [Home] and [Left].\n",
));
assert_eq!(titles, vec!["Home", "Left", "Middle", "Target"]);
}
#[test]
fn unreachable_nodes() {
let (wiki, titles) = traversal_order("# Home\nSee [Greeting].\n# Greeting\n# Orphan\n");
assert_eq!(titles, vec!["Home", "Greeting"]);
assert_eq!(wiki.text_nodes["Orphan"].traversal_index, None);
}
}