summary refs log tree commit diff stats
path: root/src/bundle/mod.rs
blob: ee395372def527612ce0c69a923cdf634fadefd5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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)
}