1use lazy_regex::{lazy_regex, Lazy};
2use mdbook::book::{Book, BookItem, Chapter};
3use mdbook::errors::Error;
4use mdbook::preprocess::{Preprocessor, PreprocessorContext};
5use pathdiff;
6use regex::Regex;
7use std::collections::HashMap;
8use std::path::PathBuf;
9use toml::value::Value;
10
11static WIKILINK_REGEX: Lazy<Regex> =
12 lazy_regex!(r"\[\[(?P<link>[^\]\|]+)(?:\|(?P<title>[^\]]+))?\]\]");
13
14pub struct Backlinks;
15
16impl Preprocessor for Backlinks {
17 fn name(&self) -> &str {
18 "backlinks"
19 }
20
21 fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
22 let mut index = create_backlink_map(&book);
24
25 for item in book.iter() {
27 if let BookItem::Chapter(ch) = item {
28 if let Some(_) = &ch.source_path {
30 for link in find_links(&ch.content, &index) {
32 if let Some(backlinks) = index.get_mut(&link) {
34 backlinks.push(ch.clone());
35 }
36 }
37 }
38 }
39 }
40
41 let backlink_prefix = if let Some(header) = ctx.config.get("mdzk.backlinks-header")
43 {
44 if let Value::String(val) = header {
45 format!("\n\n---\n\n## {}\n\n", val)
46 } else {
47 eprintln!(
48 "Warning: You must use a string value as the backlink header. Skipping..."
49 );
50 String::from("\n\n---\n\n")
51 }
52 } else {
53 String::from("\n\n")
54 };
55
56 book.for_each_mut(|item| {
58 if let BookItem::Chapter(ch) = item {
59 if let Some(source_path) = &ch.source_path {
60 if let Some(backlinks) = index.get(source_path) {
61 if backlinks.len() >= 1 {
62 ch.content += &backlink_prefix;
63 };
64
65 for backlink in backlinks.iter() {
66 let dest = backlink.source_path.clone().unwrap();
68 let diff_path =
69 pathdiff::diff_paths(dest, source_path.parent().unwrap()).unwrap();
70 ch.content += &format!(
71 "> - [{}](<{}>)\n",
72 backlink.name,
73 escape_special_chars(diff_path.to_str().unwrap())
74 );
75 }
76 }
77 }
78 }
79 });
80
81 Ok(book)
82 }
83}
84
85fn find_links(content: &str, index: &HashMap<PathBuf, Vec<Chapter>>) -> Vec<PathBuf> {
87 let mut links: Vec<PathBuf> = Vec::new();
88
89 for cap in WIKILINK_REGEX.captures_iter(&content) {
90 if let Some(dest) = cap.get(1) {
91 let mut path = PathBuf::from(dest.as_str());
92 path.set_extension("md");
93 if index.contains_key(&path) {
94 links.push(path);
95 }
96 }
97 }
98 links.sort_unstable();
99 links.dedup();
100 links
101}
102
103fn create_backlink_map(book: &Book) -> HashMap<PathBuf, Vec<Chapter>> {
105 let mut map = HashMap::new();
106 for item in book.iter() {
107 if let BookItem::Chapter(ch) = item {
108 if let Some(source_path) = &ch.source_path {
109 map.insert(source_path.clone(), Vec::new());
110 }
111 }
112 }
113 map
114}
115
116fn escape_special_chars(text: &str) -> String {
117 text.replace(" ", "%20")
118 .replace("<", "%3C")
119 .replace(">", "%3E")
120 .replace('?', "%3F")
121}