use std::{fmt::Display, fs, path::Path}; use anyhow::{Context, Result}; use log::error; use crate::{ config_file::Config, constants::{NEXT_CHAPTER, NEXT_CHAPTER_INCLUDE_ONLY}, file_tree::{FileTree, GeneratedFile}, }; use super::{replacement::untemplatize_chapter, MangledName}; pub struct ChapterName { name: MangledName, number: u32, } impl Display for ChapterName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("{:02}_{}", self.number, self.name)) } } impl ChapterName { pub fn from_str(name: &str, last_chapter_number: u32) -> Self { let name = MangledName::new(name); Self { name, number: last_chapter_number + 1, } } pub fn new(name: MangledName, last_chapter_number: u32) -> Self { Self { name, number: last_chapter_number + 1, } } pub fn to_components(self) -> (MangledName, u32) { (self.name, self.number) } fn from_components(name: MangledName, number: u32) -> Self { Self { name, number } } } pub fn generate_new_chapter( config: Config, project_root: &Path, name: String, ) -> anyhow::Result { let mut file_tree = FileTree::new(); let (last_chapter_name, last_chapter_number) = get_last_chapter_name(project_root) .context("Failed to get information on last chapter")? .unwrap_or(ChapterName { name: MangledName::from_str_unsafe("static"), number: 0, }) .to_components(); file_tree.add_file(new_main_file( project_root, &config, &name, last_chapter_number, last_chapter_name, )?); file_tree.add_file(new_chapter_file( &config, &name, project_root, last_chapter_number, )); Ok(file_tree) } fn get_last_chapter_name(project_root: &Path) -> anyhow::Result> { let chapter_dirs = project_root.join("content"); let mut chapter_names = fs::read_dir(chapter_dirs)? .filter_map(|path| -> Option> { let path = match path.context("Failed to read a path") { Ok(ok) => ok, Err(err) => return Some(Err(err)), }; let os_file_name = path.file_name(); let file_name = os_file_name .to_str() .expect("All chapter should be converted to ascii"); if file_name == "static" { None } else { Some(Ok(file_name.to_owned())) } }) .collect::>>()?; // There are no chapters, besides the default `static` one, which was sorted out if chapter_names.is_empty() { return Ok(None); } // The names are prefixed with a number chapter_names.sort(); let raw_components = chapter_names[chapter_names.len() - 1] .split_once('_') .expect("Exits"); let number: u32 = raw_components.0.parse().expect("Will be a number"); // The name is already mangled assert!( MangledName::check_mangled(raw_components.1), "'{}' is not yet mangled?! It should be: '{}'", raw_components.1, MangledName::new(raw_components.1) ); let name: MangledName = MangledName::from_str_unsafe(raw_components.1); Ok(Some(ChapterName::from_components(name, number))) } fn new_chapter_file( config: &Config, name: &str, project_root: &Path, last_chapter_number: u32, ) -> GeneratedFile { let chapter_text = untemplatize_chapter(&config.templates.chapter, &name); GeneratedFile::new( project_root .join("content") .join(ChapterName::from_str(name, last_chapter_number).to_string()) .join("chapter.tex"), chapter_text, ) } fn find(input: &str, marker: &str, source_path: &Path) -> anyhow::Result { let find_index = input.find(marker).with_context(|| { format!( "Failed to find the '{}' in '{}'.", marker, source_path.display() ) })?; Ok(find_index) } fn find_and_append( input: &mut String, marker: &str, source_path: &Path, insertion: &str, ) -> anyhow::Result<()> { let find_index = find(input, marker, source_path)?; input.insert_str(find_index, insertion); Ok(()) } fn new_main_file( project_root: &Path, config: &Config, name: &str, last_chapter_number: u32, last_chapter_name: MangledName, ) -> anyhow::Result { let main_path = project_root.join(&config.main_file); let mut main_text = fs::read_to_string(&main_path)?; let chapter_path = format!( "content/{}/{}", ChapterName::from_str(name, last_chapter_number).to_string(), "chapter.tex", ); // Check if this is the first added chapter; Then there should be no other path before this one. if last_chapter_name.as_str() != "static" && last_chapter_number != 0 { let old_path = format!( "content/{}/{}", ChapterName::new(last_chapter_name, last_chapter_number - 1).to_string(), "chapter.tex", ); let find_index = find(&main_text, NEXT_CHAPTER_INCLUDE_ONLY, &main_path)?; // The last element adds the indentation. let previous_includeonly_white_space = main_text .as_bytes() .iter() .take(find_index) .rev() .take_while(|byte| **byte == ' ' as u8 || **byte == '\n' as u8) .count(); let previous_includeonly = find_index - (old_path.len() + previous_includeonly_white_space); let old_include = { let old_bytes = main_text .as_bytes() .into_iter() .take(find_index) .rev() .take(find_index - previous_includeonly) .rev() .map(|b| *b) .collect::>(); let string = String::from_utf8(old_bytes).expect("This should be valid utf8"); string.trim().to_owned() }; if old_include != old_path.as_str() { error!( "Failed to determine the old includeonly path: Found '{}' but expected '{}'. \ Refusing to add a comment to it.", old_include, old_path ) } else { main_text.insert_str(previous_includeonly, "% "); } } find_and_append( &mut main_text, NEXT_CHAPTER_INCLUDE_ONLY, &main_path, &format!("{}\n ", chapter_path), )?; find_and_append( &mut main_text, NEXT_CHAPTER, &main_path, &format!("\\include{{{}}}\n", chapter_path), )?; Ok(GeneratedFile::new(main_path, main_text)) }