diff --git a/src/epub/x4.rs b/src/epub/x4.rs index e0e936a..7ccfca8 100644 --- a/src/epub/x4.rs +++ b/src/epub/x4.rs @@ -30,6 +30,9 @@ pub const MAX_WORD_CHARS: usize = 200; /// U+00AD, invisible unless the renderer actually needs to break there. const SOFT_HYPHEN: char = '\u{00ad}'; +/// U+00A0, rendered as visible indentation even when the X4 collapses whitespace. +const NO_BREAK_SPACE: char = '\u{00a0}'; + /// Elements whose content is code, not prose, and must be copied through /// untouched — a soft hyphen inside a stylesheet would corrupt it. const RAW_TEXT_ELEMENTS: &[&str] = &["script", "style"]; @@ -127,14 +130,28 @@ pub fn simplify_xhtml(xhtml: &str) -> String { break_long_words(&strip_attributes(xhtml, DROPPED_ATTRIBUTES)) } -/// Insert soft hyphens into words longer than [`MAX_WORD_CHARS`], in text -/// content only (§3.10). +/// Insert soft hyphens into over-long words and make `
` whitespace
+/// explicit, in text content only (§3.10).
 fn break_long_words(html: &str) -> String {
     let mut out = String::with_capacity(html.len());
     let mut cursor = 0usize;
+    let mut inside_pre = false;
+    let mut at_line_start = false;
+    // A newline right after `
` (or `
`) is not a blank line:
+    // browsers drop it, so we do too.
+    let mut pre_just_opened = false;
     while let Some(rel) = html[cursor..].find('<') {
         let start = cursor + rel;
-        soften_text(&html[cursor..start], &mut out);
+        if inside_pre {
+            preserve_pre_whitespace(
+                &html[cursor..start],
+                &mut out,
+                &mut at_line_start,
+                &mut pre_just_opened,
+            );
+        } else {
+            soften_text(&html[cursor..start], &mut out);
+        }
         let Some(end) = tag_end(html, start) else {
             out.push_str(&html[start..]);
             return out;
@@ -142,6 +159,17 @@ fn break_long_words(html: &str) -> String {
         let tag = &html[start..end];
         out.push_str(tag);
         cursor = end;
+        if is_element_tag(tag, "pre", false) {
+            inside_pre = true;
+            at_line_start = true;
+            pre_just_opened = true;
+        } else if is_element_tag(tag, "pre", true) {
+            inside_pre = false;
+        } else if inside_pre && is_element_tag(tag, "br", false) {
+            at_line_start = true;
+        } else if inside_pre && !is_element_tag(tag, "code", false) {
+            pre_just_opened = false;
+        }
         // `