From 35b70661630bbedfc971a883215b9a9b6ba5a4e4 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 27 Jul 2024 14:12:09 +0200 Subject: [PATCH 1/7] Introduce acceptance test 107 illustrating compiler crash ``` crates/aiken-lang/src/gen_uplc.rs:4515:30 internal error: entered unreachable code: Shouldn't call anything other than var or apply Lambda { parameter_name: Name { text: "__no_inline__", unique: Unique( 0, ), }, body: Var( Name { text: "tests_new_list_unbound", unique: Unique( 0, ), }, ), } ``` --- examples/acceptance_tests/107/aiken.lock | 7 +++++++ examples/acceptance_tests/107/aiken.toml | 9 +++++++++ examples/acceptance_tests/107/lib/tests.ak | 7 +++++++ 3 files changed, 23 insertions(+) create mode 100644 examples/acceptance_tests/107/aiken.lock create mode 100644 examples/acceptance_tests/107/aiken.toml create mode 100644 examples/acceptance_tests/107/lib/tests.ak diff --git a/examples/acceptance_tests/107/aiken.lock b/examples/acceptance_tests/107/aiken.lock new file mode 100644 index 00000000..6e350cda --- /dev/null +++ b/examples/acceptance_tests/107/aiken.lock @@ -0,0 +1,7 @@ +# This file was generated by Aiken +# You typically do not need to edit this file + +requirements = [] +packages = [] + +[etags] diff --git a/examples/acceptance_tests/107/aiken.toml b/examples/acceptance_tests/107/aiken.toml new file mode 100644 index 00000000..37a52681 --- /dev/null +++ b/examples/acceptance_tests/107/aiken.toml @@ -0,0 +1,9 @@ +name = "aiken-lang/107" +version = "0.0.0" +license = "Apache-2.0" +description = "Aiken contracts for project 'aiken-lang/107'" + +[repository] +user = "aiken-lang" +project = "107" +platform = "github" diff --git a/examples/acceptance_tests/107/lib/tests.ak b/examples/acceptance_tests/107/lib/tests.ak new file mode 100644 index 00000000..431a5c2b --- /dev/null +++ b/examples/acceptance_tests/107/lib/tests.ak @@ -0,0 +1,7 @@ +fn new_list() { + [] +} + +test foo() { + fn() { new_list }()() == [] +} From 8b869c0a32dc47e64bf81a51cb39daae58acdb67 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Thu, 22 Aug 2024 12:35:41 +0200 Subject: [PATCH 2/7] Do not sort functions, types and constants in generated docs The rationale is to let them in the order they are defined, so that library authors have some freedom in how they present information. On top of that, we'll also now parse specifi comments as section headers that will be inserted in the sidebar when present. --- crates/aiken-project/src/docs.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/aiken-project/src/docs.rs b/crates/aiken-project/src/docs.rs index e5c4ff57..29e3c5d5 100644 --- a/crates/aiken-project/src/docs.rs +++ b/crates/aiken-project/src/docs.rs @@ -159,7 +159,6 @@ fn generate_module( .definitions .iter() .flat_map(DocFunction::from_definition) - .sorted() .collect(); functions .iter() @@ -171,7 +170,6 @@ fn generate_module( .definitions .iter() .flat_map(DocType::from_definition) - .sorted() .collect(); types .iter() @@ -183,7 +181,6 @@ fn generate_module( .definitions .iter() .flat_map(DocConstant::from_definition) - .sorted() .collect(); constants .iter() From 0ff12b9246e6be5c4e7deb361c64863b677367b8 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Thu, 22 Aug 2024 13:59:53 +0200 Subject: [PATCH 3/7] 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. --- crates/aiken-project/src/docs.rs | 222 ++------ crates/aiken-project/src/docs/link_tree.rs | 506 +++++++++++++++++++ crates/aiken-project/src/test_framework.rs | 23 +- crates/aiken-project/templates/_layout.html | 49 +- crates/aiken-project/templates/css/index.css | 86 +++- 5 files changed, 664 insertions(+), 222 deletions(-) create mode 100644 crates/aiken-project/src/docs/link_tree.rs diff --git a/crates/aiken-project/src/docs.rs b/crates/aiken-project/src/docs.rs index 29e3c5d5..2e523397 100644 --- a/crates/aiken-project/src/docs.rs +++ b/crates/aiken-project/src/docs.rs @@ -24,6 +24,8 @@ use std::{ const MAX_COLUMNS: isize = 999; const VERSION: &str = env!("CARGO_PKG_VERSION"); +mod link_tree; + #[derive(Debug, PartialEq, Eq, Clone)] pub struct DocFile { pub path: PathBuf, @@ -39,8 +41,7 @@ struct ModuleTemplate<'a> { module_name: String, project_name: &'a str, project_version: &'a str, - modules_prefix: String, - modules: &'a Vec, + modules: &'a [DocLink], functions: Vec, types: Vec, constants: Vec, @@ -66,8 +67,7 @@ struct PageTemplate<'a> { page_title: &'a str, project_name: &'a str, project_version: &'a str, - modules_prefix: String, - modules: &'a Vec, + modules: &'a [DocLink], content: String, source: &'a DocLink, timestamp: &'a str, @@ -81,6 +81,7 @@ impl<'a> PageTemplate<'a> { #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] struct DocLink { + indent: usize, name: String, path: String, } @@ -89,6 +90,10 @@ impl DocLink { pub fn is_empty(&self) -> bool { self.name.is_empty() } + + pub fn is_separator(&self) -> bool { + self.path.is_empty() + } } /// Generate documentation files for a given project. @@ -98,10 +103,11 @@ impl DocLink { /// across multiple modules. pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>) -> Vec { let timestamp = new_timestamp(); - let (modules_prefix, modules_links) = generate_modules_links(&modules); + let modules_links = generate_modules_links(&modules); let source = match &config.repository { None => DocLink { + indent: 0, name: String::new(), path: String::new(), }, @@ -110,6 +116,7 @@ pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>) project, platform, }) => DocLink { + indent: 0, name: format!("{user}/{project}"), path: format!("https://{platform}.com/{user}/{project}"), }, @@ -119,13 +126,7 @@ pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>) let mut search_indexes: Vec = vec![]; for module in &modules { - let (indexes, file) = generate_module( - config, - module, - (&modules_prefix, &modules_links), - &source, - ×tamp, - ); + let (indexes, file) = generate_module(config, module, &modules_links, &source, ×tamp); if !indexes.is_empty() { search_indexes.extend(indexes); output_files.push(file); @@ -136,7 +137,7 @@ pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>) output_files.push(generate_readme( root, config, - (&modules_prefix, &modules_links), + &modules_links, &source, ×tamp, )); @@ -147,7 +148,7 @@ pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>) fn generate_module( config: &Config, module: &CheckedModule, - (modules_prefix, modules): (&str, &Vec), + modules: &[DocLink], source: &DocLink, timestamp: &Duration, ) -> (Vec, DocFile) { @@ -197,7 +198,6 @@ fn generate_module( aiken_version: VERSION, breadcrumbs: to_breadcrumbs(&module.name), documentation: render_markdown(&module.ast.docs.iter().join("\n")), - modules_prefix: modules_prefix.to_string(), modules, project_name: &config.name.repo.to_string(), page_title: &format!("{} - {}", module.name, config.name), @@ -279,7 +279,7 @@ fn generate_static_assets(search_indexes: Vec) -> Vec { fn generate_readme( root: &Path, config: &Config, - (modules_prefix, modules): (&str, &Vec), + modules: &[DocLink], source: &DocLink, timestamp: &Duration, ) -> DocFile { @@ -290,7 +290,6 @@ fn generate_readme( let template = PageTemplate { aiken_version: VERSION, breadcrumbs: ".", - modules_prefix: modules_prefix.to_string(), modules, project_name: &config.name.repo.to_string(), page_title: &config.name.to_string(), @@ -306,46 +305,30 @@ fn generate_readme( } } -fn generate_modules_links(modules: &[&CheckedModule]) -> (String, Vec) { - let non_empty_modules = modules.iter().filter(|module| { - module.ast.definitions.iter().any(|def| { - matches!( - def, - Definition::Fn(Function { public: true, .. }) - | Definition::DataType(DataType { public: true, .. }) - | Definition::TypeAlias(TypeAlias { public: true, .. }) - | Definition::ModuleConstant(ModuleConstant { public: true, .. }) - ) +fn generate_modules_links(modules: &[&CheckedModule]) -> Vec { + let non_empty_modules = modules + .iter() + .filter(|module| { + module.ast.definitions.iter().any(|def| { + matches!( + def, + Definition::Fn(Function { public: true, .. }) + | Definition::DataType(DataType { public: true, .. }) + | Definition::TypeAlias(TypeAlias { public: true, .. }) + | Definition::ModuleConstant(ModuleConstant { public: true, .. }) + ) + }) }) - }); + .sorted_by(|a, b| a.name.cmp(&b.name)) + .collect_vec(); + + let mut links = link_tree::LinkTree::default(); - let mut modules_links = vec![]; for module in non_empty_modules { - let module_path = [&module.name.clone(), ".html"].concat(); - modules_links.push(DocLink { - path: module_path, - name: module.name.to_string().clone(), - }); + links.insert(module.name.as_str()); } - modules_links.sort(); - let prefix = if modules_links.len() > 1 { - let prefix = find_modules_prefix(&modules_links); - - for module in &mut modules_links { - let name = module.name.strip_prefix(&prefix).unwrap_or_default(); - module.name = name.strip_prefix('/').unwrap_or(name).to_string(); - if module.name == String::new() { - module.name = "/".to_string() - } - } - - prefix - } else { - String::new() - }; - - (prefix, modules_links) + links.to_vec() } #[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)] @@ -608,141 +591,6 @@ fn new_timestamp() -> Duration { .expect("get current timestamp") } -fn find_modules_prefix(modules: &[DocLink]) -> String { - do_find_modules_prefix("", modules) -} - -fn do_find_modules_prefix(current_prefix: &str, modules: &[DocLink]) -> String { - let prefix = modules - .iter() - .fold(None, |previous_prefix, module| { - let name = module.name.strip_prefix(current_prefix).unwrap_or_default(); - let name = if name.starts_with('/') { - name.strip_prefix('/').unwrap_or_default() - } else { - name - }; - - let prefix = name.split('/').next().unwrap_or_default().to_string(); - - match previous_prefix { - None if prefix != module.name => Some(prefix), - Some(..) if Some(prefix) == previous_prefix => previous_prefix, - _ => Some(String::new()), - } - }) - .unwrap_or_default(); - - if prefix.is_empty() { - current_prefix.to_string() - } else { - let mut current_prefix = current_prefix.to_owned(); - if !current_prefix.is_empty() { - current_prefix.push('/'); - } - current_prefix.push_str(&prefix); - do_find_modules_prefix(¤t_prefix, modules) - } -} - -#[test] -fn find_modules_prefix_test() { - assert_eq!(find_modules_prefix(&[]), "".to_string()); - - assert_eq!( - find_modules_prefix(&[DocLink { - name: "aiken/list".to_string(), - path: String::new() - }]), - "aiken/list".to_string() - ); - - assert_eq!( - find_modules_prefix(&[DocLink { - name: "my_module".to_string(), - path: String::new() - }]), - "".to_string() - ); - - assert_eq!( - find_modules_prefix(&[ - DocLink { - name: "aiken/list".to_string(), - path: String::new() - }, - DocLink { - name: "aiken/bytearray".to_string(), - path: String::new(), - } - ]), - "aiken".to_string() - ); - - assert_eq!( - find_modules_prefix(&[ - DocLink { - name: "aiken/list".to_string(), - path: String::new() - }, - DocLink { - name: "foo/bytearray".to_string(), - path: String::new(), - } - ]), - "".to_string() - ); -} - -#[test] -fn find_modules_prefix_test_2() { - assert_eq!( - find_modules_prefix(&[ - DocLink { - name: "aiken/trees/bst".to_string(), - path: String::new() - }, - DocLink { - name: "aiken/trees/mt".to_string(), - path: String::new(), - } - ]), - "aiken/trees".to_string() - ); - - assert_eq!( - find_modules_prefix(&[ - DocLink { - name: "aiken/trees/bst".to_string(), - path: String::new() - }, - DocLink { - name: "aiken/trees/mt".to_string(), - path: String::new(), - }, - DocLink { - name: "aiken/sequences".to_string(), - path: String::new(), - } - ]), - "aiken".to_string() - ); - - assert_eq!( - find_modules_prefix(&[ - DocLink { - name: "aiken".to_string(), - path: String::new() - }, - DocLink { - name: "aiken/prelude".to_string(), - path: String::new(), - } - ]), - "".to_string() - ); -} - fn to_breadcrumbs(path: &str) -> String { let breadcrumbs = path .strip_prefix('/') diff --git a/crates/aiken-project/src/docs/link_tree.rs b/crates/aiken-project/src/docs/link_tree.rs new file mode 100644 index 00000000..10306f05 --- /dev/null +++ b/crates/aiken-project/src/docs/link_tree.rs @@ -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>>, + }, +} + +#[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: 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 { + 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 { 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 { + 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()) + ) +} diff --git a/crates/aiken-project/src/test_framework.rs b/crates/aiken-project/src/test_framework.rs index 60e005c4..935b931e 100644 --- a/crates/aiken-project/src/test_framework.rs +++ b/crates/aiken-project/src/test_framework.rs @@ -495,7 +495,7 @@ impl Prng { /// Obtain a Prng back from a fuzzer execution. As a reminder, fuzzers have the following /// signature: /// - /// type Fuzzer = fn(Prng) -> Option<(Prng, a)> + /// `type Fuzzer = fn(Prng) -> Option<(Prng, a)>` /// /// In nominal scenarios (i.e. when the fuzzer is made from a seed and evolve pseudo-randomly), /// it cannot yield 'None'. When replayed however, we can't easily guarantee that the changes @@ -507,8 +507,10 @@ impl Prng { fn as_prng(cst: &PlutusData) -> Prng { if let PlutusData::Constr(Constr { tag, fields, .. }) = cst { if *tag == 121 + Prng::SEEDED { - if let [PlutusData::BoundedBytes(bytes), PlutusData::BoundedBytes(choices)] = - &fields[..] + if let [ + PlutusData::BoundedBytes(bytes), + PlutusData::BoundedBytes(choices), + ] = &fields[..] { return Prng::Seeded { choices: choices.to_vec(), @@ -1087,9 +1089,11 @@ impl TryFrom for Assertion { final_else, .. } => { - if let [IfBranch { - condition, body, .. - }] = &branches[..] + if let [ + IfBranch { + condition, body, .. + }, + ] = &branches[..] { let then_is_true = match body { TypedExpr::Var { @@ -1509,13 +1513,14 @@ mod test { } "#}); - assert!(prop - .run::<()>( + assert!( + prop.run::<()>( 42, PropertyTest::DEFAULT_MAX_SUCCESS, &PlutusVersion::default() ) - .is_success()); + .is_success() + ); } #[test] diff --git a/crates/aiken-project/templates/_layout.html b/crates/aiken-project/templates/_layout.html index 16174afa..5a32c00e 100644 --- a/crates/aiken-project/templates/_layout.html +++ b/crates/aiken-project/templates/_layout.html @@ -186,23 +186,26 @@ {% endif %} + {% block sidebar_content %}{% endblock %} +

