mdbook_open_git_repo/
lib.rs

1use std::path::{Path, PathBuf};
2
3use mdbook::book::{Book, BookItem, Chapter};
4use mdbook::errors::Result;
5use mdbook::preprocess::{Preprocessor, PreprocessorContext};
6
7#[derive(Debug)]
8enum SourceControlHost {
9    GitHub,
10    GitLab,
11}
12
13static DEFAULT_LINK_TEXT: &str = "Edit this file on GitHub.";
14static DEFAULT_EDIT_TEXT: &str = "Found a bug? ";
15
16pub struct OpenOn;
17
18impl Preprocessor for OpenOn {
19    fn name(&self) -> &str {
20        "open-git-repo"
21    }
22
23    fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
24        let book_root = &ctx.root;
25        let src_root = book_root.join(&ctx.config.book.src);
26        let git_root = find_git(book_root).unwrap();
27        log::debug!("Book root: {}", book_root.display());
28        log::debug!("Src root: {}", src_root.display());
29        log::debug!("Git root: {}", git_root.display());
30
31        let repository_url = match ctx.config.get("output.html.git-repository-url") {
32            None => return Ok(book),
33            Some(url) => url,
34        };
35        let repository_url = match repository_url {
36            toml::Value::String(s) => s,
37            _ => return Ok(book),
38        };
39        log::debug!("Repository URL: {}", repository_url);
40
41        let preprocessor_config = ctx.config.get_preprocessor(self.name());
42
43        let source_control_host: SourceControlHost = match preprocessor_config {
44            None => match repo_url_to_host(repository_url) {
45                Some(host) => host,
46                None => {
47                    eprintln!("Failed to determine source control host from URL. Please specify the host in your configuration");
48                    return Ok(book);
49                }
50            },
51            Some(preprocessor_config) => {
52                if let Some(toml::Value::String(host_string)) =
53                    preprocessor_config.get("source-control-host")
54                {
55                    match host_string.as_ref() {
56                        "github" => SourceControlHost::GitHub,
57                        "gitlab" => SourceControlHost::GitLab,
58                        _ => {
59                            eprintln!("Invalid source control host. Please consult configuration guide for valid values");
60                            return Ok(book);
61                        }
62                    }
63                } else {
64                    match repo_url_to_host(repository_url) {
65                        Some(host) => host,
66                        None => {
67                            eprintln!("Failed to determine source control host from URL. Please specify the host in your configuration");
68                            return Ok(book);
69                        }
70                    }
71                }
72            }
73        };
74        log::debug!("Source Control Host: {:?}", source_control_host);
75
76        let link_text = match preprocessor_config {
77            None => DEFAULT_LINK_TEXT,
78            Some(preprocessor_config) => {
79                if let Some(toml::Value::String(link_text)) = preprocessor_config.get("link-text") {
80                    link_text
81                } else {
82                    DEFAULT_LINK_TEXT
83                }
84            }
85        };
86        log::debug!("Link Text: {}", link_text);
87
88        let edit_text = match preprocessor_config {
89            None => DEFAULT_EDIT_TEXT,
90            Some(preprocessor_config) => {
91                if let Some(toml::Value::String(edit_text)) = preprocessor_config.get("edit-text") {
92                    edit_text
93                } else {
94                    DEFAULT_EDIT_TEXT
95                }
96            }
97        };
98        log::debug!("Edit Text: {}", edit_text);
99
100        let branch = match ctx.config.get("output.html.git-branch") {
101            None => "master",
102            Some(toml::Value::String(b)) => b,
103            _ => return Ok(book),
104        };
105        log::debug!("Git Branch: {}", branch);
106
107        let mut res = None;
108        book.for_each_mut(|item: &mut BookItem| {
109            if let Some(Err(_)) = res {
110                return;
111            }
112
113            if let BookItem::Chapter(ref mut chapter) = *item {
114                res = Some(
115                    open_on(
116                        &git_root,
117                        &src_root,
118                        &repository_url,
119                        &branch,
120                        link_text,
121                        edit_text,
122                        &source_control_host,
123                        chapter,
124                    )
125                    .map(|md| {
126                        chapter.content = md;
127                    }),
128                );
129            }
130        });
131
132        res.unwrap_or(Ok(())).map(|_| book)
133    }
134}
135
136fn repo_url_to_host(repository_url: &str) -> Option<SourceControlHost> {
137    if repository_url.find("github.com").is_some() {
138        Some(SourceControlHost::GitHub)
139    } else if repository_url.find("gitlab.com").is_some() {
140        Some(SourceControlHost::GitLab)
141    } else {
142        None
143    }
144}
145
146fn open_on(
147    git_root: &Path,
148    src_root: &Path,
149    base_url: &str,
150    branch: &str,
151    link_text: &str,
152    edit_text: &str,
153    source_control_host: &SourceControlHost,
154    chapter: &mut Chapter,
155) -> Result<String> {
156    let content = &chapter.content;
157
158    let footer_start = "<footer id=\"open-git-repo\">";
159    if content.contains(footer_start) {
160        return Ok(content.into());
161    }
162
163    let path = match chapter.path.as_ref() {
164        None => return Ok("".into()),
165        Some(path) => path,
166    };
167    let path = match src_root.join(&path).canonicalize() {
168        Ok(path) => path,
169        Err(_) => return Ok(content.into()),
170    };
171    let relpath = path.strip_prefix(git_root).unwrap();
172    log::trace!("Chapter path: {}", path.display());
173    log::trace!("Relative path: {}", relpath.display());
174
175    let edit_fragment = host_to_edit_uri_fragment(source_control_host);
176    let url = format!(
177        "{}/{}/{}/{}",
178        base_url,
179        edit_fragment,
180        branch,
181        relpath.display()
182    );
183    log::trace!("URL: {}", url);
184    let link = format!("<a href=\"{}\">{}</a>", url, link_text);
185    let content = format!(
186        "{}\n{}{}{}</footer>",
187        content, footer_start, edit_text, link
188    );
189
190    Ok(content)
191}
192
193fn host_to_edit_uri_fragment(source_control_host: &SourceControlHost) -> &str {
194    match source_control_host {
195        SourceControlHost::GitHub => "edit",
196        SourceControlHost::GitLab => "-/edit",
197    }
198}
199
200fn find_git(path: &Path) -> Option<PathBuf> {
201    let mut current_path = path;
202    let mut git_dir = current_path.join(".git");
203    let root = Path::new("/");
204
205    while !git_dir.exists() {
206        current_path = match current_path.parent() {
207            Some(p) => p,
208            None => return None,
209        };
210
211        if current_path == root {
212            return None;
213        }
214
215        git_dir = current_path.join(".git");
216    }
217
218    git_dir.parent().map(|p| p.to_owned())
219}