Skip to main content

docx_rs/reader/
paragraph.rs

1use std::io::Read;
2use std::str::FromStr;
3
4use super::*;
5
6use super::attributes::*;
7
8impl ElementReader for Paragraph {
9    fn read<R: Read>(
10        r: &mut EventReader<R>,
11        attrs: &[OwnedAttribute],
12    ) -> Result<Self, ReaderError> {
13        let mut p = Paragraph::new();
14        if let Some(para_id) = read(attrs, "paraId") {
15            p = p.id(para_id);
16        }
17        loop {
18            let e = r.next_event();
19            match e {
20                Ok(XmlEvent::StartElement {
21                    attributes, name, ..
22                }) => {
23                    let e = XMLElement::from_str(&name.local_name).unwrap();
24
25                    match e {
26                        XMLElement::Run => {
27                            let run = Run::read(r, &attributes)?;
28                            p = p.add_run(run);
29                            continue;
30                        }
31                        XMLElement::Hyperlink => {
32                            let link = Hyperlink::read(r, &attributes)?;
33                            p = p.add_hyperlink(link);
34                            continue;
35                        }
36                        XMLElement::Insert => {
37                            let ins = Insert::read(r, &attributes)?;
38                            p = p.add_insert(ins);
39                            continue;
40                        }
41                        XMLElement::Delete => {
42                            let del = Delete::read(r, &attributes)?;
43                            p = p.add_delete(del);
44                            continue;
45                        }
46                        XMLElement::MoveFrom => {
47                            let mf = MoveFrom::read(r, &attributes)?;
48                            p = p.add_move_from(mf);
49                            continue;
50                        }
51                        XMLElement::MoveTo => {
52                            let mt = MoveTo::read(r, &attributes)?;
53                            p = p.add_move_to(mt);
54                            continue;
55                        }
56                        XMLElement::BookmarkStart => {
57                            let s = BookmarkStart::read(r, &attributes)?;
58                            p = p.add_bookmark_start(s.id, s.name);
59                            continue;
60                        }
61                        XMLElement::BookmarkEnd => {
62                            let e = BookmarkEnd::read(r, &attributes)?;
63                            p = p.add_bookmark_end(e.id);
64                            continue;
65                        }
66                        XMLElement::CommentRangeStart => {
67                            if let Some(id) = read(&attributes, "id") {
68                                if let Ok(id) = usize::from_str(&id) {
69                                    let comment = Comment::new(id);
70                                    p = p.add_comment_start(comment);
71                                }
72                            }
73                            continue;
74                        }
75                        XMLElement::CommentRangeEnd => {
76                            if let Some(id) = read(&attributes, "id") {
77                                if let Ok(id) = usize::from_str(&id) {
78                                    p = p.add_comment_end(id);
79                                }
80                            }
81                            continue;
82                        }
83                        // pPr
84                        XMLElement::ParagraphProperty => {
85                            if let Ok(pr) = ParagraphProperty::read(r, &attributes) {
86                                p.has_numbering = pr.numbering_property.is_some();
87                                p.property = pr;
88                            }
89                            continue;
90                        }
91                        _ => {}
92                    }
93                }
94                Ok(XmlEvent::EndElement { name, .. }) => {
95                    let e = XMLElement::from_str(&name.local_name).unwrap();
96                    if e == XMLElement::Paragraph {
97                        return Ok(p);
98                    }
99                }
100                Err(_) => return Err(ReaderError::XMLReadError),
101                _ => {}
102            }
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109
110    use super::*;
111    use crate::types::*;
112    #[cfg(test)]
113    use pretty_assertions::assert_eq;
114
115    #[test]
116    fn test_read_indent() {
117        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
118    <w:p>
119        <w:pPr>
120            <w:ind w:left="1470" w:right="1270" w:hanging="0"/>
121            <w:rPr></w:rPr>
122        </w:pPr>
123        <w:r>
124            <w:rPr></w:rPr>
125            <w:t>a</w:t>
126        </w:r>
127    </w:p>
128</w:document>"#;
129        let mut parser = EventReader::new(c.as_bytes());
130        let p = Paragraph::read(&mut parser, &[]).unwrap();
131
132        assert_eq!(
133            p,
134            Paragraph {
135                id: "12345678".to_owned(),
136                children: vec![ParagraphChild::Run(Box::new(Run::new().add_text("a")))],
137                property: ParagraphProperty {
138                    run_property: RunProperty::new(),
139                    style: None,
140                    numbering_property: None,
141                    alignment: None,
142                    indent: Some(Indent::new(
143                        Some(1470),
144                        Some(SpecialIndentType::Hanging(0)),
145                        Some(1270),
146                        None,
147                    )),
148                    line_spacing: None,
149                    ..Default::default()
150                },
151                has_numbering: false,
152            }
153        );
154    }
155
156    #[test]
157    fn test_read_indent_start_chars() {
158        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
159    <w:p>
160        <w:pPr>
161            <w:ind w:startChars="100" />
162            <w:rPr></w:rPr>
163        </w:pPr>
164        <w:r>
165            <w:rPr></w:rPr>
166            <w:t>a</w:t>
167        </w:r>
168    </w:p>
169</w:document>"#;
170        let mut parser = EventReader::new(c.as_bytes());
171        let p = Paragraph::read(&mut parser, &[]).unwrap();
172
173        assert_eq!(
174            p,
175            Paragraph {
176                id: "12345678".to_owned(),
177                children: vec![ParagraphChild::Run(Box::new(Run::new().add_text("a")))],
178                property: ParagraphProperty {
179                    run_property: RunProperty::new(),
180                    style: None,
181                    numbering_property: None,
182                    alignment: None,
183                    indent: Some(Indent::new(None, None, None, Some(100))),
184                    line_spacing: None,
185                    ..Default::default()
186                },
187                has_numbering: false,
188            }
189        );
190    }
191
192    #[test]
193    fn test_read_jc() {
194        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
195    <w:p>
196        <w:pPr>
197            <w:jc w:val="left"/>
198        </w:pPr>
199    </w:p>
200</w:document>"#;
201        let mut parser = EventReader::new(c.as_bytes());
202        let p = Paragraph::read(&mut parser, &[]).unwrap();
203
204        assert_eq!(
205            p,
206            Paragraph {
207                id: "12345678".to_owned(),
208                children: vec![],
209                property: ParagraphProperty {
210                    run_property: RunProperty::new(),
211                    style: None,
212                    numbering_property: None,
213                    alignment: Some(Justification::new(AlignmentType::Left.to_string())),
214                    indent: None,
215                    line_spacing: None,
216                    ..Default::default()
217                },
218                has_numbering: false,
219            }
220        );
221    }
222
223    #[test]
224    fn test_read_numbering() {
225        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
226    <w:p>
227        <w:pPr>
228            <w:numPr>
229                <w:ilvl w:val="0"/>
230                <w:numId w:val="1"/>
231            </w:numPr>
232        </w:pPr>
233    </w:p>
234</w:document>"#;
235        let mut parser = EventReader::new(c.as_bytes());
236        let p = Paragraph::read(&mut parser, &[]).unwrap();
237        assert_eq!(
238            p,
239            Paragraph {
240                id: "12345678".to_owned(),
241                children: vec![],
242                property: ParagraphProperty {
243                    run_property: RunProperty::new(),
244                    style: None,
245                    numbering_property: Some(
246                        NumberingProperty::new().add_num(NumberingId::new(1), IndentLevel::new(0),)
247                    ),
248                    alignment: None,
249                    indent: None,
250                    line_spacing: None,
251                    ..Default::default()
252                },
253                has_numbering: true,
254            }
255        );
256    }
257
258    #[test]
259    fn test_read_insert() {
260        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
261    <w:p>
262        <w:ins w:id="0" w:author="unknown" w:date="2019-11-15T14:19:04Z">
263            <w:r>
264                <w:rPr></w:rPr>
265                <w:t>W</w:t>
266            </w:r>
267        </w:ins>
268    </w:p>
269</w:document>"#;
270        let mut parser = EventReader::new(c.as_bytes());
271        let p = Paragraph::read(&mut parser, &[]).unwrap();
272        assert_eq!(
273            p,
274            Paragraph {
275                id: "12345678".to_owned(),
276                children: vec![ParagraphChild::Insert(
277                    Insert::new(Run::new().add_text("W"))
278                        .author("unknown")
279                        .date("2019-11-15T14:19:04Z")
280                )],
281                property: ParagraphProperty {
282                    run_property: RunProperty::new(),
283                    style: None,
284                    numbering_property: None,
285                    alignment: None,
286                    indent: None,
287                    line_spacing: None,
288                    ..Default::default()
289                },
290                has_numbering: false,
291            }
292        );
293    }
294
295    #[test]
296    fn test_read_delete() {
297        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
298    <w:p>
299        <w:del w:id="3" w:author="unknown" w:date="2019-11-15T14:19:04Z">
300            <w:r>
301                <w:rPr></w:rPr>
302                <w:delText xml:space="preserve">Hello </w:delText>
303            </w:r>
304        </w:del>
305    </w:p>
306</w:document>"#;
307        let mut parser = EventReader::new(c.as_bytes());
308        let p = Paragraph::read(&mut parser, &[]).unwrap();
309
310        assert_eq!(
311            p,
312            Paragraph {
313                id: "12345678".to_owned(),
314                children: vec![ParagraphChild::Delete(
315                    Delete::new()
316                        .add_run(Run::new().add_delete_text("Hello "))
317                        .author("unknown")
318                        .date("2019-11-15T14:19:04Z")
319                )],
320                property: ParagraphProperty {
321                    run_property: RunProperty::new(),
322                    style: None,
323                    numbering_property: None,
324                    alignment: None,
325                    indent: None,
326                    line_spacing: None,
327                    ..Default::default()
328                },
329                has_numbering: false,
330            }
331        );
332    }
333
334    #[test]
335    fn test_read_bookmark() {
336        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
337    <w:p>
338        <w:bookmarkStart w:id="0" w:name="ABCD-1234"/>
339            <w:r>
340                <w:rPr></w:rPr>
341                <w:t>Bookmarked</w:t>
342            </w:r>
343        <w:bookmarkEnd w:id="0"/>
344    </w:p>
345</w:document>"#;
346        let mut parser = EventReader::new(c.as_bytes());
347        let p = Paragraph::read(&mut parser, &[]).unwrap();
348
349        assert_eq!(
350            p,
351            Paragraph {
352                id: "12345678".to_owned(),
353                children: vec![
354                    ParagraphChild::BookmarkStart(BookmarkStart::new(0, "ABCD-1234")),
355                    ParagraphChild::Run(Box::new(Run::new().add_text("Bookmarked"))),
356                    ParagraphChild::BookmarkEnd(BookmarkEnd::new(0)),
357                ],
358                property: ParagraphProperty {
359                    run_property: RunProperty::new(),
360                    style: None,
361                    numbering_property: None,
362                    alignment: None,
363                    indent: None,
364                    line_spacing: None,
365                    ..Default::default()
366                },
367                has_numbering: false,
368            }
369        );
370    }
371
372    #[test]
373    fn test_read_two_insert() {
374        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
375    <w:p>
376        <w:ins w:id="0" w:author="unknown" w:date="2019-11-15T14:19:04Z">
377            <w:r>
378                <w:rPr></w:rPr>
379                <w:t>W</w:t>
380            </w:r>
381        </w:ins>
382        <w:ins w:id="0" w:author="unknown" w:date="2019-11-15T14:19:04Z">
383            <w:r>
384                <w:rPr></w:rPr>
385                <w:t>H</w:t>
386            </w:r>
387        </w:ins>
388    </w:p>
389</w:document>"#;
390        let mut parser = EventReader::new(c.as_bytes());
391        let p = Paragraph::read(&mut parser, &[]).unwrap();
392        assert_eq!(
393            p,
394            Paragraph {
395                id: "12345678".to_owned(),
396                children: vec![
397                    ParagraphChild::Insert(
398                        Insert::new(Run::new().add_text("W"))
399                            .author("unknown")
400                            .date("2019-11-15T14:19:04Z")
401                    ),
402                    ParagraphChild::Insert(
403                        Insert::new(Run::new().add_text("H"))
404                            .author("unknown")
405                            .date("2019-11-15T14:19:04Z")
406                    )
407                ],
408                property: ParagraphProperty {
409                    run_property: RunProperty::new(),
410                    style: None,
411                    numbering_property: None,
412                    alignment: None,
413                    indent: None,
414                    line_spacing: None,
415                    ..Default::default()
416                },
417                has_numbering: false,
418            }
419        );
420    }
421
422    #[test]
423    fn test_read_two_run_in_insert() {
424        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
425    <w:p>
426        <w:ins w:id="0" w:author="unknown" w:date="2019-11-15T14:19:04Z">
427            <w:r>
428                <w:rPr></w:rPr>
429                <w:t>W</w:t>
430            </w:r>
431            <w:r>
432                <w:rPr></w:rPr>
433                <w:t>H</w:t>
434            </w:r>
435        </w:ins>
436    </w:p>
437</w:document>"#;
438        let mut parser = EventReader::new(c.as_bytes());
439        let p = Paragraph::read(&mut parser, &[]).unwrap();
440        assert_eq!(
441            p,
442            Paragraph {
443                id: "12345678".to_owned(),
444                children: vec![ParagraphChild::Insert(
445                    Insert::new(Run::new().add_text("W"))
446                        .author("unknown")
447                        .date("2019-11-15T14:19:04Z")
448                        .add_run(Run::new().add_text("H")),
449                )],
450                property: ParagraphProperty {
451                    run_property: RunProperty::new(),
452                    style: None,
453                    numbering_property: None,
454                    alignment: None,
455                    indent: None,
456                    line_spacing: None,
457                    ..Default::default()
458                },
459                has_numbering: false,
460            }
461        );
462    }
463
464    #[test]
465    fn test_read_move_from_to() {
466        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
467    <w:p>
468        <w:moveFrom w:id="1" w:author="user1" w:date="2024-01-01T00:00:00Z">
469            <w:r>
470                <w:rPr></w:rPr>
471                <w:t>moved</w:t>
472            </w:r>
473        </w:moveFrom>
474        <w:moveTo w:id="2" w:author="user1" w:date="2024-01-01T00:00:00Z">
475            <w:r>
476                <w:rPr></w:rPr>
477                <w:t>moved</w:t>
478            </w:r>
479        </w:moveTo>
480    </w:p>
481</w:document>"#;
482        let mut parser = EventReader::new(c.as_bytes());
483        let p = Paragraph::read(&mut parser, &[]).unwrap();
484        assert_eq!(
485            p,
486            Paragraph {
487                id: "12345678".to_owned(),
488                children: vec![
489                    ParagraphChild::MoveFrom(
490                        MoveFrom::new()
491                            .add_run(Run::new().add_text("moved"))
492                            .author("user1")
493                            .date("2024-01-01T00:00:00Z")
494                    ),
495                    ParagraphChild::MoveTo(
496                        MoveTo::new(Run::new().add_text("moved"))
497                            .author("user1")
498                            .date("2024-01-01T00:00:00Z")
499                    ),
500                ],
501                property: ParagraphProperty {
502                    run_property: RunProperty::new(),
503                    style: None,
504                    numbering_property: None,
505                    alignment: None,
506                    indent: None,
507                    line_spacing: None,
508                    ..Default::default()
509                },
510                has_numbering: false,
511            }
512        );
513        // Verify raw_text excludes MoveFrom and includes MoveTo
514        assert_eq!(p.raw_text(), "moved");
515    }
516
517    #[test]
518    fn test_read_move_from_nested_in_insert() {
519        // MoveFrom inside Insert: ghost text should be skipped, not flattened into Insert runs
520        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
521    <w:p>
522        <w:ins w:id="0" w:author="unknown" w:date="2024-01-01T00:00:00Z">
523            <w:moveFrom w:id="1" w:author="unknown" w:date="2024-01-01T00:00:00Z">
524                <w:r>
525                    <w:rPr></w:rPr>
526                    <w:t>ghost</w:t>
527                </w:r>
528            </w:moveFrom>
529            <w:r>
530                <w:rPr></w:rPr>
531                <w:t>live</w:t>
532            </w:r>
533        </w:ins>
534    </w:p>
535</w:document>"#;
536        let mut parser = EventReader::new(c.as_bytes());
537        let p = Paragraph::read(&mut parser, &[]).unwrap();
538        // The MoveFrom ghost text should have been skipped by ignore_element in the insert reader
539        assert_eq!(p.raw_text(), "live");
540    }
541
542    #[test]
543    fn test_read_move_from_nested_in_move_to() {
544        // MoveFrom inside MoveTo: ghost text should be skipped, not flattened into MoveTo runs
545        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
546    <w:p>
547        <w:moveTo w:id="2" w:author="unknown" w:date="2024-01-01T00:00:00Z">
548            <w:moveFrom w:id="1" w:author="unknown" w:date="2024-01-01T00:00:00Z">
549                <w:r>
550                    <w:rPr></w:rPr>
551                    <w:t>ghost</w:t>
552                </w:r>
553            </w:moveFrom>
554            <w:r>
555                <w:rPr></w:rPr>
556                <w:t>live</w:t>
557            </w:r>
558        </w:moveTo>
559    </w:p>
560</w:document>"#;
561        let mut parser = EventReader::new(c.as_bytes());
562        let p = Paragraph::read(&mut parser, &[]).unwrap();
563        // The MoveFrom ghost text should have been skipped by ignore_element in the moveTo reader
564        assert_eq!(p.raw_text(), "live");
565    }
566}