mdbook_open_git_repo/
lib.rs1use 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}