mdbook_driver/builtin_preprocessors/
index.rs1use 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#[derive(Default)]
11#[non_exhaustive]
12pub struct IndexPreprocessor;
13
14impl IndexPreprocessor {
15 pub const NAME: &'static str = "index";
17
18 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}