use super::DocLink; use std::{cell::RefCell, rc::Rc}; /// A custom tree structure to help constructing the links in the sidebar for documentation. /// The goal is to end up generating a vector of pre-constructed elements that are simple to handle /// in the HTML template, but still enforces a visual hierarchy that helps readability of modules. /// /// So for example the following: /// - aiken/cbor /// - aiken/list /// - aiken/math /// - aiken/math/rational /// - aiken/primitive /// - aiken/primitive/bytearray /// - aiken/primitive/integer /// - cardano/asset /// - cardano/certificate /// /// is nicely turned into: /// /// aiken /// /cbor /// /list /// /math /// /rational /// /primitive /// /bytearray /// /integer /// /// cardano /// /asset /// /certificate #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] pub(crate) enum LinkTree { Empty, Leaf { value: String, }, Node { prefix: String, separator: bool, children: Vec>>, }, } #[allow(clippy::derivable_impls)] impl Default for LinkTree { /// The intended way of creating a new empty LinkTree. fn default() -> LinkTree { LinkTree::Empty } } impl LinkTree { /// Convert a LinkTree into a sequence of DocLinks, ready to be displayed at the right /// indentation level. pub fn to_vec(&self) -> Vec { self.do_to_vec(&[]) } pub fn insert(&mut self, module: &str) { /// Strip prefix and ensures to remove any leading slash "/" as well. fn strip_prefix(source: &str, prefix: &str) -> String { let result = source.strip_prefix(prefix).unwrap(); if result.starts_with('/') { result.strip_prefix('/').unwrap().to_string() } else { result.to_string() } } match self { LinkTree::Empty => { *self = LinkTree::Leaf { value: module.to_string(), } } LinkTree::Leaf { value: leaf, .. } => { // In case we try to insert a module that already exists, there's nothing to do. if module == leaf { return; } let (prefix, value) = if let Some(prefix) = common_prefix(module, leaf) { *leaf = strip_prefix(leaf, &prefix); let value = strip_prefix(module, &prefix); (prefix, value) } else { (String::new(), module.to_string()) }; // When `prefix == module`, we are usually in the case where we try to insert a // parent (e.g. `aiken/math`) into a sub-leaf (e.g. `aiken/math/rational`). So // `self` must become a child node of our newly created parent. if prefix == module { let children = vec![self.clone().into_ref()]; *self = LinkTree::Node { // Holds a value, so separator = false separator: false, children, prefix, }; // If `leaf.is_empty()`, we are in the case where we are inserting a sub-leaf // (e.g. `aiken/math/rational`) into a parent (e.g. `aiken/math`); so much that // we've run out of path segments to follow down. So `self` can turn into a node // that contains that new leaf. } else if leaf.is_empty() { let children = vec![LinkTree::Leaf { value }.into_ref()]; *self = LinkTree::Node { // Holds a value, so separator = false separator: false, children, prefix, }; // Otherwise, neither one is a child of the other, so we can nest them under a node // with the corresponding (possibly empty) prefix. } else { let mut children = vec![self.clone().into_ref(), LinkTree::Leaf { value }.into_ref()]; children.sort_by(|a, b| a.borrow().path().cmp(b.borrow().path())); *self = LinkTree::Node { // This node is a 'separator' because it doesn't // hold any value. It is just an intersection point. separator: true, children, prefix, }; } } LinkTree::Node { prefix, children, .. } => { // When `module.starts_with(prefix)` is true, it means that the module being // inserted belong to our sub-tree. We do not know *where* exactly though, so we // have to find whether there's any child that continues the path. If node, we can // add it to our children. if module.starts_with(prefix.as_str()) { let module = strip_prefix(module, prefix); for child in children.iter_mut() { if common_prefix(child.borrow().path(), module.as_str()).is_some() { return child.borrow_mut().insert(module.as_str()); } } children.push( LinkTree::Leaf { value: module.to_string(), } .into_ref(), ); children.sort_by(|a, b| a.borrow().path().cmp(b.borrow().path())); // Otherwise, we make it a neighbor that shares no common prefix. } else { let new_prefix = common_prefix(prefix, module).unwrap_or_default(); *prefix = strip_prefix(prefix, &new_prefix); let mut children = vec![ self.clone().into_ref(), LinkTree::Leaf { value: strip_prefix(module, &new_prefix), } .into_ref(), ]; children.sort_by(|a, b| a.borrow().path().cmp(b.borrow().path())); *self = LinkTree::Node { // This node is a 'separator' because it doesn't // hold any value. It is just an intersection point. separator: true, prefix: new_prefix, children, }; } } } } fn do_to_vec(&self, path: &[&str]) -> Vec { let mk_path = |value: &str| { [ path.join("/").as_str(), if path.is_empty() { "" } else { "/" }, value, ".html", ] .concat() }; match self { LinkTree::Empty => vec![], LinkTree::Leaf { value } => { let last_ix = value.split('/').count(); let module_path = mk_path(value); value .split('/') .enumerate() .map(|(offset, segment)| { if offset == last_ix - 1 { DocLink { indent: path.len() + offset, name: segment.to_string(), path: module_path.to_string(), } } else { DocLink { indent: path.len() + offset, name: segment.to_string(), path: String::new(), } } }) .collect::>() } LinkTree::Node { children, prefix, separator, } => { let mut links = if prefix.is_empty() { vec![] } else { vec![DocLink { indent: path.len(), name: prefix.clone(), path: if *separator { String::new() } else { mk_path(prefix) }, }] }; let mut next = vec![]; for segment in path { next.push(*segment); } if !prefix.is_empty() { next.push(prefix); } links.extend( children .iter() .flat_map(|child| child.borrow().do_to_vec(&next[..])), ); links } } } fn into_ref(self) -> Rc> { Rc::new(RefCell::new(self)) } fn path(&self) -> &str { match self { LinkTree::Empty => "", LinkTree::Leaf { value, .. } => value.as_str(), LinkTree::Node { prefix, .. } => prefix.as_str(), } } } #[test] fn link_tree_1() { let mut tree = LinkTree::default(); tree.insert("foo"); assert_eq!( tree.to_vec(), vec![DocLink { indent: 0, name: "foo".to_string(), path: "foo.html".to_string(), }] ) } #[test] fn link_tree_2() { let mut tree = LinkTree::default(); tree.insert("foo"); tree.insert("bar"); assert_eq!( tree.to_vec(), vec![ DocLink { indent: 0, name: "bar".to_string(), path: "bar.html".to_string(), }, DocLink { indent: 0, name: "foo".to_string(), path: "foo.html".to_string(), } ] ) } #[test] fn link_tree_3() { let mut tree = LinkTree::default(); tree.insert("aiken/list"); tree.insert("aiken/bytearray"); assert_eq!( tree.to_vec(), vec![ DocLink { indent: 0, name: "aiken".to_string(), path: String::new(), }, DocLink { indent: 1, name: "bytearray".to_string(), path: "aiken/bytearray.html".to_string(), }, DocLink { indent: 1, name: "list".to_string(), path: "aiken/list.html".to_string(), }, ] ) } #[test] fn link_tree_4() { let mut tree = LinkTree::default(); tree.insert("aiken/cbor"); tree.insert("aiken/math/rational"); tree.insert("aiken/math"); tree.insert("cardano/foo"); assert_eq!( tree.to_vec(), vec![ DocLink { indent: 0, name: "aiken".to_string(), path: String::new(), }, DocLink { indent: 1, name: "cbor".to_string(), path: "aiken/cbor.html".to_string(), }, DocLink { indent: 1, name: "math".to_string(), path: "aiken/math.html".to_string(), }, DocLink { indent: 2, name: "rational".to_string(), path: "aiken/math/rational.html".to_string(), }, DocLink { indent: 0, name: "cardano".to_string(), path: "".to_string(), }, DocLink { indent: 1, name: "foo".to_string(), path: "cardano/foo.html".to_string(), } ] ) } #[test] fn link_tree_5() { let mut tree = LinkTree::default(); tree.insert("cardano/foo"); tree.insert("cardano"); tree.insert("aiken/cbor"); tree.insert("aiken/math"); tree.insert("aiken/math/rational"); assert_eq!( tree.to_vec(), vec![ DocLink { indent: 0, name: "aiken".to_string(), path: String::new(), }, DocLink { indent: 1, name: "cbor".to_string(), path: "aiken/cbor.html".to_string(), }, DocLink { indent: 1, name: "math".to_string(), path: "aiken/math.html".to_string(), }, DocLink { indent: 2, name: "rational".to_string(), path: "aiken/math/rational.html".to_string(), }, DocLink { indent: 0, name: "cardano".to_string(), path: "cardano.html".to_string(), }, DocLink { indent: 1, name: "foo".to_string(), path: "cardano/foo.html".to_string(), } ] ) } #[test] fn link_tree_6() { let mut tree = LinkTree::default(); tree.insert("cardano/address"); tree.insert("cardano/address/credential"); tree.insert("cardano/address/credential"); tree.insert("cardano/assets"); tree.insert("cardano/assets"); tree.insert("cardano/certificate"); assert_eq!( tree.to_vec(), vec![ DocLink { indent: 0, name: "cardano".to_string(), path: "".to_string(), }, DocLink { indent: 1, name: "address".to_string(), path: "cardano/address.html".to_string(), }, DocLink { indent: 2, name: "credential".to_string(), path: "cardano/address/credential.html".to_string(), }, DocLink { indent: 1, name: "assets".to_string(), path: "cardano/assets.html".to_string(), }, DocLink { indent: 1, name: "certificate".to_string(), path: "cardano/certificate.html".to_string(), }, ] ) } /// Find the common module prefix between two module path, if any. /// /// ``` /// use aiken_project::docs::link_tree::common_prefix; /// /// assert_eq!( /// common_prefix("foo", "foo"), /// Some("foo".to_string()), /// ); /// /// assert_eq!( /// common_prefix("aiken/list", "aiken/bytearray"), /// Some("aiken".to_string()), /// ); /// /// assert_eq!( /// common_prefix("aiken/list", "cardano/asset"), /// None, /// ); /// ``` pub fn common_prefix(left: &str, right: &str) -> Option { let mut prefix = vec![]; for (left, right) in left.split('/').zip(right.split('/')) { if !left.is_empty() && left == right { prefix.push(left); } else { break; } } if prefix.is_empty() { None } else { Some(prefix.join("/")) } } #[test] fn common_prefix_1() { assert_eq!(common_prefix("", ""), None) } #[test] fn common_prefix_2() { assert_eq!(common_prefix("foo", "bar"), None) } #[test] fn common_prefix_3() { assert_eq!(common_prefix("foo", "foo"), Some("foo".to_string())) } #[test] fn common_prefix_4() { assert_eq!(common_prefix("foo", ""), None) } #[test] fn common_prefix_5() { assert_eq!( common_prefix("foo/bar", "foo/bar"), Some("foo/bar".to_string()) ) } #[test] fn common_prefix_6() { assert_eq!( common_prefix("foo/bar", "foo/bar/baz"), Some("foo/bar".to_string()) ) } #[test] fn common_prefix_7() { assert_eq!( common_prefix("foo/bar", "foo/wow/baz"), Some("foo".to_string()) ) } #[test] fn common_prefix_8() { assert_eq!( common_prefix("foo/bar/baz", "foo/wow/baz"), Some("foo".to_string()) ) }