From e1e88c780a38fef86afcd633fc3608499f1110c0 Mon Sep 17 00:00:00 2001 From: Matias Fontanini Date: Sun, 26 May 2024 10:37:53 -0700 Subject: [PATCH] feat: allow executing arbitrary programming langs --- build.rs | 41 +++++++++++++++++++++++++++++++++++++-- executors/Python.sh | 3 +++ src/demo.rs | 3 +++ src/execute.rs | 41 +++++++++++++++++++++++++++++---------- src/export.rs | 2 ++ src/lib.rs | 1 + src/main.rs | 10 ++++++---- src/markdown/elements.rs | 4 ++-- src/presenter.rs | 19 ++++++------------ src/processing/builder.rs | 17 ++++++++++++++-- 10 files changed, 108 insertions(+), 33 deletions(-) create mode 100644 executors/Python.sh diff --git a/build.rs b/build.rs index 96208d5..0790fcb 100644 --- a/build.rs +++ b/build.rs @@ -6,13 +6,13 @@ use std::{ // Take all files under `themes` and turn them into a file that contains a hashmap with their // contents by name. This is pulled in theme.rs to construct themes. -fn main() -> io::Result<()> { - let out_dir = env::var("OUT_DIR").unwrap(); +fn build_themes(out_dir: &str) -> io::Result<()> { let output_path = format!("{out_dir}/themes.rs"); let mut output_file = BufWriter::new(File::create(output_path)?); output_file.write_all(b"use std::collections::BTreeMap as Map;\n")?; output_file.write_all(b"use once_cell::sync::Lazy;\n")?; output_file.write_all(b"static THEMES: Lazy> = Lazy::new(|| Map::from([\n")?; + let mut paths = fs::read_dir("themes")?.collect::>>()?; paths.sort_by_key(|e| e.path()); for theme_file in paths { @@ -33,3 +33,40 @@ fn main() -> io::Result<()> { println!("cargo:rerun-if-changed=themes"); Ok(()) } + +fn build_executors(out_dir: &str) -> io::Result<()> { + let output_path = format!("{out_dir}/executors.rs"); + let mut output_file = BufWriter::new(File::create(output_path)?); + output_file.write_all(b"use std::collections::BTreeMap as Map;\n")?; + output_file.write_all(b"use once_cell::sync::Lazy;\n")?; + output_file.write_all(b"static EXECUTORS: Lazy> = Lazy::new(|| Map::from([\n")?; + + let mut paths = fs::read_dir("executors")?.collect::>>()?; + paths.sort_by_key(|e| e.path()); + for file in paths { + let metadata = file.metadata()?; + if !metadata.is_file() { + panic!("found non file in executors directory"); + } + let path = file.path(); + let contents = fs::read(&path)?; + let file_name = path.file_name().unwrap().to_string_lossy(); + let executor_name = file_name.split_once('.').unwrap().0; + output_file.write_all( + format!("(crate::markdown::elements::CodeLanguage::{executor_name}, {contents:?}.as_slice()),\n") + .as_bytes(), + )?; + } + output_file.write_all(b"]));\n")?; + + // Rebuild if anything changes. + println!("cargo:rerun-if-changed=executors"); + Ok(()) +} + +fn main() -> io::Result<()> { + let out_dir = env::var("OUT_DIR").unwrap(); + build_themes(&out_dir)?; + build_executors(&out_dir)?; + Ok(()) +} diff --git a/executors/Python.sh b/executors/Python.sh new file mode 100644 index 0000000..0ab391c --- /dev/null +++ b/executors/Python.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +exec python "$1" diff --git a/src/demo.rs b/src/demo.rs index 0114947..69b11cf 100644 --- a/src/demo.rs +++ b/src/demo.rs @@ -1,4 +1,5 @@ use crate::{ + execute::CodeExecuter, input::{ source::Command, user::{CommandKeyBindings, UserInput}, @@ -101,11 +102,13 @@ impl ThemesDemo { let mut resources = Resources::new("non_existent", image_registry.clone()); let mut typst = TypstRender::default(); let options = PresentationBuilderOptions::default(); + let executer = CodeExecuter; let bindings_config = Default::default(); let builder = PresentationBuilder::new( theme, &mut resources, &mut typst, + &executer, &self.themes, image_registry, bindings_config, diff --git a/src/execute.rs b/src/execute.rs index 9c46699..f31bf13 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -2,6 +2,7 @@ use crate::markdown::elements::{Code, CodeLanguage}; use std::{ + ffi::OsStr, io::{self, BufRead, BufReader, Write}, process::{self, Stdio}, sync::{Arc, Mutex}, @@ -9,37 +10,46 @@ use std::{ }; use tempfile::NamedTempFile; +include!(concat!(env!("OUT_DIR"), "/executors.rs")); + /// Allows executing code. -pub(crate) struct CodeExecuter; +pub struct CodeExecuter; impl CodeExecuter { pub(crate) fn is_execution_supported(&self, language: &CodeLanguage) -> bool { - matches!(language, CodeLanguage::Shell(_)) + if matches!(language, CodeLanguage::Shell(_)) { true } else { EXECUTORS.contains_key(language) } } /// Execute a piece of code. pub(crate) fn execute(&self, code: &Code) -> Result { - if !self.is_execution_supported(&code.language) { - return Err(CodeExecuteError::UnsupportedExecution); - } if !code.attributes.execute { return Err(CodeExecuteError::NotExecutableCode); } match &code.language { - CodeLanguage::Shell(interpreter) => Self::execute_shell(interpreter, &code.contents), - _ => Err(CodeExecuteError::UnsupportedExecution), + CodeLanguage::Shell(interpreter) => { + let args: &[&str] = &[]; + Self::execute_shell(interpreter, code.contents.as_bytes(), args) + } + lang => { + let executor = EXECUTORS.get(lang).ok_or(CodeExecuteError::UnsupportedExecution)?; + Self::execute_lang(executor, code.contents.as_bytes()) + } } } - fn execute_shell(interpreter: &str, code: &str) -> Result { + fn execute_shell(interpreter: &str, code: &[u8], args: &[S]) -> Result + where + S: AsRef, + { let mut output_file = NamedTempFile::new().map_err(CodeExecuteError::TempFile)?; - output_file.write_all(code.as_bytes()).map_err(CodeExecuteError::TempFile)?; + output_file.write_all(code).map_err(CodeExecuteError::TempFile)?; output_file.flush().map_err(CodeExecuteError::TempFile)?; let (reader, writer) = os_pipe::pipe().map_err(CodeExecuteError::Pipe)?; let writer_clone = writer.try_clone().map_err(CodeExecuteError::Pipe)?; let process_handle = process::Command::new("/usr/bin/env") .arg(interpreter) .arg(output_file.path()) + .args(args) .stdin(Stdio::null()) .stdout(writer) .stderr(writer_clone) @@ -48,7 +58,17 @@ impl CodeExecuter { let state: Arc> = Default::default(); let reader_handle = ProcessReader::spawn(process_handle, state.clone(), output_file, reader); - let handle = ExecutionHandle { state, reader_handle }; + let handle = ExecutionHandle { state, reader_handle, program_path: None }; + Ok(handle) + } + + fn execute_lang(executor: &[u8], code: &[u8]) -> Result { + let mut code_file = NamedTempFile::new().map_err(CodeExecuteError::TempFile)?; + code_file.write_all(code).map_err(CodeExecuteError::TempFile)?; + + let path = code_file.path(); + let mut handle = Self::execute_shell("bash", executor, &[path])?; + handle.program_path = Some(code_file); Ok(handle) } } @@ -78,6 +98,7 @@ pub(crate) struct ExecutionHandle { state: Arc>, #[allow(dead_code)] reader_handle: thread::JoinHandle<()>, + program_path: Option, } impl ExecutionHandle { diff --git a/src/export.rs b/src/export.rs index 0be027d..6fcb430 100644 --- a/src/export.rs +++ b/src/export.rs @@ -1,5 +1,6 @@ use crate::{ custom::KeyBindingsConfig, + execute::CodeExecuter, markdown::parse::ParseError, media::{ image::{Image, ImageSource}, @@ -83,6 +84,7 @@ impl<'a> Exporter<'a> { self.default_theme, &mut self.resources, &mut self.typst, + &CodeExecuter, &self.themes, Default::default(), KeyBindingsConfig::default(), diff --git a/src/lib.rs b/src/lib.rs index 212eaab..cc2ac64 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ pub(crate) mod typst; pub use crate::{ custom::{Config, ImageProtocol, ValidateOverflows}, demo::ThemesDemo, + execute::CodeExecuter, export::{ExportError, Exporter}, input::source::CommandSource, markdown::parse::MarkdownParser, diff --git a/src/main.rs b/src/main.rs index 60a974e..d7e45df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,9 @@ use clap::{error::ErrorKind, CommandFactory, Parser}; use comrak::Arena; use directories::ProjectDirs; use presenterm::{ - CommandSource, Config, Exporter, GraphicsMode, HighlightThemeSet, ImagePrinter, ImageProtocol, ImageRegistry, - LoadThemeError, MarkdownParser, PresentMode, PresentationBuilderOptions, PresentationTheme, PresentationThemeSet, - Presenter, PresenterOptions, Resources, Themes, ThemesDemo, TypstRender, ValidateOverflows, + CodeExecuter, CommandSource, Config, Exporter, GraphicsMode, HighlightThemeSet, ImagePrinter, ImageProtocol, + ImageRegistry, LoadThemeError, MarkdownParser, PresentMode, PresentationBuilderOptions, PresentationTheme, + PresentationThemeSet, Presenter, PresenterOptions, Resources, Themes, ThemesDemo, TypstRender, ValidateOverflows, }; use std::{ env, io, @@ -206,6 +206,7 @@ fn run(mut cli: Cli) -> Result<(), Box> { let registry = ImageRegistry(printer.clone()); let resources = Resources::new(resources_path, registry.clone()); let typst = TypstRender::new(config.typst.ppi, registry, resources_path); + let code_executer = CodeExecuter; if cli.export_pdf || cli.generate_pdf_metadata { let mut exporter = Exporter::new(parser, &default_theme, resources, typst, themes, options); let mut args = Vec::new(); @@ -232,7 +233,8 @@ fn run(mut cli: Cli) -> Result<(), Box> { bindings: config.bindings, validate_overflows, }; - let presenter = Presenter::new(&default_theme, commands, parser, resources, typst, themes, printer, options); + let presenter = + Presenter::new(&default_theme, commands, parser, resources, typst, code_executer, themes, printer, options); presenter.present(&path)?; } Ok(()) diff --git a/src/markdown/elements.rs b/src/markdown/elements.rs index 4bed5fe..8e0f263 100644 --- a/src/markdown/elements.rs +++ b/src/markdown/elements.rs @@ -186,8 +186,8 @@ pub(crate) struct Code { } /// The language of a piece of code. -#[derive(Clone, Debug, PartialEq, Eq, EnumIter)] -pub(crate) enum CodeLanguage { +#[derive(Clone, Debug, PartialEq, Eq, EnumIter, PartialOrd, Ord)] +pub enum CodeLanguage { Ada, Asp, Awk, diff --git a/src/presenter.rs b/src/presenter.rs index fd65a21..b21f56a 100644 --- a/src/presenter.rs +++ b/src/presenter.rs @@ -1,20 +1,9 @@ use crate::{ - custom::KeyBindingsConfig, - diff::PresentationDiffer, - export::ImageReplacer, - input::source::{Command, CommandSource}, - markdown::parse::{MarkdownParser, ParseError}, - media::{printer::ImagePrinter, register::ImageRegistry}, - presentation::Presentation, - processing::builder::{BuildError, PresentationBuilder, PresentationBuilderOptions, Themes}, - render::{ + custom::KeyBindingsConfig, diff::PresentationDiffer, execute::CodeExecuter, export::ImageReplacer, input::source::{Command, CommandSource}, markdown::parse::{MarkdownParser, ParseError}, media::{printer::ImagePrinter, register::ImageRegistry}, presentation::Presentation, processing::builder::{BuildError, PresentationBuilder, PresentationBuilderOptions, Themes}, render::{ draw::{RenderError, RenderResult, TerminalDrawer}, properties::WindowSize, validate::OverflowValidator, - }, - resource::Resources, - theme::PresentationTheme, - typst::TypstRender, + }, resource::Resources, theme::PresentationTheme, typst::TypstRender }; use std::{ collections::HashSet, @@ -43,6 +32,7 @@ pub struct Presenter<'a> { parser: MarkdownParser<'a>, resources: Resources, typst: TypstRender, + code_executer: CodeExecuter, state: PresenterState, slides_with_pending_widgets: HashSet, image_printer: Rc, @@ -59,6 +49,7 @@ impl<'a> Presenter<'a> { parser: MarkdownParser<'a>, resources: Resources, typst: TypstRender, + code_executer: CodeExecuter, themes: Themes, image_printer: Rc, options: PresenterOptions, @@ -69,6 +60,7 @@ impl<'a> Presenter<'a> { parser, resources, typst, + code_executer, state: PresenterState::Empty, slides_with_pending_widgets: HashSet::new(), image_printer, @@ -257,6 +249,7 @@ impl<'a> Presenter<'a> { self.default_theme, &mut self.resources, &mut self.typst, + &self.code_executer, &self.themes, ImageRegistry(self.image_printer.clone()), self.options.bindings.clone(), diff --git a/src/processing/builder.rs b/src/processing/builder.rs index db52f57..34d66a5 100644 --- a/src/processing/builder.rs +++ b/src/processing/builder.rs @@ -1,5 +1,6 @@ use crate::{ custom::{KeyBindingsConfig, OptionsConfig}, + execute::CodeExecuter, markdown::{ elements::{ Code, CodeLanguage, Highlight, HighlightGroup, ListItem, ListItemType, MarkdownElement, ParagraphElement, @@ -94,6 +95,7 @@ pub(crate) struct PresentationBuilder<'a> { chunk_mutators: Vec>, slides: Vec, highlighter: CodeHighlighter, + code_executer: &'a CodeExecuter, theme: Cow<'a, PresentationTheme>, resources: &'a mut Resources, typst: &'a mut TypstRender, @@ -113,6 +115,7 @@ impl<'a> PresentationBuilder<'a> { default_theme: &'a PresentationTheme, resources: &'a mut Resources, typst: &'a mut TypstRender, + code_executer: &'a CodeExecuter, themes: &'a Themes, image_registry: ImageRegistry, bindings_config: KeyBindingsConfig, @@ -124,6 +127,7 @@ impl<'a> PresentationBuilder<'a> { chunk_mutators: Vec::new(), slides: Vec::new(), highlighter: CodeHighlighter::default(), + code_executer, theme: Cow::Borrowed(default_theme), resources, typst, @@ -680,7 +684,7 @@ impl<'a> PresentationBuilder<'a> { self.chunk_mutators.push(Box::new(HighlightMutator::new(context))); } if code.attributes.execute { - self.push_code_execution(code); + self.push_code_execution(code)?; } Ok(()) } @@ -728,7 +732,10 @@ impl<'a> PresentationBuilder<'a> { (output, context) } - fn push_code_execution(&mut self, code: Code) { + fn push_code_execution(&mut self, code: Code) -> Result<(), BuildError> { + if !self.code_executer.is_execution_supported(&code.language) { + return Err(BuildError::UnsupportedExecution(code.language)); + } let operation = RunCodeOperation::new( code, self.theme.default_style.colors.clone(), @@ -736,6 +743,7 @@ impl<'a> PresentationBuilder<'a> { ); let operation = RenderOperation::RenderOnDemand(Rc::new(operation)); self.chunk_operations.push(operation); + Ok(()) } fn terminate_slide(&mut self) { @@ -898,6 +906,9 @@ pub enum BuildError { #[error("typst render failed: {0}")] TypstRender(#[from] TypstRenderError), + + #[error("language {0:?} does not support execution")] + UnsupportedExecution(CodeLanguage), } #[derive(Debug, Clone, PartialEq, Deserialize)] @@ -1050,12 +1061,14 @@ mod test { let theme = PresentationTheme::default(); let mut resources = Resources::new("/tmp", Default::default()); let mut typst = TypstRender::default(); + let code_executer = CodeExecuter; let themes = Themes::default(); let bindings = KeyBindingsConfig::default(); let builder = PresentationBuilder::new( &theme, &mut resources, &mut typst, + &code_executer, &themes, Default::default(), bindings,