Skip to main content

docx_rs/documents/elements/
paragraph_property.rs

1use serde::Serialize;
2use std::io::Write;
3
4use super::*;
5use crate::documents::BuildXML;
6use crate::types::{AlignmentType, SpecialIndentType};
7use crate::ParagraphBorderPosition;
8use crate::{xml_builder::*, TextAlignmentType};
9
10#[derive(Serialize, Debug, Clone, PartialEq, Default)]
11#[serde(rename_all = "camelCase")]
12pub struct ParagraphProperty {
13    pub run_property: RunProperty,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub style: Option<ParagraphStyle>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub numbering_property: Option<NumberingProperty>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub alignment: Option<Justification>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub indent: Option<Indent>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub line_spacing: Option<LineSpacing>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub keep_next: Option<bool>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub keep_lines: Option<bool>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub bidi: Option<bool>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub page_break_before: Option<bool>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub widow_control: Option<bool>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub outline_lvl: Option<OutlineLvl>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub section_property: Option<SectionProperty>,
38    pub tabs: Vec<Tab>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub paragraph_property_change: Option<ParagraphPropertyChange>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub borders: Option<ParagraphBorders>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub frame_property: Option<FrameProperty>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub text_alignment: Option<TextAlignment>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub adjust_right_ind: Option<AdjustRightInd>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub snap_to_grid: Option<bool>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub shading: Option<Shading>,
53    // read only
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub(crate) div_id: Option<String>,
56}
57
58// 17.3.1.26
59// pPr (Paragraph Properties)
60// This element specifies a set of paragraph properties which shall be applied to the contents of the parent
61// paragraph after all style/numbering/table properties have been applied to the text. These properties are defined
62// as direct formatting, since they are directly applied to the paragraph and supersede any formatting from styles.
63impl ParagraphProperty {
64    pub fn new() -> ParagraphProperty {
65        Default::default()
66    }
67
68    pub fn align(mut self, alignment_type: AlignmentType) -> Self {
69        self.alignment = Some(Justification::new(alignment_type.to_string()));
70        self
71    }
72
73    pub fn style(mut self, style_id: &str) -> Self {
74        self.style = Some(ParagraphStyle::new(Some(style_id)));
75        self
76    }
77
78    pub fn indent(
79        mut self,
80        left: Option<i32>,
81        special_indent: Option<SpecialIndentType>,
82        end: Option<i32>,
83        start_chars: Option<i32>,
84    ) -> Self {
85        self.indent = Some(Indent::new(left, special_indent, end, start_chars));
86        self
87    }
88
89    pub fn numbering(mut self, id: NumberingId, level: IndentLevel) -> Self {
90        self.numbering_property = Some(NumberingProperty::new().add_num(id, level));
91        self
92    }
93
94    pub fn numbering_property(mut self, np: NumberingProperty) -> Self {
95        self.numbering_property = Some(np);
96        self
97    }
98
99    pub fn line_spacing(mut self, spacing: LineSpacing) -> Self {
100        self.line_spacing = Some(spacing);
101        self
102    }
103
104    pub fn character_spacing(mut self, spacing: i32) -> Self {
105        self.run_property.character_spacing = Some(CharacterSpacing::new(spacing));
106        self
107    }
108
109    pub fn snap_to_grid(mut self, v: bool) -> Self {
110        self.snap_to_grid = Some(v);
111        self
112    }
113
114    pub fn shading(mut self, s: Shading) -> Self {
115        self.shading = Some(s);
116        self
117    }
118
119    pub fn keep_next(mut self, v: bool) -> Self {
120        self.keep_next = Some(v);
121        self
122    }
123
124    pub fn keep_lines(mut self, v: bool) -> Self {
125        self.keep_lines = Some(v);
126        self
127    }
128
129    pub fn outline_lvl(mut self, v: usize) -> Self {
130        if v >= 10 {
131            // clamped
132            self.outline_lvl = Some(OutlineLvl::new(9));
133            return self;
134        }
135        self.outline_lvl = Some(OutlineLvl::new(v));
136        self
137    }
138
139    pub fn page_break_before(mut self, v: bool) -> Self {
140        self.page_break_before = Some(v);
141        self
142    }
143    pub fn bidi(mut self, v: bool) -> Self {
144        self.bidi = Some(v);
145        self
146    }
147    pub fn widow_control(mut self, v: bool) -> Self {
148        self.widow_control = Some(v);
149        self
150    }
151
152    pub fn add_tab(mut self, t: Tab) -> Self {
153        self.tabs.push(t);
154        self
155    }
156
157    pub fn section_property(mut self, s: SectionProperty) -> Self {
158        self.section_property = Some(s);
159        self
160    }
161
162    pub fn paragraph_property_change(mut self, p: ParagraphPropertyChange) -> Self {
163        self.paragraph_property_change = Some(p);
164        self
165    }
166
167    pub fn frame_property(mut self, s: FrameProperty) -> Self {
168        self.frame_property = Some(s);
169        self
170    }
171
172    pub fn run_property(mut self, s: RunProperty) -> Self {
173        self.run_property = s;
174        self
175    }
176
177    pub fn text_alignment(mut self, s: TextAlignmentType) -> Self {
178        self.text_alignment = Some(TextAlignment::new(s));
179        self
180    }
181
182    pub fn adjust_right_ind(mut self, s: isize) -> Self {
183        self.adjust_right_ind = Some(AdjustRightInd::new(s));
184        self
185    }
186
187    pub(crate) fn hanging_chars(mut self, chars: i32) -> Self {
188        if let Some(indent) = self.indent {
189            self.indent = Some(indent.hanging_chars(chars));
190        }
191        self
192    }
193
194    pub(crate) fn first_line_chars(mut self, chars: i32) -> Self {
195        if let Some(indent) = self.indent {
196            self.indent = Some(indent.first_line_chars(chars));
197        }
198        self
199    }
200
201    pub fn set_borders(mut self, borders: ParagraphBorders) -> Self {
202        self.borders = Some(borders);
203        self
204    }
205
206    pub fn set_border(mut self, border: ParagraphBorder) -> Self {
207        self.borders = Some(self.borders.unwrap_or_default().set(border));
208        self
209    }
210
211    pub fn clear_border(mut self, position: ParagraphBorderPosition) -> Self {
212        self.borders = Some(self.borders.unwrap_or_default().clear(position));
213        self
214    }
215
216    pub fn clear_all_borders(mut self) -> Self {
217        self.borders = Some(self.borders.unwrap_or_default().clear_all());
218        self
219    }
220}
221
222impl BuildXML for ParagraphProperty {
223    fn build_to<W: Write>(
224        &self,
225        stream: crate::xml::writer::EventWriter<W>,
226    ) -> crate::xml::writer::Result<crate::xml::writer::EventWriter<W>> {
227        XMLBuilder::from(stream)
228            .open_paragraph_property()?
229            .add_child(&self.run_property)?
230            .add_optional_child(&self.style)?
231            .add_optional_child(&self.numbering_property)?
232            .add_optional_child(&self.frame_property)?
233            .add_optional_child(&self.alignment)?
234            .add_optional_child(&self.indent)?
235            .add_optional_child(&self.line_spacing)?
236            .add_optional_child(&self.outline_lvl)?
237            .add_optional_child(&self.paragraph_property_change)?
238            .add_optional_child(&self.borders)?
239            .add_optional_child(&self.shading)?
240            .add_optional_child(&self.text_alignment)?
241            .add_optional_child(&self.adjust_right_ind)?
242            .apply_opt(self.snap_to_grid, |v, b| b.snap_to_grid(v))?
243            .apply_if(self.keep_next, |b| b.keep_next())?
244            .apply_if(self.keep_lines, |b| b.keep_lines())?
245            .apply_if(self.page_break_before, |b| b.page_break_before())?
246            .apply_if(self.bidi, |b| b.bidi())?
247            .apply_opt(self.widow_control, |flag, b| {
248                b.widow_control(if flag { "1" } else { "0" })
249            })?
250            .apply_if(!self.tabs.is_empty(), |b| {
251                b.open_tabs()?
252                    .apply_each(&self.tabs, |tab, b| b.tab(tab.val, tab.leader, tab.pos))?
253                    .close()
254            })?
255            .add_optional_child(&self.section_property)?
256            .close()?
257            .into_inner()
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use crate::types::LineSpacingType;
265    use crate::types::SectionType;
266    #[cfg(test)]
267    use pretty_assertions::assert_eq;
268    use std::str;
269
270    #[test]
271    fn test_default() {
272        let c = ParagraphProperty::new();
273        let b = c.build();
274        assert_eq!(str::from_utf8(&b).unwrap(), r#"<w:pPr><w:rPr /></w:pPr>"#);
275    }
276
277    #[test]
278    fn test_bidi() {
279        let c = ParagraphProperty::new().bidi(true);
280        let b = c.build();
281        println!("-----Test bidi: {}", str::from_utf8(&b).unwrap());
282        assert_eq!(
283            str::from_utf8(&b).unwrap(),
284            r#"<w:pPr><w:rPr /><w:bidi /></w:pPr>"#
285        );
286    }
287    #[test]
288    fn test_alignment() {
289        let c = ParagraphProperty::new();
290        let b = c.align(AlignmentType::Right).build();
291        assert_eq!(
292            str::from_utf8(&b).unwrap(),
293            r#"<w:pPr><w:rPr /><w:jc w:val="right" /></w:pPr>"#
294        );
295    }
296
297    #[test]
298    fn test_indent() {
299        let c = ParagraphProperty::new();
300        let b = c.indent(Some(20), None, None, None).build();
301        assert_eq!(
302            str::from_utf8(&b).unwrap(),
303            r#"<w:pPr><w:rPr /><w:ind w:left="20" w:right="0" /></w:pPr>"#
304        );
305    }
306
307    #[test]
308    fn test_keep_next() {
309        let c = ParagraphProperty::new();
310        let b = c.keep_next(true).build();
311        assert_eq!(
312            str::from_utf8(&b).unwrap(),
313            r#"<w:pPr><w:rPr /><w:keepNext /></w:pPr>"#
314        );
315    }
316
317    #[test]
318    fn test_outline_lvl() {
319        let props = ParagraphProperty::new();
320        let bytes = props.outline_lvl(1).build();
321        assert_eq!(
322            str::from_utf8(&bytes).unwrap(),
323            r#"<w:pPr><w:rPr /><w:outlineLvl w:val="1" /></w:pPr>"#
324        )
325    }
326
327    #[test]
328    fn test_indent_json() {
329        let c = ParagraphProperty::new();
330        let b = c.indent(Some(20), Some(SpecialIndentType::FirstLine(10)), None, None);
331        assert_eq!(
332            serde_json::to_string(&b).unwrap(),
333            r#"{"runProperty":{},"indent":{"start":20,"startChars":null,"end":null,"specialIndent":{"type":"firstLine","val":10},"hangingChars":null,"firstLineChars":null},"tabs":[]}"#
334        );
335    }
336
337    #[test]
338    fn test_line_spacing() {
339        let props = ParagraphProperty::new();
340        let spacing = LineSpacing::new()
341            .line_rule(LineSpacingType::AtLeast)
342            .line(100);
343        let bytes = props.line_spacing(spacing).build();
344        assert_eq!(
345            str::from_utf8(&bytes).unwrap(),
346            r#"<w:pPr><w:rPr /><w:spacing w:line="100" w:lineRule="atLeast" /></w:pPr>"#
347        )
348    }
349
350    #[test]
351    fn test_shading() {
352        let props = ParagraphProperty::new().shading(Shading::new().fill("F0F0F0"));
353        let bytes = props.build();
354        assert_eq!(
355            str::from_utf8(&bytes).unwrap(),
356            r#"<w:pPr><w:rPr /><w:shd w:val="clear" w:color="auto" w:fill="F0F0F0" /></w:pPr>"#
357        );
358    }
359
360    #[test]
361    fn test_section_property() {
362        let props = ParagraphProperty::new().section_property(SectionProperty {
363            section_type: Some(SectionType::NextPage),
364            ..Default::default()
365        });
366        let bytes = props.build();
367        assert_eq!(
368            str::from_utf8(&bytes).unwrap(),
369            r#"<w:pPr><w:rPr /><w:sectPr><w:pgSz w:w="11906" w:h="16838" /><w:pgMar w:top="1985" w:right="1701" w:bottom="1701" w:left="1701" w:header="851" w:footer="992" w:gutter="0" /><w:cols w:space="425" w:num="1" /><w:type w:val="nextPage" /></w:sectPr></w:pPr>"#
370        )
371    }
372}