feat: allow executing arbitrary programming langs

This commit is contained in:
Matias Fontanini 2024-05-26 10:37:53 -07:00
parent 95ee33f1f2
commit e1e88c780a
10 changed files with 108 additions and 33 deletions

View File

@ -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<Map<&'static str, &'static [u8]>> = Lazy::new(|| Map::from([\n")?;
let mut paths = fs::read_dir("themes")?.collect::<io::Result<Vec<_>>>()?;
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<Map<crate::markdown::elements::CodeLanguage, &'static [u8]>> = Lazy::new(|| Map::from([\n")?;
let mut paths = fs::read_dir("executors")?.collect::<io::Result<Vec<_>>>()?;
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(())
}

3
executors/Python.sh Normal file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
exec python "$1"

View File

@ -1,4 +1,5 @@
use crate::{
execute::CodeExecuter,
input::{
source::Command,
user::{CommandKeyBindings, UserInput},
@ -101,11 +102,13 @@ impl<W: TerminalWrite> ThemesDemo<W> {
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,

View File

@ -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<ExecutionHandle, CodeExecuteError> {
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<ExecutionHandle, CodeExecuteError> {
fn execute_shell<S>(interpreter: &str, code: &[u8], args: &[S]) -> Result<ExecutionHandle, CodeExecuteError>
where
S: AsRef<OsStr>,
{
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<Mutex<ExecutionState>> = 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<ExecutionHandle, CodeExecuteError> {
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<Mutex<ExecutionState>>,
#[allow(dead_code)]
reader_handle: thread::JoinHandle<()>,
program_path: Option<NamedTempFile>,
}
impl ExecutionHandle {

View File

@ -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(),

View File

@ -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,

View File

@ -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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(())

View File

@ -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,

View File

@ -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<usize>,
image_printer: Rc<ImagePrinter>,
@ -59,6 +49,7 @@ impl<'a> Presenter<'a> {
parser: MarkdownParser<'a>,
resources: Resources,
typst: TypstRender,
code_executer: CodeExecuter,
themes: Themes,
image_printer: Rc<ImagePrinter>,
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(),

View File

@ -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<Box<dyn ChunkMutator>>,
slides: Vec<Slide>,
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,