Implement source linker and add URL to source code in generated docs.

Long overdue.
This commit is contained in:
KtorZ 2024-08-22 16:37:48 +02:00
parent 44e42d608d
commit b479a289cf
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
7 changed files with 140 additions and 27 deletions

7
Cargo.lock generated vendored
View File

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

View File

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

View File

@ -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, &timestamp);
let (indexes, file) =
generate_module(root, config, module, &modules_links, &source, &timestamp);
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<SearchIndex>, 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<Self> {
fn from_definition(
def: &TypedDefinition,
source_linker: &source_links::SourceLinker,
) -> Option<Self> {
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<Self> {
fn from_definition(
def: &TypedDefinition,
source_linker: &source_links::SourceLinker,
) -> Option<Self> {
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,

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

@ -220,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-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-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>

View File

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

View File

@ -56,11 +56,7 @@
</a>
</h2>
{% if !type_info.source_url.is_empty() %}
<!-- TODO: support source linking
<a class="member-source" alt="View Source" title="View Source" href="{{ type_info.source_url|safe }}">
&lt;/&gt;
</a>
-->
<a class="member-source" alt="view source" title="view source" target="_blank" href="{{ type_info.source_url|safe }}"></a>
{% endif %}
</div>
<div class="custom-type-constructors">
@ -106,11 +102,7 @@
<div class="member-name">
<h2 id="{{ constant.name }}"><pre class="hljs language-aiken">{{ constant.definition }}</pre></h2>
{% if !constant.source_url.is_empty() %}
<!-- TODO: support source linking
<a class="member-source" alt="View Source" title="View Source" href="{{ constant.source_url|safe }}">
&lt;/&gt;
</a>
-->
<a class="member-source" alt="view source" title="view source" target="_blank" href="{{ constant.source_url|safe }}"></a>
{% endif %}
</div>
<div class="rendered-markdown">{{ constant.documentation|safe }}</div>
@ -131,11 +123,7 @@
<div class="member-name">
<h2 id="{{ function.name }}"><pre class="hljs language-aiken">{{ function.signature }}</pre></h2>
{% if !function.source_url.is_empty() %}
<!-- TODO: support source linking
<a class="member-source" alt="View Source" title="View Source" href="{{ function.source_url|safe }}">
&lt;/&gt;
</a>
-->
<a class="member-source" alt="view source" title="view source" target="_blank" href="{{ function.source_url|safe }}"></a>
{% endif %}
</div>
<div class="rendered-markdown">{{ function.documentation|safe }}</div>