(home)

cyclic_transclusion_check

/// If transclusions form a cycle return `Some` and the base filename of one of the pages involved.
/// Otherwise return `None`.
pub fn cyclic_transclusion_check(page_map: &HashMap<String, Page>) -> Option<String> {
    let mut node_map: HashMap<String, _> = HashMap::new();
    let mut graph: Graph<&str, ()> = DiGraph::new();

    for base_name in page_map.keys() {
        let node = graph.add_node(base_name);
        node_map.insert(base_name.clone(), node);
    }

    for (base_name, page) in page_map {
        let node = node_map.get(base_name).unwrap();
        for transclusion in &page.hyperstring.transclusions() {
            let target_node = node_map.get(transclusion).unwrap();
            graph.add_edge(*node, *target_node, ());
        }
    }

    match petgraph::algo::toposort(&graph, None) {
        Ok(_) => None,
        Err(cycle) => Some(graph.node_weight(cycle.node_id()).unwrap().to_string()),
    }
}

#[cfg(test)]
mod cyclic_transclusion_check_tests {
    use super::*;

    #[test]
    fn test_cycle() {
        let mut page_map = HashMap::new();
        page_map.insert(
            "page-1".to_string(),
            Page {
                metadata: HashMap::new(),
                hyperstring: Hyperstring(vec![HyperstringSegment::Transclusion(
                    "page-2".to_string(),
                )]),
            },
        );
        page_map.insert(
            "page-2".to_string(),
            Page {
                metadata: HashMap::new(),
                hyperstring: Hyperstring(vec![HyperstringSegment::Transclusion(
                    "page-3".to_string(),
                )]),
            },
        );
        page_map.insert(
            "page-3".to_string(),
            Page {
                metadata: HashMap::new(),
                hyperstring: Hyperstring(vec![HyperstringSegment::Transclusion(
                    "page-1".to_string(),
                )]),
            },
        );
        let result = cyclic_transclusion_check(&page_map);
        assert!(
            matches!(result, Some(page) if page == "page-1" || page == "page-2" || page == "page-3")
        );
    }

    #[test]
    fn test_loop() {
        let mut page_map = HashMap::new();
        page_map.insert(
            "page-1".to_string(),
            Page {
                metadata: HashMap::new(),
                hyperstring: Hyperstring(vec![HyperstringSegment::Transclusion(
                    "page-1".to_string(),
                )]),
            },
        );

        let result = cyclic_transclusion_check(&page_map);
        assert_eq!(result, Some("page-1".to_string()));
    }

    #[test]
    fn test_no_cycle() {
        let mut page_map = HashMap::new();
        page_map.insert(
            "page-1".to_string(),
            Page {
                metadata: HashMap::new(),
                hyperstring: Hyperstring(vec![HyperstringSegment::Transclusion(
                    "page-2".to_string(),
                )]),
            },
        );
        page_map.insert(
            "page-2".to_string(),
            Page {
                metadata: HashMap::new(),
                hyperstring: Hyperstring(vec![HyperstringSegment::Transclusion(
                    "page-3".to_string(),
                )]),
            },
        );
        page_map.insert(
            "page-3".to_string(),
            Page {
                metadata: HashMap::new(),
                hyperstring: Hyperstring(vec![]),
            },
        );
        assert_eq!(cyclic_transclusion_check(&page_map), None);
    }
}

Backlinks