Skip to main content

llmwiki_tooling/cmd/
links.rs

1use crate::error::WikiError;
2use crate::link_index::LinkIndex;
3use crate::mention::ConceptMatcher;
4use crate::page::PageId;
5use crate::resolve;
6use crate::splice;
7use crate::wiki::Wiki;
8
9/// Run `links check`: find bare mentions that should be wikilinks.
10pub fn check(wiki: &Wiki) -> Result<usize, WikiError> {
11    let matcher = ConceptMatcher::new(wiki.autolink_pages()?);
12    let mut total_mentions = 0;
13
14    for file_path in wiki.all_scannable_files() {
15        let source = wiki.source(&file_path)?;
16        let classified = wiki.classified_ranges(&file_path)?;
17
18        let self_page = PageId::from_path(&file_path).unwrap_or_else(|| PageId::from(""));
19        let mentions = matcher.find_bare_mentions(source, classified, &self_page);
20
21        let rel_path = wiki.rel_path(&file_path);
22
23        for m in &mentions {
24            let display = wiki.display_name(&m.concept).unwrap_or(m.concept.as_str());
25            println!(
26                "{}:{}:{}: bare mention \"{}\" (should be [[{}]])",
27                rel_path.display(),
28                m.line,
29                m.col,
30                display,
31                display,
32            );
33        }
34
35        total_mentions += mentions.len();
36    }
37
38    Ok(total_mentions)
39}
40
41/// Run `links fix`: auto-link bare mentions.
42pub fn fix(wiki: &mut Wiki, write: bool) -> Result<usize, WikiError> {
43    let matcher = ConceptMatcher::new(wiki.autolink_pages()?);
44    let mut total_fixes = 0;
45
46    // Collect all changes first (read phase)
47    let mut changes: super::FileEdits = Vec::new();
48
49    for file_path in wiki.all_scannable_files() {
50        let source = wiki.source(&file_path)?;
51        let classified = wiki.classified_ranges(&file_path)?;
52
53        let self_page = PageId::from_path(&file_path).unwrap_or_else(|| PageId::from(""));
54        let mentions = matcher.find_bare_mentions(source, classified, &self_page);
55
56        if mentions.is_empty() {
57            continue;
58        }
59
60        let edits: Vec<_> = mentions
61            .iter()
62            .map(|m| {
63                let display = wiki.display_name(&m.concept).unwrap_or(m.concept.as_str());
64                (m.byte_range.clone(), format!("[[{}]]", display))
65            })
66            .collect();
67
68        changes.push((file_path, source.to_owned(), edits));
69        total_fixes += mentions.len();
70    }
71
72    // Apply changes (write phase)
73    for (file_path, source, edits) in changes {
74        let rel_path = wiki.rel_path(&file_path);
75
76        if write {
77            let result = splice::apply(&source, &edits);
78            wiki.write_file(&file_path, &result)?;
79            println!(
80                "{}: fixed {} bare mention(s)",
81                rel_path.display(),
82                edits.len()
83            );
84        } else {
85            print!("{}", splice::diff(&source, rel_path, &edits));
86        }
87    }
88
89    Ok(total_fixes)
90}
91
92/// Run `links broken`: find broken wikilinks.
93pub fn broken(wiki: &Wiki) -> Result<usize, WikiError> {
94    let mut total_broken = 0;
95
96    for file_path in wiki.all_scannable_files() {
97        let broken_links = resolve::find_broken_links(wiki, &file_path)?;
98        let source = wiki.source(&file_path)?;
99
100        let rel_path = wiki.rel_path(&file_path);
101
102        for (wl, reason) in &broken_links {
103            let (line, col) = splice::offset_to_line_col(source, wl.byte_range.start);
104            let ref_text = &source[wl.byte_range.clone()];
105            println!(
106                "{}:{}:{}: broken link {}: {}",
107                rel_path.display(),
108                line,
109                col,
110                ref_text.trim(),
111                reason,
112            );
113        }
114
115        total_broken += broken_links.len();
116    }
117
118    Ok(total_broken)
119}
120
121/// Run `links orphans`: find pages with no inbound wikilinks.
122pub fn orphans(wiki: &Wiki) -> Result<usize, WikiError> {
123    let index = LinkIndex::build(wiki)?;
124    let orphan_pages = index.orphans(wiki);
125
126    for page_id in &orphan_pages {
127        if let Some(entry) = wiki.get(page_id) {
128            println!(
129                "{}: orphan page (no inbound links)",
130                entry.rel_path.display()
131            );
132        }
133    }
134
135    Ok(orphan_pages.len())
136}