Skip to main content

mdbook_autosummary/
lib.rs

1use 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
17/// A no-op preprocessor.
18pub struct AutoSummary;
19
20impl AutoSummary {
21    pub fn new() -> Self {
22        Self
23    }
24
25    /// Create an sha256 hash of the existing src/SUMMARY.md file, if one exists
26    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    /// Generate a new summary based on the file structure
40    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}