Skip to main content

linch_docx_rs/document/paragraph/
properties.rs

1//! Paragraph properties and related types
2
3use crate::error::Result;
4use crate::xml::{get_w_val, RawXmlElement, RawXmlNode};
5use quick_xml::events::{BytesEnd, BytesStart, Event};
6use quick_xml::{Reader, Writer};
7use std::io::BufRead;
8
9/// Paragraph alignment
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum Alignment {
12    Left,
13    Center,
14    Right,
15    Justify,
16    Distribute,
17}
18
19impl Alignment {
20    pub fn from_ooxml(s: &str) -> Option<Self> {
21        match s {
22            "left" | "start" => Some(Self::Left),
23            "center" => Some(Self::Center),
24            "right" | "end" => Some(Self::Right),
25            "both" | "justify" => Some(Self::Justify),
26            "distribute" => Some(Self::Distribute),
27            _ => None,
28        }
29    }
30
31    pub fn as_ooxml(&self) -> &'static str {
32        match self {
33            Self::Left => "left",
34            Self::Center => "center",
35            Self::Right => "right",
36            Self::Justify => "both",
37            Self::Distribute => "distribute",
38        }
39    }
40}
41
42/// Indentation settings (in twips)
43#[derive(Clone, Debug, Default)]
44pub struct Indentation {
45    pub left: Option<i32>,
46    pub right: Option<i32>,
47    pub first_line: Option<i32>,
48    pub hanging: Option<i32>,
49}
50
51/// Line spacing settings
52#[derive(Clone, Debug, Default)]
53pub struct LineSpacing {
54    /// Space before paragraph (in twips)
55    pub before: Option<u32>,
56    /// Space after paragraph (in twips)
57    pub after: Option<u32>,
58    /// Line spacing value (in 240ths of a line for "auto", twips for exact/atLeast)
59    pub line: Option<u32>,
60    /// Line spacing rule
61    pub line_rule: Option<String>,
62}
63
64/// Paragraph properties (w:pPr)
65#[derive(Clone, Debug, Default)]
66pub struct ParagraphProperties {
67    /// Style ID
68    pub style: Option<String>,
69    /// Justification/alignment
70    pub justification: Option<String>,
71    /// Numbering properties
72    pub num_id: Option<u32>,
73    pub num_level: Option<u32>,
74    /// Outline level (for headings)
75    pub outline_level: Option<u8>,
76    /// Indentation
77    pub indentation: Option<Indentation>,
78    /// Line spacing
79    pub spacing: Option<LineSpacing>,
80    /// Keep with next paragraph
81    pub keep_next: Option<bool>,
82    /// Keep lines together
83    pub keep_lines: Option<bool>,
84    /// Page break before
85    pub page_break_before: Option<bool>,
86    /// Run properties for paragraph mark
87    pub run_properties: Option<crate::document::RunProperties>,
88    /// Unknown children (preserved)
89    pub unknown_children: Vec<RawXmlNode>,
90}
91
92impl ParagraphProperties {
93    /// Parse from reader (after w:pPr start tag)
94    pub fn from_reader<R: BufRead>(reader: &mut Reader<R>) -> Result<Self> {
95        let mut props = ParagraphProperties::default();
96        let mut buf = Vec::new();
97
98        loop {
99            match reader.read_event_into(&mut buf)? {
100                Event::Start(e) => {
101                    let local = e.name().local_name();
102                    match local.as_ref() {
103                        b"numPr" => {
104                            parse_num_pr(reader, &mut props)?;
105                        }
106                        b"rPr" => {
107                            props.run_properties =
108                                Some(crate::document::RunProperties::from_reader(reader)?);
109                        }
110                        _ => {
111                            let raw = RawXmlElement::from_reader(reader, &e)?;
112                            props.unknown_children.push(RawXmlNode::Element(raw));
113                        }
114                    }
115                }
116                Event::Empty(e) => {
117                    let local = e.name().local_name();
118                    match local.as_ref() {
119                        b"pStyle" => props.style = get_w_val(&e),
120                        b"jc" => props.justification = get_w_val(&e),
121                        b"outlineLvl" => {
122                            props.outline_level = get_w_val(&e).and_then(|v| v.parse().ok());
123                        }
124                        b"ind" => {
125                            props.indentation = Some(Indentation {
126                                left: crate::xml::get_attr(&e, "w:left")
127                                    .and_then(|v| v.parse().ok()),
128                                right: crate::xml::get_attr(&e, "w:right")
129                                    .and_then(|v| v.parse().ok()),
130                                first_line: crate::xml::get_attr(&e, "w:firstLine")
131                                    .and_then(|v| v.parse().ok()),
132                                hanging: crate::xml::get_attr(&e, "w:hanging")
133                                    .and_then(|v| v.parse().ok()),
134                            });
135                        }
136                        b"spacing" => {
137                            props.spacing = Some(LineSpacing {
138                                before: crate::xml::get_attr(&e, "w:before")
139                                    .and_then(|v| v.parse().ok()),
140                                after: crate::xml::get_attr(&e, "w:after")
141                                    .and_then(|v| v.parse().ok()),
142                                line: crate::xml::get_attr(&e, "w:line")
143                                    .and_then(|v| v.parse().ok()),
144                                line_rule: crate::xml::get_attr(&e, "w:lineRule"),
145                            });
146                        }
147                        b"keepNext" => props.keep_next = Some(crate::xml::parse_bool(&e)),
148                        b"keepLines" => props.keep_lines = Some(crate::xml::parse_bool(&e)),
149                        b"pageBreakBefore" => {
150                            props.page_break_before = Some(crate::xml::parse_bool(&e));
151                        }
152                        _ => {
153                            let raw = RawXmlElement {
154                                name: String::from_utf8_lossy(e.name().as_ref()).to_string(),
155                                attributes: e
156                                    .attributes()
157                                    .filter_map(|a| a.ok())
158                                    .map(|a| {
159                                        (
160                                            String::from_utf8_lossy(a.key.as_ref()).to_string(),
161                                            String::from_utf8_lossy(&a.value).to_string(),
162                                        )
163                                    })
164                                    .collect(),
165                                children: Vec::new(),
166                                self_closing: true,
167                            };
168                            props.unknown_children.push(RawXmlNode::Element(raw));
169                        }
170                    }
171                }
172                Event::End(e) if e.name().local_name().as_ref() == b"pPr" => break,
173                Event::Eof => break,
174                _ => {}
175            }
176            buf.clear();
177        }
178
179        Ok(props)
180    }
181
182    /// Write to XML writer
183    pub fn write_to<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
184        let has_content = self.style.is_some()
185            || self.justification.is_some()
186            || self.num_id.is_some()
187            || self.outline_level.is_some()
188            || self.indentation.is_some()
189            || self.spacing.is_some()
190            || self.keep_next.is_some()
191            || self.keep_lines.is_some()
192            || self.page_break_before.is_some()
193            || self.run_properties.is_some()
194            || !self.unknown_children.is_empty();
195
196        if !has_content {
197            return Ok(());
198        }
199
200        writer.write_event(Event::Start(BytesStart::new("w:pPr")))?;
201
202        if let Some(style) = &self.style {
203            let mut elem = BytesStart::new("w:pStyle");
204            elem.push_attribute(("w:val", style.as_str()));
205            writer.write_event(Event::Empty(elem))?;
206        }
207
208        if let Some(true) = self.keep_next {
209            writer.write_event(Event::Empty(BytesStart::new("w:keepNext")))?;
210        }
211        if let Some(true) = self.keep_lines {
212            writer.write_event(Event::Empty(BytesStart::new("w:keepLines")))?;
213        }
214        if let Some(true) = self.page_break_before {
215            writer.write_event(Event::Empty(BytesStart::new("w:pageBreakBefore")))?;
216        }
217
218        if self.num_id.is_some() || self.num_level.is_some() {
219            writer.write_event(Event::Start(BytesStart::new("w:numPr")))?;
220            if let Some(level) = self.num_level {
221                let mut elem = BytesStart::new("w:ilvl");
222                elem.push_attribute(("w:val", level.to_string().as_str()));
223                writer.write_event(Event::Empty(elem))?;
224            }
225            if let Some(num_id) = self.num_id {
226                let mut elem = BytesStart::new("w:numId");
227                elem.push_attribute(("w:val", num_id.to_string().as_str()));
228                writer.write_event(Event::Empty(elem))?;
229            }
230            writer.write_event(Event::End(BytesEnd::new("w:numPr")))?;
231        }
232
233        if let Some(ref sp) = self.spacing {
234            let mut elem = BytesStart::new("w:spacing");
235            if let Some(v) = sp.before {
236                elem.push_attribute(("w:before", v.to_string().as_str()));
237            }
238            if let Some(v) = sp.after {
239                elem.push_attribute(("w:after", v.to_string().as_str()));
240            }
241            if let Some(v) = sp.line {
242                elem.push_attribute(("w:line", v.to_string().as_str()));
243            }
244            if let Some(ref rule) = sp.line_rule {
245                elem.push_attribute(("w:lineRule", rule.as_str()));
246            }
247            writer.write_event(Event::Empty(elem))?;
248        }
249
250        if let Some(ref ind) = self.indentation {
251            let mut elem = BytesStart::new("w:ind");
252            if let Some(v) = ind.left {
253                elem.push_attribute(("w:left", v.to_string().as_str()));
254            }
255            if let Some(v) = ind.right {
256                elem.push_attribute(("w:right", v.to_string().as_str()));
257            }
258            if let Some(v) = ind.first_line {
259                elem.push_attribute(("w:firstLine", v.to_string().as_str()));
260            }
261            if let Some(v) = ind.hanging {
262                elem.push_attribute(("w:hanging", v.to_string().as_str()));
263            }
264            writer.write_event(Event::Empty(elem))?;
265        }
266
267        if let Some(jc) = &self.justification {
268            let mut elem = BytesStart::new("w:jc");
269            elem.push_attribute(("w:val", jc.as_str()));
270            writer.write_event(Event::Empty(elem))?;
271        }
272
273        if let Some(level) = self.outline_level {
274            let mut elem = BytesStart::new("w:outlineLvl");
275            elem.push_attribute(("w:val", level.to_string().as_str()));
276            writer.write_event(Event::Empty(elem))?;
277        }
278
279        if let Some(ref rpr) = self.run_properties {
280            rpr.write_to(writer)?;
281        }
282
283        for child in &self.unknown_children {
284            child.write_to(writer)?;
285        }
286
287        writer.write_event(Event::End(BytesEnd::new("w:pPr")))?;
288        Ok(())
289    }
290}
291
292/// Parse numbering properties
293fn parse_num_pr<R: BufRead>(reader: &mut Reader<R>, props: &mut ParagraphProperties) -> Result<()> {
294    let mut buf = Vec::new();
295
296    loop {
297        match reader.read_event_into(&mut buf)? {
298            Event::Empty(e) => {
299                let local = e.name().local_name();
300                match local.as_ref() {
301                    b"numId" => props.num_id = get_w_val(&e).and_then(|v| v.parse().ok()),
302                    b"ilvl" => props.num_level = get_w_val(&e).and_then(|v| v.parse().ok()),
303                    _ => {}
304                }
305            }
306            Event::End(e) if e.name().local_name().as_ref() == b"numPr" => break,
307            Event::Eof => break,
308            _ => {}
309        }
310        buf.clear();
311    }
312
313    Ok(())
314}