llmwiki_tooling/
resolve.rs1use crate::error::WikiError;
2use crate::page::{WikilinkFragment, WikilinkOccurrence};
3use crate::wiki::Wiki;
4
5#[derive(Debug)]
7pub enum BrokenReason {
8 PageNotFound,
9 HeadingNotFound { heading: String },
10 BlockNotFound { block_id: String },
11}
12
13impl std::fmt::Display for BrokenReason {
14 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15 match self {
16 Self::PageNotFound => write!(f, "page not found"),
17 Self::HeadingNotFound { heading } => write!(f, "heading not found: '{heading}'"),
18 Self::BlockNotFound { block_id } => write!(f, "block not found: '^{block_id}'"),
19 }
20 }
21}
22
23pub fn resolve_wikilink(wikilink: &WikilinkOccurrence, wiki: &Wiki) -> Result<(), ResolveError> {
25 let (_, entry) = wiki
26 .find(wikilink.page.as_str())
27 .ok_or(ResolveError::Broken(BrokenReason::PageNotFound))?;
28
29 if let Some(fragment) = &wikilink.fragment {
30 let target_path = wiki.root().path().join(&entry.rel_path);
31
32 match fragment {
33 WikilinkFragment::Heading(heading) => {
34 let headings = wiki.headings(&target_path).map_err(ResolveError::Wiki)?;
35 let found = headings
36 .iter()
37 .any(|h| h.text.eq_ignore_ascii_case(heading));
38 if !found {
39 return Err(ResolveError::Broken(BrokenReason::HeadingNotFound {
40 heading: heading.clone(),
41 }));
42 }
43 }
44 WikilinkFragment::Block(block_id) => {
45 let block_ids = wiki.block_ids(&target_path).map_err(ResolveError::Wiki)?;
46 let found = block_ids.iter().any(|b| b.as_str() == block_id.as_str());
47 if !found {
48 return Err(ResolveError::Broken(BrokenReason::BlockNotFound {
49 block_id: block_id.as_str().to_owned(),
50 }));
51 }
52 }
53 }
54 }
55
56 Ok(())
57}
58
59#[derive(Debug, thiserror::Error)]
60pub enum ResolveError {
61 #[error("{0}")]
62 Broken(BrokenReason),
63 #[error(transparent)]
64 Wiki(WikiError),
65}
66
67pub fn find_broken_links(
69 wiki: &Wiki,
70 file_path: &std::path::Path,
71) -> Result<Vec<(WikilinkOccurrence, BrokenReason)>, WikiError> {
72 let wikilinks = wiki.wikilinks(file_path)?;
73 let mut broken = Vec::new();
74
75 for wl in wikilinks {
76 if wl.page.as_str().is_empty() {
77 continue;
78 }
79 match resolve_wikilink(wl, wiki) {
80 Ok(()) => {}
81 Err(ResolveError::Broken(reason)) => broken.push((wl.clone(), reason)),
82 Err(ResolveError::Wiki(e)) => return Err(e),
83 }
84 }
85
86 Ok(broken)
87}