summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock39
-rw-r--r--Cargo.toml1
-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
6 files changed, 184 insertions, 8 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 28669f9..2d8d7ed 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,15 @@
 version = 3
 
 [[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
 name = "android-tzdata"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -285,6 +294,7 @@ dependencies = [
  "convert_case",
  "deunicode",
  "log",
+ "regex",
  "serde",
  "serde_derive",
  "stderrlog",
@@ -331,6 +341,35 @@ dependencies = [
 ]
 
 [[package]]
+name = "regex"
+version = "1.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
+
+[[package]]
 name = "serde"
 version = "1.0.210"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 036767f..e39e7a7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,7 @@ clap = { version = "4.5.17", features = ["derive"] }
 convert_case = "0.6.0"
 deunicode = "1.6.0"
 log = "0.4.22"
+regex = "1.10.6"
 serde = { version = "1.0.210", features = ["derive"] }
 serde_derive = "1.0.210"
 stderrlog = "0.6.0"
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(())
 }