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)
}