1use crate::model::ShapeElements;
6use crate::relationships::Relationship;
7use crate::xml::{XmlElement, alternate_content_branch};
8
9#[derive(Clone, Debug, Default, PartialEq, Eq)]
12pub struct SlideSource {
13 pub shapes: Vec<SourceShape>,
14 pub omitted: Vec<OmittedElement>,
15}
16
17#[derive(Clone, Debug, Default, PartialEq, Eq)]
19pub struct SourceShape {
20 pub path: Vec<u32>,
22 pub element: String,
24 pub id: u32,
26 pub title: Option<String>,
28 pub description: Option<String>,
30 pub media: Option<MediaKind>,
32 pub graphic_uri: Option<String>,
34 pub relationship_ids: Vec<String>,
36 pub omitted_inlines: Vec<OmittedInline>,
38 pub children: Vec<SourceShape>,
39 pub omitted: Vec<OmittedElement>,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum MediaKind {
45 Video,
46 Audio,
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct OmittedElement {
52 pub path: Vec<u32>,
53 pub element: String,
56 pub position: usize,
58 pub name: Option<String>,
60 pub description: Option<String>,
61 pub hidden: bool,
63 pub relationship_ids: Vec<String>,
65}
66
67#[derive(Clone, Debug, PartialEq, Eq)]
69pub struct OmittedInline {
70 pub path: Vec<u32>,
71 pub element: String,
72 pub cell: Option<(usize, usize)>,
74 pub paragraph: usize,
76 pub position: usize,
78}
79
80#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct NotesSource {
83 pub part_path: String,
84 pub other_text_shapes: usize,
87}
88
89pub(crate) fn slide_source(
91 root: &XmlElement,
92 relationships: &[Relationship],
93 elements: ShapeElements,
94) -> SlideSource {
95 let mut source = SlideSource::default();
96 let Some((common_index, common)) = child_named(root, "cSld") else {
97 return source;
98 };
99 let Some((tree_index, tree)) = child_named(common, "spTree") else {
100 return source;
101 };
102 let walk = Walk {
103 elements,
104 relationships,
105 };
106 walk.children(
107 tree,
108 &[common_index, tree_index],
109 &mut source.shapes,
110 &mut source.omitted,
111 );
112 source
113}
114
115pub(crate) fn notes_source(root: &XmlElement, part_path: &str) -> NotesSource {
117 let other_text_shapes = root
118 .child("cSld")
119 .and_then(|common| common.child("spTree"))
120 .map_or(0, |tree| {
121 tree.descendants_named("sp")
122 .into_iter()
123 .filter(|shape| !is_notes_decoration(shape) && has_text(shape))
124 .count()
125 });
126 NotesSource {
127 part_path: part_path.to_owned(),
128 other_text_shapes,
129 }
130}
131
132fn is_notes_decoration(shape: &XmlElement) -> bool {
133 let placeholder = shape
134 .child("nvSpPr")
135 .and_then(|properties| properties.child("nvPr"))
136 .and_then(|properties| properties.child("ph"));
137 placeholder.is_some_and(|placeholder| {
138 matches!(
139 placeholder.attribute("type"),
140 Some("body" | "sldImg" | "hdr" | "ftr" | "dt" | "sldNum")
141 )
142 })
143}
144
145fn has_text(shape: &XmlElement) -> bool {
146 shape.child("txBody").is_some_and(|body| {
147 body.descendants_named("t")
148 .iter()
149 .any(|text| !text.text_content().is_empty())
150 })
151}
152
153fn child_named<'a>(parent: &'a XmlElement, name: &str) -> Option<(u32, &'a XmlElement)> {
154 parent
155 .child_elements()
156 .enumerate()
157 .find(|(_, child)| child.local_name() == name)
158 .map(|(index, child)| (index as u32, child))
159}
160
161fn extend(path: &[u32], index: usize) -> Vec<u32> {
162 let mut path = path.to_vec();
163 path.push(index as u32);
164 path
165}
166
167struct Walk<'a> {
169 elements: ShapeElements,
170 relationships: &'a [Relationship],
171}
172
173impl Walk<'_> {
174 fn children(
177 &self,
178 parent: &XmlElement,
179 path: &[u32],
180 shapes: &mut Vec<SourceShape>,
181 omitted: &mut Vec<OmittedElement>,
182 ) {
183 for (index, child) in parent.child_elements().enumerate() {
184 let child_path = extend(path, index);
185 let local = child.local_name();
186 if local == "AlternateContent" {
187 match branch_with_index(child) {
188 Some((branch_index, branch)) => {
189 self.children(branch, &extend(&child_path, branch_index), shapes, omitted)
190 }
191 None => omitted.push(self.omitted(child, child_path, shapes.len())),
192 }
193 continue;
194 }
195 if self.elements.contains(local) {
196 shapes.push(self.shape(child, child_path));
197 } else if !matches!(local, "nvGrpSpPr" | "grpSpPr" | "extLst") {
198 omitted.push(self.omitted(child, child_path, shapes.len()));
199 }
200 }
201 }
202
203 fn omitted(&self, element: &XmlElement, path: Vec<u32>, position: usize) -> OmittedElement {
204 let properties = element.descendants_named("cNvPr").into_iter().next();
205 OmittedElement {
206 path,
207 element: content_name(element).to_owned(),
208 position,
209 name: properties
210 .and_then(|properties| properties.attribute("name"))
211 .filter(|name| !name.is_empty())
212 .map(str::to_owned),
213 description: properties
214 .and_then(|properties| properties.attribute("descr"))
215 .map(str::to_owned),
216 hidden: properties
217 .and_then(|properties| properties.attribute("hidden"))
218 .is_some_and(|value| matches!(value, "1" | "true" | "on")),
219 relationship_ids: self.relationship_ids(element),
220 }
221 }
222
223 fn shape(&self, element: &XmlElement, path: Vec<u32>) -> SourceShape {
224 let non_visual = element.child_elements().find(|child| {
225 matches!(
226 child.local_name(),
227 "nvSpPr" | "nvCxnSpPr" | "nvPicPr" | "nvGraphicFramePr" | "nvGrpSpPr"
228 )
229 });
230 let properties = non_visual.and_then(|value| value.child("cNvPr"));
231 let mut shape = SourceShape {
232 element: element.name.clone(),
233 id: properties
234 .and_then(|value| value.attribute("id"))
235 .and_then(|value| value.parse().ok())
236 .unwrap_or_default(),
237 title: properties
238 .and_then(|value| value.attribute("title"))
239 .map(str::to_owned),
240 description: properties
241 .and_then(|value| value.attribute("descr"))
242 .map(str::to_owned),
243 media: non_visual
244 .and_then(|value| value.child("nvPr"))
245 .and_then(media_kind),
246 ..SourceShape::default()
247 };
248 match element.local_name() {
249 "sp" | "cxnSp" => {
250 if let Some((index, body)) = child_named(element, "txBody") {
251 text_body_inlines(
252 body,
253 &extend(&path, index as usize),
254 None,
255 &mut shape.omitted_inlines,
256 );
257 }
258 }
259 "graphicFrame" => {
260 let graphic = child_named(element, "graphic");
261 let data = graphic.and_then(|(_, graphic)| child_named(graphic, "graphicData"));
262 shape.graphic_uri = data
263 .and_then(|(_, data)| data.attribute("uri"))
264 .map(str::to_owned);
265 if let (Some((graphic_index, _)), Some((data_index, data))) = (graphic, data)
266 && let Some((table_index, table)) = child_named(data, "tbl")
267 {
268 let table_path =
269 [path.as_slice(), &[graphic_index, data_index, table_index]].concat();
270 table_inlines(table, &table_path, &mut shape.omitted_inlines);
271 }
272 }
273 "grpSp" => self.children(element, &path, &mut shape.children, &mut shape.omitted),
274 _ => {}
275 }
276 if element.local_name() != "grpSp" {
277 shape.relationship_ids = self.relationship_ids(element);
278 }
279 shape.path = path;
280 shape
281 }
282
283 fn relationship_ids(&self, element: &XmlElement) -> Vec<String> {
286 let mut ids = Vec::new();
287 let mut pending = vec![element];
288 while let Some(current) = pending.pop() {
289 for (name, value) in ¤t.attributes {
290 if name.contains(':')
291 && !name.starts_with("xmlns")
292 && !ids.contains(value)
293 && self
294 .relationships
295 .iter()
296 .any(|relationship| &relationship.id == value)
297 {
298 ids.push(value.clone());
299 }
300 }
301 let children: Vec<&XmlElement> = current.child_elements().collect();
302 pending.extend(children.into_iter().rev());
303 }
304 ids
305 }
306}
307
308fn branch_with_index(alternate: &XmlElement) -> Option<(usize, &XmlElement)> {
309 let branch = alternate_content_branch(alternate)?;
310 alternate
311 .child_elements()
312 .enumerate()
313 .find(|(_, child)| std::ptr::eq(*child, branch))
314}
315
316fn content_name(element: &XmlElement) -> &str {
318 let content = if element.local_name() == "AlternateContent" {
319 element
320 .child_elements()
321 .flat_map(XmlElement::child_elements)
322 .next()
323 .unwrap_or(element)
324 } else {
325 element
326 };
327 &content.name
328}
329
330fn media_kind(properties: &XmlElement) -> Option<MediaKind> {
331 let mut media = None;
332 for child in properties.child_elements() {
333 match child.local_name() {
334 "videoFile" | "quickTimeFile" => return Some(MediaKind::Video),
335 "audioFile" | "audioCd" | "wavAudioFile" => media = Some(MediaKind::Audio),
336 "extLst" if media.is_none() && !child.descendants_named("media").is_empty() => {
337 media = Some(MediaKind::Video);
338 }
339 _ => {}
340 }
341 }
342 media
343}
344
345fn table_inlines(table: &XmlElement, path: &[u32], output: &mut Vec<OmittedInline>) {
346 let rows = table
347 .child_elements()
348 .enumerate()
349 .filter(|(_, child)| child.local_name() == "tr");
350 for (row_index, (row_ordinal, row)) in rows.enumerate() {
351 let cells = row
352 .child_elements()
353 .enumerate()
354 .filter(|(_, child)| child.local_name() == "tc");
355 for (cell_index, (cell_ordinal, cell)) in cells.enumerate() {
356 if let Some((body_index, body)) = child_named(cell, "txBody") {
357 let body_path =
358 [path, &[row_ordinal as u32, cell_ordinal as u32, body_index]].concat();
359 text_body_inlines(body, &body_path, Some((row_index, cell_index)), output);
360 }
361 }
362 }
363}
364
365fn text_body_inlines(
366 body: &XmlElement,
367 path: &[u32],
368 cell: Option<(usize, usize)>,
369 output: &mut Vec<OmittedInline>,
370) {
371 let paragraphs = body
372 .child_elements()
373 .enumerate()
374 .filter(|(_, child)| child.local_name() == "p");
375 for (paragraph, (paragraph_ordinal, element)) in paragraphs.enumerate() {
376 let mut runs = 0;
377 for (index, child) in element.child_elements().enumerate() {
378 match child.local_name() {
379 "r" | "fld" | "br" => runs += 1,
380 "pPr" | "endParaRPr" => {}
381 _ => {
382 output.push(OmittedInline {
383 path: [path, &[paragraph_ordinal as u32, index as u32]].concat(),
384 element: content_name(child).to_owned(),
385 cell,
386 paragraph,
387 position: runs,
388 });
389 }
390 }
391 }
392 }
393}