1use crate::model::{CrossRef, CrossRefKind, RustItemRef};
7
8pub fn link_to_rust(rust_ref: &RustItemRef, from_python_path: &str) -> String {
31 let rust_page_path = rust_path_to_file_path(&rust_ref.path);
32 let anchor = item_to_anchor(&rust_ref.name, "struct");
33 let from_path = format!("python/{}", from_python_path.replace('.', "/"));
35 let relative_path = compute_relative_path(&from_path, &format!("rust/{}", rust_page_path));
36
37 format!("[{}]({}#{})", rust_ref.name, relative_path, anchor)
38}
39
40pub fn link_to_python(python_path: &str, from_rust_path: &str) -> String {
61 let (module_path, item_name) = split_python_path(python_path);
62 let python_page_path = python_path_to_file_path(&module_path);
63 let rust_page_path = rust_path_to_file_path(from_rust_path);
64 let relative_path =
66 compute_relative_path(&format!("rust/{}", rust_page_path), &python_page_path);
67 let anchor = item_to_anchor(&item_name, "class");
68
69 format!("[{}]({}#{})", item_name, relative_path, anchor)
70}
71
72pub fn crossref_link(xref: &CrossRef, from_path: &str, from_language: Language) -> CrossRefLink {
78 match from_language {
79 Language::Python => {
80 let (module_path, item_name) = split_rust_path(&xref.rust_path);
82 let rust_page = rust_path_to_file_path(&module_path);
83 let from_full = format!("python/{}", from_path.replace('.', "/"));
85 let relative = compute_relative_path(&from_full, &format!("rust/{}", rust_page));
86 let anchor = item_to_anchor(&item_name, "struct");
87
88 CrossRefLink {
89 text: item_name,
90 url: format!("{}#{}", relative, anchor),
91 relationship: xref.relationship.clone(),
92 }
93 }
94 Language::Rust => {
95 let (module_path, item_name) = split_python_path(&xref.python_path);
97 let python_page = python_path_to_file_path(&module_path);
98 let rust_page = rust_path_to_file_path(from_path);
99 let relative = compute_relative_path(&format!("rust/{}", rust_page), &python_page);
100 let anchor = item_to_anchor(&item_name, "class");
101
102 CrossRefLink {
103 text: item_name,
104 url: format!("{}#{}", relative, anchor),
105 relationship: xref.relationship.clone(),
106 }
107 }
108 }
109}
110
111#[derive(Debug, Clone)]
113pub struct CrossRefLink {
114 pub text: String,
116 pub url: String,
118 pub relationship: CrossRefKind,
120}
121
122impl CrossRefLink {
123 pub fn to_markdown(&self) -> String {
125 format!("[{}]({})", self.text, self.url)
126 }
127
128 pub fn to_markdown_with_badge(&self) -> String {
130 let indicator = match self.relationship {
131 CrossRefKind::Binding => "[binding]",
132 CrossRefKind::Wraps => "[wraps]",
133 CrossRefKind::Delegates => "[delegates]",
134 };
135 format!("{} [{}]({})", indicator, self.text, self.url)
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq)]
141pub enum Language {
142 Python,
143 Rust,
144}
145
146pub fn render_rust_impl_details(rust_ref: &RustItemRef, from_python_path: &str) -> String {
150 let link = link_to_rust(rust_ref, from_python_path);
151
152 format!(
153 "<details>\n\
154 <summary>Rust Implementation</summary>\n\n\
155 Implemented by {} in `{}`\n\n\
156 </details>",
157 link, rust_ref.path
158 )
159}
160
161pub fn render_python_exposure_details(python_path: &str, from_rust_path: &str) -> String {
165 let link = link_to_python(python_path, from_rust_path);
166
167 format!(
168 "<details>\n\
169 <summary>Python API</summary>\n\n\
170 Exposed as {} in `{}`\n\n\
171 </details>",
172 link, python_path
173 )
174}
175
176fn rust_path_to_file_path(rust_path: &str) -> String {
184 format!("{}.md", rust_path.replace("::", "/"))
185}
186
187fn python_path_to_file_path(python_path: &str) -> String {
191 format!("python/{}.md", python_path.replace('.', "/"))
192}
193
194fn split_python_path(path: &str) -> (String, String) {
198 if let Some(pos) = path.rfind('.') {
199 (path[..pos].to_string(), path[pos + 1..].to_string())
200 } else {
201 (path.to_string(), path.to_string())
202 }
203}
204
205fn split_rust_path(path: &str) -> (String, String) {
209 if let Some(pos) = path.rfind("::") {
210 (path[..pos].to_string(), path[pos + 2..].to_string())
211 } else {
212 (path.to_string(), path.to_string())
213 }
214}
215
216fn item_to_anchor(name: &str, item_type: &str) -> String {
220 format!("{}-{}", item_type, name.to_lowercase().replace(' ', "-"))
221}
222
223fn compute_relative_path(from_path: &str, to_path: &str) -> String {
227 let from_depth = from_path.matches('/').count();
229
230 let prefix = if from_depth > 0 {
232 "../".repeat(from_depth)
233 } else {
234 "./".to_string()
235 };
236
237 format!("{}{}", prefix, to_path)
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::model::RustItemRef;
244
245 #[test]
246 fn test_rust_path_to_file_path() {
247 assert_eq!(rust_path_to_file_path("crate::utils"), "crate/utils.md");
248 assert_eq!(
249 rust_path_to_file_path("crate::module::sub"),
250 "crate/module/sub.md"
251 );
252 }
253
254 #[test]
255 fn test_python_path_to_file_path() {
256 assert_eq!(
257 python_path_to_file_path("mypackage.utils"),
258 "python/mypackage/utils.md"
259 );
260 assert_eq!(
261 python_path_to_file_path("mypackage.sub.module"),
262 "python/mypackage/sub/module.md"
263 );
264 }
265
266 #[test]
267 fn test_split_python_path() {
268 let (module, item) = split_python_path("mypackage.utils.Config");
269 assert_eq!(module, "mypackage.utils");
270 assert_eq!(item, "Config");
271
272 let (module, item) = split_python_path("toplevel");
273 assert_eq!(module, "toplevel");
274 assert_eq!(item, "toplevel");
275 }
276
277 #[test]
278 fn test_split_rust_path() {
279 let (module, item) = split_rust_path("crate::utils::Config");
280 assert_eq!(module, "crate::utils");
281 assert_eq!(item, "Config");
282
283 let (module, item) = split_rust_path("single");
284 assert_eq!(module, "single");
285 assert_eq!(item, "single");
286 }
287
288 #[test]
289 fn test_item_to_anchor() {
290 assert_eq!(item_to_anchor("Config", "struct"), "struct-config");
291 assert_eq!(item_to_anchor("MyClass", "class"), "class-myclass");
292 assert_eq!(item_to_anchor("process_data", "fn"), "fn-process_data");
293 }
294
295 #[test]
296 fn test_compute_relative_path_same_level() {
297 let rel = compute_relative_path("module_a", "module_b.md");
298 assert_eq!(rel, "./module_b.md");
299 }
300
301 #[test]
302 fn test_compute_relative_path_one_level_deep() {
303 let rel = compute_relative_path("package/module", "rust/crate/utils.md");
304 assert_eq!(rel, "../rust/crate/utils.md");
305 }
306
307 #[test]
308 fn test_compute_relative_path_two_levels_deep() {
309 let rel = compute_relative_path("package/sub/module", "rust/crate/utils.md");
310 assert_eq!(rel, "../../rust/crate/utils.md");
311 }
312
313 #[test]
314 fn test_link_to_rust() {
315 let rust_ref = RustItemRef::new("crate::utils", "Config");
316 let link = link_to_rust(&rust_ref, "mypackage.config");
317
318 assert!(link.contains("[Config]"));
319 assert!(link.contains("rust/crate/utils.md"));
320 assert!(link.contains("#struct-config"));
321 }
322
323 #[test]
324 fn test_link_to_rust_nested_module() {
325 let rust_ref = RustItemRef::new("crate::module::sub", "Helper");
326 let link = link_to_rust(&rust_ref, "mypackage.helpers");
327
328 assert!(link.contains("[Helper]"));
329 assert!(link.contains("rust/crate/module/sub.md"));
330 assert!(link.contains("#struct-helper"));
331 }
332
333 #[test]
334 fn test_link_to_python() {
335 let link = link_to_python("mypackage.utils.Config", "crate::utils");
336
337 assert!(link.contains("[Config]"));
338 assert!(link.contains("mypackage/utils.md"));
339 assert!(link.contains("#class-config"));
340 }
341
342 #[test]
343 fn test_link_to_python_from_nested_rust() {
344 let link = link_to_python("mypackage.Config", "crate::module::sub");
345
346 assert!(link.contains("[Config]"));
347 assert!(link.contains("mypackage.md"));
348 assert!(link.contains("../"));
350 }
351
352 #[test]
353 fn test_crossref_link_from_python() {
354 let xref = CrossRef::binding("mypackage.Config", "crate::utils::Config");
355 let link = crossref_link(&xref, "mypackage", Language::Python);
356
357 assert_eq!(link.text, "Config");
358 assert!(link.url.contains("rust/crate/utils.md"));
359 assert!(matches!(link.relationship, CrossRefKind::Binding));
360 }
361
362 #[test]
363 fn test_crossref_link_from_rust() {
364 let xref = CrossRef::binding("mypackage.utils.Config", "crate::utils");
365 let link = crossref_link(&xref, "crate::utils", Language::Rust);
366
367 assert_eq!(link.text, "Config");
368 assert!(link.url.contains("mypackage/utils.md"));
369 assert!(matches!(link.relationship, CrossRefKind::Binding));
370 }
371
372 #[test]
373 fn test_crossref_link_markdown() {
374 let xref = CrossRef::binding("pkg.Class", "crate::Class");
375 let link = crossref_link(&xref, "pkg", Language::Python);
376
377 let md = link.to_markdown();
378 assert!(md.starts_with("[Class]("));
379 assert!(md.contains(".md#"));
380
381 let md_badge = link.to_markdown_with_badge();
382 assert!(md_badge.starts_with("[binding]"));
383 }
384
385 #[test]
386 fn test_crossref_link_wraps_relationship() {
387 let xref = CrossRef::wraps("pkg.Wrapper", "crate::Inner");
388 let link = crossref_link(&xref, "pkg", Language::Python);
389
390 assert!(matches!(link.relationship, CrossRefKind::Wraps));
391 let md_badge = link.to_markdown_with_badge();
392 assert!(md_badge.contains("[wraps]"));
393 }
394
395 #[test]
396 fn test_crossref_link_delegates_relationship() {
397 let xref = CrossRef::delegates("pkg.Client", "crate::http::Client");
398 let link = crossref_link(&xref, "pkg", Language::Python);
399
400 assert!(matches!(link.relationship, CrossRefKind::Delegates));
401 let md_badge = link.to_markdown_with_badge();
402 assert!(md_badge.contains("[delegates]"));
403 }
404
405 #[test]
406 fn test_render_rust_impl_details() {
407 let rust_ref = RustItemRef::new("crate::utils", "Config");
408 let details = render_rust_impl_details(&rust_ref, "mypackage.config");
409
410 assert!(details.contains("<details>"));
411 assert!(details.contains("Rust Implementation"));
412 assert!(details.contains("[Config]")); assert!(details.contains("rust/crate/utils.md"));
414 assert!(details.contains("</details>"));
415 }
416
417 #[test]
418 fn test_render_python_exposure_details() {
419 let details = render_python_exposure_details("mypackage.utils.Config", "crate::utils");
420
421 assert!(details.contains("<details>"));
422 assert!(details.contains("Python API"));
423 assert!(details.contains("[Config]")); assert!(details.contains("mypackage/utils.md"));
425 assert!(details.contains("</details>"));
426 }
427
428 #[test]
429 fn test_handles_missing_crossref_gracefully() {
430 let rust_ref = RustItemRef::new("crate::standalone", "Foo");
434 let link = link_to_rust(&rust_ref, "module");
435 assert!(!link.is_empty());
436 }
437}