mdbook_driver/builtin_preprocessors/
index.rs

1use anyhow::Result;
2use mdbook_core::book::{Book, BookItem};
3use mdbook_core::static_regex;
4use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
5use std::path::Path;
6use tracing::warn;
7
8/// A preprocessor for converting file name `README.md` to `index.md` since
9/// `README.md` is the de facto index file in markdown-based documentation.
10#[derive(Default)]
11#[non_exhaustive]
12pub struct IndexPreprocessor;
13
14impl IndexPreprocessor {
15    /// Name of this preprocessor.
16    pub 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 = readme_path.as_ref().file_name().unwrap_or_default();
52    let parent_dir = index_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_regex!(README, r"(?i)^readme$");
72
73    README.is_match(
74        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}