Skip to content

Commit 04deaba

Browse files
bokuwebcursoragent
andauthored
feat: add fitText support to run properties (#869)
Support OOXML fitText in both writer and reader so runs can preserve fixed-width text settings including optional grouping id. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7326ecf commit 04deaba

7 files changed

Lines changed: 123 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use crate::documents::BuildXML;
2+
use crate::xml_builder::*;
3+
use serde::Serialize;
4+
use std::io::Write;
5+
6+
#[derive(Debug, Clone, PartialEq, Serialize)]
7+
#[serde(rename_all = "camelCase")]
8+
pub struct FitText {
9+
pub val: usize,
10+
#[serde(skip_serializing_if = "Option::is_none")]
11+
pub id: Option<u32>,
12+
}
13+
14+
impl FitText {
15+
pub fn new(val: usize) -> Self {
16+
Self { val, id: None }
17+
}
18+
19+
pub fn id(mut self, id: u32) -> Self {
20+
self.id = Some(id);
21+
self
22+
}
23+
}
24+
25+
impl BuildXML for FitText {
26+
fn build_to<W: Write>(
27+
&self,
28+
stream: crate::xml::writer::EventWriter<W>,
29+
) -> crate::xml::writer::Result<crate::xml::writer::EventWriter<W>> {
30+
XMLBuilder::from(stream).fit_text(self.val, self.id)?.into_inner()
31+
}
32+
}
33+
34+
#[cfg(test)]
35+
mod tests {
36+
37+
use super::*;
38+
#[cfg(test)]
39+
use pretty_assertions::assert_eq;
40+
use std::str;
41+
42+
#[test]
43+
fn test_fit_text() {
44+
let b = FitText::new(840).id(1266434317).build();
45+
assert_eq!(
46+
str::from_utf8(&b).unwrap(),
47+
r#"<w:fitText w:val="840" w:id="1266434317" />"#
48+
);
49+
}
50+
51+
#[test]
52+
fn test_fit_text_without_id() {
53+
let b = FitText::new(840).build();
54+
assert_eq!(str::from_utf8(&b).unwrap(), r#"<w:fitText w:val="840" />"#);
55+
}
56+
}

docx-core/src/documents/elements/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ mod doc_var;
3030
mod drawing;
3131
mod dstrike;
3232
mod fld_char;
33+
mod fit_text;
3334
mod font;
3435
mod font_scheme;
3536
mod footer_reference;
@@ -170,6 +171,7 @@ pub use doc_var::*;
170171
pub use drawing::*;
171172
pub use dstrike::*;
172173
pub use fld_char::*;
174+
pub use fit_text::*;
173175
pub use font::*;
174176
pub use font_scheme::*;
175177
pub use footer_reference::*;

docx-core/src/documents/elements/run_property.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ pub struct RunProperty {
4040
#[serde(skip_serializing_if = "Option::is_none")]
4141
pub character_spacing: Option<CharacterSpacing>,
4242
#[serde(skip_serializing_if = "Option::is_none")]
43+
pub fit_text: Option<FitText>,
44+
#[serde(skip_serializing_if = "Option::is_none")]
4345
pub stretch: Option<Stretch>,
4446
#[serde(skip_serializing_if = "Option::is_none")]
4547
pub fonts: Option<RunFonts>,
@@ -176,6 +178,15 @@ impl RunProperty {
176178
self
177179
}
178180

181+
pub fn fit_text(mut self, val: usize, id: Option<u32>) -> RunProperty {
182+
let mut fit_text = FitText::new(val);
183+
if let Some(id) = id {
184+
fit_text = fit_text.id(id);
185+
}
186+
self.fit_text = Some(fit_text);
187+
self
188+
}
189+
179190
pub fn text_border(mut self, b: TextBorder) -> Self {
180191
self.text_border = Some(b);
181192
self
@@ -229,6 +240,7 @@ impl BuildXML for RunProperty {
229240
.add_optional_child(&self.del)?
230241
.add_optional_child(&self.vert_align)?
231242
.add_optional_child(&self.character_spacing)?
243+
.add_optional_child(&self.fit_text)?
232244
.add_optional_child(&self.stretch)?
233245
.add_optional_child(&self.style)?
234246
.add_optional_child(&self.positional_tab)?
@@ -336,6 +348,16 @@ mod tests {
336348
);
337349
}
338350

351+
#[test]
352+
fn test_fit_text() {
353+
let c = RunProperty::new().fit_text(840, Some(1266434317));
354+
let b = c.build();
355+
assert_eq!(
356+
str::from_utf8(&b).unwrap(),
357+
r#"<w:rPr><w:fitText w:val="840" w:id="1266434317" /></w:rPr>"#
358+
);
359+
}
360+
339361
#[test]
340362
fn test_ptab() {
341363
let c = RunProperty::new().ptab(PositionalTab::new(

docx-core/src/reader/run.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,4 +311,25 @@ mod tests {
311311
}
312312
);
313313
}
314+
315+
#[test]
316+
fn test_read_fit_text() {
317+
let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
318+
<w:r><w:rPr>
319+
<w:fitText w:val="840" w:id="1266434317"/>
320+
</w:rPr></w:r>
321+
</w:document>"#;
322+
let mut parser = EventReader::new(c.as_bytes());
323+
let run = Run::read(&mut parser, &[]).unwrap();
324+
assert_eq!(
325+
run,
326+
Run {
327+
children: vec![],
328+
run_property: RunProperty {
329+
fit_text: Some(FitText::new(840).id(1266434317)),
330+
..RunProperty::default()
331+
},
332+
}
333+
);
334+
}
314335
}

docx-core/src/reader/run_property.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ impl ElementReader for RunProperty {
115115
}
116116
}
117117
}
118+
XMLElement::FitText => {
119+
if let Some(v) = read_val(&attributes) {
120+
if let Ok(val) = usize::from_str(&v) {
121+
let id = read(&attributes, "id")
122+
.and_then(|id| u32::from_str(&id).ok());
123+
rp = rp.fit_text(val, id);
124+
}
125+
}
126+
}
118127
XMLElement::RunFonts => {
119128
if let Ok(f) = read_run_fonts(&attributes) {
120129
rp = rp.fonts(f);

docx-core/src/reader/xml_element.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub enum XMLElement {
1818
Size,
1919
SizeCs,
2020
Spacing,
21+
FitText,
2122
Vanish,
2223
SpecVanish,
2324
TextBorder,
@@ -356,6 +357,7 @@ impl FromStr for XMLElement {
356357
"next" => Ok(XMLElement::Next),
357358
"vertAlign" => Ok(XMLElement::VertAlign),
358359
"spacing" => Ok(XMLElement::Spacing),
360+
"fitText" => Ok(XMLElement::FitText),
359361
"styles" => Ok(XMLElement::Styles),
360362
"Relationships" => Ok(XMLElement::Relationships),
361363
"Relationship" => Ok(XMLElement::Relationship),

docx-core/src/xml_builder/elements.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,17 @@ impl<W: Write> XMLBuilder<W> {
284284
.close()
285285
}
286286

287+
// i.e. <w:fitText ... >
288+
pub(crate) fn fit_text(self, val: usize, id: Option<u32>) -> Result<Self> {
289+
let mut fit_text = XmlEvent::start_element("w:fitText").attr("w:val", &format!("{}", val));
290+
let id_value;
291+
if let Some(id) = id {
292+
id_value = format!("{}", id);
293+
fit_text = fit_text.attr("w:id", &id_value);
294+
}
295+
self.write(fit_text)?.close()
296+
}
297+
287298
// i.e. <w:spacing ... >
288299
pub(crate) fn line_spacing(
289300
self,

0 commit comments

Comments
 (0)