Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 5 additions & 15 deletions src/formats/docx/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 });
}
Expand Down
31 changes: 7 additions & 24 deletions src/formats/docx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
// Converters that never write xml:space carry inter-word spacing on
// run edges; dropping it glues the words together.
let document = format!(
r#"<w:document {W}><w:body><w:p>
<w:r><w:t>lead</w:t></w:r>
<w:r><w:t> discarded </w:t></w:r>
<w:r><w:t xml:space="preserve"> kept </w:t></w:r>
<w:r><w:t>tail</w:t></w:r>
<w:r><w:t>This</w:t></w:r>
<w:r><w:t> by-law</w:t></w:r>
<w:r><w:t> grants</w:t></w:r>
</w:p></w:body></w:document>"#
);
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#"<w:document {W}><w:body><w:p>
<w:r><w:t>before{nbsp}</w:t></w:r>
<w:r><w:t>after</w:t></w:r>
</w:p></w:body></w:document>"#
);
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]
Expand Down
7 changes: 0 additions & 7 deletions src/shared/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down