mdbook_autosummary/
lib.rs1use std::{path::Path, process};
2
3use config::Config;
4use log::{debug, error};
5use mdbook::book::{Book, load_book};
6use mdbook::errors::Error;
7use mdbook::preprocess::{Preprocessor, PreprocessorContext};
8use sha2::{Digest, Sha256};
9
10use crate::parser::DocStructure;
11
12pub mod config;
13pub mod parser;
14
15const VERSION: &str = env!("CARGO_PKG_VERSION");
16
17pub struct AutoSummary;
19
20impl AutoSummary {
21 pub fn new() -> Self {
22 Self
23 }
24
25 fn hash_summary(&self, src: &Path) -> Option<Vec<u8>> {
27 let summary_path = src.join("SUMMARY.md");
28
29 if !summary_path.is_file() {
30 return None;
31 }
32 let mut hasher = Sha256::new();
33 let lines = std::fs::read_to_string(&summary_path).unwrap();
34 hasher.update(lines);
35
36 Some(hasher.finalize().to_vec())
37 }
38
39 fn gen_summary(&self, src: &Path, config: &Config) -> String {
41 let Some(mut doc) = DocStructure::from_src(src, config) else {
42 error!(
43 "Could not find an '{1}' file at '{0}'\nAn '{1}' file must exist at '{0}' when using the autosummary preprocessor!",
44 src.display(),
45 config.index_name
46 );
47 process::exit(1);
48 };
49 doc.relative_to(src)
50 .expect("Failed to turn absolute paths into relative");
51
52 let mut generated = doc.to_string().trim_end().to_string();
53 generated.push('\n');
54 generated.insert_str(
55 0,
56 &format!(
57 "<!-- Generated by mdbook-autosummary v{} - do not edit manually! -->\n\n",
58 VERSION
59 ),
60 );
61 generated
62 }
63}
64
65impl Preprocessor for AutoSummary {
66 fn name(&self) -> &str {
67 "autosummary"
68 }
69
70 fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book, Error> {
71 let src_path = ctx.root.join(&ctx.config.book.src);
72 let config = Config::from_mdbook(&ctx.config);
73
74 let generated = self.gen_summary(src_path.as_path(), &config);
75
76 let mut hasher = Sha256::new();
77 hasher.update(&generated);
78
79 let gen_hash: Vec<u8> = hasher.finalize().to_vec();
80 let existing_hash = self.hash_summary(src_path.as_path());
81
82 if existing_hash.is_some() && existing_hash.unwrap() == gen_hash {
83 debug!("Generated SUMMARY.md matches existing SUMMARY.md, skipping generation");
84 return Ok(book);
85 } else {
86 std::fs::write(src_path.join("SUMMARY.md"), generated)?;
87 }
88 let mut conf = ctx.config.build.clone();
89 conf.create_missing = false;
90
91 load_book(src_path, &conf)
92 }
93}
94
95impl Default for AutoSummary {
96 fn default() -> Self {
97 Self::new()
98 }
99}