feat: allow the ability to create html outputs

This commit is contained in:
KyleUltimate 2025-04-24 22:26:47 +08:00
parent c3fb212f90
commit 0d4ffceede
3 changed files with 122 additions and 54 deletions

View File

@ -2,7 +2,7 @@ use crate::{
MarkdownParser, Resources, MarkdownParser, Resources,
code::execute::SnippetExecutor, code::execute::SnippetExecutor,
config::{KeyBindingsConfig, PauseExportPolicy}, config::{KeyBindingsConfig, PauseExportPolicy},
export::pdf::PdfRender, export::pdf::{ExportRenderer, Renderer},
markdown::{parse::ParseError, text_style::Color}, markdown::{parse::ParseError, text_style::Color},
presentation::{ presentation::{
Presentation, Presentation,
@ -98,24 +98,12 @@ impl<'a> Exporter<'a> {
Self { parser, default_theme, resources, third_party, code_executor, themes, options, dimensions } Self { parser, default_theme, resources, third_party, code_executor, themes, options, dimensions }
} }
/// Export the given presentation into PDF. fn build_renderer(
/// &mut self,
/// This uses a separate `presenterm-export` tool.
pub fn export_pdf(
mut self,
presentation_path: &Path, presentation_path: &Path,
output_directory: OutputDirectory, output_directory: OutputDirectory,
output_path: Option<&Path>, renderer: Renderer,
) -> Result<(), ExportError> { ) -> Result<ExportRenderer, ExportError> {
println!(
"exporting using rows={}, columns={}, width={}, height={}",
self.dimensions.rows, self.dimensions.columns, self.dimensions.width, self.dimensions.height
);
println!("checking for weasyprint...");
Self::validate_weasyprint_exists()?;
Self::log("weasyprint installation found")?;
let content = fs::read_to_string(presentation_path).map_err(ExportError::ReadPresentation)?; let content = fs::read_to_string(presentation_path).map_err(ExportError::ReadPresentation)?;
let elements = self.parser.parse(&content)?; let elements = self.parser.parse(&content)?;
@ -132,7 +120,7 @@ impl<'a> Exporter<'a> {
.build(elements)?; .build(elements)?;
Self::validate_theme_colors(&presentation)?; Self::validate_theme_colors(&presentation)?;
let mut render = PdfRender::new(self.dimensions, output_directory); let mut render = ExportRenderer::new(self.dimensions.clone(), output_directory, renderer);
Self::log("waiting for images to be generated and code to be executed, if any...")?; Self::log("waiting for images to be generated and code to be executed, if any...")?;
Self::render_async_images(&mut presentation); Self::render_async_images(&mut presentation);
@ -143,10 +131,32 @@ impl<'a> Exporter<'a> {
} }
Self::log("invoking weasyprint...")?; Self::log("invoking weasyprint...")?;
Ok(render)
}
/// Export the given presentation into PDF.
pub fn export_pdf(
mut self,
presentation_path: &Path,
output_directory: OutputDirectory,
output_path: Option<&Path>,
) -> Result<(), ExportError> {
println!(
"exporting using rows={}, columns={}, width={}, height={}",
self.dimensions.rows, self.dimensions.columns, self.dimensions.width, self.dimensions.height
);
println!("checking for weasyprint...");
Self::validate_weasyprint_exists()?;
Self::log("weasyprint installation found")?;
let render = self.build_renderer(presentation_path, output_directory, Renderer::Pdf)?;
let pdf_path = match output_path { let pdf_path = match output_path {
Some(path) => path.to_path_buf(), Some(path) => path.to_path_buf(),
None => presentation_path.with_extension("pdf"), None => presentation_path.with_extension("pdf"),
}; };
render.generate(&pdf_path)?; render.generate(&pdf_path)?;
execute!( execute!(
@ -158,6 +168,36 @@ impl<'a> Exporter<'a> {
Ok(()) Ok(())
} }
/// Export the given presentation into HTML.
pub fn export_html(
mut self,
presentation_path: &Path,
output_directory: OutputDirectory,
output_path: Option<&Path>,
) -> Result<(), ExportError> {
println!(
"exporting using rows={}, columns={}, width={}, height={}",
self.dimensions.rows, self.dimensions.columns, self.dimensions.width, self.dimensions.height
);
let render = self.build_renderer(presentation_path, output_directory, Renderer::Html)?;
let output_path = match output_path {
Some(path) => path.to_path_buf(),
None => presentation_path.with_extension("html"),
};
render.generate(&output_path)?;
execute!(
io::stdout(),
PrintStyledContent(
format!("output file is at {}\n", output_path.display()).stylize().with(Color::Green.into())
)
)?;
Ok(())
}
fn render_async_images(presentation: &mut Presentation) { fn render_async_images(presentation: &mut Presentation) {
let poller = Poller::launch(); let poller = Poller::launch();
let mut pollables = Vec::new(); let mut pollables = Vec::new();

View File

@ -124,17 +124,29 @@ impl ContentManager {
} }
} }
pub(crate) struct PdfRender { pub(crate) enum Renderer {
Pdf,
Html,
}
pub(crate) struct ExportRenderer {
content_manager: ContentManager, content_manager: ContentManager,
output_type: Renderer,
dimensions: WindowSize, dimensions: WindowSize,
html_body: String, html_body: String,
background_color: Option<String>, background_color: Option<String>,
} }
impl PdfRender { impl ExportRenderer {
pub(crate) fn new(dimensions: WindowSize, output_directory: OutputDirectory) -> Self { pub(crate) fn new(dimensions: WindowSize, output_directory: OutputDirectory, output_type: Renderer) -> Self {
let image_manager = ContentManager::new(output_directory); let image_manager = ContentManager::new(output_directory);
Self { content_manager: image_manager, dimensions, html_body: "".to_string(), background_color: None } Self {
content_manager: image_manager,
dimensions,
html_body: "".to_string(),
background_color: None,
output_type,
}
} }
pub(crate) fn process_slide(&mut self, slide: Slide) -> Result<(), ExportError> { pub(crate) fn process_slide(&mut self, slide: Slide) -> Result<(), ExportError> {
@ -154,24 +166,9 @@ impl PdfRender {
Ok(()) Ok(())
} }
pub(crate) fn generate(self, pdf_path: &Path) -> Result<(), ExportError> { pub(crate) fn generate(self, output_path: &Path) -> Result<(), ExportError> {
let html_body = &self.html_body; let html_body = &self.html_body;
let script = include_str!("script.js"); let script = include_str!("script.js");
let html = format!(
r#"
<html>
<head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
{html_body}
<script>
{script}
</script>
</body>
</html>"#
);
let width = (self.dimensions.columns as f64 * FONT_SIZE as f64 * FONT_SIZE_WIDTH).ceil(); let width = (self.dimensions.columns as f64 * FONT_SIZE as f64 * FONT_SIZE_WIDTH).ceil();
let height = self.dimensions.rows * LINE_HEIGHT; let height = self.dimensions.rows * LINE_HEIGHT;
let background_color = self.background_color.unwrap_or_else(|| "black".into()); let background_color = self.background_color.unwrap_or_else(|| "black".into());
@ -220,19 +217,42 @@ impl PdfRender {
width: {width}px; width: {width}px;
}}" }}"
); );
let html = format!(
r#"
<html>
<head>
<style>
{css}
</style>
</head>
<body>
{html_body}
<script>
{script}
</script>
</body>
</html>"#
);
let html_path = self.content_manager.persist_file("index.html", html.as_bytes())?; let html_path = self.content_manager.persist_file("index.html", html.as_bytes())?;
let css_path = self.content_manager.persist_file("styles.css", css.as_bytes())?;
ThirdPartyTools::weasyprint(&[ match self.output_type {
"-s", Renderer::Pdf => {
css_path.to_string_lossy().as_ref(), ThirdPartyTools::weasyprint(&[
"--presentational-hints", "--presentational-hints",
"-e", "-e",
"utf8", "utf8",
html_path.to_string_lossy().as_ref(), html_path.to_string_lossy().as_ref(),
pdf_path.to_string_lossy().as_ref(), output_path.to_string_lossy().as_ref(),
]) ])
.run()?; .run()?;
}
Renderer::Html => {
fs::write(output_path, html.as_bytes())?;
}
}
Ok(()) Ok(())
} }
} }

View File

@ -68,15 +68,19 @@ struct Cli {
path: Option<PathBuf>, path: Option<PathBuf>,
/// Export the presentation as a PDF rather than displaying it. /// Export the presentation as a PDF rather than displaying it.
#[clap(short, long)] #[clap(short, long, group = "export")]
export_pdf: bool, export_pdf: bool,
/// Export the presentation as a HTML rather than displaying it.
#[clap(short, long, group = "export")]
export_html: bool,
/// The path in which to store temporary files used when exporting. /// The path in which to store temporary files used when exporting.
#[clap(long, requires = "export_pdf")] #[clap(long, requires = "export")]
export_temporary_path: Option<PathBuf>, export_temporary_path: Option<PathBuf>,
/// The output path for the exported PDF. /// The output path for the exported PDF.
#[clap(short = 'o', long = "output", requires = "export_pdf")] #[clap(short = 'o', long = "output", requires = "export")]
export_output: Option<PathBuf>, export_output: Option<PathBuf>,
/// Generate a JSON schema for the configuration file. /// Generate a JSON schema for the configuration file.
@ -401,7 +405,7 @@ fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
let parser = MarkdownParser::new(&arena); let parser = MarkdownParser::new(&arena);
let validate_overflows = let validate_overflows =
overflow_validation_enabled(&present_mode, &config.defaults.validate_overflows) || cli.validate_overflows; overflow_validation_enabled(&present_mode, &config.defaults.validate_overflows) || cli.validate_overflows;
if cli.export_pdf { if cli.export_pdf || cli.export_html {
let dimensions = match config.export.dimensions { let dimensions = match config.export.dimensions {
Some(dimensions) => WindowSize { Some(dimensions) => WindowSize {
rows: dimensions.rows, rows: dimensions.rows,
@ -426,7 +430,11 @@ fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
Some(path) => OutputDirectory::external(path), Some(path) => OutputDirectory::external(path),
None => OutputDirectory::temporary(), None => OutputDirectory::temporary(),
}?; }?;
exporter.export_pdf(&path, output_directory, cli.export_output.as_deref())?; if cli.export_pdf {
exporter.export_pdf(&path, output_directory, cli.export_output.as_deref())?;
} else {
exporter.export_html(&path, output_directory, cli.export_output.as_deref())?;
}
} else { } else {
let SpeakerNotesComponents { events_listener, events_publisher } = let SpeakerNotesComponents { events_listener, events_publisher } =
SpeakerNotesComponents::new(&cli, &config, &path)?; SpeakerNotesComponents::new(&cli, &config, &path)?;