onenote_parser/onenote/
outline.rs

1use crate::errors::{ErrorKind, Result};
2use crate::fsshttpb::data::exguid::ExGuid;
3use crate::one::property::layout_alignment::LayoutAlignment;
4use crate::one::property_set::{outline_element_node, outline_group, outline_node, PropertySetId};
5use crate::onenote::content::{parse_content, Content};
6use crate::onenote::list::{parse_list, List};
7use crate::onestore::object_space::ObjectSpace;
8
9/// A content outline.
10///
11/// See [\[MS-ONE\] 1.3.2.1] and [\[MS-ONE\] 2.2.20].
12///
13/// [\[MS-ONE\] 1.3.2.1]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/22e65fbe-01db-4c3f-8b00-101a6cd6f9c4
14/// [\[MS-ONE\] 2.2.20]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/b25fa331-e07e-474e-99c9-b3603b7bf937
15#[derive(Clone, Debug)]
16pub struct Outline {
17    pub(crate) child_level: u8,
18    pub(crate) list_spacing: Option<f32>,
19    pub(crate) indents: Vec<f32>,
20
21    pub(crate) alignment_in_parent: Option<LayoutAlignment>,
22    pub(crate) alignment_self: Option<LayoutAlignment>,
23
24    pub(crate) layout_max_height: Option<f32>,
25    pub(crate) layout_max_width: Option<f32>,
26    pub(crate) layout_reserved_width: Option<f32>,
27    pub(crate) layout_minimum_outline_width: Option<f32>,
28    pub(crate) is_layout_size_set_by_user: bool,
29    pub(crate) offset_horizontal: Option<f32>,
30    pub(crate) offset_vertical: Option<f32>,
31
32    pub(crate) items: Vec<OutlineItem>,
33}
34
35impl Outline {
36    /// Contents of this outline.
37    pub fn items(&self) -> &[OutlineItem] {
38        &self.items
39    }
40
41    /// The nesting level of this outline's contents.
42    ///
43    /// See [\[MS-ONE\] 2.3.8].
44    ///
45    /// [\[MS-ONE\] 2.3.8]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/b631036a-9152-4385-8165-60fc324e5efd
46    pub fn child_level(&self) -> u8 {
47        self.child_level
48    }
49
50    /// The horizontal distance between a list index number or bullet and the outline content.
51    ///
52    /// See [\[MS-ONE\] 2.3.45].
53    ///
54    /// [\[MS-ONE\] 2.3.45]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/3139a52f-fc22-48a3-9765-cebc6774d109
55    pub fn list_spacing(&self) -> Option<f32> {
56        self.list_spacing
57    }
58
59    /// The indentation of each level in the outline.
60    ///
61    /// The contents are specified in [\[MS-ONE\] 2.2.2] but the semantics described there
62    /// don't really match what the OneNote desktop and web applications seem to be doing.
63    ///
64    /// [\[MS-ONE\] 2.2.2]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/269a3e7b-d85a-4ba8-8e1d-d85e1c840772
65    pub fn indents(&self) -> &[f32] {
66        &self.indents
67    }
68
69    /// The outline's alignment relative to the parent element (if present).
70    ///
71    /// See [\[MS-ONE\] 2.3.27].
72    ///
73    /// [\[MS-ONE\] 2.3.27]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/61fa50be-c355-4b8d-ac01-761a2f7f66c0
74    pub fn alignment_in_parent(&self) -> Option<LayoutAlignment> {
75        self.alignment_in_parent
76    }
77
78    /// The outline's alignment.
79    ///
80    /// See [\[MS-ONE\] 2.3.33].
81    ///
82    /// [\[MS-ONE\] 2.3.33]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/4e7fe9db-2fdb-4239-b291-dc4b909c94ad
83    pub fn alignment_self(&self) -> Option<LayoutAlignment> {
84        self.alignment_self
85    }
86
87    /// The outline's max height in half-inch increments.
88    ///
89    /// See [\[MS-ONE\] 2.3.24].
90    ///
91    /// [\[MS-ONE\] 2.3.24]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/76ab7015-2c74-4783-8435-c68b17dd6882
92    pub fn layout_max_height(&self) -> Option<f32> {
93        self.layout_max_height
94    }
95
96    /// The outline's max width in half-inch increments.
97    ///
98    /// See [\[MS-ONE\] 2.3.22].
99    ///
100    /// [\[MS-ONE\] 2.3.22]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/a770ac4b-2225-4aa6-ba92-d3a51f97c405
101    pub fn layout_max_width(&self) -> Option<f32> {
102        self.layout_max_width
103    }
104
105    /// The outline's minimum width before the text wraps in half-inch increments.
106    ///
107    /// See [\[MS-ONE\] 2.3.46].
108    ///
109    /// [\[MS-ONE\] 2.3.46]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/e65a3ebc-da8b-4909-a423-b309e2457b36
110    pub fn layout_reserved_width(&self) -> Option<f32> {
111        self.layout_reserved_width
112    }
113
114    /// The outline's minimum width in half-inch increments.
115    ///
116    /// See [\[MS-ONE\] 2.3.49].
117    ///
118    /// [\[MS-ONE\] 2.3.49]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/ebeff222-f4e5-4c58-861c-e28b816d01ce
119    pub fn layout_minimum_outline_width(&self) -> Option<f32> {
120        self.layout_minimum_outline_width
121    }
122
123    /// Whether the [`layout_max_width()`](Self::layout_max_width()) value is set by the user.
124    ///
125    /// See [\[MS-ONE\] 2.3.44].
126    ///
127    /// [\[MS-ONE\] 2.3.44]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/19227b81-43ab-484c-aaae-d33cf13e2602
128    pub fn is_layout_size_set_by_user(&self) -> bool {
129        self.is_layout_size_set_by_user
130    }
131
132    /// The horizontal offset from the page origin in half-inch increments.
133    ///
134    /// See [\[MS-ONE\] 2.3.18].
135    ///
136    /// [\[MS-ONE\] 2.3.18]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/5fb9e84a-c9e9-4537-ab14-e5512f24669a
137    pub fn offset_horizontal(&self) -> Option<f32> {
138        self.offset_horizontal
139    }
140
141    /// The vertical offset from the page origin in half-inch increments.
142    ///
143    /// See [\[MS-ONE\] 2.3.19].
144    ///
145    /// [\[MS-ONE\] 2.3.19]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/5c4992ba-1db5-43e9-83dd-7299c562104d
146    pub fn offset_vertical(&self) -> Option<f32> {
147        self.offset_vertical
148    }
149}
150
151/// An entry in an outline list.
152#[allow(missing_docs)]
153#[derive(Clone, Debug)]
154pub enum OutlineItem {
155    Group(OutlineGroup),
156    Element(OutlineElement),
157}
158
159impl OutlineItem {
160    /// Return the outline element if the item is an element.
161    pub fn element(&self) -> Option<&OutlineElement> {
162        if let OutlineItem::Element(element) = self {
163            Some(element)
164        } else {
165            None
166        }
167    }
168}
169
170/// An outline group with a custom indentation level.
171///
172/// This is used to represent the case where the first [`OutlineElement`]
173/// has a greater indentation level than the following outline elements.
174///
175/// See [\[MS-ONE\] 2.2.22].
176///
177/// [\[MS-ONE\] 2.2.22]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/7dcc1618-46ee-4912-b918-ab4df1b52315
178#[derive(Clone, Debug)]
179pub struct OutlineGroup {
180    pub(crate) child_level: u8,
181    pub(crate) outlines: Vec<OutlineItem>,
182}
183
184impl OutlineGroup {
185    /// The nesting level of this outline group's contents.
186    ///
187    /// See [\[MS-ONE\] 2.3.8].
188    ///
189    /// [\[MS-ONE\] 2.3.8]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/b631036a-9152-4385-8165-60fc324e5efd
190    pub fn child_level(&self) -> u8 {
191        self.child_level
192    }
193
194    /// The contents of this outline group.
195    pub fn outlines(&self) -> &[OutlineItem] {
196        &self.outlines
197    }
198}
199
200/// A container for a outline's content element.
201///
202/// See [\[MS-ONE\] 1.3.2.2] and [\[MS-ONE\] 2.2.21].
203///
204/// [\[MS-ONE\] 1.3.2.2]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/97bfd6bb-6ee4-43fd-aa1c-55646c0f6387
205/// [\[MS-ONE\] 2.2.21]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/d47760a6-6f1f-4fd5-b2ad-a51fe5a72c21
206#[derive(Clone, Debug)]
207pub struct OutlineElement {
208    pub(crate) contents: Vec<Content>,
209
210    pub(crate) list_contents: Vec<List>,
211    pub(crate) list_spacing: Option<f32>,
212
213    pub(crate) child_level: u8,
214    pub(crate) children: Vec<OutlineItem>,
215}
216
217impl OutlineElement {
218    /// The outline element's contents.
219    pub fn contents(&self) -> &[Content] {
220        &self.contents
221    }
222
223    /// The list specification.
224    ///
225    /// From MS-ONE it's not really clear whether an outline element can have multiple
226    /// list specifications so we're able to return multiple specifications just in case.
227    ///
228    /// See [\[MS-ONE\] 2.2.57].
229    ///
230    /// [\[MS-ONE\] 2.2.57]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/4c32f819-5885-4a53-bd2d-d020484c92ed
231    pub fn list_contents(&self) -> &[List] {
232        &self.list_contents
233    }
234
235    /// The horizontal distance between a list index number or bullet and the outline content.
236    ///
237    /// See [\[MS-ONE\] 2.3.45].
238    ///
239    /// [\[MS-ONE\] 2.3.45]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/3139a52f-fc22-48a3-9765-cebc6774d109
240    pub fn list_spacing(&self) -> Option<f32> {
241        self.list_spacing
242    }
243
244    /// The nesting level of this outline element's contents.
245    ///
246    /// See [\[MS-ONE\] 2.3.8].
247    ///
248    /// [\[MS-ONE\] 2.3.8]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/b631036a-9152-4385-8165-60fc324e5efd
249    pub fn child_level(&self) -> u8 {
250        self.child_level
251    }
252
253    /// Outline contents that are nested below this outline's contents.
254    pub fn children(&self) -> &[OutlineItem] {
255        &self.children
256    }
257}
258
259pub(crate) fn parse_outline(outline_id: ExGuid, space: &ObjectSpace) -> Result<Outline> {
260    let outline_object = space
261        .get_object(outline_id)
262        .ok_or_else(|| ErrorKind::MalformedOneNoteData("outline node is missing".into()))?;
263    let data = outline_node::parse(outline_object)?;
264
265    let items = data
266        .children
267        .into_iter()
268        .map(|item_id| parse_outline_item(item_id, space))
269        .collect::<Result<_>>()?;
270
271    let outline = Outline {
272        items,
273        child_level: data.child_level,
274        list_spacing: data.list_spacing,
275        indents: data.outline_indent_distance.into_value(),
276        alignment_in_parent: data.layout_alignment_in_parent,
277        alignment_self: data.layout_alignment_self,
278        layout_max_height: data.layout_max_height,
279        layout_max_width: data.layout_max_width,
280        layout_reserved_width: data.layout_reserved_width,
281        layout_minimum_outline_width: data.layout_minimum_outline_width,
282        is_layout_size_set_by_user: data.is_layout_size_set_by_user,
283        offset_horizontal: data.offset_from_parent_horiz,
284        offset_vertical: data.offset_from_parent_vert,
285    };
286
287    Ok(outline)
288}
289
290fn parse_outline_item(item_id: ExGuid, space: &ObjectSpace) -> Result<OutlineItem> {
291    let content_type = space
292        .get_object(item_id)
293        .ok_or_else(|| ErrorKind::MalformedOneNoteData("outline item is missing".into()))?
294        .id();
295    let id = PropertySetId::from_jcid(content_type).ok_or_else(|| {
296        ErrorKind::MalformedOneNoteData(
297            format!("invalid property set id: 0x{:X}", content_type.0).into(),
298        )
299    })?;
300
301    let item = match id {
302        PropertySetId::OutlineGroup => OutlineItem::Group(parse_outline_group(item_id, space)?),
303        PropertySetId::OutlineElementNode => {
304            OutlineItem::Element(parse_outline_element(item_id, space)?)
305        }
306        _ => {
307            return Err(ErrorKind::MalformedOneNoteData(
308                format!("invalid outline item type: {:?}", id).into(),
309            )
310            .into())
311        }
312    };
313
314    Ok(item)
315}
316
317fn parse_outline_group(group_id: ExGuid, space: &ObjectSpace) -> Result<OutlineGroup> {
318    let group_object = space
319        .get_object(group_id)
320        .ok_or_else(|| ErrorKind::MalformedOneNoteData("outline group is missing".into()))?;
321    let data = outline_group::parse(group_object)?;
322
323    let outlines = data
324        .children
325        .into_iter()
326        .map(|item_id| parse_outline_item(item_id, space))
327        .collect::<Result<_>>()?;
328
329    let group = OutlineGroup {
330        child_level: data.child_level,
331        outlines,
332    };
333
334    Ok(group)
335}
336
337pub(crate) fn parse_outline_element(
338    element_id: ExGuid,
339    space: &ObjectSpace,
340) -> Result<OutlineElement> {
341    let element_object = space
342        .get_object(element_id)
343        .ok_or_else(|| ErrorKind::MalformedOneNoteData("outline element is missing".into()))?;
344    let data = outline_element_node::parse(element_object)?;
345
346    let children = data
347        .children
348        .into_iter()
349        .map(|item_id| parse_outline_item(item_id, space))
350        .collect::<Result<_>>()?;
351
352    let contents = data
353        .contents
354        .into_iter()
355        .map(|content_id| parse_content(content_id, space))
356        .collect::<Result<_>>()?;
357
358    let list_contents = data
359        .list_contents
360        .into_iter()
361        .map(|list_id| parse_list(list_id, space))
362        .collect::<Result<_>>()?;
363
364    let element = OutlineElement {
365        child_level: data.child_level,
366        list_spacing: data.list_spacing,
367        children,
368        contents,
369        list_contents,
370    };
371
372    Ok(element)
373}