docx_rust/document/
numbering.rs

1//! Comments part
2//!
3//! The corresponding ZIP item is `/word/numbering.xml`.
4#![allow(unused_must_use)]
5
6use hard_xml::{XmlRead, XmlResult, XmlWrite, XmlWriter};
7use std::{borrow::Cow, io::Write};
8
9use crate::{
10    formatting::{CharacterProperty, Indent, JustificationVal},
11    schema::{SCHEMA_MAIN, SCHEMA_WORDML_14},
12};
13
14#[derive(Debug, Default, XmlRead, Clone)]
15#[cfg_attr(test, derive(PartialEq))]
16#[xml(tag = "w:numbering")]
17/// Numbering defines ordered and unordered lists.
18pub struct Numbering<'a> {
19    #[xml(child = "w:abstractNum")]
20    /// Abstract numberings are not used directly when laying out your document.
21    /// Instead, they are referred to by the numberings.
22    pub abstract_numberings: Vec<AbstractNum<'a>>,
23    #[xml(child = "w:num")]
24    /// Numberings are used by your document and refer to abstract numberings for layout.
25    pub numberings: Vec<Num>,
26}
27
28#[derive(Debug, Default, XmlRead, XmlWrite, Clone)]
29#[cfg_attr(test, derive(PartialEq))]
30#[xml(tag = "w:abstractNum")]
31pub struct AbstractNum<'a> {
32    #[xml(attr = "w:abstractNumId")]
33    pub abstract_num_id: Option<isize>,
34    #[xml(child = "w:nsid")]
35    pub nsid: Option<Nsid<'a>>,
36    #[xml(child = "w:multiLevelType")]
37    pub multi_level_type: MultiLevelType<'a>,
38    #[xml(child = "w:lvl")]
39    pub levels: Vec<Level<'a>>,
40}
41
42#[derive(Debug, Default, XmlRead, XmlWrite, Clone)]
43#[cfg_attr(test, derive(PartialEq))]
44#[xml(tag = "w:nsid")]
45pub struct Nsid<'a> {
46    #[xml(attr = "w:val")]
47    pub value: Cow<'a, str>,
48}
49
50#[derive(Debug, Default, XmlRead, XmlWrite, Clone)]
51#[cfg_attr(test, derive(PartialEq))]
52#[xml(tag = "w:multiLevelType")]
53pub struct MultiLevelType<'a> {
54    #[xml(attr = "w:val")]
55    pub value: Cow<'a, str>,
56}
57
58#[derive(Debug, Default, XmlRead, XmlWrite, Clone)]
59#[cfg_attr(test, derive(PartialEq))]
60#[xml(tag = "w:lvl")]
61pub struct Level<'a> {
62    #[xml(attr = "w:ilvl")]
63    pub i_level: Option<isize>,
64    #[xml(child = "w:start")]
65    pub start: Option<LevelStart>,
66    #[xml(child = "w:numFmt")]
67    pub number_format: Option<NumFmt<'a>>,
68    #[xml(child = "w:lvlText")]
69    pub level_text: Option<LevelText<'a>>,
70    #[xml(child = "w:lvlJc")]
71    pub justification: Option<LevelJustification>,
72    #[xml(child = "w:pPr")]
73    pub p_pr: Option<PPr>,
74    #[xml(child = "w:rPr")]
75    pub r_pr: Vec<CharacterProperty<'a>>,
76}
77
78#[derive(Debug, Default, XmlRead, XmlWrite, Clone)]
79#[cfg_attr(test, derive(PartialEq))]
80#[xml(tag = "w:pPr")]
81pub struct PPr {
82    #[xml(child = "w:ind")]
83    pub indent: Option<Indent>,
84}
85
86#[derive(Debug, Default, XmlRead, XmlWrite, Clone)]
87#[cfg_attr(test, derive(PartialEq))]
88#[xml(tag = "w:numFmt")]
89/// TODO Replace by enum NumberFormat
90pub struct NumFmt<'a> {
91    #[xml(attr = "w:val")]
92    pub value: Cow<'a, str>,
93}
94
95#[derive(Debug, Default, XmlRead, XmlWrite, Clone)]
96#[cfg_attr(test, derive(PartialEq))]
97#[xml(tag = "w:start")]
98/// TODO Replace by enum NumberFormat
99pub struct LevelStart {
100    #[xml(attr = "w:val")]
101    pub value: Option<isize>,
102}
103
104#[derive(Debug, Default, XmlRead, XmlWrite, Clone)]
105#[cfg_attr(test, derive(PartialEq))]
106#[xml(tag = "w:lvlText")]
107pub struct LevelText<'a> {
108    #[xml(attr = "w:val")]
109    pub value: Cow<'a, str>,
110}
111
112#[derive(Debug, XmlRead, XmlWrite, Clone)]
113#[cfg_attr(test, derive(PartialEq))]
114#[xml(tag = "w:lvlJc")]
115pub struct LevelJustification {
116    #[xml(attr = "w:val")]
117    pub value: JustificationVal,
118}
119
120#[derive(Debug, Default, XmlRead, XmlWrite, Clone)]
121#[cfg_attr(test, derive(PartialEq))]
122#[xml(tag = "w:num")]
123pub struct Num {
124    #[xml(attr = "w:numId")]
125    pub num_id: Option<isize>,
126    #[xml(child = "w:abstractNumId")]
127    pub abstract_num_id: Option<AbstractNumId>,
128    #[xml(child = "w:lvlOverride")]
129    pub level_overrides: Vec<LevelOverride>,
130}
131
132#[derive(Debug, Default, XmlRead, XmlWrite, Clone)]
133#[cfg_attr(test, derive(PartialEq))]
134#[xml(tag = "w:lvlOverride")]
135pub struct LevelOverride {
136    #[xml(attr = "w:ilvl")]
137    pub i_level: Option<isize>,
138    #[xml(child = "w:startOverride")]
139    pub start_override: Option<StartOverride>,
140}
141
142#[derive(Debug, Default, XmlRead, XmlWrite, Clone)]
143#[cfg_attr(test, derive(PartialEq))]
144#[xml(tag = "w:startOverride")]
145pub struct StartOverride {
146    #[xml(attr = "w:val")]
147    pub value: Option<isize>,
148}
149
150#[derive(Debug, Default, XmlRead, XmlWrite, Clone)]
151#[cfg_attr(test, derive(PartialEq))]
152#[xml(tag = "w:abstractNumId")]
153pub struct AbstractNumId {
154    #[xml(attr = "w:val")]
155    pub value: Option<isize>,
156}
157
158impl<'a> Numbering<'a> {
159    /// Actual numberings refer to abstract numberings, and may overrule some settings.
160    /// This helper function takes an numbering id that is provided in a paragraph, looks up
161    /// the details in the numbering section and merges it with the abstract numbering to get
162    /// a complete AbstractNum object.
163    pub fn numbering_details(&self, id: isize) -> Option<AbstractNum> {
164        self.numberings.iter().find_map(|n| {
165            if n.num_id != Some(id) || n.abstract_num_id.is_none() {
166                None
167            } else {
168                if let Some(abstract_num_id) = &n.abstract_num_id {
169                    if let Some(abstract_numbering) = self
170                        .abstract_numberings
171                        .iter()
172                        .find(|an| an.abstract_num_id == abstract_num_id.value)
173                    {
174                        let mut an = abstract_numbering.clone();
175                        n.level_overrides.iter().for_each(|o| {
176                            let LevelOverride {
177                                i_level,
178                                start_override,
179                            } = o;
180                            if i_level.is_some() && start_override.is_some() {
181                                if let Some(level) =
182                                    an.levels.iter_mut().find(|level| level.i_level == *i_level)
183                                {
184                                    level.start = Some(LevelStart {
185                                        value: start_override.as_ref().unwrap().value.clone(),
186                                    });
187                                }
188                            }
189                        });
190                        return Some(an);
191                    }
192                }
193
194                None
195            }
196        })
197    }
198}
199
200impl<'a> XmlWrite for Numbering<'a> {
201    fn to_writer<W: Write>(&self, writer: &mut XmlWriter<W>) -> XmlResult<()> {
202        let Numbering {
203            abstract_numberings: abstract_nums,
204            numberings: nums,
205        } = self;
206
207        log::debug!("[Numbering] Started writing.");
208
209        let _ = write!(writer.inner, "{}", crate::schema::SCHEMA_XML);
210
211        writer.write_element_start("w:numbering")?;
212
213        writer.write_attribute("xmlns:w", SCHEMA_MAIN)?;
214
215        writer.write_attribute("xmlns:w14", SCHEMA_WORDML_14)?;
216
217        writer.write_element_end_open()?;
218
219        for an in abstract_nums {
220            an.to_writer(writer)?;
221        }
222
223        for num in nums {
224            num.to_writer(writer)?;
225        }
226
227        writer.write_element_end_close("w:numbering")?;
228
229        log::debug!("[Numbering] Finished writing.");
230
231        Ok(())
232    }
233}
234
235#[cfg(test)]
236
237const NUMBERING_XML: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
238    <w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
239        <w:abstractNum w:abstractNumId="990">
240            <w:nsid w:val="0000A990" />
241            <w:multiLevelType w:val="multilevel" />
242            <w:lvl w:ilvl="0">
243                <w:numFmt w:val="bullet" />
244                <w:lvlText w:val=" " />
245                <w:lvlJc w:val="left" />
246                <w:pPr>
247                    <w:ind w:left="720" w:hanging="360" />
248                </w:pPr>
249            </w:lvl>
250            <w:lvl w:ilvl="1">
251                <w:numFmt w:val="bullet" />
252                <w:lvlText w:val=" " />
253                <w:lvlJc w:val="left" />
254                <w:pPr>
255                    <w:ind w:left="1440" w:hanging="360" />
256                </w:pPr>
257            </w:lvl>
258        </w:abstractNum>
259        <w:abstractNum w:abstractNumId="99411">
260            <w:nsid w:val="00A99411" />
261            <w:multiLevelType w:val="multilevel" />
262            <w:lvl w:ilvl="0">
263                <w:start w:val="1" />
264                <w:numFmt w:val="decimal" />
265                <w:lvlText w:val="%1." />
266                <w:lvlJc w:val="left" />
267                <w:pPr>
268                    <w:ind w:left="720" w:hanging="360" />
269                </w:pPr>
270            </w:lvl>
271            <w:lvl w:ilvl="1">
272                <w:start w:val="1" />
273                <w:numFmt w:val="decimal" />
274                <w:lvlText w:val="%2." />
275                <w:lvlJc w:val="left" />
276                <w:pPr>
277                    <w:ind w:left="1440" w:hanging="360" />
278                </w:pPr>
279            </w:lvl>
280            <w:lvl w:ilvl="2">
281                <w:start w:val="1" />
282                <w:numFmt w:val="decimal" />
283                <w:lvlText w:val="%3." />
284                <w:lvlJc w:val="left" />
285                <w:pPr>
286                    <w:ind w:left="2160" w:hanging="360" />
287                </w:pPr>
288            </w:lvl>
289        </w:abstractNum>
290        <w:num w:numId="1000">
291            <w:abstractNumId w:val="990" />
292        </w:num>
293        <w:num w:numId="1001">
294            <w:abstractNumId w:val="99411" />
295            <w:lvlOverride w:ilvl="0">
296                <w:startOverride w:val="1" />
297            </w:lvlOverride>
298            <w:lvlOverride w:ilvl="1">
299                <w:startOverride w:val="1" />
300            </w:lvlOverride>
301        </w:num>
302    </w:numbering>
303"#;
304
305#[test]
306fn xml_parsing() {
307    let numbering = Numbering::from_str(NUMBERING_XML).unwrap();
308    assert_eq!(numbering.abstract_numberings.len(), 2);
309    assert_eq!(numbering.numberings.len(), 2);
310    assert_eq!(
311        numbering.abstract_numberings[0]
312            .nsid
313            .as_ref()
314            .unwrap()
315            .value,
316        "0000A990"
317    );
318    assert_eq!(
319        numbering.abstract_numberings[0].levels[0]
320            .number_format
321            .as_ref()
322            .unwrap()
323            .value,
324        "bullet"
325    );
326    assert_eq!(
327        numbering.numberings[0]
328            .abstract_num_id
329            .as_ref()
330            .unwrap()
331            .value,
332        Some(990_isize)
333    );
334}
335
336#[test]
337fn find_numbering_details() {
338    let numbering = Numbering::from_str(NUMBERING_XML).unwrap();
339    if let Some(num) = numbering.numbering_details(
340        numbering.numberings[0]
341            .abstract_num_id
342            .as_ref()
343            .unwrap()
344            .value
345            .unwrap(),
346    ) {
347        assert_eq!(
348            num.levels[0].number_format,
349            Some(NumFmt {
350                value: Cow::Borrowed("bullet")
351            })
352        );
353    }
354    if let Some(num) = numbering.numbering_details(1001) {
355        assert_eq!(
356            num.levels[0].number_format,
357            Some(NumFmt {
358                value: Cow::Borrowed("decimal")
359            })
360        );
361        assert_eq!(
362            num.levels[1].level_text,
363            Some(LevelText {
364                value: Cow::Borrowed("%2.")
365            })
366        );
367    }
368}
369
370#[test]
371fn xml_writing() {
372    fn replace_whitespace(input: &str, replacement: &str) -> String {
373        let mut result = String::new();
374        let mut last_was_whitespace_or_bracket = false;
375
376        for c in input.chars() {
377            if c.is_whitespace() {
378                if !last_was_whitespace_or_bracket {
379                    result.push_str(replacement);
380                    last_was_whitespace_or_bracket = true;
381                }
382            } else {
383                result.push(c);
384                last_was_whitespace_or_bracket = if c == '>' || c == '"' { true } else { false };
385            }
386        }
387
388        result
389    }
390
391    let numbering = Numbering::from_str(NUMBERING_XML).unwrap();
392    let result = numbering.to_string().unwrap();
393    assert_eq!(
394        replace_whitespace(NUMBERING_XML, " "),
395        replace_whitespace(
396            &result.replace(&format!(" xmlns:w14=\"{SCHEMA_WORDML_14}\""), ""),
397            " "
398        )
399    );
400}