1use std::fs::{self, File};
2use std::io::Write;
3use std::path::PathBuf;
4
5use super::MDBook;
6use crate::config::Config;
7use crate::errors::*;
8use crate::theme;
9use crate::utils::fs::write_file;
10use log::{debug, error, info, trace};
11
12/// A helper for setting up a new book and its directory structure.
13#[derive(Debug, Clone, PartialEq)]
14pub struct BookBuilder {
15 root: PathBuf,
16 create_gitignore: bool,
17 config: Config,
18 copy_theme: bool,
19}
20
21impl BookBuilder {
22 /// Create a new `BookBuilder` which will generate a book in the provided
23 /// root directory.
24 pub fn new<P: Into<PathBuf>>(root: P) -> BookBuilder {
25 BookBuilder {
26 root: root.into(),
27 create_gitignore: false,
28 config: Config::default(),
29 copy_theme: false,
30 }
31 }
32
33 /// Set the [`Config`] to be used.
34 pub fn with_config(&mut self, cfg: Config) -> &mut BookBuilder {
35 self.config = cfg;
36 self
37 }
38
39 /// Get the config used by the `BookBuilder`.
40 pub fn config(&self) -> &Config {
41 &self.config
42 }
43
44 /// Should the theme be copied into the generated book (so users can tweak
45 /// it)?
46 pub fn copy_theme(&mut self, copy: bool) -> &mut BookBuilder {
47 self.copy_theme = copy;
48 self
49 }
50
51 /// Should we create a `.gitignore` file?
52 pub fn create_gitignore(&mut self, create: bool) -> &mut BookBuilder {
53 self.create_gitignore = create;
54 self
55 }
56
57 /// Generate the actual book. This will:
58 ///
59 /// - Create the directory structure.
60 /// - Stub out some dummy chapters and the `SUMMARY.md`.
61 /// - Create a `.gitignore` (if applicable)
62 /// - Create a themes directory and populate it (if applicable)
63 /// - Generate a `book.toml` file,
64 /// - Then load the book so we can build it or run tests.
65 pub fn build(&self) -> Result<MDBook> {
66 info!("Creating a new book with stub content");
67
68 self.create_directory_structure()
69 .with_context(|| "Unable to create directory structure")?;
70
71 self.create_stub_files()
72 .with_context(|| "Unable to create stub files")?;
73
74 if self.create_gitignore {
75 self.build_gitignore()
76 .with_context(|| "Unable to create .gitignore")?;
77 }
78
79 if self.copy_theme {
80 self.copy_across_theme()
81 .with_context(|| "Unable to copy across the theme")?;
82 }
83
84 self.write_book_toml()?;
85
86 match MDBook::load(&self.root) {
87 Ok(book) => Ok(book),
88 Err(e) => {
89 error!("{}", e);
90
91 panic!(
92 "The BookBuilder should always create a valid book. If you are seeing this it \
93 is a bug and should be reported."
94 );
95 }
96 }
97 }
98
99 fn write_book_toml(&self) -> Result<()> {
100 debug!("Writing book.toml");
101 let book_toml = self.root.join("book.toml");
102 let cfg = toml::to_vec(&self.config).with_context(|| "Unable to serialize the config")?;
103
104 File::create(book_toml)
105 .with_context(|| "Couldn't create book.toml")?
106 .write_all(&cfg)
107 .with_context(|| "Unable to write config to book.toml")?;
108 Ok(())
109 }
110
111 fn copy_across_theme(&self) -> Result<()> {
112 debug!("Copying theme");
113
114 let html_config = self.config.html_config().unwrap_or_default();
115 let themedir = html_config.theme_dir(&self.root);
116
117 if !themedir.exists() {
118 debug!(
119 "{} does not exist, creating the directory",
120 themedir.display()
121 );
122 fs::create_dir(&themedir)?;
123 }
124
125 let mut index = File::create(themedir.join("index.hbs"))?;
126 index.write_all(theme::INDEX)?;
127
128 let cssdir = themedir.join("css");
129 if !cssdir.exists() {
130 fs::create_dir(&cssdir)?;
131 }
132
133 let mut general_css = File::create(cssdir.join("general.css"))?;
134 general_css.write_all(theme::GENERAL_CSS)?;
135
136 let mut chrome_css = File::create(cssdir.join("chrome.css"))?;
137 chrome_css.write_all(theme::CHROME_CSS)?;
138
139 if html_config.print.enable {
140 let mut print_css = File::create(cssdir.join("print.css"))?;
141 print_css.write_all(theme::PRINT_CSS)?;
142 }
143
144 let mut variables_css = File::create(cssdir.join("variables.css"))?;
145 variables_css.write_all(theme::VARIABLES_CSS)?;
146
147 let mut favicon = File::create(themedir.join("favicon.png"))?;
148 favicon.write_all(theme::FAVICON_PNG)?;
149
150 let mut favicon = File::create(themedir.join("favicon.svg"))?;
151 favicon.write_all(theme::FAVICON_SVG)?;
152
153 let mut js = File::create(themedir.join("book.js"))?;
154 js.write_all(theme::JS)?;
155
156 let mut highlight_css = File::create(themedir.join("highlight.css"))?;
157 highlight_css.write_all(theme::HIGHLIGHT_CSS)?;
158
159 let mut highlight_js = File::create(themedir.join("highlight.js"))?;
160 highlight_js.write_all(theme::HIGHLIGHT_JS)?;
161
162 write_file(&themedir.join("fonts"), "fonts.css", theme::fonts::CSS)?;
163 for (file_name, contents) in theme::fonts::LICENSES {
164 write_file(&themedir, file_name, contents)?;
165 }
166 for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
167 write_file(&themedir, file_name, contents)?;
168 }
169 write_file(
170 &themedir,
171 theme::fonts::SOURCE_CODE_PRO.0,
172 theme::fonts::SOURCE_CODE_PRO.1,
173 )?;
174
175 Ok(())
176 }
177
178 fn build_gitignore(&self) -> Result<()> {
179 debug!("Creating .gitignore");
180
181 let mut f = File::create(self.root.join(".gitignore"))?;
182
183 writeln!(f, "{}", self.config.build.build_dir.display())?;
184
185 Ok(())
186 }
187
188 fn create_stub_files(&self) -> Result<()> {
189 debug!("Creating example book contents");
190 let src_dir = self.root.join(&self.config.book.src);
191
192 let summary = src_dir.join("SUMMARY.md");
193 if !summary.exists() {
194 trace!("No summary found creating stub summary and chapter_1.md.");
195 let mut f = File::create(&summary).with_context(|| "Unable to create SUMMARY.md")?;
196 writeln!(f, "# Summary")?;
197 writeln!(f)?;
198 writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
199
200 let chapter_1 = src_dir.join("chapter_1.md");
201 let mut f = File::create(chapter_1).with_context(|| "Unable to create chapter_1.md")?;
202 writeln!(f, "# Chapter 1")?;
203 } else {
204 trace!("Existing summary found, no need to create stub files.");
205 }
206 Ok(())
207 }
208
209 fn create_directory_structure(&self) -> Result<()> {
210 debug!("Creating directory tree");
211 fs::create_dir_all(&self.root)?;
212
213 let src = self.root.join(&self.config.book.src);
214 fs::create_dir_all(src)?;
215
216 let build = self.root.join(&self.config.build.build_dir);
217 fs::create_dir_all(build)?;
218
219 Ok(())
220 }
221}
222