diff --git a/crates/epub/src/lib.rs b/crates/epub/src/lib.rs
index ebb9d3de5..162686eb0 100644
--- a/crates/epub/src/lib.rs
+++ b/crates/epub/src/lib.rs
@@ -436,13 +436,41 @@ fn story_html_with(doc: &Document, sid: StoryId, object: &mut dyn FnMut(&designc
let Some(st) = doc.story(sid) else { return String::new() };
let (pc, cc) = (para_classes(&doc.styles), char_classes(&doc.styles));
let mut out = String::new();
+ // Consecutive `li` paragraphs share one list (`ol` for a numbered style, `ul` for a bulleted
+ // one, an unmarked `ul` when the style has no list type of its own, e.g. typed bullets), and
+ // consecutive `pre` paragraphs of one style one block, their lines separated by newlines.
+ let mut list: Option<&str> = None;
+ let mut pre: Option<(String, bool)> = None;
for (pi, r) in st.para_ranges().iter().enumerate() {
let pf = &st.paras[pi];
// Export Tagging: the style's element and class.
let (tag, class) = export_mapping(&doc.styles, &pf.style, false, &pc);
let text_empty = st.text[r.clone()].trim().is_empty();
- let rtl = doc.styles.resolve_para(pf).0.direction == designcraft_doc::TextDirection::RightToLeft;
- let _ = write!(out, "<{tag} class=\"{}\"{}>", xml_attr(&class), if rtl { " dir=\"rtl\"" } else { "" });
+ let props = doc.styles.resolve_para(pf).0;
+ let rtl = props.direction == designcraft_doc::TextDirection::RightToLeft;
+ let continues_pre = tag == "pre" && pre.as_ref() == Some(&(class.clone(), rtl));
+ if !continues_pre && pre.take().is_some() {
+ out.push_str("\n");
+ }
+ let list_tag = (tag == "li").then_some(match props.list_type {
+ designcraft_doc::ListType::Numbers => "ol",
+ designcraft_doc::ListType::Bullets => "ul",
+ designcraft_doc::ListType::None => "ul style=\"list-style:none\"",
+ });
+ if list != list_tag {
+ if let Some(l) = list {
+ let _ = writeln!(out, "{}>", l.split(' ').next().unwrap_or(l));
+ }
+ if let Some(l) = list_tag {
+ let _ = writeln!(out, "<{l}>");
+ }
+ list = list_tag;
+ }
+ if continues_pre {
+ out.push('\n');
+ } else {
+ let _ = write!(out, "<{tag} class=\"{}\"{}>", xml_attr(&class), if rtl { " dir=\"rtl\"" } else { "" });
+ }
if text_empty {
out.push_str(" ");
}
@@ -497,7 +525,18 @@ fn story_html_with(doc: &Document, sid: StoryId, object: &mut dyn FnMut(&designc
let _ = write!(out, "<{ctag}{cls}{style_attr}>{t}{ctag}>");
}
}
- let _ = writeln!(out, "{tag}>");
+ if tag == "pre" {
+ // Closed when the next paragraph isn't a line of the same block.
+ pre = Some((class, rtl));
+ } else {
+ let _ = writeln!(out, "{tag}>");
+ }
+ }
+ if pre.is_some() {
+ out.push_str("\n");
+ }
+ if let Some(l) = list {
+ let _ = writeln!(out, "{}>", l.split(' ').next().unwrap_or(l));
}
out
}
@@ -895,6 +934,61 @@ mod tests {
assert!(!story_html(&d, sid).contains("{t}");
+ assert!(
+ html.contains(&format!(
+ "
x = 1\ny = 2\n
End
"), "{html}"); + assert_eq!(html.matches("\n