docx_rs/documents/elements/
instr_toc.rs1use 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#[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 #[serde(skip_serializing_if = "Option::is_none")]
25 pub heading_styles_range: Option<(usize, usize)>,
26 #[serde(skip_serializing_if = "Option::is_none")]
30 pub tc_field_level_range: Option<(usize, usize)>,
31 #[serde(skip_serializing_if = "Option::is_none")]
35 pub omit_page_numbers_level_range: Option<(usize, usize)>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub entry_bookmark_name: Option<String>,
39 pub styles_with_levels: Vec<StyleWithLevel>,
43 #[serde(skip_serializing_if = "Option::is_none")]
46 pub entry_and_page_number_separator: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub sequence_and_page_numbers_separator: Option<String>,
50 pub caption_label: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub caption_label_including_numbers: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub seq_field_identifier_for_prefix: Option<String>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub tc_field_identifier: Option<Option<String>>,
61 pub hyperlink: bool,
63 pub preserve_tab: bool,
65 pub preserve_new_line: bool,
67 pub use_applied_paragraph_line_level: bool,
69 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 if let Some(ref t) = self.caption_label {
175 write!(raw, " \\a "{}"", t)?;
176 }
177
178 if let Some(ref t) = self.entry_bookmark_name {
180 write!(raw, " \\b "{}"", t)?;
181 }
182
183 if let Some(ref t) = self.caption_label_including_numbers {
185 write!(raw, " \\c "{}"", t)?;
186 }
187
188 if let Some(ref t) = self.sequence_and_page_numbers_separator {
190 write!(raw, " \\d "{}"", t)?;
191 }
192
193 if let Some(ref t) = self.tc_field_identifier {
195 if let Some(ref t) = t {
196 write!(raw, " \\f "{}"", t)?;
197 } else {
198 write!(raw, " \\f")?;
199 }
200 }
201
202 if let Some(range) = self.tc_field_level_range {
204 write!(raw, " \\l "{}-{}"", range.0, range.1)?;
205 }
206
207 if let Some(range) = self.omit_page_numbers_level_range {
209 write!(raw, " \\n "{}-{}"", range.0, range.1)?;
210 }
211
212 if let Some(range) = self.heading_styles_range {
214 write!(raw, " \\o "{}-{}"", range.0, range.1)?;
215 }
216
217 if let Some(ref t) = self.entry_and_page_number_separator {
219 write!(raw, " \\p "{}"", t)?;
220 }
221
222 if let Some(ref t) = self.seq_field_identifier_for_prefix {
224 write!(raw, " \\s "{}"", t)?;
225 }
226
227 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 "{}"", s)?;
236 }
237
238 if self.hyperlink {
240 write!(raw, " \\h")?;
241 }
242
243 if self.use_applied_paragraph_line_level {
245 write!(raw, " \\u")?;
246 }
247
248 if self.preserve_tab {
250 write!(raw, " \\w")?;
251 }
252
253 if self.preserve_new_line {
255 write!(raw, " \\x")?;
256 }
257
258 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(""", "").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(""", "").replace('\"', "");
294 toc = toc.caption_label(r);
295 }
296 }
297 "\\b" => {
298 if let Some(r) = s.next() {
299 let r = r.replace(""", "").replace('\"', "");
300 toc = toc.entry_bookmark_name(r);
301 }
302 }
303 "\\c" => {
304 if let Some(r) = s.next() {
305 let r = r.replace(""", "").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(""", "").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(""", "").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(""", "").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(""", "").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(""", "").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 "1-3""#);
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 "1-3" \t "style1,2,style2,3""#
422 );
423 }
424
425 #[test]
426 fn read_toc_with_o_and_h() {
427 let i = r#"TOC \o "1-3" \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 "1-3" \l "4-5" \n "1-4" \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 "hoge" \b "test" \o "1-3" \t "MySpectacularStyle,1,MySpectacularStyle2,4""#;
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}