Modules

- {% if !modules_prefix.is_empty() %} -

{{ modules_prefix }}/

- {% endif %}
- - {% block sidebar_content %}{% endblock %}
@@ -276,9 +279,35 @@ el.prepend(a); }); + + + diff --git a/crates/aiken-project/templates/css/index.css b/crates/aiken-project/templates/css/index.css index 347f89fb..14126e18 100644 --- a/crates/aiken-project/templates/css/index.css +++ b/crates/aiken-project/templates/css/index.css @@ -5,7 +5,7 @@ --search-width: 680px; --header-height: 60px; --hash-offset: calc(var(--header-height) * 1.67); - --sidebar-width: 240px; + --sidebar-width: 260px; --gap: 24px; --small-gap: calc(var(--gap) / 2); --tiny-gap: calc(var(--small-gap) / 2); @@ -60,6 +60,15 @@ html { scroll-padding-top: var(--hash-offset); } +.tippy-box { + background-color: var(--color-text); + color: var(--color-background); +} + +.tippy-arrow { + color: var(--color-text); +} + a, a:visited { color: var(--color-link); @@ -303,10 +312,7 @@ p code { font-size: 0.95rem; max-height: calc(100vh - var(--header-height)); overflow-y: auto; - overscroll-behavior: contain; - padding-top: var(--gap); - padding-bottom: var(--gap); - padding-left: var(--gap); + padding: var(--gap) var(--small-gap); position: fixed; top: var(--header-height); transition: transform 0.5s ease; @@ -315,6 +321,8 @@ p code { height: 100%; display: flex; flex-direction: column; + overflow-x: hidden; + word-break: normal; } .sidebar h2 { @@ -340,17 +348,9 @@ p code { .sidebar li { line-height: 1.2; margin-bottom: 4px; -} - -.sidebar ul li a > strong { - font-weight: 900; - color: var(--color-link); -} - -.sidebar ul li a > strong::before { - font-size: 0.75em; - content: 'ᐅ '; - padding-right: 0.1rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .sidebar .sidebar-toggle { @@ -1059,3 +1059,57 @@ body.theme-dark { padding-top: 0; } } + +.sidebar li[data-indent] { + margin-bottom: unset; + display: flex; + flex-direction: row; + color: var(--color-background-accent); +} + +.sidebar li[data-indent] span, +.sidebar li[data-indent] a { + display: flex; + width: 100%; + position: relative; + left: -0.5rem; + padding-left: 0.75rem; +} + +.sidebar li[data-indent] a:hover, +.sidebar li[data-current] a, +.sidebar li[data-current] a:visited { + padding-right: 0.25rem; + text-decoration: none; + color: var(--color-text-accent); + background: + linear-gradient( + 115deg, + transparent 0.5rem, + var(--color-background-accent) 0.5rem + ); +} + +.sidebar li[data-indent="0"] { + font-size: 1.1em; +} + +.sidebar li[data-indent="0"]:not(first-child) { + margin-top: 0.5rem; +} + +.sidebar li[data-indent]::before { + content: '/'; + font-size: 1.05em; + font-family: monospace; + padding-left: 0.25rem; + letter-spacing: -0.1rem; + padding-left: 4rem; + display: flex; + color: var(--color-background-accent); +} + +.sidebar li[data-indent="0"]::before { display: none; } +.sidebar li[data-indent="1"]::before { padding-left: 1rem; } +.sidebar li[data-indent="2"]::before { padding-left: 2rem; } +.sidebar li[data-indent="3"]::before { padding-left: 3rem; } From 10c829edfa33ae1b616af9d6d425e5eefeb7c0e5 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Thu, 22 Aug 2024 15:08:21 +0200 Subject: [PATCH 4/7] Parse and display documentation section headers. The idea is pretty simple, we'll just look for lines starting with Markdown heading sections, and render them in the documentation. They appear both in the sidebar, and within the generated docs themselves in between functions. This, coupled with the order preservation of the declaration in a module should make the generated docs significantly more elegant to organize and present. --- crates/aiken-project/Cargo.toml | 2 +- crates/aiken-project/src/docs.rs | 118 ++++++++++++++----- crates/aiken-project/templates/css/index.css | 33 ++++++ crates/aiken-project/templates/module.html | 49 +++++--- 4 files changed, 158 insertions(+), 44 deletions(-) diff --git a/crates/aiken-project/Cargo.toml b/crates/aiken-project/Cargo.toml index 257bb6a7..5515fc16 100644 --- a/crates/aiken-project/Cargo.toml +++ b/crates/aiken-project/Cargo.toml @@ -16,7 +16,7 @@ build = "build.rs" [dependencies] aiken-lang = { path = "../aiken-lang", version = "1.0.31-alpha" } -askama = "0.12.0" +askama = { version = "0.12.0", features = ["urlencode"] } ciborium = "0.2.2" cryptoxide = "0.4.4" dirs = "4.0.0" diff --git a/crates/aiken-project/src/docs.rs b/crates/aiken-project/src/docs.rs index 2e523397..7f41249f 100644 --- a/crates/aiken-project/src/docs.rs +++ b/crates/aiken-project/src/docs.rs @@ -4,10 +4,11 @@ use crate::{ }; use aiken_lang::{ ast::{ - DataType, Definition, Function, ModuleConstant, RecordConstructor, TypeAlias, + DataType, Definition, Function, ModuleConstant, RecordConstructor, Span, TypeAlias, TypedDefinition, }, format, + parser::extra::Comment, tipo::Type, }; use askama::Template; @@ -42,7 +43,7 @@ struct ModuleTemplate<'a> { project_name: &'a str, project_version: &'a str, modules: &'a [DocLink], - functions: Vec, + functions: Vec, types: Vec, constants: Vec, documentation: String, @@ -154,16 +155,62 @@ fn generate_module( ) -> (Vec, DocFile) { let mut search_indexes = vec![]; + // Section headers + let mut section_headers = module + .extra + .comments + .iter() + .filter_map(|span| { + let comment = Comment::from((span, module.code.as_str())) + .content + .trim_start(); + if comment.starts_with("#") { + let trimmed = comment.trim_start_matches("#"); + let heading = comment.len() - trimmed.len(); + Some(( + span, + DocSection { + heading, + title: trimmed.trim_start().to_string(), + }, + )) + } else { + None + } + }) + .collect_vec(); + // Functions - let functions: Vec = module + let functions: Vec<(Span, DocFunction)> = module .ast .definitions .iter() .flat_map(DocFunction::from_definition) .collect(); - functions - .iter() - .for_each(|function| search_indexes.push(SearchIndex::from_function(module, function))); + + functions.iter().for_each(|(_, function)| { + search_indexes.push(SearchIndex::from_function(module, function)) + }); + + let no_functions = functions.is_empty(); + + let mut functions_and_headers = Vec::new(); + + for (span_fn, function) in functions { + let mut to_remove = vec![]; + for (ix, (span_h, header)) in section_headers.iter().enumerate() { + if span_h.start < span_fn.start { + functions_and_headers.push(Interspersed::Section(header.clone())); + to_remove.push(ix); + } + } + + for ix in to_remove.iter().rev() { + section_headers.remove(*ix); + } + + functions_and_headers.push(Interspersed::Function(function)) + } // Types let types: Vec = module @@ -187,7 +234,7 @@ fn generate_module( .iter() .for_each(|constant| search_indexes.push(SearchIndex::from_constant(module, constant))); - let is_empty = functions.is_empty() && types.is_empty() && constants.is_empty(); + let is_empty = no_functions && types.is_empty() && constants.is_empty(); // Module if !is_empty { @@ -203,7 +250,7 @@ fn generate_module( page_title: &format!("{} - {}", module.name, config.name), module_name: module.name.clone(), project_version: &config.version.to_string(), - functions, + functions: functions_and_headers, types, constants, source, @@ -391,7 +438,19 @@ impl SearchIndex { } } -#[derive(PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +enum Interspersed { + Section(DocSection), + Function(DocFunction), +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +struct DocSection { + heading: usize, + title: String, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] struct DocFunction { name: String, signature: String, @@ -401,26 +460,29 @@ struct DocFunction { } impl DocFunction { - fn from_definition(def: &TypedDefinition) -> Option { + fn from_definition(def: &TypedDefinition) -> Option<(Span, Self)> { match def { - Definition::Fn(func_def) if func_def.public => Some(DocFunction { - name: func_def.name.clone(), - documentation: func_def - .doc - .as_deref() - .map(render_markdown) - .unwrap_or_default(), - raw_documentation: func_def.doc.as_deref().unwrap_or_default().to_string(), - signature: format::Formatter::new() - .docs_fn_signature( - &func_def.name, - &func_def.arguments, - &func_def.return_annotation, - func_def.return_type.clone(), - ) - .to_pretty_string(MAX_COLUMNS), - source_url: "#todo".to_string(), - }), + Definition::Fn(func_def) if func_def.public => Some(( + func_def.location, + DocFunction { + name: func_def.name.clone(), + documentation: func_def + .doc + .as_deref() + .map(render_markdown) + .unwrap_or_default(), + raw_documentation: func_def.doc.as_deref().unwrap_or_default().to_string(), + signature: format::Formatter::new() + .docs_fn_signature( + &func_def.name, + &func_def.arguments, + &func_def.return_annotation, + func_def.return_type.clone(), + ) + .to_pretty_string(MAX_COLUMNS), + source_url: "#todo".to_string(), + }, + )), _ => None, } } diff --git a/crates/aiken-project/templates/css/index.css b/crates/aiken-project/templates/css/index.css index 14126e18..eeb94a50 100644 --- a/crates/aiken-project/templates/css/index.css +++ b/crates/aiken-project/templates/css/index.css @@ -290,11 +290,13 @@ p code { /* Module doc */ .module-name > a, +.module-heading > a, .module-member-kind > a { color: inherit; } .module-name > a:hover, +.module-heading > a:hover, .module-member-kind > a:hover { text-decoration: none; } @@ -328,6 +330,7 @@ p code { .sidebar h2 { margin: 0; color: var(--color-link-accent); + font-size: 1.75em; } .sidebar h3.modules-prefix { @@ -407,6 +410,10 @@ body.drawer-open .label-closed { .module-member-kind { font-size: 2rem; +} + +.module-heading, +.module-member-kind { color: var(--color-text); } @@ -1113,3 +1120,29 @@ body.theme-dark { .sidebar li[data-indent="1"]::before { padding-left: 1rem; } .sidebar li[data-indent="2"]::before { padding-left: 2rem; } .sidebar li[data-indent="3"]::before { padding-left: 3rem; } + +.sidebar li[data-heading] { + font-weight: bold; + font-size: 1em; + color: var(--color-link-accent); +} + +.sidebar li[data-heading="1"] { + margin-top: 1.5rem; + font-size: 1.4em; +} + +.sidebar li[data-heading="2"] { + margin-top: 1rem; + font-size: 1.2em; +} + +.sidebar li[data-heading="3"] { + margin-top: 0.5rem; + font-size: 1.1em; +} + +.sidebar li[data-heading]:first-child, +.sidebar li[data-heading] + li[data-heading] { + margin-top: 0; +} diff --git a/crates/aiken-project/templates/module.html b/crates/aiken-project/templates/module.html index 8a695650..17c8f0b2 100644 --- a/crates/aiken-project/templates/module.html +++ b/crates/aiken-project/templates/module.html @@ -22,8 +22,14 @@ {% if !functions.is_empty() %}

Functions

    - {% for function in functions %} -
  • {{ function.name }}
  • + {% for function_or_section in functions %} + {% match function_or_section %} + {% when Interspersed::Function with (function) %} +
  • {{ function.name }}
  • + + {% when Interspersed::Section with (section) %} +
  • {{ section.title }}
  • + {% endmatch %} {% endfor %}
{% endif %} @@ -118,20 +124,33 @@

Functions

- {% for function in functions %} -
-
-

{{ function.signature }}

- {% if !function.source_url.is_empty() %} - + {% for function_or_section in functions %} + {% match function_or_section %} + {% when Interspersed::Function with (function) %} +
+
+

{{ function.signature }}

+ {% if !function.source_url.is_empty() %} + + {% endif %} +
+
{{ function.documentation|safe }}
+
+ {% when Interspersed::Section with (section) %} + {% if section.heading == 1 %} +

{{ section.title }}

+ {% else if section.heading == 2 %} +

{{ section.title }}

+ {% else if section.heading == 3 %} +

{{ section.title }}

+ {% else %} +

{{ section.title }}

{% endif %} -
-
{{ function.documentation|safe }}
-
+ {% endmatch %} {% endfor %} {% endif %} From 7eee3ce63cfa63e22c92e3821a21cc0138030794 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Thu, 22 Aug 2024 15:08:34 +0200 Subject: [PATCH 5/7] Fix CLI output colors when generating documentation. --- crates/aiken-project/src/telemetry.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/aiken-project/src/telemetry.rs b/crates/aiken-project/src/telemetry.rs index 2b7da6de..3c7fe883 100644 --- a/crates/aiken-project/src/telemetry.rs +++ b/crates/aiken-project/src/telemetry.rs @@ -97,10 +97,11 @@ impl EventListener for Terminal { root, } => { eprintln!( - "{} {} {} ({})", - " Generating documentation" + "{} {} for {} {} ({})", + " Generating" .if_supports_color(Stderr, |s| s.bold()) .if_supports_color(Stderr, |s| s.purple()), + "documentation".if_supports_color(Stderr, |s| s.bold()), name.if_supports_color(Stderr, |s| s.bold()), version, root.to_str() @@ -140,10 +141,11 @@ impl EventListener for Terminal { } Event::GeneratingDocFiles { output_path } => { eprintln!( - "{} in {}", - " Generating documentation files" + "{} {} to {}", + " Writing" .if_supports_color(Stderr, |s| s.bold()) .if_supports_color(Stderr, |s| s.purple()), + "documentation files".if_supports_color(Stderr, |s| s.bold()), output_path .to_str() .unwrap_or("") From 44e42d608da5990ced5572d20ea2d5920246b1b2 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Thu, 22 Aug 2024 15:29:49 +0200 Subject: [PATCH 6/7] Allow hidden modules, not generating any documentation. This is useful when splitting module for dependencies, yet without the desire to expose internal constructors and types. This merely skips the documentation generation; but doesn't prevent the hidden module from being accessible. --- crates/aiken-project/src/docs.rs | 23 ++++++++++++++--------- crates/aiken-project/src/module.rs | 9 +++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/crates/aiken-project/src/docs.rs b/crates/aiken-project/src/docs.rs index 7f41249f..79bfa4d0 100644 --- a/crates/aiken-project/src/docs.rs +++ b/crates/aiken-project/src/docs.rs @@ -127,6 +127,10 @@ pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>) let mut search_indexes: Vec = vec![]; for module in &modules { + if module.skip_doc_generation() { + continue; + } + let (indexes, file) = generate_module(config, module, &modules_links, &source, ×tamp); if !indexes.is_empty() { search_indexes.extend(indexes); @@ -356,15 +360,16 @@ fn generate_modules_links(modules: &[&CheckedModule]) -> Vec { let non_empty_modules = modules .iter() .filter(|module| { - module.ast.definitions.iter().any(|def| { - matches!( - def, - Definition::Fn(Function { public: true, .. }) - | Definition::DataType(DataType { public: true, .. }) - | Definition::TypeAlias(TypeAlias { public: true, .. }) - | Definition::ModuleConstant(ModuleConstant { public: true, .. }) - ) - }) + !module.skip_doc_generation() + && module.ast.definitions.iter().any(|def| { + matches!( + def, + Definition::Fn(Function { public: true, .. }) + | Definition::DataType(DataType { public: true, .. }) + | Definition::TypeAlias(TypeAlias { public: true, .. }) + | Definition::ModuleConstant(ModuleConstant { public: true, .. }) + ) + }) }) .sorted_by(|a, b| a.name.cmp(&b.name)) .collect_vec(); diff --git a/crates/aiken-project/src/module.rs b/crates/aiken-project/src/module.rs index f6a3b4e2..cb28ff5c 100644 --- a/crates/aiken-project/src/module.rs +++ b/crates/aiken-project/src/module.rs @@ -291,6 +291,15 @@ pub struct CheckedModule { } impl CheckedModule { + pub fn skip_doc_generation(&self) -> bool { + self.ast + .docs + .first() + .map(|s| s.as_str().trim()) + .unwrap_or_default() + == "@hidden" + } + pub fn to_cbor(&self) -> Vec { let mut module_bytes = vec![]; From b479a289cf341cffebb1cf8dd5b4b7065d51bf27 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Thu, 22 Aug 2024 16:37:48 +0200 Subject: [PATCH 7/7] Implement source linker and add URL to source code in generated docs. Long overdue. --- Cargo.lock | 7 ++ crates/aiken-project/Cargo.toml | 1 + crates/aiken-project/src/docs.rs | 39 ++++++--- crates/aiken-project/src/docs/source_links.rs | 84 +++++++++++++++++++ crates/aiken-project/templates/_layout.html | 2 + crates/aiken-project/templates/css/index.css | 16 ++++ crates/aiken-project/templates/module.html | 18 +--- 7 files changed, 140 insertions(+), 27 deletions(-) create mode 100644 crates/aiken-project/src/docs/source_links.rs diff --git a/Cargo.lock b/Cargo.lock index d3d6085b..d3eb9d7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,7 @@ dependencies = [ "askama", "blst", "built", + "camino", "ciborium", "cryptoxide", "dirs", @@ -463,6 +464,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" + [[package]] name = "cc" version = "1.1.8" diff --git a/crates/aiken-project/Cargo.toml b/crates/aiken-project/Cargo.toml index 5515fc16..eb021e28 100644 --- a/crates/aiken-project/Cargo.toml +++ b/crates/aiken-project/Cargo.toml @@ -17,6 +17,7 @@ build = "build.rs" [dependencies] aiken-lang = { path = "../aiken-lang", version = "1.0.31-alpha" } askama = { version = "0.12.0", features = ["urlencode"] } +camino = "1.1.9" ciborium = "0.2.2" cryptoxide = "0.4.4" dirs = "4.0.0" diff --git a/crates/aiken-project/src/docs.rs b/crates/aiken-project/src/docs.rs index 79bfa4d0..767d8ceb 100644 --- a/crates/aiken-project/src/docs.rs +++ b/crates/aiken-project/src/docs.rs @@ -26,6 +26,7 @@ const MAX_COLUMNS: isize = 999; const VERSION: &str = env!("CARGO_PKG_VERSION"); mod link_tree; +mod source_links; #[derive(Debug, PartialEq, Eq, Clone)] pub struct DocFile { @@ -131,7 +132,8 @@ pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>) continue; } - let (indexes, file) = generate_module(config, module, &modules_links, &source, ×tamp); + let (indexes, file) = + generate_module(root, config, module, &modules_links, &source, ×tamp); if !indexes.is_empty() { search_indexes.extend(indexes); output_files.push(file); @@ -151,6 +153,7 @@ pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>) } fn generate_module( + root: &Path, config: &Config, module: &CheckedModule, modules: &[DocLink], @@ -159,6 +162,8 @@ fn generate_module( ) -> (Vec, DocFile) { let mut search_indexes = vec![]; + let source_linker = source_links::SourceLinker::new(root, config, module); + // Section headers let mut section_headers = module .extra @@ -189,7 +194,7 @@ fn generate_module( .ast .definitions .iter() - .flat_map(DocFunction::from_definition) + .flat_map(|def| DocFunction::from_definition(def, &source_linker)) .collect(); functions.iter().for_each(|(_, function)| { @@ -221,7 +226,7 @@ fn generate_module( .ast .definitions .iter() - .flat_map(DocType::from_definition) + .flat_map(|def| DocType::from_definition(def, &source_linker)) .collect(); types .iter() @@ -232,7 +237,7 @@ fn generate_module( .ast .definitions .iter() - .flat_map(DocConstant::from_definition) + .flat_map(|def| DocConstant::from_definition(def, &source_linker)) .collect(); constants .iter() @@ -465,7 +470,10 @@ struct DocFunction { } impl DocFunction { - fn from_definition(def: &TypedDefinition) -> Option<(Span, Self)> { + fn from_definition( + def: &TypedDefinition, + source_linker: &source_links::SourceLinker, + ) -> Option<(Span, Self)> { match def { Definition::Fn(func_def) if func_def.public => Some(( func_def.location, @@ -485,7 +493,8 @@ impl DocFunction { func_def.return_type.clone(), ) .to_pretty_string(MAX_COLUMNS), - source_url: "#todo".to_string(), + source_url: source_linker + .url(func_def.location.map_end(|_| func_def.end_position)), }, )), _ => None, @@ -503,7 +512,10 @@ struct DocConstant { } impl DocConstant { - fn from_definition(def: &TypedDefinition) -> Option { + fn from_definition( + def: &TypedDefinition, + source_linker: &source_links::SourceLinker, + ) -> Option { match def { Definition::ModuleConstant(const_def) if const_def.public => Some(DocConstant { name: const_def.name.clone(), @@ -516,7 +528,7 @@ impl DocConstant { definition: format::Formatter::new() .docs_const_expr(&const_def.name, &const_def.value) .to_pretty_string(MAX_COLUMNS), - source_url: "#todo".to_string(), + source_url: source_linker.url(const_def.location), }), _ => None, } @@ -536,7 +548,10 @@ struct DocType { } impl DocType { - fn from_definition(def: &TypedDefinition) -> Option { + fn from_definition( + def: &TypedDefinition, + source_linker: &source_links::SourceLinker, + ) -> Option { match def { Definition::TypeAlias(info) if info.public => Some(DocType { name: info.alias.clone(), @@ -548,7 +563,7 @@ impl DocType { constructors: vec![], parameters: info.parameters.clone(), opaque: false, - source_url: "#todo".to_string(), + source_url: source_linker.url(info.location), }), Definition::DataType(info) if info.public && !info.opaque => Some(DocType { @@ -570,7 +585,7 @@ impl DocType { .collect(), parameters: info.parameters.clone(), opaque: info.opaque, - source_url: "#todo".to_string(), + source_url: source_linker.url(info.location), }), Definition::DataType(info) if info.public && info.opaque => Some(DocType { @@ -583,7 +598,7 @@ impl DocType { constructors: vec![], parameters: info.parameters.clone(), opaque: info.opaque, - source_url: "#todo".to_string(), + source_url: source_linker.url(info.location), }), _ => None, diff --git a/crates/aiken-project/src/docs/source_links.rs b/crates/aiken-project/src/docs/source_links.rs new file mode 100644 index 00000000..092539ec --- /dev/null +++ b/crates/aiken-project/src/docs/source_links.rs @@ -0,0 +1,84 @@ +use crate::{ + config::{Config, Platform}, + CheckedModule, +}; +use aiken_lang::{ast::Span, line_numbers::LineNumbers}; +use camino::{Utf8Component, Utf8Path}; +use std::path::Path; + +pub struct SourceLinker { + line_numbers: LineNumbers, + url_pattern: Option<(String, String)>, +} + +impl SourceLinker { + pub fn new(root: &Path, config: &Config, module: &CheckedModule) -> Self { + let utf8_path = <&Utf8Path>::try_from( + module + .input_path + .as_path() + .strip_prefix(root) + .expect("root path isn't a prefix of project modules' paths!"), + ) + .expect("module path contains non UTF-8 characters!"); + + let path_in_repo = to_url_path(utf8_path).unwrap_or_default(); + + let url_pattern = config + .repository + .as_ref() + .map(|repository| match repository.platform { + Platform::Github => ( + format!( + "https://github.com/{}/{}/blob/{}/{}#L", + repository.user, repository.project, config.version, path_in_repo + ), + "-L".into(), + ), + Platform::Gitlab => ( + format!( + "https://gitlab.com/{}/{}/-/blob/{}/{}#L", + repository.user, repository.project, config.version, path_in_repo + ), + "-".into(), + ), + Platform::Bitbucket => ( + format!( + "https://bitbucket.com/{}/{}/src/{}/{}#lines-", + repository.user, repository.project, config.version, path_in_repo + ), + ":".into(), + ), + }); + + SourceLinker { + line_numbers: LineNumbers::new(&module.code), + url_pattern, + } + } + + pub fn url(&self, span: Span) -> String { + match &self.url_pattern { + Some((base, line_sep)) => { + let start_line = self.line_numbers.line_number(span.start).unwrap(); + let end_line = self.line_numbers.line_number(span.end).unwrap(); + format!("{base}{start_line}{line_sep}{end_line}") + } + None => "".into(), + } + } +} + +fn to_url_path(path: &Utf8Path) -> Option { + let mut buf = String::new(); + for c in path.components() { + if let Utf8Component::Normal(s) = c { + buf.push_str(s); + } + buf.push('/'); + } + + let _ = buf.pop(); + + Some(buf) +} diff --git a/crates/aiken-project/templates/_layout.html b/crates/aiken-project/templates/_layout.html index 5a32c00e..ae91bcc3 100644 --- a/crates/aiken-project/templates/_layout.html +++ b/crates/aiken-project/templates/_layout.html @@ -220,6 +220,8 @@ + + diff --git a/crates/aiken-project/templates/css/index.css b/crates/aiken-project/templates/css/index.css index eeb94a50..ec987959 100644 --- a/crates/aiken-project/templates/css/index.css +++ b/crates/aiken-project/templates/css/index.css @@ -464,6 +464,22 @@ body.drawer-open .label-closed { margin: 0 0 0 var(--small-gap); } +a.member-source, +a.member-source:hover, +a.member-source:visited { + text-decoration: none; + color: var(--color-text-accent); +} + +a.member-source::before { + content: '{ ... }'; + transition: all 0.5s ease-out; +} + +a.member-source:hover::before { + content: '{ view source }'; +} + /* Custom type constructors */ .constructor-list { diff --git a/crates/aiken-project/templates/module.html b/crates/aiken-project/templates/module.html index 17c8f0b2..4e087251 100644 --- a/crates/aiken-project/templates/module.html +++ b/crates/aiken-project/templates/module.html @@ -56,11 +56,7 @@ {% if !type_info.source_url.is_empty() %} - + {% endif %}
@@ -106,11 +102,7 @@

{{ constant.definition }}

{% if !constant.source_url.is_empty() %} - + {% endif %}
{{ constant.documentation|safe }}
@@ -131,11 +123,7 @@

{{ function.signature }}

{% if !function.source_url.is_empty() %} - + {% endif %}
{{ function.documentation|safe }}