docx_rs/documents/elements/
table_of_contents.rs

1use serde::ser::{SerializeStruct, Serializer};
2use serde::Serialize;
3use std::io::Write;
4
5use crate::types::*;
6use crate::xml_builder::*;
7use crate::{documents::*, escape};
8
9#[derive(Debug, Clone, PartialEq)]
10pub enum TocContent {
11    Paragraph(Box<Paragraph>),
12    Table(Box<Table>),
13}
14
15impl Serialize for TocContent {
16    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
17    where
18        S: Serializer,
19    {
20        match *self {
21            TocContent::Paragraph(ref p) => {
22                let mut t = serializer.serialize_struct("Paragraph", 2)?;
23                t.serialize_field("type", "paragraph")?;
24                t.serialize_field("data", p)?;
25                t.end()
26            }
27            TocContent::Table(ref c) => {
28                let mut t = serializer.serialize_struct("Table", 2)?;
29                t.serialize_field("type", "table")?;
30                t.serialize_field("data", c)?;
31                t.end()
32            }
33        }
34    }
35}
36
37#[derive(Serialize, Debug, Clone, PartialEq, Default)]
38pub struct TableOfContentsReviewData {
39    pub author: String,
40    pub date: String,
41}
42
43/// https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_TOCTOC_topic_ID0ELZO1.html
44/// This struct is only used by writers
45#[derive(Serialize, Debug, Clone, PartialEq, Default)]
46pub struct TableOfContents {
47    pub instr: InstrToC,
48    pub items: Vec<TableOfContentsItem>,
49    // don't use
50    pub auto: bool,
51    pub dirty: bool,
52    /// Skip StructuredDataTag rendering
53    pub without_sdt: bool,
54    pub alias: Option<String>,
55    pub page_ref_placeholder: Option<String>,
56    // it is inserted in before toc.
57    #[serde(skip_serializing_if = "Vec::is_empty")]
58    pub before_contents: Vec<TocContent>,
59    // it is inserted in after toc.
60    #[serde(skip_serializing_if = "Vec::is_empty")]
61    pub after_contents: Vec<TocContent>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub delete: Option<TableOfContentsReviewData>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub paragraph_property: Option<ParagraphProperty>,
66}
67
68impl TableOfContents {
69    pub fn new() -> Self {
70        Self::default()
71    }
72
73    pub fn with_instr_text(s: &str) -> Self {
74        let instr = InstrToC::with_instr_text(s);
75        Self {
76            instr,
77            ..Self::default()
78        }
79    }
80
81    pub fn heading_styles_range(mut self, start: usize, end: usize) -> Self {
82        self.instr = self.instr.heading_styles_range(start, end);
83        self
84    }
85
86    pub fn tc_field_identifier(mut self, f: Option<String>) -> Self {
87        self.instr = self.instr.tc_field_identifier(f);
88        self
89    }
90
91    pub fn add_style_with_level(mut self, s: StyleWithLevel) -> Self {
92        self.instr = self.instr.add_style_with_level(s);
93        self
94    }
95
96    pub fn hyperlink(mut self) -> Self {
97        self.instr = self.instr.hyperlink();
98        self
99    }
100
101    pub fn alias(mut self, a: impl Into<String>) -> Self {
102        self.alias = Some(a.into());
103        self
104    }
105
106    pub fn delete(mut self, author: impl Into<String>, date: impl Into<String>) -> Self {
107        self.delete = Some(TableOfContentsReviewData {
108            author: escape::escape(&author.into()),
109            date: date.into(),
110        });
111        self
112    }
113
114    // pub fn tc_field_level_range(mut self, start: usize, end: usize) -> Self {
115    //     self.instr = self.instr.tc_field_level_range(start, end);
116    //     self
117    // }
118
119    pub fn add_item(mut self, t: TableOfContentsItem) -> Self {
120        self.items.push(t);
121        self
122    }
123
124    pub fn auto(mut self) -> Self {
125        self.auto = true;
126        self
127    }
128
129    pub fn dirty(mut self) -> Self {
130        self.dirty = true;
131        self
132    }
133
134    pub fn add_before_paragraph(mut self, p: Paragraph) -> Self {
135        self.before_contents
136            .push(TocContent::Paragraph(Box::new(p)));
137        self
138    }
139
140    pub fn add_after_paragraph(mut self, p: Paragraph) -> Self {
141        self.after_contents.push(TocContent::Paragraph(Box::new(p)));
142        self
143    }
144
145    pub fn add_before_table(mut self, t: Table) -> Self {
146        self.before_contents.push(TocContent::Table(Box::new(t)));
147        self
148    }
149
150    pub fn add_after_table(mut self, t: Table) -> Self {
151        self.after_contents.push(TocContent::Table(Box::new(t)));
152        self
153    }
154
155    pub fn without_sdt(mut self) -> Self {
156        self.without_sdt = true;
157        self
158    }
159
160    pub fn paragraph_property(mut self, p: ParagraphProperty) -> Self {
161        self.paragraph_property = Some(p);
162        self
163    }
164}
165
166impl BuildXML for TableOfContents {
167    fn build_to<W: Write>(
168        &self,
169        stream: xml::writer::EventWriter<W>,
170    ) -> xml::writer::Result<xml::writer::EventWriter<W>> {
171        let mut p = StructuredDataTagProperty::new();
172        if let Some(ref alias) = self.alias {
173            p = p.alias(alias);
174        }
175
176        if self.items.is_empty() {
177            let mut b = XMLBuilder::from(stream);
178
179            if !self.without_sdt {
180                b = b
181                    .open_structured_tag()?
182                    .add_child(&p)?
183                    .open_structured_tag_content()?;
184            }
185
186            for c in self.before_contents.iter() {
187                match c {
188                    TocContent::Paragraph(p) => {
189                        b = b.add_child(&p)?;
190                    }
191                    TocContent::Table(t) => {
192                        b = b.add_child(&t)?;
193                    }
194                }
195            }
196
197            let mut p1 = if let Some(ref del) = self.delete {
198                Paragraph::new().add_delete(
199                    Delete::new().author(&del.author).date(&del.date).add_run(
200                        Run::new()
201                            .add_field_char(FieldCharType::Begin, true)
202                            .add_delete_instr_text(DeleteInstrText::TOC(self.instr.clone()))
203                            .add_field_char(FieldCharType::Separate, false),
204                    ),
205                )
206            } else {
207                Paragraph::new().add_run(
208                    Run::new()
209                        .add_field_char(FieldCharType::Begin, true)
210                        .add_instr_text(InstrText::TOC(self.instr.clone()))
211                        .add_field_char(FieldCharType::Separate, false),
212                )
213            };
214
215            if let Some(ref p) = self.paragraph_property {
216                p1 = p1.paragraph_property(p.clone());
217            }
218
219            b = b.add_child(&p1)?;
220
221            let p2 = Paragraph::new().add_run(Run::new().add_field_char(FieldCharType::End, false));
222
223            if self.after_contents.is_empty() {
224                b = b.add_child(&p2)?;
225            } else {
226                for (i, c) in self.after_contents.iter().enumerate() {
227                    match c {
228                        TocContent::Paragraph(p) => {
229                            // Merge paragraph
230                            if i == 0 {
231                                let mut new_p = p.clone();
232                                new_p.children.insert(
233                                    0,
234                                    ParagraphChild::Run(Box::new(
235                                        Run::new().add_field_char(FieldCharType::End, false),
236                                    )),
237                                );
238                                b = b.add_child(&new_p)?
239                            } else {
240                                b = b.add_child(&p)?;
241                            }
242                        }
243                        TocContent::Table(t) => {
244                            // insert empty line for table
245                            // because it causes docx error if table is added after TOC
246                            if i == 0 {
247                                b = b.add_child(
248                                    &Paragraph::new().add_run(Run::new().add_text("")),
249                                )?;
250                            }
251                            b = b.add_child(&t)?;
252                        }
253                    }
254                }
255            }
256
257            if !self.without_sdt {
258                b = b.close()?.close()?;
259            }
260
261            b.into_inner()
262        } else {
263            let items: Vec<TableOfContentsItem> = self
264                .items
265                .iter()
266                .map(|item| {
267                    let mut item = item.clone();
268                    item.instr = self.instr.clone();
269                    item.dirty = self.dirty;
270                    if item.page_ref.is_none() {
271                        item.page_ref = self.page_ref_placeholder.clone();
272                    }
273                    item
274                })
275                .collect();
276
277            let mut b = XMLBuilder::from(stream);
278
279            if !self.without_sdt {
280                b = b
281                    .open_structured_tag()?
282                    .add_child(&p)?
283                    .open_structured_tag_content()?;
284            }
285
286            for c in self.before_contents.iter() {
287                match c {
288                    TocContent::Paragraph(p) => {
289                        b = b.add_child(&p)?;
290                    }
291                    TocContent::Table(t) => {
292                        b = b.add_child(&t)?;
293                    }
294                }
295            }
296
297            b = b.add_child(&items)?;
298
299            for (i, c) in self.after_contents.iter().enumerate() {
300                match c {
301                    TocContent::Paragraph(p) => {
302                        b = b.add_child(&p)?;
303                    }
304                    TocContent::Table(t) => {
305                        // insert empty line for table
306                        // because it causes docx error if table is added after TOC
307                        if i == 0 {
308                            b = b.add_child(&Paragraph::new().add_run(Run::new().add_text("")))?;
309                        }
310                        b = b.add_child(&t)?;
311                    }
312                }
313            }
314
315            if !self.without_sdt {
316                b = b.close()?.close()?;
317            }
318            b.into_inner()
319        }
320    }
321}
322
323#[cfg(test)]
324mod tests {
325
326    use super::*;
327    #[cfg(test)]
328    use pretty_assertions::assert_eq;
329    use std::str;
330
331    #[test]
332    fn test_toc() {
333        let b = TableOfContents::new().heading_styles_range(1, 3).build();
334        assert_eq!(
335            str::from_utf8(&b).unwrap(),
336            r#"<w:sdt><w:sdtPr><w:rPr /></w:sdtPr><w:sdtContent><w:p w14:paraId="12345678"><w:pPr><w:rPr /></w:pPr><w:r><w:rPr /><w:fldChar w:fldCharType="begin" w:dirty="true" /><w:instrText>TOC \o &quot;1-3&quot;</w:instrText><w:fldChar w:fldCharType="separate" w:dirty="false" /></w:r></w:p><w:p w14:paraId="12345678"><w:pPr><w:rPr /></w:pPr><w:r><w:rPr /><w:fldChar w:fldCharType="end" w:dirty="false" /></w:r></w:p></w:sdtContent></w:sdt>"#
337        );
338    }
339
340    #[test]
341    fn test_toc_without_sdt() {
342        let b = TableOfContents::new()
343            .without_sdt()
344            .heading_styles_range(1, 3)
345            .build();
346        assert_eq!(
347            str::from_utf8(&b).unwrap(),
348            r#"<w:p w14:paraId="12345678"><w:pPr><w:rPr /></w:pPr><w:r><w:rPr /><w:fldChar w:fldCharType="begin" w:dirty="true" /><w:instrText>TOC \o &quot;1-3&quot;</w:instrText><w:fldChar w:fldCharType="separate" w:dirty="false" /></w:r></w:p><w:p w14:paraId="12345678"><w:pPr><w:rPr /></w:pPr><w:r><w:rPr /><w:fldChar w:fldCharType="end" w:dirty="false" /></w:r></w:p>"#
349        );
350    }
351
352    /*
353    #[test]
354    fn test_toc_with_items() {
355        let b = TableOfContents::new()
356            .heading_styles_range(1, 3)
357            .add_items(Paragraph::new().add_run(Run::new().add_text("Hello")))
358            .build();
359        assert_eq!(
360            str::from_utf8(&b).unwrap(),
361            r#"<w:sdt><w:sdtPr /><w:sdtContent><w:p w14:paraId="12345678"><w:pPr><w:rPr /></w:pPr><w:r><w:rPr /><w:fldChar w:fldCharType="begin" w:dirty="false" /><w:instrText>TOC \o &quot;1-3&quot;</w:instrText><w:fldChar w:fldCharType="separate" w:dirty="false" /></w:r><w:r><w:rPr /><w:t xml:space="preserve">Hello</w:t></w:r><w:r><w:rPr /><w:fldChar w:fldCharType="end" w:dirty="false" /></w:r></w:p></w:sdtContent></w:sdt>"#
362        );
363    }
364    */
365}