use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::EntityRef;
use crate::layout_paths::{
LayoutEntityKey, ProtobufEntityKind, heading_slug, layout_entity_rel_path, package_index_rel,
package_page_rel, relative_path_from_dir,
};
use crate::options::{Layout, Options};
use crate::paths::{entity_category_dir, entity_rel_path};
use crate::{EntityBody, ReferenceManual, StoredEntity};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LinkContext {
pub layout: Layout,
pub book_root: String,
pub markdown_root: String,
pub entity_paths: HashMap<EntityRef, PathBuf>,
layout_entities: HashMap<LayoutEntityKey, PathBuf>,
pub render_from: Option<PathBuf>,
}
impl LinkContext {
pub fn empty(
layout: Layout,
book_root: impl Into<String>,
markdown_root: impl Into<String>,
) -> Self {
Self {
layout,
book_root: book_root.into(),
markdown_root: markdown_root.into(),
entity_paths: HashMap::new(),
layout_entities: HashMap::new(),
render_from: None,
}
}
pub fn from_manual(manual: &ReferenceManual, opts: &Options) -> Self {
let mut ctx = Self::empty(opts.layout, &opts.book_root, &opts.markdown_root);
for module in &manual.modules {
for contract in &module.contracts {
let is_protobuf = contract.family == "protobuf";
for group in &contract.groups {
if group.id.as_str().is_empty() {
continue;
}
for entity in &group.entities {
ctx.register_stored_entity(
module.id.as_str(),
group.id.as_str(),
&group.dir,
entity,
is_protobuf,
opts.layout,
&opts.markdown_root,
);
}
}
}
}
ctx
}
#[allow(clippy::too_many_arguments)]
pub fn register_stored_entity(
&mut self,
module: &str,
group: &str,
group_dir: &str,
entity: &StoredEntity,
is_protobuf: bool,
layout: Layout,
markdown_root: &str,
) {
let entity_ref = EntityRef {
module: module.to_string(),
group: group.to_string(),
category: entity.category.clone(),
name: entity.name.clone(),
};
if is_protobuf {
if let Some(kind) = protobuf_entity_kind(entity) {
let key = LayoutEntityKey {
package: group.to_string(),
kind,
name: entity.name.clone(),
};
let path = layout_entity_rel_path(layout, markdown_root, &key);
self.layout_entities.insert(key, path.clone());
self.entity_paths.insert(entity_ref, path);
}
return;
}
let rel = match layout {
Layout::Package => package_page_rel(markdown_root, group),
Layout::Entity | Layout::Split => {
let rel_path = entity_rel_path(
group_dir,
entity_category_dir(&entity.category),
&entity.name,
);
PathBuf::from(format!("{markdown_root}/{rel_path}"))
}
};
self.entity_paths.insert(entity_ref, rel);
}
pub fn layout_entity_keys(&self) -> impl Iterator<Item = &LayoutEntityKey> {
self.layout_entities.keys()
}
pub fn package_page_rel(&self, package: &str) -> PathBuf {
package_page_rel(&self.markdown_root, package)
}
pub fn package_index_rel(&self, package: &str) -> PathBuf {
package_index_rel(self.layout, &self.markdown_root, package)
}
pub fn layout_entity_path(
&self,
package: &str,
kind: ProtobufEntityKind,
name: &str,
) -> Option<&PathBuf> {
self.layout_entities.get(&LayoutEntityKey {
package: package.to_string(),
kind,
name: name.to_string(),
})
}
pub fn entity_path(&self, entity_ref: &EntityRef) -> Option<&PathBuf> {
self.entity_paths.get(entity_ref)
}
pub fn link_from(
&self,
from: &Path,
package: &str,
kind: ProtobufEntityKind,
name: &str,
) -> String {
let Some(target) = self.layout_entity_path(package, kind, name) else {
return format!("`.{package}.{name}`");
};
match self.layout {
Layout::Package => self.package_layout_link(from, target, name),
Layout::Entity | Layout::Split => self.file_link(from, target),
}
}
pub fn link_entity(&self, from: &Path, entity_ref: &EntityRef) -> String {
let Some(target) = self.entity_paths.get(entity_ref) else {
return format!("`{}`", entity_ref.name);
};
match self.layout {
Layout::Package => self.package_layout_link(from, target, &entity_ref.name),
Layout::Entity | Layout::Split => self.file_link(from, target),
}
}
pub fn link_type(&self, from: &Path, fqn: &str) -> String {
let Some((pkg, ident)) = split_proto_type_name(fqn) else {
return format!("`{fqn}`");
};
if self
.layout_entity_path(pkg, ProtobufEntityKind::Message, ident)
.is_some()
{
return self.link_from(from, pkg, ProtobufEntityKind::Message, ident);
}
if self
.layout_entity_path(pkg, ProtobufEntityKind::Enum, ident)
.is_some()
{
return self.link_from(from, pkg, ProtobufEntityKind::Enum, ident);
}
format!("`{fqn}`")
}
pub fn summary_link(&self, from: &Path, target: &Path, title: &str) -> String {
let from_dir = from.parent().unwrap_or(Path::new(""));
let rel = relative_path_from_dir(from_dir, target);
format!("[{title}]({rel})")
}
fn file_link(&self, from: &Path, target: &Path) -> String {
let from_dir = from.parent().unwrap_or(Path::new(""));
let rel = relative_path_from_dir(from_dir, target);
let label = target.file_stem().unwrap_or_default().to_string_lossy();
format!("[{label}]({rel})")
}
fn package_layout_link(&self, from: &Path, target: &Path, name: &str) -> String {
if from == target {
format!("[{name}](#{})", heading_slug(name))
} else {
let from_dir = from.parent().unwrap_or(Path::new(""));
let rel = relative_path_from_dir(from_dir, target);
format!("[{name}]({rel}#{})", heading_slug(name))
}
}
}
fn protobuf_entity_kind(entity: &StoredEntity) -> Option<ProtobufEntityKind> {
match &entity.body {
EntityBody::Service(_) => Some(ProtobufEntityKind::Service),
EntityBody::Operation(_) => None,
EntityBody::Schema(body) => {
if body.fence_body.starts_with("enum ") {
Some(ProtobufEntityKind::Enum)
} else {
Some(ProtobufEntityKind::Message)
}
}
_ => None,
}
}
fn split_proto_type_name(fqn: &str) -> Option<(&str, &str)> {
let fqn = fqn.strip_prefix('.').unwrap_or(fqn);
let (pkg, name) = fqn.rsplit_once('.')?;
if pkg.is_empty() || name.is_empty() {
return None;
}
Some((pkg, name))
}