use crate::markdown::MarkdownError;
use crate::markdown::export::native_document_image::NativeDocumentImage;
use crate::markdown::export::native_style::NativeDocumentStyle;
use crate::markdown::svg_rasterize::SvgRasterizeOps;
const TEXT_CAPTION_TRUNCATED: &str = "... Export truncated by native export safety limit.";
const PAGE_WIDTH: u32 = 900;
const MIN_PAGE_HEIGHT: u32 = 480;
const PAGE_MARGIN: u32 = 48;
const BLOCK_GAP: u32 = 22;
const HEADING_EXTRA_GAP: u32 = 10;
const CODE_FONT_FAMILY: &str =
"Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace";
const MAX_TEXT_LINES: usize = 600;
struct NativeTextElementStyle<'a> {
font_family: &'static str,
font_weight: &'static str,
font_size: u32,
text_color: &'a str,
}
impl<'a> NativeTextElementStyle<'a> {
fn new(line: &super::native_text::NativeTextLine, text_color: &'a str) -> Self {
Self {
font_family: font_family_for(line),
font_weight: font_weight_for(line),
font_size: line.font_size,
text_color,
}
}
}
struct NativeTextSpanElementOps;
impl NativeTextSpanElementOps {
fn render(spans: &[super::native_text::NativeTextSpan]) -> String {
spans.iter().map(Self::render_one).collect()
}
fn render_one(span: &super::native_text::NativeTextSpan) -> String {
let [r, g, b] = span.color;
let color = format!("#{r:02x}{g:02x}{b:02x}");
let text = super::native_text_runs::NativeTextRuns::render(&span.text);
format!(r##"<tspan fill="{color}">{text}</tspan>"##)
}
}
fn font_family_for(line: &super::native_text::NativeTextLine) -> &'static str {
if line.is_code {
return CODE_FONT_FAMILY;
}
super::native_text_runs::NativeTextRuns::font_family()
}
fn font_weight_for(line: &super::native_text::NativeTextLine) -> &'static str {
if line.bold {
return "bold";
}
"normal"
}
pub(crate) struct NativeHtmlDocument {
blocks: Vec<super::native_blocks::NativeDocumentBlock>,
style: NativeDocumentStyle,
}
impl NativeHtmlDocument {
pub(crate) fn parse(html: &str) -> Result<Self, MarkdownError> {
let style = NativeDocumentStyle::parse(html);
let is_dark = super::native_text::is_dark_background(style.background_color());
super::native_blocks::NativeDocumentBlocks::parse(html, is_dark)
.map(|blocks| Self { blocks, style })
}
pub(crate) fn render_image(&self) -> Result<NativeDocumentImage, MarkdownError> {
let svg = self.render_svg()?;
SvgRasterizeOps::rasterize_svg(&svg, 1.0)
.map(NativeDocumentImage::from)
.map_err(|error| MarkdownError::ExportFailed(error.to_string()))
}
fn render_svg(&self) -> Result<String, MarkdownError> {
let blocks = self.visible_blocks();
let mut content = String::new();
let mut y = PAGE_MARGIN;
for block in &blocks {
match block {
super::native_blocks::NativeDocumentBlock::Text(line) => {
if line.is_heading() {
y += HEADING_EXTRA_GAP;
}
y += line.line_height();
content.push_str(&self.text_element(line, y));
}
super::native_blocks::NativeDocumentBlock::Svg(svg) => {
y += BLOCK_GAP;
let scale = svg.scale_for(PAGE_WIDTH - PAGE_MARGIN * 2);
content.push_str(&self.svg_element(svg, y, scale));
y += (svg.height as f32 * scale).ceil() as u32 + BLOCK_GAP;
}
}
}
let page_height = (y + PAGE_MARGIN).max(MIN_PAGE_HEIGHT);
let background_color = self.style.background_color();
Ok(format!(
r##"<svg xmlns="http://www.w3.org/2000/svg" width="{PAGE_WIDTH}" height="{page_height}" viewBox="0 0 {PAGE_WIDTH} {page_height}"><rect width="100%" height="100%" fill="{background_color}"/>{content}</svg>"##
))
}
fn visible_blocks(&self) -> Vec<super::native_blocks::NativeDocumentBlock> {
let mut blocks = self.blocks.clone();
truncate_text_blocks(&mut blocks);
blocks
}
fn text_element(&self, line: &super::native_text::NativeTextLine, y: u32) -> String {
if line.spans.is_empty() {
return self.plain_text_element(line, y);
}
self.spans_text_element(line, y)
}
fn plain_text_element(&self, line: &super::native_text::NativeTextLine, y: u32) -> String {
let style = NativeTextElementStyle::new(line, self.style.text_color());
let content = super::native_text_runs::NativeTextRuns::render(&line.text);
format!(
r##"<text x="{PAGE_MARGIN}" y="{y}" font-size="{font_size}" font-weight="{font_weight}" font-family="{font_family}" fill="{text_color}">{content}</text>"##,
font_size = style.font_size,
font_weight = style.font_weight,
font_family = style.font_family,
text_color = style.text_color,
)
}
fn spans_text_element(&self, line: &super::native_text::NativeTextLine, y: u32) -> String {
let style = NativeTextElementStyle::new(line, self.style.text_color());
let spans_html = NativeTextSpanElementOps::render(&line.spans);
format!(
r##"<text x="{PAGE_MARGIN}" y="{y}" font-size="{font_size}" font-weight="{font_weight}" font-family="{font_family}">{spans_html}</text>"##,
font_size = style.font_size,
font_weight = style.font_weight,
font_family = style.font_family,
)
}
fn svg_element(
&self,
svg: &super::native_blocks::NativeSvgBlock,
y: u32,
scale: f32,
) -> String {
let scale = format!("{scale:.4}");
format!(
r#"<g transform="translate({PAGE_MARGIN} {y}) scale({scale})">{}</g>"#,
svg.svg
)
}
}
fn truncate_text_blocks(blocks: &mut Vec<super::native_blocks::NativeDocumentBlock>) {
let mut text_count = 0;
blocks.retain(|block| match block {
super::native_blocks::NativeDocumentBlock::Text(_) => {
text_count += 1;
text_count <= MAX_TEXT_LINES
}
super::native_blocks::NativeDocumentBlock::Svg(_) => true,
});
if text_count > MAX_TEXT_LINES {
blocks.push(super::native_blocks::NativeDocumentBlock::Text(
super::native_text::NativeTextLine::body(TEXT_CAPTION_TRUNCATED.to_string()),
));
}
}
#[cfg(test)]
#[path = "native_document_tests.rs"]
mod tests;