1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
use mdbook::book::{Book, BookItem, Chapter};
use mdbook::errors::Error;
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use regex::Regex;
use std::collections::HashMap;
use std::path::PathBuf;
use toml::value::Value;

pub struct Backlinks;

impl Preprocessor for Backlinks {
    fn name(&self) -> &str {
        "backlinks"
    }

    fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
        // index maps each chapters source_path to its backlinks
        let mut index = create_backlink_map(&book);

        // Populate index backlinks
        for item in book.iter() {
            if let BookItem::Chapter(ch) = item {
                // Skip if source_path is None (because nothing to link to)
                if let Some(_) = &ch.source_path {
                    // Loop over the internal links found in the chapter
                    for link in find_links(&ch.content, &index) {
                        // Push current chapter into Vec corresponding to the chapter it links to
                        if let Some(backlinks) = index.get_mut(&link) {
                            backlinks.push(ch.clone());
                        }
                    }
                }
            }
        }

        // Should probably clean up this, but why bother?...
        let backlink_prefix = if let Some(header) = ctx.config.get("preprocessor.backlinks.header")
        {
            if let Value::String(val) = header {
                format!("\n\n---\n\n## {}\n\n", val)
            } else {
                eprintln!(
                    "Warning: You must use a string value as the backlink header. Skipping..."
                );
                String::from("\n\n---\n\n")
            }
        } else {
            String::from("\n\n")
        };

        // Add backlinks to each chapter
        book.for_each_mut(|item| {
            if let BookItem::Chapter(ch) = item {
                if let Some(source_path) = &ch.source_path {
                    if let Some(backlinks) = index.get(source_path) {
                        ch.content += &backlink_prefix;
                        for backlink in backlinks.iter() {
                            // This is really ugly, but the unwraps should be safe
                            let dest = backlink.source_path.clone().unwrap();
                            ch.content += &format!(
                                "> - [{}](</{}>)\n",
                                backlink.name,
                                dest.to_str()
                                    .unwrap()
                                    .replace(" ", "%20")
                                    .replace("<", "&lt;")
                                    .replace(">", "&gt;")
                            );
                        }
                    }
                }
            }
        });

        Ok(book)
    }
}

/// Finds all the links in content linking to chapters listed in index.
fn find_links(content: &str, index: &HashMap<PathBuf, Vec<Chapter>>) -> Vec<PathBuf> {
    let mut links: Vec<PathBuf> = Vec::new();
    let re =
        Regex::new(r"(?:\[[^\[]+?\])?(?:\[{2}|\()(.*?)(?:\.md)?(?:\|.*?)?(?:\]{2}|\))").unwrap();
    for cap in re.captures_iter(&content) {
        if let Some(dest) = cap.get(1) {
            let mut path = PathBuf::from(dest.as_str());
            path.set_extension("md");
            if index.contains_key(&path) {
                links.push(path);
            }
        }
    }
    links.sort_unstable();
    links.dedup();
    links
}

/// Creates an index that maps all the book's chapter's source_path to an empty vector of backlinks
fn create_backlink_map(book: &Book) -> HashMap<PathBuf, Vec<Chapter>> {
    let mut map = HashMap::new();
    for item in book.iter() {
        if let BookItem::Chapter(ch) = item {
            if let Some(source_path) = &ch.source_path {
                map.insert(source_path.clone(), Vec::new());
            }
        }
    }
    map
}