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_paragraph_borders() {
225        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
226    <w:p>
227        <w:pPr>
228            <w:pBdr>
229                <w:top w:val="double" w:sz="24" w:space="1" w:color="622423"/>
230                <w:between w:val="dashed" w:sz="8" w:space="2" w:color="auto"/>
231            </w:pBdr>
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
238        assert_eq!(
239            p.property.borders,
240            Some(
241                ParagraphBorders::with_empty()
242                    .set(
243                        ParagraphBorder::new(ParagraphBorderPosition::Top)
244                            .val(BorderType::Double)
245                            .size(24)
246                            .space(1)
247                            .color("622423"),
248                    )
249                    .set(
250                        ParagraphBorder::new(ParagraphBorderPosition::Between)
251                            .val(BorderType::Dashed)
252                            .size(8)
253                            .space(2)
254                            .color("auto"),
255                    ),
256            )
257        );
258    }
259
260    #[test]
261    fn test_read_numbering() {
262        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
263    <w:p>
264        <w:pPr>
265            <w:numPr>
266                <w:ilvl w:val="0"/>
267                <w:numId w:val="1"/>
268            </w:numPr>
269        </w:pPr>
270    </w:p>
271</w:document>"#;
272        let mut parser = EventReader::new(c.as_bytes());
273        let p = Paragraph::read(&mut parser, &[]).unwrap();
274        assert_eq!(
275            p,
276            Paragraph {
277                id: "12345678".to_owned(),
278                children: vec![],
279                property: ParagraphProperty {
280                    run_property: RunProperty::new(),
281                    style: None,
282                    numbering_property: Some(
283                        NumberingProperty::new().add_num(NumberingId::new(1), IndentLevel::new(0),)
284                    ),
285                    alignment: None,
286                    indent: None,
287                    line_spacing: None,
288                    ..Default::default()
289                },
290                has_numbering: true,
291            }
292        );
293    }
294
295    #[test]
296    fn test_read_insert() {
297        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
298    <w:p>
299        <w:ins w:id="0" w:author="unknown" w:date="2019-11-15T14:19:04Z">
300            <w:r>
301                <w:rPr></w:rPr>
302                <w:t>W</w:t>
303            </w:r>
304        </w:ins>
305    </w:p>
306</w:document>"#;
307        let mut parser = EventReader::new(c.as_bytes());
308        let p = Paragraph::read(&mut parser, &[]).unwrap();
309        assert_eq!(
310            p,
311            Paragraph {
312                id: "12345678".to_owned(),
313                children: vec![ParagraphChild::Insert(
314                    Insert::new(Run::new().add_text("W"))
315                        .author("unknown")
316                        .date("2019-11-15T14:19:04Z")
317                )],
318                property: ParagraphProperty {
319                    run_property: RunProperty::new(),
320                    style: None,
321                    numbering_property: None,
322                    alignment: None,
323                    indent: None,
324                    line_spacing: None,
325                    ..Default::default()
326                },
327                has_numbering: false,
328            }
329        );
330    }
331
332    #[test]
333    fn test_read_delete() {
334        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
335    <w:p>
336        <w:del w:id="3" w:author="unknown" w:date="2019-11-15T14:19:04Z">
337            <w:r>
338                <w:rPr></w:rPr>
339                <w:delText xml:space="preserve">Hello </w:delText>
340            </w:r>
341        </w:del>
342    </w:p>
343</w:document>"#;
344        let mut parser = EventReader::new(c.as_bytes());
345        let p = Paragraph::read(&mut parser, &[]).unwrap();
346
347        assert_eq!(
348            p,
349            Paragraph {
350                id: "12345678".to_owned(),
351                children: vec![ParagraphChild::Delete(
352                    Delete::new()
353                        .add_run(Run::new().add_delete_text("Hello "))
354                        .author("unknown")
355                        .date("2019-11-15T14:19:04Z")
356                )],
357                property: ParagraphProperty {
358                    run_property: RunProperty::new(),
359                    style: None,
360                    numbering_property: None,
361                    alignment: None,
362                    indent: None,
363                    line_spacing: None,
364                    ..Default::default()
365                },
366                has_numbering: false,
367            }
368        );
369    }
370
371    #[test]
372    fn test_read_bookmark() {
373        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
374    <w:p>
375        <w:bookmarkStart w:id="0" w:name="ABCD-1234"/>
376            <w:r>
377                <w:rPr></w:rPr>
378                <w:t>Bookmarked</w:t>
379            </w:r>
380        <w:bookmarkEnd w:id="0"/>
381    </w:p>
382</w:document>"#;
383        let mut parser = EventReader::new(c.as_bytes());
384        let p = Paragraph::read(&mut parser, &[]).unwrap();
385
386        assert_eq!(
387            p,
388            Paragraph {
389                id: "12345678".to_owned(),
390                children: vec![
391                    ParagraphChild::BookmarkStart(BookmarkStart::new(0, "ABCD-1234")),
392                    ParagraphChild::Run(Box::new(Run::new().add_text("Bookmarked"))),
393                    ParagraphChild::BookmarkEnd(BookmarkEnd::new(0)),
394                ],
395                property: ParagraphProperty {
396                    run_property: RunProperty::new(),
397                    style: None,
398                    numbering_property: None,
399                    alignment: None,
400                    indent: None,
401                    line_spacing: None,
402                    ..Default::default()
403                },
404                has_numbering: false,
405            }
406        );
407    }
408
409    #[test]
410    fn test_read_two_insert() {
411        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
412    <w:p>
413        <w:ins w:id="0" w:author="unknown" w:date="2019-11-15T14:19:04Z">
414            <w:r>
415                <w:rPr></w:rPr>
416                <w:t>W</w:t>
417            </w:r>
418        </w:ins>
419        <w:ins w:id="0" w:author="unknown" w:date="2019-11-15T14:19:04Z">
420            <w:r>
421                <w:rPr></w:rPr>
422                <w:t>H</w:t>
423            </w:r>
424        </w:ins>
425    </w:p>
426</w:document>"#;
427        let mut parser = EventReader::new(c.as_bytes());
428        let p = Paragraph::read(&mut parser, &[]).unwrap();
429        assert_eq!(
430            p,
431            Paragraph {
432                id: "12345678".to_owned(),
433                children: vec![
434                    ParagraphChild::Insert(
435                        Insert::new(Run::new().add_text("W"))
436                            .author("unknown")
437                            .date("2019-11-15T14:19:04Z")
438                    ),
439                    ParagraphChild::Insert(
440                        Insert::new(Run::new().add_text("H"))
441                            .author("unknown")
442                            .date("2019-11-15T14:19:04Z")
443                    )
444                ],
445                property: ParagraphProperty {
446                    run_property: RunProperty::new(),
447                    style: None,
448                    numbering_property: None,
449                    alignment: None,
450                    indent: None,
451                    line_spacing: None,
452                    ..Default::default()
453                },
454                has_numbering: false,
455            }
456        );
457    }
458
459    #[test]
460    fn test_read_two_run_in_insert() {
461        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
462    <w:p>
463        <w:ins w:id="0" w:author="unknown" w:date="2019-11-15T14:19:04Z">
464            <w:r>
465                <w:rPr></w:rPr>
466                <w:t>W</w:t>
467            </w:r>
468            <w:r>
469                <w:rPr></w:rPr>
470                <w:t>H</w:t>
471            </w:r>
472        </w:ins>
473    </w:p>
474</w:document>"#;
475        let mut parser = EventReader::new(c.as_bytes());
476        let p = Paragraph::read(&mut parser, &[]).unwrap();
477        assert_eq!(
478            p,
479            Paragraph {
480                id: "12345678".to_owned(),
481                children: vec![ParagraphChild::Insert(
482                    Insert::new(Run::new().add_text("W"))
483                        .author("unknown")
484                        .date("2019-11-15T14:19:04Z")
485                        .add_run(Run::new().add_text("H")),
486                )],
487                property: ParagraphProperty {
488                    run_property: RunProperty::new(),
489                    style: None,
490                    numbering_property: None,
491                    alignment: None,
492                    indent: None,
493                    line_spacing: None,
494                    ..Default::default()
495                },
496                has_numbering: false,
497            }
498        );
499    }
500
501    #[test]
502    fn test_read_move_from_to() {
503        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
504    <w:p>
505        <w:moveFrom w:id="1" w:author="user1" w:date="2024-01-01T00:00:00Z">
506            <w:r>
507                <w:rPr></w:rPr>
508                <w:t>moved</w:t>
509            </w:r>
510        </w:moveFrom>
511        <w:moveTo w:id="2" w:author="user1" w:date="2024-01-01T00:00:00Z">
512            <w:r>
513                <w:rPr></w:rPr>
514                <w:t>moved</w:t>
515            </w:r>
516        </w:moveTo>
517    </w:p>
518</w:document>"#;
519        let mut parser = EventReader::new(c.as_bytes());
520        let p = Paragraph::read(&mut parser, &[]).unwrap();
521        assert_eq!(
522            p,
523            Paragraph {
524                id: "12345678".to_owned(),
525                children: vec![
526                    ParagraphChild::MoveFrom(
527                        MoveFrom::new()
528                            .add_run(Run::new().add_text("moved"))
529                            .author("user1")
530                            .date("2024-01-01T00:00:00Z")
531                    ),
532                    ParagraphChild::MoveTo(
533                        MoveTo::new(Run::new().add_text("moved"))
534                            .author("user1")
535                            .date("2024-01-01T00:00:00Z")
536                    ),
537                ],
538                property: ParagraphProperty {
539                    run_property: RunProperty::new(),
540                    style: None,
541                    numbering_property: None,
542                    alignment: None,
543                    indent: None,
544                    line_spacing: None,
545                    ..Default::default()
546                },
547                has_numbering: false,
548            }
549        );
550        // Verify raw_text excludes MoveFrom and includes MoveTo
551        assert_eq!(p.raw_text(), "moved");
552    }
553
554    #[test]
555    fn test_read_move_from_nested_in_insert() {
556        // MoveFrom inside Insert: ghost text should be skipped, not flattened into Insert runs
557        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
558    <w:p>
559        <w:ins w:id="0" w:author="unknown" w:date="2024-01-01T00:00:00Z">
560            <w:moveFrom w:id="1" w:author="unknown" w:date="2024-01-01T00:00:00Z">
561                <w:r>
562                    <w:rPr></w:rPr>
563                    <w:t>ghost</w:t>
564                </w:r>
565            </w:moveFrom>
566            <w:r>
567                <w:rPr></w:rPr>
568                <w:t>live</w:t>
569            </w:r>
570        </w:ins>
571    </w:p>
572</w:document>"#;
573        let mut parser = EventReader::new(c.as_bytes());
574        let p = Paragraph::read(&mut parser, &[]).unwrap();
575        // The MoveFrom ghost text should have been skipped by ignore_element in the insert reader
576        assert_eq!(p.raw_text(), "live");
577    }
578
579    #[test]
580    fn test_read_move_from_nested_in_move_to() {
581        // MoveFrom inside MoveTo: ghost text should be skipped, not flattened into MoveTo runs
582        let c = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
583    <w:p>
584        <w:moveTo w:id="2" w:author="unknown" w:date="2024-01-01T00:00:00Z">
585            <w:moveFrom w:id="1" w:author="unknown" w:date="2024-01-01T00:00:00Z">
586                <w:r>
587                    <w:rPr></w:rPr>
588                    <w:t>ghost</w:t>
589                </w:r>
590            </w:moveFrom>
591            <w:r>
592                <w:rPr></w:rPr>
593                <w:t>live</w:t>
594            </w:r>
595        </w:moveTo>
596    </w:p>
597</w:document>"#;
598        let mut parser = EventReader::new(c.as_bytes());
599        let p = Paragraph::read(&mut parser, &[]).unwrap();
600        // The MoveFrom ghost text should have been skipped by ignore_element in the moveTo reader
601        assert_eq!(p.raw_text(), "live");
602    }
603}