565 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Rust
		
	
	
	
			
		
		
	
	
			565 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Rust
		
	
	
	
| 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<Rc<RefCell<LinkTree>>>,
 | |
|     },
 | |
| }
 | |
| 
 | |
| #[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<DocLink> {
 | |
|         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: ref mut 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 {
 | |
|                 ref mut prefix,
 | |
|                 ref mut 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<DocLink> {
 | |
|         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::<Vec<_>>()
 | |
|             }
 | |
| 
 | |
|             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<RefCell<LinkTree>> {
 | |
|         Rc::new(RefCell::new(self))
 | |
|     }
 | |
| 
 | |
|     fn path(&self) -> &str {
 | |
|         match self {
 | |
|             LinkTree::Empty => "",
 | |
|             LinkTree::Leaf { ref value, .. } => value.as_str(),
 | |
|             LinkTree::Node { ref 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<String> {
 | |
|     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())
 | |
|     )
 | |
| }
 |