use std::{fs, path::Path}; use anyhow::{Context, Result}; use log::error; use regex::{Captures, Regex}; use crate::config_file::Config; /// From the `regex` documentation fn replace_all( re: &Regex, haystack: &str, replacement: impl Fn(&Captures) -> anyhow::Result, ) -> Result { let mut new = String::with_capacity(haystack.len()); let mut last_match = 0; for caps in re.captures_iter(haystack) { let m = caps.get(0).unwrap(); new.push_str(&haystack[last_match..m.start()]); new.push_str(&replacement(&caps)?); last_match = m.end(); } new.push_str(&haystack[last_match..]); Ok(new) } pub fn bundle(config: Config, project_root: &Path) -> Result { let main_path = project_root.join(&config.main_file); let output = bundle_rec(&main_path, project_root) .with_context(|| format!("Failed to bundle main file ('{}')", config.main_file))?; Ok(output) } /// This inlines all `\input`s and `\include`s in the file. fn bundle_rec(file_path: &Path, project_root: &Path) -> Result { let file = fs::read_to_string(file_path) .with_context(|| format!("Failed to read file: '{}'", file_path.display()))?; let mut output = file; let mut changed = true; let re = Regex::new(r"\\input\{(.*)\}|\\include\{(.*)\}").unwrap(); while re.is_match(&output) && changed { changed = false; // Bundle `\input`s let re = Regex::new(r"\\input\{(.*)\}").unwrap(); let orig_output = output.clone(); output = replace_all(&re, &output, |cap| replace_path_input(cap, &project_root)) .with_context(|| { format!( "Failed to replace a \\input occurence in '{}'", file_path.display() ) })?; if orig_output != output { changed = true; } // Bundle `\include`s let re = Regex::new(r"\\include\{(.*)\}").unwrap(); let orig_output = output.clone(); output = replace_all(&re, &output, |cap| replace_path_include(cap, &project_root)) .with_context(|| { format!( "Failed to replace a \\include occurence in '{}'", file_path.display() ) })?; if orig_output != output { changed = true; } if !changed { error!("Nothing changed! It looks like something is wrong with your file.") } } Ok(output) } fn replace_path(capture: &Captures, project_root: &Path) -> Result { let (_, [orig_path]) = capture.extract(); let base_path = project_root.join(orig_path); let path = if base_path.exists() { base_path } else { // `\input`s and `\include`s can also automatically add the `.tex` let mut out = base_path.clone(); out.set_extension("tex"); out }; let mut value = fs::read_to_string(&path) .with_context(|| format!("Failed to read file: '{}'", path.display()))?; if value.ends_with('\n') { value = value.strip_suffix('\n').expect("We checked").to_owned(); } Ok(value) } fn replace_path_include(capture: &Captures, project_root: &Path) -> Result { let value = replace_path(capture, project_root)?; // TODO: This is not exactly what include does, but it should approximate it rather well <2024-09-16> Ok(format!("\\clearpage{{}}\n{}\n\\clearpage{{}}", value)) } fn replace_path_input(capture: &Captures, project_root: &Path) -> Result { replace_path(capture, project_root) }