1 | use std::fs::{self, File}; |
2 | use std::io::Write; |
3 | use std::path::PathBuf; |
4 | |
5 | use super::MDBook; |
6 | use crate::config::Config; |
7 | use crate::errors::*; |
8 | use crate::theme; |
9 | use crate::utils::fs::write_file; |
10 | use log::{debug, error, info, trace}; |
11 | |
12 | /// A helper for setting up a new book and its directory structure. |
13 | #[derive (Debug, Clone, PartialEq)] |
14 | pub struct BookBuilder { |
15 | root: PathBuf, |
16 | create_gitignore: bool, |
17 | config: Config, |
18 | copy_theme: bool, |
19 | } |
20 | |
21 | impl 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 | |