book.rs
1 use std::collections::{HashMap, VecDeque}; 2 use std::fmt::{self, Display, Formatter}; 3 use std::fs::{self, File}; 4 use std::io::{Read, Write}; 5 use std::path::{Path, PathBuf}; 6 7 use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; 8 // use crate::build_opts::BuildOpts; 9 // use crate::config::Config; 10 use crate::errors::*; 11 12 /// Load a book into memory from its `src/` directory. 13 pub fn load_book<P: AsRef<Path>>( 14 root_dir: P, 15 cfg: &Config, 16 build_opts: &BuildOpts, 17 ) -> Result<LoadedBook> { 18 if cfg.has_localized_dir_structure() { 19 match build_opts.language_ident { 20 // Build a single book's translation. 21 Some(_) => Ok(LoadedBook::Single(load_single_book_translation( 22 &root_dir, 23 cfg, 24 &build_opts.language_ident, 25 )?)), 26 // Build all available translations at once. 27 None => { 28 let mut translations = HashMap::new(); 29 for (lang_ident, _) in cfg.language.0.iter() { 30 let book = 31 load_single_book_translation(&root_dir, cfg, &Some(lang_ident.clone()))?; 32 translations.insert(lang_ident.clone(), book); 33 } 34 Ok(LoadedBook::Localized(LocalizedBooks(translations))) 35 } 36 } 37 } else { 38 Ok(LoadedBook::Single(load_single_book_translation( 39 &root_dir, cfg, &None, 40 )?)) 41 } 42 } 43 44 fn load_single_book_translation<P: AsRef<Path>>( 45 root_dir: P, 46 cfg: &Config, 47 language_ident: &Option<String>, 48 ) -> Result<Book> { 49 let localized_src_dir = root_dir 50 .as_ref() 51 .join(cfg.get_localized_src_path(language_ident.as_ref()).unwrap()); 52 let fallback_src_dir = root_dir.as_ref().join(cfg.get_fallback_src_path()); 53 54 let summary_md = localized_src_dir.join("SUMMARY.md"); 55 56 let mut summary_content = String::new(); 57 File::open(&summary_md) 58 .with_context(|| { 59 format!( 60 "Couldn't open SUMMARY.md in {:?} directory", 61 localized_src_dir 62 ) 63 })? 64 .read_to_string(&mut summary_content)?; 65 66 let summary = parse_summary(&summary_content) 67 .with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?; 68 69 if cfg.build.create_missing { 70 create_missing(&localized_src_dir, &summary) 71 .with_context(|| "Unable to create missing chapters")?; 72 } 73 74 load_book_from_disk(&summary, localized_src_dir, fallback_src_dir, cfg) 75 } 76 77 fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> { 78 let mut items: Vec<_> = summary 79 .prefix_chapters 80 .iter() 81 .chain(summary.numbered_chapters.iter()) 82 .chain(summary.suffix_chapters.iter()) 83 .collect(); 84 85 while !items.is_empty() { 86 let next = items.pop().expect("already checked"); 87 88 if let SummaryItem::Link(ref link) = *next { 89 if let Some(ref location) = link.location { 90 let filename = src_dir.join(location); 91 if !filename.exists() { 92 create_missing_link(&filename, link)?; 93 } 94 } 95 96 items.extend(&link.nested_items); 97 } 98 } 99 100 Ok(()) 101 } 102 103 fn create_missing_link(filename: &Path, link: &Link) -> Result<()> { 104 if let Some(parent) = filename.parent() { 105 if !parent.exists() { 106 fs::create_dir_all(parent).map_err(|e| { 107 Error::from(format!( 108 "Unable to create missing directory {:?}: {}", 109 parent, e 110 )) 111 })?; 112 } 113 } 114 debug!("Creating missing file {}", filename.display()); 115 116 let mut f = File::create(&filename)?; 117 writeln!(f, "# {}", link.name)?; 118 119 Ok(()) 120 } 121 122 /// A dumb tree structure representing a book. 123 /// 124 /// For the moment a book is just a collection of [`BookItems`] which are 125 /// accessible by either iterating (immutably) over the book with [`iter()`], or 126 /// recursively applying a closure to each section to mutate the chapters, using 127 /// [`for_each_mut()`]. 128 /// 129 /// 130 /// [`iter()`]: #method.iter 131 /// [`for_each_mut()`]: #method.for_each_mut 132 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] 133 pub struct Book { 134 /// The sections in this book. 135 pub sections: Vec<BookItem>, 136 /// Chapter title overrides for this book. 137 #[serde(default)] 138 pub chapter_titles: HashMap<PathBuf, String>, 139 __non_exhaustive: (), 140 } 141 142 impl Book { 143 /// Create an empty book. 144 pub fn new() -> Self { 145 Default::default() 146 } 147 148 /// Get a depth-first iterator over the items in the book. 149 pub fn iter(&self) -> BookItems<'_> { 150 BookItems { 151 items: self.sections.iter().collect(), 152 } 153 } 154 155 /// Recursively apply a closure to each item in the book, allowing you to 156 /// mutate them. 157 /// 158 /// # Note 159 /// 160 /// Unlike the `iter()` method, this requires a closure instead of returning 161 /// an iterator. This is because using iterators can possibly allow you 162 /// to have iterator invalidation errors. 163 pub fn for_each_mut<F>(&mut self, mut func: F) 164 where 165 F: FnMut(&mut BookItem), 166 { 167 for_each_mut(&mut func, &mut self.sections); 168 } 169 170 /// Append a `BookItem` to the `Book`. 171 pub fn push_item<I: Into<BookItem>>(&mut self, item: I) -> &mut Self { 172 self.sections.push(item.into()); 173 self 174 } 175 } 176 177 pub fn for_each_mut<'a, F, I>(func: &mut F, items: I) 178 where 179 F: FnMut(&mut BookItem), 180 I: IntoIterator<Item = &'a mut BookItem>, 181 { 182 for item in items { 183 if let BookItem::Chapter(ch) = item { 184 for_each_mut(func, &mut ch.sub_items); 185 } 186 187 func(item); 188 } 189 } 190 191 /// A collection of `Books`, each one a single localization. 192 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] 193 pub struct LocalizedBooks(pub HashMap<String, Book>); 194 195 impl LocalizedBooks { 196 /// Get a depth-first iterator over the items in the book. 197 pub fn iter(&self) -> BookItems<'_> { 198 let mut items = VecDeque::new(); 199 200 for (_, book) in self.0.iter() { 201 items.extend(book.iter().items); 202 } 203 204 BookItems { items: items } 205 } 206 207 /// Recursively apply a closure to each item in the book, allowing you to 208 /// mutate them. 209 /// 210 /// # Note 211 /// 212 /// Unlike the `iter()` method, this requires a closure instead of returning 213 /// an iterator. This is because using iterators can possibly allow you 214 /// to have iterator invalidation errors. 215 pub fn for_each_mut<F>(&mut self, mut func: F) 216 where 217 F: FnMut(&mut BookItem), 218 { 219 for (_, book) in self.0.iter_mut() { 220 book.for_each_mut(&mut func); 221 } 222 } 223 } 224 225 /// A book which has been loaded and is ready for rendering. 226 /// 227 /// This exists because the result of loading a book directory can be multiple 228 /// books, each one representing a separate translation, or a single book with 229 /// no translations. 230 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 231 pub enum LoadedBook { 232 /// The book was loaded with all translations. 233 Localized(LocalizedBooks), 234 /// The book was loaded without any additional translations. 235 Single(Book), 236 } 237 238 impl LoadedBook { 239 /// Get a depth-first iterator over the items in the book. 240 pub fn iter(&self) -> BookItems<'_> { 241 match self { 242 LoadedBook::Localized(books) => books.iter(), 243 LoadedBook::Single(book) => book.iter(), 244 } 245 } 246 247 /// Recursively apply a closure to each item in the book, allowing you to 248 /// mutate them. 249 /// 250 /// # Note 251 /// 252 /// Unlike the `iter()` method, this requires a closure instead of returning 253 /// an iterator. This is because using iterators can possibly allow you 254 /// to have iterator invalidation errors. 255 pub fn for_each_mut<F>(&mut self, mut func: F) 256 where 257 F: FnMut(&mut BookItem), 258 { 259 match self { 260 LoadedBook::Localized(books) => books.for_each_mut(&mut func), 261 LoadedBook::Single(book) => book.for_each_mut(&mut func), 262 } 263 } 264 265 /// Returns one of the books loaded. Used for compatibility. 266 pub fn first(&self) -> &Book { 267 match self { 268 LoadedBook::Localized(books) => books.0.iter().next().unwrap().1, 269 LoadedBook::Single(book) => &book, 270 } 271 } 272 } 273 274 /// Enum representing any type of item which can be added to a book. 275 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 276 pub enum BookItem { 277 /// A nested chapter. 278 Chapter(Chapter), 279 /// A section separator. 280 Separator, 281 /// A part title. 282 PartTitle(String), 283 } 284 285 impl From<Chapter> for BookItem { 286 fn from(other: Chapter) -> BookItem { 287 BookItem::Chapter(other) 288 } 289 } 290 291 /// The representation of a "chapter", usually mapping to a single file on 292 /// disk however it may contain multiple sub-chapters. 293 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] 294 pub struct Chapter<R> { 295 /// The chapter's name. 296 pub name: String, 297 /// The chapter's contents. 298 pub content: String, 299 /// The chapter's section number, if it has one. 300 pub number: Option<SectionNumber>, 301 /// Nested items. 302 pub sub_items: Vec<BookItem>, 303 /// The chapter's route 304 pub path: Option<R>, 305 /// An ordered list of the names of each chapter above this one in the hierarchy. 306 pub parent_names: Vec<String>, 307 } 308 309 impl<R> Chapter<R> { 310 /// Create a new chapter with the provided content. 311 pub fn new<P: Into<R>>( 312 name: &str, 313 content: String, 314 p: P, 315 parent_names: Vec<String>, 316 ) -> Chapter { 317 let path: R = p.into(); 318 Chapter { 319 name: name.to_string(), 320 content, 321 path: Some(path.clone()), 322 parent_names, 323 ..Default::default() 324 } 325 } 326 327 /// Create a new draft chapter that is not attached to a source markdown file (and thus 328 /// has no content). 329 pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self { 330 Chapter { 331 name: name.to_string(), 332 content: String::new(), 333 path: None, 334 parent_names, 335 ..Default::default() 336 } 337 } 338 339 /// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file. 340 pub fn is_draft_chapter(&self) -> bool { 341 self.path.is_none() 342 } 343 } 344 345 /// Use the provided `Summary` to load a `Book` from disk. 346 /// 347 /// You need to pass in the book's source directory because all the links in 348 /// `SUMMARY.md` give the chapter locations relative to it. 349 pub(crate) fn load_book_from_disk<P: AsRef<Path>>( 350 summary: &Summary, 351 localized_src_dir: P, 352 fallback_src_dir: P, 353 cfg: &Config, 354 ) -> Result<Book> { 355 debug!("Loading the book from disk"); 356 357 let prefix = summary.prefix_chapters.iter(); 358 let numbered = summary.numbered_chapters.iter(); 359 let suffix = summary.suffix_chapters.iter(); 360 361 let summary_items = prefix.chain(numbered).chain(suffix); 362 363 let mut chapters = Vec::new(); 364 365 for summary_item in summary_items { 366 let chapter = load_summary_item( 367 summary_item, 368 localized_src_dir.as_ref(), 369 fallback_src_dir.as_ref(), 370 Vec::new(), 371 cfg, 372 )?; 373 chapters.push(chapter); 374 } 375 376 Ok(Book { 377 sections: chapters, 378 chapter_titles: HashMap::new(), 379 __non_exhaustive: (), 380 }) 381 } 382 383 fn load_summary_item<P: AsRef<Path> + Clone>( 384 item: &SummaryItem, 385 localized_src_dir: P, 386 fallback_src_dir: P, 387 parent_names: Vec<String>, 388 cfg: &Config, 389 ) -> Result<BookItem> { 390 match item { 391 SummaryItem::Separator => Ok(BookItem::Separator), 392 SummaryItem::Link(ref link) => { 393 load_chapter(link, localized_src_dir, fallback_src_dir, parent_names, cfg) 394 .map(BookItem::Chapter) 395 } 396 SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())), 397 } 398 } 399 400 fn load_chapter<P: AsRef<Path>>( 401 link: &Link, 402 localized_src_dir: P, 403 fallback_src_dir: P, 404 parent_names: Vec<String>, 405 cfg: &Config, 406 ) -> Result<Chapter> { 407 let src_dir_localized = localized_src_dir.as_ref(); 408 let src_dir_fallback = fallback_src_dir.as_ref(); 409 410 let mut ch = if let Some(ref link_location) = link.location { 411 debug!("Loading {} ({})", link.name, link_location.display()); 412 413 let mut src_dir = src_dir_localized; 414 let mut location = if link_location.is_absolute() { 415 link_location.clone() 416 } else { 417 src_dir.join(link_location) 418 }; 419 420 if !location.exists() && !link_location.is_absolute() { 421 src_dir = src_dir_fallback; 422 location = src_dir.join(link_location); 423 debug!( 424 "Falling back to default translation in path \"{}\"", 425 location.display() 426 ); 427 } 428 if !location.exists() && cfg.build.create_missing { 429 create_missing_link(&location, &link) 430 .with_context(|| "Unable to create missing link reference")?; 431 } 432 433 let mut f = File::open(&location) 434 .with_context(|| format!("Chapter file not found, {}", link_location.display()))?; 435 436 let mut content = String::new(); 437 f.read_to_string(&mut content).with_context(|| { 438 format!("Unable to read \"{}\" ({})", link.name, location.display()) 439 })?; 440 441 if content.as_bytes().starts_with(b"\xef\xbb\xbf") { 442 content.replace_range(..3, ""); 443 } 444 445 let stripped = location 446 .strip_prefix(&src_dir) 447 .expect("Chapters are always inside a book"); 448 449 Chapter::new(&link.name, content, stripped, parent_names.clone()) 450 } else { 451 Chapter::new_draft(&link.name, parent_names.clone()) 452 }; 453 454 let mut sub_item_parents = parent_names; 455 456 ch.number = link.number.clone(); 457 458 sub_item_parents.push(link.name.clone()); 459 let sub_items = link 460 .nested_items 461 .iter() 462 .map(|i| { 463 load_summary_item( 464 i, 465 src_dir_localized, 466 src_dir_fallback, 467 sub_item_parents.clone(), 468 cfg, 469 ) 470 }) 471 .collect::<Result<Vec<_>>>()?; 472 473 ch.sub_items = sub_items; 474 475 Ok(ch) 476 } 477 478 /// A depth-first iterator over the items in a book. 479 /// 480 /// # Note 481 /// 482 /// This struct shouldn't be created directly, instead prefer the 483 /// [`Book::iter()`] method. 484 pub struct BookItems<'a> { 485 items: VecDeque<&'a BookItem>, 486 } 487 488 impl<'a> Iterator for BookItems<'a> { 489 type Item = &'a BookItem; 490 491 fn next(&mut self) -> Option<Self::Item> { 492 let item = self.items.pop_front(); 493 494 if let Some(&BookItem::Chapter(ref ch)) = item { 495 // if we wanted a breadth-first iterator we'd `extend()` here 496 for sub_item in ch.sub_items.iter().rev() { 497 self.items.push_front(sub_item); 498 } 499 } 500 501 item 502 } 503 } 504 505 impl Display for Chapter { 506 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 507 if let Some(ref section_number) = self.number { 508 write!(f, "{} ", section_number)?; 509 } 510 511 write!(f, "{}", self.name) 512 } 513 }