docx_rs/documents/elements/
instr_toc.rs

1use serde::Serialize;
2use std::io::Write;
3
4use crate::documents::*;
5use crate::xml_builder::XMLBuilder;
6
7#[derive(Serialize, Debug, Clone, PartialEq, Default)]
8#[cfg_attr(feature = "wasm", derive(ts_rs::TS))]
9#[cfg_attr(feature = "wasm", ts(export))]
10pub struct StyleWithLevel(pub (String, usize));
11
12impl StyleWithLevel {
13    pub fn new(s: impl Into<String>, l: usize) -> Self {
14        Self((s.into(), l))
15    }
16}
17// https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_TOCTOC_topic_ID0ELZO1.html
18#[derive(Serialize, Debug, Clone, PartialEq, Default)]
19#[cfg_attr(feature = "wasm", derive(ts_rs::TS))]
20#[cfg_attr(feature = "wasm", ts(export))]
21#[serde(rename_all = "camelCase")]
22pub struct InstrToC {
23    // \o If no heading range is specified, all heading levels used in the document are listed.
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub heading_styles_range: Option<(usize, usize)>,
26    // \l Includes TC fields that assign entries to one of the levels specified by text in this switch's field-argument as a range having the form startLevel-endLevel,
27    //    where startLevel and endLevel are integers, and startLevel has a value equal-to or less-than endLevel.
28    //    TC fields that assign entries to lower levels are skipped.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub tc_field_level_range: Option<(usize, usize)>,
31    // \n Without field-argument, omits page numbers from the table of contents.
32    // .Page numbers are omitted from all levels unless a range of entry levels is specified by text in this switch's field-argument.
33    // A range is specified as for \l.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub omit_page_numbers_level_range: Option<(usize, usize)>,
36    // \b includes entries only from the portion of the document marked by the bookmark named by text in this switch's field-argument.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub entry_bookmark_name: Option<String>,
39    // \t Uses paragraphs formatted with styles other than the built-in heading styles.
40    // .  text in this switch's field-argument specifies those styles as a set of comma-separated doublets,
41    //    with each doublet being a comma-separated set of style name and table of content level. \t can be combined with \o.
42    pub styles_with_levels: Vec<StyleWithLevel>,
43    //  struct S texWin Lis switch's field-argument specifies a sequence of character
44    // .  The default is a tab with leader dots.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub entry_and_page_number_separator: Option<String>,
47    // \d
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub sequence_and_page_numbers_separator: Option<String>,
50    // \a
51    pub caption_label: Option<String>,
52    // \c
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub caption_label_including_numbers: Option<String>,
55    // \s
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub seq_field_identifier_for_prefix: Option<String>,
58    // \f
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub tc_field_identifier: Option<Option<String>>,
61    // \h
62    pub hyperlink: bool,
63    // \w
64    pub preserve_tab: bool,
65    // \x
66    pub preserve_new_line: bool,
67    // \u
68    pub use_applied_paragraph_line_level: bool,
69    // \z Hides tab leader and page numbers in Web layout view.
70    pub hide_tab_and_page_numbers_in_webview: bool,
71}
72
73impl InstrToC {
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    pub fn with_instr_text(s: &str) -> Self {
79        Self::from_str(s).expect("should convert to InstrToC")
80    }
81
82    pub fn heading_styles_range(mut self, start: usize, end: usize) -> Self {
83        self.heading_styles_range = Some((start, end));
84        self
85    }
86
87    pub fn tc_field_level_range(mut self, start: usize, end: usize) -> Self {
88        self.tc_field_level_range = Some((start, end));
89        self
90    }
91
92    pub fn tc_field_identifier(mut self, t: Option<String>) -> Self {
93        self.tc_field_identifier = Some(t);
94        self
95    }
96
97    pub fn omit_page_numbers_level_range(mut self, start: usize, end: usize) -> Self {
98        self.omit_page_numbers_level_range = Some((start, end));
99        self
100    }
101
102    pub fn entry_and_page_number_separator(mut self, t: impl Into<String>) -> Self {
103        self.entry_and_page_number_separator = Some(t.into());
104        self
105    }
106
107    pub fn entry_bookmark_name(mut self, t: impl Into<String>) -> Self {
108        self.entry_bookmark_name = Some(t.into());
109        self
110    }
111
112    pub fn caption_label(mut self, t: impl Into<String>) -> Self {
113        self.caption_label = Some(t.into());
114        self
115    }
116
117    pub fn caption_label_including_numbers(mut self, t: impl Into<String>) -> Self {
118        self.caption_label_including_numbers = Some(t.into());
119        self
120    }
121
122    pub fn sequence_and_page_numbers_separator(mut self, t: impl Into<String>) -> Self {
123        self.sequence_and_page_numbers_separator = Some(t.into());
124        self
125    }
126
127    pub fn seq_field_identifier_for_prefix(mut self, t: impl Into<String>) -> Self {
128        self.seq_field_identifier_for_prefix = Some(t.into());
129        self
130    }
131
132    pub fn hyperlink(mut self) -> Self {
133        self.hyperlink = true;
134        self
135    }
136
137    pub fn preserve_tab(mut self) -> Self {
138        self.preserve_tab = true;
139        self
140    }
141
142    pub fn preserve_new_line(mut self) -> Self {
143        self.preserve_new_line = true;
144        self
145    }
146
147    pub fn use_applied_paragraph_line_level(mut self) -> Self {
148        self.use_applied_paragraph_line_level = true;
149        self
150    }
151
152    pub fn hide_tab_and_page_numbers_in_webview(mut self) -> Self {
153        self.hide_tab_and_page_numbers_in_webview = true;
154        self
155    }
156
157    pub fn add_style_with_level(mut self, s: StyleWithLevel) -> Self {
158        self.styles_with_levels.push(s);
159        self
160    }
161}
162
163impl BuildXML for InstrToC {
164    fn build_to<W: Write>(
165        &self,
166        stream: xml::writer::EventWriter<W>,
167    ) -> xml::writer::Result<xml::writer::EventWriter<W>> {
168        let mut b = XMLBuilder::from(stream);
169        let raw = b.inner_mut()?;
170
171        write!(raw, "TOC")?;
172
173        // \a
174        if let Some(ref t) = self.caption_label {
175            write!(raw, " \\a &quot;{}&quot;", t)?;
176        }
177
178        // \b
179        if let Some(ref t) = self.entry_bookmark_name {
180            write!(raw, " \\b &quot;{}&quot;", t)?;
181        }
182
183        // \c
184        if let Some(ref t) = self.caption_label_including_numbers {
185            write!(raw, " \\c &quot;{}&quot;", t)?;
186        }
187
188        // \d
189        if let Some(ref t) = self.sequence_and_page_numbers_separator {
190            write!(raw, " \\d &quot;{}&quot;", t)?;
191        }
192
193        // \f
194        if let Some(ref t) = self.tc_field_identifier {
195            if let Some(ref t) = t {
196                write!(raw, " \\f &quot;{}&quot;", t)?;
197            } else {
198                write!(raw, " \\f")?;
199            }
200        }
201
202        // \l
203        if let Some(range) = self.tc_field_level_range {
204            write!(raw, " \\l &quot;{}-{}&quot;", range.0, range.1)?;
205        }
206
207        // \n
208        if let Some(range) = self.omit_page_numbers_level_range {
209            write!(raw, " \\n &quot;{}-{}&quot;", range.0, range.1)?;
210        }
211
212        // \o
213        if let Some(range) = self.heading_styles_range {
214            write!(raw, " \\o &quot;{}-{}&quot;", range.0, range.1)?;
215        }
216
217        // \p
218        if let Some(ref t) = self.entry_and_page_number_separator {
219            write!(raw, " \\p &quot;{}&quot;", t)?;
220        }
221
222        // \s
223        if let Some(ref t) = self.seq_field_identifier_for_prefix {
224            write!(raw, " \\s &quot;{}&quot;", t)?;
225        }
226
227        // \t
228        if !self.styles_with_levels.is_empty() {
229            let s = self
230                .styles_with_levels
231                .iter()
232                .map(|s| format!("{},{}", (s.0).0, (s.0).1))
233                .collect::<Vec<String>>()
234                .join(",");
235            write!(raw, " \\t &quot;{}&quot;", s)?;
236        }
237
238        // \h
239        if self.hyperlink {
240            write!(raw, " \\h")?;
241        }
242
243        // \u
244        if self.use_applied_paragraph_line_level {
245            write!(raw, " \\u")?;
246        }
247
248        // \w
249        if self.preserve_tab {
250            write!(raw, " \\w")?;
251        }
252
253        // \x
254        if self.preserve_new_line {
255            write!(raw, " \\x")?;
256        }
257
258        // \z
259        if self.hide_tab_and_page_numbers_in_webview {
260            write!(raw, " \\z")?;
261        }
262
263        b.into_inner()
264    }
265}
266
267fn parse_level_range(i: &str) -> Option<(usize, usize)> {
268    let r = i.replace("&quot;", "").replace('\"', "");
269    let r: Vec<&str> = r.split('-').collect();
270    if let Some(s) = r.first() {
271        if let Ok(s) = usize::from_str(s) {
272            if let Some(e) = r.get(1) {
273                if let Ok(e) = usize::from_str(e) {
274                    return Some((s, e));
275                }
276            }
277        }
278    }
279    None
280}
281
282impl std::str::FromStr for InstrToC {
283    type Err = ();
284
285    fn from_str(instr: &str) -> Result<Self, Self::Err> {
286        let mut s = instr.split(' ').peekable();
287        let mut toc = InstrToC::new();
288        loop {
289            if let Some(i) = s.next() {
290                match i {
291                    "\\a" => {
292                        if let Some(r) = s.next() {
293                            let r = r.replace("&quot;", "").replace('\"', "");
294                            toc = toc.caption_label(r);
295                        }
296                    }
297                    "\\b" => {
298                        if let Some(r) = s.next() {
299                            let r = r.replace("&quot;", "").replace('\"', "");
300                            toc = toc.entry_bookmark_name(r);
301                        }
302                    }
303                    "\\c" => {
304                        if let Some(r) = s.next() {
305                            let r = r.replace("&quot;", "").replace('\"', "");
306                            toc = toc.caption_label_including_numbers(r);
307                        }
308                    }
309                    "\\d" => {
310                        if let Some(r) = s.next() {
311                            let r = r.replace("&quot;", "").replace('\"', "");
312                            toc = toc.sequence_and_page_numbers_separator(r);
313                        }
314                    }
315                    "\\f" => {
316                        if let Some(n) = s.peek() {
317                            if !n.starts_with("\\") {
318                                if let Some(r) = s.next() {
319                                    let r = r.replace("&quot;", "").replace('\"', "");
320                                    if r.is_empty() {
321                                        toc = toc.tc_field_identifier(None);
322                                    } else {
323                                        toc = toc.tc_field_identifier(Some(r));
324                                    }
325                                }
326                            } else {
327                                toc = toc.tc_field_identifier(None);
328                            }
329                        }
330                    }
331                    "\\h" => toc = toc.hyperlink(),
332                    "\\l" => {
333                        if let Some(r) = s.next() {
334                            if let Some((s, e)) = parse_level_range(r) {
335                                toc = toc.tc_field_level_range(s, e);
336                            }
337                        }
338                    }
339                    "\\n" => {
340                        if let Some(r) = s.next() {
341                            if let Some((s, e)) = parse_level_range(r) {
342                                toc = toc.omit_page_numbers_level_range(s, e);
343                            }
344                        }
345                    }
346                    "\\o" => {
347                        if let Some(r) = s.next() {
348                            if let Some((s, e)) = parse_level_range(r) {
349                                toc = toc.heading_styles_range(s, e);
350                            }
351                        }
352                    }
353                    "\\p" => {
354                        if let Some(r) = s.next() {
355                            let r = r.replace("&quot;", "").replace('\"', "");
356                            toc = toc.entry_and_page_number_separator(r);
357                        }
358                    }
359                    "\\s" => {
360                        if let Some(r) = s.next() {
361                            let r = r.replace("&quot;", "").replace('\"', "");
362                            toc = toc.seq_field_identifier_for_prefix(r);
363                        }
364                    }
365                    "\\t" => {
366                        if let Some(r) = s.next() {
367                            let r = r.replace("&quot;", "").replace('\"', "");
368                            let mut r = r.split(',');
369                            loop {
370                                if let Some(style) = r.next() {
371                                    if let Some(level) = r.next() {
372                                        if let Ok(level) = usize::from_str(level) {
373                                            toc = toc.add_style_with_level(StyleWithLevel((
374                                                style.to_string(),
375                                                level,
376                                            )));
377                                            continue;
378                                        }
379                                    }
380                                }
381                                break;
382                            }
383                        }
384                    }
385                    "\\u" => toc = toc.use_applied_paragraph_line_level(),
386                    "\\w" => toc = toc.preserve_tab(),
387                    "\\x" => toc = toc.preserve_new_line(),
388                    "\\z" => toc = toc.hide_tab_and_page_numbers_in_webview(),
389                    _ => {}
390                }
391            } else {
392                return Ok(toc);
393            }
394        }
395    }
396}
397
398#[cfg(test)]
399mod tests {
400
401    use super::*;
402    #[cfg(test)]
403    use pretty_assertions::assert_eq;
404    use std::str;
405
406    #[test]
407    fn test_toc() {
408        let b = InstrToC::new().heading_styles_range(1, 3).build();
409        assert_eq!(str::from_utf8(&b).unwrap(), r#"TOC \o &quot;1-3&quot;"#);
410    }
411
412    #[test]
413    fn test_toc_with_styles() {
414        let b = InstrToC::new()
415            .heading_styles_range(1, 3)
416            .add_style_with_level(StyleWithLevel::new("style1", 2))
417            .add_style_with_level(StyleWithLevel::new("style2", 3))
418            .build();
419        assert_eq!(
420            str::from_utf8(&b).unwrap(),
421            r#"TOC \o &quot;1-3&quot; \t &quot;style1,2,style2,3&quot;"#
422        );
423    }
424
425    #[test]
426    fn read_toc_with_o_and_h() {
427        let i = r#"TOC \o &quot;1-3&quot; \h"#;
428        let i = InstrToC::from_str(i).unwrap();
429        assert_eq!(i, InstrToC::new().heading_styles_range(1, 3).hyperlink());
430    }
431
432    #[test]
433    fn read_toc_with_l_and_n() {
434        let i = r#"TOC \o &quot;1-3&quot; \l &quot;4-5&quot; \n &quot;1-4&quot; \h"#;
435        let i = InstrToC::from_str(i).unwrap();
436        assert_eq!(
437            i,
438            InstrToC::new()
439                .heading_styles_range(1, 3)
440                .hyperlink()
441                .omit_page_numbers_level_range(1, 4)
442                .tc_field_level_range(4, 5)
443        );
444    }
445
446    #[test]
447    fn read_toc_with_a_and_b_and_t() {
448        let i = r#"TOC \a &quot;hoge&quot; \b &quot;test&quot; \o &quot;1-3&quot; \t &quot;MySpectacularStyle,1,MySpectacularStyle2,4&quot;"#;
449        let i = InstrToC::from_str(i).unwrap();
450        assert_eq!(
451            i,
452            InstrToC::new()
453                .caption_label("hoge")
454                .entry_bookmark_name("test")
455                .heading_styles_range(1, 3)
456                .add_style_with_level(StyleWithLevel::new("MySpectacularStyle", 1))
457                .add_style_with_level(StyleWithLevel::new("MySpectacularStyle2", 4))
458        );
459    }
460
461    #[test]
462    fn with_instr_text() {
463        let s = r#"TOC \o "1-3" \h \z \u"#;
464        let i = InstrToC::with_instr_text(s);
465        assert_eq!(
466            i,
467            InstrToC::new()
468                .heading_styles_range(1, 3)
469                .use_applied_paragraph_line_level()
470                .hide_tab_and_page_numbers_in_webview()
471                .hyperlink()
472        );
473    }
474
475    #[test]
476    fn with_instr_text2() {
477        let s = r#"TOC \f \h \z \u"#;
478        let i = InstrToC::with_instr_text(s);
479        assert_eq!(
480            i,
481            InstrToC::new()
482                .tc_field_identifier(None)
483                .use_applied_paragraph_line_level()
484                .hide_tab_and_page_numbers_in_webview()
485                .hyperlink()
486        );
487    }
488
489    #[test]
490    fn with_instr_text3() {
491        let s = r#"TOC \f abc \h \z \u"#;
492        let i = InstrToC::with_instr_text(s);
493        assert_eq!(
494            i,
495            InstrToC::new()
496                .tc_field_identifier(Some("abc".to_string()))
497                .use_applied_paragraph_line_level()
498                .hide_tab_and_page_numbers_in_webview()
499                .hyperlink()
500        );
501    }
502}