use std::collections::VecDeque;
use std::{env, fmt::Write, fs, path::Path, process::Command};
#[derive(Default, PartialEq)]
struct Slide {
title: Option<String>,
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>,
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)
}
}
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),
}
}
}