aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-09-16 18:41:51 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-09-16 18:41:51 +0200
commitb59cc730fdbcc81752d6c9b23ac00a3b0aff1c8a (patch)
tree9be6b3004c259d8afe08dce170fa04c6d3265f35 /src
parentchore(references): Add testing data (diff)
downloadlpm-b59cc730fdbcc81752d6c9b23ac00a3b0aff1c8a.zip
feat(bundle): Support bundling a document into one TeX file
Diffstat (limited to 'src')
-rw-r--r--src/bundle/mod.rs115
-rw-r--r--src/cli.rs8
-rw-r--r--src/file_tree/mod.rs3
-rw-r--r--src/main.rs26
4 files changed, 144 insertions, 8 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)
+}
diff --git a/src/cli.rs b/src/cli.rs
index 9fee349..b3ee30c 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -21,6 +21,14 @@ pub enum Command {
/// Generates a new part
#[command(subcommand)]
New(What),
+
+ /// Bundle the project into one TeX file
+ ///
+ /// This command has some known issues and invariants:
+ /// - It does not consider, that `\include`s or `\input`s could be commented out.
+ /// - It does also not consider the case, where `\include` or `\input` have been re-defined
+ #[command(verbatim_doc_comment)]
+ Bundle,
}
#[derive(Subcommand, Debug)]
diff --git a/src/file_tree/mod.rs b/src/file_tree/mod.rs
index 96637b3..84f5ad2 100644
--- a/src/file_tree/mod.rs
+++ b/src/file_tree/mod.rs
@@ -24,8 +24,7 @@
//! you.
use std::{
- fs::{self, File},
- io,
+ fs::{self},
path::{Path, PathBuf},
};
diff --git a/src/main.rs b/src/main.rs
index 8d1c977..5ea9a74 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,18 +2,19 @@ use std::{env, ffi::OsString, fs, path::PathBuf};
use anyhow::{bail, Context};
use clap::Parser;
+use cli::Command;
use log::debug;
use crate::{
cli::{
Args,
- Command::New,
What::{Chapter, Section},
},
config_file::Config,
new::{chapter::generate_new_chapter, section::generate_new_section},
};
+pub mod bundle;
pub mod cli;
pub mod config_file;
pub mod new;
@@ -38,8 +39,14 @@ fn main() -> anyhow::Result<()> {
let config_file = fs::read_to_string(project_root.join("lpm.toml"))?;
let config: Config = toml::from_str(&config_file).context("Reading toml from string")?;
- let file_tree = match args.cli {
- New(new_command) => match new_command {
+ let maybe_file_tree = match args.cli {
+ Command::Bundle => {
+ let output = bundle::bundle(config, &project_root)?;
+
+ print!("{}", output);
+ None
+ }
+ Command::New(new_command) => match new_command {
Section { name, chapter } => {
let chapter = if let Some(val) = chapter {
// The user probably has not added the preceeding chapter number to the chapter
@@ -61,13 +68,20 @@ fn main() -> anyhow::Result<()> {
get_upwards_chapter()?
};
- generate_new_section(&config, name, &project_root, &chapter)?
+ Some(generate_new_section(
+ &config,
+ name,
+ &project_root,
+ &chapter,
+ )?)
}
- Chapter { name } => generate_new_chapter(config, &project_root, name)?,
+ Chapter { name } => Some(generate_new_chapter(config, &project_root, name)?),
},
};
- file_tree.materialize()?;
+ if let Some(file_tree) = maybe_file_tree {
+ file_tree.materialize()?;
+ }
Ok(())
}