llmwiki_tooling/cmd/
links.rs1use 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
9pub 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
41pub 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 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 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
92pub 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
121pub 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}