diff options
-rw-r--r-- | Cargo.lock | 39 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/bundle/mod.rs | 115 | ||||
-rw-r--r-- | src/cli.rs | 8 | ||||
-rw-r--r-- | src/file_tree/mod.rs | 3 | ||||
-rw-r--r-- | src/main.rs | 26 |
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(()) } |