mirror of
https://github.com/mfontanini/presenterm.git
synced 2025-05-07 16:33:01 +00:00
Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
c0298506c5 | ||
|
55b5474d9d | ||
|
14722e548f | ||
|
2a4ea80a46 | ||
|
b25fa12b82 | ||
|
725312e71c | ||
|
5565d420f5 | ||
|
afb0f0797f | ||
|
68a210da5a | ||
|
60f6208594 | ||
|
e2dab4d7ef | ||
|
14d2edfeb5 | ||
|
8f40a8295b | ||
|
8d54fe225a | ||
|
257fa137c5 | ||
|
262b2af3e7 | ||
|
7b2ba0eb8c | ||
|
cae76380fa | ||
|
78a3df199e | ||
|
fe818344fe | ||
|
6ff8e87924 | ||
|
76561b1281 | ||
|
519aad16e8 | ||
|
0d4ffceede | ||
|
c3fb212f90 | ||
|
d0ea46ce85 |
5
.github/workflows/merge.yaml
vendored
5
.github/workflows/merge.yaml
vendored
@ -45,7 +45,7 @@ jobs:
|
|||||||
source ./.venv/bin/activate
|
source ./.venv/bin/activate
|
||||||
uv pip install weasyprint
|
uv pip install weasyprint
|
||||||
|
|
||||||
- name: Export demo presentation as PDF
|
- name: Export demo presentation as PDF and HTML
|
||||||
run: |
|
run: |
|
||||||
cat >/tmp/config.yaml <<EOL
|
cat >/tmp/config.yaml <<EOL
|
||||||
export:
|
export:
|
||||||
@ -54,7 +54,8 @@ jobs:
|
|||||||
columns: 135
|
columns: 135
|
||||||
EOL
|
EOL
|
||||||
source ./.venv/bin/activate
|
source ./.venv/bin/activate
|
||||||
cargo run -- -e -c /tmp/config.yaml examples/demo.md
|
cargo run -- --export-pdf -c /tmp/config.yaml examples/demo.md
|
||||||
|
cargo run -- --export-html -c /tmp/config.yaml examples/demo.md
|
||||||
|
|
||||||
nix-flake:
|
nix-flake:
|
||||||
name: Validate nix flake
|
name: Validate nix flake
|
||||||
|
@ -6,12 +6,12 @@
|
|||||||
|
|
||||||
## New features
|
## New features
|
||||||
|
|
||||||
* Support for [slide transitions](https://mfontanini.github.io/presenterm/features/slide-transitons.html) is now available ([#530](https://github.com/mfontanini/presenterm/issues/530)):
|
* Support for [slide transitions](https://mfontanini.github.io/presenterm/features/slide-transitions.html) is now available ([#530](https://github.com/mfontanini/presenterm/issues/530)):
|
||||||
* Add fade slide transition ([#534](https://github.com/mfontanini/presenterm/issues/534)).
|
* Add fade slide transition ([#534](https://github.com/mfontanini/presenterm/issues/534)).
|
||||||
* Add slide horizontally slide transition animation ([#528](https://github.com/mfontanini/presenterm/issues/528)).
|
* Add slide horizontally slide transition animation ([#528](https://github.com/mfontanini/presenterm/issues/528)).
|
||||||
* Add `collapse_horizontal` slide transition ([#560](https://github.com/mfontanini/presenterm/issues/560)).
|
* Add `collapse_horizontal` slide transition ([#560](https://github.com/mfontanini/presenterm/issues/560)).
|
||||||
* Add `--output` option to specify the path where the output file is written to during an export ([#526](https://github.com/mfontanini/presenterm/issues/526)).
|
* Add `--output` option to specify the path where the output file is written to during an export ([#526](https://github.com/mfontanini/presenterm/issues/526)) - thanks @marianozunino.
|
||||||
* Allow specifying [start/end lines](https://mfontanini.github.io/presenterm/features/code/highlighting.html#including-external-code-snippets) in file snippet type ([#565](https://github.com/mfontanini/presenterm/issues/565)) - thanks @marianozunino.
|
* Allow specifying [start/end lines](https://mfontanini.github.io/presenterm/features/code/highlighting.html#including-external-code-snippets) in file snippet type ([#565](https://github.com/mfontanini/presenterm/issues/565)).
|
||||||
* Allow letting [pauses become new slides](https://mfontanini.github.io/presenterm/configuration/settings.html#pause-behavior) when exporting ([#557](https://github.com/mfontanini/presenterm/issues/557)).
|
* Allow letting [pauses become new slides](https://mfontanini.github.io/presenterm/configuration/settings.html#pause-behavior) when exporting ([#557](https://github.com/mfontanini/presenterm/issues/557)).
|
||||||
* Allow [using images on right in footer](https://mfontanini.github.io/presenterm/features/themes/definition.html#footer-images) ([#554](https://github.com/mfontanini/presenterm/issues/554)).
|
* Allow [using images on right in footer](https://mfontanini.github.io/presenterm/features/themes/definition.html#footer-images) ([#554](https://github.com/mfontanini/presenterm/issues/554)).
|
||||||
* Add [`max_rows` configuration](https://mfontanini.github.io/presenterm/configuration/settings.html#maximum-presentation-height) to cap vertical size ([#531](https://github.com/mfontanini/presenterm/issues/531)).
|
* Add [`max_rows` configuration](https://mfontanini.github.io/presenterm/configuration/settings.html#maximum-presentation-height) to cap vertical size ([#531](https://github.com/mfontanini/presenterm/issues/531)).
|
||||||
|
@ -10,7 +10,7 @@ custom themes, in the following directories:
|
|||||||
|
|
||||||
The configuration file will be looked up automatically in the directories above under the name `config.yaml`. e.g. on
|
The configuration file will be looked up automatically in the directories above under the name `config.yaml`. e.g. on
|
||||||
Linux you should create it under `~/.config/presenterm/config.yaml`. You can also specify a custom path to this file
|
Linux you should create it under `~/.config/presenterm/config.yaml`. You can also specify a custom path to this file
|
||||||
when running _presenterm_ via the `--config-path` parameter.
|
when running _presenterm_ via the `--config-file` parameter.
|
||||||
|
|
||||||
A [sample configuration file](https://github.com/mfontanini/presenterm/blob/master/config.sample.yaml) is provided in
|
A [sample configuration file](https://github.com/mfontanini/presenterm/blob/master/config.sample.yaml) is provided in
|
||||||
the repository that you can use as a base.
|
the repository that you can use as a base.
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Slide transitions
|
# Slide transitions
|
||||||
|
|
||||||
Slide transitions allow animating your presentation every time you move from a slide to the next/previous one. See the
|
Slide transitions allow animating your presentation every time you move from a slide to the next/previous one. See the
|
||||||
[configuration page](../configuration/settings.md) to learn how to configure transitions.
|
[configuration page](../configuration/settings.md#slide-transitions) to learn how to configure transitions.
|
||||||
|
|
||||||
The following animations are supported:
|
The following animations are supported:
|
||||||
|
|
||||||
|
@ -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::output::{ExportRenderer, OutputFormat},
|
||||||
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: OutputFormat,
|
||||||
) -> 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, OutputFormat::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, OutputFormat::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();
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
pub mod exporter;
|
pub mod exporter;
|
||||||
pub(crate) mod html;
|
pub(crate) mod html;
|
||||||
pub(crate) mod pdf;
|
pub(crate) mod output;
|
||||||
|
@ -8,15 +8,11 @@ use crate::{
|
|||||||
presentation::Slide,
|
presentation::Slide,
|
||||||
render::{engine::RenderEngine, properties::WindowSize},
|
render::{engine::RenderEngine, properties::WindowSize},
|
||||||
terminal::{
|
terminal::{
|
||||||
image::{
|
image::printer::TerminalImage,
|
||||||
Image, ImageSource,
|
|
||||||
printer::{ImageProperties, TerminalImage},
|
|
||||||
},
|
|
||||||
virt::{TerminalGrid, VirtualTerminal},
|
virt::{TerminalGrid, VirtualTerminal},
|
||||||
},
|
},
|
||||||
tools::ThirdPartyTools,
|
tools::ThirdPartyTools,
|
||||||
};
|
};
|
||||||
use image::{ImageEncoder, codecs::png::PngEncoder};
|
|
||||||
use std::{
|
use std::{
|
||||||
fs, io,
|
fs, io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@ -37,8 +33,9 @@ struct HtmlSlide {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HtmlSlide {
|
impl HtmlSlide {
|
||||||
fn new(grid: TerminalGrid, content_manager: &mut ContentManager) -> Result<Self, ExportError> {
|
fn new(grid: TerminalGrid) -> Result<Self, ExportError> {
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
|
rows.push(String::from("<div class=\"container\">"));
|
||||||
for (y, row) in grid.rows.into_iter().enumerate() {
|
for (y, row) in grid.rows.into_iter().enumerate() {
|
||||||
let mut finalized_row = "<div class=\"content-line\"><pre>".to_string();
|
let mut finalized_row = "<div class=\"content-line\"><pre>".to_string();
|
||||||
let mut current_style = row.first().map(|c| c.style).unwrap_or_default();
|
let mut current_style = row.first().map(|c| c.style).unwrap_or_default();
|
||||||
@ -57,11 +54,11 @@ impl HtmlSlide {
|
|||||||
other => current_string.push(other),
|
other => current_string.push(other),
|
||||||
}
|
}
|
||||||
if let Some(image) = grid.images.get(&(y as u16, x as u16)) {
|
if let Some(image) = grid.images.get(&(y as u16, x as u16)) {
|
||||||
let image_path = content_manager.persist_image(&image.image)?;
|
let TerminalImage::Raw(raw_image) = image.image.image() else { panic!("not in raw image mode") };
|
||||||
let image_path_str = image_path.display();
|
let image_contents = raw_image.to_inline_html();
|
||||||
let width_pixels = (image.width_columns as f64 * FONT_SIZE as f64 * FONT_SIZE_WIDTH).ceil();
|
let width_pixels = (image.width_columns as f64 * FONT_SIZE as f64 * FONT_SIZE_WIDTH).ceil();
|
||||||
let image_tag = format!(
|
let image_tag = format!(
|
||||||
"<img width=\"{width_pixels}\" src=\"file://{image_path_str}\" style=\"position: absolute\" />"
|
"<img width=\"{width_pixels}\" src=\"{image_contents}\" style=\"position: absolute\" />"
|
||||||
);
|
);
|
||||||
current_string.push_str(&image_tag);
|
current_string.push_str(&image_tag);
|
||||||
}
|
}
|
||||||
@ -73,6 +70,7 @@ impl HtmlSlide {
|
|||||||
finalized_row.push_str("</pre></div>");
|
finalized_row.push_str("</pre></div>");
|
||||||
rows.push(finalized_row);
|
rows.push(finalized_row);
|
||||||
}
|
}
|
||||||
|
rows.push(String::from("</div>"));
|
||||||
|
|
||||||
Ok(HtmlSlide { rows, background_color: grid.background_color.as_ref().map(color_to_html) })
|
Ok(HtmlSlide { rows, background_color: grid.background_color.as_ref().map(color_to_html) })
|
||||||
}
|
}
|
||||||
@ -84,35 +82,11 @@ impl HtmlSlide {
|
|||||||
|
|
||||||
pub(crate) struct ContentManager {
|
pub(crate) struct ContentManager {
|
||||||
output_directory: OutputDirectory,
|
output_directory: OutputDirectory,
|
||||||
image_count: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContentManager {
|
impl ContentManager {
|
||||||
pub(crate) fn new(output_directory: OutputDirectory) -> Self {
|
pub(crate) fn new(output_directory: OutputDirectory) -> Self {
|
||||||
Self { output_directory, image_count: 0 }
|
Self { output_directory }
|
||||||
}
|
|
||||||
|
|
||||||
fn persist_image(&mut self, image: &Image) -> Result<PathBuf, ExportError> {
|
|
||||||
match image.source.clone() {
|
|
||||||
ImageSource::Filesystem(path) => Ok(path),
|
|
||||||
ImageSource::Generated => {
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
let dimensions = image.image().dimensions();
|
|
||||||
let TerminalImage::Ascii(image) = image.image() else { panic!("not in ascii mode") };
|
|
||||||
let image = image.image();
|
|
||||||
PngEncoder::new(&mut buffer).write_image(
|
|
||||||
image.as_bytes(),
|
|
||||||
dimensions.0,
|
|
||||||
dimensions.1,
|
|
||||||
image.color().into(),
|
|
||||||
)?;
|
|
||||||
let name = format!("img-{}.png", self.image_count);
|
|
||||||
let path = self.output_directory.path().join(name);
|
|
||||||
fs::write(&path, buffer)?;
|
|
||||||
self.image_count += 1;
|
|
||||||
Ok(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn persist_file(&self, name: &str, data: &[u8]) -> io::Result<PathBuf> {
|
fn persist_file(&self, name: &str, data: &[u8]) -> io::Result<PathBuf> {
|
||||||
@ -122,17 +96,29 @@ impl ContentManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct PdfRender {
|
pub(crate) enum OutputFormat {
|
||||||
|
Pdf,
|
||||||
|
Html,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ExportRenderer {
|
||||||
content_manager: ContentManager,
|
content_manager: ContentManager,
|
||||||
|
output_format: OutputFormat,
|
||||||
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: OutputFormat) -> 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_format: output_type,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn process_slide(&mut self, slide: Slide) -> Result<(), ExportError> {
|
pub(crate) fn process_slide(&mut self, slide: Slide) -> Result<(), ExportError> {
|
||||||
@ -141,7 +127,7 @@ impl PdfRender {
|
|||||||
engine.render(slide.iter_operations())?;
|
engine.render(slide.iter_operations())?;
|
||||||
|
|
||||||
let grid = terminal.into_contents();
|
let grid = terminal.into_contents();
|
||||||
let slide = HtmlSlide::new(grid, &mut self.content_manager)?;
|
let slide = HtmlSlide::new(grid)?;
|
||||||
if self.background_color.is_none() {
|
if self.background_color.is_none() {
|
||||||
self.background_color.clone_from(&slide.background_color);
|
self.background_color.clone_from(&slide.background_color);
|
||||||
}
|
}
|
||||||
@ -152,19 +138,24 @@ 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 html = format!(
|
let script = include_str!("script.js");
|
||||||
r#"<html>
|
|
||||||
<head>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{html_body}</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());
|
||||||
|
let container = match self.output_format {
|
||||||
|
OutputFormat::Pdf => String::from("display: contents;"),
|
||||||
|
OutputFormat::Html => String::from(
|
||||||
|
"
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
",
|
||||||
|
),
|
||||||
|
};
|
||||||
let css = format!(
|
let css = format!(
|
||||||
r"
|
r"
|
||||||
pre {{
|
pre {{
|
||||||
@ -180,8 +171,14 @@ impl PdfRender {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: {FONT_SIZE}px;
|
font-size: {FONT_SIZE}px;
|
||||||
line-height: {LINE_HEIGHT}px;
|
line-height: {LINE_HEIGHT}px;
|
||||||
background-color: {background_color};
|
|
||||||
width: {width}px;
|
width: {width}px;
|
||||||
|
height: {height}px;
|
||||||
|
transform-origin: top left;
|
||||||
|
background-color: {background_color};
|
||||||
|
}}
|
||||||
|
|
||||||
|
.container {{
|
||||||
|
{container}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
.content-line {{
|
.content-line {{
|
||||||
@ -191,25 +188,73 @@ impl PdfRender {
|
|||||||
width: {width}px;
|
width: {width}px;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
.hidden {{
|
||||||
|
display: none;
|
||||||
|
}}
|
||||||
|
|
||||||
@page {{
|
@page {{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: {height}px;
|
height: {height}px;
|
||||||
width: {width}px;
|
width: {width}px;
|
||||||
}}"
|
}}"
|
||||||
);
|
);
|
||||||
|
let html_script = match self.output_format {
|
||||||
|
OutputFormat::Pdf => String::new(),
|
||||||
|
OutputFormat::Html => {
|
||||||
|
format!(
|
||||||
|
"
|
||||||
|
<script>
|
||||||
|
let originalWidth = {width};
|
||||||
|
let originalHeight = {height};
|
||||||
|
{script}
|
||||||
|
</script>"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let style = match self.output_format {
|
||||||
|
OutputFormat::Pdf => String::new(),
|
||||||
|
OutputFormat::Html => format!(
|
||||||
|
"
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
{css}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let html = format!(
|
||||||
|
r"
|
||||||
|
<html>
|
||||||
|
{style}
|
||||||
|
<body>
|
||||||
|
{html_body}
|
||||||
|
{html_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())?;
|
let css_path = self.content_manager.persist_file("styles.css", css.as_bytes())?;
|
||||||
ThirdPartyTools::weasyprint(&[
|
|
||||||
"-s",
|
match self.output_format {
|
||||||
css_path.to_string_lossy().as_ref(),
|
OutputFormat::Pdf => {
|
||||||
"--presentational-hints",
|
ThirdPartyTools::weasyprint(&[
|
||||||
"-e",
|
"-s",
|
||||||
"utf8",
|
css_path.to_string_lossy().as_ref(),
|
||||||
html_path.to_string_lossy().as_ref(),
|
"--presentational-hints",
|
||||||
pdf_path.to_string_lossy().as_ref(),
|
"-e",
|
||||||
])
|
"utf8",
|
||||||
.run()?;
|
html_path.to_string_lossy().as_ref(),
|
||||||
|
output_path.to_string_lossy().as_ref(),
|
||||||
|
])
|
||||||
|
.run()?;
|
||||||
|
}
|
||||||
|
OutputFormat::Html => {
|
||||||
|
fs::write(output_path, html.as_bytes())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
45
src/export/script.js
Normal file
45
src/export/script.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const allLines = document.querySelectorAll('body > div');
|
||||||
|
const pageBreakMarkers = document.querySelectorAll('.container');
|
||||||
|
let currentPageIndex = 0;
|
||||||
|
|
||||||
|
|
||||||
|
function showCurrentPage() {
|
||||||
|
allLines.forEach((line) => {
|
||||||
|
line.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
allLines[currentPageIndex].classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function scaler() {
|
||||||
|
var w = document.documentElement.clientWidth;
|
||||||
|
var h = document.documentElement.clientHeight;
|
||||||
|
let widthScaledAmount= w/originalWidth;
|
||||||
|
let heightScaledAmount= h/originalHeight;
|
||||||
|
let scaledAmount = Math.min(widthScaledAmount, heightScaledAmount);
|
||||||
|
document.querySelector("body").style.transform = `scale(${scaledAmount})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyPress(event) {
|
||||||
|
if (event.key === 'ArrowLeft') {
|
||||||
|
if (currentPageIndex > 0) {
|
||||||
|
currentPageIndex--;
|
||||||
|
showCurrentPage();
|
||||||
|
}
|
||||||
|
} else if (event.key === 'ArrowRight') {
|
||||||
|
if (currentPageIndex < pageBreakMarkers.length - 1) {
|
||||||
|
currentPageIndex++;
|
||||||
|
showCurrentPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyPress);
|
||||||
|
window.addEventListener("resize", scaler);
|
||||||
|
|
||||||
|
scaler();
|
||||||
|
showCurrentPage();
|
||||||
|
});
|
||||||
|
|
22
src/main.rs
22
src/main.rs
@ -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(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.
|
||||||
@ -289,8 +293,8 @@ impl CoreComponents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn select_graphics_mode(cli: &Cli, config: &Config) -> GraphicsMode {
|
fn select_graphics_mode(cli: &Cli, config: &Config) -> GraphicsMode {
|
||||||
if cli.export_pdf {
|
if cli.export_pdf | cli.export_html {
|
||||||
GraphicsMode::AsciiBlocks
|
GraphicsMode::Raw
|
||||||
} else {
|
} else {
|
||||||
let protocol = cli.image_protocol.as_ref().unwrap_or(&config.defaults.image_protocol);
|
let protocol = cli.image_protocol.as_ref().unwrap_or(&config.defaults.image_protocol);
|
||||||
match GraphicsMode::try_from(protocol) {
|
match GraphicsMode::try_from(protocol) {
|
||||||
@ -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)?;
|
||||||
|
@ -959,7 +959,10 @@ impl<'a> PresentationBuilder<'a> {
|
|||||||
let mutators = mem::take(&mut self.chunk_mutators);
|
let mutators = mem::take(&mut self.chunk_mutators);
|
||||||
|
|
||||||
if !self.slide_state.skip_slide {
|
if !self.slide_state.skip_slide {
|
||||||
self.slide_chunks.push(SlideChunk::new(operations, mutators));
|
// Don't allow a last empty pause in slide since it adds nothing
|
||||||
|
if self.slide_chunks.is_empty() || !Self::is_chunk_empty(&operations) {
|
||||||
|
self.slide_chunks.push(SlideChunk::new(operations, mutators));
|
||||||
|
}
|
||||||
|
|
||||||
let chunks = mem::take(&mut self.slide_chunks);
|
let chunks = mem::take(&mut self.slide_chunks);
|
||||||
let builder = SlideBuilder::default().chunks(chunks);
|
let builder = SlideBuilder::default().chunks(chunks);
|
||||||
@ -977,6 +980,18 @@ impl<'a> PresentationBuilder<'a> {
|
|||||||
self.slide_state.last_element = LastElement::None;
|
self.slide_state.last_element = LastElement::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_chunk_empty(operations: &[RenderOperation]) -> bool {
|
||||||
|
if operations.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for operation in operations {
|
||||||
|
if !matches!(operation, RenderOperation::RenderLineBreak) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn generate_footer(&self) -> Result<Vec<RenderOperation>, BuildError> {
|
fn generate_footer(&self) -> Result<Vec<RenderOperation>, BuildError> {
|
||||||
let generator = FooterGenerator::new(self.theme.footer.clone(), &self.footer_vars, &self.theme.palette)?;
|
let generator = FooterGenerator::new(self.theme.footer.clone(), &self.footer_vars, &self.theme.palette)?;
|
||||||
Ok(vec![
|
Ok(vec![
|
||||||
@ -1721,6 +1736,7 @@ theme:
|
|||||||
ListItem { depth: 1, contents: "two".into(), item_type: ListItemType::Unordered },
|
ListItem { depth: 1, contents: "two".into(), item_type: ListItemType::Unordered },
|
||||||
ListItem { depth: 0, contents: "three".into(), item_type: ListItemType::Unordered },
|
ListItem { depth: 0, contents: "three".into(), item_type: ListItemType::Unordered },
|
||||||
]),
|
]),
|
||||||
|
MarkdownElement::Paragraph(vec!["hi".into()]),
|
||||||
];
|
];
|
||||||
let slides = build_presentation_with_options(elements, options).into_slides();
|
let slides = build_presentation_with_options(elements, options).into_slides();
|
||||||
assert_eq!(slides[0].iter_chunks().count(), expected_chunks);
|
assert_eq!(slides[0].iter_chunks().count(), expected_chunks);
|
||||||
@ -1756,6 +1772,20 @@ theme:
|
|||||||
assert_eq!(slides.len(), 2);
|
assert_eq!(slides.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn incremental_lists_end_of_slide() {
|
||||||
|
let elements = vec![
|
||||||
|
MarkdownElement::Comment { comment: "incremental_lists: true".into(), source_position: Default::default() },
|
||||||
|
MarkdownElement::List(vec![
|
||||||
|
ListItem { depth: 0, contents: "one".into(), item_type: ListItemType::Unordered },
|
||||||
|
ListItem { depth: 1, contents: "two".into(), item_type: ListItemType::Unordered },
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
let slides = build_presentation(elements).into_slides();
|
||||||
|
// There shouldn't be an extra one at the end
|
||||||
|
assert_eq!(slides[0].iter_chunks().count(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn skip_slide() {
|
fn skip_slide() {
|
||||||
let elements = vec![
|
let elements = vec![
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
use itertools::Itertools;
|
|
||||||
|
|
||||||
use super::{BuildError, BuildResult, ExecutionMode, PresentationBuilderOptions};
|
use super::{BuildError, BuildResult, ExecutionMode, PresentationBuilderOptions};
|
||||||
use crate::{
|
use crate::{
|
||||||
ImageRegistry,
|
ImageRegistry,
|
||||||
@ -18,13 +16,14 @@ use crate::{
|
|||||||
properties::WindowSize,
|
properties::WindowSize,
|
||||||
},
|
},
|
||||||
resource::Resources,
|
resource::Resources,
|
||||||
theme::{CodeBlockStyle, PresentationTheme},
|
theme::{Alignment, CodeBlockStyle, PresentationTheme},
|
||||||
third_party::{ThirdPartyRender, ThirdPartyRenderRequest},
|
third_party::{ThirdPartyRender, ThirdPartyRenderRequest},
|
||||||
ui::execution::{
|
ui::execution::{
|
||||||
RunAcquireTerminalSnippet, RunImageSnippet, RunSnippetOperation, SnippetExecutionDisabledOperation,
|
RunAcquireTerminalSnippet, RunImageSnippet, RunSnippetOperation, SnippetExecutionDisabledOperation,
|
||||||
disabled::ExecutionType, snippet::DisplaySeparator,
|
disabled::ExecutionType, snippet::DisplaySeparator,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use itertools::Itertools;
|
||||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
pub(crate) struct SnippetProcessorState<'a> {
|
pub(crate) struct SnippetProcessorState<'a> {
|
||||||
@ -308,7 +307,15 @@ impl<'a> SnippetProcessor<'a> {
|
|||||||
ExecutionMode::AlongSnippet => DisplaySeparator::On,
|
ExecutionMode::AlongSnippet => DisplaySeparator::On,
|
||||||
ExecutionMode::ReplaceSnippet => DisplaySeparator::Off,
|
ExecutionMode::ReplaceSnippet => DisplaySeparator::Off,
|
||||||
};
|
};
|
||||||
let alignment = self.code_style(&snippet).alignment;
|
let default_alignment = self.code_style(&snippet).alignment;
|
||||||
|
// If we're replacing the snippet output and we have center alignment, use center alignment but
|
||||||
|
// without any margins and minimum sizes so we truly center the output.
|
||||||
|
let alignment = match (&mode, default_alignment) {
|
||||||
|
(ExecutionMode::ReplaceSnippet, Alignment::Center { .. }) => {
|
||||||
|
Alignment::Center { minimum_margin: Default::default(), minimum_size: 0 }
|
||||||
|
}
|
||||||
|
(_, alignment) => alignment,
|
||||||
|
};
|
||||||
let default_colors = self.theme.default_style.style.colors;
|
let default_colors = self.theme.default_style.style.colors;
|
||||||
let mut execution_output_style = self.theme.execution_output.clone();
|
let mut execution_output_style = self.theme.execution_output.clone();
|
||||||
if snippet.attributes.no_background {
|
if snippet.attributes.no_background {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
|
use self::printer::{ImageProperties, TerminalImage};
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
use protocols::ascii::AsciiImage;
|
use protocols::ascii::AsciiImage;
|
||||||
|
|
||||||
use self::printer::{ImageProperties, TerminalImage};
|
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
@ -43,6 +42,7 @@ impl Image {
|
|||||||
TerminalImage::Ascii(image) => image.clone(),
|
TerminalImage::Ascii(image) => image.clone(),
|
||||||
TerminalImage::Kitty(image) => DynamicImage::from(image.as_rgba8()).into(),
|
TerminalImage::Kitty(image) => DynamicImage::from(image.as_rgba8()).into(),
|
||||||
TerminalImage::Iterm(image) => DynamicImage::from(image.as_rgba8()).into(),
|
TerminalImage::Iterm(image) => DynamicImage::from(image.as_rgba8()).into(),
|
||||||
|
TerminalImage::Raw(_) => unreachable!("raw is only used for exports"),
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
TerminalImage::Sixel(image) => DynamicImage::from(image.as_rgba8()).into(),
|
TerminalImage::Sixel(image) => DynamicImage::from(image.as_rgba8()).into(),
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,7 @@ use super::{
|
|||||||
ascii::{AsciiImage, AsciiPrinter},
|
ascii::{AsciiImage, AsciiPrinter},
|
||||||
iterm::{ItermImage, ItermPrinter},
|
iterm::{ItermImage, ItermPrinter},
|
||||||
kitty::{KittyImage, KittyMode, KittyPrinter},
|
kitty::{KittyImage, KittyMode, KittyPrinter},
|
||||||
|
raw::{RawImage, RawPrinter},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -54,6 +55,7 @@ pub(crate) enum TerminalImage {
|
|||||||
Kitty(KittyImage),
|
Kitty(KittyImage),
|
||||||
Iterm(ItermImage),
|
Iterm(ItermImage),
|
||||||
Ascii(AsciiImage),
|
Ascii(AsciiImage),
|
||||||
|
Raw(RawImage),
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
Sixel(super::protocols::sixel::SixelImage),
|
Sixel(super::protocols::sixel::SixelImage),
|
||||||
}
|
}
|
||||||
@ -64,6 +66,7 @@ impl ImageProperties for TerminalImage {
|
|||||||
Self::Kitty(image) => image.dimensions(),
|
Self::Kitty(image) => image.dimensions(),
|
||||||
Self::Iterm(image) => image.dimensions(),
|
Self::Iterm(image) => image.dimensions(),
|
||||||
Self::Ascii(image) => image.dimensions(),
|
Self::Ascii(image) => image.dimensions(),
|
||||||
|
Self::Raw(image) => image.dimensions(),
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
Self::Sixel(image) => image.dimensions(),
|
Self::Sixel(image) => image.dimensions(),
|
||||||
}
|
}
|
||||||
@ -74,6 +77,7 @@ pub enum ImagePrinter {
|
|||||||
Kitty(KittyPrinter),
|
Kitty(KittyPrinter),
|
||||||
Iterm(ItermPrinter),
|
Iterm(ItermPrinter),
|
||||||
Ascii(AsciiPrinter),
|
Ascii(AsciiPrinter),
|
||||||
|
Raw(RawPrinter),
|
||||||
Null,
|
Null,
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
Sixel(super::protocols::sixel::SixelPrinter),
|
Sixel(super::protocols::sixel::SixelPrinter),
|
||||||
@ -91,6 +95,7 @@ impl ImagePrinter {
|
|||||||
GraphicsMode::Kitty { mode, inside_tmux } => Self::new_kitty(mode, inside_tmux)?,
|
GraphicsMode::Kitty { mode, inside_tmux } => Self::new_kitty(mode, inside_tmux)?,
|
||||||
GraphicsMode::Iterm2 => Self::new_iterm(),
|
GraphicsMode::Iterm2 => Self::new_iterm(),
|
||||||
GraphicsMode::AsciiBlocks => Self::new_ascii(),
|
GraphicsMode::AsciiBlocks => Self::new_ascii(),
|
||||||
|
GraphicsMode::Raw => Self::new_raw(),
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
GraphicsMode::Sixel => Self::new_sixel()?,
|
GraphicsMode::Sixel => Self::new_sixel()?,
|
||||||
};
|
};
|
||||||
@ -109,6 +114,10 @@ impl ImagePrinter {
|
|||||||
Self::Ascii(AsciiPrinter)
|
Self::Ascii(AsciiPrinter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn new_raw() -> Self {
|
||||||
|
Self::Raw(RawPrinter)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
fn new_sixel() -> Result<Self, CreatePrinterError> {
|
fn new_sixel() -> Result<Self, CreatePrinterError> {
|
||||||
Ok(Self::Sixel(super::protocols::sixel::SixelPrinter::new()?))
|
Ok(Self::Sixel(super::protocols::sixel::SixelPrinter::new()?))
|
||||||
@ -124,6 +133,7 @@ impl PrintImage for ImagePrinter {
|
|||||||
Self::Iterm(printer) => TerminalImage::Iterm(printer.register(spec)?),
|
Self::Iterm(printer) => TerminalImage::Iterm(printer.register(spec)?),
|
||||||
Self::Ascii(printer) => TerminalImage::Ascii(printer.register(spec)?),
|
Self::Ascii(printer) => TerminalImage::Ascii(printer.register(spec)?),
|
||||||
Self::Null => return Err(RegisterImageError::Unsupported),
|
Self::Null => return Err(RegisterImageError::Unsupported),
|
||||||
|
Self::Raw(printer) => TerminalImage::Raw(printer.register(spec)?),
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
Self::Sixel(printer) => TerminalImage::Sixel(printer.register(spec)?),
|
Self::Sixel(printer) => TerminalImage::Sixel(printer.register(spec)?),
|
||||||
};
|
};
|
||||||
@ -139,6 +149,7 @@ impl PrintImage for ImagePrinter {
|
|||||||
(Self::Iterm(printer), TerminalImage::Iterm(image)) => printer.print(image, options, terminal),
|
(Self::Iterm(printer), TerminalImage::Iterm(image)) => printer.print(image, options, terminal),
|
||||||
(Self::Ascii(printer), TerminalImage::Ascii(image)) => printer.print(image, options, terminal),
|
(Self::Ascii(printer), TerminalImage::Ascii(image)) => printer.print(image, options, terminal),
|
||||||
(Self::Null, _) => Ok(()),
|
(Self::Null, _) => Ok(()),
|
||||||
|
(Self::Raw(printer), TerminalImage::Raw(image)) => printer.print(image, options, terminal),
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
(Self::Sixel(printer), TerminalImage::Sixel(image)) => printer.print(image, options, terminal),
|
(Self::Sixel(printer), TerminalImage::Sixel(image)) => printer.print(image, options, terminal),
|
||||||
_ => Err(PrintImageError::Unsupported),
|
_ => Err(PrintImageError::Unsupported),
|
||||||
@ -165,6 +176,7 @@ impl fmt::Debug for ImageRegistry {
|
|||||||
ImagePrinter::Iterm(_) => "Iterm",
|
ImagePrinter::Iterm(_) => "Iterm",
|
||||||
ImagePrinter::Ascii(_) => "Ascii",
|
ImagePrinter::Ascii(_) => "Ascii",
|
||||||
ImagePrinter::Null => "Null",
|
ImagePrinter::Null => "Null",
|
||||||
|
ImagePrinter::Raw(_) => "Raw",
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
ImagePrinter::Sixel(_) => "Sixel",
|
ImagePrinter::Sixel(_) => "Sixel",
|
||||||
};
|
};
|
||||||
|
@ -36,10 +36,6 @@ impl AsciiImage {
|
|||||||
cached_sizes.insert(cache_key, image.into_rgba8());
|
cached_sizes.insert(cache_key, image.into_rgba8());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn image(&self) -> &DynamicImage {
|
|
||||||
&self.inner.image
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageProperties for AsciiImage {
|
impl ImageProperties for AsciiImage {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
pub(crate) mod ascii;
|
pub(crate) mod ascii;
|
||||||
pub(crate) mod iterm;
|
pub(crate) mod iterm;
|
||||||
pub(crate) mod kitty;
|
pub(crate) mod kitty;
|
||||||
|
pub(crate) mod raw;
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
pub(crate) mod sixel;
|
pub(crate) mod sixel;
|
||||||
|
61
src/terminal/image/protocols/raw.rs
Normal file
61
src/terminal/image/protocols/raw.rs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
use crate::terminal::{
|
||||||
|
image::printer::{ImageProperties, ImageSpec, PrintImage, PrintImageError, PrintOptions, RegisterImageError},
|
||||||
|
printer::TerminalIo,
|
||||||
|
};
|
||||||
|
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||||
|
use image::{GenericImageView, ImageEncoder, ImageFormat, codecs::png::PngEncoder};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
pub(crate) struct RawImage {
|
||||||
|
contents: Vec<u8>,
|
||||||
|
format: ImageFormat,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawImage {
|
||||||
|
pub(crate) fn to_inline_html(&self) -> String {
|
||||||
|
let mime_type = self.format.to_mime_type();
|
||||||
|
let data = STANDARD.encode(&self.contents);
|
||||||
|
format!("data:{mime_type};base64,{data}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageProperties for RawImage {
|
||||||
|
fn dimensions(&self) -> (u32, u32) {
|
||||||
|
(self.width, self.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct RawPrinter;
|
||||||
|
|
||||||
|
impl PrintImage for RawPrinter {
|
||||||
|
type Image = RawImage;
|
||||||
|
|
||||||
|
fn register(&self, spec: ImageSpec) -> Result<Self::Image, RegisterImageError> {
|
||||||
|
let image = match spec {
|
||||||
|
ImageSpec::Generated(image) => {
|
||||||
|
let mut contents = Vec::new();
|
||||||
|
let encoder = PngEncoder::new(&mut contents);
|
||||||
|
let (width, height) = image.dimensions();
|
||||||
|
encoder.write_image(image.as_bytes(), width, height, image.color().into())?;
|
||||||
|
RawImage { contents, format: ImageFormat::Png, width, height }
|
||||||
|
}
|
||||||
|
ImageSpec::Filesystem(path) => {
|
||||||
|
let contents = fs::read(path)?;
|
||||||
|
let format = image::guess_format(&contents)?;
|
||||||
|
let image = image::load_from_memory_with_format(&contents, format)?;
|
||||||
|
let (width, height) = image.dimensions();
|
||||||
|
RawImage { contents, format, width, height }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print<T>(&self, _image: &Self::Image, _options: &PrintOptions, _terminal: &mut T) -> Result<(), PrintImageError>
|
||||||
|
where
|
||||||
|
T: TerminalIo,
|
||||||
|
{
|
||||||
|
Err(PrintImageError::Other("raw images can't be printed".into()))
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ pub enum GraphicsMode {
|
|||||||
inside_tmux: bool,
|
inside_tmux: bool,
|
||||||
},
|
},
|
||||||
AsciiBlocks,
|
AsciiBlocks,
|
||||||
|
Raw,
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
Sixel,
|
Sixel,
|
||||||
}
|
}
|
||||||
|
@ -178,10 +178,9 @@ impl FooterLine {
|
|||||||
palette: &ColorPalette,
|
palette: &ColorPalette,
|
||||||
) -> Result<Self, InvalidFooterTemplateError> {
|
) -> Result<Self, InvalidFooterTemplateError> {
|
||||||
use FooterTemplateChunk::*;
|
use FooterTemplateChunk::*;
|
||||||
let mut line = Line::default();
|
|
||||||
let FooterVariables { current_slide, total_slides, author, title, sub_title, event, location, date } = vars;
|
let FooterVariables { current_slide, total_slides, author, title, sub_title, event, location, date } = vars;
|
||||||
let arena = Arena::default();
|
let arena = Arena::default();
|
||||||
let parser = MarkdownParser::new(&arena);
|
let mut reassembled = String::new();
|
||||||
for chunk in template.0 {
|
for chunk in template.0 {
|
||||||
let raw_text = match chunk {
|
let raw_text = match chunk {
|
||||||
CurrentSlide => Cow::Owned(current_slide.to_string()),
|
CurrentSlide => Cow::Owned(current_slide.to_string()),
|
||||||
@ -199,20 +198,22 @@ impl FooterLine {
|
|||||||
if raw_text.lines().count() != 1 {
|
if raw_text.lines().count() != 1 {
|
||||||
return Err(InvalidFooterTemplateError::NoNewlines);
|
return Err(InvalidFooterTemplateError::NoNewlines);
|
||||||
}
|
}
|
||||||
let starting_length = raw_text.len();
|
reassembled.push_str(&raw_text);
|
||||||
let raw_text = raw_text.trim_start();
|
}
|
||||||
let left_whitespace = starting_length - raw_text.len();
|
// Inline parsing loses leading/trailing whitespaces so re-add them ourselves
|
||||||
let raw_text = raw_text.trim_end();
|
let starting_length = reassembled.len();
|
||||||
let right_whitespace = starting_length - raw_text.len() - left_whitespace;
|
let raw_text = reassembled.trim_start();
|
||||||
let inlines = parser.parse_inlines(raw_text)?;
|
let left_whitespace = starting_length - raw_text.len();
|
||||||
let mut contents = inlines.resolve(palette)?;
|
let raw_text = raw_text.trim_end();
|
||||||
if left_whitespace != 0 {
|
let right_whitespace = starting_length - raw_text.len() - left_whitespace;
|
||||||
contents.0.insert(0, " ".repeat(left_whitespace).into());
|
let parser = MarkdownParser::new(&arena);
|
||||||
}
|
let inlines = parser.parse_inlines(&reassembled)?;
|
||||||
if right_whitespace != 0 {
|
let mut line = inlines.resolve(palette)?;
|
||||||
contents.0.push(" ".repeat(right_whitespace).into());
|
if left_whitespace != 0 {
|
||||||
}
|
line.0.insert(0, " ".repeat(left_whitespace).into());
|
||||||
line.0.extend(contents.0);
|
}
|
||||||
|
if right_whitespace != 0 {
|
||||||
|
line.0.push(" ".repeat(right_whitespace).into());
|
||||||
}
|
}
|
||||||
line.apply_style(style);
|
line.apply_style(style);
|
||||||
Ok(Self(line))
|
Ok(Self(line))
|
||||||
@ -320,4 +321,25 @@ mod tests {
|
|||||||
let template = FooterTemplate(vec![chunk]);
|
let template = FooterTemplate(vec![chunk]);
|
||||||
FooterLine::new(template, &Default::default(), &VARIABLES, &PALETTE).expect_err("render succeeded");
|
FooterLine::new(template, &Default::default(), &VARIABLES, &PALETTE).expect_err("render succeeded");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interleaved_spans() {
|
||||||
|
let chunks = vec![
|
||||||
|
FooterTemplateChunk::Literal("<span style=\"color: palette:red\">".into()),
|
||||||
|
FooterTemplateChunk::CurrentSlide,
|
||||||
|
FooterTemplateChunk::Literal(" / ".into()),
|
||||||
|
FooterTemplateChunk::TotalSlides,
|
||||||
|
FooterTemplateChunk::Literal("</span>".into()),
|
||||||
|
FooterTemplateChunk::Literal("<span style=\"color: green\">".into()),
|
||||||
|
FooterTemplateChunk::Title,
|
||||||
|
FooterTemplateChunk::Literal("</span>".into()),
|
||||||
|
];
|
||||||
|
let template = FooterTemplate(chunks);
|
||||||
|
let line = FooterLine::new(template, &Default::default(), &VARIABLES, &PALETTE).expect("render failed");
|
||||||
|
let expected = &[
|
||||||
|
Text::new("1 / 5", TextStyle::default().fg_color(Color::new(255, 0, 0))),
|
||||||
|
Text::new("hi", TextStyle::default().fg_color(Color::Green)),
|
||||||
|
];
|
||||||
|
assert_eq!(line.0.0, expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user