summary refs log tree commit diff stats
path: root/src/bundle/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bundle/mod.rs')
-rw-r--r--src/bundle/mod.rs115
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)
+}