From 0212e407e5a0f851d75dab9c04d37c0da16c0e62 Mon Sep 17 00:00:00 2001 From: Tyler Hallada Date: Mon, 7 Sep 2026 17:34:40 +0000 Subject: [PATCH] Make code-block whitespace explicit in the X4 edition The X4 firmware collapses whitespace inside
 regardless of
white-space: pre-wrap, so code blocks came out as one run-on line. Turn
newlines into 
, tabs and indentation into no-break spaces, and drop the newline right after
 as browsers do.

Co-Authored-By: Claude Fable 5.1 
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
---
 src/epub/x4.rs | 161 +++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 157 insertions(+), 4 deletions(-)

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;
+        }
         // `