Skip to main content

odox_core/doc/
slides.rs

1//! The order of a deck's slides, and whether it has one more or one less.
2//!
3//! A slide is a `draw:page` with the notes, the shapes and the animations it
4//! carries, and it is named: links, custom shows and the start page refer to a
5//! slide by its `draw:name`. Moving one changes nothing that refers to it.
6//! Deleting one takes its name out of the places that list it. Duplicating one
7//! gives the copy a name of its own and a fresh id for everything in it that
8//! has one, because an id names one element of a document, and the references
9//! between those elements inside the copy follow.
10//
11// Author: David M. Anderson
12// Built with AI assistance (Claude, Anthropic)
13
14use std::collections::{HashMap, HashSet};
15
16use super::Presentation;
17use crate::edit::Refused;
18use crate::xml::{Attribute, Element, Node, Ns};
19
20/// The attributes that name another element by its id, which a copy of a
21/// slide has to point at the copy's own.
22const REFERENCES: [&str; 3] = ["start-shape", "end-shape", "targetElement"];
23
24fn is_page(node: &Node) -> bool {
25    matches!(node, Node::Element(e) if e.is(&Ns::Draw, "page"))
26}
27
28fn is_id(attribute: &Attribute) -> bool {
29    attribute.name.is(&Ns::Draw, "id")
30        || attribute.name.local.as_ref() == "id" && attribute.name.prefix.as_deref() == Some("xml")
31}
32
33impl Presentation {
34    fn presentation_mut(&mut self) -> Option<&mut Element> {
35        self.document
36            .content
37            .child_mut(&Ns::Office, "body")?
38            .child_mut(&Ns::Office, "presentation")
39    }
40
41    /// Move a slide one place earlier or later among the slides, and answer
42    /// where it is now. The slide is named by its position among the body's
43    /// children, as [`Presentation::slides`] gives it.
44    ///
45    /// # Errors
46    ///
47    /// There is no such slide, or no slide on that side of it.
48    pub fn move_slide(&mut self, position: usize, later: bool) -> Result<usize, Refused> {
49        let presentation = self.presentation_mut().ok_or(Refused::NotFound)?;
50        if !presentation.children.get(position).is_some_and(is_page) {
51            return Err(Refused::NotFound);
52        }
53        let other = if later {
54            (position + 1..presentation.children.len())
55                .find(|&i| is_page(&presentation.children[i]))
56        } else {
57            (0..position)
58                .rev()
59                .find(|&i| is_page(&presentation.children[i]))
60        }
61        .ok_or(Refused::NotFound)?;
62        presentation.children.swap(position, other);
63        Ok(other)
64    }
65
66    /// Take a slide out of the deck, with its notes, and its name out of the
67    /// custom shows and the start page that list it.
68    ///
69    /// # Errors
70    ///
71    /// There is no such slide, or it is the only one.
72    pub fn delete_slide(&mut self, position: usize) -> Result<(), Refused> {
73        let presentation = self.presentation_mut().ok_or(Refused::NotFound)?;
74        let Some(Node::Element(page)) = presentation.children.get(position) else {
75            return Err(Refused::NotFound);
76        };
77        if !page.is(&Ns::Draw, "page") {
78            return Err(Refused::NotFound);
79        }
80        if presentation.children.iter().filter(|n| is_page(n)).count() == 1 {
81            return Err(Refused::LastOne);
82        }
83        let name = page.attr(&Ns::Draw, "name").map(ToOwned::to_owned);
84        presentation.children.remove(position);
85        if let Some(name) = name {
86            forget_name(presentation, &name);
87        }
88        Ok(())
89    }
90
91    /// Put a copy of a slide after it, and answer where the copy is. The copy
92    /// has a name no slide has, and every `xml:id` and `draw:id` in it, the
93    /// notes included, is new, with the references to them inside the copy
94    /// following.
95    ///
96    /// # Errors
97    ///
98    /// There is no such slide.
99    pub fn duplicate_slide(&mut self, position: usize) -> Result<usize, Refused> {
100        let mut taken = HashSet::new();
101        collect_ids(&self.document.content, &mut taken);
102        if let Some(styles) = &self.document.styles_part {
103            collect_ids(styles, &mut taken);
104        }
105        let presentation = self.presentation_mut().ok_or(Refused::NotFound)?;
106        let Some(Node::Element(original)) = presentation.children.get(position) else {
107            return Err(Refused::NotFound);
108        };
109        if !original.is(&Ns::Draw, "page") {
110            return Err(Refused::NotFound);
111        }
112        let names: HashSet<String> = presentation
113            .elements()
114            .filter(|e| e.is(&Ns::Draw, "page"))
115            .filter_map(|e| e.attr(&Ns::Draw, "name").map(ToOwned::to_owned))
116            .collect();
117        let mut copy = original.clone();
118        if let Some(attribute) = copy.attrs.iter_mut().find(|a| a.name.is(&Ns::Draw, "name")) {
119            attribute.value = (2..=names.len() + 2)
120                .map(|n| format!("{} ({n})", attribute.value))
121                .find(|candidate| !names.contains(candidate))
122                .unwrap_or_default();
123        }
124        let mut renamed = HashMap::new();
125        let mut counter = 0;
126        renumber(&mut copy, &mut renamed, &mut taken, &mut counter);
127        follow_references(&mut copy, &renamed);
128        presentation
129            .children
130            .insert(position + 1, Node::Element(copy));
131        Ok(position + 1)
132    }
133}
134
135/// Every id an element and what is under it has.
136fn collect_ids(element: &Element, into: &mut HashSet<String>) {
137    for attribute in element.attrs.iter().filter(|a| is_id(a)) {
138        into.insert(attribute.value.clone());
139    }
140    for child in element.elements() {
141        collect_ids(child, into);
142    }
143}
144
145/// Give every id in an element and under it a new one, remembering which
146/// replaced which.
147fn renumber(
148    element: &mut Element,
149    renamed: &mut HashMap<String, String>,
150    taken: &mut HashSet<String>,
151    counter: &mut usize,
152) {
153    for attribute in element.attrs.iter_mut().filter(|a| is_id(a)) {
154        let fresh = renamed.entry(attribute.value.clone()).or_insert_with(|| {
155            loop {
156                *counter += 1;
157                let candidate = format!("odox{counter}");
158                if taken.insert(candidate.clone()) {
159                    break candidate;
160                }
161            }
162        });
163        attribute.value.clone_from(fresh);
164    }
165    for child in &mut element.children {
166        if let Node::Element(child) = child {
167            renumber(child, renamed, taken, counter);
168        }
169    }
170}
171
172/// Point the references inside an element at the ids that replaced the ones
173/// they named.
174fn follow_references(element: &mut Element, renamed: &HashMap<String, String>) {
175    for attribute in &mut element.attrs {
176        if REFERENCES.contains(&attribute.name.local.as_ref())
177            && let Some(fresh) = renamed.get(&attribute.value)
178        {
179            attribute.value.clone_from(fresh);
180        }
181    }
182    for child in &mut element.children {
183        if let Node::Element(child) = child {
184            follow_references(child, renamed);
185        }
186    }
187}
188
189/// Take a slide's name out of the places that list slides by name: the start
190/// page, and the pages of each custom show, which goes where it is left with
191/// none.
192fn forget_name(presentation: &mut Element, name: &str) {
193    for child in &mut presentation.children {
194        let Node::Element(element) = child else {
195            continue;
196        };
197        if element.is(&Ns::Presentation, "settings")
198            && element.attr(&Ns::Presentation, "start-page") == Some(name)
199        {
200            element.remove_attr(&Ns::Presentation, "start-page");
201        }
202        if element.is(&Ns::Presentation, "show")
203            && let Some(attribute) = element
204                .attrs
205                .iter_mut()
206                .find(|a| a.name.is(&Ns::Presentation, "pages"))
207        {
208            attribute.value = attribute
209                .value
210                .split(',')
211                .filter(|page| *page != name)
212                .collect::<Vec<_>>()
213                .join(",");
214        }
215    }
216    presentation.children.retain(|node| {
217        !matches!(node, Node::Element(e) if e.is(&Ns::Presentation, "show") && e.attr(&Ns::Presentation, "pages") == Some(""))
218    });
219}