use super::as_docx::{hex, rgb8}; use super::layout::{Item, Page, items}; /// Whether a character may appear in the output. XML 1.0 forbids the C0 controls other than tab, /// line feed and carriage return, and U+FFFE/U+FFFF. Text extracted from PDFs often holds them /// (fonts without a usable `/ToUnicode`), and Word refuses a whole document over one (#72). pub(crate) fn allowed(c: char) -> bool { matches!(c, '\t' | '\n' | '\r') || !(c.is_ascii_control() || c == '\u{FFFE}' || c == '\u{FFFF}') } pub(crate) fn esc(s: &str) -> String { let mut o = String::with_capacity(s.len()); for c in s.chars().filter(|c| allowed(*c)) { match c { '&' => o.push_str("&"), '<' => o.push_str("<"), '>' => o.push_str(">"), '"' => o.push_str("""), c => o.push(c), } } o } pub(crate) fn base64(data: &[u8]) -> String { const A: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; let mut s = String::with_capacity(data.len().div_ceil(3) * 4); for c in data.chunks(3) { let n = (c[0] as u32) << 16 | (*c.get(1).unwrap_or(&0) as u32) << 8 | *c.get(2).unwrap_or(&0) as u32; s.push(A[(n >> 18) as usize & 63] as char); s.push(A[(n >> 12) as usize & 63] as char); s.push(if c.len() > 1 { A[(n >> 6) as usize & 63] as char } else { '=' }); s.push(if c.len() > 2 { A[n as usize & 63] as char } else { '=' }); } s } /// One HTML file: headings and paragraphs, tables as real `` elements, images inline, a /// rule between pages. pub fn html(pages: &[Page], title: &str) -> String { html_with_mode(pages, title, false) } /// A simple HTML export with no CSS; page layout and colours are omitted. pub fn html_simple(pages: &[Page], title: &str) -> String { html_with_mode(pages, title, true) } fn html_with_mode(pages: &[Page], title: &str, simple: bool) -> String { let mut s = format!( "\n\n\n\n{}\n{}\n\n\n", esc(title), if simple { "" } else { "" } ); for it in items(pages) { match it { Item::Para(runs, lvl) => { let Some(first) = runs.first().map(|r| r.block) else { continue }; let mut t = String::new(); for r in &runs { let mut run = esc(&r.text); if r.block.italic { run = format!("{run}"); } if r.block.bold && lvl == 0 { run = format!("{run}"); } // The paragraph has its first run's colour; a run in another colour says so. let colour = rgb8(r.block.color); if !simple && colour != rgb8(first.color) { let [red, green, blue] = colour; run = format!("{run}"); } t.push_str(&run); } let style = if simple { String::new() } else { hex(first.color).map_or(String::new(), |h| format!(" style=\"color:#{h}\"")) }; match lvl { 0 => s.push_str(&format!("{t}

\n")), l => s.push_str(&format!("{t}\n")), } } Item::Table(t) => { s.push_str("
\n"); for row in &t.rows { s.push_str(""); for c in row { let mut body = esc(&c.text); if c.italic { body = format!("{body}"); } if c.bold { body = format!("{body}"); } let span = if c.span > 1 { format!(" colspan=\"{}\"", c.span) } else { String::new() }; let style = if simple { String::new() } else { hex(c.color).map_or(String::new(), |h| format!(" style=\"color:#{h}\"")) }; s.push_str(&format!("{body}")); } s.push_str("\n"); } s.push_str("
\n"); } Item::Img(im) => { let mime = if im.ext == "jpg" { "image/jpeg" } else { "image/png" }; s.push_str(&format!("

\"\"

\n", base64(&im.bytes))); } Item::PageBreak => s.push_str("
\n"), } } s.push_str("\n\n"); s }