diff --git a/src/formats/docx/content.rs b/src/formats/docx/content.rs index c53c1ecf..44906f25 100644 --- a/src/formats/docx/content.rs +++ b/src/formats/docx/content.rs @@ -14,7 +14,7 @@ use crate::shared::delta::rebase_emphasis; use crate::shared::fields::{FieldFrame, field_result}; use crate::shared::header::resolve_header_rows; use crate::shared::list::{ListEntry, ListKey, flush_list}; -use crate::shared::text::{clean_text, is_xml_space}; +use crate::shared::text::clean_text; use std::cell::RefCell; use std::collections::HashMap; @@ -475,20 +475,10 @@ impl<'a, 'b, 'e> InlineWalker<'a, 'b, 'e> { } match child.local.as_str() { "t" => { - // Open XML text-space contract: edge whitespace in w:t - // is significant only under xml:space="preserve"; - // unmarked edges are discarded (Word never renders them). - // Only XML whitespace counts: a no-break space is - // character data, not whitespace the contract may drop. - let preserved = child.attr_qualified(ns::XML, "space") == Some("preserve"); - let raw = child.text(); - // The contract applies to the XML text, before - // normalization turns a no-break space into a space. - let text = clean_text(if preserved { - raw.as_ref() - } else { - raw.trim_matches(is_xml_space) - }); + // Run edges carry the spacing between words in documents + // that never mark xml:space, and XML leaves unmarked + // whitespace to the application, so it is kept. + let text = clean_text(child.text().as_ref()); if !text.is_empty() { self.push(Inline::Text { text, style }); } diff --git a/src/formats/docx/mod.rs b/src/formats/docx/mod.rs index 21dfef57..eee5317e 100644 --- a/src/formats/docx/mod.rs +++ b/src/formats/docx/mod.rs @@ -248,36 +248,19 @@ mod tests { } #[test] - fn unpreserved_edge_whitespace_is_discarded() { - // M3: w:t edge whitespace is significant only under - // xml:space="preserve". + fn unmarked_run_edge_whitespace_is_kept() { + // Converters that never write xml:space carry inter-word spacing on + // run edges; dropping it glues the words together. let document = format!( r#" - lead - discarded - kept - tail + This + by-law + grants "# ); let doc = parse(&docx_parts(&[("word/document.xml", &document)])).unwrap(); let Some(Block::Paragraph(inlines)) = doc.blocks.first() else { panic!() }; - assert_eq!(crate::model::inlines_to_plain_text(inlines), "leaddiscarded kept tail"); - } - - #[test] - fn unpreserved_no_break_space_is_kept() { - // The xml:space contract governs XML whitespace; a no-break space is - // character data, so it survives an unmarked edge. - let nbsp = '\u{a0}'; - let document = format!( - r#" - before{nbsp} - after - "# - ); - let doc = parse(&docx_parts(&[("word/document.xml", &document)])).unwrap(); - let Some(Block::Paragraph(inlines)) = doc.blocks.first() else { panic!() }; - assert_eq!(crate::model::inlines_to_plain_text(inlines), "before after"); + assert_eq!(crate::model::inlines_to_plain_text(inlines), "This by-law grants"); } #[test] diff --git a/src/shared/text.rs b/src/shared/text.rs index f5bd0daa..aa151948 100644 --- a/src/shared/text.rs +++ b/src/shared/text.rs @@ -26,13 +26,6 @@ pub fn clean_text(text: &str) -> String { out } -/// XML whitespace, the S production of XML 1.0: the only characters an -/// `xml:space` contract governs. Everything else, a no-break space included, -/// is character data. -pub fn is_xml_space(c: char) -> bool { - matches!(c, ' ' | '\t' | '\r' | '\n') -} - /// Collapse whitespace runs to single spaces. pub fn collapse_ws(text: &str) -> String { let mut out = String::with_capacity(text.len());