Skip to main content

mdbook_typstpdf/config/
book.rs

1use std::path::{Path, PathBuf};
2
3use super::{Config, BEST_PRACTICE_TEMPLATE, TARGET_CHAPTERS_DIR, TARGET_TEMPLATE_DIR};
4
5impl Config {
6    pub fn get_chapter_full_file_name(
7        &self,
8        chapter: &mdbook_renderer::book::Chapter,
9        ctx: &mdbook_renderer::RenderContext,
10    ) -> Option<PathBuf> {
11        let chapter_dir = self.get_chapters_dir(ctx);
12        match &chapter.source_path {
13            None => None,
14            Some(source_path) => {
15                let chapter_file = chapter_dir.join(source_path.with_extension("typ"));
16                // let chapter_file = chapter_dir.join(chapter_file_name);
17                Some(chapter_file)
18            }
19        }
20    }
21
22    pub fn get_chapter_relative_chapter_file_name(
23        &self,
24        chapter: &mdbook_renderer::book::Chapter,
25        _ctx: &mdbook_renderer::RenderContext,
26    ) -> Option<PathBuf> {
27        let chapter_dir = PathBuf::from(TARGET_CHAPTERS_DIR);
28        match &chapter.source_path {
29            None => None,
30            Some(source_path) => {
31                let chapter_file = chapter_dir.join(source_path.with_extension("typ")); // chapters/chapter_1.typ
32                                                                                        // let chapter_file = chapter_dir.join(chapter_file_name);
33                Some(chapter_file)
34            }
35        }
36    }
37    pub fn append_chapter_to_typst_output(
38        &self,
39        ctx: &mdbook_renderer::RenderContext,
40        typst_output: &mut String,
41    ) -> anyhow::Result<()> {
42        let book = &ctx.book;
43        let mut typ_content = Vec::new();
44
45        for item in book.items.iter() {
46            self.process_book_item(item, &mut typ_content, ctx)?;
47        }
48
49        // Add all include directives to the typst output
50        for line in typ_content {
51            typst_output.push_str(&format!("{}\n", line));
52        }
53
54        Ok(())
55    }
56
57    // Helper function to recursively process book items (chapters, sections)
58    fn process_book_item(
59        &self,
60        item: &mdbook_renderer::book::BookItem,
61        typ_content: &mut Vec<String>,
62        ctx: &mdbook_renderer::RenderContext,
63    ) -> anyhow::Result<()> {
64        match item {
65            mdbook_renderer::book::BookItem::Chapter(chapter) => {
66                if let Some(chapter_path) =
67                    self.get_chapter_relative_chapter_file_name(chapter, ctx)
68                {
69                    log::debug!(
70                        "Including chapter: {} with path: {}",
71                        chapter.name,
72                        chapter_path.display()
73                    );
74
75                    // Add include directive for the chapter with its original path under "chapter"
76                    typ_content.push(format!("#include \"{}\"", chapter_path.display()));
77                }
78
79                // Process sub-items recursively
80                for sub_item in &chapter.sub_items {
81                    self.process_book_item(sub_item, typ_content, ctx)?;
82                }
83            }
84            mdbook_renderer::book::BookItem::Separator => {
85                // Just log that we're skipping the separator
86                log::info!("Skipping separator in book structure");
87            }
88            mdbook_renderer::book::BookItem::PartTitle(title) => {
89                // Add part title as a comment
90                log::info!("Adding part title: {}", title);
91                typ_content.push(format!("// Part: {}", title));
92            }
93        }
94        Ok(())
95    }
96
97    pub fn convert_book(
98        &self,
99        _chapter_file_list: &mut [PathBuf],
100        ctx: &mdbook_renderer::RenderContext,
101    ) -> anyhow::Result<()> {
102        let target_template_dir = self.get_typst_templates_dir(ctx);
103        if !target_template_dir.exists() {
104            return Err(anyhow::anyhow!(
105                "template directory {} not found",
106                target_template_dir.display()
107            ));
108        }
109
110        // // Convert all markdown files preserving their original paths
111        // self.convert_all_chapters(ctx)?;
112
113        // check the templates hashmap
114        if self.templates.is_empty() {
115            let mut typst_output = String::new();
116
117            // Don't add package imports here as they're now in each chapter file
118            // Just include the chapters
119            self.append_chapter_to_typst_output(ctx, &mut typst_output)?;
120
121            // write the typst_output to the file
122            self.write_typst_file(ctx, &typst_output, None)?;
123        } else {
124            for (name, file_name) in &self.templates {
125                let mut typst_output = String::new();
126                // get the file name from the template_file
127                let file_name = Path::new(file_name).file_name().unwrap().to_str().unwrap();
128                let dst = target_template_dir.join(file_name);
129                // if the dst file doesn't exist or is not a file, error out
130                if !dst.exists() || !dst.is_file() {
131                    return Err(anyhow::anyhow!("template file {} not found", dst.display()));
132                }
133
134                typst_output.push_str(&format!(
135                    "#import \"{}/{}\": {}\n",
136                    TARGET_TEMPLATE_DIR, file_name, BEST_PRACTICE_TEMPLATE
137                ));
138
139                // Templates still need their metadata and setup
140                typst_output.push_str("\n\n//Document Metadata\n");
141
142                typst_output.push_str("#let metadata = (\n");
143                for (key, value) in &self.template_parameters {
144                    // please escape the value if it contains double quotes
145                    let value = value.replace("\"", "\\\"");
146                    // please escape the \n in the value with \\n
147                    let value = value.replace("\n", "\\n");
148                    typst_output.push_str(&format!("    {}: \"{}\",\n", key, value));
149                }
150                typst_output.push_str(")\n");
151
152                typst_output.push_str(&format!("#show: doc => {}(\n", BEST_PRACTICE_TEMPLATE));
153                for key in self.template_parameters.keys() {
154                    typst_output.push_str(&format!("    {}: metadata.{},\n", key, key));
155                }
156                typst_output.push_str("  doc\n");
157                typst_output.push_str(")\n\n");
158
159                // append all chapter files to the typst_output
160                self.append_chapter_to_typst_output(ctx, &mut typst_output)?;
161
162                // write the typst_output to the file
163                self.write_typst_file(ctx, &typst_output, Some(name))?;
164            }
165        }
166
167        Ok(())
168    }
169
170    pub fn write_typst_file(
171        &self,
172        ctx: &mdbook_renderer::RenderContext,
173        typst_output: &str,
174        template_name: Option<&str>,
175    ) -> anyhow::Result<()> {
176        let book_name = self.get_book_name(template_name, ctx);
177        let typst_dir = self.get_typst_dir(ctx);
178        let output_file = typst_dir.join(format!("{}.typ", book_name));
179        std::fs::write(output_file, typst_output)
180            .map_err(|e| anyhow::anyhow!("failed to write typst file:{}", e))
181        // Ok(())
182    }
183
184    pub fn convert_book_to_pdf(&self, ctx: &mdbook_renderer::RenderContext) -> anyhow::Result<()> {
185        if self.templates.is_empty() {
186            self.invoke_typst_command(ctx, None)?;
187        } else {
188            for name in self.templates.keys() {
189                self.invoke_typst_command(ctx, Some(name))?;
190            }
191        }
192        // log::info!("destination:{}", ctx.destination.display());
193        Ok(())
194    }
195
196    pub fn invoke_typst_command(
197        &self,
198        ctx: &mdbook_renderer::RenderContext,
199        template_name: Option<&str>,
200    ) -> anyhow::Result<()> {
201        let book_name = self.get_book_name(template_name, ctx);
202        let typst_dir = self.get_typst_dir(ctx);
203        let typst_file = typst_dir.join(format!("{}.typ", book_name));
204        if !typst_file.exists() || !typst_file.is_file() {
205            return Err(anyhow::anyhow!(
206                "typst file {} not found",
207                typst_file.display()
208            ));
209        }
210        let pdf_dir = self.get_pdf_dir(ctx);
211        // create the pdf_dir if it doesn't exist
212        if !pdf_dir.exists() {
213            std::fs::create_dir_all(&pdf_dir)?;
214        }
215        let output_file = pdf_dir.join(format!("{}.pdf", book_name));
216        // run the typst command to convert the typst file to pdf
217        let status = std::process::Command::new("typst")
218            .arg("compile")
219            .arg(&typst_file)
220            .arg(&output_file)
221            .status()?;
222        if !status.success() {
223            return Err(anyhow::anyhow!(
224                "failed to convert typst file:{} to pdf",
225                typst_file.display()
226            ));
227        }
228        log::info!(
229            "converted typst file:{} to pdf:{}",
230            typst_file.display(),
231            output_file.display()
232        );
233        Ok(())
234    }
235}