diff --git a/crates/export/src/as_html.rs b/crates/export/src/as_html.rs
index 8c250ca484..e2b3379ac2 100644
--- a/crates/export/src/as_html.rs
+++ b/crates/export/src/as_html.rs
@@ -1,5 +1,5 @@
use super::as_docx::{hex, rgb8};
-use super::layout::{Item, Page, items};
+use super::layout::{Item, Page, Run, Table, 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
@@ -94,6 +94,64 @@ pub fn html_simple_with_language(pages: &[Page], title: &str, language: Option<&
html_with_mode(pages, title, true, language)
}
+fn style_color(color: [f64; 3], simple: bool) -> String {
+ if simple { String::new() } else { hex(color).map_or(String::new(), |h| format!(" style=\"color:#{h}\"")) }
+}
+
+fn wrap_inline(mut text: String, bold: bool, italic: bool) -> String {
+ if text.trim().is_empty() {
+ return text;
+ }
+ if italic {
+ text = format!("{text}");
+ }
+ if bold {
+ text = format!("{text}");
+ }
+ text
+}
+
+fn para_html(runs: &[Run<'_>], lvl: u8, simple: bool) -> Option {
+ let non_empty: Vec<_> = runs.iter().map(|r| (r.block, esc(&r.text))).filter(|(_, text)| !text.is_empty()).collect();
+ let &(first, _) = non_empty.iter().find(|(_, text)| !text.trim().is_empty())?;
+ let mut t = String::new();
+ for (block, text) in non_empty {
+ if text.trim().is_empty() {
+ t.push_str(&text);
+ continue;
+ }
+ let mut run = wrap_inline(text, block.bold && lvl == 0, block.italic);
+ // The paragraph has its first visible run's colour; a run in another colour says so.
+ let colour = rgb8(block.color);
+ if !simple && colour != rgb8(first.color) {
+ let [red, green, blue] = colour;
+ run = format!("{run}");
+ }
+ t.push_str(&run);
+ }
+ let style = style_color(first.color, simple);
+ Some(match lvl.min(6) {
+ 0 => format!("{t}
\n"),
+ l => format!("{t}\n"),
+ })
+}
+
+fn table_html(t: &Table, simple: bool) -> String {
+ let mut s = String::from("\n");
+ for row in &t.rows {
+ s.push_str("");
+ for c in row {
+ let body = wrap_inline(esc(&c.text), c.bold, c.italic);
+ let span = if c.span > 1 { format!(" colspan=\"{}\"", c.span) } else { String::new() };
+ let style = style_color(c.color, simple);
+ s.push_str(&format!("| {body} | "));
+ }
+ s.push_str("
\n");
+ }
+ s.push_str("
\n");
+ s
+}
+
fn html_with_mode(pages: &[Page], title: &str, simple: bool, language: Option<&str>) -> String {
let lang = language_tag(language).map(|tag| format!(" lang=\"{}\"", esc(tag))).unwrap_or_default();
let mut s = format!(
@@ -108,50 +166,11 @@ fn html_with_mode(pages: &[Page], title: &str, simple: bool, language: Option<&s
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")),
+ if let Some(p) = para_html(&runs, lvl, simple) {
+ s.push_str(&p);
}
}
- 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::Table(t) => s.push_str(&table_html(&t, simple)),
Item::Img(im) => {
let mime = if im.ext == "jpg" { "image/jpeg" } else { "image/png" };
s.push_str(&format!("
\n", base64(&im.bytes)));
@@ -162,3 +181,71 @@ fn html_with_mode(pages: &[Page], title: &str, simple: bool, language: Option<&s
s.push_str("