1use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6use crate::EntityRef;
7use crate::layout_paths::{
8 LayoutEntityKey, ProtobufEntityKind, heading_slug, layout_entity_rel_path, package_index_rel,
9 package_page_rel, relative_path_from_dir,
10};
11use crate::options::{Layout, Options};
12use crate::paths::{entity_category_dir, entity_rel_path};
13use crate::{EntityBody, ReferenceManual, StoredEntity};
14
15#[derive(Clone, Debug, Default, PartialEq, Eq)]
17pub struct LinkContext {
18 pub layout: Layout,
20 pub book_root: String,
22 pub markdown_root: String,
24 pub entity_paths: HashMap<EntityRef, PathBuf>,
26 layout_entities: HashMap<LayoutEntityKey, PathBuf>,
28 pub render_from: Option<PathBuf>,
30}
31
32impl LinkContext {
33 pub fn empty(
35 layout: Layout,
36 book_root: impl Into<String>,
37 markdown_root: impl Into<String>,
38 ) -> Self {
39 Self {
40 layout,
41 book_root: book_root.into(),
42 markdown_root: markdown_root.into(),
43 entity_paths: HashMap::new(),
44 layout_entities: HashMap::new(),
45 render_from: None,
46 }
47 }
48
49 pub fn from_manual(manual: &ReferenceManual, opts: &Options) -> Self {
51 let mut ctx = Self::empty(opts.layout, &opts.book_root, &opts.markdown_root);
52 for module in &manual.modules {
53 for contract in &module.contracts {
54 let is_protobuf = contract.family == "protobuf";
55 for group in &contract.groups {
56 if group.id.as_str().is_empty() {
57 continue;
58 }
59 for entity in &group.entities {
60 ctx.register_stored_entity(
61 module.id.as_str(),
62 group.id.as_str(),
63 &group.dir,
64 entity,
65 is_protobuf,
66 opts.layout,
67 &opts.markdown_root,
68 );
69 }
70 }
71 }
72 }
73 ctx
74 }
75
76 #[allow(clippy::too_many_arguments)]
78 pub fn register_stored_entity(
79 &mut self,
80 module: &str,
81 group: &str,
82 group_dir: &str,
83 entity: &StoredEntity,
84 is_protobuf: bool,
85 layout: Layout,
86 markdown_root: &str,
87 ) {
88 let entity_ref = EntityRef {
89 module: module.to_string(),
90 group: group.to_string(),
91 category: entity.category.clone(),
92 name: entity.name.clone(),
93 };
94
95 if is_protobuf {
96 if let Some(kind) = protobuf_entity_kind(entity) {
97 let key = LayoutEntityKey {
98 package: group.to_string(),
99 kind,
100 name: entity.name.clone(),
101 };
102 let path = layout_entity_rel_path(layout, markdown_root, &key);
103 self.layout_entities.insert(key, path.clone());
104 self.entity_paths.insert(entity_ref, path);
105 }
106 return;
107 }
108
109 let rel = match layout {
110 Layout::Package => package_page_rel(markdown_root, group),
111 Layout::Entity | Layout::Split => {
112 let rel_path = entity_rel_path(
113 group_dir,
114 entity_category_dir(&entity.category),
115 &entity.name,
116 );
117 PathBuf::from(format!("{markdown_root}/{rel_path}"))
118 }
119 };
120 self.entity_paths.insert(entity_ref, rel);
121 }
122
123 pub fn layout_entity_keys(&self) -> impl Iterator<Item = &LayoutEntityKey> {
125 self.layout_entities.keys()
126 }
127
128 pub fn package_page_rel(&self, package: &str) -> PathBuf {
130 package_page_rel(&self.markdown_root, package)
131 }
132
133 pub fn package_index_rel(&self, package: &str) -> PathBuf {
135 package_index_rel(self.layout, &self.markdown_root, package)
136 }
137
138 pub fn layout_entity_path(
140 &self,
141 package: &str,
142 kind: ProtobufEntityKind,
143 name: &str,
144 ) -> Option<&PathBuf> {
145 self.layout_entities.get(&LayoutEntityKey {
146 package: package.to_string(),
147 kind,
148 name: name.to_string(),
149 })
150 }
151
152 pub fn entity_path(&self, entity_ref: &EntityRef) -> Option<&PathBuf> {
154 self.entity_paths.get(entity_ref)
155 }
156
157 pub fn link_from(
159 &self,
160 from: &Path,
161 package: &str,
162 kind: ProtobufEntityKind,
163 name: &str,
164 ) -> String {
165 let Some(target) = self.layout_entity_path(package, kind, name) else {
166 return format!("`.{package}.{name}`");
167 };
168 match self.layout {
169 Layout::Package => self.package_layout_link(from, target, name),
170 Layout::Entity | Layout::Split => self.file_link(from, target),
171 }
172 }
173
174 pub fn link_entity(&self, from: &Path, entity_ref: &EntityRef) -> String {
176 let Some(target) = self.entity_paths.get(entity_ref) else {
177 return format!("`{}`", entity_ref.name);
178 };
179 match self.layout {
180 Layout::Package => self.package_layout_link(from, target, &entity_ref.name),
181 Layout::Entity | Layout::Split => self.file_link(from, target),
182 }
183 }
184
185 pub fn link_type(&self, from: &Path, fqn: &str) -> String {
187 let Some((pkg, ident)) = split_proto_type_name(fqn) else {
188 return format!("`{fqn}`");
189 };
190 if self
191 .layout_entity_path(pkg, ProtobufEntityKind::Message, ident)
192 .is_some()
193 {
194 return self.link_from(from, pkg, ProtobufEntityKind::Message, ident);
195 }
196 if self
197 .layout_entity_path(pkg, ProtobufEntityKind::Enum, ident)
198 .is_some()
199 {
200 return self.link_from(from, pkg, ProtobufEntityKind::Enum, ident);
201 }
202 format!("`{fqn}`")
203 }
204
205 pub fn summary_link(&self, from: &Path, target: &Path, title: &str) -> String {
207 let from_dir = from.parent().unwrap_or(Path::new(""));
208 let rel = relative_path_from_dir(from_dir, target);
209 format!("[{title}]({rel})")
210 }
211
212 fn file_link(&self, from: &Path, target: &Path) -> String {
213 let from_dir = from.parent().unwrap_or(Path::new(""));
214 let rel = relative_path_from_dir(from_dir, target);
215 let label = target.file_stem().unwrap_or_default().to_string_lossy();
216 format!("[{label}]({rel})")
217 }
218
219 fn package_layout_link(&self, from: &Path, target: &Path, name: &str) -> String {
220 if from == target {
221 format!("[{name}](#{})", heading_slug(name))
222 } else {
223 let from_dir = from.parent().unwrap_or(Path::new(""));
224 let rel = relative_path_from_dir(from_dir, target);
225 format!("[{name}]({rel}#{})", heading_slug(name))
226 }
227 }
228}
229
230fn protobuf_entity_kind(entity: &StoredEntity) -> Option<ProtobufEntityKind> {
231 match &entity.body {
232 EntityBody::Service(_) => Some(ProtobufEntityKind::Service),
233 EntityBody::Operation(_) => None,
234 EntityBody::Schema(body) => {
235 if body.fence_body.starts_with("enum ") {
236 Some(ProtobufEntityKind::Enum)
237 } else {
238 Some(ProtobufEntityKind::Message)
239 }
240 }
241 _ => None,
242 }
243}
244
245fn split_proto_type_name(fqn: &str) -> Option<(&str, &str)> {
246 let fqn = fqn.strip_prefix('.').unwrap_or(fqn);
247 let (pkg, name) = fqn.rsplit_once('.')?;
248 if pkg.is_empty() || name.is_empty() {
249 return None;
250 }
251 Some((pkg, name))
252}