1use regex::Regex;
2use std::path::Path;
3
4use super::{Preprocessor, PreprocessorContext};
5use crate::book::{Book, BookItem};
6use crate::errors::*;
7use log::warn;
8use once_cell::sync::Lazy;
9
10/// A preprocessor for converting file name `README.md` to `index.md` since
11/// `README.md` is the de facto index file in markdown-based documentation.
12#[derive(Default)]
13pub struct IndexPreprocessor;
14
15impl IndexPreprocessor {
16 pub(crate) const NAME: &'static str = "index";
17
18 /// Create a new `IndexPreprocessor`.
19 pub fn new() -> Self {
20 IndexPreprocessor
21 }
22}
23
24impl Preprocessor for IndexPreprocessor {
25 fn name(&self) -> &str {
26 Self::NAME
27 }
28
29 fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
30 let source_dir = ctx.root.join(&ctx.config.book.src);
31 book.for_each_mut(|section: &mut BookItem| {
32 if let BookItem::Chapter(ref mut ch) = *section {
33 if let Some(ref mut path) = ch.path {
34 if is_readme_file(&path) {
35 let mut index_md = source_dir.join(path.with_file_name("index.md"));
36 if index_md.exists() {
37 warn_readme_name_conflict(&path, &&mut index_md);
38 }
39
40 path.set_file_name("index.md");
41 }
42 }
43 }
44 });
45
46 Ok(book)
47 }
48}
49
50fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
51 let file_name: &OsStr = readme_path.as_ref().file_name().unwrap_or_default();
52 let parent_dir: &Path = index_pathOption<&Path>
53 .as_ref()
54 .parent()
55 .unwrap_or_else(|| index_path.as_ref());
56 warn!(
57 "It seems that there are both {:?} and index.md under \"{}\".",
58 file_name,
59 parent_dir.display()
60 );
61 warn!(
62 "mdbook converts {:?} into index.html by default. It may cause",
63 file_name
64 );
65 warn!("unexpected behavior if putting both files under the same directory.");
66 warn!("To solve the warning, try to rearrange the book structure or disable");
67 warn!("\"index\" preprocessor to stop the conversion.");
68}
69
70fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
71 static RE: Lazy<Regex> = Lazy::new(|| Regex::new(re:r"(?i)^readme$").unwrap());
72
73 RE.is_match(
74 text:path.as_ref()
75 .file_stem()
76 .and_then(std::ffi::OsStr::to_str)
77 .unwrap_or_default(),
78 )
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn file_stem_exactly_matches_readme_case_insensitively() {
87 let path = "path/to/Readme.md";
88 assert!(is_readme_file(path));
89
90 let path = "path/to/README.md";
91 assert!(is_readme_file(path));
92
93 let path = "path/to/rEaDmE.md";
94 assert!(is_readme_file(path));
95
96 let path = "path/to/README.markdown";
97 assert!(is_readme_file(path));
98
99 let path = "path/to/README";
100 assert!(is_readme_file(path));
101
102 let path = "path/to/README-README.md";
103 assert!(!is_readme_file(path));
104 }
105}
106