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