1use std::collections::{HashMap, HashSet};
15
16use super::Presentation;
17use crate::edit::Refused;
18use crate::xml::{Attribute, Element, Node, Ns};
19
20const 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 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 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 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
135fn 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
145fn 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
172fn 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
189fn 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}