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 }}