Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pdf support #114

Merged
merged 13 commits into from
Feb 2, 2024
25 changes: 21 additions & 4 deletions cli/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::{
};

use logid::{log, logging::event_entry::AddonKind, pipe};

use unimarkup_core::{
commons::config::{output::OutputFormatKind, Config},
Unimarkup,
Expand Down Expand Up @@ -46,11 +47,19 @@ pub fn compile(config: Config) -> Result<(), GeneralError> {
for format in um.get_formats() {
match format {
OutputFormatKind::Html => write_file(
&um.render_html()
&um.render_html(false)
.map_err(|_| GeneralError::Render)?
.to_string(),
&out_path,
OutputFormatKind::Html.extension(),
format.extension(),
)?,
OutputFormatKind::Pdf => write_raw_file(
&um.render_pdf().map_err(|err| {
log!(err);
GeneralError::Render
})?,
&out_path,
format.extension(),
)?,
OutputFormatKind::Umi => write_file(
&um.render_umi()
Expand All @@ -66,8 +75,8 @@ pub fn compile(config: Config) -> Result<(), GeneralError> {
Ok(())
}

fn write_file(
content: &str,
fn write_raw_file(
content: &[u8],
out_path: impl AsRef<Path>,
extension: &str,
) -> Result<(), GeneralError> {
Expand All @@ -87,3 +96,11 @@ fn write_file(
)
})
}

fn write_file(
content: &str,
out_path: impl AsRef<Path>,
extension: &str,
) -> Result<(), GeneralError> {
write_raw_file(content.as_bytes(), out_path, extension)
}
5 changes: 4 additions & 1 deletion commons/src/config/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub struct Output {
/// Defines the output format to render to.
/// If this option is not set, the input is rendered to all supported formats.
///
/// **Supported formats:** `html`
/// **Supported formats:** `html`, `pdf`
#[arg(long, alias = "output-formats", value_parser = parse_to_hashset::<OutputFormatKind>, required = false, default_value = "html")]
pub formats: HashSet<OutputFormatKind>,
/// `true` overwrites existing output files
Expand Down Expand Up @@ -71,6 +71,7 @@ pub enum OutputFormatKind {
#[default]
Html,
Umi,
Pdf,
}

impl OutputFormatKind {
Expand All @@ -79,6 +80,7 @@ impl OutputFormatKind {
match self {
OutputFormatKind::Html => "html",
OutputFormatKind::Umi => "umi",
OutputFormatKind::Pdf => "pdf",
}
}
}
Expand All @@ -90,6 +92,7 @@ impl FromStr for OutputFormatKind {
match s.to_lowercase().as_str() {
"html" => Ok(OutputFormatKind::Html),
"umi" => Ok(OutputFormatKind::Umi),
"pdf" => Ok(OutputFormatKind::Pdf),
o => Err(format!("Bad output format: {}", o)),
}
}
Expand Down
9 changes: 7 additions & 2 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub use unimarkup_commons as commons;
pub use unimarkup_inline as inline;
pub use unimarkup_parser as parser;
pub use unimarkup_render as render;
use unimarkup_render::pdf::render::render_pdf;

use crate::commons::config::output::OutputFormatKind;
use crate::commons::config::Config;
Expand Down Expand Up @@ -42,11 +43,15 @@ impl Unimarkup {
unimarkup_render::render::render(&self.doc, renderer)
}

pub fn render_html(&self) -> Result<Html, RenderError> {
self.render(HtmlRenderer::default())
pub fn render_html(&self, use_paged_js: bool) -> Result<Html, RenderError> {
self.render(HtmlRenderer::new(use_paged_js))
}

pub fn render_umi(&self) -> Result<Umi, RenderError> {
self.render(UmiRenderer::default())
}

pub fn render_pdf(&self) -> Result<Vec<u8>, RenderError> {
render_pdf(&self.render_html(true)?.to_string())
}
}
2 changes: 1 addition & 1 deletion core/tests/runner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ fn run_spec_test(case: test_runner::test_file::TestCase) {
let input = test.input.trim_end();
let um = Unimarkup::parse(input, cfg);
test_file::TestOutputs {
html: Some(um.render_html().unwrap().to_string()),
html: Some(um.render_html(false).unwrap().to_string()),
um: Some(test.input.clone()),
}
},
Expand Down
2 changes: 1 addition & 1 deletion inline/tests/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ fn run_spec_test(case: test_runner::test_file::TestCase) {
let input = test.input.trim_end();
let um = unimarkup_core::Unimarkup::parse(input, cfg);
test_file::TestOutputs {
html: Some(um.render_html().unwrap().to_string()),
html: Some(um.render_html(false).unwrap().to_string()),
um: Some(test.input.clone()),
}
},
Expand Down
4 changes: 3 additions & 1 deletion render/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ unimarkup-inline = { path = "../inline/", version = "0" }
unimarkup-parser = { path = "../parser/", version = "0" }
syntect = "5.0"
spreadsheet-ods = "0.17.0"
headless_chrome = { version = "1.0.8" }
tempfile = "3.8.0"
serde.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
serde_yaml.workspace = true
20 changes: 14 additions & 6 deletions render/src/html/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub struct HtmlElements(Vec<HtmlElement>);
pub struct HtmlHead {
pub elements: HtmlElements,
pub syntax_highlighting_used: bool,
pub paged_js_used: bool,
pub styles: HtmlAttributes, //TODO: replace with CSS struct
}

Expand All @@ -41,6 +42,7 @@ impl HtmlHead {
self.elements.append(&mut other.elements);
self.styles.append(&mut other.styles);
self.syntax_highlighting_used |= other.syntax_highlighting_used;
self.paged_js_used |= other.paged_js_used;
}
}

Expand Down Expand Up @@ -97,6 +99,7 @@ impl OutputFormat for Html {
head: HtmlHead {
elements: HtmlElements(Vec::new()),
syntax_highlighting_used: false,
paged_js_used: false,
styles: HtmlAttributes(Vec::new()),
},
body: HtmlBody {
Expand Down Expand Up @@ -181,13 +184,18 @@ impl std::ops::DerefMut for HtmlElements {
impl std::fmt::Display for HtmlHead {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<head>{}", self.elements)?;

if self.syntax_highlighting_used {
write!(
let highlighting = if self.paged_js_used {
let _ = write!(
f,
"<style>{}</style>",
include_str!("../../styles/syntax_highlighting.css")
)?;
"<script>{}</script>",
include_str!("paged.polyfill.min.js")
);
include_str!("../../styles/syntax_highlighting_paged_js.css")
} else {
include_str!("../../styles/syntax_highlighting.css")
};
if self.syntax_highlighting_used {
write!(f, "<style>{}</style>", highlighting)?;
}

//TODO: write other head styles (try to use LightningCss optimizations)
Expand Down
4 changes: 4 additions & 0 deletions render/src/html/paged.polyfill.min.js

Large diffs are not rendered by default.

18 changes: 17 additions & 1 deletion render/src/html/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,25 @@ use super::{
};

#[derive(Debug, Default)]
pub struct HtmlRenderer {}
pub struct HtmlRenderer {
use_paged_js: bool,
}

impl HtmlRenderer {
pub fn new(use_paged_js: bool) -> Self {
HtmlRenderer { use_paged_js }
}
}

impl Renderer<Html> for HtmlRenderer {
fn get_target(&mut self) -> Result<Html, crate::log_id::RenderError> {
let html = Html::with_head(HtmlHead {
paged_js_used: self.use_paged_js,
..HtmlHead::default()
});
Ok(html)
}

fn render_paragraph(
&mut self,
paragraph: &unimarkup_parser::elements::atomic::Paragraph,
Expand Down
2 changes: 2 additions & 0 deletions render/src/html/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub enum HtmlTag {
Ul,
Li,
A,
Script,
}

impl HtmlTag {
Expand Down Expand Up @@ -59,6 +60,7 @@ impl HtmlTag {
HtmlTag::Ul => "ul",
HtmlTag::Li => "li",
HtmlTag::A => "a",
HtmlTag::Script => "script",
}
}
}
Expand Down
1 change: 1 addition & 0 deletions render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@

pub mod html;
pub mod log_id;
pub mod pdf;
pub mod render;
pub mod umi;
3 changes: 3 additions & 0 deletions render/src/log_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ pub enum RenderError {

#[error("Output format `append()` failed. See log: '{}: {}'", .0.event_id, .0.entry_id)]
BadAppend(FinalizedEvent<LogId>),

#[error("Unexpected error during pdf render: {}", .0)]
UnexpectedPdfError(String),
}
1 change: 1 addition & 0 deletions render/src/pdf/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod render;
41 changes: 41 additions & 0 deletions render/src/pdf/render.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::io::Write;

use headless_chrome::types::PrintToPdfOptions;
use headless_chrome::{Browser, LaunchOptions};
use tempfile::Builder;

use crate::log_id::RenderError;
use crate::log_id::RenderError::UnexpectedPdfError;

pub fn render_pdf(html: &str) -> Result<Vec<u8>, RenderError> {
let mut temp_html_file = Builder::new()
.suffix(".html")
.tempfile()
.map_err(|err| UnexpectedPdfError(err.to_string()))?;

temp_html_file
.write_all(html.as_bytes())
.map_err(|err| UnexpectedPdfError(err.to_string()))?;
let temp_file_url = format!(
"file://{}",
temp_html_file
.as_ref()
.as_os_str()
.to_str()
.ok_or(RenderError::Unimplemented)?
);

let browser = Browser::new(LaunchOptions::default())
.map_err(|err| UnexpectedPdfError(err.to_string()))?;
let pdf_bytes = browser
.new_tab()
.map_err(|err| UnexpectedPdfError(err.to_string()))?
.navigate_to(temp_file_url.as_str())
.map_err(|err| UnexpectedPdfError(err.to_string()))?
.wait_until_navigated()
.map_err(|err| UnexpectedPdfError(err.to_string()))?
.print_to_pdf(Some(PrintToPdfOptions::default()))
.map_err(|err| UnexpectedPdfError(err.to_string()))?;

Ok(pdf_bytes)
}
8 changes: 7 additions & 1 deletion render/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ pub trait OutputFormat: Default {
pub trait Renderer<T: OutputFormat> {
// Note: Default implementation with `Err(RenderError::Unimplemented)` prevents breaking changes when adding new functions to this trait.

/// Returns the [`OutputFormat`] for the [`Renderer`]. <br>
/// May be used to set custom modifications in the output format
fn get_target(&mut self) -> Result<T, RenderError> {
Err(RenderError::Unimplemented)
}

//--------------------------------- BLOCKS ---------------------------------

/// Render a Unimarkup [`Paragraph`] to the output format `T`.
Expand Down Expand Up @@ -260,7 +266,7 @@ pub trait Renderer<T: OutputFormat> {

/// Render Unimarkup [`Block`s](Block) to the output format `T`.
fn render_blocks(&mut self, blocks: &[Block], context: &Context) -> Result<T, RenderError> {
let mut t = T::default();
let mut t = self.get_target()?;

for block in blocks {
let rendered_block = match self.render_block(block, context) {
Expand Down
5 changes: 5 additions & 0 deletions render/src/umi/render.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use unimarkup_inline::element::InlineElement;
use unimarkup_parser::elements::blocks::Block;

use crate::log_id::RenderError;
use crate::render::{Context, OutputFormat, Renderer};
use std::collections::HashMap;

Expand Down Expand Up @@ -270,4 +271,8 @@ impl Renderer<Umi> for UmiRenderer {
context.get_lang().to_string(),
))
}

fn get_target(&mut self) -> Result<Umi, RenderError> {
Ok(Umi::default())
}
}
Loading
Loading