Merge branch 'generated-docs-improvements'

This commit is contained in:
KtorZ 2024-08-22 16:38:15 +02:00
commit 39c1b5a68a
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
15 changed files with 1007 additions and 292 deletions

View File

@ -20,6 +20,12 @@
- **aiken-project**: warning on compiler version mismatch. See [de870e2](https://github.com/aiken-lang/aiken/commit/de870e2529eb2336957e228cd30d4850ec2619a2). @rvcas - **aiken-project**: warning on compiler version mismatch. See [de870e2](https://github.com/aiken-lang/aiken/commit/de870e2529eb2336957e228cd30d4850ec2619a2). @rvcas
- **aiken-project**: source links to generated documentation for types, constants and functions. @KtorZ
- **aiken-project**: comments containing Markdown section headings (`#`, `##`, `###` etc.) will now be preserved and rendered in generated documentation. @KtorZ
- **aiken-project**: modules starting with `@hidden` in their docs will be skipped from docs generation. @KtorZ
- **uplc**: support evaluation of Plutus V3 transactions, including new purposes introduced in Conway. @KtorZ - **uplc**: support evaluation of Plutus V3 transactions, including new purposes introduced in Conway. @KtorZ
### Changed ### Changed
@ -75,6 +81,10 @@
- **aiken-project**: provide better error (include input ref) when inputs are missing during transaction evaluation. See [#974](https://github.com/aiken-lang/aiken/issues/974). @KtorZ - **aiken-project**: provide better error (include input ref) when inputs are missing during transaction evaluation. See [#974](https://github.com/aiken-lang/aiken/issues/974). @KtorZ
- **aiken-project**: module inhabitants are no longer alphabetically sorted when generating documentation. Instead, the order in which they are defined in the module is used. @KtorZ
- **aiken-project**: the sidebar links to modules within a package is now fully hierarchical and (hopefully) better-looking. @KtorZ
### Removed ### Removed
- **aiken-lang**: clause guards are no longer part of the language. See [#886](https://github.com/aiken-lang/aiken/issues/886). @KtorZ. - **aiken-lang**: clause guards are no longer part of the language. See [#886](https://github.com/aiken-lang/aiken/issues/886). @KtorZ.

7
Cargo.lock generated vendored
View File

@ -131,6 +131,7 @@ dependencies = [
"askama", "askama",
"blst", "blst",
"built", "built",
"camino",
"ciborium", "ciborium",
"cryptoxide", "cryptoxide",
"dirs", "dirs",
@ -463,6 +464,12 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "camino"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.8" version = "1.1.8"

View File

@ -16,7 +16,8 @@ build = "build.rs"
[dependencies] [dependencies]
aiken-lang = { path = "../aiken-lang", version = "1.0.31-alpha" } aiken-lang = { path = "../aiken-lang", version = "1.0.31-alpha" }
askama = "0.12.0" askama = { version = "0.12.0", features = ["urlencode"] }
camino = "1.1.9"
ciborium = "0.2.2" ciborium = "0.2.2"
cryptoxide = "0.4.4" cryptoxide = "0.4.4"
dirs = "4.0.0" dirs = "4.0.0"

View File

@ -4,10 +4,11 @@ use crate::{
}; };
use aiken_lang::{ use aiken_lang::{
ast::{ ast::{
DataType, Definition, Function, ModuleConstant, RecordConstructor, TypeAlias, DataType, Definition, Function, ModuleConstant, RecordConstructor, Span, TypeAlias,
TypedDefinition, TypedDefinition,
}, },
format, format,
parser::extra::Comment,
tipo::Type, tipo::Type,
}; };
use askama::Template; use askama::Template;
@ -24,6 +25,9 @@ use std::{
const MAX_COLUMNS: isize = 999; const MAX_COLUMNS: isize = 999;
const VERSION: &str = env!("CARGO_PKG_VERSION"); const VERSION: &str = env!("CARGO_PKG_VERSION");
mod link_tree;
mod source_links;
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub struct DocFile { pub struct DocFile {
pub path: PathBuf, pub path: PathBuf,
@ -39,9 +43,8 @@ struct ModuleTemplate<'a> {
module_name: String, module_name: String,
project_name: &'a str, project_name: &'a str,
project_version: &'a str, project_version: &'a str,
modules_prefix: String, modules: &'a [DocLink],
modules: &'a Vec<DocLink>, functions: Vec<Interspersed>,
functions: Vec<DocFunction>,
types: Vec<DocType>, types: Vec<DocType>,
constants: Vec<DocConstant>, constants: Vec<DocConstant>,
documentation: String, documentation: String,
@ -66,8 +69,7 @@ struct PageTemplate<'a> {
page_title: &'a str, page_title: &'a str,
project_name: &'a str, project_name: &'a str,
project_version: &'a str, project_version: &'a str,
modules_prefix: String, modules: &'a [DocLink],
modules: &'a Vec<DocLink>,
content: String, content: String,
source: &'a DocLink, source: &'a DocLink,
timestamp: &'a str, timestamp: &'a str,
@ -81,6 +83,7 @@ impl<'a> PageTemplate<'a> {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
struct DocLink { struct DocLink {
indent: usize,
name: String, name: String,
path: String, path: String,
} }
@ -89,6 +92,10 @@ impl DocLink {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.name.is_empty() self.name.is_empty()
} }
pub fn is_separator(&self) -> bool {
self.path.is_empty()
}
} }
/// Generate documentation files for a given project. /// Generate documentation files for a given project.
@ -98,10 +105,11 @@ impl DocLink {
/// across multiple modules. /// across multiple modules.
pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>) -> Vec<DocFile> { pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>) -> Vec<DocFile> {
let timestamp = new_timestamp(); 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 { let source = match &config.repository {
None => DocLink { None => DocLink {
indent: 0,
name: String::new(), name: String::new(),
path: String::new(), path: String::new(),
}, },
@ -110,6 +118,7 @@ pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>)
project, project,
platform, platform,
}) => DocLink { }) => DocLink {
indent: 0,
name: format!("{user}/{project}"), name: format!("{user}/{project}"),
path: format!("https://{platform}.com/{user}/{project}"), path: format!("https://{platform}.com/{user}/{project}"),
}, },
@ -119,13 +128,12 @@ pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>)
let mut search_indexes: Vec<SearchIndex> = vec![]; let mut search_indexes: Vec<SearchIndex> = vec![];
for module in &modules { for module in &modules {
let (indexes, file) = generate_module( if module.skip_doc_generation() {
config, continue;
module, }
(&modules_prefix, &modules_links),
&source, let (indexes, file) =
&timestamp, generate_module(root, config, module, &modules_links, &source, &timestamp);
);
if !indexes.is_empty() { if !indexes.is_empty() {
search_indexes.extend(indexes); search_indexes.extend(indexes);
output_files.push(file); output_files.push(file);
@ -136,7 +144,7 @@ pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>)
output_files.push(generate_readme( output_files.push(generate_readme(
root, root,
config, config,
(&modules_prefix, &modules_links), &modules_links,
&source, &source,
&timestamp, &timestamp,
)); ));
@ -145,33 +153,80 @@ pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>)
} }
fn generate_module( fn generate_module(
root: &Path,
config: &Config, config: &Config,
module: &CheckedModule, module: &CheckedModule,
(modules_prefix, modules): (&str, &Vec<DocLink>), modules: &[DocLink],
source: &DocLink, source: &DocLink,
timestamp: &Duration, timestamp: &Duration,
) -> (Vec<SearchIndex>, DocFile) { ) -> (Vec<SearchIndex>, DocFile) {
let mut search_indexes = vec![]; let mut search_indexes = vec![];
let source_linker = source_links::SourceLinker::new(root, config, module);
// 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 // Functions
let functions: Vec<DocFunction> = module let functions: Vec<(Span, DocFunction)> = module
.ast .ast
.definitions .definitions
.iter() .iter()
.flat_map(DocFunction::from_definition) .flat_map(|def| DocFunction::from_definition(def, &source_linker))
.sorted()
.collect(); .collect();
functions
.iter() functions.iter().for_each(|(_, function)| {
.for_each(|function| search_indexes.push(SearchIndex::from_function(module, 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 // Types
let types: Vec<DocType> = module let types: Vec<DocType> = module
.ast .ast
.definitions .definitions
.iter() .iter()
.flat_map(DocType::from_definition) .flat_map(|def| DocType::from_definition(def, &source_linker))
.sorted()
.collect(); .collect();
types types
.iter() .iter()
@ -182,14 +237,13 @@ fn generate_module(
.ast .ast
.definitions .definitions
.iter() .iter()
.flat_map(DocConstant::from_definition) .flat_map(|def| DocConstant::from_definition(def, &source_linker))
.sorted()
.collect(); .collect();
constants constants
.iter() .iter()
.for_each(|constant| search_indexes.push(SearchIndex::from_constant(module, constant))); .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 // Module
if !is_empty { if !is_empty {
@ -200,13 +254,12 @@ fn generate_module(
aiken_version: VERSION, aiken_version: VERSION,
breadcrumbs: to_breadcrumbs(&module.name), breadcrumbs: to_breadcrumbs(&module.name),
documentation: render_markdown(&module.ast.docs.iter().join("\n")), documentation: render_markdown(&module.ast.docs.iter().join("\n")),
modules_prefix: modules_prefix.to_string(),
modules, modules,
project_name: &config.name.repo.to_string(), project_name: &config.name.repo.to_string(),
page_title: &format!("{} - {}", module.name, config.name), page_title: &format!("{} - {}", module.name, config.name),
module_name: module.name.clone(), module_name: module.name.clone(),
project_version: &config.version.to_string(), project_version: &config.version.to_string(),
functions, functions: functions_and_headers,
types, types,
constants, constants,
source, source,
@ -282,7 +335,7 @@ fn generate_static_assets(search_indexes: Vec<SearchIndex>) -> Vec<DocFile> {
fn generate_readme( fn generate_readme(
root: &Path, root: &Path,
config: &Config, config: &Config,
(modules_prefix, modules): (&str, &Vec<DocLink>), modules: &[DocLink],
source: &DocLink, source: &DocLink,
timestamp: &Duration, timestamp: &Duration,
) -> DocFile { ) -> DocFile {
@ -293,7 +346,6 @@ fn generate_readme(
let template = PageTemplate { let template = PageTemplate {
aiken_version: VERSION, aiken_version: VERSION,
breadcrumbs: ".", breadcrumbs: ".",
modules_prefix: modules_prefix.to_string(),
modules, modules,
project_name: &config.name.repo.to_string(), project_name: &config.name.repo.to_string(),
page_title: &config.name.to_string(), page_title: &config.name.to_string(),
@ -309,46 +361,31 @@ fn generate_readme(
} }
} }
fn generate_modules_links(modules: &[&CheckedModule]) -> (String, Vec<DocLink>) { fn generate_modules_links(modules: &[&CheckedModule]) -> Vec<DocLink> {
let non_empty_modules = modules.iter().filter(|module| { let non_empty_modules = modules
module.ast.definitions.iter().any(|def| { .iter()
matches!( .filter(|module| {
def, !module.skip_doc_generation()
Definition::Fn(Function { public: true, .. }) && module.ast.definitions.iter().any(|def| {
| Definition::DataType(DataType { public: true, .. }) matches!(
| Definition::TypeAlias(TypeAlias { public: true, .. }) def,
| Definition::ModuleConstant(ModuleConstant { public: true, .. }) 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 { for module in non_empty_modules {
let module_path = [&module.name.clone(), ".html"].concat(); links.insert(module.name.as_str());
modules_links.push(DocLink {
path: module_path,
name: module.name.to_string().clone(),
});
} }
modules_links.sort();
let prefix = if modules_links.len() > 1 { links.to_vec()
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)
} }
#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)] #[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
@ -411,7 +448,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 { struct DocFunction {
name: String, name: String,
signature: String, signature: String,
@ -421,26 +470,33 @@ struct DocFunction {
} }
impl DocFunction { impl DocFunction {
fn from_definition(def: &TypedDefinition) -> Option<Self> { fn from_definition(
def: &TypedDefinition,
source_linker: &source_links::SourceLinker,
) -> Option<(Span, Self)> {
match def { match def {
Definition::Fn(func_def) if func_def.public => Some(DocFunction { Definition::Fn(func_def) if func_def.public => Some((
name: func_def.name.clone(), func_def.location,
documentation: func_def DocFunction {
.doc name: func_def.name.clone(),
.as_deref() documentation: func_def
.map(render_markdown) .doc
.unwrap_or_default(), .as_deref()
raw_documentation: func_def.doc.as_deref().unwrap_or_default().to_string(), .map(render_markdown)
signature: format::Formatter::new() .unwrap_or_default(),
.docs_fn_signature( raw_documentation: func_def.doc.as_deref().unwrap_or_default().to_string(),
&func_def.name, signature: format::Formatter::new()
&func_def.arguments, .docs_fn_signature(
&func_def.return_annotation, &func_def.name,
func_def.return_type.clone(), &func_def.arguments,
) &func_def.return_annotation,
.to_pretty_string(MAX_COLUMNS), func_def.return_type.clone(),
source_url: "#todo".to_string(), )
}), .to_pretty_string(MAX_COLUMNS),
source_url: source_linker
.url(func_def.location.map_end(|_| func_def.end_position)),
},
)),
_ => None, _ => None,
} }
} }
@ -456,7 +512,10 @@ struct DocConstant {
} }
impl DocConstant { impl DocConstant {
fn from_definition(def: &TypedDefinition) -> Option<Self> { fn from_definition(
def: &TypedDefinition,
source_linker: &source_links::SourceLinker,
) -> Option<Self> {
match def { match def {
Definition::ModuleConstant(const_def) if const_def.public => Some(DocConstant { Definition::ModuleConstant(const_def) if const_def.public => Some(DocConstant {
name: const_def.name.clone(), name: const_def.name.clone(),
@ -469,7 +528,7 @@ impl DocConstant {
definition: format::Formatter::new() definition: format::Formatter::new()
.docs_const_expr(&const_def.name, &const_def.value) .docs_const_expr(&const_def.name, &const_def.value)
.to_pretty_string(MAX_COLUMNS), .to_pretty_string(MAX_COLUMNS),
source_url: "#todo".to_string(), source_url: source_linker.url(const_def.location),
}), }),
_ => None, _ => None,
} }
@ -489,7 +548,10 @@ struct DocType {
} }
impl DocType { impl DocType {
fn from_definition(def: &TypedDefinition) -> Option<Self> { fn from_definition(
def: &TypedDefinition,
source_linker: &source_links::SourceLinker,
) -> Option<Self> {
match def { match def {
Definition::TypeAlias(info) if info.public => Some(DocType { Definition::TypeAlias(info) if info.public => Some(DocType {
name: info.alias.clone(), name: info.alias.clone(),
@ -501,7 +563,7 @@ impl DocType {
constructors: vec![], constructors: vec![],
parameters: info.parameters.clone(), parameters: info.parameters.clone(),
opaque: false, opaque: false,
source_url: "#todo".to_string(), source_url: source_linker.url(info.location),
}), }),
Definition::DataType(info) if info.public && !info.opaque => Some(DocType { Definition::DataType(info) if info.public && !info.opaque => Some(DocType {
@ -523,7 +585,7 @@ impl DocType {
.collect(), .collect(),
parameters: info.parameters.clone(), parameters: info.parameters.clone(),
opaque: info.opaque, 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 { Definition::DataType(info) if info.public && info.opaque => Some(DocType {
@ -536,7 +598,7 @@ impl DocType {
constructors: vec![], constructors: vec![],
parameters: info.parameters.clone(), parameters: info.parameters.clone(),
opaque: info.opaque, opaque: info.opaque,
source_url: "#todo".to_string(), source_url: source_linker.url(info.location),
}), }),
_ => None, _ => None,
@ -611,141 +673,6 @@ fn new_timestamp() -> Duration {
.expect("get current timestamp") .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(&current_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 { fn to_breadcrumbs(path: &str) -> String {
let breadcrumbs = path let breadcrumbs = path
.strip_prefix('/') .strip_prefix('/')

View 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())
)
}

View File

@ -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<String> {
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)
}

View File

@ -291,6 +291,15 @@ pub struct CheckedModule {
} }
impl 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<u8> { pub fn to_cbor(&self) -> Vec<u8> {
let mut module_bytes = vec![]; let mut module_bytes = vec![];

View File

@ -97,10 +97,11 @@ impl EventListener for Terminal {
root, root,
} => { } => {
eprintln!( eprintln!(
"{} {} {} ({})", "{} {} for {} {} ({})",
" Generating documentation" " Generating"
.if_supports_color(Stderr, |s| s.bold()) .if_supports_color(Stderr, |s| s.bold())
.if_supports_color(Stderr, |s| s.purple()), .if_supports_color(Stderr, |s| s.purple()),
"documentation".if_supports_color(Stderr, |s| s.bold()),
name.if_supports_color(Stderr, |s| s.bold()), name.if_supports_color(Stderr, |s| s.bold()),
version, version,
root.to_str() root.to_str()
@ -140,10 +141,11 @@ impl EventListener for Terminal {
} }
Event::GeneratingDocFiles { output_path } => { Event::GeneratingDocFiles { output_path } => {
eprintln!( eprintln!(
"{} in {}", "{} {} to {}",
" Generating documentation files" " Writing"
.if_supports_color(Stderr, |s| s.bold()) .if_supports_color(Stderr, |s| s.bold())
.if_supports_color(Stderr, |s| s.purple()), .if_supports_color(Stderr, |s| s.purple()),
"documentation files".if_supports_color(Stderr, |s| s.bold()),
output_path output_path
.to_str() .to_str()
.unwrap_or("") .unwrap_or("")

View File

@ -495,7 +495,7 @@ impl Prng {
/// Obtain a Prng back from a fuzzer execution. As a reminder, fuzzers have the following /// Obtain a Prng back from a fuzzer execution. As a reminder, fuzzers have the following
/// signature: /// signature:
/// ///
/// type Fuzzer<a> = fn(Prng) -> Option<(Prng, a)> /// `type Fuzzer<a> = fn(Prng) -> Option<(Prng, a)>`
/// ///
/// In nominal scenarios (i.e. when the fuzzer is made from a seed and evolve pseudo-randomly), /// 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 /// 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 { fn as_prng(cst: &PlutusData) -> Prng {
if let PlutusData::Constr(Constr { tag, fields, .. }) = cst { if let PlutusData::Constr(Constr { tag, fields, .. }) = cst {
if *tag == 121 + Prng::SEEDED { if *tag == 121 + Prng::SEEDED {
if let [PlutusData::BoundedBytes(bytes), PlutusData::BoundedBytes(choices)] = if let [
&fields[..] PlutusData::BoundedBytes(bytes),
PlutusData::BoundedBytes(choices),
] = &fields[..]
{ {
return Prng::Seeded { return Prng::Seeded {
choices: choices.to_vec(), choices: choices.to_vec(),
@ -1087,9 +1089,11 @@ impl TryFrom<TypedExpr> for Assertion<TypedExpr> {
final_else, final_else,
.. ..
} => { } => {
if let [IfBranch { if let [
condition, body, .. IfBranch {
}] = &branches[..] condition, body, ..
},
] = &branches[..]
{ {
let then_is_true = match body { let then_is_true = match body {
TypedExpr::Var { TypedExpr::Var {
@ -1509,13 +1513,14 @@ mod test {
} }
"#}); "#});
assert!(prop assert!(
.run::<()>( prop.run::<()>(
42, 42,
PropertyTest::DEFAULT_MAX_SUCCESS, PropertyTest::DEFAULT_MAX_SUCCESS,
&PlutusVersion::default() &PlutusVersion::default()
) )
.is_success()); .is_success()
);
} }
#[test] #[test]

View File

@ -186,23 +186,26 @@
</ul> </ul>
{% endif %} {% endif %}
{% block sidebar_content %}{% endblock %}
<h2>Modules</h2> <h2>Modules</h2>
{% if !modules_prefix.is_empty() %}
<h3 class="modules-prefix">{{ modules_prefix }}/</h3>
{% endif %}
<ul> <ul>
{% for module in modules %} {% for module in modules %}
<li><a href="{{ breadcrumbs }}/{{ module.path }}"> {% if module.is_separator() %}
{% if self.is_current_module(module) %} <li data-indent="{{ module.indent }}"><span>{{ module.name }}</span></li>
<strong>{{ module.name }}</strong>
{% else %} {% else %}
{{ module.name }} {% if self.is_current_module(module) %}
<li data-indent="{{ module.indent }}" data-current><a href="{{ breadcrumbs }}/{{ module.path }}">
<strong>{{ module.name }}</strong>
</a></li>
{% else %}
<li data-indent="{{ module.indent }}"><a href="{{ breadcrumbs }}/{{ module.path }}">
{{ module.name }}
</a></li>
{% endif %}
{% endif %} {% endif %}
</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% block sidebar_content %}{% endblock %}
</nav> </nav>
<main class="content"> <main class="content">
@ -217,6 +220,8 @@
<symbol id="icon-menu" viewBox="0 0 24 24"><path d="M3 13h18c0.552 0 1-0.448 1-1s-0.448-1-1-1h-18c-0.552 0-1 0.448-1 1s0.448 1 1 1zM3 7h18c0.552 0 1-0.448 1-1s-0.448-1-1-1h-18c-0.552 0-1 0.448-1 1s0.448 1 1 1zM3 19h18c0.552 0 1-0.448 1-1s-0.448-1-1-1h-18c-0.552 0-1 0.448-1 1s0.448 1 1 1z"></path></symbol> <symbol id="icon-menu" viewBox="0 0 24 24"><path d="M3 13h18c0.552 0 1-0.448 1-1s-0.448-1-1-1h-18c-0.552 0-1 0.448-1 1s0.448 1 1 1zM3 7h18c0.552 0 1-0.448 1-1s-0.448-1-1-1h-18c-0.552 0-1 0.448-1 1s0.448 1 1 1zM3 19h18c0.552 0 1-0.448 1-1s-0.448-1-1-1h-18c-0.552 0-1 0.448-1 1s0.448 1 1 1z"></path></symbol>
<symbol id="icon-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></symbol>
<symbol id="icon-moon" viewBox="0 0 24 24"><path d="M21.996 12.882c0.022-0.233-0.038-0.476-0.188-0.681-0.325-0.446-0.951-0.544-1.397-0.219-0.95 0.693-2.060 1.086-3.188 1.162-1.368 0.092-2.765-0.283-3.95-1.158-1.333-0.985-2.139-2.415-2.367-3.935s0.124-3.124 1.109-4.456c0.142-0.191 0.216-0.435 0.191-0.691-0.053-0.55-0.542-0.952-1.092-0.898-2.258 0.22-4.314 1.18-5.895 2.651-1.736 1.615-2.902 3.847-3.137 6.386-0.254 2.749 0.631 5.343 2.266 7.311s4.022 3.313 6.772 3.567 5.343-0.631 7.311-2.266 3.313-4.022 3.567-6.772zM19.567 14.674c-0.49 1.363-1.335 2.543-2.416 3.441-1.576 1.309-3.648 2.016-5.848 1.813s-4.108-1.278-5.417-2.854-2.016-3.648-1.813-5.848c0.187-2.032 1.117-3.814 2.507-5.106 0.782-0.728 1.71-1.3 2.731-1.672-0.456 1.264-0.577 2.606-0.384 3.899 0.303 2.023 1.38 3.934 3.156 5.247 1.578 1.167 3.448 1.668 5.272 1.545 0.752-0.050 1.496-0.207 2.21-0.465z"></path></symbol> <symbol id="icon-moon" viewBox="0 0 24 24"><path d="M21.996 12.882c0.022-0.233-0.038-0.476-0.188-0.681-0.325-0.446-0.951-0.544-1.397-0.219-0.95 0.693-2.060 1.086-3.188 1.162-1.368 0.092-2.765-0.283-3.95-1.158-1.333-0.985-2.139-2.415-2.367-3.935s0.124-3.124 1.109-4.456c0.142-0.191 0.216-0.435 0.191-0.691-0.053-0.55-0.542-0.952-1.092-0.898-2.258 0.22-4.314 1.18-5.895 2.651-1.736 1.615-2.902 3.847-3.137 6.386-0.254 2.749 0.631 5.343 2.266 7.311s4.022 3.313 6.772 3.567 5.343-0.631 7.311-2.266 3.313-4.022 3.567-6.772zM19.567 14.674c-0.49 1.363-1.335 2.543-2.416 3.441-1.576 1.309-3.648 2.016-5.848 1.813s-4.108-1.278-5.417-2.854-2.016-3.648-1.813-5.848c0.187-2.032 1.117-3.814 2.507-5.106 0.782-0.728 1.71-1.3 2.731-1.672-0.456 1.264-0.577 2.606-0.384 3.899 0.303 2.023 1.38 3.934 3.156 5.247 1.578 1.167 3.448 1.668 5.272 1.545 0.752-0.050 1.496-0.207 2.21-0.465z"></path></symbol>
<symbol id="icon-more-horizontal" viewBox="0 0 24 24"><path d="M14 12c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414zM21 12c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414zM7 12c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414z"></path></symbol> <symbol id="icon-more-horizontal" viewBox="0 0 24 24"><path d="M14 12c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414zM21 12c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414zM7 12c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414z"></path></symbol>
@ -276,9 +281,35 @@
el.prepend(a); el.prepend(a);
}); });
</script> </script>
<script src="https://unpkg.com/@popperjs/core@2"></script>
<script src="https://unpkg.com/tippy.js@6"></script>
<script src="{{ breadcrumbs }}/js/lunr.min.js?v={{ aiken_version }}"></script> <script src="{{ breadcrumbs }}/js/lunr.min.js?v={{ aiken_version }}"></script>
<script src="{{ breadcrumbs }}/js/index.js?v={{ timestamp }}"></script> <script src="{{ breadcrumbs }}/js/index.js?v={{ timestamp }}"></script>
<!-- Load the search index using JSONP to avoid CORS issues --> <!-- Load the search index using JSONP to avoid CORS issues -->
<script src="{{ breadcrumbs }}/search-data.js?v={{ timestamp }}"></script> <script src="{{ breadcrumbs }}/search-data.js?v={{ timestamp }}"></script>
<script>
void function() {
if (typeof tippy !== "undefined") {
const overflowed = Array.from(document
.querySelectorAll('.sidebar li:not([data-indent])'))
.filter(x => x.offsetWidth < x.scrollWidth);
tippy(overflowed, {
arrow: true,
placement: 'right',
content: (el) => el.children[0]?.innerText,
});
tippy('.sidebar li[data-indent] a', {
arrow: true,
placement: 'right',
content: (el) => el
.getAttribute('href')
.replaceAll(/\.?\.\//g, '')
.replace('.html', ''),
});
}
}();
</script>
</body> </body>
</html> </html>

View File

@ -5,7 +5,7 @@
--search-width: 680px; --search-width: 680px;
--header-height: 60px; --header-height: 60px;
--hash-offset: calc(var(--header-height) * 1.67); --hash-offset: calc(var(--header-height) * 1.67);
--sidebar-width: 240px; --sidebar-width: 260px;
--gap: 24px; --gap: 24px;
--small-gap: calc(var(--gap) / 2); --small-gap: calc(var(--gap) / 2);
--tiny-gap: calc(var(--small-gap) / 2); --tiny-gap: calc(var(--small-gap) / 2);
@ -60,6 +60,15 @@ html {
scroll-padding-top: var(--hash-offset); 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,
a:visited { a:visited {
color: var(--color-link); color: var(--color-link);
@ -281,11 +290,13 @@ p code {
/* Module doc */ /* Module doc */
.module-name > a, .module-name > a,
.module-heading > a,
.module-member-kind > a { .module-member-kind > a {
color: inherit; color: inherit;
} }
.module-name > a:hover, .module-name > a:hover,
.module-heading > a:hover,
.module-member-kind > a:hover { .module-member-kind > a:hover {
text-decoration: none; text-decoration: none;
} }
@ -303,10 +314,7 @@ p code {
font-size: 0.95rem; font-size: 0.95rem;
max-height: calc(100vh - var(--header-height)); max-height: calc(100vh - var(--header-height));
overflow-y: auto; overflow-y: auto;
overscroll-behavior: contain; padding: var(--gap) var(--small-gap);
padding-top: var(--gap);
padding-bottom: var(--gap);
padding-left: var(--gap);
position: fixed; position: fixed;
top: var(--header-height); top: var(--header-height);
transition: transform 0.5s ease; transition: transform 0.5s ease;
@ -315,11 +323,14 @@ p code {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-x: hidden;
word-break: normal;
} }
.sidebar h2 { .sidebar h2 {
margin: 0; margin: 0;
color: var(--color-link-accent); color: var(--color-link-accent);
font-size: 1.75em;
} }
.sidebar h3.modules-prefix { .sidebar h3.modules-prefix {
@ -340,17 +351,9 @@ p code {
.sidebar li { .sidebar li {
line-height: 1.2; line-height: 1.2;
margin-bottom: 4px; margin-bottom: 4px;
} overflow: hidden;
text-overflow: ellipsis;
.sidebar ul li a > strong { white-space: nowrap;
font-weight: 900;
color: var(--color-link);
}
.sidebar ul li a > strong::before {
font-size: 0.75em;
content: 'ᐅ ';
padding-right: 0.1rem;
} }
.sidebar .sidebar-toggle { .sidebar .sidebar-toggle {
@ -407,6 +410,10 @@ body.drawer-open .label-closed {
.module-member-kind { .module-member-kind {
font-size: 2rem; font-size: 2rem;
}
.module-heading,
.module-member-kind {
color: var(--color-text); color: var(--color-text);
} }
@ -457,6 +464,22 @@ body.drawer-open .label-closed {
margin: 0 0 0 var(--small-gap); 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 */ /* Custom type constructors */
.constructor-list { .constructor-list {
@ -1059,3 +1082,83 @@ body.theme-dark {
padding-top: 0; 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; }
.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;
}

View File

@ -22,8 +22,14 @@
{% if !functions.is_empty() %} {% if !functions.is_empty() %}
<h2>Functions</h2> <h2>Functions</h2>
<ul> <ul>
{% for function in functions %} {% for function_or_section in functions %}
<li><a href="#{{ function.name }}">{{ function.name }}</a></li> {% match function_or_section %}
{% when Interspersed::Function with (function) %}
<li><a href="#{{ function.name }}">{{ function.name }}</a></li>
{% when Interspersed::Section with (section) %}
<li data-heading="{{ section.heading }}"><a href="#{{ section.title|urlencode }}">{{ section.title }}</a></li>
{% endmatch %}
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
@ -50,11 +56,7 @@
</a> </a>
</h2> </h2>
{% if !type_info.source_url.is_empty() %} {% if !type_info.source_url.is_empty() %}
<!-- TODO: support source linking <a class="member-source" alt="view source" title="view source" target="_blank" href="{{ type_info.source_url|safe }}"></a>
<a class="member-source" alt="View Source" title="View Source" href="{{ type_info.source_url|safe }}">
&lt;/&gt;
</a>
-->
{% endif %} {% endif %}
</div> </div>
<div class="custom-type-constructors"> <div class="custom-type-constructors">
@ -100,11 +102,7 @@
<div class="member-name"> <div class="member-name">
<h2 id="{{ constant.name }}"><pre class="hljs language-aiken">{{ constant.definition }}</pre></h2> <h2 id="{{ constant.name }}"><pre class="hljs language-aiken">{{ constant.definition }}</pre></h2>
{% if !constant.source_url.is_empty() %} {% if !constant.source_url.is_empty() %}
<!-- TODO: support source linking <a class="member-source" alt="view source" title="view source" target="_blank" href="{{ constant.source_url|safe }}"></a>
<a class="member-source" alt="View Source" title="View Source" href="{{ constant.source_url|safe }}">
&lt;/&gt;
</a>
-->
{% endif %} {% endif %}
</div> </div>
<div class="rendered-markdown">{{ constant.documentation|safe }}</div> <div class="rendered-markdown">{{ constant.documentation|safe }}</div>
@ -118,20 +116,29 @@
<h1 id="module-functions" class="module-member-kind"> <h1 id="module-functions" class="module-member-kind">
<a href="#module-functions">Functions</a> <a href="#module-functions">Functions</a>
</h1> </h1>
{% for function in functions %} {% for function_or_section in functions %}
<div class="member"> {% match function_or_section %}
<div class="member-name"> {% when Interspersed::Function with (function) %}
<h2 id="{{ function.name }}"><pre class="hljs language-aiken">{{ function.signature }}</pre></h2> <div class="member">
{% if !function.source_url.is_empty() %} <div class="member-name">
<!-- TODO: support source linking <h2 id="{{ function.name }}"><pre class="hljs language-aiken">{{ function.signature }}</pre></h2>
<a class="member-source" alt="View Source" title="View Source" href="{{ function.source_url|safe }}"> {% if !function.source_url.is_empty() %}
&lt;/&gt; <a class="member-source" alt="view source" title="view source" target="_blank" href="{{ function.source_url|safe }}"></a>
</a> {% endif %}
--> </div>
<div class="rendered-markdown">{{ function.documentation|safe }}</div>
</div>
{% when Interspersed::Section with (section) %}
{% if section.heading == 1 %}
<h2 id="{{ section.title|urlencode }}" class="module-heading"><a href="#{{ section.title|urlencode }}">{{ section.title }}</a></h2>
{% else if section.heading == 2 %}
<h2 id="{{ section.title|urlencode }}" class="module-heading"><a href="#{{ section.title|urlencode }}">{{ section.title }}</a></h3>
{% else if section.heading == 3 %}
<h3 id="{{ section.title|urlencode }}" class="module-heading"><a href="#{{ section.title|urlencode }}">{{ section.title }}</a></h4>
{% else %}
<h4 id="{{ section.title|urlencode }}" class="module-heading"><a href="#{{ section.title|urlencode }}">{{ section.title }}</a></h5>
{% endif %} {% endif %}
</div> {% endmatch %}
<div class="rendered-markdown">{{ function.documentation|safe }}</div>
</div>
{% endfor %} {% endfor %}
</section> </section>
{% endif %} {% endif %}

View File

@ -0,0 +1,7 @@
# This file was generated by Aiken
# You typically do not need to edit this file
requirements = []
packages = []
[etags]

View File

@ -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"

View File

@ -0,0 +1,7 @@
fn new_list() {
[]
}
test foo() {
fn() { new_list }()() == []
}