From 9a5a35334a05ce86a28b22479eddbfc4742d238e Mon Sep 17 00:00:00 2001 From: pakiat <49967840+pakiat@users.noreply.github.com> Date: Sat, 14 Nov 2026 11:52:27 +0000 Subject: [PATCH] craft-docx: a field's Thai result takes the complex-script formatting only where Word does Measured in Word for Windows: Thai text as a field's result follows the rule ordinary text does. mark_runs_as_word_draws_them counts the result of a field kept as one object as the text of its run. Co-Authored-By: Claude Opus 5.5 --- crates/docx/tests/it/bidi.rs | 37 ++++++++++++++++++++++++++++++++++ crates/wordmodel/src/styles.rs | 22 ++++++++++++++++---- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/crates/docx/tests/it/bidi.rs b/crates/docx/tests/it/bidi.rs index 6dae963aa..bea7e2131 100644 --- a/crates/docx/tests/it/bidi.rs +++ b/crates/docx/tests/it/bidi.rs @@ -290,3 +290,40 @@ fn thai_text_takes_the_complex_script_formatting_only_in_a_complex_script_run() assert_eq!(drawn, [("Angsana New".to_string(), 22.0), ("Tahoma".to_string(), 11.0), ("Angsana New".to_string(), 22.0)], "{when}"); } } + +/// Measured in Word for Windows: Thai text as a field's result follows the same rule as ordinary +/// text, also when the reader keeps the field as one object with its result. +#[test] +fn a_fields_thai_result_takes_the_complex_script_formatting_only_in_a_complex_script_run() { + let styles = r#" +"#; + let field = |rpr: &str| { + let r = |inner: &str| format!("{rpr}{inner}"); + format!( + "{}{}{}{}{}", + r(r#""#), + r(r#" QUOTE "x" "#), + r(r#""#), + r("ทดสอบ"), + r(r#""#) + ) + }; + let drawn = |d: &Document| { + let p = paras(d)[0]; + let at = p.text.find(craft_wordmodel::para::OBJ).expect("the field is one object"); + let run = p.runs.iter().scan(0, |end, r| { + let start = *end; + *end += r.len; + Some((start, r)) + }); + let (_, run) = run.into_iter().find(|(start, r)| *start <= at && at < start + r.len).unwrap(); + let rc = d.styles.resolve_char(p.props.style.as_deref(), &run.props); + if rc.uses_complex('ท') { (rc.font_cs.clone(), rc.size_cs) } else { (rc.font.clone(), rc.size) } + }; + for (what, rpr, want) in [("nothing", "", ("Angsana New", 10.0)), ("w:cs", "", ("Tahoma", 20.0))] { + let d = craft_docx::read(&docx_with_styles(&field(rpr), styles)).unwrap(); + assert_eq!(drawn(&d), (want.0.to_string(), want.1), "a run with {what}: read"); + let again = craft_docx::read(&craft_docx::write(&d).unwrap()).unwrap(); + assert_eq!(drawn(&again), (want.0.to_string(), want.1), "a run with {what}: after a save"); + } +} diff --git a/crates/wordmodel/src/styles.rs b/crates/wordmodel/src/styles.rs index 81d8cd272..1d725efa2 100644 --- a/crates/wordmodel/src/styles.rs +++ b/crates/wordmodel/src/styles.rs @@ -841,11 +841,25 @@ impl StyleSheet { /// right to left. pub fn mark_runs_as_word_draws_them(&self, para: &mut crate::para::Paragraph, table: Option<&CharProps>) { use crate::bidi::{is_complex_script, is_rtl_script}; - let style = para.props.style.as_deref(); - let mut at = 0; - for run in &mut para.runs { - let text = para.text.get(at..at + run.len).unwrap_or(""); + use crate::para::{InlineObject, OBJ}; + // What each run draws: its text, a field kept as one object standing for its result + // (drawn in the run's formatting, and measured in Word to follow the same rule). + let mut drawn = Vec::with_capacity(para.runs.len()); + let (mut at, mut objects) = (0, para.objects.iter()); + for run in ¶.runs { + let mut text = String::new(); + for c in para.text.get(at..at + run.len).unwrap_or("").chars() { + if c != OBJ { + text.push(c); + } else if let Some(InlineObject::Field { result, code: false, .. }) = objects.next() { + text.push_str(result); + } + } at += run.len; + drawn.push(text); + } + let style = para.props.style.as_deref(); + for (run, text) in para.runs.iter_mut().zip(&drawn) { if text.is_empty() || text.chars().any(is_rtl_script) { continue; }