docx_rs/documents/elements/
table_of_contents.rs1use 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#[derive(Serialize, Debug, Clone, PartialEq, Default)]
46pub struct TableOfContents {
47 pub instr: InstrToC,
48 pub items: Vec<TableOfContentsItem>,
49 pub auto: bool,
51 pub dirty: bool,
52 pub without_sdt: bool,
54 pub alias: Option<String>,
55 pub page_ref_placeholder: Option<String>,
56 #[serde(skip_serializing_if = "Vec::is_empty")]
58 pub before_contents: Vec<TocContent>,
59 #[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 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 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 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 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 "1-3"</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 "1-3"</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 }