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,
code::execute::SnippetExecutor,
config::{KeyBindingsConfig, PauseExportPolicy},
export::pdf::PdfRender,
export::pdf::{ExportRenderer, Renderer},
markdown::{parse::ParseError, text_style::Color},
presentation::{
Presentation,
@ -98,24 +98,12 @@ impl<'a> Exporter<'a> {
Self { parser, default_theme, resources, third_party, code_executor, themes, options, dimensions }
}
/// Export the given presentation into PDF.
///
/// This uses a separate `presenterm-export` tool.
pub fn export_pdf(
mut self,
fn build_renderer(
&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")?;
renderer: Renderer,
) -> Result<ExportRenderer, ExportError> {
let content = fs::read_to_string(presentation_path).map_err(ExportError::ReadPresentation)?;
let elements = self.parser.parse(&content)?;
@ -132,7 +120,7 @@ impl<'a> Exporter<'a> {
.build(elements)?;
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::render_async_images(&mut presentation);
@ -143,10 +131,32 @@ impl<'a> Exporter<'a> {
}
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 {
Some(path) => path.to_path_buf(),
None => presentation_path.with_extension("pdf"),
};
render.generate(&pdf_path)?;
execute!(
@ -158,6 +168,36 @@ impl<'a> Exporter<'a> {
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) {
let poller = Poller::launch();
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,
output_type: Renderer,
dimensions: WindowSize,
html_body: String,
background_color: Option<String>,
}
impl PdfRender {
pub(crate) fn new(dimensions: WindowSize, output_directory: OutputDirectory) -> Self {
impl ExportRenderer {
pub(crate) fn new(dimensions: WindowSize, output_directory: OutputDirectory, output_type: Renderer) -> Self {
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> {
@ -154,24 +166,9 @@ impl PdfRender {
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 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 height = self.dimensions.rows * LINE_HEIGHT;
let background_color = self.background_color.unwrap_or_else(|| "black".into());
@ -220,19 +217,42 @@ impl PdfRender {
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 css_path = self.content_manager.persist_file("styles.css", css.as_bytes())?;
ThirdPartyTools::weasyprint(&[
"-s",
css_path.to_string_lossy().as_ref(),
"--presentational-hints",
"-e",
"utf8",
html_path.to_string_lossy().as_ref(),
pdf_path.to_string_lossy().as_ref(),
])
.run()?;
match self.output_type {
Renderer::Pdf => {
ThirdPartyTools::weasyprint(&[
"--presentational-hints",
"-e",
"utf8",
html_path.to_string_lossy().as_ref(),
output_path.to_string_lossy().as_ref(),
])
.run()?;
}
Renderer::Html => {
fs::write(output_path, html.as_bytes())?;
}
}
Ok(())
}
}

View File

@ -68,15 +68,19 @@ struct Cli {
path: Option<PathBuf>,
/// Export the presentation as a PDF rather than displaying it.
#[clap(short, long)]
#[clap(short, long, group = "export")]
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.
#[clap(long, requires = "export_pdf")]
#[clap(long, requires = "export")]
export_temporary_path: Option<PathBuf>,
/// 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>,
/// 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 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 {
Some(dimensions) => WindowSize {
rows: dimensions.rows,
@ -426,7 +430,11 @@ fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
Some(path) => OutputDirectory::external(path),
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 {
let SpeakerNotesComponents { events_listener, events_publisher } =
SpeakerNotesComponents::new(&cli, &config, &path)?;