Better module hierarchy and style for generated docs.
This commit also reverse the order of the sections in the sidebar such that modules are now placed last.
This commit is contained in:
506
crates/aiken-project/src/docs/link_tree.rs
Normal file
506
crates/aiken-project/src/docs/link_tree.rs
Normal file
@@ -0,0 +1,506 @@
|
||||
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,
|
||||
..
|
||||
} => {
|
||||
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 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 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) {
|
||||
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 mut children = vec![
|
||||
self.clone().into_ref(),
|
||||
LinkTree::Leaf {
|
||||
value: module.to_string(),
|
||||
}
|
||||
.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: String::new(),
|
||||
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/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(),
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
/// Find the common module prefix between two module path, if any.
|
||||
///
|
||||
/// ```rust
|
||||
/// 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())
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user