mdbook_typstpdf/config/
book.rs1use 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 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")); 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 for line in typ_content {
51 typst_output.push_str(&format!("{}\n", line));
52 }
53
54 Ok(())
55 }
56
57 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 typ_content.push(format!("#include \"{}\"", chapter_path.display()));
77 }
78
79 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 log::info!("Skipping separator in book structure");
87 }
88 mdbook_renderer::book::BookItem::PartTitle(title) => {
89 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 if self.templates.is_empty() {
115 let mut typst_output = String::new();
116
117 self.append_chapter_to_typst_output(ctx, &mut typst_output)?;
120
121 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 let file_name = Path::new(file_name).file_name().unwrap().to_str().unwrap();
128 let dst = target_template_dir.join(file_name);
129 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 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 let value = value.replace("\"", "\\\"");
146 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 self.append_chapter_to_typst_output(ctx, &mut typst_output)?;
161
162 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 }
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 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 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 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}