diff options
Diffstat (limited to 'src/bundle/mod.rs')
-rw-r--r-- | src/bundle/mod.rs | 115 |
1 files changed, 115 insertions, 0 deletions
diff --git a/src/bundle/mod.rs b/src/bundle/mod.rs new file mode 100644 index 0000000..ee39537 --- /dev/null +++ b/src/bundle/mod.rs @@ -0,0 +1,115 @@ +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<String>, +) -> Result<String> { + 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<String> { + 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<String> { + 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<String> { + 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<String> { + 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<String> { + replace_path(capture, project_root) +} |