rust-beam 0.3.0

A LaTeX slide generator you can write in faster than beamer.
use std::collections::VecDeque;
use std::{env, fmt::Write, fs, path::Path, process::Command};

#[derive(Default, PartialEq)]
struct Slide {
    title: Option<String>,
    // Background image
    image: Option<String>,
    lines: Vec<String>,
}

impl Slide {
    fn as_tex(&self, fragile: bool) -> Result<String, std::fmt::Error> {
        let mut tex: String = String::new();
        let mut current_env: Vec<String> = Vec::new();
        let mut prev_leader: Option<char> = None;

        macro_rules! format_env {
            ($env_name: literal, $title: literal, $line_prefix: literal) => {{
                let mut iter = current_env.iter();
                if $title {
                    write!(
                        &mut tex,
                        "\\begin{{{}}}{{{}}}\n",
                        $env_name,
                        iter.next().unwrap()
                    )?;
                } else {
                    write!(&mut tex, "\\begin{{{}}}", $env_name)?;
                }
                for line in iter {
                    tex.push('\t');
                    tex.push_str($line_prefix);
                    tex.push_str(line);
                    tex.push('\n');
                }
                write!(&mut tex, "\\end{{{}}}\n", $env_name)?;
            }};
        }

        macro_rules! push_env {
            () => {
                match prev_leader {
                    Some(prev_leader) => match prev_leader {
                        '-' => format_env!("itemize", false, "\\item "),
                        '>' => format_env!("block", true, ""),
                        '<' => format_env!("exampleblock", true, ""),
                        '!' => format_env!("alertblock", true, ""),
                        _ => panic!("Somehow the previous leader is '{}', even when it should be \"None\"", prev_leader)
                    },
                    None => {}
                }
            };
        }

        for line in &self.lines {
            match line.chars().next() {
                Some(leader) => {
                    if Some(leader) != prev_leader {
                        push_env!();
                        current_env = Vec::new();
                    }
                    if ['-', '>', '<', '!'].contains(&leader) {
                        current_env.push(line[1..].to_string());
                        prev_leader = Some(leader);
                    } else {
                        tex.push_str(line);
                        tex.push('\n');
                        prev_leader = None;
                    }
                }
                None => {
                    push_env!();
                    current_env = Vec::new();
                    tex.push('\n');
                    prev_leader = None;
                }
            }
        }

        push_env!();

        match &self.title {
            Some(title) => {
                tex.insert_str(0, &format!("\\begin{{frame}}{}{{{}}}\n", if fragile {"[fragile]"} else {""}, title));
                tex.push_str(r#"\end{frame}"#);
            }
            None => {
                tex.insert_str(0, &format!("\\begin{{frame}}{}\n", if fragile {"[fragile]"} else {""}));
                tex.push_str(r#"\end{frame}"#);
            }
        }

        match &self.image {
            Some(image) => {
                let mut prepend_str = String::from(
                    r#"{\setbeamertemplate{background}{\includegraphics[width=\paperwidth,height=\paperheight]{../"#,
                );
                prepend_str.push_str(image);
                prepend_str.push_str("}}\n");

                tex.insert_str(0, &prepend_str);
                tex.push_str("\n}");
            }
            None => {}
        }

        Ok(tex)
    }

    fn is_empty(&self) -> bool {
        self == &Slide::default()
    }
}

enum Content {
    Section(String),
    Slide(Slide),
}

struct Presentation {
    preamble: Vec<String>,
    /// Whether frame environments should have the [fragile] option passed to them.
    fragile: bool,
    contents: Vec<Content>,
}

impl Presentation {
    fn as_tex(&self) -> Result<String, std::fmt::Error> {
        let mut tex: String = String::new();
        for line in &self.preamble {
            tex.push_str(line);
            tex.push('\n');
        }
        tex.push('\n');
        tex.push_str(r#"\begin{document}"#);
        tex.push('\n');
        for content in &self.contents {
            match content {
                Content::Section(title) => {
                    write!(&mut tex, "\\section{{{}}}", title)?;
                }
                Content::Slide(slide) => match slide.as_tex(self.fragile) {
                    Ok(slide_tex) => tex.push_str(&slide_tex),
                    Err(e) => panic!("Internal error with write! macro occurred when converting Slide to tex: {}", e),
                },
            }
            tex.push('\n');
            tex.push('\n');
        }
        tex.push_str(r#"\end{document}"#);
        tex.push('\n');
        Ok(tex)
    }
}

// Parses the contents of a .beam file into an instance of the Presentation struct

fn beam_to_presentation(string: &str, fragile: bool) -> Presentation {
    let mut preamble: Vec<String> = Vec::new();
    let mut contents: Vec<Content> = Vec::new();
    let mut current_slide: Slide = Slide::default();

    macro_rules! push_slide {
        () => {
            if !current_slide.is_empty() {
                contents.push(Content::Slide(current_slide));
                current_slide = Slide::default();
            }
        };
    }

    for (num, line) in string.split('\n').enumerate() {
        if let Some(c) = line.chars().next() {
            match c {
                '#' => {
                    push_slide!();
                    contents.push(Content::Section(line[1..].to_string()));
                }
                '^' => {
                    preamble.push(String::from(line[1..].trim()));
                }
                '~' => {
                    push_slide!();
                    let trimmed_title = line[1..].trim();
                    current_slide.title = if !trimmed_title.is_empty() {
                        Some(String::from(trimmed_title))
                    } else {
                        None
                    };
                }
                '@' => {
                    if current_slide.image.is_some() {
                        panic!(
                            "Background image set twice for the same slide in line {}",
                            num
                        );
                    } else {
                        current_slide.image = Some(String::from(line[1..].trim()));
                    }
                }
                '*' => {
                    current_slide.lines.push(format!("\n\\begin{{center}}\n\t\\includegraphics[width=6cm]{{../{}}}\n\\end{{center}}", line[1..].trim()));
                }
                '%' => {}
                _ => {
                    current_slide.lines.push(String::from(line));
                }
            }
        } else {
            current_slide.lines.push(String::new());
        }
    }
    push_slide!();
    Presentation { preamble, fragile, contents }
}

fn main() {
    let mut args: VecDeque<String> = env::args().collect();
    args.pop_front();

    let fragile = !args.is_empty() && args[0] == "--fragile";
    if fragile {
        args.pop_front();
    }

    for arg in &args {
        let build_dir = Path::new("build");
        let name = if arg.len() > 5 && &arg[arg.len() - 5..] == ".beam" {
            &arg[..arg.len() - 5]
        } else {
            arg
        };
        let tex_name = &format!("{}.tex", name);
        let pdf_name = &format!("{}.pdf", name);
        if build_dir.exists() && !build_dir.is_dir() {
            panic!("Path \"build\" exists but is not a directory");
        } else if !build_dir.exists() {
            fs::create_dir(build_dir).expect("Fialed to create directory \"build\"");
        }
        match fs::read_to_string(&format!("{}.beam", name)) {
            Ok(string) => {
                env::set_current_dir(build_dir)
                    .expect("Failed to set current working directory to \"build\"");
                match beam_to_presentation(&string, fragile).as_tex() {
                    Ok(tex) => fs::write(tex_name, tex),
                    Err(e) => panic!("Internal error with write! macro occurred when converting Presentation to tex: {}", e)
                }
                    .unwrap_or_else(|_| panic!("Failed to write to {}.tex", arg));
                let latexmk = Command::new("latexmk").args(["-pdf", "-f", tex_name]).output().expect("\"latexmk\" could not be executed. Check that it is installed, in PATH, and executable");
                if !latexmk.status.success() {
                    println!("Failed to compile {}", tex_name);
                }
                env::set_current_dir("..").expect("Failed to reset current working directory");
                if latexmk.status.success() {
                    fs::rename(build_dir.join(pdf_name), pdf_name).unwrap_or_else(|_| {
                        panic!(
                            "Could not rename {:?} to {}",
                            build_dir.join(pdf_name),
                            pdf_name
                        )
                    });
                }
            }
            Err(e) => panic!("Path \"{}\" could not be read\n{}", arg, e),
        }
    }
}