Skip to main content

microcad_docgen/mdbook/
mod.rs

1// Copyright © 2026 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Generate a markdown book from a symbol tree.
5
6use std::io::Write;
7
8use microcad_builtin::Symbol;
9use microcad_lang::{builtin::Builtin, symbol::SymbolDef};
10
11use crate::{DocGen, md::ToMd};
12
13/// Mdbook generator.
14///
15/// Read mdbook documentation: https://rust-lang.github.io/mdBook/
16pub struct MdBook {
17    /// Output path.
18    pub path: std::path::PathBuf,
19}
20
21impl MdBook {
22    pub fn new(path: impl AsRef<std::path::Path>) -> Self {
23        Self {
24            path: path.as_ref().to_path_buf(),
25        }
26    }
27
28    /// Because this function is tested and imports a built-in file, it has intentionally no error handling.
29    fn generate_book_toml_string(&self) -> String {
30        let book_toml: toml::Value =
31            toml::de::from_str(include_str!("book.toml")).expect("Valid toml");
32        let str = toml::ser::to_string(&book_toml).expect("No error");
33        format!(
34            r#"# Copyright © 2026 The µcad authors <info@ucad.xyz>
35# SPDX-License-Identifier: AGPL-3.0-or-later
36#
37# NOTE: Auto-generated code. 
38# This markdown book has been generated from µcad source via `microcad-docgen`.
39# Changes in the book might be overwritten.
40{str}
41"#
42        )
43    }
44
45    /// Generate the toml file for the book
46    fn write_book_toml(&self) -> std::io::Result<()> {
47        let mut file = std::fs::File::create(self.path.join("book.toml"))?;
48        file.write_all(self.generate_book_toml_string().as_bytes())?;
49        Ok(())
50    }
51
52    /// Return the path for a symbol.
53    ///
54    /// For example `std::geo2d::Circle` returns `geo2d/Circle.md`.
55    fn symbol_path(symbol: &Symbol) -> std::path::PathBuf {
56        let path: std::path::PathBuf = symbol
57            .full_name()
58            .iter()
59            .skip(1)
60            .map(|id| id.to_string())
61            .collect();
62        symbol.with_def(|def| match def {
63            SymbolDef::SourceFile(..) | SymbolDef::Module(..) => path.join("README.md"),
64            _ => {
65                let mut path = path.clone();
66                path.set_extension("md");
67                path
68            }
69        })
70    }
71
72    fn _generate_summary(
73        &self,
74        writer: &mut impl std::fmt::Write,
75        symbol: &Symbol,
76        depth: usize,
77    ) -> std::fmt::Result {
78        fn entry(
79            writer: &mut impl std::fmt::Write,
80            id: impl std::fmt::Display,
81            path: impl AsRef<std::path::Path>,
82            depth: usize,
83        ) -> std::fmt::Result {
84            writeln!(
85                writer,
86                "{:indent$}- [`{id}`]({path})",
87                "",
88                indent = 2 * depth,
89                path = path.as_ref().display()
90            )
91        }
92
93        fn recurse<'a>(
94            self_: &MdBook,
95            writer: &mut impl std::fmt::Write,
96            symbols: impl IntoIterator<Item = &'a Symbol>,
97            depth: usize,
98        ) -> std::fmt::Result {
99            symbols
100                .into_iter()
101                .try_for_each(|symbol| self_._generate_summary(writer, symbol, depth))
102        }
103
104        let path = Self::symbol_path(symbol);
105
106        entry(writer, symbol.id(), path, depth)?;
107        let depth = depth + 1;
108
109        let children: Vec<_> = symbol.iter().filter(|symbol| symbol.is_public()).collect();
110
111        let modules: Vec<_> = children
112            .iter()
113            .filter(|symbol| {
114                symbol.with_def(|def| {
115                    matches!(def, SymbolDef::SourceFile(..) | SymbolDef::Module(..))
116                })
117            })
118            .collect();
119
120        if !modules.is_empty() {
121            recurse(self, writer, modules.into_iter(), depth)?;
122        }
123
124        // All workbenches (including built-ins) are in separate file.
125        let workbenches: Vec<_> = children
126            .iter()
127            .filter(|symbol| {
128                symbol.with_def(|def| {
129                    matches!(
130                        def,
131                        SymbolDef::Workbench(_) | SymbolDef::Builtin(Builtin::Workbench(_))
132                    )
133                })
134            })
135            .collect();
136
137        if !workbenches.is_empty() {
138            recurse(self, writer, workbenches.into_iter(), depth)?;
139        }
140
141        Ok(())
142    }
143
144    fn generate_summary(
145        &self,
146        writer: &mut impl std::fmt::Write,
147        symbol: &Symbol,
148    ) -> std::fmt::Result {
149        writeln!(writer, "# Summary")?;
150        writeln!(writer)?;
151        self._generate_summary(writer, symbol, 0)
152    }
153
154    fn write_symbol(&self, symbol: &Symbol) -> std::io::Result<()> {
155        symbol.riter().try_for_each(|symbol| {
156            let path = &self.path.join("src").join(Self::symbol_path(&symbol));
157            std::fs::create_dir_all(path.parent().expect("A parent"))?;
158            symbol.with_def(|def| match def {
159                SymbolDef::SourceFile(_)
160                | SymbolDef::Module(_)
161                | SymbolDef::Workbench(_)
162                | SymbolDef::Builtin(Builtin::Workbench(_)) => symbol.to_md().write(path),
163                _ => Ok(()),
164            })
165        })
166    }
167
168    fn write_summary(&self, symbol: &Symbol) -> std::io::Result<()> {
169        // 1. Create the SUMMARY.md file
170        let mut file = std::fs::File::create(self.path.join("src").join("SUMMARY.md"))?;
171
172        // 2. We use a String as a buffer because generate_summary requires std::fmt::Write
173        let mut buffer = String::new();
174
175        // 3. Call generate_summary. We map the fmt::Error to an io::Error.
176
177        self.generate_summary(&mut buffer, symbol)
178            .map_err(std::io::Error::other)?;
179        file.write_all(buffer.as_bytes())?;
180        Ok(())
181    }
182}
183
184impl DocGen for MdBook {
185    fn doc_gen(&self, symbol: &Symbol) -> std::io::Result<()> {
186        std::fs::create_dir_all(self.path.join("src"))?;
187
188        self.write_book_toml()?;
189        self.write_summary(symbol)?;
190        self.write_symbol(symbol)
191    }
192}