From b59cc730fdbcc81752d6c9b23ac00a3b0aff1c8a Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Mon, 16 Sep 2024 18:41:51 +0200 Subject: feat(bundle): Support bundling a document into one TeX file --- src/bundle/mod.rs | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/cli.rs | 8 ++++ src/file_tree/mod.rs | 3 +- src/main.rs | 26 +++++++++--- 4 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 src/bundle/mod.rs (limited to 'src') 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, +) -> Result { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(()) } -- cgit 1.4.1