diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/cli.rs (renamed from src/command_line_interface.rs) | 19 | ||||
-rw-r--r-- | src/config_file.rs | 19 | ||||
-rw-r--r-- | src/data.rs | 27 | ||||
-rw-r--r-- | src/file_tree/mod.rs | 88 | ||||
-rw-r--r-- | src/main.rs | 127 | ||||
-rw-r--r-- | src/new/chapter.rs | 143 | ||||
-rw-r--r-- | src/new/mod.rs | 95 | ||||
-rw-r--r-- | src/new/project.rs | 111 | ||||
-rw-r--r-- | src/new/section.rs | 102 |
9 files changed, 357 insertions, 374 deletions
diff --git a/src/command_line_interface.rs b/src/cli.rs index 5d24ae5..fe1b194 100644 --- a/src/command_line_interface.rs +++ b/src/cli.rs @@ -2,7 +2,7 @@ use clap::{Parser, Subcommand}; /// A project manager for LaTeX #[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None)] +#[command(author, version, about, long_about = None)] pub struct Args { #[command(subcommand)] pub cli: Command, @@ -12,13 +12,17 @@ pub struct Args { pub enum Command { /// Generates a new part #[command(subcommand)] - New(SubCommand), + New(What), } #[derive(Subcommand, Debug)] -pub enum SubCommand { +pub enum What { /// Adds a section Section { + /// The name of the chapter to extend, can be empty when the current_dir is inside a + /// chapter already. + #[arg(long, short)] + chapter: Option<String>, /// Name of the new Section name: String, }, @@ -28,13 +32,4 @@ pub enum SubCommand { /// Name of the new Chapter name: String, }, - // /// generates a new project - // Project { - // /// Name of the new Project - // name: String, - // /// Name of the first chapter - // first_chapter: String, - // // /// Name of the first section - // // first_section: String, - // }, } diff --git a/src/config_file.rs b/src/config_file.rs new file mode 100644 index 0000000..838a78d --- /dev/null +++ b/src/config_file.rs @@ -0,0 +1,19 @@ +use serde_derive::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct Config { + pub last_chapter: LastChapter, + pub templates: Template, +} + +#[derive(Deserialize, Serialize)] +pub struct LastChapter { + pub user_name: String, + pub number: u32, +} + +#[derive(Deserialize, Serialize)] +pub struct Template { + pub section: String, + pub chapter: String +} diff --git a/src/data.rs b/src/data.rs deleted file mode 100644 index 72609b8..0000000 --- a/src/data.rs +++ /dev/null @@ -1,27 +0,0 @@ -use serde_derive::{Deserialize, Serialize}; - -#[derive(Deserialize, Serialize)] -pub struct Data { - pub last_chapter: LastChapter, -} - -#[derive(Deserialize, Serialize)] -pub struct LastChapter { - pub user_name: String, - pub number: u32, -} - -//fn main() { -// let config: Config = toml::from_str(r#" -// ip = '127.0.0.1' -// -// [keys] -// github = 'xxxxxxxxxxxxxxxxx' -// travis = 'yyyyyyyyyyyyyyyyy' -// "#).unwrap(); -// -// assert_eq!(config.ip, "127.0.0.1"); -// assert_eq!(config.port, None); -// assert_eq!(config.keys.github, "xxxxxxxxxxxxxxxxx"); -// assert_eq!(config.keys.travis.as_ref().unwrap(), "yyyyyyyyyyyyyyyyy"); -//} diff --git a/src/file_tree/mod.rs b/src/file_tree/mod.rs new file mode 100644 index 0000000..d6f0c3c --- /dev/null +++ b/src/file_tree/mod.rs @@ -0,0 +1,88 @@ +/* +* Copyright (C) 2023 - 2024: +* The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org> +* SPDX-License-Identifier: LGPL-3.0-or-later +* +* This file is part of the Trixy crate for Trinitrix. +* +* Trixy is free software: you can redistribute it and/or modify +* it under the terms of the Lesser GNU General Public License as +* published by the Free Software Foundation, either version 3 of +* the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* and the Lesser GNU General Public License along with this program. +* If not, see <https://www.gnu.org/licenses/>. +*/ + +//! [`FileTree`]s are the fundamental data structure used by trixy to present generated data to +//! you. + +use std::{ + fs, io, + path::{Path, PathBuf}, +}; + +/// A file tree containing all files that were generated. These are separated into host and +/// auxiliary files. See their respective descriptions about what differentiates them. +#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct FileTree { + /// Files, that are supposed to be included in the compiled crate. + pub files: Vec<GeneratedFile>, +} + +/// A generated files +#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct GeneratedFile { + /// The path this generated file would like to be placed at. + /// This path is relative to the crate root. + pub path: PathBuf, + + /// The content of this file. + /// + /// This should already be formatted and ready to be used. + pub value: String, +} + +impl GeneratedFile { + pub fn new(path: PathBuf, value: String) -> Self { + Self { path, value } + } + pub fn new_in_out_dir(name: String, value: String, out_dir: &Path) -> Self { + let path = out_dir.join(name); + Self { path, value } + } + + pub fn materialize(self) -> io::Result<()> { + fs::create_dir_all(self.path.parent().expect("This path should have a parent"))?; + fs::write(self.path, self.value.as_bytes())?; + Ok(()) + } +} + +impl FileTree { + pub fn new() -> Self { + Self::default() + } + + pub fn add_file(&mut self, file: GeneratedFile) { + self.files.push(file) + } + + pub fn extend(&mut self, files: Vec<GeneratedFile>) { + files.into_iter().for_each(|file| self.add_file(file)); + } + + pub fn materialize(self) -> io::Result<()> { + self.files + .into_iter() + .map(|file| -> io::Result<()> { file.materialize() }) + .collect::<io::Result<()>>()?; + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index b19e7bf..8c2ea62 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,38 +1,111 @@ +use std::{env, ffi::OsString, fs, path::PathBuf}; + +use anyhow::{bail, Context}; use clap::Parser; -use command_line_interface::{ - Args, - Command::New, - SubCommand::{Chapter, Section}, +use log::debug; + +use crate::{ + cli::{ + Args, + Command::New, + What::{Chapter, Section}, + }, + config_file::Config, + new::{chapter::generate_new_chapter, section::generate_new_section}, }; -use new::{chapter::generate_new_chapter, section::generate_new_section}; -pub mod command_line_interface; -pub mod data; +pub mod cli; +pub mod config_file; pub mod new; -fn main() { +// The copyright header tells you, where this file is from. +pub mod file_tree; + +fn main() -> anyhow::Result<()> { + env_logger::init(); let args = Args::parse(); - match args.cli { + let project_root = get_project_root_by_lmp_toml().context("Looking for the project root")?; + + let config_file = fs::read_to_string(project_root.join("lpm.toml"))?; + let config: Config = toml::from_str(&config_file).context("Reading toml from string")?; + + let file_tree = match args.cli { New(new_command) => match new_command { - Section { name } => generate_new_section(name).unwrap(), - Chapter { name } => generate_new_chapter(name).unwrap(), - // Project { - // name, - // first_chapter, - // //first_section, - // } => { - // let preamble_path = PathBuf::from(""); - // let resource_path = PathBuf::from(""); - // generate_new_project( - // name, - // first_chapter, - // //first_section, - // preamble_path, - // resource_path, - // ) - // .unwrap() - // } + Section { name, chapter } => { + let chapter = if let Some(val) = chapter { + // The user probably has not added the preceeding chapter number to the chapter + // string + if val.starts_with(|c: char| c.is_numeric()) { + eprintln!( + "Your chapter name starts with a number, assuming \ + that you have already added the chapter number" + ); + val + } else { + bail!( + "Calculating the chapter number is not yet \ + implemented, please add it yourself" + ); + } + } else { + // The user thinks that they are already inside a chapter + get_upwards_chapter()? + }; + + generate_new_section(&config, name, &project_root, &chapter)? + } + Chapter { name } => generate_new_chapter(config, &project_root, name)?, }, + }; + + file_tree.materialize()?; + + Ok(()) +} + +pub fn get_project_root_by_lmp_toml() -> anyhow::Result<PathBuf> { + let path = env::current_dir()?; + let mut path_ancestors = path.as_path().ancestors(); + + while let Some(path_segment) = path_ancestors.next() { + if fs::read_dir(path_segment)?.into_iter().any(|path_segment| { + path_segment + .expect("The read_dir shouldn't error out here") + .file_name() + == OsString::from("lpm.toml") + }) { + return Ok(PathBuf::from(path_segment)); + } } + bail!("Ran out of places to find lpm.toml") +} + +fn get_upwards_chapter() -> anyhow::Result<String> { + let current_path = env::current_dir()?; + + for anc in current_path.as_path().ancestors() { + debug!("Reading directory {}", anc.display()); + + for dir in fs::read_dir(anc)? { + let dir = dir?; + debug!("Checking path: {}", dir.file_name().to_string_lossy()); + + if dir.file_name() == OsString::from("chapter.tex") { + match anc + .file_name() + .expect("This should always be a file") + .to_str() + { + Some(str) => return Ok(str.to_owned()), + None => bail!( + "Failed to convert your path ('{}') to a string!", + dir.file_name().to_string_lossy() + ), + } + } + } + } + + bail!("Failed to get a chapter name, please specify one with the `--chapter` flag!") } diff --git a/src/new/chapter.rs b/src/new/chapter.rs index 88f2a85..749202f 100644 --- a/src/new/chapter.rs +++ b/src/new/chapter.rs @@ -1,67 +1,108 @@ -use std::{ - fs::{self, File}, - io::{self, Write}, -}; +use std::{fs, path::Path}; use convert_case::{Case, Casing}; -use crate::data::Data; +use crate::{ + config_file::Config, + file_tree::{FileTree, GeneratedFile}, +}; -use super::{get_project_root, CHAPTER}; +pub fn generate_new_chapter( + config: Config, + project_root: &Path, + name: String, +) -> anyhow::Result<FileTree> { + let mut file_tree = FileTree::new(); + file_tree.add_file(new_main_file(project_root, &config, &name)?); + file_tree.add_file(new_chapter_file(&config, &name, project_root)); + file_tree.add_file(new_lpm_toml_file(config, name, project_root)); -pub fn generate_new_chapter(name: String) -> io::Result<()> { - let project_root = get_project_root().unwrap(); + Ok(file_tree) +} - let raw_data_file = fs::read_to_string(project_root.join("lpm.toml")).unwrap(); - let mut data_file: Data = toml::from_str(&raw_data_file).unwrap(); - let mut main_file = fs::read_to_string(project_root.join("main.tex")).unwrap(); +fn new_lpm_toml_file(mut config: Config, name: String, project_root: &Path) -> GeneratedFile { + config.last_chapter.user_name = name.to_case(Case::Snake); + config.last_chapter.number += 1; - fs::create_dir(project_root.join(format!("content/{}", name.to_case(Case::Snake),))).unwrap(); - let mut new_chapter = File::create(project_root.join(format!( - "content/{}/chapter_{:02}.tex", - name.to_case(Case::Snake), - data_file.last_chapter.number + 1 - ))) - .unwrap(); - new_chapter - .write_all(CHAPTER.replace("REPLACEMENT_CHAPTER", &name).as_bytes()) - .unwrap(); + GeneratedFile::new( + project_root.join("lpm.toml"), + toml::to_string(&config).expect("We changed it ourselfes, the conversion should work"), + ) +} - fs::create_dir(project_root.join(format!("content/{}/sections", name.to_case(Case::Snake),))) - .unwrap(); +fn new_chapter_file(config: &Config, name: &str, project_root: &Path) -> GeneratedFile { + let chapter_text = config + .templates + .chapter + .replace("REPLACEMENT_CHAPTER", &name); - main_file = main_file.replace( - &format!( - "\\includeonly{{content/{}/{}}}", - &data_file.last_chapter.user_name, - &format!("chapter_{:02}", data_file.last_chapter.number) - ), - &format!( - "\\includeonly{{content/{}/{}}}", - name.to_case(Case::Snake), - &format!("chapter_{:02}", data_file.last_chapter.number + 1) - ), - ); - let find_index = main_file.find("% NEXT_CHAPTER").unwrap(); - main_file.insert_str( + GeneratedFile::new( + project_root + .join("content") + .join(format! {"{:02}_{}", config.last_chapter.number + 1, name.to_case(Case::Snake)}) + .join("chapter.tex"), + chapter_text, + ) +} + +fn new_main_file( + project_root: &Path, + config: &Config, + name: &str, +) -> anyhow::Result<GeneratedFile> { + let mut main_text = fs::read_to_string(project_root.join("main.tex"))?; + + if &config.last_chapter.user_name == "static" && config.last_chapter.number == 0 { + // This is the first added chapter; The `\includeonly` will be empty. + main_text = main_text.replace( + "\\includeonly{}", + &format!( + "\\includeonly{{content/{}/{}}}", + &format!( + "{:02}_{}", + config.last_chapter.number + 1, + &name.to_case(Case::Snake) + ), + "chapter.tex", + ), + ) + } else { + main_text = main_text.replace( + &format!( + "\\includeonly{{content/{}/{}}}", + &format!( + "{:02}_{}", + config.last_chapter.number, &config.last_chapter.user_name + ), + "chapter.tex", + ), + &format!( + "\\includeonly{{content/{}/{}}}", + &format!( + "{:02}_{}", + config.last_chapter.number + 1, + &name.to_case(Case::Snake) + ), + "chapter.tex", + ), + ) + }; + + let find_index = main_text + .find("% NEXT_CHAPTER") + .expect("The % NEXT_CHAPTER maker must exist"); + main_text.insert_str( find_index, &format!( "\\include{{content/{}/{}}}\n ", - name.to_case(Case::Snake), - &format!("chapter_{:02}", data_file.last_chapter.number + 1) + &format!( + "{:02}_{}", + config.last_chapter.number + 1, + &name.to_case(Case::Snake) + ), + "chapter.tex", ), ); - data_file.last_chapter.user_name = name.to_case(Case::Snake); - data_file.last_chapter.number += 1; - - fs::write( - project_root.join("lpm.toml"), - toml::to_string(&data_file).expect("We changed it ourselfes, the conversion should work"), - ) - .unwrap(); - - fs::write(project_root.join("main.tex"), main_file).unwrap(); - - Ok(()) + Ok(GeneratedFile::new(project_root.join("main.tex"), main_text)) } diff --git a/src/new/mod.rs b/src/new/mod.rs index 33783c4..a85187c 100644 --- a/src/new/mod.rs +++ b/src/new/mod.rs @@ -1,97 +1,2 @@ pub mod chapter; -pub mod project; pub mod section; - -use std::{ - env, - ffi::OsString, - fs::read_dir, - io::{self, ErrorKind}, - path::PathBuf, -}; - -const SECTION: &'static str = r#"%! TEX root = ../../../main.tex -% LTeX: language=de-DE - -\lesson{REPLACMENT_SECTION_TITLE}{DATE}{} -Dies ist etwas Text -"#; - -const CHAPTER: &'static str = r#"%! TEX root = ../main.tex -% LTeX: language=de-DE - -\chapter{REPLACEMENT_CHAPTER} -"#; - -const TITLE_FILE: &'static str = r#"% LTeX: language=de-DE - -\maketitle -\tableofcontents -\vspace*{\fill} -\makeatletter -Copyright \textcopyright{} \@authors{} \@years{}\\ -\ \\ -Dieses Werk ist lizenziert unter den Bedingungen der CC BY-SA 4.0. -Der Lizenztext ist online unter \url{http://creativecommons.org/licenses/by-sa/4.0/legalcode} abrufbar. -\makeatother -\clearpage -"#; - -const MAIN_FILE: &'static str = r#"%\documentclass[a4paper, 12pt, nosolutions]{report} -\documentclass[a4paper, 12pt]{report} -% LTeX: language=de-DE -\input{headers/preamble.tex} -\input{headers/preamble_local.tex} - - -\title{\textbf{Titel}} -\author{Name\thanks{Beispiel}} -\authors{Name} -\years{2022 - 2023} -\date{\DTMDate{2002-12-4}} - -\includeonly{content/REPLACEMENT_CHAPTER/chapter_01} - -\begin{document} - \input{content/static/title} - - \include{content/REPLACEMENT_CHAPTER/chapter_01} - % NEXT_CHAPTER - - \printbibliography\relax -\end{document} -"#; - -pub fn get_project_root() -> io::Result<PathBuf> { - let path = env::current_dir()?; - let mut path_ancestors = path.as_path().ancestors(); - - while let Some(path_segment) = path_ancestors.next() { - if read_dir(path_segment)?.into_iter().any(|path_segment| { - path_segment - .expect("The read_dir shouldn't error out here") - .file_name() - == OsString::from("lpm.toml") - }) { - return Ok(PathBuf::from(path_segment)); - } - } - Err(io::Error::new( - ErrorKind::NotFound, - "Ran out of places to find lpm.toml", - )) -} - -pub fn get_all_chapters() -> io::Result<Vec<String>> { - let path = get_project_root()?; - let output = read_dir(path.join("content"))? - .map(|path| { - path.expect("The values sholud be fine") - .file_name() - .to_str() - .expect("all names should be valid utf-8") - .to_owned() - }) - .collect(); - Ok(output) -} diff --git a/src/new/project.rs b/src/new/project.rs deleted file mode 100644 index 56edead..0000000 --- a/src/new/project.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::{ - env, - fs::{self, File}, - io::{self, Write}, - path::PathBuf, - process::Command, -}; - -use convert_case::{Case, Casing}; - -use crate::data::Data; - -use super::{CHAPTER, MAIN_FILE, TITLE_FILE}; - -const NEEDED_DIRECTORYS: &'static [&'static str] = - &["build", "content", "headers", "references", "resources"]; - -pub fn generate_new_project( - name: String, - first_chapter: String, - //first_section: String, - preamble_path: PathBuf, - resource_path: PathBuf, -) -> io::Result<()> { - fs::create_dir(&name).unwrap(); - env::set_current_dir(&name).unwrap(); - let project_root = env::current_dir().unwrap(); - - for directory in NEEDED_DIRECTORYS { - fs::create_dir(directory).unwrap(); - } - - // content - env::set_current_dir(project_root.join("content")).unwrap(); - fs::create_dir(&first_chapter.to_case(Case::Snake)).unwrap(); - fs::create_dir("static").unwrap(); - - env::set_current_dir("static").unwrap(); - let mut title_file = File::create("title.tex").unwrap(); - title_file.write_all(TITLE_FILE.as_bytes()).unwrap(); - - env::set_current_dir( - project_root - .join("content") - .join(&first_chapter.to_case(Case::Snake)), - ) - .unwrap(); - fs::create_dir("sections").unwrap(); - let mut chapter_file = File::create("chapter_01.tex").unwrap(); - chapter_file - .write_all( - CHAPTER - .replace("REPLACEMENT_CHAPTER", &first_chapter) - .as_bytes(), - ) - .unwrap(); - - //env::set_current_dir("sections").unwrap(); - //let mut section_file = File::create(format!("{}.tex", &first_section)).unwrap(); - //section_file - // .write_all(SECTION.as_bytes()) - // .unwrap(); - - // headers - env::set_current_dir(project_root.join("headers")).unwrap(); - File::create("preamble_local.tex").unwrap(); - fs::copy(fs::canonicalize(preamble_path).unwrap(), "preamble.tex").unwrap(); - - // resources - env::set_current_dir(project_root.join("resources")).unwrap(); - fs::canonicalize(resource_path) - .unwrap() - .read_dir() - .unwrap() - .map(|path| path.expect("The provided path should work")) - .for_each(|path| { - fs::copy(path.path(), path.file_name()).unwrap(); - }); - - // root files - env::set_current_dir(project_root).unwrap(); - let mut main_file = File::create("main.tex").unwrap(); - main_file - .write_all( - MAIN_FILE - .replace("REPLACEMENT_CHAPTER", &first_chapter.to_case(Case::Snake)) - .as_bytes(), - ) - .unwrap(); - - let data_file = Data { - last_chapter: crate::data::LastChapter { - user_name: first_chapter.to_case(Case::Snake), - number: 1, - }, - }; - let mut lpm_file = File::create("lpm.toml").unwrap(); - lpm_file - .write_all(toml::to_string(&data_file).unwrap().as_bytes()) - .unwrap(); - - Command::new("git") - .arg("init") - .output() - .expect("failed to execute process"); - - let mut lpm_file = File::create(".gitignore").unwrap(); - lpm_file.write_all(b"/build").unwrap(); - - Ok(()) -} diff --git a/src/new/section.rs b/src/new/section.rs index 31742c2..a359fb0 100644 --- a/src/new/section.rs +++ b/src/new/section.rs @@ -1,63 +1,63 @@ -use std::{ - env, - fs::{self, read_dir}, - io::{self, ErrorKind}, - path::PathBuf, - time::SystemTime, -}; +use std::{fs, path::Path, time::SystemTime}; +use anyhow::Context; use chrono::{DateTime, Local}; use convert_case::{Case, Casing}; +use log::debug; -use super::SECTION; +use crate::{ + config_file::Config, + file_tree::{FileTree, GeneratedFile}, +}; -pub fn generate_new_section(name: String) -> io::Result<()> { - let chapter_root = get_section_root()?; - let chapter_main_file_path = read_dir(&chapter_root)? - .into_iter() - .map(|path| path.unwrap()) - .filter(|path| path.file_name().to_str().unwrap().contains("chapter_")) - .last() - .unwrap() - .path(); - let mut main_file = fs::read_to_string(&chapter_main_file_path).unwrap(); +pub fn generate_new_section( + config: &Config, + name: String, + project_root: &Path, + chapter_name: &str, +) -> anyhow::Result<FileTree> { + let chapter_root = project_root.join("content").join(chapter_name); + debug!("Chapter root is: {}", chapter_root.display()); - main_file.push_str(&format!( - "\\input{{content/{}/sections/{}}}\n", - chapter_root.file_name().unwrap().to_str().unwrap(), - &name.to_case(Case::Snake) - )); - fs::write(chapter_main_file_path, main_file)?; - fs::write( - chapter_root.join(format!("sections/{}.tex", name.to_case(Case::Snake))), - SECTION.replace("REPLACMENT_SECTION_TITLE", &name).replace( + let mut file_tree = FileTree::new(); + + let new_section_text = config + .templates + .section + .replace("REPLACMENT_SECTION_TITLE", &name) + .replace( "DATE", &format!( "{}", - DateTime::<Local>::from(SystemTime::now()).format("%Y-%m-%d") + // FIXME: The time is not really precise enough to display the time. <2024-03-31> + DateTime::<Local>::from(SystemTime::now()).format("%Y-%m-%d (%_H:%_S)") ), - ), - )?; - Ok(()) -} + ); + + let new_section_file = GeneratedFile::new( + chapter_root + .join("sections") + .join(format!("{}.tex", name.to_case(Case::Snake))), + new_section_text, + ); + file_tree.add_file(new_section_file); + + let chapter_file_path = chapter_root.join("chapter.tex"); + let mut chapter_file_text = fs::read_to_string(&chapter_file_path).with_context(|| { + format!( + "Failed to read the chapter main file ('{}') to string", + &chapter_file_path.display(), + ) + })?; + + chapter_file_text.push_str(&format!( + "\\input{{content/{}/sections/{}}}\n", + chapter_name, + &name.to_case(Case::Snake) + )); + + let chapter_file = GeneratedFile::new(chapter_file_path, chapter_file_text); + file_tree.add_file(chapter_file); -pub fn get_section_root() -> io::Result<PathBuf> { - let path = env::current_dir()?; - let mut path_ancestors = path.as_path().ancestors(); - - while let Some(path_segment) = path_ancestors.next() { - if read_dir(path_segment)?.into_iter().any(|path| { - path.expect("The read_dir shouldn't error out here") - .file_name() - .to_str() - .unwrap() - .contains("chapter_") - }) { - return Ok(PathBuf::from(path_segment)); - } - } - Err(io::Error::new( - ErrorKind::NotFound, - "Ran out of places to find chapter_root", - )) + Ok(file_tree) } |